sushilideaclan01's picture
Add creative modifier endpoints and frontend components
fb69858
"use client";
import React, { useState, useCallback, useRef } from "react";
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/Card";
interface CreativeUploaderProps {
onImageReady: (imageUrl: string, imageBytes?: File) => void;
isLoading?: boolean;
}
export const CreativeUploader: React.FC<CreativeUploaderProps> = ({
onImageReady,
isLoading = false,
}) => {
const [uploadMode, setUploadMode] = useState<"file" | "url">("file");
const [urlInput, setUrlInput] = useState("");
const [previewUrl, setPreviewUrl] = useState<string | null>(null);
const [selectedFile, setSelectedFile] = useState<File | null>(null);
const [isDragging, setIsDragging] = useState(false);
const [error, setError] = useState<string | null>(null);
const fileInputRef = useRef<HTMLInputElement>(null);
const handleFileSelect = useCallback((file: File) => {
setError(null);
// Validate file type
const allowedTypes = ["image/png", "image/jpeg", "image/jpg", "image/webp"];
if (!allowedTypes.includes(file.type)) {
setError("Invalid file type. Please upload PNG, JPG, or WebP images.");
return;
}
// Validate file size (max 10MB)
if (file.size > 10 * 1024 * 1024) {
setError("File too large. Maximum size is 10MB.");
return;
}
setSelectedFile(file);
const objectUrl = URL.createObjectURL(file);
setPreviewUrl(objectUrl);
}, []);
const handleDragOver = useCallback((e: React.DragEvent) => {
e.preventDefault();
setIsDragging(true);
}, []);
const handleDragLeave = useCallback((e: React.DragEvent) => {
e.preventDefault();
setIsDragging(false);
}, []);
const handleDrop = useCallback((e: React.DragEvent) => {
e.preventDefault();
setIsDragging(false);
const files = e.dataTransfer.files;
if (files.length > 0) {
handleFileSelect(files[0]);
}
}, [handleFileSelect]);
const handleFileInputChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
const files = e.target.files;
if (files && files.length > 0) {
handleFileSelect(files[0]);
}
}, [handleFileSelect]);
const handleUrlSubmit = useCallback(() => {
setError(null);
if (!urlInput.trim()) {
setError("Please enter an image URL.");
return;
}
// Basic URL validation
try {
new URL(urlInput);
} catch {
setError("Invalid URL format.");
return;
}
setPreviewUrl(urlInput);
setSelectedFile(null);
}, [urlInput]);
const handleContinue = useCallback(() => {
if (uploadMode === "file" && selectedFile) {
// For file upload, we pass both the preview URL and the file
onImageReady(previewUrl!, selectedFile);
} else if (uploadMode === "url" && previewUrl) {
onImageReady(previewUrl);
}
}, [uploadMode, selectedFile, previewUrl, onImageReady]);
const handleClear = useCallback(() => {
setPreviewUrl(null);
setSelectedFile(null);
setUrlInput("");
setError(null);
if (fileInputRef.current) {
fileInputRef.current.value = "";
}
}, []);
return (
<Card variant="glass">
<CardHeader>
<CardTitle>Upload Your Creative</CardTitle>
<CardDescription>
Upload an existing ad creative to analyze and modify with new angles or concepts
</CardDescription>
</CardHeader>
<CardContent>
{/* Mode Toggle */}
<div className="flex gap-2 mb-6">
<button
type="button"
onClick={() => {
setUploadMode("file");
handleClear();
}}
className={`flex-1 py-2 px-4 rounded-lg font-medium transition-all ${
uploadMode === "file"
? "bg-blue-500 text-white"
: "bg-gray-100 text-gray-600 hover:bg-gray-200"
}`}
>
File Upload
</button>
<button
type="button"
onClick={() => {
setUploadMode("url");
handleClear();
}}
className={`flex-1 py-2 px-4 rounded-lg font-medium transition-all ${
uploadMode === "url"
? "bg-blue-500 text-white"
: "bg-gray-100 text-gray-600 hover:bg-gray-200"
}`}
>
Image URL
</button>
</div>
{/* Error Display */}
{error && (
<div className="mb-4 p-3 bg-red-50 border border-red-200 rounded-lg text-red-600 text-sm">
{error}
</div>
)}
{/* File Upload Area */}
{uploadMode === "file" && !previewUrl && (
<div
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onDrop={handleDrop}
onClick={() => fileInputRef.current?.click()}
className={`border-2 border-dashed rounded-xl p-8 text-center cursor-pointer transition-all ${
isDragging
? "border-blue-500 bg-blue-50"
: "border-gray-300 hover:border-blue-400 hover:bg-gray-50"
}`}
>
<input
ref={fileInputRef}
type="file"
accept="image/png,image/jpeg,image/jpg,image/webp"
onChange={handleFileInputChange}
className="hidden"
/>
<div className="flex flex-col items-center gap-3">
<svg
className="w-12 h-12 text-gray-400"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"
/>
</svg>
<div className="text-gray-600">
<span className="font-semibold text-blue-500">Click to upload</span> or drag and drop
</div>
<p className="text-xs text-gray-500">PNG, JPG, or WebP (max 10MB)</p>
</div>
</div>
)}
{/* URL Input */}
{uploadMode === "url" && !previewUrl && (
<div className="space-y-4">
<div className="flex gap-2">
<input
type="url"
value={urlInput}
onChange={(e) => setUrlInput(e.target.value)}
placeholder="https://example.com/image.png"
className="flex-1 px-4 py-3 rounded-xl border-2 border-gray-300 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
/>
<button
type="button"
onClick={handleUrlSubmit}
className="px-6 py-3 bg-blue-500 text-white font-medium rounded-xl hover:bg-blue-600 transition-colors"
>
Load
</button>
</div>
<p className="text-xs text-gray-500">
Enter a direct link to an image (PNG, JPG, or WebP)
</p>
</div>
)}
{/* Image Preview */}
{previewUrl && (
<div className="space-y-4">
<div className="relative rounded-xl overflow-hidden bg-gray-100">
<img
src={previewUrl}
alt="Creative preview"
className="w-full h-auto max-h-96 object-contain"
onError={() => {
setError("Failed to load image. Please check the URL or try a different image.");
setPreviewUrl(null);
}}
/>
</div>
<div className="flex gap-3">
<button
type="button"
onClick={handleClear}
className="flex-1 py-3 px-4 border-2 border-gray-300 text-gray-700 font-medium rounded-xl hover:bg-gray-50 transition-colors"
>
Change Image
</button>
<button
type="button"
onClick={handleContinue}
disabled={isLoading}
className="flex-1 py-3 px-4 bg-gradient-to-r from-blue-500 to-cyan-500 text-white font-bold rounded-xl hover:from-blue-600 hover:to-cyan-600 transition-all disabled:opacity-50 disabled:cursor-not-allowed"
>
{isLoading ? "Analyzing..." : "Analyze Creative"}
</button>
</div>
</div>
)}
</CardContent>
</Card>
);
};