Spaces:
Running
Running
| import { useState, useRef, useEffect } from 'react'; | |
| import { motion, AnimatePresence } from 'framer-motion'; | |
| import { ShieldCheck, KeyRound, AlertTriangle } from 'lucide-react'; | |
| const STORAGE_KEY = 'fv_access_granted'; | |
| export function useAccessControl() { | |
| const [granted, setGranted] = useState<boolean>(() => { | |
| return sessionStorage.getItem(STORAGE_KEY) === 'true'; | |
| }); | |
| const grant = () => { | |
| sessionStorage.setItem(STORAGE_KEY, 'true'); | |
| setGranted(true); | |
| }; | |
| return { granted, grant }; | |
| } | |
| interface SecretGateProps { | |
| onGranted: () => void; | |
| } | |
| export default function SecretGate({ onGranted }: SecretGateProps) { | |
| const [code, setCode] = useState(''); | |
| const [error, setError] = useState(false); | |
| const [errorMessage, setErrorMessage] = useState(''); | |
| const [shake, setShake] = useState(false); | |
| const [loading, setLoading] = useState(false); | |
| const inputRef = useRef<HTMLInputElement>(null); | |
| useEffect(() => { | |
| inputRef.current?.focus(); | |
| }, []); | |
| const handleSubmit = async (e: React.FormEvent) => { | |
| e.preventDefault(); | |
| setLoading(true); | |
| setError(false); | |
| setErrorMessage(''); | |
| try { | |
| const apiUrl = import.meta.env.VITE_API_URL || 'http://localhost:8000'; | |
| const response = await fetch(`${apiUrl}/api/validate-code`, { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ code }), | |
| }); | |
| const data = await response.json(); | |
| if (response.ok && data.status === 'success') { | |
| onGranted(); | |
| } else { | |
| setError(true); | |
| setShake(true); | |
| setErrorMessage(data.detail || 'Invalid or expired code.'); | |
| setCode(''); | |
| setTimeout(() => setShake(false), 600); | |
| } | |
| } catch (err) { | |
| setError(true); | |
| setShake(true); | |
| setErrorMessage('Server connection failed.'); | |
| setTimeout(() => setShake(false), 600); | |
| } finally { | |
| setLoading(false); | |
| } | |
| }; | |
| return ( | |
| <div style={{ | |
| position: 'fixed', inset: 0, zIndex: 9999, | |
| background: 'radial-gradient(ellipse at 30% 20%, rgba(59,130,246,0.12) 0%, transparent 60%), radial-gradient(ellipse at 70% 80%, rgba(34,197,94,0.08) 0%, transparent 60%), var(--bg-0)', | |
| display: 'flex', alignItems: 'center', justifyContent: 'center', | |
| backdropFilter: 'blur(12px)', | |
| }}> | |
| {/* Ambient orbs */} | |
| <div style={{ position: 'absolute', top: '10%', left: '15%', width: 400, height: 400, background: 'rgba(59,130,246,0.06)', borderRadius: '50%', filter: 'blur(120px)', pointerEvents: 'none' }} /> | |
| <div style={{ position: 'absolute', bottom: '15%', right: '10%', width: 350, height: 350, background: 'rgba(34,197,94,0.05)', borderRadius: '50%', filter: 'blur(100px)', pointerEvents: 'none' }} /> | |
| <motion.div | |
| initial={{ opacity: 0, y: 40, scale: 0.96 }} | |
| animate={shake ? { x: [-12, 12, -8, 8, -4, 4, 0] } : { opacity: 1, y: 0, scale: 1 }} | |
| transition={shake ? { duration: 0.5 } : { duration: 0.5, ease: 'easeOut' }} | |
| style={{ | |
| width: '100%', maxWidth: '460px', margin: '0 24px', | |
| background: 'rgba(255,255,255,0.04)', | |
| border: error ? '1px solid rgba(239,68,68,0.4)' : '1px solid rgba(255,255,255,0.08)', | |
| borderRadius: '20px', | |
| padding: '48px 40px', | |
| backdropFilter: 'blur(24px)', | |
| boxShadow: error | |
| ? '0 0 0 1px rgba(239,68,68,0.2), 0 32px 80px rgba(0,0,0,0.5)' | |
| : '0 32px 80px rgba(0,0,0,0.5), 0 0 0 1px rgba(255,255,255,0.04)', | |
| transition: 'border-color 0.3s, box-shadow 0.3s', | |
| position: 'relative', | |
| }} | |
| > | |
| {/* Header */} | |
| <div style={{ textAlign: 'center', marginBottom: '36px' }}> | |
| <motion.div | |
| animate={{ rotate: [0, -5, 5, -3, 3, 0] }} | |
| transition={{ duration: 2, repeat: Infinity, repeatDelay: 4 }} | |
| style={{ | |
| width: 72, height: 72, borderRadius: '50%', | |
| background: 'linear-gradient(135deg, rgba(59,130,246,0.15), rgba(34,197,94,0.1))', | |
| border: '1px solid rgba(59,130,246,0.25)', | |
| display: 'flex', alignItems: 'center', justifyContent: 'center', | |
| margin: '0 auto 24px', | |
| }} | |
| > | |
| <KeyRound size={30} color="#60a5fa" /> | |
| </motion.div> | |
| <div style={{ | |
| display: 'inline-flex', alignItems: 'center', gap: 6, | |
| padding: '4px 14px', | |
| background: 'rgba(59,130,246,0.1)', | |
| border: '1px solid rgba(59,130,246,0.2)', | |
| borderRadius: 100, marginBottom: 20, | |
| }}> | |
| <ShieldCheck size={12} color="#3b82f6" /> | |
| <span style={{ fontSize: '0.72rem', fontWeight: 700, color: '#60a5fa', letterSpacing: '0.1em', textTransform: 'uppercase' }}> | |
| Restricted Access | |
| </span> | |
| </div> | |
| <h2 style={{ fontSize: '1.75rem', fontWeight: 800, marginBottom: 12, letterSpacing: '-0.03em' }} | |
| className="display-font"> | |
| Enter Access Code | |
| </h2> | |
| <p style={{ color: 'var(--text-3)', fontSize: '0.9rem', lineHeight: 1.7 }}> | |
| This platform is restricted to authorised personnel. Enter your secret access code to continue. | |
| </p> | |
| </div> | |
| {/* Form */} | |
| <form onSubmit={handleSubmit} style={{ display: 'flex', flexDirection: 'column', gap: 16 }}> | |
| <div style={{ position: 'relative' }}> | |
| <input | |
| ref={inputRef} | |
| type="password" | |
| value={code} | |
| onChange={e => { setCode(e.target.value); setError(false); }} | |
| placeholder="FV-ALPHA-101" | |
| autoComplete="off" | |
| disabled={loading} | |
| style={{ | |
| width: '100%', | |
| padding: '14px 18px', | |
| background: 'rgba(255,255,255,0.05)', | |
| border: error ? '1px solid rgba(239,68,68,0.6)' : '1px solid rgba(255,255,255,0.1)', | |
| borderRadius: 12, | |
| color: 'var(--text-1)', | |
| fontSize: '1rem', | |
| letterSpacing: '0.15em', | |
| outline: 'none', | |
| transition: 'border-color 0.2s', | |
| boxSizing: 'border-box', | |
| }} | |
| /> | |
| </div> | |
| <AnimatePresence> | |
| {error && ( | |
| <motion.div | |
| initial={{ opacity: 0, y: -8 }} | |
| animate={{ opacity: 1, y: 0 }} | |
| exit={{ opacity: 0 }} | |
| style={{ | |
| display: 'flex', alignItems: 'center', gap: 8, | |
| padding: '10px 14px', | |
| background: 'rgba(239,68,68,0.08)', | |
| border: '1px solid rgba(239,68,68,0.2)', | |
| borderRadius: 10, | |
| color: '#f87171', fontSize: '0.85rem', | |
| }} | |
| > | |
| <AlertTriangle size={15} /> | |
| {errorMessage} | |
| </motion.div> | |
| )} | |
| </AnimatePresence> | |
| <button | |
| type="submit" | |
| className="btn btn-secondary" | |
| disabled={loading} | |
| style={{ width: '100%', padding: '14px', fontSize: '0.95rem', fontWeight: 700, marginTop: 4, cursor: loading ? 'wait' : 'pointer' }} | |
| > | |
| {loading ? 'Verifying...' : 'Unlock Platform →'} | |
| </button> | |
| </form> | |
| <p style={{ textAlign: 'center', marginTop: 24, fontSize: '0.78rem', color: 'var(--text-3)' }}> | |
| Don't have an access code?{' '} | |
| <a href="mailto:oladeji.lawrence@gmail.com" style={{ color: '#60a5fa', textDecoration: 'none' }}> | |
| Request access → | |
| </a> | |
| </p> | |
| </motion.div> | |
| </div> | |
| ); | |
| } | |