feat(bookings): enhance contract viewer and backoffice UI consistency

This commit is contained in:
ghost2023
2026-06-06 09:11:13 +03:00
parent 7a19a161e1
commit da0f1e4cc9
7 changed files with 185 additions and 85 deletions

View File

@@ -1,12 +1,10 @@
/** Shared surfaces for booking list & detail — frosted glass, neutral accents. */
export const bookingGlass = {
card:
"border border-border/50 bg-card/75 shadow-sm backdrop-blur-md supports-[backdrop-filter]:bg-card/60",
card: "border border-border/50 bg-card/75 shadow-sm backdrop-blur-md supports-[backdrop-filter]:bg-card/60",
panel:
"border border-border/50 bg-card/80 shadow-sm backdrop-blur-md supports-[backdrop-filter]:bg-card/65",
rail:
"border border-border/40 bg-muted/20 backdrop-blur-sm supports-[backdrop-filter]:bg-muted/15",
rail: "border border-border/40 bg-muted/20 backdrop-blur-sm supports-[backdrop-filter]:bg-muted/15",
iconWell:
"border border-border/50 bg-background/70 text-foreground/75 shadow-sm backdrop-blur-sm supports-[backdrop-filter]:bg-background/50",
iconWellHero:
@@ -22,9 +20,8 @@ export const bookingGlass = {
} as const;
export const bookingSurface = {
page:
"min-h-screen bg-gradient-to-b from-muted/30 via-background to-background",
pageInner: "mx-auto max-w-[1600px] space-y-5 p-6 lg:p-8",
page: "min-h-screen bg-background ",
pageInner: "mx-auto max-w-[1600px] space-y-5 ",
hero: `relative overflow-hidden rounded-2xl ${bookingGlass.card}`,
heroGlow:
"pointer-events-none absolute -right-24 -top-24 size-72 rounded-full bg-muted/40 blur-3xl",

View File

@@ -56,7 +56,10 @@ const FreightDashboardHeader = ({
if (!isUserMenuOpen) return;
const handlePointerDown = (event: MouseEvent) => {
if (userMenuRef.current && !userMenuRef.current.contains(event.target as Node)) {
if (
userMenuRef.current &&
!userMenuRef.current.contains(event.target as Node)
) {
setIsUserMenuOpen(false);
}
};
@@ -74,10 +77,14 @@ const FreightDashboardHeader = ({
}, [isUserMenuOpen]);
return (
<header className="flex h-[82px] shrink-0 items-center justify-between gap-4 px-6">
<header className="flex h-20 shrink-0 items-center justify-between gap-4 px-6">
<div className="min-w-0">
<h1 className="truncate text-xl font-bold tracking-tight text-gray-900">{pageMeta.title}</h1>
<p className="mt-0.5 truncate text-sm text-gray-500">{pageMeta.subtitle}</p>
<h1 className="truncate text-xl font-bold tracking-tight text-foreground">
{pageMeta.title}
</h1>
<p className="mt-0.5 truncate text-sm text-secondary-foreground">
{pageMeta.subtitle}
</p>
</div>
<div className="flex shrink-0 items-center gap-2">
@@ -85,14 +92,24 @@ const FreightDashboardHeader = ({
<button
type="button"
onClick={onToggleTheme}
aria-label={theme === "dark" ? "Switch to light mode" : "Switch to dark mode"}
aria-label={
theme === "dark" ? "Switch to light mode" : "Switch to dark mode"
}
className={iconButtonClass}
>
{theme === "dark" ? <Sun className="h-5 w-5" /> : <Moon className="h-5 w-5" />}
{theme === "dark" ? (
<Sun className="h-5 w-5" />
) : (
<Moon className="h-5 w-5" />
)}
</button>
) : null}
<button type="button" aria-label="Change language" className={iconButtonClass}>
<button
type="button"
aria-label="Change language"
className={iconButtonClass}
>
<Languages className="h-5 w-5" />
</button>
@@ -101,7 +118,11 @@ const FreightDashboardHeader = ({
<span className="absolute right-2 top-2 h-2 w-2 rounded-full bg-red-500 ring-2 ring-white" />
</button>
<button type="button" aria-label="Notifications" className={iconButtonClass}>
<button
type="button"
aria-label="Notifications"
className={iconButtonClass}
>
<Bell className="h-5 w-5" />
<span className="absolute right-2 top-2 h-2 w-2 rounded-full bg-red-500 ring-2 ring-white" />
</button>
@@ -136,8 +157,12 @@ const FreightDashboardHeader = ({
className="absolute right-0 top-full z-50 mt-2 w-52 overflow-hidden rounded-xl border border-gray-200 bg-white py-1 shadow-lg"
>
<div className="border-b border-gray-100 px-4 py-3">
<p className="text-sm font-semibold text-gray-900">{userName}</p>
{userEmail ? <p className="text-xs text-gray-500">{userEmail}</p> : null}
<p className="text-sm font-semibold text-gray-900">
{userName}
</p>
{userEmail ? (
<p className="text-xs text-gray-500">{userEmail}</p>
) : null}
</div>
<a
href="#profile"

View File

@@ -12,7 +12,9 @@ function getInitialTheme(): Theme {
if (typeof window === "undefined") return "light";
const stored = window.localStorage.getItem(THEME_STORAGE_KEY);
if (stored === "dark" || stored === "light") return stored;
return window.matchMedia?.("(prefers-color-scheme: dark)").matches ? "dark" : "light";
return window.matchMedia?.("(prefers-color-scheme: dark)").matches
? "dark"
: "light";
}
export interface FreightDashboardLayoutProps {
@@ -29,7 +31,7 @@ export interface FreightDashboardLayoutProps {
}
const panelClass =
"rounded-2xl border border-gray-200/80 bg-white shadow-[0_1px_3px_rgba(15,23,42,0.06)]";
"rounded-lg border border-gray-200/80 bg-white shadow-[0_1px_3px_rgba(15,23,42,0.06)]";
const FreightDashboardLayout = ({
sidebarSections,
@@ -59,7 +61,8 @@ const FreightDashboardLayout = ({
window.localStorage.setItem(THEME_STORAGE_KEY, theme);
}, [theme, enableThemeToggle]);
const toggleTheme = () => setTheme((current) => (current === "dark" ? "light" : "dark"));
const toggleTheme = () =>
setTheme((current) => (current === "dark" ? "light" : "dark"));
return (
<>
@@ -71,17 +74,17 @@ const FreightDashboardLayout = ({
/>
<div
className="flex h-[100dvh] overflow-hidden bg-[#eceef2] p-3 antialiased md:p-4"
className="flex h-[100dvh] overflow-hidden bg-[#eceef2] p-2 antialiased"
style={{ fontFamily: "'Outfit', var(--font-sans)" }}
>
<div className="flex h-full min-h-0 w-full gap-3 md:gap-3">
<div className="flex h-full min-h-0 w-full gap-2">
<FreightSidebar
sections={sidebarSections}
activeHref={activeHref}
onNavigate={onNavigate}
/>
<div className="flex h-full min-h-0 min-w-0 flex-1 flex-col gap-3 md:gap-3">
<div className="flex h-full min-h-0 min-w-0 flex-1 flex-col gap-2">
<div className={`shrink-0 ${panelClass}`}>
<FreightDashboardHeader
pageMeta={pageMeta}

View File

@@ -1,4 +1,10 @@
import { type MouseEvent, useCallback, useEffect, useMemo, useState } from "react";
import {
type MouseEvent,
useCallback,
useEffect,
useMemo,
useState,
} from "react";
import { ChevronDown, ChevronRight } from "lucide-react";
import { cn } from "@/lib/utils";
@@ -20,21 +26,28 @@ const collectSidebarHrefs = (items: SidebarItem[]): string[] =>
items.flatMap((item) => {
const hrefs: string[] = [];
if (item.href) hrefs.push(item.href.toLowerCase());
if (item.children?.length) hrefs.push(...collectSidebarHrefs(item.children));
if (item.children?.length)
hrefs.push(...collectSidebarHrefs(item.children));
return hrefs;
});
const flattenSectionItems = (sections: SidebarSection[]) =>
sections.flatMap((section) => section.items);
const FreightSidebar = ({ sections, activeHref, onNavigate }: FreightSidebarProps) => {
const FreightSidebar = ({
sections,
activeHref,
onNavigate,
}: FreightSidebarProps) => {
const items = useMemo(() => flattenSectionItems(sections), [sections]);
const activePath = activeHref?.toLowerCase() ?? "";
const isHrefActive = useCallback(
(href: string) => {
const normalized = href.toLowerCase();
return activePath === normalized || activePath.startsWith(`${normalized}/`);
return (
activePath === normalized || activePath.startsWith(`${normalized}/`)
);
},
[activePath],
);
@@ -72,7 +85,8 @@ const FreightSidebar = ({ sections, activeHref, onNavigate }: FreightSidebarProp
return acc;
}, [activePath, branchContainsActive, isHrefActive, items]);
const [expanded, setExpanded] = useState<Record<string, boolean>>(defaultExpanded);
const [expanded, setExpanded] =
useState<Record<string, boolean>>(defaultExpanded);
useEffect(() => {
setExpanded((current) => ({ ...defaultExpanded, ...current }));
@@ -108,7 +122,11 @@ const FreightSidebar = ({ sections, activeHref, onNavigate }: FreightSidebarProp
: "text-gray-900",
);
const renderNavBranch = (children: SidebarItem[], depth: number, parentKey: string) =>
const renderNavBranch = (
children: SidebarItem[],
depth: number,
parentKey: string,
) =>
children.map((child) => {
const key = sidebarItemKey(child, parentKey);
const isGroup = Boolean(child.children?.length) && !child.href;
@@ -181,7 +199,9 @@ const FreightSidebar = ({ sections, activeHref, onNavigate }: FreightSidebarProp
const hasChildren = Boolean(item.children?.length);
const itemHref = item.href.toLowerCase();
const childActive = hasChildren ? branchContainsActive(item.children!) : false;
const childActive = hasChildren
? branchContainsActive(item.children!)
: false;
const isCurrentItem = hasChildren
? activePath === itemHref
: isHrefActive(itemHref);
@@ -206,10 +226,12 @@ const FreightSidebar = ({ sections, activeHref, onNavigate }: FreightSidebarProp
href={item.href}
onClick={(event) => navigateTo(event, item.href!)}
aria-current={isCurrentItem ? "page" : undefined}
className="flex min-w-0 flex-1 items-center gap-3 px-3 py-2.5 text-base font-medium leading-snug"
className="flex min-w-0 flex-1 items-center gap-3 px-3 py-2.5 text-sm leading-snug"
>
{item.icon ? (
<span className={iconClass(leafActive, isActive)}>{item.icon}</span>
<span className={iconClass(leafActive, isActive)}>
{item.icon}
</span>
) : null}
<span className="truncate">{item.label}</span>
</a>
@@ -257,10 +279,12 @@ const FreightSidebar = ({ sections, activeHref, onNavigate }: FreightSidebarProp
};
return (
<aside className="flex h-full max-h-full w-[340px] shrink-0 flex-col overflow-hidden rounded-lg border border-gray-200 bg-white shadow-sm">
<aside className="flex h-full max-h-full w-[280px] shrink-0 flex-col overflow-hidden rounded-lg border border-gray-200 bg-white shadow-sm">
<div className="flex shrink-0 items-center gap-2.5 border-b border-gray-100 px-5 py-5">
<img src={EDR_LOGO} alt="EDR Freight" className="h-9 w-auto" />
<span className="text-lg font-semibold tracking-tight text-gray-900">EDR Freight</span>
<span className="text-lg font-semibold tracking-tight text-gray-900">
EDR Freight
</span>
</div>
<nav className="flex min-h-0 flex-1 flex-col gap-5 overflow-y-auto overscroll-contain px-3 py-4">
@@ -270,7 +294,7 @@ const FreightSidebar = ({ sections, activeHref, onNavigate }: FreightSidebarProp
className={cn(
"px-3 pb-1 text-xs font-semibold uppercase tracking-wide",
// Use a very light gray for ALL section titles, not just when mutedTitle is specified
"text-gray-400"
"text-sidebar-secondary-foreground",
)}
>
{section.title}