style: enhance UI components for New Booking page and Step Indicator

This commit is contained in:
Nathnael
2026-06-10 11:07:02 +00:00
parent b37e0cf7a7
commit 38b3f747fb
3 changed files with 320 additions and 182 deletions

View File

@@ -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,
}}
>
EthioDjibouti Railway EthioDjibouti 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",
}} }}

View File

@@ -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>
); );
} }

View File

@@ -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>