c91e292066
Design system (Layout2): - Solid token-driven theme (no animated grid); single-source tokens for bg, cards (bg/border/radius), popups, fonts (Inter), and full semantic + data-viz color palette. Swept ~168 hardcoded colors to tokens. Dashboard: - Reflow: To Do + Calendar row (fixed right rail) over Activity / In-Progress / Team Performance; CSS height-match (To Do = Calendar); responsive stacking. - 'Tasks Not Started' -> 'To Do' card with R#/Assigned columns; compact money fmt. Tasks page: - Combined tabs into Tasks + Completed with a Status column. - Column-header sort + filter (Status, R#/new-vs-revision, Project, Task Type, Company) via portaled, scrollable FilterDropdown; removed toolbar filters and the projects side panel; headers stay visible when empty; renamed to 'Tasks'. Perf: - FK/filter index migration; removed redundant client/external scope waterfall (rely on RLS); parallel follow-up queries. Mobile: - Responsive grids; mobile topbar = hamburger only, blends with bg, proper offset. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
76 lines
2.5 KiB
React
76 lines
2.5 KiB
React
import { useState, useEffect } from 'react';
|
|
import { useNavigate, useLocation, Link } from 'react-router-dom';
|
|
import { useAuth } from '../context/AuthContext';
|
|
|
|
export default function Login() {
|
|
const { login, currentUser } = useAuth();
|
|
const navigate = useNavigate();
|
|
const location = useLocation();
|
|
const successMessage = location.state?.message || '';
|
|
const [email, setEmail] = useState('');
|
|
const [password, setPassword] = useState('');
|
|
const [error, setError] = useState('');
|
|
const [loading, setLoading] = useState(false);
|
|
|
|
useEffect(() => {
|
|
if (currentUser) {
|
|
navigate('/dashboard', { replace: true });
|
|
}
|
|
}, [currentUser, navigate]);
|
|
|
|
const handleSuccess = async (e) => {
|
|
e.preventDefault();
|
|
setLoading(true);
|
|
setError('');
|
|
const { error: err } = await login(email, password);
|
|
if (err) {
|
|
setError('Invalid email or password.');
|
|
}
|
|
setLoading(false);
|
|
// Navigation handled by useEffect watching currentUser
|
|
};
|
|
|
|
return (
|
|
<div className="auth-page">
|
|
<div className="auth-card">
|
|
<div className="auth-logo">
|
|
<img className="brand-logo brand-logo-auth" src="/fourge-logo.png" alt="Fourge Branding" />
|
|
<p>Client & Project Portal</p>
|
|
</div>
|
|
|
|
<form onSubmit={handleSuccess}>
|
|
<div className="form-group">
|
|
<label>Email Address</label>
|
|
<input
|
|
type="email"
|
|
placeholder="you@example.com"
|
|
value={email}
|
|
onChange={e => { setEmail(e.target.value); setError(''); }}
|
|
required
|
|
/>
|
|
</div>
|
|
<div className="form-group">
|
|
<label>Password</label>
|
|
<input
|
|
type="password"
|
|
placeholder="••••••••"
|
|
value={password}
|
|
onChange={e => { setPassword(e.target.value); setError(''); }}
|
|
required
|
|
/>
|
|
</div>
|
|
{successMessage && <p style={{ color: 'var(--success)', fontSize: 13, marginBottom: 12 }}>{successMessage}</p>}
|
|
{error && <p style={{ color: 'var(--danger)', fontSize: 13, marginBottom: 12 }}>{error}</p>}
|
|
<button type="submit" className="btn btn-primary w-full btn-lg" disabled={loading}>
|
|
{loading ? 'Signing in...' : 'Sign In'}
|
|
</button>
|
|
</form>
|
|
|
|
<p style={{ textAlign: 'center', marginTop: 20, fontSize: 13, color: 'var(--text-secondary)' }}>
|
|
Contact Fourge Branding to get access.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|