| // src/components/layout/LoadingScreen.tsx | |
| import { useEffect, useState, useRef, useMemo } from "react"; | |
| import { motion, AnimatePresence } from "framer-motion"; | |
| import { | |
| Atom, FlaskConical, BookOpen, Microscope, Cpu, Sparkles, | |
| Brain, Rocket, Zap, Globe, Database, Cloud, Shield, | |
| Lock, Key, Fingerprint, QrCode, Scan, Eye, EyeOff, | |
| Loader2, RefreshCw, CheckCircle, XCircle, AlertCircle, | |
| Activity, BarChart, PieChart, TrendingUp, Users, MessageSquare, | |
| FileText, FolderOpen, Star, Heart, Share2, Download, Upload, | |
| Camera, Image, Video, Music, Code, Terminal, GitBranch, | |
| Coffee, Gift, PartyPopper, Rocket as RocketIcon | |
| } from "lucide-react"; | |
| interface LoadingScreenProps { | |
| onFinish?: () => void; | |
| duration?: number; | |
| skipAnimation?: boolean; | |
| backgroundImage?: string; | |
| theme?: "light" | "dark" | "auto"; | |
| } | |
| const LOADING_MESSAGES = [ | |
| { threshold: 0, message: "Initializing quantum circuits…", icon: Atom }, | |
| { threshold: 10, message: "Warping through research papers…", icon: BookOpen }, | |
| { threshold: 20, message: "Loading your digital lab…", icon: FlaskConical }, | |
| { threshold: 30, message: "Calibrating AI models…", icon: Brain }, | |
| { threshold: 40, message: "Fetching latest preprints…", icon: Rocket }, | |
| { threshold: 50, message: "Preparing your workspace…", icon: Sparkles }, | |
| { threshold: 60, message: "Analyzing research trends…", icon: TrendingUp }, | |
| { threshold: 70, message: "Connecting to research networks…", icon: Globe }, | |
| { threshold: 80, message: "Optimizing your experience…", icon: Activity }, | |
| { threshold: 90, message: "Almost ready…", icon: Zap }, | |
| ]; | |
| const QUOTES = [ | |
| { text: "The important thing is not to stop questioning.", author: "Albert Einstein" }, | |
| { text: "Research is creating new knowledge.", author: "Neil Armstrong" }, | |
| { text: "Curiosity is the engine of achievement.", author: "Ken Robinson" }, | |
| { text: "The joy of discovery is certainly the liveliest.", author: "Marie Curie" }, | |
| { text: "Imagination is more important than knowledge.", author: "Albert Einstein" }, | |
| { text: "Science is a way of thinking much more than it is a body of knowledge.", author: "Carl Sagan" }, | |
| { text: "The most beautiful thing we can experience is the mysterious.", author: "Albert Einstein" }, | |
| { text: "Nothing in life is to be feared, it is only to be understood.", author: "Marie Curie" }, | |
| { text: "The science of today is the technology of tomorrow.", author: "Edward Teller" }, | |
| { text: "Innovation distinguishes between a leader and a follower.", author: "Steve Jobs" }, | |
| { text: "The only source of knowledge is experience.", author: "Albert Einstein" }, | |
| { text: "Success is not final, failure is not fatal.", author: "Winston Churchill" }, | |
| { text: "The future belongs to those who believe in the beauty of their dreams.", author: "Eleanor Roosevelt" }, | |
| { text: "Do what you can, with what you have, where you are.", author: "Theodore Roosevelt" }, | |
| { text: "The secret of getting ahead is getting started.", author: "Mark Twain" }, | |
| ]; | |
| const PARTICLES_COUNT = 80; | |
| export function LoadingScreen({ | |
| onFinish, | |
| duration = 3000, | |
| skipAnimation = false, | |
| backgroundImage, | |
| theme = "auto" | |
| }: LoadingScreenProps) { | |
| const [progress, setProgress] = useState(0); | |
| const [currentQuote, setCurrentQuote] = useState(QUOTES[0]); | |
| const [showComplete, setShowComplete] = useState(false); | |
| const [particles, setParticles] = useState<Array<{ x: number; y: number; delay: number; duration: number; size: number; color?: string }>>([]); | |
| const animationFrameRef = useRef<number | undefined>(undefined); | |
| const startTimeRef = useRef<number | undefined>(undefined); | |
| const quoteIntervalRef = useRef<NodeJS.Timeout | undefined>(undefined); | |
| const [isDark, setIsDark] = useState(false); | |
| // Check theme | |
| useEffect(() => { | |
| if (theme === "dark") setIsDark(true); | |
| else if (theme === "light") setIsDark(false); | |
| else if (theme === "auto") { | |
| setIsDark(window.matchMedia("(prefers-color-scheme: dark)").matches); | |
| const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)"); | |
| const handler = (e: MediaQueryListEvent) => setIsDark(e.matches); | |
| mediaQuery.addEventListener("change", handler); | |
| return () => mediaQuery.removeEventListener("change", handler); | |
| } | |
| }, [theme]); | |
| // Initialize particles | |
| useEffect(() => { | |
| const colors = ["#6366f1", "#ec4899", "#f59e0b", "#10b981", "#3b82f6"]; | |
| const newParticles = Array.from({ length: PARTICLES_COUNT }, () => ({ | |
| x: Math.random() * 100, | |
| y: Math.random() * 100, | |
| delay: Math.random() * 10, | |
| duration: Math.random() * 8 + 4, | |
| size: Math.random() * 4 + 1, | |
| color: colors[Math.floor(Math.random() * colors.length)], | |
| })); | |
| setParticles(newParticles); | |
| }, []); | |
| // Quote rotation | |
| useEffect(() => { | |
| let quoteIndex = 0; | |
| quoteIntervalRef.current = setInterval(() => { | |
| quoteIndex = (quoteIndex + 1) % QUOTES.length; | |
| setCurrentQuote(QUOTES[quoteIndex]); | |
| }, 4000); | |
| return () => { | |
| if (quoteIntervalRef.current) clearInterval(quoteIntervalRef.current); | |
| }; | |
| }, []); | |
| // Progress animation | |
| useEffect(() => { | |
| if (skipAnimation) { | |
| setProgress(100); | |
| setTimeout(() => { | |
| setShowComplete(true); | |
| setTimeout(() => onFinish?.(), 500); | |
| }, 500); | |
| return; | |
| } | |
| startTimeRef.current = performance.now(); | |
| const updateProgress = (now: number) => { | |
| const elapsed = now - startTimeRef.current!; | |
| const newProgress = Math.min(100, (elapsed / duration) * 100); | |
| setProgress(newProgress); | |
| if (newProgress < 100) { | |
| animationFrameRef.current = requestAnimationFrame(updateProgress); | |
| } else { | |
| setShowComplete(true); | |
| setTimeout(() => onFinish?.(), 600); | |
| } | |
| }; | |
| animationFrameRef.current = requestAnimationFrame(updateProgress); | |
| return () => { | |
| if (animationFrameRef.current) cancelAnimationFrame(animationFrameRef.current); | |
| if (quoteIntervalRef.current) clearInterval(quoteIntervalRef.current); | |
| }; | |
| }, [duration, onFinish, skipAnimation]); | |
| const currentMessageData = useMemo(() => { | |
| for (let i = LOADING_MESSAGES.length - 1; i >= 0; i--) { | |
| if (progress >= LOADING_MESSAGES[i].threshold) return LOADING_MESSAGES[i]; | |
| } | |
| return LOADING_MESSAGES[0]; | |
| }, [progress]); | |
| const radius = 45; | |
| const circumference = 2 * Math.PI * radius; | |
| const strokeDashoffset = circumference - (progress / 100) * circumference; | |
| return ( | |
| <motion.div | |
| className="fixed inset-0 z-[9999] flex flex-col items-center justify-center overflow-hidden" | |
| initial={{ opacity: 0 }} | |
| animate={{ opacity: 1 }} | |
| exit={{ opacity: 0, scale: 1.02 }} | |
| transition={{ duration: 0.5 }} | |
| > | |
| {/* Background */} | |
| {backgroundImage ? ( | |
| <div className="absolute inset-0 bg-cover bg-center bg-no-repeat" style={{ backgroundImage: `url(${backgroundImage})` }} /> | |
| ) : ( | |
| <div className="absolute inset-0 bg-gradient-to-br from-slate-950 via-indigo-950 to-purple-950"> | |
| <div className="absolute inset-0 bg-[radial-gradient(ellipse_at_center,_var(--tw-gradient-stops))] from-primary/20 via-transparent to-transparent animate-pulse" /> | |
| <div className="absolute inset-0 bg-grid-pattern opacity-10" /> | |
| </div> | |
| )} | |
| {/* Animated Grid Pattern */} | |
| <div className="absolute inset-0 opacity-10"> | |
| <div className="absolute inset-0" style={{ backgroundImage: `url("data:image/svg+xml,%3Csvg width='60' height='60' viewBox='0 0 60 60' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='none' fillRule='evenodd'%3E%3Cg fill='%239C92AC' fillOpacity='0.4'%3E%3Cpath d='M36 34v-4h-2v4h-4v2h4v4h2v-4h4v-2h-4zm0-30V0h-2v4h-4v2h4v4h2V6h4V4h-4zM6 34v-4H4v4H0v2h4v4h2v-4h4v-2H6zM6 4V0H4v4H0v2h4v4h2V6h4V4H6z'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E")` }} /> | |
| </div> | |
| {/* Floating Particles */} | |
| <div className="absolute inset-0 overflow-hidden pointer-events-none"> | |
| {particles.map((particle, i) => ( | |
| <motion.div | |
| key={i} | |
| className="absolute rounded-full" | |
| style={{ | |
| left: `${particle.x}%`, | |
| top: `${particle.y}%`, | |
| width: particle.size, | |
| height: particle.size, | |
| backgroundColor: particle.color, | |
| boxShadow: `0 0 ${particle.size * 2}px ${particle.color}`, | |
| }} | |
| animate={{ | |
| y: [null, -20, 0], | |
| opacity: [0, 0.6, 0], | |
| scale: [1, 1.5, 1], | |
| }} | |
| transition={{ | |
| duration: particle.duration, | |
| repeat: Infinity, | |
| delay: particle.delay, | |
| ease: "easeInOut", | |
| }} | |
| /> | |
| ))} | |
| </div> | |
| {/* Main Content */} | |
| <div className="relative z-10 flex flex-col items-center max-w-md px-6 text-center"> | |
| {/* Logo Animation */} | |
| <div className="relative mb-8"> | |
| <motion.div | |
| animate={{ rotate: 360 }} | |
| transition={{ repeat: Infinity, duration: 12, ease: "linear" }} | |
| className="relative" | |
| > | |
| <Atom className="w-32 h-32 text-primary/90 drop-shadow-glow" strokeWidth={1.5} /> | |
| </motion.div> | |
| {/* Orbiting Particles */} | |
| {[...Array(6)].map((_, i) => ( | |
| <motion.div | |
| key={i} | |
| className="absolute top-1/2 left-1/2" | |
| style={{ x: "-50%", y: "-50%" }} | |
| animate={{ | |
| rotate: [0, 360], | |
| translateX: [0, 85, 0, -85, 0], | |
| translateY: [0, 55, 0, -55, 0], | |
| }} | |
| transition={{ | |
| duration: 5 + i, | |
| repeat: Infinity, | |
| ease: "linear", | |
| }} | |
| > | |
| {i === 0 && <Sparkles className="w-4 h-4 text-yellow-400 drop-shadow-glow-yellow" />} | |
| {i === 1 && <RocketIcon className="w-4 h-4 text-pink-400 drop-shadow-glow-pink" />} | |
| {i === 2 && <Brain className="w-4 h-4 text-purple-400 drop-shadow-glow-purple" />} | |
| {i === 3 && <Zap className="w-4 h-4 text-blue-400 drop-shadow-glow-blue" />} | |
| {i === 4 && <Star className="w-4 h-4 text-orange-400 drop-shadow-glow-orange" />} | |
| {i === 5 && <Heart className="w-4 h-4 text-red-400 drop-shadow-glow-red" />} | |
| </motion.div> | |
| ))} | |
| {/* Progress Circle */} | |
| <svg className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-36 h-36 -rotate-90"> | |
| <circle cx="72" cy="72" r={radius} fill="none" stroke="rgba(255,255,255,0.1)" strokeWidth="5" /> | |
| <motion.circle | |
| cx="72" cy="72" r={radius} fill="none" stroke="url(#gradient)" strokeWidth="5" strokeLinecap="round" | |
| strokeDasharray={circumference} strokeDashoffset={strokeDashoffset} | |
| initial={false} | |
| /> | |
| <defs> | |
| <linearGradient id="gradient" x1="0%" y1="0%" x2="100%" y2="100%"> | |
| <stop offset="0%" stopColor="#6366f1" /> | |
| <stop offset="30%" stopColor="#8b5cf6" /> | |
| <stop offset="60%" stopColor="#ec4899" /> | |
| <stop offset="100%" stopColor="#f59e0b" /> | |
| </linearGradient> | |
| </defs> | |
| </svg> | |
| {/* Inner Icon */} | |
| <motion.div | |
| className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2" | |
| animate={{ scale: [1, 1.1, 1], rotate: [0, 360] }} | |
| transition={{ duration: 3, repeat: Infinity, ease: "easeInOut" }} | |
| > | |
| {currentMessageData.icon && <currentMessageData.icon className="w-10 h-10 text-white/80 drop-shadow-glow" />} | |
| </motion.div> | |
| </div> | |
| {/* Title */} | |
| <motion.h1 | |
| className="text-4xl md:text-5xl font-extrabold bg-gradient-to-r from-primary via-secondary to-primary bg-clip-text text-transparent bg-300% animate-gradient mb-2" | |
| animate={{ backgroundPosition: ["0% 50%", "100% 50%", "0% 50%"] }} | |
| transition={{ repeat: Infinity, duration: 6, ease: "linear" }} | |
| > | |
| IE Research Platform | |
| </motion.h1> | |
| <motion.p | |
| className="text-sm text-indigo-200/70 mb-6" | |
| initial={{ opacity: 0 }} | |
| animate={{ opacity: 1 }} | |
| transition={{ delay: 0.5 }} | |
| > | |
| Where ideas transform into discoveries | |
| </motion.p> | |
| {/* Animated Message */} | |
| <AnimatePresence mode="wait"> | |
| <motion.div | |
| key={currentMessageData.message} | |
| initial={{ opacity: 0, y: 15 }} | |
| animate={{ opacity: 1, y: 0 }} | |
| exit={{ opacity: 0, y: -15 }} | |
| className="flex items-center gap-2 text-base text-indigo-200/80 font-mono" | |
| > | |
| <Loader2 className="w-4 h-4 animate-spin" /> | |
| <span>{currentMessageData.message}</span> | |
| </motion.div> | |
| </AnimatePresence> | |
| {/* Progress Bar */} | |
| <div className="w-80 mt-6"> | |
| <div className="flex justify-between text-xs text-indigo-300/60 mb-1"> | |
| <span>Loading System</span> | |
| <span className="font-mono">{Math.round(progress)}%</span> | |
| </div> | |
| <div className="h-2 bg-white/10 rounded-full overflow-hidden"> | |
| <motion.div | |
| className="h-full bg-gradient-to-r from-primary via-secondary to-primary bg-300% animate-gradient rounded-full" | |
| style={{ width: `${progress}%` }} | |
| transition={{ duration: 0.3 }} | |
| /> | |
| </div> | |
| </div> | |
| {/* Icons Row */} | |
| <div className="flex gap-6 mt-8 text-indigo-300/60"> | |
| {[FlaskConical, BookOpen, Microscope, Cpu, Globe, Database, Shield, Lock].map((Icon, idx) => ( | |
| <motion.div | |
| key={idx} | |
| animate={{ y: [0, -10, 0] }} | |
| transition={{ | |
| duration: 1.5, | |
| repeat: Infinity, | |
| delay: idx * 0.2, | |
| ease: "easeInOut", | |
| }} | |
| > | |
| <Icon className="w-5 h-5" /> | |
| </motion.div> | |
| ))} | |
| </div> | |
| {/* Quote */} | |
| <AnimatePresence mode="wait"> | |
| <motion.div | |
| key={currentQuote.text} | |
| initial={{ opacity: 0, y: 15 }} | |
| animate={{ opacity: 1, y: 0 }} | |
| exit={{ opacity: 0, y: -15 }} | |
| className="mt-8 max-w-sm" | |
| > | |
| <p className="text-xs text-indigo-200/50 italic leading-relaxed"> | |
| "{currentQuote.text}" | |
| </p> | |
| <p className="text-[10px] text-indigo-300/40 mt-2"> | |
| — {currentQuote.author} | |
| </p> | |
| </motion.div> | |
| </AnimatePresence> | |
| {/* Version Info */} | |
| <motion.div | |
| className="absolute bottom-6 left-0 right-0 text-center" | |
| initial={{ opacity: 0 }} | |
| animate={{ opacity: 0.5 }} | |
| transition={{ delay: 0.8 }} | |
| > | |
| <p className="text-[10px] text-indigo-400/40 font-mono"> | |
| Version 2.0.0 | Build 2026.06 | © IE Research Platform | |
| </p> | |
| </motion.div> | |
| </div> | |
| {/* Completion Animation Overlay */} | |
| <AnimatePresence> | |
| {showComplete && ( | |
| <motion.div | |
| className="absolute inset-0 flex items-center justify-center premium-glass bg-black/80 backdrop-blur-3xl z-20" | |
| initial={{ opacity: 0 }} | |
| animate={{ opacity: 1 }} | |
| exit={{ opacity: 0 }} | |
| > | |
| <motion.div | |
| className="text-center" | |
| initial={{ scale: 0.8, opacity: 0 }} | |
| animate={{ scale: 1, opacity: 1 }} | |
| transition={{ type: "spring", damping: 15 }} | |
| > | |
| <motion.div | |
| className="w-20 h-20 mx-auto mb-4 rounded-full bg-gradient-to-r from-primary to-secondary flex items-center justify-center" | |
| initial={{ scale: 0 }} | |
| animate={{ scale: 1 }} | |
| transition={{ type: "spring", delay: 0.2 }} | |
| > | |
| <PartyPopper className="w-12 h-12 text-white" /> | |
| </motion.div> | |
| <motion.h2 | |
| className="text-2xl font-bold text-white mb-2" | |
| initial={{ opacity: 0, y: 10 }} | |
| animate={{ opacity: 1, y: 0 }} | |
| transition={{ delay: 0.3 }} | |
| > | |
| Ready to Research! | |
| </motion.h2> | |
| <motion.p | |
| className="text-gray-300" | |
| initial={{ opacity: 0 }} | |
| animate={{ opacity: 1 }} | |
| transition={{ delay: 0.4 }} | |
| > | |
| Your workspace is ready | |
| </motion.p> | |
| </motion.div> | |
| </motion.div> | |
| )} | |
| </AnimatePresence> | |
| {/* CSS Styles */} | |
| <style>{` | |
| .drop-shadow-glow { | |
| filter: drop-shadow(0 0 10px rgba(99, 102, 241, 0.5)); | |
| } | |
| .drop-shadow-glow-yellow { | |
| filter: drop-shadow(0 0 8px rgba(245, 158, 11, 0.5)); | |
| } | |
| .drop-shadow-glow-pink { | |
| filter: drop-shadow(0 0 8px rgba(236, 72, 153, 0.5)); | |
| } | |
| .drop-shadow-glow-purple { | |
| filter: drop-shadow(0 0 8px rgba(139, 92, 246, 0.5)); | |
| } | |
| .drop-shadow-glow-blue { | |
| filter: drop-shadow(0 0 8px rgba(59, 130, 246, 0.5)); | |
| } | |
| .drop-shadow-glow-orange { | |
| filter: drop-shadow(0 0 8px rgba(249, 115, 22, 0.5)); | |
| } | |
| .drop-shadow-glow-red { | |
| filter: drop-shadow(0 0 8px rgba(239, 68, 68, 0.5)); | |
| } | |
| @keyframes gradient { | |
| 0%, 100% { background-position: 0% 50%; } | |
| 50% { background-position: 100% 50%; } | |
| } | |
| .animate-gradient { | |
| background-size: 300% 300%; | |
| animation: gradient 6s ease infinite; | |
| } | |
| .bg-grid-pattern { | |
| background-image: linear-gradient(rgba(99, 102, 241, 0.1) 1px, transparent 1px), | |
| linear-gradient(90deg, rgba(99, 102, 241, 0.1) 1px, transparent 1px); | |
| background-size: 50px 50px; | |
| } | |
| `}</style> | |
| </motion.div> | |
| ); | |
| } | |
| // Default export for lazy loading | |
| export default LoadingScreen; | |
| // Also export as named export for consistency | |
| export { LoadingScreen as LoadingScreenComponent }; |
Xet Storage Details
- Size:
- 18.5 kB
- Xet hash:
- 3af3081f240258c6739fda9c8cb27afff576838e546a53ab7e145d2a9e2a582b
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.