This repository has been archived on 2026-05-01. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
Nest/nest-front/src/pages/public/LoginPage.tsx
2026-03-26 11:10:25 +01:00

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>
);
}