Skip to content

Commit db8a031

Browse files
committed
Enhance README and React Hooks for Cloudinary Integration
- Updated README.md to include two options for using Cloudinary: `makeCloudinaryAPI` for React apps and `CloudinaryClient` for server-side logic. - Added detailed examples for using the new hooks: `useCloudinaryUpload`, `useCloudinaryImage`, `useCloudinaryAssets`, `useCloudinaryAsset`, and `useCloudinaryOperations`. - Improved type definitions and validation for API functions in the React hooks. - Refactored the `CloudinaryClient` to support optional configuration and improved documentation for better clarity.
1 parent 462e6b7 commit db8a031

3 files changed

Lines changed: 896 additions & 99 deletions

File tree

README.md

Lines changed: 233 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -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
79137
import { action, query } from "./_generated/server";
80138
import { 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";
83140
import { 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)
93146
export 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

115281
For 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

Comments
 (0)