Files
doormilxpress_astryx/src/pages/Login.jsx
2026-08-29 15:42:56 +05:30

223 lines
9.0 KiB
JavaScript

import React, { useState } from 'react';
import { useLocation, useNavigate } from 'react-router-dom';
import { AlertCircle, Check, Loader2, Eye, EyeOff, CheckCircle2 } from 'lucide-react';
import { DOORMILE_WORDMARK_URL } from '@/assets/brand';
import { Checkbox } from '@/components/ds';
import { useAuth } from '@/lib/AuthContext';
import { readStoredUser } from '@/api/doormile';
import './Login.css';
/**
* Console sign-in — `POST /admin/login`.
*
* The credential is checked by the Doormile API, so there is no client-side
* password policy here beyond "you typed something": a length rule invented on
* this side would reject a valid account whose password predates the rule, and
* the server is the only thing that actually knows.
*
* The look is the Doormile Express console's sign-in design — lit brand panel
* on the left, one elevated card on the right — so the two consoles open the
* same way. It is expressed in a page-scoped stylesheet (Login.css) rather than
* utility classes because the panel needs a layered gradient, an oversized arc
* and a masked line pattern, none of which are Tailwind's to give. Nothing in
* that stylesheet is reachable from any other page.
*/
const FEATURES = ['Real-time fleet visibility', 'AI-optimised dispatch routes', 'Tenant, pricing & invoice control'];
export default function Login() {
const navigate = useNavigate();
const location = useLocation();
const { login } = useAuth();
const from = location.state?.from;
const returnTo = typeof from === 'string' && from.startsWith('/doormile') ? from : '/doormile/dispatch';
const [form, setForm] = useState({ email: '', password: '', remember: true });
const [showPassword, setShowPassword] = useState(false);
const [errors, setErrors] = useState(/** @type {{ email?: string, password?: string }} */({}));
const [authError, setAuthError] = useState('');
const [status, setStatus] = useState('idle');
const validate = () => {
const next = {};
if (!form.email.trim()) next.email = 'Enter your work email';
else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(form.email.trim())) next.email = 'Enter a valid email address';
if (!form.password) next.password = 'Enter your password';
setErrors(next);
return Object.keys(next).length === 0;
};
const submit = async (event) => {
event.preventDefault();
setAuthError('');
if (!validate()) return;
setStatus('loading');
const result = await login(form.email.trim(), form.password);
if (!result.success) {
setStatus('idle');
setAuthError(result.message);
return;
}
/* The data layer scopes some requests off the signed-in identity read
straight from localStorage rather than through React context — the zone
picker, for one, narrows the hub list to the tenant's own city. Those
keys are written here, at the one moment the identity is known. */
const user = readStoredUser() || {};
localStorage.setItem('authname', user.email || user.name || form.email.trim());
localStorage.setItem('firstname', user.name || '');
localStorage.setItem('userid', user.id ?? '');
localStorage.setItem('roleid', user.role ?? '');
localStorage.setItem('tenantid', user.tenantid ?? '');
setStatus('success');
setTimeout(() => navigate(returnTo, { replace: true }), 320);
};
const busy = status !== 'idle';
return (
<div className="dm-login">
{/* ---- Brand panel. The ONLY Doormile logo on this page. ---- */}
<aside className="dm-login__brand">
<img className="dm-login__logo" src={DOORMILE_WORDMARK_URL} alt="Doormile Express" />
<div className="dm-login__copy">
<h1 className="dm-login__headline">Operate your dispatch, end to end.</h1>
<p className="dm-login__lede">
Orders, AI route optimisation, live rider tracking and billing — all in the Doormile Express operator
console.
</p>
<ul className="dm-login__features">
{FEATURES.map((feature) => (
<li key={feature} className="dm-login__feature">
<span className="dm-login__tick" aria-hidden="true">
<Check size={14} strokeWidth={2.5} />
</span>
{feature}
</li>
))}
</ul>
</div>
</aside>
{/* ---- Form panel ---- */}
<main className="dm-login__panel">
<div className="dm-login__stack">
<div className="dm-login__card">
<h2 className="dm-login__title">Welcome back</h2>
<p className="dm-login__subtitle">Sign in to the Doormile Express console</p>
<hr className="dm-login__divider" />
{authError && (
<div role="alert" className="dm-login__alert">
<AlertCircle size={16} aria-hidden="true" />
<p>{authError}</p>
</div>
)}
<form onSubmit={submit} noValidate className="dm-login__fields">
<div className="dm-login__field">
<label className="dm-login__label" htmlFor="dm-login-email">
Email address
</label>
<input
id="dm-login-email"
className="dm-login__input"
type="email"
autoComplete="username"
autoFocus
disabled={busy}
aria-invalid={Boolean(errors.email)}
aria-describedby={errors.email ? 'dm-login-email-error' : undefined}
value={form.email}
onChange={(e) => setForm({ ...form, email: e.target.value })}
placeholder="you@company.com"
/>
{errors.email && (
<p id="dm-login-email-error" className="dm-login__error">
{errors.email}
</p>
)}
</div>
<div className="dm-login__field">
<label className="dm-login__label" htmlFor="dm-login-password">
Password
</label>
<div className="dm-login__pw">
<input
id="dm-login-password"
className="dm-login__input"
type={showPassword ? 'text' : 'password'}
autoComplete="current-password"
disabled={busy}
aria-invalid={Boolean(errors.password)}
aria-describedby={errors.password ? 'dm-login-password-error' : undefined}
value={form.password}
onChange={(e) => setForm({ ...form, password: e.target.value })}
placeholder="Enter your password"
/>
<button
type="button"
className="dm-login__pw-toggle"
onClick={() => setShowPassword((v) => !v)}
aria-label={showPassword ? 'Hide password' : 'Show password'}
aria-pressed={showPassword}
>
{showPassword ? <EyeOff size={18} /> : <Eye size={18} />}
</button>
</div>
{errors.password && (
<p id="dm-login-password-error" className="dm-login__error">
{errors.password}
</p>
)}
</div>
<div className="dm-login__meta">
<label className="dm-login__remember">
<Checkbox
checked={form.remember}
onCheckedChange={(v) => setForm({ ...form, remember: Boolean(v) })}
disabled={busy}
/>
Remember me
</label>
<button
type="button"
className="dm-login__forgot"
disabled={busy}
onClick={() => setAuthError('Ask a platform administrator to reset your password.')}
>
Forgot password?
</button>
</div>
<button type="submit" className="dm-login__submit" disabled={busy}>
{status === 'loading' && <Loader2 size={16} className="dm-login__spin" aria-hidden="true" />}
{status === 'success' && <CheckCircle2 size={16} aria-hidden="true" />}
<span>{status === 'loading' ? 'Signing in…' : status === 'success' ? 'Signed in' : 'Sign in'}</span>
</button>
</form>
</div>
<div className="dm-login__footer">
<span>&copy; {new Date().getFullYear()} Doormile</span>
<a href="https://doormile.com/terms-of-service" target="_blank" rel="noreferrer">
Terms &amp; Conditions
</a>
<a href="https://doormile.com/privacy-policy" target="_blank" rel="noreferrer">
Privacy Policy
</a>
</div>
</div>
</main>
</div>
);
}