updates on the design on the kpi cards

This commit is contained in:
2026-08-20 16:53:34 +05:30
parent ce32465a6f
commit 1bf89adb6a
10 changed files with 973 additions and 274 deletions

View File

@@ -1,6 +1,6 @@
import { useState, useEffect } from 'react';
import { useState, useEffect, useRef } from 'react';
import { useNavigate } from 'react-router-dom';
import Loader from 'components/Loader';
import { MdCheck, MdOutlineVisibility, MdOutlineVisibilityOff } from 'react-icons/md';
import logo from 'assets/images/doormile-logo.png';
import { useDispatch } from 'react-redux';
@@ -8,71 +8,400 @@ import { OpenToast } from 'components/third-party/OpenToast';
import { setLoginUser } from 'store/reducers/loginUserSlice';
import { markSessionStart } from 'utils/session';
import { loginAdmin } from 'pages/api/doormileApi';
import { DT } from 'themes/dt/tokens';
// Astryx design system — see themes/astryx.js for the Doormile brand theme
// and CLAUDE.md's <!-- ASTRYX:START --> block for the CLI workflow.
// NOTE: custom CSS (xstyle/stylex.create()) isn't wired up yet — see the
// comment in config-overrides.js. Everything below uses Astryx component
// props only; the brand gradient panel and the two logo images are plain
// native elements with inline `style` for that reason.
// Astryx design system — see themes/astryx.js for the Doormile theme.
//
// Two things about how this page is built:
//
// 1. The brand panel and the password toggle are plain elements with a scoped
// stylesheet, not Astryx layout components. Astryx has no breakpoint props,
// no gradient/decoration surface and no end-slot on TextInput, so those
// three needs cannot be expressed through component props at all. The
// stylesheet below is the same escape hatch `themes/dt/primitives.js`
// (TableScroll) and `PageLayout.js` (PageShell) already use.
//
// 2. EVERY selector in that stylesheet is prefixed `.dm-login`. The brand red
// lives on this page only — the console's own palette (black accent, slate
// ink) is deliberately untouched, so nothing here leaks into the operator
// pages. Do not lift these colours into themes/astryx.js.
import { AppShell } from '@astryxdesign/core/AppShell';
import { Theme } from '@astryxdesign/core/theme';
import { HStack } from '@astryxdesign/core/HStack';
import { VStack } from '@astryxdesign/core/VStack';
import { Center } from '@astryxdesign/core/Center';
import { Card } from '@astryxdesign/core/Card';
import { Heading } from '@astryxdesign/core/Heading';
import { Text } from '@astryxdesign/core/Text';
import { TextInput } from '@astryxdesign/core/TextInput';
import { Button } from '@astryxdesign/core/Button';
import { Link } from '@astryxdesign/core/Link';
import { Divider } from '@astryxdesign/core/Divider';
import { doormileTheme } from 'themes/astryx';
const brandPanelStyle = {
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
position: 'relative',
overflow: 'hidden',
width: '46%',
height: '100vh',
color: '#fff',
padding: 48,
background: `linear-gradient(150deg, ${DT.brand} 0%, #3A3A3A 100%)`
};
const logoLockupStyle = { position: 'absolute', top: 48, left: 48, maxHeight: 60 };
const bulletDotStyle = {
width: 22,
height: 22,
borderRadius: '50%',
backgroundColor: 'rgba(255, 255, 255, 0.18)',
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: 13,
fontWeight: 700,
flexShrink: 0
};
// doormile-logo.png is a white asset; recolour to brand red for this
// white-background card (the brand-panel logo stays white as-is).
const formLogoStyle = {
maxHeight: 48,
filter: 'brightness(0) saturate(100%) invert(15%) sepia(93%) saturate(5437%) hue-rotate(346deg) brightness(81%) contrast(92%)'
};
// Doormile brand red. Stated once; every rule below reads the custom property
// so the two tones can never drift apart.
const BRAND = '#C8152D';
const BRAND_DEEP = '#A90F24';
const BULLETS = ['Real-time fleet visibility', 'AI-optimised dispatch routes', 'Tenant, pricing & invoice control'];
// ---------------------------------------------------------------------------
// Scoped stylesheet.
//
// Astryx exposes stable class hooks (`astryx-card`, `astryx-text-input`,
// `astryx-button`, `astryx-field-label`, `astryx-link`) alongside its generated
// StyleX classes, and a two-class descendant selector out-specifies them —
// verified in the browser rather than assumed. That is what lets this restyle
// real Astryx components instead of re-implementing them.
// ---------------------------------------------------------------------------
const LOGIN_CSS = `
.dm-login {
--dm-brand: ${BRAND};
--dm-brand-deep: ${BRAND_DEEP};
--dm-ring: rgba(200, 21, 45, 0.10);
display: flex;
min-height: 100vh;
width: 100%;
}
/* ---- Left brand panel --------------------------------------------------- */
.dm-login__brand {
flex: 0 0 44%;
position: relative;
overflow: hidden;
display: flex;
flex-direction: column;
justify-content: center;
padding: 56px;
color: #fff;
/* Depth without decoration: one light lift from the top-left, one shadow
into the bottom-right, over a restrained two-stop brand ramp. The panel
should read as lit, not as gradient-y. */
background:
radial-gradient(115% 85% at 10% 6%, rgba(255, 255, 255, 0.12) 0%, transparent 55%),
radial-gradient(95% 85% at 95% 100%, rgba(0, 0, 0, 0.22) 0%, transparent 62%),
linear-gradient(155deg, var(--dm-brand) 0%, #B31228 55%, var(--dm-brand-deep) 100%);
}
/* A single oversized arc, barely visible — the "logistics curve". One shape,
two hairlines, no pattern library. */
.dm-login__brand::before {
content: '';
position: absolute;
right: -240px;
bottom: -280px;
width: 640px;
height: 640px;
border-radius: 50%;
border: 1px solid rgba(255, 255, 255, 0.14);
box-shadow: 0 0 0 72px rgba(255, 255, 255, 0.035);
pointer-events: none;
}
/* Faint route lines, masked so they fade out rather than tile edge to edge. */
.dm-login__brand::after {
content: '';
position: absolute;
inset: 0;
pointer-events: none;
background-image: repeating-linear-gradient(118deg, rgba(255, 255, 255, 0.055) 0 1px, transparent 1px 78px);
-webkit-mask-image: radial-gradient(130% 100% at 85% 12%, #000 0%, transparent 68%);
mask-image: radial-gradient(130% 100% at 85% 12%, #000 0%, transparent 68%);
}
.dm-login__logo {
position: absolute;
top: 56px;
left: 56px;
height: 46px;
width: auto;
z-index: 1;
}
.dm-login__copy {
position: relative;
z-index: 1;
max-width: 460px;
}
.dm-login__headline {
margin: 0;
font-family: var(--font-family-heading);
font-size: 34px;
line-height: 1.18;
letter-spacing: -0.02em;
font-weight: 700;
color: #fff;
}
.dm-login__lede {
margin: 18px 0 0;
font-size: 16px;
line-height: 1.65;
color: rgba(255, 255, 255, 0.82);
}
.dm-login__features {
margin: 34px 0 0;
padding: 0;
list-style: none;
display: flex;
flex-direction: column;
gap: 16px;
}
.dm-login__feature {
display: flex;
align-items: center;
gap: 12px;
font-size: 15px;
line-height: 1.4;
color: rgba(255, 255, 255, 0.94);
}
.dm-login__tick {
flex: 0 0 auto;
width: 22px;
height: 22px;
border-radius: 50%;
display: inline-flex;
align-items: center;
justify-content: center;
background: rgba(255, 255, 255, 0.16);
border: 1px solid rgba(255, 255, 255, 0.22);
color: #fff;
}
/* ---- Right form panel --------------------------------------------------- */
.dm-login__form {
flex: 1 1 auto;
min-width: 0;
display: flex;
align-items: center;
justify-content: center;
background: #f8f9fb;
padding: 48px 24px;
}
.dm-login__stack {
width: 100%;
max-width: 436px;
}
.dm-login .astryx-card {
background: #ffffff;
border: 1px solid #e5e7eb;
border-radius: 16px;
/* Elevated, not floating. */
box-shadow: 0 12px 35px rgba(15, 23, 42, 0.08);
}
.dm-login__title {
margin: 0;
font-family: var(--font-family-heading);
font-size: 26px;
line-height: 1.25;
letter-spacing: -0.01em;
font-weight: 700;
color: #101828;
}
.dm-login__subtitle {
margin: 6px 0 0;
font-size: 14px;
line-height: 1.5;
color: #667085;
}
/* ---- Fields ------------------------------------------------------------- */
.dm-login .astryx-field-label {
font-size: 13px;
font-weight: 600;
color: #344054;
}
.dm-login .astryx-text-input {
height: 48px;
border-radius: 10px;
border-color: #d1d5db;
background: #ffffff;
padding-inline: 14px;
transition: border-color 160ms ease, box-shadow 160ms ease;
}
.dm-login .astryx-text-input input {
font-size: 15px;
}
/* One focus treatment for the whole form. :focus-within so the ring follows
the field, not the bare <input> that sits inside Astryx's bordered wrapper.
Plain two-class specificity is enough: the resting rule above already
out-specifies Astryx's own StyleX rules on this element, and this selector
is strictly more specific than that one. If a future Astryx version starts
winning here, the escape hatch is StyleX's own repeated-:not(#id) idiom —
but write it with IDs that simply never exist rather than copying StyleX's
escaped-hash form, because a backslash inside THIS template literal is
consumed by JS before the CSS parser sees it and the rule is then dropped
silently. */
.dm-login .astryx-text-input:focus-within {
border-color: var(--dm-brand);
box-shadow: 0 0 0 3px var(--dm-ring);
}
/* Chrome paints autofilled inputs pale blue, which is the "browser-looking"
field the design brief calls out. An inset shadow the size of the box is the
only way to repaint it. */
.dm-login .astryx-text-input input:-webkit-autofill,
.dm-login .astryx-text-input input:-webkit-autofill:hover,
.dm-login .astryx-text-input input:-webkit-autofill:focus {
-webkit-box-shadow: 0 0 0 40px #ffffff inset;
box-shadow: 0 0 0 40px #ffffff inset;
-webkit-text-fill-color: #101828;
}
/* ---- Password visibility toggle ----------------------------------------- */
.dm-login__pw {
position: relative;
}
/* Keep typed characters clear of the toggle. */
.dm-login__pw .astryx-text-input {
padding-right: 46px;
}
.dm-login__pw-toggle {
position: absolute;
right: 4px;
bottom: 0;
height: 48px;
width: 42px;
display: inline-flex;
align-items: center;
justify-content: center;
border: 0;
padding: 0;
background: transparent;
color: #667085;
border-radius: 8px;
cursor: pointer;
transition: color 160ms ease, background-color 160ms ease;
}
.dm-login__pw-toggle:hover {
color: #344054;
background: #f2f4f7;
}
.dm-login__pw-toggle:focus-visible {
outline: none;
box-shadow: 0 0 0 3px var(--dm-ring);
color: var(--dm-brand);
}
/* ---- Primary action ----------------------------------------------------- */
.dm-login .astryx-button {
height: 48px;
border-radius: 10px;
background: var(--dm-brand);
border-color: var(--dm-brand);
color: #ffffff;
font-size: 15px;
font-weight: 600;
transition: background-color 160ms ease, transform 120ms ease, box-shadow 160ms ease;
}
.dm-login .astryx-button:hover:not([disabled]):not([aria-disabled='true']) {
background: var(--dm-brand-deep);
border-color: var(--dm-brand-deep);
}
.dm-login .astryx-button:active:not([disabled]):not([aria-disabled='true']) {
transform: translateY(1px);
background: #970d20;
}
.dm-login .astryx-button:focus-visible {
outline: none;
box-shadow: 0 0 0 3px var(--dm-ring);
}
.dm-login .astryx-button[disabled],
.dm-login .astryx-button[aria-disabled='true'] {
opacity: 0.7;
cursor: progress;
}
/* ---- Footer ------------------------------------------------------------- */
.dm-login__footer {
margin-top: 28px;
display: flex;
align-items: center;
justify-content: center;
flex-wrap: wrap;
gap: 8px 20px;
font-size: 13px;
color: #98a2b3;
}
.dm-login__footer .astryx-link {
font-size: 13px;
color: #667085;
}
.dm-login__footer .astryx-link:hover {
color: var(--dm-brand);
}
/* ---- Tablet ------------------------------------------------------------- */
@media (max-width: 1100px) {
.dm-login__brand { flex-basis: 42%; padding: 40px; }
.dm-login__logo { top: 40px; left: 40px; height: 40px; }
.dm-login__headline { font-size: 28px; }
.dm-login__lede { font-size: 15px; margin-top: 14px; }
.dm-login__features { margin-top: 26px; gap: 13px; }
}
/* ---- Mobile ------------------------------------------------------------- */
/* The brand panel COLLAPSES to a logo strip rather than being hidden and the
logo re-rendered above the card. That keeps exactly one <img> of the logo in
the DOM at every breakpoint — a duplicate that is merely display:none is
still a duplicate to a screen reader and to anyone reading the markup. */
@media (max-width: 860px) {
.dm-login { flex-direction: column; min-height: 100dvh; }
.dm-login__brand {
flex: 0 0 auto;
align-items: center;
justify-content: center;
padding: 22px 24px;
}
.dm-login__brand::before,
.dm-login__brand::after { display: none; }
.dm-login__logo { position: static; height: 34px; }
.dm-login__copy { display: none; }
.dm-login__form {
flex: 1 1 auto;
align-items: flex-start;
padding: 32px 20px 40px;
}
.dm-login__title { font-size: 23px; }
.dm-login__footer { margin-top: 22px; gap: 8px 16px; }
}
@media (max-width: 400px) {
.dm-login__form { padding-inline: 16px; }
}
`;
const Login = () => {
const dispatch = useDispatch();
const [loading, setLoading] = useState(false);
const navigate = useNavigate();
const [loading, setLoading] = useState(false);
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [showPassword, setShowPassword] = useState(false);
const emailRef = useRef(null);
const passwordRef = useRef(null);
useEffect(() => {
// 'authname' is the AUTH_PRESENCE_KEY (utils/session.js) — App.js and the
@@ -83,10 +412,24 @@ const Login = () => {
}
}, []);
// Astryx's TextInput has no `autoComplete` prop and BaseProps declares only
// `xstyle`, so an unknown prop would be dropped rather than forwarded. It
// does forward `ref` to the real <input>, which is the supported way to
// reach the element — password managers need these attributes to offer a
// saved credential at all. Re-applied when the toggle swaps `type`.
useEffect(() => {
emailRef.current?.setAttribute('autocomplete', 'email');
passwordRef.current?.setAttribute('autocomplete', 'current-password');
}, [showPassword]);
const opentoast = (message, variant = 'error') => OpenToast(message, variant, 2000);
const handleSubmit = async (e) => {
e.preventDefault();
// Guard against a second submit while the first is still in flight —
// the button is disabled while loading, but Enter in a field is not.
if (loading) return;
if (!email || !password) {
opentoast('Enter your email and password');
return;
@@ -123,83 +466,105 @@ const Login = () => {
return (
<Theme theme={doormileTheme} mode="light">
<AppShell contentPadding={0}>
{loading && <Loader />}
<HStack gap={0} height="100vh" wrap="nowrap">
{/* ---- Left brand panel (plain element: see the note at the top of
this file about custom styling not going through Astryx yet) ---- */}
<div style={brandPanelStyle}>
<img src={logo} alt="Doormile" style={logoLockupStyle} />
<style>{LOGIN_CSS}</style>
<VStack gap={2} maxWidth={430}>
<Heading level={1} color="inherit">
Operate your dispatch, end to end.
</Heading>
<Text type="large" color="inherit">
<div className="dm-login">
{/* ---- Brand panel. The ONLY Doormile logo on this page. ---- */}
<aside className="dm-login__brand">
<img className="dm-login__logo" src={logo} alt="Doormile" />
<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.
</Text>
<VStack gap={1.5} padding={0}>
</p>
<ul className="dm-login__features">
{BULLETS.map((t) => (
<HStack key={t} gap={1.25} vAlign="center">
<span style={bulletDotStyle}>✓</span>
<Text color="inherit">{t}</Text>
</HStack>
<li key={t} className="dm-login__feature">
<span className="dm-login__tick" aria-hidden="true">
<MdCheck size={14} />
</span>
{t}
</li>
))}
</VStack>
</VStack>
</div>
</ul>
</div>
</aside>
{/* ---- Right form panel ---- */}
<Center axis="both" width="54%" height="100vh">
<VStack width="100%" maxWidth={440} gap={4} padding={3}>
<Card padding={8} elevation="med">
<VStack gap={6} padding={0}>
<VStack gap={1.5} hAlign="center" padding={0}>
<img src={logo} alt="Doormile" style={formLogoStyle} />
<Heading level={2}>Welcome back</Heading>
<Text type="supporting">Sign in to the Doormile Express console</Text>
</VStack>
{/* ---- Form panel ---- */}
<main className="dm-login__form">
<div className="dm-login__stack">
<Card padding={8} elevation="none">
<VStack gap={5} padding={0}>
<div>
<h2 className="dm-login__title">Welcome back</h2>
<p className="dm-login__subtitle">Sign in to the Doormile Express console</p>
</div>
<Divider />
<form noValidate onSubmit={handleSubmit}>
<VStack gap={4} padding={0}>
<TextInput
ref={emailRef}
hasAutoFocus
label="E-mail Address"
label="Email address"
type="email"
size="lg"
placeholder="you@company.com"
value={email}
isDisabled={loading}
onChange={(value) => setEmail(value.toLocaleLowerCase())}
/>
<TextInput
label="Password"
type="password"
<div className="dm-login__pw">
<TextInput
ref={passwordRef}
label="Password"
type={showPassword ? 'text' : 'password'}
size="lg"
placeholder="Enter your password"
value={password}
isDisabled={loading}
onChange={(value) => setPassword(value)}
/>
<button
type="button"
className="dm-login__pw-toggle"
onClick={() => setShowPassword((v) => !v)}
aria-label={showPassword ? 'Hide password' : 'Show password'}
aria-pressed={showPassword}
>
{showPassword ? <MdOutlineVisibilityOff size={19} /> : <MdOutlineVisibility size={19} />}
</button>
</div>
<Button
label={loading ? 'Signing in…' : 'Sign in'}
type="submit"
variant="primary"
size="lg"
value={password}
onChange={(value) => setPassword(value)}
width="100%"
isLoading={loading}
isDisabled={loading}
/>
<Button label="Sign in" type="submit" variant="primary" size="lg" width="100%" />
</VStack>
</form>
</VStack>
</Card>
{/* footer */}
<HStack justify="center" wrap="wrap" gap={2}>
<Link href="https://doormile.com" target="_blank" isExternalLink type="supporting">
&copy; All rights reserved
</Link>
<div className="dm-login__footer">
<span>&copy; {new Date().getFullYear()} Doormile</span>
<Link href="https://doormile.com/terms-of-service" target="_blank" isExternalLink type="supporting">
Terms and Conditions
Terms &amp; Conditions
</Link>
<Link href="https://doormile.com/privacy-policy" target="_blank" isExternalLink type="supporting">
Privacy Policy
</Link>
</HStack>
</VStack>
</Center>
</HStack>
</div>
</div>
</main>
</div>
</AppShell>
</Theme>
);

View File

@@ -38,7 +38,7 @@ import { Table, TableHeader, TableBody, TableRow, TableCell, TableHeaderCell } f
import { MobileCard, MobileCardList, MobileField, MobileFieldGrid } from 'components/nearle_components/MobileCard';
import PageHeader from 'components/nearle_components/PageHeader';
import { PageShell, KpiGrid, DataCard } from 'components/nearle_components/PageLayout';
import HoverSocialCard from 'components/cards/statistics/HoverSocialCard';
import StatCard from 'components/nearle_components/StatCard';
import { parseDoormileTimestamp } from 'utils/doormileTimestamp';
import { fetchPaymentType, fetchRidersList, finalCreatedeliveries, notifyRider } from '../../api/api';
import { OpenToast } from 'components/third-party/OpenToast';
@@ -561,17 +561,16 @@ const OrdersPreview = () => {
}
/>
{/* Was four <HoverSocialCard>s — solid-colour tiles that were the only
KPI design in the app not coming from StatCard, so this row looked
nothing like the KPI row on the other fifteen pages. Same data,
same semantic colours, shared tile. */}
{aiMode == 1 && (
<KpiGrid>
<HoverSocialCard
secondary={metaData?.total_orders}
primary="Orders"
percentage={<MdDirectionsBike />}
color={STATUS.delivered}
/>
<HoverSocialCard secondary={metaData?.total_riders} primary="Riders" percentage={<MdDirectionsBike />} color={STATUS.pending} />
<HoverSocialCard secondary={zoneData?.length} primary="Zones" percentage={<FaMapLocationDot />} color="#0ea5e9" />
<HoverSocialCard secondary={zoneData?.length} primary="Kilometer" percentage={<FaMapLocationDot />} color={STATUS.cancelled} />
<StatCard title="Orders" value={metaData?.total_orders} icon={<MdDirectionsBike size={20} />} color={STATUS.delivered} />
<StatCard title="Riders" value={metaData?.total_riders} icon={<MdDirectionsBike size={20} />} color={STATUS.pending} />
<StatCard title="Zones" value={zoneData?.length} icon={<FaMapLocationDot size={20} />} color={STATUS.info} />
<StatCard title="Kilometer" value={zoneData?.length} icon={<FaMapLocationDot size={20} />} color={STATUS.cancelled} />
</KpiGrid>
)}