embedingHF's picture
Upload folder using huggingface_hub
84fc8e7 verified
Raw
History Blame Contribute Delete
19.2 kB
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]); // default to image
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);
// drag state
const [isDragOver, setIsDragOver] = useState(false);
const fileInputRef = useRef<HTMLInputElement>(null);
// handle preset selection
const selectPreset = (file: typeof PRESET_FILES[0]) => {
if (isConverting) return;
setSelectedFile(file);
setIsConverted(false);
setProgress(0);
// set first formatting option as default
const options = CONVERSION_OPTIONS[file.type];
setTargetFormat(options[0]);
};
// simulate conversion
const handleConvert = () => {
if (!selectedFile || isConverting) return;
setIsConverting(true);
setIsConverted(false);
setProgress(0);
const interval = setInterval(() => {
setProgress((prev) => {
// dynamic GPU metrics simulate load
setGpuLoad(Math.floor(Math.random() * 35) + 60); // 60-95% GPU load
if (prev >= 100) {
clearInterval(interval);
setGpuLoad(0);
setTimeout(() => {
setIsConverting(false);
setIsConverted(true);
setShowNotification(true);
setTimeout(() => setShowNotification(false), 4000);
}, 300);
return 100;
}
// accelerate progress conversion
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>
);
}