| import { useEffect, useState, useRef } from 'react'; |
| import { motion, AnimatePresence, useReducedMotion } from 'motion/react'; |
| import { Monitor, Shuffle, Server, Database, Zap, X, Gift, Lock, Sparkles } from 'lucide-react'; |
| import type { LandingAuthMode } from './LandingPage'; |
|
|
| type Feature = { icon: React.ElementType; title: string; desc: string; color: string; accent: string; image: string | null }; |
|
|
| |
| function DSAPreview({ tier }: { tier: 'free' | 'pro' }) { |
| const code = `def twoSum(nums, target):\n seen = {}\n for i, num in enumerate(nums):\n comp = target - num\n if comp in seen:\n return [seen[comp], i]\n seen[num] = i`; |
| const [typed, setTyped] = useState(''); |
| const [done, setDone] = useState(false); |
| useEffect(() => { |
| let i = 0; |
| const t = setInterval(() => { |
| i++; |
| setTyped(code.slice(0, i)); |
| if (i >= code.length) { clearInterval(t); setDone(true); } |
| }, 30); |
| return () => clearInterval(t); |
| }, []); |
| return ( |
| <div className="space-y-4"> |
| <div className="flex items-center gap-3 p-4 rounded-xl bg-white/5 border border-white/10"> |
| <span className="font-bold text-white flex items-center gap-2">Two Sum {tier === 'free' && <span className="text-xs font-normal text-slate-500 bg-black/50 px-2 py-0.5 rounded-full border border-white/10">200 Problems Available</span>}</span> |
| <span className="px-2 py-0.5 rounded-full bg-emerald-500/20 text-emerald-400 text-xs font-bold">Easy</span> |
| <div className={`px-2 py-0.5 rounded-full bg-orange-500/10 text-orange-400 text-xs flex items-center gap-1 ${tier === 'free' ? 'blur-sm opacity-50 relative' : ''}`}> |
| Amazon |
| {tier === 'free' && <Lock size={10} className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 text-white blur-none opacity-100" />} |
| </div> |
| </div> |
| <div className="rounded-xl bg-[#0d0d1a] border border-white/10 p-4 font-mono text-sm text-green-300 whitespace-pre min-h-[160px]">{typed}<span className="animate-pulse">β</span></div> |
| <AnimatePresence>{done && <motion.div initial={{ opacity: 0, y: 8 }} animate={{ opacity: 1, y: 0 }} className="inline-flex items-center gap-2 px-4 py-2 rounded-full bg-emerald-500/20 text-emerald-400 font-bold text-sm">β Accepted</motion.div>}</AnimatePresence> |
| </div> |
| ); |
| } |
|
|
| |
| function AITutorPreview() { |
| const [phase, setPhase] = useState(0); |
| const words = "A HashMap stores key-value pairs using a hash function. When you call put(key, value), Java computes hashCode() of the key, maps it to a bucket index. Average O(1) for get/put!".split(' '); |
| const [wordIdx, setWordIdx] = useState(0); |
| useEffect(() => { |
| const t1 = setTimeout(() => setPhase(1), 500); |
| const t2 = setTimeout(() => setPhase(2), 1400); |
| return () => { clearTimeout(t1); clearTimeout(t2); }; |
| }, []); |
| useEffect(() => { |
| if (phase !== 2) return; |
| if (wordIdx >= words.length) return; |
| const t = setTimeout(() => setWordIdx(w => w + 1), 80); |
| return () => clearTimeout(t); |
| }, [phase, wordIdx]); |
| return ( |
| <div className="rounded-xl bg-[#0d0d1a] border border-white/10 p-4 space-y-3 min-h-[200px]"> |
| <div className="flex justify-end"><div className="max-w-[75%] px-4 py-2 rounded-2xl rounded-tr-sm bg-blue-600 text-white text-sm">Explain how HashMap works in Java</div></div> |
| {phase >= 1 && ( |
| <div className="flex gap-2 items-start"> |
| <div className="w-7 h-7 rounded-full bg-gradient-to-br from-violet-500 to-purple-700 flex items-center justify-center text-[10px] font-black text-white shrink-0">AI</div> |
| {phase === 1 ? ( |
| <div className="flex gap-1 items-center px-4 py-3 rounded-2xl rounded-tl-sm border border-violet-500/30 bg-violet-500/10"> |
| {[0,1,2].map(i => <motion.div key={i} className="w-1.5 h-1.5 rounded-full bg-violet-400" animate={{ y: [0,-5,0] }} transition={{ duration: 0.6, repeat: Infinity, delay: i*0.15 }} />)} |
| </div> |
| ) : ( |
| <div className="px-4 py-2 rounded-2xl rounded-tl-sm border border-violet-500/30 bg-violet-500/10 text-slate-200 text-sm leading-relaxed">{words.slice(0, wordIdx).join(' ')}</div> |
| )} |
| </div> |
| )} |
| </div> |
| ); |
| } |
|
|
| |
| function QuestsPreview() { |
| const [xp, setXp] = useState(0); |
| const [coins, setCoins] = useState(0); |
| useEffect(() => { |
| const t = setInterval(() => { |
| setXp(v => Math.min(v + 2, 68)); |
| setCoins(v => Math.min(v + 10, 340)); |
| }, 30); |
| return () => clearInterval(t); |
| }, []); |
| const nodes = [ |
| { label: 'Arrays Basics', done: true }, { label: 'Sorting Algorithms', done: true }, |
| { label: 'Binary Search', active: true }, { label: 'Trees & Graphs', locked: true }, { label: 'Dynamic Programming', locked: true } |
| ]; |
| return ( |
| <div className="flex gap-6"> |
| <div className="flex flex-col items-center gap-0"> |
| {nodes.map((n, i) => ( |
| <div key={i} className="flex flex-col items-center"> |
| <motion.div initial={{ scale: 0 }} animate={{ scale: 1 }} transition={{ delay: i * 0.1 }} |
| className={`w-10 h-10 rounded-full flex items-center justify-center text-sm font-bold border-2 ${n.done ? 'bg-emerald-500/20 border-emerald-500 text-emerald-400' : n.active ? 'bg-amber-500/20 border-amber-400 text-amber-300' : 'bg-white/5 border-white/20 text-slate-500'}`}> |
| {n.done ? 'β' : n.active ? 'βΆ' : 'π'} |
| </motion.div> |
| {i < nodes.length - 1 && <div className={`w-0.5 h-8 ${n.done ? 'bg-emerald-500' : 'bg-white/10'}`} />} |
| </div> |
| ))} |
| </div> |
| <div className="flex-1 space-y-4"> |
| {nodes.map((n, i) => <div key={i} className={`text-sm font-medium ${n.done ? 'text-emerald-300' : n.active ? 'text-amber-300' : 'text-slate-500'}`}>{n.label}</div>)} |
| <div className="pt-4 border-t border-white/10"> |
| <div className="flex justify-between text-xs text-slate-400 mb-1"><span>XP Progress</span><span>{xp}%</span></div> |
| <div className="h-2 rounded-full bg-white/10"><motion.div className="h-full rounded-full bg-gradient-to-r from-amber-500 to-orange-400" style={{ width: `${xp}%` }} /></div> |
| <div className="mt-3 text-amber-400 font-bold text-sm">πͺ {coins} coins earned</div> |
| </div> |
| </div> |
| </div> |
| ); |
| } |
|
|
| |
| function DailyContestPreview() { |
| const [secs, setSecs] = useState(2843); |
| const [submitted, setSubmitted] = useState(false); |
| const [live, setLive] = useState(1247); |
| useEffect(() => { |
| const t = setInterval(() => setSecs(s => Math.max(0, s - 1)), 1000); |
| const t2 = setInterval(() => setLive(v => v + Math.floor(Math.random() * 3)), 2000); |
| return () => { clearInterval(t); clearInterval(t2); }; |
| }, []); |
| const h = Math.floor(secs / 3600), m = Math.floor((secs % 3600) / 60), s = secs % 60; |
| const fmt = (n: number) => String(n).padStart(2, '0'); |
| const lb = [['#1','user_ace','320','12:45'],['#2','coder99','295','14:20'],['#3','ryp_star','280','16:55'],['#4','devpro','240','18:30'],['#5','you','180','22:10']]; |
| const badges = ['π₯','π₯','π₯','','']; |
| return ( |
| <div className="space-y-4"> |
| <div className="flex items-center justify-between"> |
| <div className="font-mono text-2xl font-black text-white">{fmt(h)}:{fmt(m)}:{fmt(s)}</div> |
| <div className="text-xs text-emerald-400 animate-pulse">β {live.toLocaleString()} solving now</div> |
| </div> |
| <div className="rounded-xl overflow-hidden border border-white/10"> |
| {lb.map(([rank, user, score, time], i) => ( |
| <motion.div key={i} initial={{ opacity: 0, x: -10 }} animate={{ opacity: 1, x: 0 }} transition={{ delay: i * 0.08 }} |
| className="flex items-center gap-3 px-4 py-2.5 border-b border-white/5 text-sm hover:bg-white/5"> |
| <span className="w-8 text-slate-400">{badges[i] || rank}</span> |
| <span className="flex-1 font-medium text-white">{user}</span> |
| <span className="text-emerald-400 font-bold">{score}</span> |
| <span className="text-slate-500">{time}</span> |
| </motion.div> |
| ))} |
| </div> |
| <div className="flex justify-center"> |
| <button onClick={() => setSubmitted(true)} className="px-6 py-2 rounded-xl bg-blue-600 text-white font-bold text-sm hover:bg-blue-500 transition-colors">Submit Solution</button> |
| </div> |
| <AnimatePresence>{submitted && <motion.div initial={{ opacity: 0, scale: 0.8 }} animate={{ opacity: 1, scale: 1 }} exit={{ opacity: 0 }} className="text-center text-emerald-400 font-bold">β Accepted! +100 pts</motion.div>}</AnimatePresence> |
| </div> |
| ); |
| } |
|
|
| |
| function WeeklyArenaPreview() { |
| const prizes = [{ icon: 'π₯', label: '1st Place', reward: 'βΉ500 + Gold Badge' }, { icon: 'π₯', label: '2nd Place', reward: 'βΉ250' }, { icon: 'π₯', label: '3rd Place', reward: 'βΉ100' }]; |
| const pts = [[1,850],[2,600],[3,340],[4,120]]; |
| const maxR = 900; |
| const w = 260, h = 80, padX = 20, padY = 10; |
| const toX = (i: number) => padX + (i / (pts.length - 1)) * (w - 2 * padX); |
| const toY = (r: number) => padY + ((r - 120) / (maxR - 120)) * (h - 2 * padY); |
| const d = pts.map(([,r], i) => `${i === 0 ? 'M' : 'L'}${toX(i)},${toY(r)}`).join(' '); |
| return ( |
| <div className="space-y-4"> |
| <div className="grid grid-cols-3 gap-3"> |
| {prizes.map((p, i) => ( |
| <motion.div key={i} initial={{ opacity: 0, y: 12 }} animate={{ opacity: 1, y: 0 }} transition={{ delay: i * 0.1 }} |
| className="rounded-xl border border-amber-500/20 bg-amber-500/5 p-3 text-center"> |
| <div className="text-2xl mb-1">{p.icon}</div> |
| <div className="text-xs text-slate-400">{p.label}</div> |
| <div className="text-amber-400 font-bold text-xs mt-1">{p.reward}</div> |
| </motion.div> |
| ))} |
| </div> |
| <div className="rounded-xl bg-white/5 border border-white/10 p-4"> |
| <div className="text-xs text-slate-400 mb-2">Your Global Rank (4 weeks)</div> |
| <svg viewBox={`0 0 ${w} ${h}`} className="w-full"> |
| <motion.path d={d} fill="none" stroke="#10b981" strokeWidth="2" initial={{ pathLength: 0 }} animate={{ pathLength: 1 }} transition={{ duration: 1.2 }} /> |
| {pts.map(([wk, r], i) => <circle key={i} cx={toX(i)} cy={toY(r)} r="4" fill="#10b981" />)} |
| </svg> |
| <div className="flex items-center gap-2 mt-2"><span className="text-white font-bold">#342</span><span className="text-emerald-400 text-xs">β 18 places this week</span></div> |
| </div> |
| </div> |
| ); |
| } |
|
|
| |
| function CompilerPreview({ tier }: { tier: 'free' | 'pro' }) { |
| const [running, setRunning] = useState(false); |
| const [lines, setLines] = useState<string[]>([]); |
| const output = ['Hello, RYP!', 'Count: 0', 'Count: 1', 'Count: 2', 'Count: 3', 'Count: 4']; |
| const run = () => { |
| if (running) return; |
| setRunning(true); setLines([]); |
| output.forEach((line, i) => setTimeout(() => { |
| setLines(prev => [...prev, line]); |
| if (i === output.length - 1) setRunning(false); |
| }, 800 + i * 200)); |
| }; |
| const code = `print("Hello, RYP!")\nfor i in range(5):\n print(f"Count: {i}")`; |
| return ( |
| <div className="grid grid-cols-2 gap-3 min-h-[180px]"> |
| <div className="rounded-xl bg-[#0d0d1a] border border-white/10 p-3"> |
| <div className="flex items-center justify-between mb-2"> |
| <div className="flex gap-1 items-center bg-black/50 p-1 rounded-lg border border-white/10"> |
| {['Python', 'Java', 'C'].map(l => <span key={l} className={`px-2 py-0.5 text-[10px] font-bold rounded bg-white/10 ${l==='Python' ? 'text-white' : 'text-slate-400'}`}>{l}</span>)} |
| {['C++', 'Go', 'Rust'].map(l => ( |
| <span key={l} className={`px-2 py-0.5 text-[10px] font-bold rounded flex items-center gap-1 bg-white/5 text-slate-600 ${tier === 'free' ? 'blur-[1px]' : 'text-slate-400 blur-none'}`}> |
| {l} {tier === 'free' && <Lock size={8} />} |
| </span> |
| ))} |
| </div> |
| <button onClick={run} className="px-3 py-1 rounded-lg bg-emerald-600 text-white text-xs font-bold hover:bg-emerald-500 transition-colors"> |
| {running ? 'β³' : 'βΆ Run'} |
| </button> |
| </div> |
| <pre className="font-mono text-xs text-blue-300 whitespace-pre leading-5">{code}</pre> |
| |
| <div className="mt-3 flex justify-end pt-3 border-t border-white/10"> |
| <button className={`px-3 py-1.5 rounded-lg text-xs font-bold transition-all flex items-center gap-1.5 ${tier === 'free' ? 'bg-white/5 text-slate-500 cursor-not-allowed opacity-70' : 'bg-violet-600 hover:bg-violet-500 text-white shadow-[0_0_10px_rgba(139,92,246,0.5)]'}`}> |
| <Sparkles size={12} className={tier === 'free' ? 'text-slate-600' : 'text-amber-300'} /> |
| {tier === 'free' ? 'Analyze Code (Pro)' : 'Analyze Code (AI)'} |
| {tier === 'free' && <Lock size={12} />} |
| </button> |
| </div> |
| </div> |
| <div className="rounded-xl bg-black border border-white/10 p-3 font-mono text-xs"> |
| <div className="text-slate-500 mb-2">$ output</div> |
| {lines.map((l, i) => <motion.div key={i} initial={{ opacity: 0, x: -5 }} animate={{ opacity: 1, x: 0 }} className="text-green-400">$ {l}</motion.div>)} |
| {running && <motion.div animate={{ opacity: [1,0,1] }} transition={{ repeat: Infinity, duration: 0.8 }} className="text-slate-500">_</motion.div>} |
| </div> |
| </div> |
| ); |
| } |
|
|
| |
| function SystemDesignPreview({ tier }: { tier: 'free' | 'pro' }) { |
| const boxes = [ |
| { label: 'Client', Icon: Monitor, x: 20, y: 80 }, |
| { label: 'Load Balancer', Icon: Shuffle, x: 160, y: 80 }, |
| { label: 'API Server 1', Icon: Server, x: 300, y: 30 }, |
| { label: 'API Server 2', Icon: Server, x: 300, y: 130 }, |
| { label: 'Redis Cache', Icon: Zap, x: 440, y: 30 }, |
| { label: 'PostgreSQL', Icon: Database, x: 440, y: 130 }, |
| ]; |
| const arrows = [[0,1],[1,2],[1,3],[2,4],[3,5]]; |
| return ( |
| <div> |
| <svg viewBox="0 0 560 190" className="w-full"> |
| {arrows.map(([a,b], i) => { |
| const from = boxes[a], to = boxes[b]; |
| return <motion.line key={i} x1={from.x+70} y1={from.y+18} x2={to.x} y2={to.y+18} stroke="rgba(99,102,241,0.5)" strokeWidth="1.5" initial={{ pathLength: 0 }} animate={{ pathLength: 1 }} transition={{ delay: i*0.15, duration: 0.4 }} />; |
| })} |
| {boxes.map((box, i) => ( |
| <motion.g key={i} initial={{ opacity: 0, scale: 0.5 }} animate={{ opacity: 1, scale: 1 }} transition={{ delay: i*0.12 }}> |
| <rect x={box.x} y={box.y} width="70" height="36" rx="8" fill="rgba(99,102,241,0.15)" stroke="rgba(99,102,241,0.4)" strokeWidth="1.5"/> |
| <text x={box.x+35} y={box.y+22} textAnchor="middle" fill="#a5b4fc" fontSize="8" fontFamily="Inter,sans-serif" fontWeight="600">{box.label}</text> |
| </motion.g> |
| ))} |
| </svg> |
| <div className="text-center text-xs text-slate-500 mt-2">HLD: URL Shortener System</div> |
| |
| <div className="mt-4 pt-4 border-t border-white/10 space-y-2"> |
| <div className="text-xs font-bold text-slate-400 mb-2 uppercase tracking-wider">Available Modules</div> |
| {['01. Client-Server Model', '02. Network Protocols', '03. Load Balancing', '04. Database Scaling'].map(m => ( |
| <div key={m} className="px-3 py-2 rounded-lg bg-white/5 border border-white/10 text-xs text-slate-300">{m}</div> |
| ))} |
| {['05. Caching Strategies', '06. Message Queues', '07. Microservices'].map(m => ( |
| <div key={m} className={`px-3 py-2 rounded-lg flex items-center justify-between text-xs ${tier === 'free' ? 'bg-black/30 border border-white/5 text-slate-600 blur-[1px]' : 'bg-white/5 border border-white/10 text-slate-300'}`}> |
| <span>{m}</span> |
| {tier === 'free' && <Lock size={12} className="text-slate-500" />} |
| </div> |
| ))} |
| </div> |
| </div> |
| ); |
| } |
|
|
| |
| function CSFundamentalsPreview({ tier }: { tier: 'free' | 'pro' }) { |
| const [tab, setTab] = useState('OS'); |
| const cards: Record<string, { q: string; a: string }> = { |
| OS: { q: 'What is a deadlock?', a: 'A state where processes wait for each other\'s resources indefinitely. Conditions: Mutual Exclusion, Hold & Wait, No Preemption, Circular Wait.' }, |
| DBMS: { q: 'What is ACID?', a: 'Atomicity, Consistency, Isolation, Durability β properties guaranteeing reliable DB transactions.' }, |
| CN: { q: 'TCP vs UDP?', a: 'TCP: reliable, ordered, connection-based. UDP: fast, connectionless, no guarantee.' }, |
| }; |
| return ( |
| <div className="space-y-4"> |
| <div className="flex gap-2"> |
| {['OS','DBMS','CN'].map(t => ( |
| <button key={t} onClick={() => setTab(t)} className={`px-4 py-1.5 rounded-full text-xs font-bold transition-all ${tab===t ? 'bg-violet-500/20 text-violet-300 border border-violet-500/40' : 'text-slate-400 hover:text-white border border-white/10'}`}>{t}</button> |
| ))} |
| </div> |
| <AnimatePresence mode="wait"> |
| <motion.div key={tab} initial={{ opacity: 0, y: 8 }} animate={{ opacity: 1, y: 0 }} exit={{ opacity: 0, y: -8 }} transition={{ duration: 0.2 }} |
| className="rounded-xl border border-white/10 bg-white/5 p-5 space-y-3"> |
| <div className="text-sm font-bold text-white">{cards[tab].q}</div> |
| <div className="text-sm text-slate-300 leading-relaxed">{cards[tab].a}</div> |
| <div className="flex items-center gap-2 pt-2 border-t border-white/10"> |
| <div className="h-1 flex-1 rounded-full bg-white/10"><div className="h-full w-[4%] rounded-full bg-violet-500" /></div> |
| <span className="text-xs text-slate-500">Card 1 of 24</span> |
| </div> |
| </motion.div> |
| </AnimatePresence> |
| |
| <div className="mt-4 pt-4 border-t border-white/10 space-y-2"> |
| <div className="text-xs font-bold text-slate-400 mb-2 uppercase tracking-wider">Available Modules</div> |
| {['01. OS Basics', '02. Process Management', '03. Memory Management', '04. File Systems'].map(m => ( |
| <div key={m} className="px-3 py-2 rounded-lg bg-white/5 border border-white/10 text-xs text-slate-300">{m}</div> |
| ))} |
| {['05. Concurrency', '06. Deadlocks Deep Dive', '07. Networking Layers'].map(m => ( |
| <div key={m} className={`px-3 py-2 rounded-lg flex items-center justify-between text-xs ${tier === 'free' ? 'bg-black/30 border border-white/5 text-slate-600 blur-[1px]' : 'bg-white/5 border border-white/10 text-slate-300'}`}> |
| <span>{m}</span> |
| {tier === 'free' && <Lock size={12} className="text-slate-500" />} |
| </div> |
| ))} |
| </div> |
| </div> |
| ); |
| } |
|
|
| |
| function AptitudePreview({ tier }: { tier: 'free' | 'pro' }) { |
| const [secs, setSecs] = useState(30); |
| const [selected, setSelected] = useState<string | null>(null); |
| const [score, setScore] = useState(0); |
| const opts = ['A) 20 m/s','B) 25 m/s','C) 30 m/s','D) 35 m/s']; |
| const correct = 'B) 25 m/s'; |
| useEffect(() => { |
| if (selected) return; |
| const t = setInterval(() => setSecs(s => Math.max(0, s - 1)), 1000); |
| return () => clearInterval(t); |
| }, [selected]); |
| const pick = (opt: string) => { |
| if (selected) return; |
| setSelected(opt); |
| if (opt === correct) setScore(10); |
| }; |
| return ( |
| <div className="space-y-4"> |
| <div className="flex items-center justify-between"> |
| <div className="text-xs text-slate-400">β± {secs}s remaining</div> |
| <motion.div animate={score > 0 ? { scale: [1,1.3,1] } : {}} className="text-sm font-bold text-amber-400">Score: {score > 0 ? `+${score}` : '0'}</motion.div> |
| </div> |
| <div className="text-sm font-medium text-white leading-relaxed">If a train travels 360 km in 4 hours, what is its speed in m/s?</div> |
| <div className="grid grid-cols-2 gap-2"> |
| {opts.map(opt => { |
| const isCorrect = opt === correct; |
| const isSelected = selected === opt; |
| let cls = 'border border-white/10 bg-white/5 text-slate-300 hover:border-white/30'; |
| if (selected) cls = isCorrect ? 'border-emerald-500 bg-emerald-500/20 text-emerald-300 font-bold' : isSelected ? 'border-red-500 bg-red-500/10 text-red-400' : 'border-white/5 bg-white/[0.02] text-slate-600'; |
| return <button key={opt} onClick={() => pick(opt)} className={`px-4 py-2.5 rounded-xl text-xs text-left transition-all ${cls}`}>{opt} {selected && isCorrect ? 'β' : ''}</button>; |
| })} |
| </div> |
| <AnimatePresence>{selected && <motion.div initial={{ opacity:0, height:0 }} animate={{ opacity:1, height:'auto' }} className="text-xs text-slate-300 bg-white/5 rounded-xl p-3 border border-white/10">π‘ 360 km/4h = 90 km/h = 90 Γ (1000/3600) = <span className="text-emerald-400 font-bold">25 m/s</span></motion.div>}</AnimatePresence> |
| |
| <div className="mt-4 pt-4 border-t border-white/10 space-y-2"> |
| <div className="text-xs font-bold text-slate-400 mb-2 uppercase tracking-wider">Available Modules</div> |
| {['01. Time & Distance', '02. Percentages', '03. Profit & Loss', '04. Simple Interest'].map(m => ( |
| <div key={m} className="px-3 py-2 rounded-lg bg-white/5 border border-white/10 text-xs text-slate-300">{m}</div> |
| ))} |
| {['05. Permutations', '06. Probability', '07. Logical Reasoning'].map(m => ( |
| <div key={m} className={`px-3 py-2 rounded-lg flex items-center justify-between text-xs ${tier === 'free' ? 'bg-black/30 border border-white/5 text-slate-600 blur-[1px]' : 'bg-white/5 border border-white/10 text-slate-300'}`}> |
| <span>{m}</span> |
| {tier === 'free' && <Lock size={12} className="text-slate-500" />} |
| </div> |
| ))} |
| </div> |
| </div> |
| ); |
| } |
|
|
| |
| function SwagStorePreview({ tier }: { tier: 'free' | 'pro' }) { |
| const [coins, setCoins] = useState(0); |
| const items = [ |
| { name: 'RYP Pen', price: 100, icon: 'ποΈ', bg: 'bg-indigo-500/10' }, |
| { name: 'RYP T-Shirt', price: 1500, icon: 'π', bg: 'bg-blue-500/10' }, |
| { name: 'Sticker Pack', price: 300, icon: 'β¨', bg: 'bg-purple-500/10', premium: true }, |
| { name: 'Coffee Mug', price: 800, icon: 'β', bg: 'bg-orange-500/10', premium: true }, |
| ]; |
|
|
| useEffect(() => { |
| const t = setInterval(() => { |
| setCoins(prev => (prev >= 2000 ? 0 : prev + 50)); |
| }, 100); |
| return () => clearInterval(t); |
| }, []); |
|
|
| return ( |
| <div className="space-y-4"> |
| <div className="flex items-center justify-between p-4 rounded-xl border border-white/10 bg-white/5"> |
| <div className="text-sm text-slate-300">Your RYP Balance</div> |
| <div className="flex items-center gap-2"> |
| <motion.div animate={{ rotateY: 360 }} transition={{ duration: 2, repeat: Infinity, ease: 'linear' }} className="w-5 h-5 rounded-full bg-amber-400 border border-amber-500 shadow-[0_0_10px_rgba(251,191,36,0.5)] flex items-center justify-center"> |
| <span className="text-[10px] font-black text-amber-900">R</span> |
| </motion.div> |
| <span className="text-xl font-bold text-amber-400">{coins}</span> |
| </div> |
| </div> |
| <div className="grid grid-cols-2 sm:grid-cols-4 gap-3"> |
| {items.map((item, i) => { |
| const isLocked = item.premium && tier === 'free'; |
| return ( |
| <motion.div key={i} className={`p-4 rounded-xl border border-white/10 ${item.bg} flex flex-col items-center gap-2 relative overflow-hidden`}> |
| <div className={`text-3xl mb-1 ${isLocked ? 'blur-sm grayscale' : ''}`}>{item.icon}</div> |
| <div className={`text-sm font-bold text-center ${isLocked ? 'text-slate-500 blur-[1px]' : 'text-white'}`}>{item.name}</div> |
| <div className={`text-xs font-medium ${isLocked ? 'text-slate-600 blur-[1px]' : 'text-amber-400/80'}`}>{item.price} Coins</div> |
| |
| {!isLocked && coins >= item.price && ( |
| <motion.div initial={{ opacity: 0, y: 10 }} animate={{ opacity: 1, y: 0 }} className="absolute inset-x-0 bottom-0 py-1.5 bg-emerald-500/20 text-emerald-400 text-[10px] font-bold text-center border-t border-emerald-500/30"> |
| Redeemable! |
| </motion.div> |
| )} |
| {!isLocked && <motion.div className="absolute inset-0 bg-white/5 pointer-events-none" initial={{ top: '100%' }} animate={{ top: coins >= item.price ? '0%' : `${100 - (coins / item.price) * 100}%` }} transition={{ duration: 0.2 }} />} |
| |
| {isLocked && ( |
| <div className="absolute inset-0 bg-black/40 flex flex-col items-center justify-center backdrop-blur-[1px]"> |
| <Lock size={20} className="text-white/50 mb-1" /> |
| <span className="text-[10px] font-bold text-white/50">Pro Only</span> |
| </div> |
| )} |
| </motion.div> |
| ); |
| })} |
| </div> |
| </div> |
| ); |
| } |
|
|
| |
| const PREVIEW_MAP: Record<string, React.FC<{ tier: 'free' | 'pro' }>> = { |
| 'DSA Practice': DSAPreview, |
| 'RYP AI Tutor': AITutorPreview, |
| 'RYP Quests': QuestsPreview, |
| 'Daily Contest': DailyContestPreview, |
| 'Weekly Arena': WeeklyArenaPreview, |
| 'Online Compiler': CompilerPreview, |
| 'System Design': SystemDesignPreview, |
| 'CS Fundamentals': CSFundamentalsPreview, |
| 'Aptitude': AptitudePreview, |
| 'RYP Swag Store': SwagStorePreview, |
| }; |
|
|
| |
| interface FeaturePreviewModalProps { |
| feature: Feature | null; |
| onClose: () => void; |
| onGetStarted: (mode?: LandingAuthMode) => void; |
| } |
|
|
| export default function FeaturePreviewModal({ feature, onClose, onGetStarted }: FeaturePreviewModalProps) { |
| const reduced = useReducedMotion(); |
| const [tier, setTier] = useState<'free' | 'pro'>('free'); |
| const PreviewComponent = feature ? PREVIEW_MAP[feature.title] : null; |
|
|
| useEffect(() => { |
| if (!feature) return; |
| const handler = (e: KeyboardEvent) => { if (e.key === 'Escape') onClose(); }; |
| window.addEventListener('keydown', handler); |
| return () => window.removeEventListener('keydown', handler); |
| }, [feature, onClose]); |
|
|
| if (!feature) return null; |
|
|
| const panelAnim = reduced |
| ? {} |
| : { initial: { opacity: 0, scale: 0.92, y: 30 }, animate: { opacity: 1, scale: 1, y: 0 }, exit: { opacity: 0, scale: 0.95, y: 20 } }; |
|
|
| return ( |
| <div className="fixed inset-0 z-[100] flex items-center justify-center md:p-4" onClick={onClose}> |
| <div className="absolute inset-0 bg-black/80 backdrop-blur-xl" /> |
| <motion.div |
| {...panelAnim} |
| transition={{ duration: 0.35, ease: 'easeOut' }} |
| onClick={e => e.stopPropagation()} |
| className="relative w-full max-w-3xl mx-4 md:mx-auto bg-[#0a0a12] border border-white/10 rounded-t-[2rem] md:rounded-[2.5rem] shadow-[0_0_80px_rgba(0,0,0,0.8)] max-h-[85vh] md:max-h-[80vh] overflow-y-auto fixed bottom-0 md:relative md:bottom-auto" |
| > |
| {/* Close */} |
| <button onClick={onClose} className="absolute top-4 right-4 text-slate-400 hover:text-white text-2xl z-10 w-8 h-8 flex items-center justify-center rounded-full hover:bg-white/10 transition-colors">Γ</button> |
| |
| {/* Header with Toggle */} |
| <div className={`p-6 pb-4 border-b border-white/10 bg-gradient-to-r ${feature.color} bg-opacity-10`}> |
| <div className="flex justify-between items-start"> |
| <div> |
| <div className={`w-12 h-12 rounded-2xl flex items-center justify-center mb-3 bg-gradient-to-br ${feature.color} shadow-lg`}> |
| <feature.icon size={22} className="text-white" /> |
| </div> |
| <h3 className="text-xl font-black text-white">{feature.title}</h3> |
| <p className="text-slate-400 text-sm mt-1">{feature.desc}</p> |
| </div> |
| {/* Free vs Pro Toggle */} |
| <div className="flex items-center gap-1 bg-black/50 p-1 rounded-xl border border-white/10 shrink-0"> |
| <button |
| onClick={() => setTier('free')} |
| className={`px-3 py-1.5 rounded-lg text-xs font-bold transition-colors ${tier === 'free' ? 'bg-white/10 text-white' : 'text-slate-500 hover:text-slate-300'}`} |
| > |
| Free |
| </button> |
| <button |
| onClick={() => setTier('pro')} |
| className={`px-3 py-1.5 rounded-lg text-xs font-bold transition-all flex items-center gap-1 ${tier === 'pro' ? 'bg-gradient-to-r from-violet-500 to-fuchsia-500 text-white shadow-[0_0_10px_rgba(139,92,246,0.5)]' : 'text-slate-500 hover:text-slate-300'}`} |
| > |
| <Zap size={12} className={tier === 'pro' ? 'text-amber-300' : ''} /> Pro |
| </button> |
| </div> |
| </div> |
| </div> |
| |
| {/* Preview */} |
| <div className="p-6"> |
| {PreviewComponent ? <PreviewComponent tier={tier} /> : <div className="text-slate-400 text-sm">Preview coming soon.</div>} |
| </div> |
| |
| {/* CTA */} |
| <div className="px-6 pb-6"> |
| <button |
| onClick={() => { onClose(); onGetStarted('register'); }} |
| className="w-full py-3.5 rounded-xl bg-white text-black font-extrabold hover:scale-[1.02] transition-transform text-sm" |
| > |
| Try it Now β |
| </button> |
| </div> |
| </motion.div> |
| </div> |
| ); |
| } |
|
|