| import React, { useState, useEffect, useRef, useCallback } from "react"; |
| import { |
| Download, |
| Upload, |
| Share2, |
| BookOpen, |
| Linkedin, |
| Menu, |
| X, |
| Feather, |
| Crown, |
| RotateCcw, |
| } from "lucide-react"; |
|
|
| const App = () => { |
| const [currentSection, setCurrentSection] = useState("home"); |
| const [mobileMenuOpen, setMobileMenuOpen] = useState(false); |
| const [scrollY, setScrollY] = useState(0); |
| const [isLoaded, setIsLoaded] = useState(false); |
| const [uploadedImage, setUploadedImage] = useState<File | null>(null); |
| const [processedImageUrl, setProcessedImageUrl] = useState<string | null>( |
| null |
| ); |
| const [isProcessing, setIsProcessing] = useState(false); |
|
|
| const [generatorSettings, setGeneratorSettings] = useState({ |
| quality: "low", |
| palette: "math", |
| }); |
|
|
| const canvasRef = useRef<HTMLCanvasElement>(null); |
|
|
| useEffect(() => { |
| const handleScroll = () => setScrollY(window.scrollY); |
| window.addEventListener("scroll", handleScroll); |
| return () => window.removeEventListener("scroll", handleScroll); |
| }, []); |
|
|
| useEffect(() => setIsLoaded(true), []); |
|
|
| const processImage = async (imageFile: File) => { |
| |
| const quality = generatorSettings.quality.toLowerCase(); |
| const palette = generatorSettings.palette.toLowerCase(); |
|
|
| const fd = new FormData(); |
| fd.append("file", imageFile); |
| fd.append("quality", quality); |
| fd.append("palette", palette); |
|
|
| console.log("Sending to backend:", { |
| quality, |
| palette, |
| file: imageFile.name, |
| }); |
|
|
| try { |
| setIsProcessing(true); |
| const apiUrl = import.meta.env.DEV ? "http://localhost:5000/process" : "/process"; |
| const res = await fetch(apiUrl, { |
| method: "POST", |
| body: fd, |
| }); |
|
|
| if (!res.ok) { |
| const err = await res.json().catch(() => null); |
| throw new Error(err?.detail || `Server returned ${res.status}`); |
| } |
|
|
| const blob = await res.blob(); |
| const url = URL.createObjectURL(blob); |
| setProcessedImageUrl(url); |
| } catch (err: any) { |
| console.error("Processing failed:", err); |
| alert("Processing failed: " + (err.message || err)); |
| } finally { |
| setIsProcessing(false); |
| } |
| }; |
|
|
| const handleImageUpload = async (e: React.ChangeEvent<HTMLInputElement>) => { |
| const file = e.target.files?.[0]; |
| if (!file) return; |
|
|
| setUploadedImage(file); |
| setProcessedImageUrl(null); |
| await processImage(file); |
| }; |
|
|
| const handleRegenerate = async () => { |
| if (!uploadedImage) return; |
| setProcessedImageUrl(null); |
| await processImage(uploadedImage); |
| }; |
|
|
| const exportCanvas = () => { |
| if (processedImageUrl) { |
| const link = document.createElement("a"); |
| link.href = processedImageUrl; |
| link.download = "symbol_art.png"; |
| document.body.appendChild(link); |
| link.click(); |
| document.body.removeChild(link); |
| } |
| }; |
|
|
| const shareArt = () => { |
| alert("Sharing functionality is not implemented yet."); |
| }; |
|
|
| const renderNav = useCallback(() => { |
| const navItems = [ |
| { |
| name: "Home", |
| section: "home", |
| icon: <Feather className="w-4 h-4" />, |
| }, |
| { |
| name: "About", |
| section: "about", |
| icon: <BookOpen className="w-4 h-4" />, |
| }, |
| ]; |
|
|
| return ( |
| <nav |
| className={`fixed top-0 z-50 w-full transition-all duration-700 ${scrollY > 50 |
| ? "bg-midnight/90 backdrop-blur-md border-b border-gold-accent/20" |
| : "bg-transparent" |
| }`} |
| > |
| <div className="max-w-7xl mx-auto px-6 py-6 flex items-center justify-between"> |
| <a |
| href="#" |
| className="text-3xl font-display text-gold-accent tracking-widest hover:opacity-80 transition-opacity" |
| > |
| Symbol Art |
| </a> |
| <div className="hidden md:flex items-center space-x-12"> |
| {navItems.map((item) => ( |
| <a |
| key={item.section} |
| href={`#${item.section}`} |
| onClick={() => setCurrentSection(item.section)} |
| className={`flex items-center gap-2 font-body text-lg tracking-wide transition-all duration-300 hover:text-gold-accent ${currentSection === item.section ? "text-gold-accent" : "text-gray-400" |
| }`} |
| > |
| {item.icon} |
| <span className="uppercase text-sm">{item.name}</span> |
| </a> |
| ))} |
| <a |
| href="#app" |
| onClick={() => setCurrentSection("home")} |
| className="px-8 py-2 border border-gold-accent/50 text-gold-accent font-display tracking-widest hover:bg-gold-accent hover:text-midnight transition-all duration-500 rounded-sm" |
| > |
| CREATE |
| </a> |
| </div> |
| <button |
| onClick={() => setMobileMenuOpen(true)} |
| className="md:hidden text-gold-accent p-2" |
| > |
| <Menu className="w-6 h-6" /> |
| </button> |
| </div> |
| |
| {/* Mobile Menu */} |
| {mobileMenuOpen && ( |
| <div className="md:hidden fixed inset-0 bg-midnight z-50 flex flex-col items-center justify-center space-y-8 animate-fade-in"> |
| <button |
| onClick={() => setMobileMenuOpen(false)} |
| className="absolute top-6 right-6 text-gold-accent p-2" |
| > |
| <X className="w-8 h-8" /> |
| </button> |
| {navItems.map((item) => ( |
| <a |
| key={item.section} |
| href={`#${item.section}`} |
| onClick={() => { |
| setCurrentSection(item.section); |
| setMobileMenuOpen(false); |
| }} |
| className={`text-4xl font-display text-gold-accent hover:scale-110 transition-transform`} |
| > |
| {item.name} |
| </a> |
| ))} |
| </div> |
| )} |
| </nav> |
| ); |
| }, [scrollY, currentSection, mobileMenuOpen]); |
|
|
| |
| const QualityButton = ({ value, label }: { value: string, label: string }) => ( |
| <button |
| onClick={() => |
| setGeneratorSettings((prev) => ({ |
| ...prev, |
| quality: value, |
| })) |
| } |
| className={`relative group px-6 py-3 min-w-[120px] transition-all duration-500 overflow-hidden ${generatorSettings.quality === value |
| ? "text-midnight" |
| : "text-gold-accent" |
| }`} |
| > |
| <div className={`absolute inset-0 border border-gold-accent/30 transition-all duration-500 ${generatorSettings.quality === value ? "bg-gold-accent" : "bg-transparent group-hover:bg-gold-accent/10" |
| }`} /> |
| <span className={`relative z-10 font-display text-lg tracking-wider ${generatorSettings.quality === value ? "font-bold" : "" |
| }`}> |
| {label} |
| </span> |
| </button> |
| ); |
|
|
| const renderSection = () => ( |
| <div className="space-y-24 pb-24"> |
| {/* Home Section */} |
| {currentSection === "home" && ( |
| <div |
| id="home" |
| className="pt-40 min-h-screen flex flex-col items-center relative" |
| > |
| {/* Decorative background elements */} |
| <div className="absolute top-1/4 left-10 w-64 h-64 bg-purple-900/10 rounded-full blur-3xl pointer-events-none" /> |
| <div className="absolute bottom-1/4 right-10 w-96 h-96 bg-gold-accent/5 rounded-full blur-3xl pointer-events-none" /> |
| |
| <div className="text-center space-y-8 z-10 px-4 max-w-5xl mx-auto"> |
| <h1 className="text-6xl md:text-8xl font-display text-gold-accent leading-tight drop-shadow-2xl animate-fade-in-up"> |
| The Art of <br /> |
| <span className="font-script text-7xl md:text-9xl text-white/90">Equations</span> |
| </h1> |
| <p className="mt-8 text-xl md:text-2xl font-body italic text-gray-300 max-w-2xl mx-auto leading-relaxed"> |
| "Logic and beauty are two sides of the same coin. Let your images speak the language of the universe." |
| </p> |
| |
| <div className="mt-16 flex justify-center"> |
| <a |
| href="#app" |
| className="group relative px-12 py-4 overflow-hidden" |
| > |
| <div className="absolute inset-0 w-full h-full border border-gold-accent/50 rotate-1 transition-transform group-hover:rotate-0" /> |
| <div className="absolute inset-0 w-full h-full border border-gold-accent/50 -rotate-1 transition-transform group-hover:rotate-0" /> |
| <span className="relative font-display text-xl text-gold-accent tracking-[0.2em] group-hover:text-white transition-colors"> |
| BEGIN JOURNEY |
| </span> |
| </a> |
| </div> |
| </div> |
| |
| {/* Canvas & Controls Section */} |
| <div id="app" className="w-[90%] max-w-6xl mt-32 p-1 bg-gradient-to-br from-gold-accent/20 via-transparent to-gold-accent/20 rounded-sm"> |
| <div className="w-full bg-midnight/95 p-8 md:p-12 relative overflow-hidden backdrop-blur-xl border border-white/5"> |
| |
| <div className="flex flex-col md:flex-row items-center justify-between mb-12 border-b border-white/5 pb-8"> |
| <h2 className="text-4xl font-display text-white flex items-center gap-4"> |
| <Crown className="w-8 h-8 text-gold-accent" /> |
| Your Canvas |
| </h2> |
| <div className="flex gap-4 mt-4 md:mt-0"> |
| <button |
| onClick={exportCanvas} |
| className="p-3 text-gold-accent hover:text-white transition-colors" |
| title="Download Masterpiece" |
| > |
| <Download className="w-6 h-6" /> |
| </button> |
| <button |
| onClick={shareArt} |
| className="p-3 text-gold-accent hover:text-white transition-colors" |
| > |
| <Share2 className="w-6 h-6" /> |
| </button> |
| </div> |
| </div> |
| |
| <div className="grid lg:grid-cols-2 gap-16"> |
| {/* The Frame / Display */} |
| <div className="relative group"> |
| <div className="absolute -inset-1 bg-gradient-to-r from-gold-accent/30 to-purple-900/30 rounded-lg blur opacity-50 group-hover:opacity-100 transition duration-1000"></div> |
| <div className="relative w-full h-[500px] bg-black/40 border border-white/10 flex items-center justify-center overflow-hidden"> |
| {isProcessing ? ( |
| <div className="text-center space-y-4"> |
| <div className="w-16 h-16 border-t-2 border-gold-accent rounded-full animate-spin mx-auto" /> |
| <p className="font-display text-xl text-gold-accent animate-pulse">Weaving Symbols...</p> |
| </div> |
| ) : processedImageUrl ? ( |
| <img |
| src={processedImageUrl} |
| alt="Processed Art" |
| className="w-full h-full object-contain" |
| /> |
| ) : ( |
| <div className="text-center p-8 border border-dashed border-white/10 rounded-lg m-8"> |
| <Feather className="w-12 h-12 text-white/20 mx-auto mb-4" /> |
| <p className="font-body text-2xl text-white/30 italic"> |
| "Silence awaits your vision." |
| </p> |
| </div> |
| )} |
| </div> |
| </div> |
| |
| {/* The Controls */} |
| <div className="space-y-12 flex flex-col justify-center"> |
| |
| {/* Upload */} |
| <div className="space-y-4"> |
| <h3 className="text-2xl font-display text-gold-accent">1. The Source</h3> |
| <p className="font-body text-gray-400">Select the image you wish to transmute.</p> |
| <label className="block w-full cursor-pointer group"> |
| <div className="w-full h-32 border border-white/10 bg-white/5 flex flex-col items-center justify-center transition-all duration-300 group-hover:bg-white/10 group-hover:border-gold-accent/50"> |
| <input |
| type="file" |
| accept="image/*" |
| className="hidden" |
| onChange={handleImageUpload} |
| /> |
| <Upload className="w-8 h-8 text-gold-accent mb-2 transition-transform group-hover:-translate-y-1" /> |
| <span className="font-display text-sm text-gray-300 tracking-widest uppercase"> |
| {uploadedImage ? "Change Image" : "Upload Image"} |
| </span> |
| </div> |
| </label> |
| </div> |
| |
| {/* Settings */} |
| <div className="space-y-6"> |
| <h3 className="text-2xl font-display text-gold-accent">2. The Complexity</h3> |
| <div className="flex flex-wrap gap-4"> |
| {/* Preserving exactly the titles: Low, Medium */} |
| <QualityButton value="low" label="Low" /> |
| <QualityButton value="medium" label="Medium" /> |
| </div> |
| </div> |
| |
| {/* Regenerate Button */} |
| {uploadedImage && ( |
| <div className="space-y-4"> |
| <button |
| onClick={handleRegenerate} |
| disabled={isProcessing} |
| className="w-full flex items-center justify-center gap-3 px-6 py-3 bg-gradient-to-r from-gold-accent/20 to-purple-900/20 border border-gold-accent/50 text-gold-accent font-display tracking-widest hover:from-gold-accent/40 hover:to-purple-900/40 hover:border-gold-accent transition-all duration-500 rounded-sm disabled:opacity-50 disabled:cursor-not-allowed" |
| > |
| <RotateCcw className="w-5 h-5" /> |
| REGENERATE |
| </button> |
| </div> |
| )} |
| |
| </div> |
| </div> |
| </div> |
| </div> |
| </div> |
| )} |
| |
| {/* About Section */} |
| {currentSection === "about" && ( |
| <div |
| id="about" |
| className="pt-40 min-h-screen flex flex-col items-center text-center px-6" |
| > |
| <h2 className="text-5xl md:text-6xl font-display text-gold-accent mb-12"> |
| The Philosophy |
| </h2> |
| <div className="max-w-4xl space-y-8 font-body text-xl text-gray-300 leading-relaxed text-left md:text-center"> |
| <p> |
| In a world of pixels, we search for meaning. <strong className="text-white">Symbol Art</strong> is not just a generator; |
| it is an exploration of the underlying mathematical beauty that governs our reality. |
| </p> |
| <p> |
| By converting raw imagery into fundamental symbols, we strip away the noise and reveal the |
| structural elegance hidden within. It is a tribute to the language of the universe. |
| </p> |
| </div> |
| </div> |
| )} |
| |
| {/* FOOTER: 'Where it is made' functionality preservation */} |
| <footer className="w-full py-16 border-t border-white/5 mt-20"> |
| <div className="max-w-7xl mx-auto px-6 flex flex-col items-center space-y-8"> |
| <div className="text-3xl font-display text-gold-accent">Symbol Art</div> |
| <div className="font-body text-gray-500 italic">"Where Logic Meets Emotion"</div> |
| <div className="flex gap-8"> |
| <a |
| href="https://www.linkedin.com/in/rizulgarg2159" |
| target="_blank" |
| rel="noopener noreferrer" |
| className="text-gray-400 hover:text-gold-accent transition-colors" |
| > |
| <Linkedin className="w-6 h-6" /> |
| </a> |
| </div> |
| {/* Preserved attribution */} |
| <p className="text-sm text-gray-600 font-display tracking-widest uppercase mt-8"> |
| Created by Rizul Garg |
| </p> |
| </div> |
| </footer> |
| </div> |
| ); |
|
|
| return ( |
| <div className="bg-midnight text-white min-h-screen"> |
| {renderNav()} |
| {renderSection()} |
| </div> |
| ); |
| }; |
|
|
| export default App; |
|
|