RYP / src /components /AuthPage.tsx
Soumya79's picture
Upload 1361 files
3bda374 verified
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>
);
}