style: ui fixes to booking detail page

This commit is contained in:
Nathnael
2026-06-11 07:11:43 +00:00
parent 83edb1e063
commit 2cd585a3ac
12 changed files with 567 additions and 218 deletions

View File

@@ -29,7 +29,11 @@ import { CardTitle, PageShell, SectionCard } from "./components/layout";
import { CountChip, DocRow, IconSquare } from "./components/Documents";
import { EstimateCard } from "./components/pricing";
import { HeaderButton, PageHeader } from "./components/PageHeader";
import { MutationErrors, NoticeBanner } from "./components/Notices";
import {
ActionRequiredBanner,
MutationErrors,
NoticeBanner,
} from "./components/Notices";
import { ScheduleCard } from "./components/ScheduleCard";
import { ShipmentDetailsCard } from "./components/ShipmentDetailsCard";
import { StatusHero } from "./components/StatusHero";
@@ -132,30 +136,39 @@ export function DraftBookingView({
<PageHeader
booking={booking}
actions={
<HeaderButton
dark
icon={<Pencil size={16} />}
label="Continue editing"
onClick={() => navigate(`/bookings/${booking.id}/edit`)}
/>
<>
<HeaderButton
icon={<Download size={16} />}
label="Documents"
onClick={() =>
documentsRef.current?.scrollIntoView({ behavior: "smooth" })
}
/>
<HeaderButton
dark
icon={<Pencil size={16} />}
label="Continue editing"
onClick={() => navigate(`/bookings/${booking.id}/edit`)}
/>
</>
}
/>
{booking.status === "CHANGES_REQUESTED" &&
booking.latestChangeRequestNote && (
<NoticeBanner
tone="amber"
icon={<AlertCircle size={18} />}
title="Changes requested by staff"
>
{booking.latestChangeRequestNote}
</NoticeBanner>
)}
<MutationErrors
mutations={[uploadMutation, submitMutation, cancelMutation]}
/>
<StatusHero booking={booking} />
<StatusHero booking={booking}>
{booking.status === "CHANGES_REQUESTED" &&
booking.latestChangeRequestNote ? (
<ActionRequiredBanner
title="Review the requested changes, then resubmit."
onAction={() => navigate(`/bookings/${booking.id}/edit`)}
>
{booking.latestChangeRequestNote}
</ActionRequiredBanner>
) : undefined}
</StatusHero>
<BodyGrid
left={
@@ -169,48 +182,34 @@ export function DraftBookingView({
</Text>
</Group>
<Stack gap={4}>
<StepLine
index={1}
done
title="Booking details"
desc="Route, cargo and service are set."
action={
<StepGhostButton
onClick={() => navigate(`/bookings/${booking.id}/edit`)}
>
Edit
</StepGhostButton>
}
/>
<StepLine index={1} done title="Cargo & route details" />
<StepLine
index={2}
active={!allDocsUploaded}
done={allDocsUploaded}
highlight
title="Upload documents"
desc={
title={
allDocsUploaded
? "All required documents uploaded."
: `${uploadedCount} of ${REQUIRED_DOC_FIELDS.length} uploaded.`
? "Required documents"
: "Upload required documents"
}
desc={`${uploadedCount} of ${REQUIRED_DOC_FIELDS.length} uploaded.`}
action={
!allDocsUploaded && (
<StepGhostButton
onClick={() =>
documentsRef.current?.scrollIntoView({
behavior: "smooth",
})
}
>
Upload
</StepGhostButton>
)
<StepGhostButton
icon={<Upload size={16} color="#334155" />}
onClick={() =>
documentsRef.current?.scrollIntoView({
behavior: "smooth",
})
}
>
Add documents
</StepGhostButton>
}
/>
<StepLine
index={3}
done={false}
title="Submit for review"
active={allDocsUploaded}
title="Review & submit"
desc="Send your booking to EDR staff."
/>
</Stack>
@@ -223,7 +222,10 @@ export function DraftBookingView({
onClick={handleSubmitRequest}
disabled={submitMutation.isPending}
loading={submitMutation.isPending}
styles={{ root: { height: 46 }, label: { fontWeight: 700 } }}
styles={{
root: { height: 46 },
label: { fontSize: 14, fontWeight: 800 },
}}
>
{submitMutation.isPending ? "Submitting…" : "Submit for review"}
</Button>
@@ -268,7 +270,7 @@ export function DraftBookingView({
? (file?.name ?? "Uploaded")
: selected
? selected.name
: "Required · not uploaded"
: "Not added yet"
}
status={
isUploaded ? "verified" : selected ? "ready" : "missing"
@@ -297,22 +299,26 @@ export function DraftBookingView({
/>
<Group gap={6} wrap="nowrap">
<Button
variant="default"
variant="white"
radius={9}
leftSection={<Upload size={14} />}
leftSection={<Upload size={16} color="#334155" />}
onClick={() =>
fileInputRefs.current[doc.key]?.click()
}
styles={{
root: { height: 34, paddingInline: 12 },
root: {
height: 34,
paddingInline: 13,
border: "1.5px solid #CBD5E1",
},
label: {
fontSize: 12.5,
fontWeight: 700,
color: "#475569",
color: "#334155",
},
}}
>
{selected ? "Change" : "Choose"}
{selected ? "Change" : "Add"}
</Button>
{selected && (
<ActionIcon
@@ -361,7 +367,7 @@ export function DraftBookingView({
<EstimateCard
pricing={pricing}
title="Estimated Cost"
chip="Pending review"
chip="Not invoiced"
/>
<ScheduleCard booking={booking} title="Schedule & Service" />
<SupportCard onCancel={() => setCancelDialogOpen(true)} />

View File

@@ -10,12 +10,14 @@ import { ActivityCard } from "./components/ActivityCard";
import { ContractCard } from "./components/ContractCard";
import { DocRow, IconSquare } from "./components/Documents";
import { BodyGrid, CardTitle, PageShell, SectionCard } from "./components/layout";
import { CancelledBanner } from "./components/Notices";
import { HeaderButton, PageHeader } from "./components/PageHeader";
import { PaymentCard } from "./components/pricing";
import { ScheduleCard } from "./components/ScheduleCard";
import { ShipmentDetailsCard } from "./components/ShipmentDetailsCard";
import { StatusHero } from "./components/StatusHero";
import { SupportCard } from "./components/SupportCard";
import { fmtDate, isNegative } from "./utils";
export function ReadonlyBookingView({ booking }: { booking: Freight.IBooking }) {
const navigate = useNavigate();
@@ -49,7 +51,23 @@ export function ReadonlyBookingView({ booking }: { booking: Freight.IBooking })
}
/>
<StatusHero booking={booking} />
{isNegative(status) ? (
<CancelledBanner
pillLabel={status === "REJECTED" ? "Rejected" : "Cancelled"}
title={`This booking was ${
status === "REJECTED" ? "rejected" : "cancelled"
} on ${fmtDate(booking.updatedAt)}.`}
subtitle={
status === "REJECTED"
? "This booking request has been rejected."
: "This booking process has been terminated."
}
reason={booking.latestChangeRequestNote}
onRebook={() => navigate("/bookings/new")}
/>
) : (
<StatusHero booking={booking} />
)}
<ContractCard booking={booking} navigate={navigate} />

View File

@@ -1,5 +1,4 @@
import { Box, Group, Text } from "@mantine/core";
import { Train } from "lucide-react";
import type { Freight } from "@edr/types";
@@ -59,16 +58,11 @@ export function ActivityCard({ booking }: { booking: Freight.IBooking }) {
style={{
width: 18,
height: 18,
display: "flex",
alignItems: "center",
justifyContent: "center",
borderRadius: 999,
backgroundColor: first ? "#0EA371" : "#D4E9DF",
boxShadow: first ? "0 0 0 4px #BFE8D4" : undefined,
}}
>
{first && <Train size={10} color="#fff" />}
</Box>
/>
{!lastItem && (
<Box
flex={1}
@@ -91,13 +85,15 @@ export function ActivityCard({ booking }: { booking: Freight.IBooking }) {
style={{
borderRadius: 999,
backgroundColor: "#ECF6F1",
border: "1px solid #CDEBDD",
padding: "3px 9px",
fontSize: 10.5,
fontWeight: 800,
letterSpacing: 0.3,
color: "#0A6F4D",
}}
>
Latest
Current
</Box>
)}
</Group>

View File

@@ -10,29 +10,27 @@ const CONTRACT_CONFIG: Record<
title: string;
description: string;
buttonLabel?: string;
urgent?: boolean;
}
> = {
APPROVED_PENDING_SIGNATURE: {
title: "Contract being prepared",
title: "Your contract is being prepared.",
description:
"Your booking has been approved. The contract will be available shortly.",
},
CONTRACT_READY: {
title: "Action required — sign your contract",
title: "Review and sign your contract to proceed.",
description:
"Your contract is ready. Review the agreement and apply your digital signature to proceed.",
buttonLabel: "View & sign contract",
urgent: true,
"Once all parties sign, we'll issue your payment reference (PNR) and schedule the cargo.",
buttonLabel: "Review & Sign",
},
SIGNED_CUSTOMER: {
title: "You have signed the contract",
title: "You have signed the contract.",
description:
"Your signature has been submitted. Awaiting the final staff signature.",
buttonLabel: "View contract",
},
FULLY_EXECUTED: {
title: "Contract fully executed",
title: "Contract fully executed — proceed to payment.",
description:
"The contract has been signed by all parties. You can now proceed to payment.",
buttonLabel: "View contract",
@@ -48,37 +46,54 @@ export function ContractCard({
}) {
const c = CONTRACT_CONFIG[booking.status as string];
if (!c) return null;
const urgent = c.urgent;
return (
<Paper
radius={20}
p="lg"
withBorder={!urgent}
bg={urgent ? "#0EA371" : "#ECF6F1"}
style={{ borderColor: urgent ? "transparent" : "#CDEBDD" }}
radius={16}
px={22}
py={20}
bg="#F1FAF6"
style={{ border: "1px solid #CFEBDD" }}
>
<Group justify="space-between" align="center" wrap="wrap" gap="md">
<Group justify="space-between" align="center" wrap="wrap" gap={20}>
<Group gap={16} align="center" wrap="nowrap">
<Box
style={{
width: 44,
height: 44,
flexShrink: 0,
width: 46,
height: 46,
display: "flex",
alignItems: "center",
justifyContent: "center",
borderRadius: 12,
borderRadius: 13,
backgroundColor: "#fff",
color: urgent ? "#0EA371" : "#0A6F4D",
border: "1px solid #CDEBDD",
color: "#0A6F4D",
}}
>
<FileSignature size={22} />
</Box>
<Box>
<Text fz="15px" fw={800} c={urgent ? "#fff" : "#10202F"}>
<Box miw={0}>
<Box
component="span"
style={{
display: "inline-flex",
borderRadius: 999,
backgroundColor: "#0A6F4D",
padding: "4px 10px",
fontSize: 10.5,
fontWeight: 800,
letterSpacing: 0.3,
color: "#fff",
textTransform: "uppercase",
}}
>
What's next
</Box>
<Text mt={6} fz="15.5px" fw={700} c="#10202F">
{c.title}
</Text>
<Text mt={2} fz="13px" c={urgent ? "rgba(255,255,255,0.8)" : "#6B7C8E"}>
<Text mt={2} fz="13px" c="#5B6B7A">
{c.description}
</Text>
</Box>
@@ -87,10 +102,10 @@ export function ContractCard({
<Button
onClick={() => navigate(`/bookings/${booking.id}/contract`)}
radius={10}
color={urgent ? "#0C1A2B" : "edr-green"}
leftSection={<FileSignature size={15} />}
color="edr-green"
leftSection={<FileSignature size={18} />}
styles={{
root: { height: 42, paddingInline: 16 },
root: { height: 42, paddingInline: 18 },
label: { fontSize: 13, fontWeight: 700 },
}}
>

View File

@@ -1,7 +1,33 @@
import { Box, Group, Text } from "@mantine/core";
import { CheckCircle2, FileText } from "lucide-react";
import { CheckCircle2, FileText, MinusCircle } from "lucide-react";
import type { ReactNode } from "react";
type DocStatus = "verified" | "ready" | "missing";
const STATUS_PILL: Record<
DocStatus,
{ label: string; bg: string; color: string; border?: string; icon?: ReactNode }
> = {
verified: {
label: "Uploaded",
bg: "#ECF6F1",
color: "#0A6F4D",
border: "#CDEBDD",
icon: <CheckCircle2 size={13} />,
},
ready: {
label: "Ready",
bg: "#EAF1FB",
color: "#2E5B96",
},
missing: {
label: "Not added",
bg: "#F1F4F7",
color: "#9AA8B5",
icon: <MinusCircle size={13} />,
},
};
export function DocRow({
title,
meta,
@@ -11,12 +37,15 @@ export function DocRow({
}: {
title: string;
meta: string;
status: "verified" | "ready" | "missing";
status: DocStatus;
action: ReactNode;
last?: boolean;
}) {
const missing = status === "missing";
const tileBg = status === "ready" ? "#EAF1FB" : "#F1F4F7";
const tileFg = status === "ready" ? "#2E5B96" : "#475569";
const tileFg =
status === "ready" ? "#2E5B96" : missing ? "#9AA8B5" : "#475569";
const pill = STATUS_PILL[status];
return (
<Group
@@ -41,52 +70,36 @@ export function DocRow({
color: tileFg,
}}
>
<FileText size={18} />
<FileText size={20} />
</Box>
<Box miw={0} flex={1}>
<Text truncate fz="13.5px" fw={700} c="#10202F">
<Text truncate fz="13.5px" fw={700} c={missing ? "#6B7C8E" : "#10202F"}>
{title}
</Text>
<Text truncate fz="12px" c="#9AA8B5">
{meta}
</Text>
</Box>
{status === "verified" && (
<Group
component="span"
gap={6}
align="center"
wrap="nowrap"
style={{
display: "inline-flex",
borderRadius: 999,
backgroundColor: "#ECF6F1",
padding: "5px 11px",
fontSize: 11.5,
fontWeight: 700,
color: "#0A6F4D",
}}
>
<CheckCircle2 size={13} />
Verified
</Group>
)}
{status === "ready" && (
<Box
component="span"
style={{
display: "inline-flex",
borderRadius: 999,
backgroundColor: "#EAF1FB",
padding: "5px 11px",
fontSize: 11.5,
fontWeight: 700,
color: "#2E5B96",
}}
>
Ready
</Box>
)}
<Group
component="span"
gap={6}
align="center"
wrap="nowrap"
style={{
display: "inline-flex",
flexShrink: 0,
borderRadius: 999,
backgroundColor: pill.bg,
border: pill.border ? `1px solid ${pill.border}` : undefined,
padding: "5px 11px",
fontSize: 11.5,
fontWeight: 700,
color: pill.color,
}}
>
{pill.icon}
{pill.label}
</Group>
{action}
</Group>
);
@@ -147,15 +160,15 @@ export function CountChip({
style={{
display: "inline-flex",
borderRadius: 999,
padding: "5px 11px",
padding: "5px 10px",
fontSize: 11.5,
fontWeight: 700,
backgroundColor: done ? "#ECF6F1" : "#FDF3E0",
color: done ? "#0A6F4D" : "#9A5B00",
backgroundColor: done ? "#ECF6F1" : "#F1F4F7",
color: done ? "#0A6F4D" : "#6B7C8E",
}}
>
{done && <CheckCircle2 size={13} />}
{uploaded}/{total} uploaded
{uploaded} of {total} added
</Group>
);
}

View File

@@ -1,5 +1,5 @@
import { Box, Group, Text } from "@mantine/core";
import { AlertCircle } from "lucide-react";
import { Box, Button, Group, Paper, Stack, Text } from "@mantine/core";
import { AlertCircle, AlertTriangle, PencilLine, StickyNote, XCircle } from "lucide-react";
import type { ReactNode } from "react";
export function NoticeBanner({
@@ -51,6 +51,194 @@ export function NoticeBanner({
);
}
export function ActionRequiredBanner({
title,
children,
onAction,
}: {
title: string;
children: ReactNode;
onAction?: () => void;
}) {
return (
<Group
justify="space-between"
align="center"
wrap="wrap"
gap={24}
px={22}
py={20}
style={{
borderRadius: 16,
backgroundColor: "#FDF3E0",
border: "1px solid #F4D9A8",
}}
>
<Group gap={16} align="center" wrap="nowrap" miw={0} flex={1}>
<Box
style={{
flexShrink: 0,
width: 44,
height: 44,
display: "flex",
alignItems: "center",
justifyContent: "center",
borderRadius: 12,
backgroundColor: "#fff",
border: "1px solid #F4D9A8",
color: "#C77F12",
}}
>
<AlertTriangle size={23} />
</Box>
<Box miw={0}>
<Text
fz="10.5px"
fw={800}
c="#B07A2A"
tt="uppercase"
style={{ letterSpacing: 0.6 }}
>
Action required · You
</Text>
<Text mt={5} fz="15.5px" fw={700} c="#10202F">
{title}
</Text>
<Text mt={2} fz="13px" c="#7A6A4E" style={{ lineHeight: 1.45 }}>
{children}
</Text>
</Box>
</Group>
{onAction && (
<Button
onClick={onAction}
radius={10}
color="#F2A516"
leftSection={<PencilLine size={18} color="#5A3D08" />}
styles={{
root: { height: 42, paddingInline: 18, flexShrink: 0 },
label: { fontSize: 13, fontWeight: 800, color: "#3D2A05" },
}}
>
Review & Resubmit
</Button>
)}
</Group>
);
}
export function CancelledBanner({
pillLabel = "Cancelled",
title,
subtitle,
reason,
onRebook,
}: {
pillLabel?: string;
title: string;
subtitle: string;
reason?: string | null;
onRebook?: () => void;
}) {
return (
<Paper
radius={16}
p={20}
bg="#FCEEEA"
style={{ border: "1px solid #F3C6BB" }}
>
<Stack gap={14}>
<Group justify="space-between" align="center" wrap="wrap" gap={20}>
<Group gap={16} align="center" wrap="nowrap" miw={0}>
<Box
style={{
flexShrink: 0,
width: 46,
height: 46,
display: "flex",
alignItems: "center",
justifyContent: "center",
borderRadius: 13,
backgroundColor: "#fff",
border: "1px solid #F3C6BB",
color: "#D93C15",
}}
>
<XCircle size={24} />
</Box>
<Box miw={0}>
<Box
component="span"
style={{
display: "inline-flex",
borderRadius: 999,
backgroundColor: "#D93C15",
padding: "4px 10px",
fontSize: 10.5,
fontWeight: 800,
letterSpacing: 0.3,
color: "#fff",
textTransform: "uppercase",
}}
>
{pillLabel}
</Box>
<Text mt={6} fz="15.5px" fw={700} c="#10202F">
{title}
</Text>
<Text mt={2} fz="13px" c="#6B7C8E">
{subtitle}
</Text>
</Box>
</Group>
{onRebook && (
<Button
onClick={onRebook}
radius={10}
color="edr-green"
styles={{
root: { height: 42, paddingInline: 18 },
label: { fontSize: 13, fontWeight: 700 },
}}
>
Rebook shipment
</Button>
)}
</Group>
{reason && (
<Group
gap={11}
align="flex-start"
wrap="nowrap"
p={14}
style={{
borderRadius: 12,
backgroundColor: "#fff",
border: "1px solid #F3C6BB",
}}
>
<StickyNote size={20} color="#B0341A" style={{ flexShrink: 0 }} />
<Box miw={0}>
<Text
fz="10.5px"
fw={700}
c="#B0341A"
tt="uppercase"
style={{ letterSpacing: 0.5 }}
>
Reason for cancellation
</Text>
<Text mt={3} fz="13.5px" c="#3D2A26" style={{ lineHeight: 1.45 }}>
{reason}
</Text>
</Box>
</Group>
)}
</Stack>
</Paper>
);
}
export function MutationErrors({
mutations,
}: {

View File

@@ -27,7 +27,7 @@ export function PageHeader({
<Group justify="space-between" align="flex-start" wrap="wrap" gap="md">
<Stack gap={8} miw={0}>
<Group gap={12} align="center" wrap="wrap">
<Text fz="18px" fw={800}>
<Text fz="26px" fw={800} c="#10202F">
{booking.reference}
</Text>
<Group

View File

@@ -3,11 +3,49 @@ import type { ReactNode } from "react";
import type { Freight } from "@edr/types";
import { fmtDate } from "../utils";
import { fmtDate, isDraftLike, isNegative } from "../utils";
import { CardTitle, SectionCard } from "./layout";
type Row = { label: string; value: ReactNode; muted?: boolean };
function StatusPill({ status }: { status: string }) {
const negative = isNegative(status);
const draft = isDraftLike(status);
const dot = negative ? "#C0392B" : draft ? "#94A3B8" : "#0EA371";
const color = negative ? "#A93226" : draft ? "#475569" : "#0A6F4D";
const bg = negative ? "#FBEAE7" : draft ? "#F1F4F7" : "#ECF6F1";
const border = negative ? "#F3C8C1" : draft ? "#E1E7EE" : "#CDEBDD";
const label = status
.replace(/_/g, " ")
.toLowerCase()
.replace(/\b\w/g, (m) => m.toUpperCase());
return (
<Group
component="span"
gap={6}
align="center"
wrap="nowrap"
style={{
display: "inline-flex",
borderRadius: 999,
backgroundColor: bg,
border: `1px solid ${border}`,
padding: "4px 10px",
fontSize: 11.5,
fontWeight: 700,
color,
}}
>
<Box
component="span"
style={{ width: 6, height: 6, borderRadius: 999, backgroundColor: dot }}
/>
{label}
</Group>
);
}
export function ScheduleCard({
booking,
title,
@@ -30,6 +68,11 @@ export function ScheduleCard({
muted: !booking.trainId,
};
const statusRow: Row = {
label: "Status",
value: <StatusPill status={booking.status as string} />,
};
const rows: Row[] = consignment
? [
{ label: "Consignment ID", value: booking.reference },
@@ -40,6 +83,7 @@ export function ScheduleCard({
{ label: "Consolidation", value: consolidation },
]
: [
statusRow,
{ label: "Service", value: service },
{ label: "Equipment return", value: equipmentReturn },
{ label: "Proposed date", value: fmtDate(booking.scheduledDate) },

View File

@@ -1,5 +1,5 @@
import { Box, Group, Text } from "@mantine/core";
import { AlertTriangle, Check, Clock, FileText, History } from "lucide-react";
import { AlertTriangle, Check, FileText, History } from "lucide-react";
import type { Freight } from "@edr/types";
@@ -7,7 +7,13 @@ import { PROGRESS_STAGES, STATUS_MAP } from "../constants";
import { fmtDate, isDraftLike, isNegative } from "../utils";
import { SectionCard } from "./layout";
export function StatusHero({ booking }: { booking: Freight.IBooking }) {
export function StatusHero({
booking,
children,
}: {
booking: Freight.IBooking;
children?: React.ReactNode;
}) {
const status = booking.status as string;
const cfg = STATUS_MAP[status] ?? STATUS_MAP.DRAFT;
const negative = isNegative(status);
@@ -34,7 +40,7 @@ export function StatusHero({ booking }: { booking: Freight.IBooking }) {
);
return (
<SectionCard>
<SectionCard p={28}>
<Group justify="space-between" align="center" wrap="wrap" gap="md">
<Group gap={16} align="center" wrap="nowrap">
<Box
@@ -52,10 +58,10 @@ export function StatusHero({ booking }: { booking: Freight.IBooking }) {
<HeroIcon size={26} />
</Box>
<Box>
<Text size="lg" fw={700}>
<Text fz="21px" fw={800} c="#10202F">
{cfg.title}
</Text>
<Text mt={4} fz="14px" c="#6B7C8E">
<Text mt={5} fz="14px" c="#6B7C8E">
{cfg.description}
</Text>
</Box>
@@ -72,25 +78,33 @@ export function StatusHero({ booking }: { booking: Freight.IBooking }) {
backgroundColor: "#F7FAFC",
}}
>
<Clock size={20} color="#0A6F4D" />
<History size={22} color="#64748B" />
<Box>
<Text fz="12px" fw={600}>
<Text
fz="10.5px"
fw={700}
c="#9AA8B5"
tt="uppercase"
style={{ letterSpacing: 0.6 }}
>
{chipLabel}
</Text>
<Text fz="13px" fw={700}>
<Text fz="13.5px" fw={700} c="#10202F">
{chipValue}
</Text>
</Box>
</Group>
</Group>
<Box my={24} h={1} w="100%" bg="#EEF2F6" />
<Box my={26} h={1} w="100%" bg="#EEF2F6" />
<ProgressTracker
current={cfg.stage}
tone={draft ? "ink" : "green"}
negative={negative}
/>
{children ?? (
<ProgressTracker
current={cfg.stage}
tone={draft ? "ink" : "green"}
negative={negative}
/>
)}
</SectionCard>
);
}
@@ -185,7 +199,7 @@ function ProgressTracker({
</Box>
<Text
fz="13.5px"
fw={700}
fw={state === "active" ? 800 : 700}
ta="center"
c={state === "idle" ? "#9AA8B5" : "#10202F"}
>
@@ -193,7 +207,7 @@ function ProgressTracker({
</Text>
<Text
fz="11.5px"
fw={500}
fw={state === "active" ? 700 : 500}
ta="center"
c={state === "active" ? activeSub : "#9AA8B5"}
>

View File

@@ -2,6 +2,41 @@ import { Box, Button, Group, Text } from "@mantine/core";
import { Check } from "lucide-react";
import type { ReactNode } from "react";
function StepCircle({
index,
done,
active,
}: {
index: number;
done?: boolean;
active?: boolean;
}) {
const style: React.CSSProperties = done
? { backgroundColor: "#0EA371", color: "#fff" }
: active
? { backgroundColor: "#0C1A2B", color: "#fff" }
: { backgroundColor: "#EEF2F6", color: "#9AA8B5" };
return (
<Box
style={{
flexShrink: 0,
width: 26,
height: 26,
display: "flex",
alignItems: "center",
justifyContent: "center",
borderRadius: 999,
fontSize: 12,
fontWeight: 700,
...style,
}}
>
{done ? <Check size={16} strokeWidth={3} /> : index}
</Box>
);
}
export function StepLine({
index,
title,
@@ -9,63 +44,49 @@ export function StepLine({
action,
done,
active,
highlight,
}: {
index: number;
title: string;
desc: string;
desc?: string;
action?: ReactNode;
done?: boolean;
active?: boolean;
highlight?: boolean;
}) {
const badgeStyle: React.CSSProperties = done
? { backgroundColor: "#0EA371", color: "#fff" }
: active
? { backgroundColor: "#0C1A2B", color: "#fff" }
: {
backgroundColor: "#EEF2F6",
color: "#9AA8B5",
border: "1px solid #E1E7EE",
};
if (active) {
return (
<Group
gap={14}
align="center"
justify="space-between"
wrap="nowrap"
px={14}
py={12}
style={{ borderRadius: 12, backgroundColor: "#F4F7FA" }}
>
<Group gap={14} align="center" wrap="nowrap" miw={0}>
<StepCircle index={index} active />
<Box miw={0}>
<Text fz="14px" fw={800} c="#10202F">
{title}
</Text>
{desc && (
<Text mt={2} fz="12.5px" c="#6B7C8E">
{desc}
</Text>
)}
</Box>
</Group>
{action}
</Group>
);
}
return (
<Group
gap={14}
align="center"
wrap="nowrap"
px="sm"
py={12}
style={{
borderRadius: 12,
backgroundColor: highlight ? "#F4F7FA" : undefined,
}}
>
<Box
style={{
flexShrink: 0,
width: 28,
height: 28,
display: "flex",
alignItems: "center",
justifyContent: "center",
borderRadius: 999,
fontSize: 12,
fontWeight: 700,
...badgeStyle,
}}
>
{done ? <Check size={14} /> : index}
</Box>
<Box miw={0} flex={1}>
<Text fz="13.5px" fw={700} c="#10202F">
{title}
</Text>
<Text fz="12.5px" c="#9AA8B5">
{desc}
</Text>
</Box>
{action}
<Group gap={14} align="center" wrap="nowrap" px={12} py={10}>
<StepCircle index={index} done={done} />
<Text fz="14px" fw={700} c={done ? "#6B7C8E" : "#9AA8B5"}>
{title}
</Text>
</Group>
);
}
@@ -73,18 +94,26 @@ export function StepLine({
export function StepGhostButton({
children,
onClick,
icon,
}: {
children: ReactNode;
onClick?: () => void;
icon?: ReactNode;
}) {
return (
<Button
variant="default"
variant="white"
radius={9}
onClick={onClick}
leftSection={icon}
styles={{
root: { height: 32, paddingInline: 12 },
label: { fontSize: 12.5, fontWeight: 700, color: "#475569" },
root: {
height: 34,
paddingInline: 14,
border: "1.5px solid #CBD5E1",
flexShrink: 0,
},
label: { fontSize: 12.5, fontWeight: 700, color: "#334155" },
}}
>
{children}

View File

@@ -4,7 +4,7 @@ import type { ReactNode, Ref } from "react";
export function PageShell({ children }: { children: ReactNode }) {
return (
<Box mih="100vh">
<Box mx="auto" w="100%" px="lg" py={28}>
<Box mx="auto" w="100%" px={32} pt={28} pb={32}>
<Stack gap="lg">{children}</Stack>
</Box>
</Box>
@@ -48,7 +48,7 @@ export function SectionCard({ children, ref, ...props }: SectionCardProps) {
export function CardTitle({ children }: { children: ReactNode }) {
return (
<Text size="lg" fw={700}>
<Text fz="16px" fw={800} c="#10202F">
{children}
</Text>
);

View File

@@ -1,5 +1,5 @@
import { Box, Button, Group, Stack, Text } from "@mantine/core";
import { FileText } from "lucide-react";
import { CheckCircle2, Clock, FileText } from "lucide-react";
import type { Freight } from "@edr/types";
@@ -41,24 +41,45 @@ export function EstimateCard({
<SectionCard p={22}>
<Group justify="space-between" align="center">
<CardTitle>{title}</CardTitle>
<Box
<Group
component="span"
gap={6}
align="center"
wrap="nowrap"
style={{
display: "inline-flex",
borderRadius: 999,
backgroundColor: "#F1F4F7",
padding: "5px 11px",
fontSize: 11.5,
fontWeight: 700,
color: "#475569",
color: "#6B7C8E",
}}
>
<Clock size={13} />
{chip}
</Box>
</Group>
</Group>
<Box mt={12}>
<Text fz="26px" fw={800} c="#10202F">
{priceTotal(pricing)}
</Text>
<Group gap={8} align="flex-end" wrap="nowrap">
<Text fz="26px" fw={800} c="#10202F" lh={1.1}>
{priceTotal(pricing)}
</Text>
<Box
component="span"
mb={4}
style={{
borderRadius: 6,
backgroundColor: "#F1F4F7",
padding: "3px 7px",
fontSize: 11,
fontWeight: 700,
color: "#6B7C8E",
}}
>
est.
</Box>
</Group>
<Text mt={4} fz="12.5px" c="#9AA8B5">
A firm price is confirmed after EDR reviews your booking.
</Text>
@@ -101,9 +122,13 @@ export function PaymentCard({
<SectionCard p={22}>
<Group justify="space-between" align="center">
<CardTitle>Payment</CardTitle>
<Box
<Group
component="span"
gap={6}
align="center"
wrap="nowrap"
style={{
display: "inline-flex",
borderRadius: 999,
padding: "5px 11px",
fontSize: 11.5,
@@ -113,10 +138,11 @@ export function PaymentCard({
border: paid ? "1px solid #CDEBDD" : undefined,
}}
>
{paid && <CheckCircle2 size={13} />}
{paid
? "Paid"
: (booking.paymentStatus?.replace(/_/g, " ") ?? "Pending")}
</Box>
</Group>
</Group>
<Box mt={12}>
<Text fz="26px" fw={800} c="#10202F">