| | "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); |
| | |
| | |
| | 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; |
| | } |
| | |
| | |
| | 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; |
| | } |
| | |
| | |
| | try { |
| | new URL(urlInput); |
| | } catch { |
| | setError("Invalid URL format."); |
| | return; |
| | } |
| | |
| | setPreviewUrl(urlInput); |
| | setSelectedFile(null); |
| | }, [urlInput]); |
| |
|
| | const handleContinue = useCallback(() => { |
| | if (uploadMode === "file" && selectedFile) { |
| | |
| | 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> |
| | ); |
| | }; |
| |
|