| import { useState, useEffect, type FormEvent } from 'react'; |
| import { motion, AnimatePresence } from 'motion/react'; |
| import { Eye, EyeOff } from 'lucide-react'; |
| import { AUTH_TOKEN_KEY, loginUser, registerUser, type AuthUser } from '../lib/authClient'; |
| import BrandLogo from './BrandLogo'; |
|
|
| type AuthMode = 'login' | 'register'; |
|
|
| type AuthPageProps = { |
| onLoggedIn: (user: AuthUser) => void; |
| initialMode?: AuthMode; |
| onBackToLanding?: () => void; |
| }; |
|
|
| export default function AuthPage({ |
| onLoggedIn, |
| initialMode = 'login', |
| onBackToLanding, |
| }: AuthPageProps) { |
| const [mode, setMode] = useState<AuthMode>(initialMode); |
| const [name, setName] = useState(''); |
| const [email, setEmail] = useState(''); |
| const [password, setPassword] = useState(''); |
| const [confirmPassword, setConfirmPassword] = useState(''); |
| const [showPassword, setShowPassword] = useState(false); |
| const [error, setError] = useState(''); |
| const [loading, setLoading] = useState(false); |
|
|
| useEffect(() => { |
| setMode(initialMode); |
| setError(''); |
| }, [initialMode]); |
|
|
| const isLogin = mode === 'login'; |
|
|
| const handleSubmit = async (event: FormEvent<HTMLFormElement>) => { |
| event.preventDefault(); |
| setLoading(true); |
| setError(''); |
|
|
| if (!email || !password) { |
| setError('Please fill in all required fields.'); |
| setLoading(false); |
| return; |
| } |
|
|
| if (!isLogin) { |
| if (!name.trim()) { |
| setError('Please enter your full name.'); |
| setLoading(false); |
| return; |
| } |
| if (password !== confirmPassword) { |
| setError('Passwords do not match.'); |
| setLoading(false); |
| return; |
| } |
| } |
|
|
| if (password.length < 6) { |
| setError('Password must be at least 6 characters long.'); |
| setLoading(false); |
| return; |
| } |
|
|
| try { |
| if (isLogin) { |
| const { token, user } = await loginUser(email, password); |
| localStorage.setItem(AUTH_TOKEN_KEY, token); |
| window.setTimeout(() => onLoggedIn(user), 450); |
| } else { |
| const { token, user } = await registerUser(email, password, name.trim()); |
| localStorage.setItem(AUTH_TOKEN_KEY, token); |
| window.setTimeout(() => onLoggedIn(user), 450); |
| } |
| } catch (err: unknown) { |
| const message = err instanceof Error ? err.message : 'Something went wrong.'; |
| setError(message); |
| } finally { |
| setLoading(false); |
| } |
| }; |
|
|
| return ( |
| <div className="relative flex h-screen items-center justify-center overflow-hidden bg-[#05070b] text-white"> |
| {/* Background elements */} |
| <div className="pointer-events-none absolute inset-0 bg-[radial-gradient(circle_at_top_left,rgba(56,189,248,0.05),transparent_26%),radial-gradient(circle_at_top_right,rgba(249,115,22,0.03),transparent_20%),radial-gradient(circle_at_bottom_left,rgba(99,102,241,0.04),transparent_22%),linear-gradient(135deg,#040507_0%,#07090d_50%,#050608_100%)]" /> |
| <div className="pointer-events-none absolute inset-0 bg-[linear-gradient(rgba(148,163,184,0.02)_1px,transparent_1px),linear-gradient(90deg,rgba(148,163,184,0.02)_1px,transparent_1px)] bg-[size:120px_120px] opacity-20" /> |
| <CircuitCorner className="right-0 top-0 hidden text-slate-300/10 sm:block" /> |
| <CircuitCorner className="bottom-0 left-0 hidden -scale-x-100 -scale-y-100 text-slate-300/10 sm:block" /> |
| |
| {/* Box */} |
| <motion.div |
| initial={{ opacity: 0, y: 24, scale: 0.98 }} |
| animate={{ opacity: 1, y: 0, scale: 1 }} |
| transition={{ duration: 0.45, ease: 'easeOut' }} |
| className="relative z-10 flex w-full max-w-[420px] flex-col items-center rounded-[16px] border border-white/5 bg-[#0c0c0c] p-8 shadow-2xl sm:p-10" |
| > |
| {/* Logo */} |
| <div className="mb-6 flex justify-center"> |
| <BrandLogo |
| size="md" |
| title="Reach Your Potential" |
| subtitle="Code. Practice. Grow." |
| /> |
| </div> |
| |
| {/* Toggle */} |
| <div className="mb-6 grid w-full grid-cols-2 gap-2 rounded-[12px] border border-white/5 bg-[#151515] p-1"> |
| <button |
| type="button" |
| onClick={() => { setMode('login'); setError(''); setConfirmPassword(''); }} |
| className={`rounded-[10px] py-2 text-[13px] font-semibold transition-colors ${isLogin ? 'bg-[#172554] text-[#3b82f6]' : 'text-gray-500 hover:text-white' |
| }`} |
| > |
| Sign In |
| </button> |
| <button |
| type="button" |
| onClick={() => { setMode('register'); setError(''); }} |
| className={`rounded-[10px] py-2 text-[13px] font-semibold transition-colors ${!isLogin ? 'bg-[#172554] text-[#3b82f6]' : 'text-gray-500 hover:text-white' |
| }`} |
| > |
| Create Account |
| </button> |
| </div> |
| |
| {/* Welcome text */} |
| <h2 className="mb-6 text-center text-[18px] font-bold text-white sm:text-[20px]"> |
| {isLogin ? "Welcome back. Let's get started." : 'Join us. Create your account.'} |
| </h2> |
| |
| {/* Form */} |
| <form className="w-full" onSubmit={handleSubmit}> |
| <div className="flex flex-col gap-3"> |
| <AnimatePresence initial={false}> |
| {!isLogin && ( |
| <motion.div |
| initial={{ opacity: 0, height: 0 }} |
| animate={{ opacity: 1, height: 'auto' }} |
| exit={{ opacity: 0, height: 0 }} |
| transition={{ duration: 0.2 }} |
| className="overflow-hidden" |
| > |
| <div> |
| <input |
| type="text" |
| placeholder="Full name" |
| value={name} |
| onChange={(e) => setName(e.target.value)} |
| className="w-full rounded-md border border-transparent bg-[#1a1a1a] px-4 py-3 text-[14px] text-white placeholder-gray-500 outline-none transition-colors focus:border-white/10 focus:bg-[#222]" |
| /> |
| </div> |
| </motion.div> |
| )} |
| </AnimatePresence> |
| |
| <input |
| type="email" |
| placeholder="example@gmail.com" |
| value={email} |
| onChange={(e) => setEmail(e.target.value)} |
| className="w-full rounded-md border border-transparent bg-[#1a1a1a] px-4 py-3 text-[14px] text-white placeholder-gray-500 outline-none transition-colors focus:border-white/10 focus:bg-[#222]" |
| /> |
| |
| <div> |
| <div className="relative"> |
| <input |
| type={showPassword ? 'text' : 'password'} |
| placeholder="Password" |
| value={password} |
| onChange={(e) => setPassword(e.target.value)} |
| className="w-full rounded-md border border-transparent bg-[#1a1a1a] py-3 pl-4 pr-10 text-[14px] text-white placeholder-gray-500 outline-none transition-colors focus:border-white/10 focus:bg-[#222]" |
| /> |
| <button |
| type="button" |
| onClick={() => setShowPassword(!showPassword)} |
| className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-500 transition-colors hover:text-gray-300" |
| > |
| {showPassword ? <EyeOff size={16} /> : <Eye size={16} />} |
| </button> |
| </div> |
| </div> |
| |
| <AnimatePresence initial={false}> |
| {!isLogin && ( |
| <motion.div |
| initial={{ opacity: 0, height: 0 }} |
| animate={{ opacity: 1, height: 'auto' }} |
| exit={{ opacity: 0, height: 0 }} |
| transition={{ duration: 0.2 }} |
| className="overflow-hidden" |
| > |
| <div className="pb-1"> |
| <div className="relative"> |
| <input |
| type={showPassword ? 'text' : 'password'} |
| placeholder="Retype password" |
| value={confirmPassword} |
| onChange={(e) => setConfirmPassword(e.target.value)} |
| className="w-full rounded-md border border-transparent bg-[#1a1a1a] py-3 pl-4 pr-10 text-[14px] text-white placeholder-gray-500 outline-none transition-colors focus:border-white/10 focus:bg-[#222]" |
| /> |
| <button |
| type="button" |
| onClick={() => setShowPassword(!showPassword)} |
| className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-500 transition-colors hover:text-gray-300" |
| > |
| {showPassword ? <EyeOff size={16} /> : <Eye size={16} />} |
| </button> |
| </div> |
| {password && confirmPassword && ( |
| <div className="mt-2 text-center text-xs"> |
| {password === confirmPassword ? ( |
| <span className="text-emerald-500">Passwords match</span> |
| ) : ( |
| <span className="text-red-500">Passwords do not match</span> |
| )} |
| </div> |
| )} |
| </div> |
| </motion.div> |
| )} |
| </AnimatePresence> |
| </div> |
| |
| <button |
| type="submit" |
| disabled={loading} |
| className="mt-4 w-full shrink-0 rounded-md bg-[#172554] px-4 py-3 text-[14px] font-medium text-[#3b82f6] transition-colors hover:bg-[#1e3a8a] disabled:opacity-50" |
| > |
| {loading ? 'Processing...' : isLogin ? 'Sign In' : 'Create Account'} |
| </button> |
| </form> |
| |
| {error && <div className="mt-4 w-full text-center text-xs text-red-500">{error}</div>} |
| |
| {/* Separator */} |
| <div className="my-5 flex w-full items-center gap-3"> |
| <div className="h-[1px] flex-1 bg-white/[0.06]" /> |
| <span className="text-[11px] text-[#555]">or</span> |
| <div className="h-[1px] flex-1 bg-white/[0.06]" /> |
| </div> |
| |
| {/* Google button */} |
| <button |
| type="button" |
| onClick={() => { window.location.href = '/api/auth/google'; }} |
| className="flex w-full items-center justify-center gap-3 rounded-md border border-transparent bg-[#1a1a1a] px-4 py-3 text-[14px] font-medium text-white transition-colors hover:border-white/5 hover:bg-[#222]" |
| > |
| <svg className="h-[18px] w-[18px]" viewBox="0 0 24 24"> |
| <path d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z" fill="#4285F4" /> |
| <path d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z" fill="#34A853" /> |
| <path d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z" fill="#FBBC05" /> |
| <path d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z" fill="#EA4335" /> |
| </svg> |
| Continue with Google |
| </button> |
| |
| {/* Subtexts */} |
| <div className="mt-5 text-[11px] text-[#555]"> |
| Secured by RYP infrastructure |
| </div> |
| |
| <div className="mt-5 text-[13px] text-[#888]"> |
| Skip & continue to{' '} |
| <button |
| type="button" |
| onClick={onBackToLanding} |
| className="font-medium text-[#3b82f6] transition-colors hover:text-[#60a5fa]" |
| > |
| Home |
| </button> |
| </div> |
| </motion.div> |
| </div> |
| ); |
| } |
|
|
| function CircuitCorner({ className }: { className?: string }) { |
| return ( |
| <svg |
| viewBox="0 0 360 260" |
| fill="none" |
| aria-hidden="true" |
| className={`pointer-events-none absolute h-[240px] w-[360px] ${className || ''}`} |
| > |
| <path d="M15 34H132C148 34 161 47 161 63V82C161 95 171 105 184 105H212" stroke="currentColor" strokeWidth="1.5" /> |
| <path d="M110 63H246C262 63 275 76 275 92V120C275 136 288 149 304 149H346" stroke="currentColor" strokeWidth="1.5" /> |
| <path d="M74 118H164C181 118 195 132 195 149V178C195 194 208 207 224 207H318" stroke="currentColor" strokeWidth="1.5" /> |
| <path d="M228 18V47C228 62 240 74 255 74H346" stroke="currentColor" strokeWidth="1.5" /> |
| <path d="M318 149V112C318 96 331 83 347 83H360" stroke="currentColor" strokeWidth="1.5" /> |
| <path d="M318 208V229C318 244 330 256 345 256H360" stroke="currentColor" strokeWidth="1.5" /> |
| <circle cx="110" cy="63" r="4" stroke="currentColor" strokeWidth="1.5" /> |
| <circle cx="74" cy="118" r="4" stroke="currentColor" strokeWidth="1.5" /> |
| <circle cx="228" cy="18" r="4" stroke="currentColor" strokeWidth="1.5" /> |
| <circle cx="318" cy="149" r="4" stroke="currentColor" strokeWidth="1.5" /> |
| <circle cx="212" cy="105" r="4" stroke="currentColor" strokeWidth="1.5" /> |
| <circle cx="346" cy="149" r="4" stroke="currentColor" strokeWidth="1.5" /> |
| </svg> |
| ); |
| } |
|
|