@@ -72,22 +72,75 @@ npx convex env set CLOUDINARY_API_SECRET <your_api_secret>
7272
7373## Quick Start
7474
75- ### Using CloudinaryClient (Recommended)
75+ ### Option 1: Using makeCloudinaryAPI (Recommended for React Apps)
76+
77+ This approach creates public API functions that can be called from React clients.
78+
79+ ** Backend (convex/cloudinary.ts):**
80+
81+ ``` ts
82+ import { makeCloudinaryAPI } from " @imaxis/cloudinary-convex" ;
83+ import { components } from " ./_generated/api" ;
84+
85+ // Export all API functions - uses environment variables automatically
86+ export const {
87+ upload,
88+ transform,
89+ deleteAsset,
90+ listAssets,
91+ getAsset,
92+ updateAsset,
93+ generateUploadCredentials,
94+ finalizeUpload,
95+ } = makeCloudinaryAPI (components .cloudinary );
96+ ```
97+
98+ ** React Client:**
99+
100+ ``` tsx
101+ import { api } from " ../convex/_generated/api" ;
102+ import { useQuery , useAction } from " convex/react" ;
103+ import { useCloudinaryUpload } from " @imaxis/cloudinary-convex/react" ;
104+
105+ function ImageGallery() {
106+ // List images
107+ const images = useQuery (api .cloudinary .listAssets , { limit: 20 });
108+
109+ // Upload with progress tracking
110+ const { upload, isUploading, progress } = useCloudinaryUpload (
111+ api .cloudinary .upload
112+ );
113+
114+ const handleUpload = async (file : File ) => {
115+ const base64 = await fileToBase64 (file );
116+ const result = await upload (base64 , { folder: " uploads" });
117+ console .log (" Uploaded:" , result .secureUrl );
118+ };
119+
120+ return (
121+ <div >
122+ { isUploading && <p >Uploading... { progress } %</p >}
123+ { images ?.map ((img ) => (
124+ <img key = { img .publicId } src = { img .secureUrl } alt = " " />
125+ ))}
126+ </div >
127+ );
128+ }
129+ ```
130+
131+ ### Option 2: Using CloudinaryClient (For Server-Side Logic)
132+
133+ This approach is ideal when you need more control or want to build custom logic.
76134
77135``` ts
78136// convex/images.ts
79137import { action , query } from " ./_generated/server" ;
80138import { components } from " ./_generated/api" ;
81- import { CloudinaryClient } from " @imaxis/cloudinary-convex" ;
82- import { vAssetResponse } from " @imaxis/cloudinary-convex/lib" ;
139+ import { CloudinaryClient , vAssetResponse } from " @imaxis/cloudinary-convex" ;
83140import { v } from " convex/values" ;
84141
85- // Initialize client
86- const cloudinary = new CloudinaryClient (components .cloudinary , {
87- cloudName: process .env .CLOUDINARY_CLOUD_NAME ,
88- apiKey: process .env .CLOUDINARY_API_KEY ,
89- apiSecret: process .env .CLOUDINARY_API_SECRET ,
90- });
142+ // Initialize client - uses environment variables automatically
143+ const cloudinary = new CloudinaryClient (components .cloudinary );
91144
92145// Upload Action (Base64)
93146export const uploadImage = action ({
@@ -110,43 +163,174 @@ export const getImages = query({
110163});
111164```
112165
113- ### Handling Large Files (Direct Upload)
166+ ## React Hooks
167+
168+ The component provides React hooks for common operations. ** Important:** These hooks require function references from your app's API (created via ` makeCloudinaryAPI ` ), not the component directly.
169+
170+ ### Available Hooks
171+
172+ ``` tsx
173+ import {
174+ useCloudinaryUpload ,
175+ useCloudinaryImage ,
176+ useCloudinaryAssets ,
177+ useCloudinaryAsset ,
178+ useCloudinaryOperations ,
179+ CloudinaryImage ,
180+ CloudinaryUpload ,
181+ } from " @imaxis/cloudinary-convex/react" ;
182+ ```
183+
184+ ### Upload Hook
185+
186+ ``` tsx
187+ import { api } from " ../convex/_generated/api" ;
188+
189+ function UploadButton() {
190+ const { upload, isUploading, progress, error, reset } = useCloudinaryUpload (
191+ api .cloudinary .upload
192+ );
193+
194+ const handleFileChange = async (e : React .ChangeEvent <HTMLInputElement >) => {
195+ const file = e .target .files ?.[0 ];
196+ if (file ) {
197+ try {
198+ const result = await upload (file , { folder: " uploads" });
199+ console .log (" Upload complete:" , result );
200+ } catch (err ) {
201+ console .error (" Upload failed:" , err );
202+ }
203+ }
204+ };
205+
206+ return (
207+ <div >
208+ <input type = " file" onChange = { handleFileChange } disabled = { isUploading } />
209+ { isUploading && <p >Uploading... { progress } %</p >}
210+ { error && <p style = { { color: " red" }} >{ error } </p >}
211+ </div >
212+ );
213+ }
214+ ```
215+
216+ ### Image Transformation Hook
217+
218+ ``` tsx
219+ function TransformedImage({ publicId }: { publicId: string }) {
220+ const { transformedUrl, isLoading } = useCloudinaryImage (
221+ api .cloudinary .transform ,
222+ publicId ,
223+ { width: 300 , height: 300 , crop: " fill" }
224+ );
225+
226+ if (isLoading ) return <div >Loading...</div >;
227+ return <img src = { transformedUrl } alt = " " />;
228+ }
229+ ```
230+
231+ ### Asset Operations Hook
232+
233+ ``` tsx
234+ function AssetManager({ publicId }: { publicId: string }) {
235+ const { deleteAsset, updateAsset } = useCloudinaryOperations (
236+ api .cloudinary .deleteAsset ,
237+ api .cloudinary .updateAsset
238+ );
239+
240+ const handleDelete = async () => {
241+ await deleteAsset (publicId );
242+ };
243+
244+ const handleUpdateTags = async () => {
245+ await updateAsset (publicId , { tags: [" featured" , " hero" ] });
246+ };
247+
248+ return (
249+ <div >
250+ <button onClick = { handleUpdateTags } >Add Tags</button >
251+ <button onClick = { handleDelete } >Delete</button >
252+ </div >
253+ );
254+ }
255+ ```
256+
257+ ### Pre-built Components
258+
259+ ``` tsx
260+ // Drag-and-drop upload component
261+ <CloudinaryUpload
262+ uploadFn = { api .cloudinary .upload }
263+ onUploadComplete = { (result ) => console .log (" Uploaded:" , result )}
264+ onUploadError = { (error ) => console .error (" Error:" , error )}
265+ options = { { folder: " uploads" }}
266+ />
267+
268+ // Image with transformations
269+ <CloudinaryImage
270+ transformFn = { api .cloudinary .transform }
271+ publicId = " my-image-id"
272+ transformation = { { width: 400 , height: 300 , crop: " fill" }}
273+ alt = " My image"
274+ loader = { <Spinner />}
275+ fallback = { <Placeholder />}
276+ />
277+ ```
278+
279+ ## Handling Large Files (Direct Upload)
114280
115281For files >10MB, use the direct upload flow to bypass Convex limits.
116282
117283** Backend:**
118284
119285``` ts
120- export const getUploadCredentials = action ({
121- args: { filename: v .optional (v .string ()) },
122- handler : async (ctx , args ) => {
123- return await cloudinary .generateUploadCredentials (ctx , {
124- folder: " large-uploads" ,
286+ // Already included in makeCloudinaryAPI:
287+ // - generateUploadCredentials
288+ // - finalizeUpload
289+ ```
290+
291+ ** React:**
292+
293+ ``` tsx
294+ import { api } from " ../convex/_generated/api" ;
295+ import { useAction } from " convex/react" ;
296+
297+ function LargeFileUpload() {
298+ const getCredentials = useAction (api .cloudinary .generateUploadCredentials );
299+ const finalizeUpload = useMutation (api .cloudinary .finalizeUpload );
300+
301+ const handleLargeUpload = async (file : File ) => {
302+ // Step 1: Get signed credentials
303+ const credentials = await getCredentials ({ folder: " large-uploads" });
304+
305+ // Step 2: Upload directly to Cloudinary
306+ const formData = new FormData ();
307+ formData .append (" file" , file );
308+ Object .entries (credentials .uploadParams ).forEach (([key , value ]) => {
309+ if (value ) formData .append (key , value );
125310 });
126- },
127- });
128311
129- export const finalizeUpload = action ({
130- args: {
131- publicId: v .string (),
132- uploadResult: v .any (), // Use vCloudinaryUploadResponse for strict typing
133- },
134- handler : async (ctx , args ) => {
135- return await ctx .runMutation (components .cloudinary .lib .finalizeUpload , {
136- publicId: args .publicId ,
137- uploadResult: args .uploadResult ,
312+ const response = await fetch (credentials .uploadUrl , {
313+ method: " POST" ,
314+ body: formData ,
138315 });
139- },
140- });
141- ```
316+ const result = await response .json ();
142317
143- ** Client (React):**
318+ // Step 3: Store metadata in Convex
319+ await finalizeUpload ({
320+ publicId: result .public_id ,
321+ uploadResult: result ,
322+ });
144323
145- ``` tsx
146- const result = await cloudinary .uploadDirect (ctx , file , {
147- folder: " uploads" ,
148- onProgress : (progress ) => setUploadProgress (progress ),
149- });
324+ console .log (" Large file uploaded:" , result .secure_url );
325+ };
326+
327+ return (
328+ <input
329+ type = " file"
330+ onChange = { (e ) => handleLargeUpload (e .target .files ?.[0 ]! )}
331+ />
332+ );
333+ }
150334```
151335
152336## Database Schema
@@ -163,6 +347,20 @@ The component manages an `assets` table:
163347}
164348```
165349
350+ ## Exported Validators
351+
352+ For type-safe function definitions:
353+
354+ ``` ts
355+ import {
356+ vAssetResponse ,
357+ vUploadResult ,
358+ vTransformResult ,
359+ vDeleteResult ,
360+ vTransformation ,
361+ } from " @imaxis/cloudinary-convex" ;
362+ ```
363+
166364## Resources
167365
168366- ** [ Live Demo] ( https://cloudinary-convex-studio.vercel.app/ ) **
0 commit comments