120 lines
4.9 KiB
TypeScript
120 lines
4.9 KiB
TypeScript
import { useState, useCallback, useEffect } from 'react';
|
|
import { Link, useNavigate, useLocation } from 'react-router-dom';
|
|
import { useAuth } from '../../contexts/AuthContext';
|
|
|
|
export default function LoginPage() {
|
|
const { login, isAuthenticated } = useAuth();
|
|
const navigate = useNavigate();
|
|
const location = useLocation();
|
|
const from = (location.state as { from?: Location })?.from?.pathname ?? '/';
|
|
|
|
const [email, setEmail] = useState('');
|
|
const [password, setPassword] = useState('');
|
|
const [errors, setErrors] = useState<{ email?: string; password?: string; form?: string }>({});
|
|
const [loading, setLoading] = useState(false);
|
|
|
|
useEffect(() => {
|
|
if (isAuthenticated) navigate(from, { replace: true });
|
|
}, [isAuthenticated, from, navigate]);
|
|
|
|
const validate = (): boolean => {
|
|
const next: typeof errors = {};
|
|
if (!email.trim()) next.email = 'Email is required.';
|
|
else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) next.email = 'Enter a valid email address.';
|
|
if (!password) next.password = 'Password is required.';
|
|
setErrors(next);
|
|
return Object.keys(next).length === 0;
|
|
};
|
|
|
|
const handleSubmit = useCallback(async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
if (!validate()) return;
|
|
setLoading(true);
|
|
const result = await login(email, password);
|
|
setLoading(false);
|
|
if (!result.success) {
|
|
setErrors({ form: result.error });
|
|
}
|
|
}, [email, password, login]);
|
|
|
|
return (
|
|
<div
|
|
style={{
|
|
minHeight: '100vh',
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
padding: '2rem 1rem',
|
|
}}
|
|
>
|
|
<div style={{ width: '100%', maxWidth: '420px' }}>
|
|
<div style={{ textAlign: 'center', marginBottom: '2rem' }}>
|
|
<div className="section-label">Authentication</div>
|
|
<h1 style={{ fontFamily: 'var(--font-heading)', color: 'var(--color-text)', fontSize: '2rem', marginTop: '0.5rem' }}>
|
|
LOGIN
|
|
</h1>
|
|
<div style={{ fontFamily: 'var(--font-mono)', color: 'var(--color-text-muted)', fontSize: '0.72rem', marginTop: '0.5rem' }}>
|
|
CROWMATE STUDIO / HEADLESS HAZARD COMMUNITY
|
|
</div>
|
|
</div>
|
|
|
|
<form onSubmit={handleSubmit}>
|
|
{errors.form && (
|
|
<div style={{ background: 'rgba(220,38,38,0.1)', border: '1px solid rgba(220,38,38,0.3)', color: 'var(--color-red)', fontFamily: 'var(--font-mono)', fontSize: '0.78rem', padding: '0.75rem', marginBottom: '1.25rem', borderRadius: '6px' }}>
|
|
[ERROR] {errors.form}
|
|
</div>
|
|
)}
|
|
|
|
{/* Email */}
|
|
<div style={{ marginBottom: '1rem' }}>
|
|
<label style={{ display: 'block', fontFamily: 'var(--font-mono)', color: 'var(--color-text-muted)', fontSize: '0.75rem', marginBottom: '0.4rem' }}>
|
|
Email Address
|
|
</label>
|
|
<input
|
|
className={`input-terminal${errors.email ? ' error' : ''}`}
|
|
type="email"
|
|
autoComplete="email"
|
|
placeholder="your@email.com"
|
|
value={email}
|
|
onChange={(e) => { setEmail(e.target.value); setErrors((p) => ({ ...p, email: undefined })); }}
|
|
aria-describedby={errors.email ? 'email-error' : undefined}
|
|
/>
|
|
{errors.email && <div id="email-error" style={{ color: 'var(--color-red)', fontSize: '0.72rem', marginTop: '0.25rem' }}>{errors.email}</div>}
|
|
</div>
|
|
|
|
{/* Password */}
|
|
<div style={{ marginBottom: '1.5rem' }}>
|
|
<label style={{ display: 'block', fontFamily: 'var(--font-mono)', color: 'var(--color-text-muted)', fontSize: '0.75rem', marginBottom: '0.4rem' }}>
|
|
Password
|
|
</label>
|
|
<input
|
|
className={`input-terminal${errors.password ? ' error' : ''}`}
|
|
type="password"
|
|
autoComplete="current-password"
|
|
placeholder="••••••••"
|
|
value={password}
|
|
onChange={(e) => { setPassword(e.target.value); setErrors((p) => ({ ...p, password: undefined })); }}
|
|
aria-describedby={errors.password ? 'pass-error' : undefined}
|
|
/>
|
|
{errors.password && <div id="pass-error" style={{ color: 'var(--color-red)', fontSize: '0.72rem', marginTop: '0.25rem' }}>{errors.password}</div>}
|
|
</div>
|
|
|
|
<button
|
|
type="submit"
|
|
className="btn-terminal"
|
|
disabled={loading}
|
|
style={{ width: '100%', justifyContent: 'center', opacity: loading ? 0.7 : 1, marginBottom: '1.25rem' }}
|
|
>
|
|
{loading ? 'Authenticating...' : '> Login'}
|
|
</button>
|
|
|
|
<div style={{ textAlign: 'center', fontFamily: 'var(--font-mono)', fontSize: '0.78rem', color: 'var(--color-text-muted)' }}>
|
|
No account?{' '}
|
|
<Link to="/register" style={{ color: 'var(--color-green)' }}>Register here</Link>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|