updates on the ui changes and changed into doormile
This commit is contained in:
222
src/pages/Login.jsx
Normal file
222
src/pages/Login.jsx
Normal file
@@ -0,0 +1,222 @@
|
||||
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>© {new Date().getFullYear()} Doormile</span>
|
||||
<a href="https://doormile.com/terms-of-service" target="_blank" rel="noreferrer">
|
||||
Terms & Conditions
|
||||
</a>
|
||||
<a href="https://doormile.com/privacy-policy" target="_blank" rel="noreferrer">
|
||||
Privacy Policy
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user