'use client';
import { useEffect, useRef, useState } from "react";
type SignedUploadPayload = {
apiKey: string;
cloudName: string;
folder: string;
signature: string;
timestamp: number;
};
export function useCloudinaryUpload(onUploadingChange?: (isUploading: boolean) => void) {
const activeUploadRef = useRef<XMLHttpRequest | null>(null);
const uploadRequestIdRef = useRef(0);
const [uploadedUrl, setUploadedUrl] = useState("");
const [isUploading, setIsUploading] = useState(false);
const [uploadProgress, setUploadProgress] = useState(0);
const [uploadError, setUploadError] = useState<string | null>(null);
const updateUploadingState = (isUploadingNow: boolean) => {
setIsUploading(isUploadingNow);
onUploadingChange?.(isUploadingNow);
};
const resetUploadState = () => {
setUploadedUrl("");
setUploadError(null);
setUploadProgress(0);
updateUploadingState(false);
};
const cancelUpload = () => {
uploadRequestIdRef.current += 1;
activeUploadRef.current?.abort();
activeUploadRef.current = null;
resetUploadState();
};
const uploadFile = async (file: File) => {
uploadRequestIdRef.current += 1;
const requestId = uploadRequestIdRef.current;
activeUploadRef.current?.abort();
setUploadedUrl("");
setUploadError(null);
setUploadProgress(0);
updateUploadingState(true);
try {
const signResponse = await fetch("/api/cloudinary/sign", {
method: "POST",
});
if (!signResponse.ok) {
throw new Error("Failed to get upload signature");
}
const { apiKey, cloudName, folder, signature, timestamp } =
(await signResponse.json()) as SignedUploadPayload;
const formData = new FormData();
formData.append("file", file);
formData.append("api_key", apiKey);
formData.append("folder", folder);
formData.append("signature", signature);
formData.append("timestamp", String(timestamp));
const uploadUrl = `https://api.cloudinary.com/v1_1/${cloudName}/image/upload`;
const secureUrl = await new Promise<string>((resolve, reject) => {
const xhr = new XMLHttpRequest();
activeUploadRef.current = xhr;
xhr.open("POST", uploadUrl);
xhr.upload.onprogress = (event) => {
if (!event.lengthComputable) return;
setUploadProgress((event.loaded / event.total) * 100);
};
xhr.onerror = () => reject(new Error("Cloudinary upload failed"));
xhr.onabort = () => reject(new Error("Upload was cancelled"));
xhr.onload = () => {
if (xhr.status < 200 || xhr.status >= 300) {
reject(new Error("Cloudinary upload failed"));
return;
}
const response = JSON.parse(xhr.responseText) as {
secure_url?: string;
};
if (!response.secure_url) {
reject(new Error("Cloudinary response did not include a URL"));
return;
}
resolve(response.secure_url);
};
xhr.send(formData);
});
if (requestId !== uploadRequestIdRef.current) {
return;
}
setUploadProgress(100);
setUploadedUrl(secureUrl);
} catch (error) {
if (requestId !== uploadRequestIdRef.current) {
return;
}
const message = error instanceof Error ? error.message : "Failed to upload image";
if (message !== "Upload was cancelled") {
setUploadError(message);
}
} finally {
if (requestId === uploadRequestIdRef.current) {
activeUploadRef.current = null;
updateUploadingState(false);
}
}
};
useEffect(() => {
return () => {
activeUploadRef.current?.abort();
};
}, []);
return {
cancelUpload,
isUploading,
uploadError,
uploadFile,
uploadedUrl,