mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-30 08:48:11 +00:00
style: enhance UI components for New Booking page and Step Indicator
This commit is contained in:
@@ -22,7 +22,7 @@ import {
|
|||||||
Sun,
|
Sun,
|
||||||
User,
|
User,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { Fragment, type ReactNode, useState } from "react";
|
import { type CSSProperties, Fragment, type ReactNode, useState } from "react";
|
||||||
|
|
||||||
export interface SidebarItem {
|
export interface SidebarItem {
|
||||||
label: string;
|
label: string;
|
||||||
@@ -47,6 +47,18 @@ export interface AppLayoutProps {
|
|||||||
type Theme = "light" | "dark";
|
type Theme = "light" | "dark";
|
||||||
const THEME_KEY = "edr-theme";
|
const THEME_KEY = "edr-theme";
|
||||||
|
|
||||||
|
const EDR = {
|
||||||
|
primary: "#0EA371",
|
||||||
|
primaryDark: "#0A6F4D",
|
||||||
|
soft: "#ECF6F1",
|
||||||
|
border: "#E6ECF2",
|
||||||
|
bg: "#F4F7FA",
|
||||||
|
text: "#10202F",
|
||||||
|
muted: "#6B7C8E",
|
||||||
|
ink: "#0C1A2B",
|
||||||
|
accent: "#F2A516",
|
||||||
|
};
|
||||||
|
|
||||||
function getStoredTheme(): Theme {
|
function getStoredTheme(): Theme {
|
||||||
if (typeof window === "undefined") return "light";
|
if (typeof window === "undefined") return "light";
|
||||||
const stored = localStorage.getItem(THEME_KEY);
|
const stored = localStorage.getItem(THEME_KEY);
|
||||||
@@ -82,24 +94,18 @@ function getActivePage(items: SidebarItem[], activePath: string): { label: strin
|
|||||||
const navClassNames = (active: boolean) => {
|
const navClassNames = (active: boolean) => {
|
||||||
if (active) {
|
if (active) {
|
||||||
return {
|
return {
|
||||||
root: "rounded-[10px] font-medium transition-all duration-150 bg-[var(--mantine-color-edr-soft-0)]! ring-1 ring-inset ring-[var(--mantine-color-edr-green-5)]/15",
|
root: `rounded-[10px] font-medium transition-all duration-150 bg-[#ECF6F1]! ring-1 ring-inset ring-[#0EA371]/15`,
|
||||||
label: "text-[var(--mantine-color-edr-green-7)]! font-bold!",
|
label: `text-[#0A6F4D]! font-bold!`,
|
||||||
section: "text-[var(--mantine-color-edr-green-7)]!",
|
section: `text-[#0A6F4D]!`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
root: "rounded-[10px] font-medium transition-all duration-150 hover:bg-[var(--mantine-color-edr-slate-soft-0)]!",
|
root: `rounded-[10px] font-medium transition-all duration-150 hover:bg-[#F1F4F7]!`,
|
||||||
label: "text-[var(--mantine-color-edr-slate-0)]! font-semibold! hover:text-[var(--mantine-color-edr-ink-0)]!",
|
label: `text-[#3D4D5C]! font-semibold! hover:text-[#0C1A2B]!`,
|
||||||
section: "text-[var(--mantine-color-edr-muted-0)]! hover:text-[var(--mantine-color-edr-ink-0)]!",
|
section: `text-[#54657A]! hover:text-[#0C1A2B]!`,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
// 36×36 header control chips
|
|
||||||
const islandCls =
|
|
||||||
"size-9 rounded-full border border-[var(--mantine-color-edr-border-0)] bg-white flex items-center justify-center shrink-0 cursor-pointer";
|
|
||||||
const squareIslandCls =
|
|
||||||
"size-9 rounded-[10px] border border-[var(--mantine-color-edr-border-0)] bg-white flex items-center justify-center shrink-0 cursor-pointer";
|
|
||||||
|
|
||||||
export function AppLayout({
|
export function AppLayout({
|
||||||
title = "EDR Freight",
|
title = "EDR Freight",
|
||||||
sidebarItems,
|
sidebarItems,
|
||||||
@@ -133,6 +139,21 @@ export function AppLayout({
|
|||||||
activePath === item.href.toLowerCase() ||
|
activePath === item.href.toLowerCase() ||
|
||||||
activePath.startsWith(item.href.toLowerCase() + "/");
|
activePath.startsWith(item.href.toLowerCase() + "/");
|
||||||
|
|
||||||
|
// Shared header "island" styles — every control is a consistent 36px chip.
|
||||||
|
const islandStyle: CSSProperties = {
|
||||||
|
width: 36,
|
||||||
|
height: 36,
|
||||||
|
borderRadius: 999,
|
||||||
|
border: `1px solid ${EDR.border}`,
|
||||||
|
backgroundColor: "#fff",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
flexShrink: 0,
|
||||||
|
cursor: "pointer",
|
||||||
|
};
|
||||||
|
const toggleStyle: CSSProperties = { ...islandStyle, borderRadius: 10 };
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AppShell
|
<AppShell
|
||||||
layout="alt"
|
layout="alt"
|
||||||
@@ -140,27 +161,37 @@ export function AppLayout({
|
|||||||
header={{ height: 56 }}
|
header={{ height: 56 }}
|
||||||
padding={0}
|
padding={0}
|
||||||
>
|
>
|
||||||
{/* ── Header ── */}
|
{/* ── Header (frosted glass; compact floating islands, mirrors the pen) ── */}
|
||||||
<AppShell.Header
|
<AppShell.Header
|
||||||
withBorder={false}
|
withBorder={false}
|
||||||
style={{
|
style={{
|
||||||
backdropFilter: "blur(14px)",
|
backdropFilter: "blur(14px)",
|
||||||
WebkitBackdropFilter: "blur(14px)",
|
WebkitBackdropFilter: "blur(14px)",
|
||||||
background: "transparent",
|
|
||||||
boxShadow: "none",
|
|
||||||
border: "none",
|
border: "none",
|
||||||
|
boxShadow: "none",
|
||||||
|
background:'transparent',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Group h="100%" px={24} justify="space-between" wrap="nowrap">
|
<Group h="100%" px={24} justify="space-between" wrap="nowrap">
|
||||||
|
{/* Left: sidebar toggle + page title */}
|
||||||
<Group gap={12} wrap="nowrap" align="center">
|
<Group gap={12} wrap="nowrap" align="center">
|
||||||
<UnstyledButton onClick={toggleMobile} className={squareIslandCls} aria-label="Toggle sidebar">
|
<UnstyledButton onClick={toggleMobile} style={toggleStyle} aria-label="Toggle sidebar">
|
||||||
<MenuIcon size={18} color="var(--mantine-color-edr-text-0)" strokeWidth={1.8} />
|
<MenuIcon size={18} color={EDR.text} strokeWidth={1.8} />
|
||||||
</UnstyledButton>
|
</UnstyledButton>
|
||||||
<Text fw={700} fz="lg" c="edr-text">
|
<Text
|
||||||
|
style={{
|
||||||
|
fontFamily: '"Inter", sans-serif',
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: 700,
|
||||||
|
color: EDR.text,
|
||||||
|
lineHeight: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
{activePage ? activePage.label : title}
|
{activePage ? activePage.label : title}
|
||||||
</Text>
|
</Text>
|
||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
|
{/* Right: search + bell + avatar */}
|
||||||
<Group gap={10} wrap="nowrap" align="center">
|
<Group gap={10} wrap="nowrap" align="center">
|
||||||
{/* Search pill */}
|
{/* Search pill */}
|
||||||
<Group
|
<Group
|
||||||
@@ -171,22 +202,22 @@ export function AppLayout({
|
|||||||
width: 260,
|
width: 260,
|
||||||
height: 36,
|
height: 36,
|
||||||
borderRadius: 999,
|
borderRadius: 999,
|
||||||
border: "1px solid var(--mantine-color-edr-border-0)",
|
border: `1px solid ${EDR.border}`,
|
||||||
backgroundColor: "var(--mantine-color-white)",
|
backgroundColor: "#fff",
|
||||||
padding: "0 14px",
|
padding: "0 14px",
|
||||||
cursor: "text",
|
cursor: "text",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Search size={15} color="var(--mantine-color-edr-muted-0)" strokeWidth={1.8} />
|
<Search size={15} color={EDR.muted} strokeWidth={1.8} />
|
||||||
<Text size="sm" c="edr-muted" style={{ userSelect: "none" }}>
|
<Text size="sm" style={{ color: EDR.muted, userSelect: "none" }}>
|
||||||
Search shipments, bookings…
|
Search shipments, bookings…
|
||||||
</Text>
|
</Text>
|
||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
{/* Bell */}
|
{/* Bell */}
|
||||||
<Box style={{ position: "relative" }}>
|
<Box style={{ position: "relative" }}>
|
||||||
<UnstyledButton className={islandCls} aria-label="Notifications">
|
<UnstyledButton style={islandStyle} aria-label="Notifications">
|
||||||
<Bell size={17} color="var(--mantine-color-edr-text-0)" strokeWidth={1.8} />
|
<Bell size={17} color={EDR.text} strokeWidth={1.8} />
|
||||||
</UnstyledButton>
|
</UnstyledButton>
|
||||||
<Box
|
<Box
|
||||||
style={{
|
style={{
|
||||||
@@ -196,30 +227,30 @@ export function AppLayout({
|
|||||||
width: 7,
|
width: 7,
|
||||||
height: 7,
|
height: 7,
|
||||||
borderRadius: "50%",
|
borderRadius: "50%",
|
||||||
backgroundColor: "var(--mantine-color-edr-accent-0)",
|
backgroundColor: EDR.accent,
|
||||||
border: "1.5px solid var(--mantine-color-white)",
|
border: "1.5px solid #fff",
|
||||||
pointerEvents: "none",
|
pointerEvents: "none",
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{enableThemeToggle && (
|
{enableThemeToggle && (
|
||||||
<UnstyledButton onClick={toggleTheme} className={islandCls} aria-label="Toggle theme">
|
<UnstyledButton onClick={toggleTheme} style={islandStyle} aria-label="Toggle theme">
|
||||||
{theme === "dark"
|
{theme === "dark"
|
||||||
? <Sun size={17} color="var(--mantine-color-edr-text-0)" strokeWidth={1.8} />
|
? <Sun size={17} color={EDR.text} strokeWidth={1.8} />
|
||||||
: <Moon size={17} color="var(--mantine-color-edr-text-0)" strokeWidth={1.8} />}
|
: <Moon size={17} color={EDR.text} strokeWidth={1.8} />}
|
||||||
</UnstyledButton>
|
</UnstyledButton>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Avatar dropdown */}
|
{/* Avatar pill */}
|
||||||
<Menu width={220} position="bottom-end" withinPortal shadow="md" offset={8} radius="md">
|
<Menu width={220} position="bottom-end" withinPortal shadow="md" offset={8} radius="md">
|
||||||
<Menu.Target>
|
<Menu.Target>
|
||||||
<UnstyledButton
|
<UnstyledButton
|
||||||
style={{
|
style={{
|
||||||
height: 36,
|
height: 36,
|
||||||
borderRadius: 999,
|
borderRadius: 999,
|
||||||
border: "1px solid var(--mantine-color-edr-border-0)",
|
border: `1px solid ${EDR.border}`,
|
||||||
backgroundColor: "var(--mantine-color-white)",
|
backgroundColor: "#fff",
|
||||||
padding: "0 10px 0 4px",
|
padding: "0 10px 0 4px",
|
||||||
display: "flex",
|
display: "flex",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
@@ -230,12 +261,13 @@ export function AppLayout({
|
|||||||
<Avatar radius="xl" size={28} color="edr-green.5">
|
<Avatar radius="xl" size={28} color="edr-green.5">
|
||||||
<Text fw={700} fz={12} c="white">{initials}</Text>
|
<Text fw={700} fz={12} c="white">{initials}</Text>
|
||||||
</Avatar>
|
</Avatar>
|
||||||
<ChevronDown size={15} color="var(--mantine-color-edr-muted-0)" strokeWidth={1.8} />
|
<ChevronDown size={15} color={EDR.muted} strokeWidth={1.8} />
|
||||||
</UnstyledButton>
|
</UnstyledButton>
|
||||||
</Menu.Target>
|
</Menu.Target>
|
||||||
|
|
||||||
<Menu.Dropdown>
|
<Menu.Dropdown>
|
||||||
<Box px="sm" py="xs">
|
<Box px="sm" py="xs">
|
||||||
<Text size="sm" fw={600} truncate c="edr-text">{userName}</Text>
|
<Text size="sm" fw={600} truncate style={{ color: EDR.text }}>{userName}</Text>
|
||||||
{userEmail && <Text size="xs" c="dimmed" truncate>{userEmail}</Text>}
|
{userEmail && <Text size="xs" c="dimmed" truncate>{userEmail}</Text>}
|
||||||
</Box>
|
</Box>
|
||||||
<Divider />
|
<Divider />
|
||||||
@@ -255,33 +287,49 @@ export function AppLayout({
|
|||||||
<AppShell.Navbar
|
<AppShell.Navbar
|
||||||
withBorder={false}
|
withBorder={false}
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: "var(--mantine-color-white)",
|
backgroundColor: "#ffffff",
|
||||||
borderRight: "1px solid var(--mantine-color-edr-border-0)",
|
borderRight: `1px solid ${EDR.border}`,
|
||||||
display: "flex",
|
display: "flex",
|
||||||
flexDirection: "column",
|
flexDirection: "column",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* Brand */}
|
{/* Brand */}
|
||||||
<Box className="h-[60px] shrink-0 flex items-center px-[18px]">
|
<Box
|
||||||
|
style={{
|
||||||
|
height: 60,
|
||||||
|
flexShrink: 0,
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
padding: "0 18px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
<Group gap={11} wrap="nowrap">
|
<Group gap={11} wrap="nowrap">
|
||||||
<img
|
<img
|
||||||
src="/assets/edr-logo.png"
|
src="/assets/edr-logo.png"
|
||||||
alt="EDR"
|
alt="EDR"
|
||||||
className="size-10 object-contain shrink-0"
|
style={{ width: 40, height: 40, objectFit: "contain", flexShrink: 0 }}
|
||||||
/>
|
/>
|
||||||
<Box>
|
<Box>
|
||||||
<Text
|
<Text
|
||||||
fw={800}
|
|
||||||
style={{
|
style={{
|
||||||
|
fontFamily: '"Inter", sans-serif',
|
||||||
|
fontWeight: 800,
|
||||||
fontSize: 19,
|
fontSize: 19,
|
||||||
letterSpacing: "0.03em",
|
letterSpacing: "0.03em",
|
||||||
color: "var(--mantine-color-edr-green-5)",
|
color: EDR.primary,
|
||||||
lineHeight: 1.15,
|
lineHeight: 1.15,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
EDR FREIGHT
|
EDR FREIGHT
|
||||||
</Text>
|
</Text>
|
||||||
<Text fz={10.5} fw={500} c="edr-muted" style={{ lineHeight: 1.2 }}>
|
<Text
|
||||||
|
style={{
|
||||||
|
fontSize: 10.5,
|
||||||
|
fontWeight: 500,
|
||||||
|
color: EDR.muted,
|
||||||
|
lineHeight: 1.2,
|
||||||
|
}}
|
||||||
|
>
|
||||||
Ethio–Djibouti Railway
|
Ethio–Djibouti Railway
|
||||||
</Text>
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
@@ -302,13 +350,17 @@ export function AppLayout({
|
|||||||
item.section && item.section !== prevSection ? (
|
item.section && item.section !== prevSection ? (
|
||||||
<Text
|
<Text
|
||||||
key={`section-${item.section}`}
|
key={`section-${item.section}`}
|
||||||
fz={11}
|
size="xs"
|
||||||
tt="uppercase"
|
tt="uppercase"
|
||||||
px="sm"
|
px="sm"
|
||||||
mt={i === 0 ? 4 : "md"}
|
mt={i === 0 ? 4 : "md"}
|
||||||
mb={4}
|
mb={4}
|
||||||
c="edr-muted"
|
style={{
|
||||||
style={{ fontWeight: 600, letterSpacing: "0.08em" }}
|
fontWeight: 600,
|
||||||
|
letterSpacing: "0.08em",
|
||||||
|
color: EDR.muted,
|
||||||
|
fontSize: 11,
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{item.section}
|
{item.section}
|
||||||
</Text>
|
</Text>
|
||||||
@@ -369,38 +421,42 @@ export function AppLayout({
|
|||||||
marginBottom: 10,
|
marginBottom: 10,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
{/* Train image fills the full card */}
|
||||||
<img
|
<img
|
||||||
src="/assets/train-edr.jpg"
|
src="/assets/train-edr.jpg"
|
||||||
alt=""
|
alt=""
|
||||||
style={{
|
style={{
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
bottom: "-16px",
|
bottom:"-16px",
|
||||||
left: 0,
|
left:0,
|
||||||
right: "-70px",
|
right:"-70px",
|
||||||
width: "120%",
|
width: "120%",
|
||||||
objectFit: "cover",
|
objectFit: "cover",
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* Diagonal green overlay: lower-left → upper-right cut */}
|
||||||
<Box
|
<Box
|
||||||
style={{
|
style={{
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
inset: 0,
|
inset: 0,
|
||||||
top: "-75%",
|
top:"-75%",
|
||||||
background: "linear-gradient(165deg, #006149 40%, #0DC9A4 70%, #0DC9A402 80%)",
|
background: "linear-gradient(165deg, #006149 40%, #0DC9A4 70%, #0DC9A402 80%)",
|
||||||
clipPath: "polygon(0 0, 100% 0, 100% 58%, 0 72%)",
|
clipPath: "polygon(0 0, 100% 0, 100% 58%, 0 72%)",
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Box
|
|
||||||
style={{ position: "relative", zIndex: 1, padding: "16px 16px 0" }}
|
{/* Text sits on top of the green overlay */}
|
||||||
className="max-w-[180px]"
|
<Box style={{ position: "relative", zIndex: 1, padding: "16px 16px 0" }} className="max-w-[180px]">
|
||||||
>
|
<Text style={{ fontSize: 15, fontWeight: 800, color: "#fff", lineHeight: 1.3, marginBottom: 4 }}>
|
||||||
<Text fw={800} style={{ fontSize: 15, color: "#fff", lineHeight: 1.3, marginBottom: 4 }}>
|
|
||||||
Moving Africa Forward
|
Moving Africa Forward
|
||||||
</Text>
|
</Text>
|
||||||
<Text style={{ fontSize: 11, color: "rgba(255,255,255,0.7)", lineHeight: 1.5 }}>
|
<Text style={{ fontSize: 11, color: "rgba(255,255,255,0.7)", lineHeight: 1.5 }}>
|
||||||
Reliable. Efficient. Connected.
|
Reliable. Efficient. Connected.
|
||||||
</Text>
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
|
{/* Learn More — visible on the image area */}
|
||||||
<Box
|
<Box
|
||||||
style={{
|
style={{
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
@@ -410,24 +466,22 @@ export function AppLayout({
|
|||||||
display: "inline-flex",
|
display: "inline-flex",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
gap: 5,
|
gap: 5,
|
||||||
backgroundColor: "var(--mantine-color-white)",
|
backgroundColor: "#fff",
|
||||||
borderRadius: 8,
|
borderRadius: 8,
|
||||||
padding: "6px 12px",
|
padding: "6px 12px",
|
||||||
cursor: "pointer",
|
cursor: "pointer",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Text fw={600} style={{ fontSize: 12, color: "var(--mantine-color-edr-green-7)" }}>
|
<Text style={{ fontSize: 12, fontWeight: 600, color: EDR.primaryDark }}>Learn More</Text>
|
||||||
Learn More
|
<span style={{ fontSize: 12, color: EDR.primaryDark }}>→</span>
|
||||||
</Text>
|
|
||||||
<span style={{ fontSize: 12, color: "var(--mantine-color-edr-green-7)" }}>→</span>
|
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{/* Profile footer */}
|
{/* Profile */}
|
||||||
<Box
|
<Box
|
||||||
style={{
|
style={{
|
||||||
borderRadius: 12,
|
borderRadius: 12,
|
||||||
border: "1px solid var(--mantine-color-edr-border-0)",
|
border: `1px solid ${EDR.border}`,
|
||||||
padding: "10px",
|
padding: "10px",
|
||||||
display: "flex",
|
display: "flex",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
@@ -435,20 +489,25 @@ export function AppLayout({
|
|||||||
cursor: "pointer",
|
cursor: "pointer",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Avatar radius="xl" size={38} color="edr-green.5" style={{ flexShrink: 0 }}>
|
<Avatar
|
||||||
|
radius="xl"
|
||||||
|
size={38}
|
||||||
|
color="edr-green.5"
|
||||||
|
style={{ flexShrink: 0 }}
|
||||||
|
>
|
||||||
<Text fw={600} fz={13} c="white">{initials}</Text>
|
<Text fw={600} fz={13} c="white">{initials}</Text>
|
||||||
</Avatar>
|
</Avatar>
|
||||||
<Box style={{ minWidth: 0, flex: 1 }}>
|
<Box style={{ minWidth: 0, flex: 1 }}>
|
||||||
<Text size="sm" fw={600} truncate c="edr-text" style={{ lineHeight: 1.3 }}>
|
<Text size="sm" fw={600} truncate style={{ color: EDR.text, lineHeight: 1.3 }}>
|
||||||
{userName}
|
{userName}
|
||||||
</Text>
|
</Text>
|
||||||
{userEmail && (
|
{userEmail && (
|
||||||
<Text size="xs" truncate c="edr-muted" style={{ lineHeight: 1.3 }}>
|
<Text size="xs" truncate style={{ color: EDR.muted, lineHeight: 1.3 }}>
|
||||||
{userEmail}
|
{userEmail}
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
<ChevronDown size={16} color="var(--mantine-color-edr-muted-0)" style={{ flexShrink: 0 }} />
|
<ChevronDown size={16} color={EDR.muted} style={{ flexShrink: 0 }} />
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
</AppShell.Navbar>
|
</AppShell.Navbar>
|
||||||
@@ -456,9 +515,9 @@ export function AppLayout({
|
|||||||
{/* ── Main ── */}
|
{/* ── Main ── */}
|
||||||
<AppShell.Main
|
<AppShell.Main
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: "var(--mantine-color-edr-bg-0)",
|
backgroundColor: EDR.bg,
|
||||||
backgroundImage:
|
backgroundImage:
|
||||||
"radial-gradient(58% 42% at 100% 0%, rgba(14,163,113,0.16) 0%, rgba(14,163,113,0.06) 38%, rgba(14,163,113,0) 72%)",
|
"radial-gradient(58% 42% at 100% 0%, rgba(14,163,113,0.18) 0%, rgba(14,163,113,0.0.8) 38%, rgba(14,163,113,0.04) 72%)",
|
||||||
backgroundRepeat: "no-repeat",
|
backgroundRepeat: "no-repeat",
|
||||||
backgroundAttachment: "fixed",
|
backgroundAttachment: "fixed",
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import { useMemo, useState } from "react";
|
|
||||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
|
||||||
import { useForm } from "react-hook-form";
|
|
||||||
import { useNavigate } from "react-router-dom";
|
|
||||||
import { AlertCircle, Check, ChevronLeft, ChevronRight } from "lucide-react";
|
|
||||||
import { Alert, Box, Button, Text } from "@mantine/core";
|
|
||||||
import { api } from "@/services/api";
|
import { api } from "@/services/api";
|
||||||
import type { CreateBookingPayload } from "@/services/bookings.service";
|
import type { CreateBookingPayload } from "@/services/bookings.service";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { Alert, Box, Button, Group, Text, Title } from "@mantine/core";
|
||||||
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
|
import { AlertCircle, Check, ChevronLeft, ChevronRight } from "lucide-react";
|
||||||
|
import { useMemo, useState } from "react";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
import {
|
import {
|
||||||
BookingFormInputValues,
|
BookingFormInputValues,
|
||||||
STEPS,
|
STEPS,
|
||||||
@@ -99,14 +99,6 @@ export default function NewBookingPage() {
|
|||||||
const findShippingLineId = (name: string): string | undefined =>
|
const findShippingLineId = (name: string): string | undefined =>
|
||||||
shippingLines.find((l) => l.name === name)?.id;
|
shippingLines.find((l) => l.name === name)?.id;
|
||||||
|
|
||||||
const findCargoTypeId = (name: string): string | undefined => {
|
|
||||||
for (const group of cargoTree) {
|
|
||||||
const child = group.children?.find((c) => c.name === name);
|
|
||||||
if (child) return child.id;
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
};
|
|
||||||
|
|
||||||
const findContainerCargoTypeId = (): string => {
|
const findContainerCargoTypeId = (): string => {
|
||||||
const group = cargoTree.find(
|
const group = cargoTree.find(
|
||||||
(g) => g.code === "CONTAINER" || /container/i.test(g.name),
|
(g) => g.code === "CONTAINER" || /container/i.test(g.name),
|
||||||
@@ -199,102 +191,145 @@ export default function NewBookingPage() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form
|
<Box
|
||||||
id="new-booking-form"
|
style={{
|
||||||
className="flex flex-col"
|
padding: "28px 0 0",
|
||||||
onSubmit={handleSubmit}
|
minHeight: "calc(100dvh - var(--app-shell-header-height, 56px))",
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{/* Step indicator — sticky */}
|
{/* ── Page header ─────────────────────────────────────────────────── */}
|
||||||
<Box className="sticky top-0 z-20 border-b border-gray-100 bg-white/90 backdrop-blur-sm">
|
<Group justify="space-between" px={"lg"} align="flex-end" wrap="wrap" gap="md" mb="lg">
|
||||||
<Box className="mx-auto max-w-4xl px-6 py-4">
|
<Box>
|
||||||
<StepIndicator step={step} />
|
<Title order={1} fw={800} fz={26} style={{ letterSpacing: "-0.01em" }}>
|
||||||
|
New Booking
|
||||||
|
</Title>
|
||||||
|
<Text size="sm" c="edr-muted" mt={4}>
|
||||||
|
Fill in the details below to book your freight shipment.
|
||||||
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
<Button
|
||||||
|
variant="default"
|
||||||
|
radius="md"
|
||||||
|
leftSection={<ChevronLeft size={16} />}
|
||||||
|
onClick={() => navigate("/bookings")}
|
||||||
|
>
|
||||||
|
Back to Bookings
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
|
||||||
{/* Step content */}
|
<form
|
||||||
<Box flex={1}>
|
id="new-booking-form"
|
||||||
<Box className="mx-auto max-w-4xl px-6 py-8">
|
className="flex flex-col"
|
||||||
{createMutation.isError && (
|
style={{ flex: 1 }}
|
||||||
<Alert
|
onSubmit={handleSubmit}
|
||||||
color="red"
|
>
|
||||||
icon={<AlertCircle size={16} />}
|
{/* Step indicator */}
|
||||||
radius="md"
|
<Box
|
||||||
mb="lg"
|
>
|
||||||
>
|
<Box className="mx-auto max-w-5xl" style={{ paddingInline: "16px" , }}>
|
||||||
<Text size="sm" fw={600}>
|
<StepIndicator step={step} />
|
||||||
Failed to save draft
|
</Box>
|
||||||
</Text>
|
|
||||||
<Text size="sm" mt={4} c="red.7">
|
|
||||||
{createMutation.error instanceof Error
|
|
||||||
? createMutation.error.message
|
|
||||||
: "An unexpected error occurred. Please try again."}
|
|
||||||
</Text>
|
|
||||||
</Alert>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{step === 1 && <Step1ContractType form={form} />}
|
|
||||||
{step === 2 && <Step2ServiceType form={form} />}
|
|
||||||
{step === 3 && (
|
|
||||||
<Step4Route
|
|
||||||
form={form}
|
|
||||||
referenceData={referenceData}
|
|
||||||
isLoading={refDataLoading}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{step === 4 && (
|
|
||||||
<Step5CargoDetails
|
|
||||||
form={form}
|
|
||||||
direction={direction}
|
|
||||||
referenceData={referenceData}
|
|
||||||
isLoading={refDataLoading}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{step === 5 && (
|
|
||||||
<Step8Review form={form} setStep={setStep} direction={direction} />
|
|
||||||
)}
|
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
|
||||||
|
|
||||||
{/* Navigation footer — sticky */}
|
{/* Step content */}
|
||||||
<Box className="sticky bottom-0 z-20 border-t border-gray-200 bg-white/90 px-6 py-4 backdrop-blur-sm">
|
<Box flex={1}>
|
||||||
<Box className="mx-auto flex max-w-4xl items-center justify-between">
|
<Box className="mx-auto max-w-5xl" style={{ padding: "32px 24px" }}>
|
||||||
<Button
|
{createMutation.isError && (
|
||||||
type="button"
|
<Alert
|
||||||
variant="default"
|
color="red"
|
||||||
radius="md"
|
icon={<AlertCircle size={16} />}
|
||||||
leftSection={<ChevronLeft size={16} />}
|
radius="md"
|
||||||
onClick={() => setStep((s) => Math.max(1, s - 1))}
|
mb="lg"
|
||||||
disabled={step === 1}
|
>
|
||||||
>
|
<Text size="sm" fw={600}>
|
||||||
Back
|
Failed to save draft
|
||||||
</Button>
|
</Text>
|
||||||
|
<Text size="sm" mt={4} c="red.7">
|
||||||
|
{createMutation.error instanceof Error
|
||||||
|
? createMutation.error.message
|
||||||
|
: "An unexpected error occurred. Please try again."}
|
||||||
|
</Text>
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
{step < STEPS.length ? (
|
{step === 1 && <Step1ContractType form={form} />}
|
||||||
|
{step === 2 && <Step2ServiceType form={form} />}
|
||||||
|
{step === 3 && (
|
||||||
|
<Step4Route
|
||||||
|
form={form}
|
||||||
|
referenceData={referenceData}
|
||||||
|
isLoading={refDataLoading}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{step === 4 && (
|
||||||
|
<Step5CargoDetails
|
||||||
|
form={form}
|
||||||
|
direction={direction}
|
||||||
|
referenceData={referenceData}
|
||||||
|
isLoading={refDataLoading}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{step === 5 && (
|
||||||
|
<Step8Review form={form} setStep={setStep} direction={direction} />
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Navigation footer */}
|
||||||
|
<Box
|
||||||
|
style={{
|
||||||
|
position: "sticky",
|
||||||
|
bottom: 0,
|
||||||
|
zIndex: 20,
|
||||||
|
borderTop: "1px solid var(--mantine-color-edr-border-0)",
|
||||||
|
backgroundColor: "rgba(255,255,255,0.94)",
|
||||||
|
backdropFilter: "blur(14px)",
|
||||||
|
WebkitBackdropFilter: "blur(14px)",
|
||||||
|
padding: "16px 24px",
|
||||||
|
marginTop: "auto",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Group justify="space-between" className="mx-auto max-w-4xl">
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
color="edr-green"
|
variant="default"
|
||||||
radius="md"
|
radius="md"
|
||||||
rightSection={<ChevronRight size={16} />}
|
leftSection={<ChevronLeft size={16} />}
|
||||||
onClick={handleContinue}
|
onClick={() => setStep((s) => Math.max(1, s - 1))}
|
||||||
|
disabled={step === 1}
|
||||||
>
|
>
|
||||||
Continue
|
Back
|
||||||
</Button>
|
</Button>
|
||||||
) : (
|
|
||||||
<Button
|
{step < STEPS.length ? (
|
||||||
type="submit"
|
<Button
|
||||||
form="new-booking-form"
|
type="button"
|
||||||
color="edr-green"
|
color="edr-green"
|
||||||
radius="md"
|
radius="md"
|
||||||
loading={createMutation.isPending}
|
rightSection={<ChevronRight size={16} />}
|
||||||
leftSection={
|
onClick={handleContinue}
|
||||||
createMutation.isPending ? undefined : <Check size={16} />
|
>
|
||||||
}
|
Continue
|
||||||
>
|
</Button>
|
||||||
{createMutation.isPending ? "Saving Draft..." : "Save as Draft"}
|
) : (
|
||||||
</Button>
|
<Button
|
||||||
)}
|
type="submit"
|
||||||
|
form="new-booking-form"
|
||||||
|
color="edr-green"
|
||||||
|
radius="md"
|
||||||
|
loading={createMutation.isPending}
|
||||||
|
leftSection={
|
||||||
|
createMutation.isPending ? undefined : <Check size={16} />
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{createMutation.isPending ? "Saving Draft..." : "Save as Draft"}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</Group>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</form>
|
||||||
</form>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Fragment } from "react";
|
|
||||||
import { Check } from "lucide-react";
|
import { Check } from "lucide-react";
|
||||||
|
import { Fragment } from "react";
|
||||||
import { STEPS } from "./schema";
|
import { STEPS } from "./schema";
|
||||||
|
|
||||||
export function StepIndicator({ step }: { step: number }) {
|
export function StepIndicator({ step }: { step: number }) {
|
||||||
@@ -9,29 +9,73 @@ export function StepIndicator({ step }: { step: number }) {
|
|||||||
<Fragment key={item.id}>
|
<Fragment key={item.id}>
|
||||||
<div className="flex shrink-0 flex-col items-center gap-1">
|
<div className="flex shrink-0 flex-col items-center gap-1">
|
||||||
<div
|
<div
|
||||||
className={`flex h-7 w-7 items-center justify-center rounded-full text-xs font-semibold transition-all duration-200 ${
|
style={{
|
||||||
step > item.id
|
width: 28,
|
||||||
? "bg-emerald-600 text-white shadow-sm shadow-emerald-600/40"
|
height: 28,
|
||||||
|
borderRadius: "50%",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: 600,
|
||||||
|
flexShrink: 0,
|
||||||
|
transition: "all 0.2s",
|
||||||
|
...(step > item.id
|
||||||
|
? {
|
||||||
|
backgroundColor: "var(--mantine-color-edr-green-5)",
|
||||||
|
color: "#fff",
|
||||||
|
boxShadow: "0 2px 8px rgba(14,163,113,0.4)",
|
||||||
|
}
|
||||||
: step === item.id
|
: step === item.id
|
||||||
? "border-2 border-emerald-500 text-emerald-600 shadow-sm shadow-emerald-500/30"
|
? {
|
||||||
: "bg-gray-100 text-gray-400"
|
border: "2.5px solid var(--mantine-color-edr-green-5)",
|
||||||
}`}
|
color: "var(--mantine-color-edr-green-7)",
|
||||||
|
backgroundColor: "#fff",
|
||||||
|
boxShadow: "0 0 0 3px rgba(14,163,113,0.12)",
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
backgroundColor: "#fff",
|
||||||
|
color: "var(--mantine-color-edr-muted-0)",
|
||||||
|
border: "2px solid var(--mantine-color-edr-border-0)",
|
||||||
|
}),
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{step > item.id ? <Check className="h-3.5 w-3.5" /> : item.id}
|
{step > item.id ? (
|
||||||
|
<Check style={{ width: 13, height: 13 }} />
|
||||||
|
) : (
|
||||||
|
item.id
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<span
|
<span
|
||||||
className={`hidden text-[10px] font-medium lg:block transition-colors ${
|
style={{
|
||||||
step >= item.id ? "text-gray-800" : "text-gray-400"
|
fontSize: 10,
|
||||||
}`}
|
fontWeight: 500,
|
||||||
|
display: "none",
|
||||||
|
transition: "color 0.2s",
|
||||||
|
color:
|
||||||
|
step >= item.id
|
||||||
|
? "var(--mantine-color-edr-text-0)"
|
||||||
|
: "var(--mantine-color-edr-muted-0)",
|
||||||
|
}}
|
||||||
|
className="lg:!block"
|
||||||
>
|
>
|
||||||
{item.short}
|
{item.short}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{index < STEPS.length - 1 && (
|
{index < STEPS.length - 1 && (
|
||||||
<div
|
<div
|
||||||
className={`mx-1 h-0.5 flex-1 rounded-full transition-all duration-300 ${
|
style={{
|
||||||
step > item.id ? "bg-emerald-500" : "bg-gray-200"
|
flex: 1,
|
||||||
}`}
|
height: 2,
|
||||||
|
borderRadius: 999,
|
||||||
|
margin: "0 6px",
|
||||||
|
marginBottom: 14,
|
||||||
|
transition: "background-color 0.3s",
|
||||||
|
backgroundColor:
|
||||||
|
step > item.id
|
||||||
|
? "var(--mantine-color-edr-green-5)"
|
||||||
|
: "var(--mantine-color-edr-border-0)",
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Fragment>
|
</Fragment>
|
||||||
|
|||||||
Reference in New Issue
Block a user