Buckets:
| import { useState, useRef, useEffect } from 'react'; | |
| import { motion, AnimatePresence } from 'motion/react'; | |
| import { useI18n } from '@/lib/hooks/use-i18n'; | |
| import { ArrowRight, ShieldCheck, LoaderCircle } from 'lucide-react'; | |
| interface AccessCodeModalProps { | |
| open: boolean; | |
| onSuccess: () => void; | |
| } | |
| export function AccessCodeModal({ open, onSuccess }: AccessCodeModalProps) { | |
| const { t } = useI18n(); | |
| const [code, setCode] = useState(''); | |
| const [error, setError] = useState(''); | |
| const [loading, setLoading] = useState(false); | |
| const [success, setSuccess] = useState(false); | |
| const inputRef = useRef<HTMLInputElement>(null); | |
| useEffect(() => { | |
| if (open && inputRef.current) { | |
| inputRef.current.focus(); | |
| } | |
| }, [open]); | |
| async function handleSubmit(e: React.FormEvent) { | |
| e.preventDefault(); | |
| if (!code || loading) return; | |
| setError(''); | |
| setLoading(true); | |
| try { | |
| const res = await fetch('/api/access-code/verify', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ code }), | |
| }); | |
| if (res.ok) { | |
| setSuccess(true); | |
| setTimeout(onSuccess, 600); | |
| } else { | |
| setError(t('accessCode.error')); | |
| setCode(''); | |
| inputRef.current?.focus(); | |
| } | |
| } catch { | |
| setError(t('accessCode.error')); | |
| } finally { | |
| setLoading(false); | |
| } | |
| } | |
| return ( | |
| <AnimatePresence> | |
| {open && ( | |
| <motion.div | |
| className="fixed inset-0 z-[200] flex items-center justify-center overflow-hidden" | |
| initial={{ opacity: 0 }} | |
| animate={{ opacity: 1 }} | |
| exit={{ opacity: 0, transition: { duration: 0.3 } }} | |
| > | |
| {/* Background — subtle mesh gradient */} | |
| <div className="absolute inset-0 bg-background"> | |
| <div | |
| className="absolute inset-0 opacity-30 dark:opacity-20" | |
| style={{ | |
| backgroundImage: ` | |
| radial-gradient(ellipse 80% 60% at 20% 40%, var(--primary) 0%, transparent 60%), | |
| radial-gradient(ellipse 60% 80% at 80% 20%, oklch(0.6 0.15 280) 0%, transparent 50%), | |
| radial-gradient(ellipse 50% 50% at 60% 80%, oklch(0.5 0.12 300) 0%, transparent 50%) | |
| `, | |
| }} | |
| /> | |
| {/* Subtle noise texture */} | |
| <div | |
| className="absolute inset-0 opacity-[0.03] dark:opacity-[0.05]" | |
| style={{ | |
| backgroundImage: `url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E")`, | |
| backgroundSize: '128px 128px', | |
| }} | |
| /> | |
| </div> | |
| {/* Content card */} | |
| <motion.div | |
| className="relative z-10 w-full max-w-sm mx-4" | |
| initial={{ opacity: 0, y: 20, scale: 0.96 }} | |
| animate={{ opacity: 1, y: 0, scale: 1 }} | |
| exit={{ opacity: 0, y: -20, scale: 0.96 }} | |
| transition={{ duration: 0.4, ease: [0.16, 1, 0.3, 1] }} | |
| > | |
| <div className="rounded-2xl border border-border/50 bg-card/80 p-8 shadow-xl shadow-black/5 backdrop-blur-xl dark:bg-card/60 dark:shadow-black/20"> | |
| {/* Icon */} | |
| <motion.div | |
| className="mx-auto mb-6 flex h-14 w-14 items-center justify-center rounded-full bg-primary/10" | |
| initial={{ scale: 0.5, opacity: 0 }} | |
| animate={{ scale: 1, opacity: 1 }} | |
| transition={{ delay: 0.15, duration: 0.4, ease: [0.16, 1, 0.3, 1] }} | |
| > | |
| <ShieldCheck className="h-7 w-7 text-primary" strokeWidth={1.5} /> | |
| </motion.div> | |
| {/* Title */} | |
| <motion.h1 | |
| className="mb-1 text-center text-lg font-semibold tracking-tight text-foreground" | |
| initial={{ opacity: 0, y: 8 }} | |
| animate={{ opacity: 1, y: 0 }} | |
| transition={{ delay: 0.2, duration: 0.4 }} | |
| > | |
| {t('accessCode.title')} | |
| </motion.h1> | |
| <motion.p | |
| className="mb-6 text-center text-sm text-muted-foreground" | |
| initial={{ opacity: 0 }} | |
| animate={{ opacity: 1 }} | |
| transition={{ delay: 0.25, duration: 0.4 }} | |
| > | |
| MultiMind Classroom | |
| </motion.p> | |
| {/* Form */} | |
| <motion.form | |
| onSubmit={handleSubmit} | |
| className="space-y-4" | |
| initial={{ opacity: 0, y: 8 }} | |
| animate={{ opacity: 1, y: 0 }} | |
| transition={{ delay: 0.3, duration: 0.4 }} | |
| > | |
| <div className="relative"> | |
| <input | |
| ref={inputRef} | |
| type="password" | |
| placeholder={t('accessCode.placeholder')} | |
| value={code} | |
| onChange={(e) => { | |
| setCode(e.target.value); | |
| if (error) setError(''); | |
| }} | |
| className={` | |
| w-full rounded-xl border bg-background/60 px-4 py-3 pr-12 text-sm | |
| outline-none transition-all duration-200 | |
| placeholder:text-muted-foreground/50 | |
| focus:border-primary/40 focus:ring-2 focus:ring-primary/10 | |
| ${error ? 'border-destructive/50 focus:border-destructive/50 focus:ring-destructive/10' : 'border-border/60'} | |
| `} | |
| disabled={loading || success} | |
| autoComplete="off" | |
| /> | |
| <button | |
| type="submit" | |
| disabled={!code || loading || success} | |
| className={` | |
| absolute right-2 top-1/2 -translate-y-1/2 flex h-8 w-8 items-center | |
| justify-center rounded-lg transition-all duration-200 | |
| ${code && !loading && !success ? 'bg-primary text-primary-foreground hover:opacity-90 cursor-pointer' : 'text-muted-foreground/30 cursor-default'} | |
| `} | |
| > | |
| {loading ? ( | |
| <LoaderCircle className="h-4 w-4 animate-spin" /> | |
| ) : success ? ( | |
| <motion.div | |
| initial={{ scale: 0 }} | |
| animate={{ scale: 1 }} | |
| transition={{ type: 'spring', stiffness: 300, damping: 20 }} | |
| > | |
| <ShieldCheck className="h-4 w-4 text-emerald-500" /> | |
| </motion.div> | |
| ) : ( | |
| <ArrowRight className="h-4 w-4" /> | |
| )} | |
| </button> | |
| </div> | |
| {/* Error message */} | |
| <AnimatePresence mode="wait"> | |
| {error && ( | |
| <motion.p | |
| className="text-center text-sm text-destructive" | |
| initial={{ opacity: 0, y: -4, height: 0 }} | |
| animate={{ opacity: 1, y: 0, height: 'auto' }} | |
| exit={{ opacity: 0, y: -4, height: 0 }} | |
| transition={{ duration: 0.2 }} | |
| > | |
| {error} | |
| </motion.p> | |
| )} | |
| </AnimatePresence> | |
| </motion.form> | |
| </div> | |
| </motion.div> | |
| </motion.div> | |
| )} | |
| </AnimatePresence> | |
| ); | |
| } | |
Xet Storage Details
- Size:
- 7.82 kB
- Xet hash:
- 7604fbae771a1e10b95d39caab2cd078c4d7ac9d8bc94c0fc32e41e30eac0116
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.