Init project
This commit is contained in:
151
nest-front/client/src/pages/public/LoginPage.tsx
Normal file
151
nest-front/client/src/pages/public/LoginPage.tsx
Normal file
@@ -0,0 +1,151 @@
|
||||
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} noValidate className="crt-box" style={{ padding: '2rem' }}>
|
||||
{/* Demo hint */}
|
||||
<div style={{ background: 'rgba(255,176,0,0.06)', border: '1px solid rgba(255,176,0,0.2)', padding: '0.75rem', marginBottom: '1.5rem' }}>
|
||||
<div style={{ fontFamily: 'var(--font-mono)', color: 'var(--color-amber)', fontSize: '0.7rem', letterSpacing: '0.05em', marginBottom: '0.4rem' }}>
|
||||
[DEMO] Quick login emails:
|
||||
</div>
|
||||
{[
|
||||
{ label: 'Dev/Admin', email: 'kestrel@crowmate.dev' },
|
||||
{ label: 'Com Staff', email: 'vesper@crowmate.dev' },
|
||||
{ label: 'User', email: 'glitch@mail.com' },
|
||||
].map(({ label, email: e }) => (
|
||||
<button
|
||||
key={e}
|
||||
type="button"
|
||||
onClick={() => { setEmail(e); setPassword('password'); }}
|
||||
style={{
|
||||
background: 'transparent',
|
||||
border: 'none',
|
||||
color: 'var(--color-text-muted)',
|
||||
cursor: 'pointer',
|
||||
fontFamily: 'var(--font-mono)',
|
||||
fontSize: '0.65rem',
|
||||
display: 'block',
|
||||
textAlign: 'left',
|
||||
padding: '0.1rem 0',
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
> {label}: {e}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{errors.form && (
|
||||
<div style={{ background: 'rgba(255,34,68,0.08)', border: '1px solid rgba(255,34,68,0.3)', color: 'var(--color-red)', fontFamily: 'var(--font-mono)', fontSize: '0.78rem', padding: '0.75rem', marginBottom: '1.25rem' }}>
|
||||
[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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user