| import { useState, useRef, ChangeEvent } from "react"; |
| import { Download, Play, FileUp, Sparkles, Cpu, CheckCircle, RefreshCw, FileText, Image as ImageIcon, Video, Music } from "lucide-react"; |
| import { motion, AnimatePresence } from "motion/react"; |
|
|
| interface HeroProps { |
| downloadUrl: string; |
| } |
|
|
| const PRESET_FILES = [ |
| { name: "quarterly_report.docx", size: "2.4 MB", type: "document" }, |
| { name: "product_photo.png", size: "8.1 MB", type: "image" }, |
| { name: "marketing_pitch.mp4", size: "45.2 MB", type: "video" }, |
| { name: "intro_music.mp3", size: "5.7 MB", type: "audio" }, |
| ]; |
|
|
| const CONVERSION_OPTIONS: Record<string, string[]> = { |
| document: ["PDF Document (.pdf)", "Markdown Text (.md)", "eBook (.epub)", "Web Page (.html)"], |
| image: ["WebP Image (.webp)", "JPEG Premium (.jpg)", "PNG Lossless (.png)", "PDF Page (.pdf)"], |
| video: ["MP4 Video (.mp4)", "GIF Animation (.gif)", "MP3 Audio (.mp3)", "AVI Format (.avi)"], |
| audio: ["WAV Lossless (.wav)", "AAC Advanced (.aac)", "MP3 Standard (.mp3)", "FLAC Studio (.flac)"], |
| }; |
|
|
| export default function Hero({ downloadUrl }: HeroProps) { |
| const [selectedFile, setSelectedFile] = useState<typeof PRESET_FILES[0] | null>(PRESET_FILES[1]); |
| const [targetFormat, setTargetFormat] = useState<string>("WebP Image (.webp)"); |
| const [isConverting, setIsConverting] = useState(false); |
| const [progress, setProgress] = useState(0); |
| const [gpuLoad, setGpuLoad] = useState(0); |
| const [isConverted, setIsConverted] = useState(false); |
| const [showNotification, setShowNotification] = useState(false); |
|
|
| |
| const [isDragOver, setIsDragOver] = useState(false); |
| const fileInputRef = useRef<HTMLInputElement>(null); |
|
|
| |
| const selectPreset = (file: typeof PRESET_FILES[0]) => { |
| if (isConverting) return; |
| setSelectedFile(file); |
| setIsConverted(false); |
| setProgress(0); |
| |
| const options = CONVERSION_OPTIONS[file.type]; |
| setTargetFormat(options[0]); |
| }; |
|
|
| |
| const handleConvert = () => { |
| if (!selectedFile || isConverting) return; |
| setIsConverting(true); |
| setIsConverted(false); |
| setProgress(0); |
|
|
| const interval = setInterval(() => { |
| setProgress((prev) => { |
| |
| setGpuLoad(Math.floor(Math.random() * 35) + 60); |
| if (prev >= 100) { |
| clearInterval(interval); |
| setGpuLoad(0); |
| setTimeout(() => { |
| setIsConverting(false); |
| setIsConverted(true); |
| setShowNotification(true); |
| setTimeout(() => setShowNotification(false), 4000); |
| }, 300); |
| return 100; |
| } |
| |
| const step = Math.floor(Math.random() * 15) + 8; |
| return Math.min(prev + step, 100); |
| }); |
| }, 150); |
| }; |
|
|
| const triggerScrollToFeatures = () => { |
| const el = document.getElementById("features"); |
| if (el) { |
| window.scrollTo({ |
| top: el.offsetTop - 85, |
| behavior: "smooth", |
| }); |
| } |
| }; |
|
|
| const handleCustomFileUpload = (e: ChangeEvent<HTMLInputElement>) => { |
| const file = e.target.files?.[0]; |
| if (file) { |
| const type = file.type.startsWith("image/") |
| ? "image" |
| : file.type.startsWith("video/") |
| ? "video" |
| : file.type.startsWith("audio/") |
| ? "audio" |
| : "document"; |
|
|
| const mappedFile = { |
| name: file.name, |
| size: `${(file.size / (1024 * 1024)).toFixed(1)} MB`, |
| type, |
| }; |
| setSelectedFile(mappedFile); |
| setIsConverted(false); |
| setProgress(0); |
| setTargetFormat(CONVERSION_OPTIONS[type][0]); |
| } |
| }; |
|
|
| const iconForType = (type: string) => { |
| switch (type) { |
| case "document": |
| return <FileText className="w-5 h-5 text-indigo-500" />; |
| case "image": |
| return <ImageIcon className="w-5 h-5 text-violet-500" />; |
| case "video": |
| return <Video className="w-5 h-5 text-fuchsia-500" />; |
| case "audio": |
| return <Music className="w-5 h-5 text-emerald-500" />; |
| default: |
| return <FileText className="w-5 h-5 text-slate-500" />; |
| } |
| }; |
|
|
| return ( |
| <section |
| id="home" |
| className="relative pt-32 pb-24 md:pt-40 md:pb-36 bg-gradient-to-b from-[#FAF9FE] via-[#FBFBFD] to-white overflow-hidden min-h-screen flex flex-col justify-center" |
| > |
| {/* Decorative Orbs */} |
| <div className="absolute top-1/4 left-1/10 w-96 h-96 bg-violet-200/40 rounded-full blur-3xl pointer-events-none" /> |
| <div className="absolute bottom-1/5 right-1/10 w-112 h-112 bg-indigo-200/30 rounded-full blur-3xl pointer-events-none" /> |
| |
| <div className="max-w-7xl mx-auto px-4 md:px-8 grid grid-cols-1 lg:grid-cols-12 gap-12 lg:gap-8 items-center relative z-10 w-full"> |
| {/* Left column: Text & CTAs */} |
| <div className="lg:col-span-6 flex flex-col justify-center text-center lg:text-left"> |
| {/* Badge */} |
| <div className="inline-flex items-center space-x-2 bg-violet-50 border border-violet-100 px-3.5 py-1.5 rounded-full mx-auto lg:mx-0 w-fit mb-6"> |
| <Sparkles className="w-3.5 h-3.5 text-violet-600 animate-pulse" /> |
| <span className="text-xs font-semibold text-violet-700 font-mono uppercase tracking-wider"> |
| v1.4.2 Released — Faster & Secure |
| </span> |
| </div> |
| |
| {/* Headline */} |
| <h1 className="font-display text-4xl sm:text-5xl lg:text-6xl font-bold text-slate-900 tracking-tight leading-[1.1] mb-6"> |
| Convert any file in{" "} |
| <span className="bg-gradient-to-r from-violet-600 via-indigo-600 to-fuchsia-600 bg-clip-text text-transparent"> |
| seconds |
| </span> |
| </h1> |
| |
| {/* Subtitle */} |
| <p className="text-slate-600 text-lg sm:text-xl font-light leading-relaxed max-w-2xl mx-auto lg:mx-0 mb-8"> |
| {`Lumina Convert delivers ultra-secure, offline-first conversion supporting 200+ formats without exporting raw data to the cloud. Empowered by full local GPU acceleration.`} |
| </p> |
| |
| {/* Direct CTA Buttons */} |
| <div className="flex flex-col sm:flex-row items-center justify-center lg:justify-start gap-4 mb-8"> |
| <a |
| href={downloadUrl} |
| download |
| id="hero-download-setup-btn" |
| className="w-full sm:w-auto inline-flex items-center justify-center space-x-3 px-8 py-4 rounded-full bg-slate-900 text-white font-medium shadow-lg shadow-slate-900/10 hover:bg-violet-600 hover:shadow-violet-500/30 active:scale-98 transition-all duration-300 cursor-pointer" |
| > |
| <Download className="w-5 h-5 animate-bounce" /> |
| <span className="text-base">Download for Windows</span> |
| </a> |
| <button |
| onClick={triggerScrollToFeatures} |
| id="hero-explore-features-btn" |
| className="w-full sm:w-auto inline-flex items-center justify-center space-x-2 px-8 py-4 rounded-full bg-white text-slate-700 border border-slate-200/80 font-medium hover:border-slate-300 hover:bg-slate-50 active:scale-98 transition-all duration-300 cursor-pointer" |
| > |
| <span>Explore Features</span> |
| </button> |
| </div> |
| |
| {/* Extra metadata */} |
| <div className="flex items-center justify-center lg:justify-start space-x-6 text-slate-500 text-xs font-medium font-mono"> |
| <span className="flex items-center space-x-1.5 border-r border-slate-200 pr-6"> |
| <span className="w-2 h-2 rounded-full bg-emerald-500 animate-pulse" /> |
| <span>100% Offline Converter</span> |
| </span> |
| <span>Free Standard Version</span> |
| </div> |
| </div> |
| |
| {/* Right column: Beautiful Interactive Glassmorphic Convertible Console */} |
| <div className="lg:col-span-6 w-full flex justify-center"> |
| <div className="w-full max-w-lg rounded-3xl bg-white/70 backdrop-blur-xl border border-white/60 shadow-2xl shadow-slate-200/50 p-6 relative"> |
| <div className="absolute top-3 left-4 flex space-x-1.5"> |
| <span className="w-3 h-3 rounded-full bg-rose-400" /> |
| <span className="w-3 h-3 rounded-full bg-amber-400" /> |
| <span className="w-3 h-3 rounded-full bg-emerald-400" /> |
| </div> |
| <div className="text-center font-mono text-[11px] text-slate-400 mb-6"> |
| LuminaConvert_WebStudio_Lite |
| </div> |
| |
| {/* Test Presets bar */} |
| <div className="mb-4"> |
| <label className="block text-xs font-semibold text-slate-500 font-mono mb-2 uppercase tracking-wider"> |
| Select a test file structure |
| </label> |
| <div className="grid grid-cols-4 gap-2"> |
| {PRESET_FILES.map((file, idx) => ( |
| <button |
| key={idx} |
| onClick={() => selectPreset(file)} |
| id={`hero-preset-file-${idx}`} |
| className={`p-2 rounded-xl border text-center transition-all focus:outline-none cursor-pointer flex flex-col items-center justify-center ${ |
| selectedFile?.name === file.name |
| ? "bg-violet-50/60 border-violet-200 shadow-sm" |
| : "bg-stone-50/20 border-slate-100 hover:bg-slate-50" |
| }`} |
| > |
| {iconForType(file.type)} |
| <span className="text-[9px] font-medium text-slate-700 truncate w-full mt-1"> |
| {file.name.substring(0, 10)}... |
| </span> |
| </button> |
| ))} |
| </div> |
| </div> |
| |
| {/* Main converter box */} |
| <div |
| className={`bg-slate-50/50 rounded-2xl border-2 border-dashed p-6 transition-all relative ${ |
| isDragOver ? "border-violet-400 bg-violet-50/20" : "border-slate-200/60" |
| }`} |
| onDragOver={(e) => { |
| e.preventDefault(); |
| setIsDragOver(true); |
| }} |
| onDragLeave={() => setIsDragOver(false)} |
| onDrop={(e) => { |
| e.preventDefault(); |
| setIsDragOver(false); |
| const file = e.dataTransfer.files?.[0]; |
| if (file) { |
| const type = file.type.startsWith("image/") |
| ? "image" |
| : file.type.startsWith("video/") |
| ? "video" |
| : file.type.startsWith("audio/") |
| ? "audio" |
| : "document"; |
| |
| setSelectedFile({ |
| name: file.name, |
| size: `${(file.size / (1024 * 1024)).toFixed(1)} MB`, |
| type, |
| }); |
| setIsConverted(false); |
| setProgress(0); |
| setTargetFormat(CONVERSION_OPTIONS[type][0]); |
| } |
| }} |
| > |
| <input |
| type="file" |
| ref={fileInputRef} |
| onChange={handleCustomFileUpload} |
| className="hidden" |
| id="hero-hidden-file-input" |
| /> |
| |
| {selectedFile ? ( |
| <div className="space-y-4"> |
| <div className="flex items-center justify-between"> |
| <div className="flex items-center space-x-3"> |
| <div className="p-2.5 rounded-xl bg-white border border-slate-100 shadow-sm"> |
| {iconForType(selectedFile.type)} |
| </div> |
| <div className="text-left"> |
| <h4 className="text-sm font-semibold text-slate-800 line-clamp-1 max-w-[180px]"> |
| {selectedFile.name} |
| </h4> |
| <p className="text-xs text-slate-500 font-mono">{selectedFile.size}</p> |
| </div> |
| </div> |
| |
| <button |
| onClick={() => setSelectedFile(null)} |
| id="hero-remove-file-btn" |
| className="text-xs font-semibold text-rose-500 hover:text-rose-600 hover:underline cursor-pointer" |
| > |
| Remove |
| </button> |
| </div> |
| |
| {/* Target format picker */} |
| <div className="text-left bg-white border border-slate-100/80 p-3.5 rounded-xl shadow-sm mt-3"> |
| <span className="block text-[10px] font-mono text-slate-400 tracking-wider font-semibold uppercase mb-1"> |
| Target format |
| </span> |
| <select |
| value={targetFormat} |
| onChange={(e) => { |
| setTargetFormat(e.target.value); |
| setIsConverted(false); |
| setProgress(0); |
| }} |
| id="hero-target-format-select" |
| className="w-full text-sm font-semibold text-slate-700 bg-transparent focus:outline-none cursor-pointer" |
| > |
| {CONVERSION_OPTIONS[selectedFile.type].map((opt, i) => ( |
| <option key={i} value={opt}> |
| {opt} |
| </option> |
| ))} |
| </select> |
| </div> |
| </div> |
| ) : ( |
| <button |
| onClick={() => fileInputRef.current?.click()} |
| id="hero-upload-area-btn" |
| className="w-full h-full flex flex-col items-center justify-center p-6 focus:outline-none cursor-pointer" |
| > |
| <div className="w-12 h-12 rounded-2xl bg-white border border-slate-100 flex items-center justify-center text-violet-500 shadow-sm mb-3"> |
| <FileUp className="w-6 h-6" /> |
| </div> |
| <span className="text-sm font-medium text-slate-700">Drag & drop or browse locally</span> |
| <span className="text-xs text-slate-400 mt-1">supports pdf, png, mp4, docx, mp3</span> |
| </button> |
| )} |
| </div> |
| |
| {/* Actions / Progress tracking */} |
| <div className="mt-5 space-y-4"> |
| {isConverting && ( |
| <div className="space-y-2 p-4 rounded-xl bg-indigo-50/50 border border-indigo-100/50"> |
| <div className="flex items-center justify-between text-xs font-mono"> |
| <span className="flex items-center space-x-1.5 text-indigo-700"> |
| <RefreshCw className="w-3.5 h-3.5 animate-spin" /> |
| <span>Processing files...</span> |
| </span> |
| <span className="font-bold text-indigo-700">{progress}%</span> |
| </div> |
| <div className="w-full h-1.5 bg-indigo-100 rounded-full overflow-hidden"> |
| <div |
| className="h-full bg-gradient-to-r from-violet-600 to-indigo-600 rounded-full transition-all duration-150" |
| style={{ width: `${progress}%` }} |
| /> |
| </div> |
| <div className="flex items-center justify-between text-[10px] font-mono text-slate-400 pt-1"> |
| <span className="flex items-center space-x-1"> |
| <Cpu className="w-3 h-3 text-violet-500 animate-pulse" /> |
| <span>GPU Hardware: {gpuLoad}% (Local Core)</span> |
| </span> |
| <span>100% Offline safe</span> |
| </div> |
| </div> |
| )} |
| |
| {isConverted && ( |
| <motion.div |
| initial={{ opacity: 0, y: 10 }} |
| animate={{ opacity: 1, y: 0 }} |
| className="flex items-center justify-between p-4 rounded-2xl bg-emerald-50/80 border border-emerald-100 text-left" |
| > |
| <div className="flex items-center space-x-2.5"> |
| <CheckCircle className="w-5 h-5 text-emerald-600 flex-shrink-0" /> |
| <div> |
| <h5 className="text-xs font-semibold text-emerald-800 uppercase tracking-wider font-mono"> |
| Conversion Complete |
| </h5> |
| <p className="text-slate-600 text-sm font-medium truncate max-w-[200px]"> |
| {selectedFile?.name.split(".")[0] || ""}. |
| {targetFormat.match(/\(([^)]+)\)/)?.[1]?.replace(".", "") || "webp"} |
| </p> |
| </div> |
| </div> |
| <button |
| onClick={() => { |
| alert("Offline prototype converter success! The full offline desktop version is up to 200x faster for files up to 50GB. Redirecting to download full app package..."); |
| window.location.href = downloadUrl; |
| }} |
| id="hero-download-converted-mock-btn" |
| className="px-3.5 py-1.5 rounded-lg bg-emerald-600 text-white text-xs font-semibold hover:bg-emerald-700 transition" |
| > |
| Download |
| </button> |
| </motion.div> |
| )} |
| |
| <button |
| disabled={!selectedFile || isConverting} |
| onClick={handleConvert} |
| id="hero-action-convert-btn" |
| className={`w-full py-4 rounded-2xl font-semibold flex items-center justify-center space-x-2 border transition-all cursor-pointer ${ |
| !selectedFile || isConverting |
| ? "bg-slate-100 text-slate-400 border-slate-100 cursor-not-allowed" |
| : "bg-slate-900 border-slate-900 text-white shadow-md hover:bg-violet-600 hover:border-violet-600 active:scale-98" |
| }`} |
| > |
| <Play className="w-4 h-4 fill-current" /> |
| <span>Convert Locally Now</span> |
| </button> |
| </div> |
| </div> |
| </div> |
| </div> |
| |
| {/* Floating success toast notifier */} |
| <AnimatePresence> |
| {showNotification && ( |
| <motion.div |
| initial={{ opacity: 0, y: 50 }} |
| animate={{ opacity: 1, y: 0 }} |
| exit={{ opacity: 0, y: 50 }} |
| className="fixed bottom-6 right-6 z-50 bg-slate-950 text-white py-3.5 px-5 rounded-2xl flex items-center space-x-3 shadow-2xl border border-white/10" |
| > |
| <div className="w-2 h-2 rounded-full bg-emerald-400 animate-ping" /> |
| <span className="text-xs font-medium font-mono text-slate-200"> |
| ⚡ Local CUDA Core GPU optimization completed processing! |
| </span> |
| </motion.div> |
| )} |
| </AnimatePresence> |
| </section> |
| ); |
| } |
|
|