Skip to content

Commit 382b7cd

Browse files
Done with image media uploads to cloudinary
1 parent 6a60031 commit 382b7cd

File tree

8 files changed

+219
-152
lines changed

8 files changed

+219
-152
lines changed

env_skeleton

+3-2
Original file line numberDiff line numberDiff line change
@@ -33,11 +33,12 @@ GOOGLE_ANALYTICS_PRIVATE_KEY=
3333

3434

3535

36+
CLOUDINARY_CLOUD_NAME=
3637
NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME=
37-
NEXT_PUBLIC_CLOUDINARY_API_KEY=
38+
CLOUDINARY_API_KEY=
3839
CLOUDINARY_API_SECRET=
3940
CLOUDINARY_URL=
40-
41+
NEXT_PUBLIC_CLOUDINARY_UPLOAD_PRESET=
4142

4243

4344

next.config.js

+1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ const nextConfig = {
88
"avatars.githubusercontent.com",
99
"scontent.facc6-1.fna.fbcdn.net",
1010
"media.licdn.com",
11+
"res.cloudinary.com",
1112
],
1213
},
1314
// output: "export",

src/app/admin/editor/page.jsx

+32-80
Original file line numberDiff line numberDiff line change
@@ -165,34 +165,47 @@ const Editor = () => {
165165
return updatedContent;
166166
};
167167

168+
169+
const dataURLtoFile = (dataUrl) => {
170+
const arr = dataUrl.split(",");
171+
const mime = arr[0].match(/:(.*?);/)[1];
172+
const bstr = atob(arr[1]);
173+
let n = bstr.length;
174+
const u8arr = new Uint8Array(n);
175+
while (n--) {
176+
u8arr[n] = bstr.charCodeAt(n);
177+
}
178+
return new File([u8arr], `upload-${Date.now()}`, { type: mime });
179+
};
180+
181+
168182

169183
const processMediaUploads = async (content) => {
170184
const base64Images = extractBase64Images(content);
171185
const base64Map = {};
172186

173-
// Upload each base64 image using your new endpoint
187+
// For each base64 image, convert to a File and upload it via handleImageUpload
174188
for (let base64 of base64Images) {
175189
try {
176-
const res = await fetch("/api/uploadMedia", {
177-
method: "POST",
178-
headers: { "Content-Type": "application/json" },
179-
body: JSON.stringify({ base64, folder: "blog_media" }),
180-
});
181-
const data = await res.json();
182-
if (!res.ok) throw new Error(data.error);
183-
const url = data.url; // URL returned from the endpoint
184-
base64Map[base64] = url; // Store the mapping for replacement
185-
186-
// Optionally update your local media array (if you need to track it)
187-
setMedia((prev) => [...prev, url]);
190+
// Convert the base64 string to a File object
191+
const file = dataURLtoFile(base64);
192+
// Upload the file using the refactored handleImageUpload function
193+
const { url, publicId } = await handleImageUpload(file, "blog_media");
194+
// Store the mapping from base64 to Cloudinary URL so you can replace it in the content
195+
base64Map[base64] = url;
196+
197+
// Optionally update your local media state array
198+
setMedia((prev) => [...prev, { url, publicId }]);
188199
} catch (error) {
189200
console.error("Upload failed:", error);
190201
}
191202
}
192203

204+
// Replace each base64 string in your content with its uploaded URL
193205
return replaceBase64WithUrls(content, base64Map);
194206
};
195207

208+
196209

197210

