OpenMAIC-React / src /components /access-code-modal.tsx
muthuk1's picture
Rebrand: OpenMAIC → MultiMind Classroom — rename in all source, DB name, cookie name, zip extension, prompts, docs, skills
ed07c96 verified
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>
);
}