diff --git a/src/app/admin/products/create/page.jsx b/src/app/admin/products/create/page.jsx
index 3ead615..1782ae8 100644
--- a/src/app/admin/products/create/page.jsx
+++ b/src/app/admin/products/create/page.jsx
@@ -2,7 +2,7 @@
import { useState, useEffect, useRef } from "react";
import { useRouter } from "next/navigation";
-import { FiUpload, FiX } from "react-icons/fi";
+import { FiUpload, FiX, FiVideo } from "react-icons/fi";
import { FaChevronUp, FaChevronDown } from "react-icons/fa";
import { useAuth } from "@/context/AuthContext";
import MinimalEditor from "@/app/components/admin/MinimalEditor";
@@ -99,7 +99,51 @@ export default function CreateProductPage() {
faq: "",
});
const [uploading, setUploading] = useState(false);
+ const [video, setVideo] = useState(null);
+ const [uploadingVideo, setUploadingVideo] = useState(false);
+ const [videoProgress, setVideoProgress] = useState(0);
const thumbnailsRef = useRef(null);
+ const videoXhrRef = useRef(null);
+
+ // ================= PREVENT REFRESH =================
+ useEffect(() => {
+ const handleBeforeUnload = (e) => {
+ if (uploading || uploadingVideo) {
+ e.preventDefault();
+ e.returnValue = "Are you sure you want to leave? Upload is in progress.";
+ return e.returnValue;
+ }
+ };
+
+ const handleGlobalClick = (e) => {
+ if (uploading || uploadingVideo) {
+ const target = e.target.closest("a");
+ if (target && target.href && !target.href.startsWith("javascript:") && target.target !== "_blank") {
+ if (!window.confirm("An upload is in progress. Are you sure you want to leave?")) {
+ e.preventDefault();
+ e.stopPropagation();
+ }
+ }
+ }
+ };
+
+ window.addEventListener("beforeunload", handleBeforeUnload);
+ document.addEventListener("click", handleGlobalClick, { capture: true });
+
+ return () => {
+ window.removeEventListener("beforeunload", handleBeforeUnload);
+ document.removeEventListener("click", handleGlobalClick, { capture: true });
+ };
+ }, [uploading, uploadingVideo]);
+
+ // ================= ABORT ON UNMOUNT =================
+ useEffect(() => {
+ return () => {
+ if (videoXhrRef.current) {
+ videoXhrRef.current.abort();
+ }
+ };
+ }, []);
const scrollThumbs = (direction) => {
if (!thumbnailsRef.current) return;
@@ -220,6 +264,116 @@ export default function CreateProductPage() {
}));
};
+ /* =========================
+ VIDEO UPLOAD HANDLING
+ ========================== */
+
+ const handleVideoUpload = async (e) => {
+ const file = e.target.files?.[0];
+ if (!file) return;
+
+ if (!file.type.startsWith("video/")) {
+ toast.error("Please select a video file");
+ return;
+ }
+
+ if (file.size > 50 * 1024 * 1024) {
+ toast.error("Video must be under 50MB");
+ return;
+ }
+
+ setUploadingVideo(true);
+ setVideoProgress(0);
+ const toastId = toast.loading("Preparing upload...", { duration: Infinity });
+
+ try {
+ // Step 1: Get signed credentials from backend
+ const sigRes = await fetch(
+ `${process.env.NEXT_PUBLIC_API_URL}/api/products/video-signature`,
+ { credentials: "include" }
+ );
+
+ if (!sigRes.ok) {
+ toast.error("Failed to authorize upload", { id: toastId });
+ setUploadingVideo(false);
+ return;
+ }
+
+ const { signature, timestamp, api_key, cloud_name, folder } = await sigRes.json();
+
+ // Step 2: Upload directly to Cloudinary
+ const formData = new FormData();
+ formData.append("file", file);
+ formData.append("api_key", api_key);
+ formData.append("timestamp", timestamp);
+ formData.append("signature", signature);
+ formData.append("folder", folder);
+ formData.append("resource_type", "video");
+
+ toast.loading("Uploading video... 0%", { id: toastId, duration: Infinity });
+
+ const xhr = new XMLHttpRequest();
+ videoXhrRef.current = xhr;
+ xhr.open("POST", `https://api.cloudinary.com/v1_1/${cloud_name}/video/upload`);
+
+ xhr.upload.onprogress = (event) => {
+ if (event.lengthComputable) {
+ const pct = Math.round((event.loaded / event.total) * 100);
+ setVideoProgress(pct);
+ toast.loading(`Uploading video... ${pct}%`, { id: toastId, duration: Infinity });
+ }
+ };
+
+ xhr.onload = () => {
+ videoXhrRef.current = null;
+ try {
+ const data = JSON.parse(xhr.responseText);
+ if (xhr.status >= 200 && xhr.status < 300) {
+ setVideo({
+ url: data.secure_url,
+ public_id: data.public_id,
+ });
+ toast.success("Video uploaded successfully", { id: toastId, duration: 4000 });
+ } else {
+ toast.error(data.error?.message || "Video upload failed", { id: toastId, duration: 4000 });
+ }
+ } catch {
+ toast.error("Video upload failed", { id: toastId, duration: 4000 });
+ } finally {
+ setUploadingVideo(false);
+ setVideoProgress(0);
+ }
+ };
+
+ xhr.onerror = () => {
+ videoXhrRef.current = null;
+ toast.error("Video upload failed — check your connection", { id: toastId, duration: 4000 });
+ setUploadingVideo(false);
+ setVideoProgress(0);
+ };
+
+ xhr.onabort = () => {
+ videoXhrRef.current = null;
+ toast.dismiss(toastId);
+ setUploadingVideo(false);
+ setVideoProgress(0);
+ };
+
+ xhr.send(formData);
+ } catch (err) {
+ toast.error("Video upload failed", { id: toastId, duration: 4000 });
+ setUploadingVideo(false);
+ setVideoProgress(0);
+ }
+
+ e.target.value = "";
+ };
+
+ const handleRemoveVideo = () => {
+ setVideo(null);
+ toast.success("Video removed");
+ };
+
/* =========================
SUBMIT
========================== */
@@ -266,6 +420,7 @@ export default function CreateProductPage() {
description: form.description,
faq: form.faq,
images: images,
+ video: video || { url: null, public_id: null },
}),
},
);
@@ -376,6 +531,69 @@ ${uploading ? "opacity-60 pointer-events-none" : ""}
)}
+
+ {/* VIDEO UPLOAD SECTION */}
+
+
+
+
+ Product Video
+
+ Max 50MB • All formats
+
+
+ {video?.url ? (
+
+
+
+
+ ) : (
+
+ )}
+
{/* FORM FIELDS */}
@@ -484,8 +702,8 @@ ${uploading ? "opacity-60 pointer-events-none" : ""}
diff --git a/src/app/admin/products/edit/[slug]/EditProductClient.jsx b/src/app/admin/products/edit/[slug]/EditProductClient.jsx
index be5b82b..11bbe40 100644
--- a/src/app/admin/products/edit/[slug]/EditProductClient.jsx
+++ b/src/app/admin/products/edit/[slug]/EditProductClient.jsx
@@ -4,7 +4,7 @@ import { useEffect, useState, useRef } from "react";
import { useParams, useRouter } from "next/navigation";
import { useAuth } from "@/context/AuthContext";
import MinimalEditor from "@/app/components/admin/MinimalEditor";
-import { FiUpload, FiX } from "react-icons/fi";
+import { FiUpload, FiX, FiVideo } from "react-icons/fi";
import { FaChevronUp, FaChevronDown } from "react-icons/fa";
import EditProductSkeleton from "@/app/components/admin/EditProductSkeleton";
import toast from "react-hot-toast";
@@ -96,6 +96,11 @@ export default function EditProductPage() {
const [replyMap, setReplyMap] = useState({});
const [sendingMailId, setSendingMailId] = useState(null);
const [updating, setUpdating] = useState(false);
+ const [video, setVideo] = useState(null);
+ const [uploading, setUploading] = useState(false);
+ const [uploadingVideo, setUploadingVideo] = useState(false);
+ const [videoProgress, setVideoProgress] = useState(0);
+ const videoXhrRef = useRef(null);
const [form, setForm] = useState({
title: "",
@@ -110,6 +115,46 @@ export default function EditProductPage() {
});
const sensors = useSensors(useSensor(PointerSensor));
+ // ================= PREVENT REFRESH =================
+ useEffect(() => {
+ const handleBeforeUnload = (e) => {
+ if (uploading || uploadingVideo || updating) {
+ e.preventDefault();
+ e.returnValue = "Are you sure you want to leave? Upload is in progress.";
+ return e.returnValue;
+ }
+ };
+
+ const handleGlobalClick = (e) => {
+ if (uploading || uploadingVideo || updating) {
+ const target = e.target.closest("a");
+ if (target && target.href && !target.href.startsWith("javascript:") && target.target !== "_blank") {
+ if (!window.confirm("An upload or update is in progress. Are you sure you want to leave?")) {
+ e.preventDefault();
+ e.stopPropagation();
+ }
+ }
+ }
+ };
+
+ window.addEventListener("beforeunload", handleBeforeUnload);
+ document.addEventListener("click", handleGlobalClick, { capture: true });
+
+ return () => {
+ window.removeEventListener("beforeunload", handleBeforeUnload);
+ document.removeEventListener("click", handleGlobalClick, { capture: true });
+ };
+ }, [uploading, uploadingVideo, updating]);
+
+ // ================= ABORT ON UNMOUNT =================
+ useEffect(() => {
+ return () => {
+ if (videoXhrRef.current) {
+ videoXhrRef.current.abort();
+ }
+ };
+ }, []);
+
// ================= FETCH PRODUCT =================
useEffect(() => {
const fetchProduct = async () => {
@@ -139,6 +184,7 @@ export default function EditProductPage() {
setImages(data.images || []);
setActiveImage(data.images?.[0]?.url || null);
+ setVideo(data.video?.url ? data.video : null);
setTimeout(() => setLoading(false), 200);
};
@@ -183,25 +229,32 @@ export default function EditProductPage() {
formData.append("images", file);
}
- const res = await fetch(
- `${process.env.NEXT_PUBLIC_API_URL}/api/products/upload`,
- {
- method: "POST",
- credentials: "include",
- body: formData,
- },
- );
+ try {
+ setUploading(true);
+ const res = await fetch(
+ `${process.env.NEXT_PUBLIC_API_URL}/api/products/upload`,
+ {
+ method: "POST",
+ credentials: "include",
+ body: formData,
+ },
+ );
- const data = await res.json();
- if (!res.ok) {
- router.push("/admin/products");
- return;
- }
+ const data = await res.json();
+ if (!res.ok) {
+ router.push("/admin/products");
+ return;
+ }
- setImages((prev) => [...prev, ...data]);
+ setImages((prev) => [...prev, ...data]);
- if (!activeImage && data.length > 0) {
- setActiveImage(data[0].url);
+ if (!activeImage && data.length > 0) {
+ setActiveImage(data[0].url);
+ }
+ } catch (err) {
+ toast.error("Image upload failed");
+ } finally {
+ setUploading(false);
}
};
@@ -288,6 +341,114 @@ export default function EditProductPage() {
setForm({ ...form, [activeTab]: value });
};
+ // ================= VIDEO UPLOAD (Direct to Cloudinary) =================
+ const handleVideoUpload = async (e) => {
+ const file = e.target.files?.[0];
+ if (!file) return;
+
+ // Client-side validation
+ if (!file.type.startsWith("video/")) {
+ toast.error("Please select a video file");
+ return;
+ }
+
+ if (file.size > 50 * 1024 * 1024) {
+ toast.error("Video must be under 50MB");
+ return;
+ }
+
+ setUploadingVideo(true);
+ setVideoProgress(0);
+ const toastId = toast.loading("Preparing upload...", { duration: Infinity });
+
+ try {
+ // Step 1: Get signed credentials from backend
+ const sigRes = await fetch(
+ `${process.env.NEXT_PUBLIC_API_URL}/api/products/video-signature`,
+ { credentials: "include" }
+ );
+
+ if (!sigRes.ok) {
+ toast.error("Failed to authorize upload", { id: toastId });
+ setUploadingVideo(false);
+ return;
+ }
+
+ const { signature, timestamp, api_key, cloud_name, folder } = await sigRes.json();
+
+ // Step 2: Upload directly to Cloudinary
+ const formData = new FormData();
+ formData.append("file", file);
+ formData.append("api_key", api_key);
+ formData.append("timestamp", timestamp);
+ formData.append("signature", signature);
+ formData.append("folder", folder);
+ formData.append("resource_type", "video");
+
+ toast.loading("Uploading video... 0%", { id: toastId, duration: Infinity });
+
+ const xhr = new XMLHttpRequest();
+ videoXhrRef.current = xhr;
+ xhr.open("POST", `https://api.cloudinary.com/v1_1/${cloud_name}/video/upload`);
+
+ xhr.upload.onprogress = (event) => {
+ if (event.lengthComputable) {
+ const pct = Math.round((event.loaded / event.total) * 100);
+ setVideoProgress(pct);
+ toast.loading(`Uploading video... ${pct}%`, { id: toastId, duration: Infinity });
+ }
+ };
+
+ xhr.onload = () => {
+ videoXhrRef.current = null;
+ try {
+ const data = JSON.parse(xhr.responseText);
+ if (xhr.status >= 200 && xhr.status < 300) {
+ setVideo({
+ url: data.secure_url,
+ public_id: data.public_id,
+ });
+ toast.success("Video uploaded successfully", { id: toastId, duration: 4000 });
+ } else {
+ toast.error(data.error?.message || "Video upload failed", { id: toastId, duration: 4000 });
+ }
+ } catch {
+ toast.error("Video upload failed", { id: toastId, duration: 4000 });
+ } finally {
+ setUploadingVideo(false);
+ setVideoProgress(0);
+ }
+ };
+
+ xhr.onerror = () => {
+ videoXhrRef.current = null;
+ toast.error("Video upload failed — check your connection", { id: toastId, duration: 4000 });
+ setUploadingVideo(false);
+ setVideoProgress(0);
+ };
+
+ xhr.onabort = () => {
+ videoXhrRef.current = null;
+ toast.dismiss(toastId);
+ setUploadingVideo(false);
+ setVideoProgress(0);
+ };
+
+ xhr.send(formData);
+ } catch (err) {
+ toast.error("Video upload failed", { id: toastId, duration: 4000 });
+ setUploadingVideo(false);
+ setVideoProgress(0);
+ }
+
+ e.target.value = "";
+ };
+
+ const handleRemoveVideo = () => {
+ setVideo(null);
+ toast.success("Video removed");
+ };
+
const handleUpdate = async () => {
try {
setUpdating(true);
@@ -311,6 +472,7 @@ export default function EditProductPage() {
description: form.description,
faq: form.faq,
images,
+ video: video || { url: null, public_id: null },
}),
},
);
@@ -427,6 +589,70 @@ export default function EditProductPage() {
+ {/* VIDEO SECTION */}
+
+
+
+
+ Product Video
+
+ Max 50MB • All formats supported
+
+
+ {video?.url ? (
+
+
+
+
+ ) : (
+
+ )}
+
+
{/* RIGHT FORM */}
{/* LEFT ARROW */}
@@ -466,7 +501,7 @@ export default function ProductDetails({ product }) {
{/* DOTS */}
- {product.images?.map((_, index) => (
+ {Array.from({ length: totalSlides }).map((_, index) => (
- {/* Dark Overlay */}
-
setShowGallery(false)}
- />
-
- {/* Modal Content */}
-
- {/* Close Button */}
-
-
- {/* Left Arrow */}
-
+
+
- {/* SLIDING STRIP WRAPPER — this is the key: full size, overflow hidden */}
-
-
- {product.images?.map((img, index) => (
-
-

-
- ))}
-
-
+ {/* Left Arrow */}
+
- {/* Right Arrow */}
-
+
+
+ {/* Right Arrow */}
+
+
+ {/* Bottom Thumbnails */}
+
+ {product.images?.map((img, index) => (
+

setCurrentIndex(index)}
+ className={`h-16 w-16 flex-shrink-0 object-cover cursor-pointer rounded-md transition duration-200 ${
+ currentIndex === index && !isVideoSlide
+ ? "ring-2 ring-white scale-110 opacity-100"
+ : "opacity-50 hover:opacity-100"
+ }`}
+ />
+ ))}
+
+ {/* Video Thumbnail in Modal */}
+ {videoUrl && (
+
setCurrentIndex(product.images?.length || 0)}
+ className={`h-16 w-16 flex-shrink-0 cursor-pointer rounded-md transition duration-200 relative flex items-center justify-center bg-gray-900 border border-white/20 ${
+ isVideoSlide
+ ? "ring-2 ring-white scale-110 opacity-100"
+ : "opacity-50 hover:opacity-100"
+ }`}
+ >
+
+
+ )}
+
)}
diff --git a/src/lib/api.js b/src/lib/api.js
index 0e32da0..a34b4ca 100644
--- a/src/lib/api.js
+++ b/src/lib/api.js
@@ -22,7 +22,9 @@ export async function fetchProducts(categorySlug, search, page = 1, limit = 24)
export const fetchProductBySlug = async (slug) => {
- const res = await fetch(`${BASE_URL}/products/${slug}`);
+ const res = await fetch(`${BASE_URL}/products/${slug}`, {
+ cache: "no-store",
+ });
return res.json();
};