198211
const autoSaveDraft = async () => {
@@ -253,6 +266,8 @@ const Editor = () => {
253266
draft,
254267
keywords: Array.isArray(keywords) ? keywords : keywords.split(","),
255268
description,
269+
media, // ✅ Store media in local storage
270+
256271
};
257272

258273
setPreviewData(blogData);
@@ -321,66 +336,6 @@ const Editor = () => {
321336

322337

323338

324-
const saveBlog = async () => {
325-
const isUnique = await fetchSlugUniqueness(slug); // ✅ Ensure uniqueness before saving
326-
327-
if (!isUnique) {
328-
alert(`Title: ${title} is not unique. Choose another one.`);
329-
return;
330-
}
331-
332-
// ✅ Process sanitization & YouTube fix only once at save time
333-
let cleanContent = DOMPurify.sanitize(blogContent, {
334-
FORBID_TAGS: ["script"],
335-
ADD_TAGS: ["iframe", "style"],
336-
ADD_ATTR: ["allow", "allowfullscreen", "frameborder", "src", "width", "height", "title"],
337-
});
338-
339-
// ✅ Fix YouTube Shorts without clearing iframes
340-
const tempDiv = document.createElement("div");
341-
tempDiv.innerHTML = cleanContent;
342-
343-
tempDiv.querySelectorAll("iframe").forEach((iframe) => {
344-
let src = iframe.src;
345-
if (src.includes("youtube.com/shorts/")) {
346-
src = src.replace("youtube.com/shorts/", "youtube.com/embed/");
347-
}
348-
iframe.src = src; // ✅ Apply fixed URL
349-
});
350-
351-
const processedContent = tempDiv.innerHTML; // ✅ Final sanitized + fixed content
352-
353-
console.log("AFTER CONTENT: ", processedContent);
354-
355-
// ✅ Update state before saving so iframes don't disappear
356-
setBlogContent(processedContent);
357-
358-
const blogData = {
359-
image,
360-
title,
361-
slug,
362-
category,
363-
blogContent: processedContent, // ✅ Use sanitized content
364-
readingTime,
365-
date,
366-
draft,
367-
keywords: typeof keywords === "string"
368-
? keywords.split(",").map((keyword) => keyword.trim())
369-
: keywords,
370-
description,
371-
};
372-
373-
setPreviewData(blogData);
374-
setSaved(true);
375-
376-
setTimeout(() => {
377-
setSaved(false);
378-
}, 5000);
379-
localStorage.removeItem("previewData");
380-
};
381-
382-
383-
384339

385340
useEffect(() => {
386341
const savedData = previewData; // Automatically handled by the useLocalStorage hook
@@ -456,18 +411,15 @@ const Editor = () => {
456411
// const [localStorageAutoSaveDuration, setLocalStorageAutoSaveDuration] = useLocalStorage("autoSaveDuration", "");
457412

458413

459-
useEffect(() => {
460-
console.log("AutoSave status:", autoSave?'active':'deactivated');
461-
console.log("AutoSave Duration:", autoSaveDuration );
462-
}, [autoSave]);
414+
463415

464416
useEffect(() => {
465417
let interval;
466418
setLocalStorageAutoSave(autoSave);
467419
if (autoSave) {
468-
console.log("Starting Auto Save...");
420+
// console.log("Starting Auto Save...");
469421
interval = setInterval(async () => {
470-
console.log("Calling Auto save");
422+
// console.log("Calling Auto save");
471423
await autoSaveDraft();
472424
}, autoSaveDuration);
473425
}
@@ -519,7 +471,7 @@ const Editor = () => {
519471
/>
520472
</div>
521473
<div className={styles.imgContainer}>
522-
<ImageUploader image={image} setImage={setImage} />
474+
<ImageUploader image={image} setImage={setImage} media={media} setMedia={setMedia} />
523475
{image && (
524476
<div
525477
className={styles.close}

src/app/api/deleteMedia/route.js

+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { NextResponse } from "next/server";
2+
import cloudinary from "cloudinary";
3+
4+
5+
6+
cloudinary.v2.config({
7+
cloud_name: process.env.CLOUDINARY_CLOUD_NAME, // not NEXT_PUBLIC_*
8+
api_key: process.env.CLOUDINARY_API_KEY,
9+
api_secret: process.env.CLOUDINARY_API_SECRET,
10+
// secure: true,
11+
});
12+
export async function POST(request) {
13+
try {
14+
const { publicId } = await request.json();
15+
16+
if (!publicId) {
17+
return NextResponse.json({ message: "publicId is required" }, { status: 400 });
18+
}
19+
20+
// Delete the image using the publicId
21+
const result = await cloudinary.v2.uploader.destroy(publicId, { invalidate: true });
22+
23+
// Optionally, you might want to check if the deletion was successful
24+
if (result.result !== "ok") {
25+
return NextResponse.json({ message: "Image deletion failed", result }, { status: 500 });
26+
}
27+
28+
return NextResponse.json({ message: "Image deleted successfully", result }, { status: 200 });
29+
} catch (error) {
30+
console.error("Error deleting image:", error);
31+
return NextResponse.json(
32+
{ message: "Something went terribly wrong" },
33+
{ status: 500 }
34+
);
35+
}
36+
}

src/app/api/uploadMedia/route.js

+31-28
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,36 @@
11
import { v2 as cloudinary } from "cloudinary";
22
import { NextResponse } from "next/server";
33

4-
54
cloudinary.config({
6-
cloud_name: process.env.NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME,
7-
api_key: process.env.NEXT_PUBLIC_CLOUDINARY_API_KEY,
8-
api_secret: process.env.CLOUDINARY_API_SECRET,
9-
secure: true,
10-
});
11-
12-
export async function POST(req) {
13-
try {
14-
const { base64, folder = "blog_media" } = await req.json();
15-
if (!base64) {
16-
return NextResponse.json({ error: "No image provided" }, { status: 400 });
17-
}
18-
19-
// Generate a unique public_id based on timestamp
20-
const publicId = `upload-${Date.now()}`;
21-
22-
const uploadedImage = await cloudinary.uploader.upload(base64, {
23-
folder,
24-
public_id: publicId, // Ensure unique filename
25-
resource_type: "image",
26-
});
27-
28-
return NextResponse.json({ url: uploadedImage.secure_url }, { status: 200 });
29-
} catch (error) {
30-
console.error("Upload error:", error);
31-
return NextResponse.json({ error: error.message }, { status: 500 });
5+
cloud_name: process.env.NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME,
6+
api_key: process.env.NEXT_PUBLIC_CLOUDINARY_API_KEY,
7+
api_secret: process.env.CLOUDINARY_API_SECRET,
8+
secure: true,
9+
});
10+
11+
export async function POST(req) {
12+
try {
13+
console.log("Received API Call: /api/uploadMedia");
14+
15+
const { base64, folder = "blog_media" } = await req.json();
16+
if (!base64) {
17+
return NextResponse.json({ error: "No image provided" }, { status: 400 });
3218
}
33-
}
19+
20+
// Generate a unique public_id (optional)
21+
const publicId = `upload-${Date.now()}`;
22+
23+
// Upload the image using unsigned upload preset
24+
const uploadedImage = await cloudinary.uploader.upload(base64, {
25+
folder,
26+
public_id: publicId,
27+
resource_type: "image",
28+
upload_preset: process.env.NEXT_PUBLIC_CLOUDINARY_UPLOAD_PRESET, // Use your preset here
29+
});
30+
31+
return NextResponse.json({ url: uploadedImage.secure_url }, { status: 200 });
32+
} catch (error) {
33+
console.error("Upload error:", error);
34+
return NextResponse.json({ error: error.message }, { status: 500 });
35+
}
36+
}

src/components/imageuploader/ImageUploader.jsx

+75-13
Original file line numberDiff line numberDiff line change
@@ -8,28 +8,90 @@ import Image from "next/image";
88
import { handleImageUpload, handleImageDelete } from "@/utils/imageHandler";
99

1010

11-
const ImageUploader = ({ image, setImage }) => {
11+
const ImageUploader = ({ image, setImage, media, setMedia }) => {
1212
const [uploading, setUploading] = useState(false);
1313

1414

1515

16-
const onDrop = (acceptedFiles) => {
17-
if (acceptedFiles && acceptedFiles[0]) {
18-
19-
console.log("File Received: ", acceptedFiles, acceptedFiles[0]);
20-
21-
const selectedImage = acceptedFiles[0];
16+
const convertFileToBase64 = (file) => {
17+
return new Promise((resolve, reject) => {
2218
const reader = new FileReader();
23-
reader.onloadend = () => {
24-
const imageUrl = reader.result; // Base64 string
25-
setImage(imageUrl);
26-
};
27-
reader.readAsDataURL(selectedImage);
28-
}
19+
reader.onload = () => resolve(reader.result);
20+
reader.onerror = (error) => reject(error);
21+
22+
console.log("Reading File:", file);
23+
24+
if (!(file instanceof File)) {
25+
reject(new Error("Invalid file type"));
26+
return;
27+
}
28+
29+
reader.readAsDataURL(file);
30+
});
2931
};
32+
33+
34+
const onDrop = async (acceptedFiles) => {
35+
if (!acceptedFiles || acceptedFiles.length === 0) {
36+
console.error("No files received.");
37+
return;
38+
}
39+
40+
setUploading(true);
41+
const selectedFile = acceptedFiles[0];
42+
43+
console.log("Received File:", selectedFile);
44+
console.log("File Type:", typeof selectedFile);
45+
console.log("File Instance Check:", selectedFile instanceof File); // Should be true
46+
console.log("Blob Instance Check:", selectedFile instanceof Blob); // Should also be true
47+
48+
try {
3049

50+
if (image && media && media.length > 0) {
51+
// Find the media object for the currently displayed image
52+
const currentMedia = media.find((item) => item.url === image);
53+
if (currentMedia) {
54+
console.log("Deleting current image:", currentMedia);
55+
await handleImageDelete(currentMedia.publicId);
56+
// Remove it from the media array and clear the image state
57+
setMedia((prev) =>
58+
prev.filter((item) => item.publicId !== currentMedia.publicId)
59+
);
60+
setImage(null);
61+
}
62+
}
3163

3264

65+
// ✅ Confirm the file is valid
66+
if (!(selectedFile instanceof File)) {
67+
throw new Error("Selected file is not a valid File object.");
68+
}
69+
70+
const base64 = await convertFileToBase64(selectedFile);
71+
console.log("Base64 Conversion Success:", base64.slice(0, 50) + "..."); // Log first 50 chars to confirm
72+
73+
if (!base64) {
74+
console.error("Failed to convert file to base64.");
75+
return;
76+
}
77+
78+
79+
// ✅ Upload to Cloudinary
80+
const { url: newImageUrl, publicId: newPublicId } = await handleImageUpload(selectedFile, "thumbnails");
81+
console.log("Upload Success:", newImageUrl);
82+
83+
setImage(newImageUrl);
84+
setMedia((prev) => [...prev, { url: newImageUrl, publicId: newPublicId }]);
85+
86+
// setMedia((prev) => [...prev, newImageUrl]);
87+
} catch (error) {
88+
console.error("Upload failed:", error);
89+
} finally {
90+
setUploading(false);
91+
}
92+
};
93+
94+
3395

3496
const { getRootProps, getInputProps, isDragActive } = useDropzone({ onDrop });
3597

0 commit comments

Comments
 (0)