diff --git a/app/api/upload/route.ts b/app/api/upload/route.ts index f77c6d9..ff3117e 100644 --- a/app/api/upload/route.ts +++ b/app/api/upload/route.ts @@ -54,7 +54,7 @@ export async function POST(req: Request) { const svgString = await cloudinaryToSVG(cloudinaryResponse.secure_url); return NextResponse.json( - { url: cloudinaryResponse.secure_url, svg: svgString }, + { url: cloudinaryResponse.secure_url, svg: svgString, success: true }, { status: 201 }, ); } diff --git a/app/generate/page.tsx b/app/generate/page.tsx index db2a9bf..3612174 100644 --- a/app/generate/page.tsx +++ b/app/generate/page.tsx @@ -1,38 +1,492 @@ "use client"; -import { useState } from "react"; +import { Button } from "@/components/ui/button"; +import { AnimatePresence, motion } from "framer-motion"; +import { + AlertCircle, + CheckCircle, + Copy, + Download, + File, + Loader2, + Upload, + X, +} from "lucide-react"; +import { useCallback, useEffect, useRef, useState } from "react"; + +interface UploadedFile { + file: File; + name: string; + size: string; + progress: number; + preview: string; +} + +interface ApiResponse { + url: string; + svg: string; + success: boolean; + error?: string; +} export default function UploadPage() { - const [file, setFile] = useState(null); - const [data, setData] = useState(null); + const [uploadedFiles, setUploadedFiles] = useState([]); + const [isDragActive, setIsDragActive] = useState(false); + const [isUploading, setIsUploading] = useState(false); + const [svgResult, setSvgResult] = useState(null); + const [copySuccess, setCopySuccess] = useState(false); + const [error, setError] = useState(null); + const fileInputRef = useRef(null); + + useEffect(() => { + return () => { + uploadedFiles.forEach((file) => { + if (file.preview) { + URL.revokeObjectURL(file.preview); + } + }); + }; + }, [uploadedFiles]); + + const formatFileSize = (bytes: number): string => { + if (bytes === 0) return "0 Bytes"; + const k = 1024; + const sizes = ["Bytes", "KB", "MB", "GB"]; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i]; + }; + + const handleFiles = (files: FileList) => { + const file = files[0]; + if (!file) return; + + if (!file.type.startsWith("image/")) { + setError("Please select an image file"); + return; + } + + // Check file size (max 5MB) + if (file.size > 5 * 1024 * 1024) { + setError("File size must be less than 5MB"); + return; + } + + // Create preview URL + const previewUrl = URL.createObjectURL(file); - const handleFileChange = (e: React.ChangeEvent) => { + const newFile: UploadedFile = { + file, + name: file.name, + size: formatFileSize(file.size), + progress: 0, + preview: previewUrl, + }; + + setUploadedFiles([newFile]); + setError(null); + setSvgResult(null); + }; + + const handleDrop = useCallback((e: React.DragEvent) => { + e.preventDefault(); + setIsDragActive(false); + + const files = e.dataTransfer.files; + if (files.length > 0) { + handleFiles(files); + } + }, []); + + const handleDragOver = useCallback((e: React.DragEvent) => { + e.preventDefault(); + setIsDragActive(true); + }, []); + + const handleDragLeave = useCallback((e: React.DragEvent) => { + e.preventDefault(); + setIsDragActive(false); + }, []); + + const handleFileInput = (e: React.ChangeEvent) => { if (e.target.files) { - setFile(e.target.files[0]); + handleFiles(e.target.files); + } + }; + + const removeFile = () => { + uploadedFiles.forEach((file) => { + if (file.preview) { + URL.revokeObjectURL(file.preview); + } + }); + + setUploadedFiles([]); + setSvgResult(null); + setError(null); + if (fileInputRef.current) { + fileInputRef.current.value = ""; } }; const handleUpload = async () => { - if (!file) return; + if (uploadedFiles.length === 0) return; - const formData = new FormData(); - formData.append("image", file); + setIsUploading(true); + setError(null); - const res = await fetch("/api/upload", { - method: "POST", - body: formData, - }); + // Simulate progress + const file = uploadedFiles[0]; + const updateProgress = (progress: number) => { + setUploadedFiles([{ ...file, progress }]); + }; + + try { + updateProgress(20); + + const formData = new FormData(); + formData.append("image", file.file); - const data = await res.json(); - setData(data.url + "\n\n" + data.svg); - console.log("Response:", data); + updateProgress(50); + + const res = await fetch("/api/upload", { + method: "POST", + body: formData, + }); + + updateProgress(80); + + const data: ApiResponse = await res.json(); + console.log(data); + + updateProgress(100); + + if (data.success && data.svg) { + setSvgResult(fixSvg(data.svg)); + } else { + setError(data.error || "Failed to convert image to SVG"); + } + } catch (error) { + console.error("Upload error:", error); + setError("Failed to upload image. Please try again."); + } finally { + setIsUploading(false); + } + }; + + const copyToClipboard = async () => { + if (!svgResult) return; + + try { + await navigator.clipboard.writeText(svgResult); + setCopySuccess(true); + setTimeout(() => setCopySuccess(false), 2000); + } catch (error) { + console.error("Failed to copy:", error); + } + }; + + const downloadSvg = () => { + if (!svgResult) return; + + const blob = new Blob([svgResult], { type: "image/svg+xml" }); + const url = URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = `converted-${ + uploadedFiles[0]?.name.split(".")[0] || "image" + }.svg`; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + }; + + const fixSvg = (svg: string) => { + // Find width and height in the tag + const widthMatch = svg.match(/width="(\d+)"/); + const heightMatch = svg.match(/height="(\d+)"/); + + if (widthMatch && heightMatch) { + const width = widthMatch[1]; + const height = heightMatch[1]; + + // If no viewBox exists, inject one + if (!/viewBox=/.test(svg)) { + svg = svg.replace( + /]*)>/, + ``, + ); + } else { + // Still normalize width/height + svg = svg + .replace(/width="[^"]*"/, 'width="100%"') + .replace(/height="[^"]*"/, 'height="100%"'); + } + } + + return svg; }; return ( -
- - -

{data}

+
+
+ + {/* Header */} +
+

+ Upload and Convert Images +

+

+ Upload images to convert them to SVG format. +

+
+ + {/* Upload Area */} +
+ + +
+ +

+ {" "} + or drag and drop +

+

+ Maximum file size 5 MB. +

+
+
+ + {/* Error Message */} + + {error && ( + + +

{error}

+
+ )} +
+ + {/* Uploaded Files */} + + {uploadedFiles.map((file, index) => ( + +
+
+ +
+

+ {file.name} +

+

+ {file.size} +

+
+
+ +
+ + {/* Image Preview */} +
+
+ {file.name} +
+

+ Original Image Preview +

+
+ + {/* Progress Bar */} +
+
+
+
+ + {file.progress}% + + {file.progress === 100 && !isUploading && ( + + )} +
+ + ))} + + + {uploadedFiles.length > 0 && !svgResult && ( +
+ + +
+ )} + + {/* SVG Result */} + + {svgResult && ( + +

+ 🎉 SVG Generated Successfully! +

+ + {/* SVG Preview and Code Side by Side */} +
+ {/* SVG Preview */} +
+

+ SVG Preview +

+
+ {/* Checkerboard background for transparency */} +
+
+
+

+ Generated SVG Vector +

+
+ + {/* SVG Code */} +
+

+ SVG Code +

+
+
+                        {svgResult}
+                      
+
+

+ Raw SVG Source Code +

+
+
+ + {/* Action Buttons */} +
+ + +
+ + {/* Additional Info */} +
+
+ +

+ Success! Your image + has been converted to a scalable SVG vector. The SVG can + be resized without losing quality and is perfect for web + use. +

+
+
+ + )} + + +
); }