diff --git a/app/ui/src/app/components/HiFiUploadPage.tsx b/app/ui/src/app/components/HiFiUploadPage.tsx new file mode 100644 index 000000000..71e36e509 --- /dev/null +++ b/app/ui/src/app/components/HiFiUploadPage.tsx @@ -0,0 +1,527 @@ +"use client"; + +import { useRef, type ChangeEventHandler, type DragEventHandler } from "react"; +import ShellNavbar from "@/app/components/ShellNavbar"; +import { formatSize, PageFooter } from "@/app/utils/routeUtils"; + +type HiFiUploadPageProps = { + title: string; + dropzoneText: string; + description: string; + accept: string; + file: File | null; + isDragging: boolean; + isProcessing: boolean; + error: string | null; + onSelectFile: (file: File) => void; + onDragEnter: DragEventHandler; + onDragLeave: DragEventHandler; + onDrop: DragEventHandler; + onRemoveFile: () => void; + onCancel: () => void; + onNext: () => void; +}; + +export default function HiFiUploadPage({ + title, + dropzoneText, + description, + accept, + file, + isDragging, + isProcessing, + error, + onSelectFile, + onDragEnter, + onDragLeave, + onDrop, + onRemoveFile, + onCancel, + onNext, +}: HiFiUploadPageProps) { + const inputRef = useRef(null); + + const openFilePicker = () => { + if (!isProcessing) inputRef.current?.click(); + }; + + const handleDragEnter: DragEventHandler = (event) => { + event.preventDefault(); + if (!isProcessing) onDragEnter(event); + }; + + const handleDragLeave: DragEventHandler = (event) => { + event.preventDefault(); + if (!isProcessing) onDragLeave(event); + }; + + const handleDrop: DragEventHandler = (event) => { + event.preventDefault(); + if (!isProcessing) onDrop(event); + }; + + const handleFileChange: ChangeEventHandler = (event) => { + const selectedFile = event.target.files?.[0]; + if (!isProcessing && selectedFile) onSelectFile(selectedFile); + event.target.value = ""; + }; + + return ( + <> + + +
+ + +
+
+
+
+

{title}

+
event.preventDefault()} + onDragLeave={handleDragLeave} + onDrop={handleDrop} + aria-disabled={isProcessing} + > + {isProcessing ? ( +
+ ) : ( + <> + +
+

{dropzoneText}

+ +
+ + )} + +
+

{description}

+
+ +
+ + + {file ? file.name : "No file added"} + + {file && ( + <> + + {formatSize(file.size)} + + + + )} +
+
+ + {error && ( +

+ {error} +

+ )} + +
+ + +
+
+
+ + +
+ + ); +} diff --git a/app/ui/src/app/components/ShellNavbar.tsx b/app/ui/src/app/components/ShellNavbar.tsx index f0e3c25b6..591bd60cf 100644 --- a/app/ui/src/app/components/ShellNavbar.tsx +++ b/app/ui/src/app/components/ShellNavbar.tsx @@ -1,7 +1,6 @@ // app/components/ShellNavbar.tsx "use client"; -import Link from "next/link"; import { usePathname } from "next/navigation"; /** @@ -11,14 +10,14 @@ import { usePathname } from "next/navigation"; */ export default function ShellNavbar() { const pathname = usePathname(); - const isLandingPage = pathname === "/"; + const usesWhiteOnboardingBackground = + pathname === "/" || pathname === "/welcome"; const brandStyles = { - fontSize: "12px", + fontFamily: "var(--font-manrope), Arial, Helvetica, sans-serif", + fontSize: "20px", fontWeight: 700, - letterSpacing: "0.08em", - color: "#111", - textTransform: "uppercase" as const, - fontFamily: "inherit", + lineHeight: "28px", + color: "var(--edit-text-primary)", whiteSpace: "nowrap" as const, overflow: "hidden", textOverflow: "ellipsis", @@ -28,13 +27,16 @@ export default function ShellNavbar() { return (
- {isLandingPage ? ( - Delivery Optimizer - ) : ( - - Delivery Optimizer - - )} + Delivery Optimizer
); } diff --git a/app/ui/src/app/components/navbar/MobileNavbar.tsx b/app/ui/src/app/components/navbar/MobileNavbar.tsx index 81099069d..870dbc308 100644 --- a/app/ui/src/app/components/navbar/MobileNavbar.tsx +++ b/app/ui/src/app/components/navbar/MobileNavbar.tsx @@ -34,7 +34,7 @@ export default function MobileNavbar({ onMenuClick }: MobileNavbarProps) { /> - DELIVERY OPTIMIZER + Delivery Optimizer
); diff --git a/app/ui/src/app/components/navbar/Navbar.tsx b/app/ui/src/app/components/navbar/Navbar.tsx index 7af1239ac..396659acc 100644 --- a/app/ui/src/app/components/navbar/Navbar.tsx +++ b/app/ui/src/app/components/navbar/Navbar.tsx @@ -13,7 +13,7 @@ type NavbarProps = { export default function Navbar({ onSave }: NavbarProps) { return (
- DELIVERY OPTIMIZER + Delivery Optimizer diff --git a/app/ui/src/app/edit/components/address/CSVUploadOverlay.tsx b/app/ui/src/app/edit/components/address/CSVUploadOverlay.tsx index 519bbafcf..1c9652db0 100644 --- a/app/ui/src/app/edit/components/address/CSVUploadOverlay.tsx +++ b/app/ui/src/app/edit/components/address/CSVUploadOverlay.tsx @@ -201,29 +201,31 @@ function StepColumnMapper({ onChange={(e) => onMappingChange(header, e.target.value as MappableField) } - className="absolute inset-0 w-full h-full opacity-0 cursor-pointer" + className="absolute inset-0 w-full h-full appearance-none bg-transparent px-3 pr-10 text-[14px] leading-[1.5] text-[var(--edit-text-primary)] cursor-pointer truncate" > - + {( Object.keys(FIELD_LABELS) as Exclude< MappableField, "" >[] ).map((f) => ( - ))} - - {mapping[header] - ? FIELD_LABELS[ - mapping[header] as Exclude - ] - : "Select"} - chevrons — shared by edit address/vehicle (avoids fragile Tailwind arbitrary URLs). */ diff --git a/app/ui/src/app/layout.tsx b/app/ui/src/app/layout.tsx index 08182e2d1..7ba0981e5 100644 --- a/app/ui/src/app/layout.tsx +++ b/app/ui/src/app/layout.tsx @@ -1,8 +1,28 @@ import type { Metadata, Viewport } from "next"; +import { Geist, Geist_Mono, Manrope } from "next/font/google"; import ServiceWorkerRegistration from "@/app/components/ServiceWorkerRegistration"; import "./globals.css"; import FeedbackLauncher from "./components/FeedbackLauncher"; +const geistSans = Geist({ + variable: "--font-geist-sans", + subsets: ["latin"], +}); + +const geistMono = Geist_Mono({ + variable: "--font-geist-mono", + subsets: ["latin"], +}); + +const manrope = Manrope({ + variable: "--font-manrope", + subsets: ["latin"], +}); + +type RootLayoutProps = { + children: React.ReactNode; +}; + export const metadata: Metadata = { title: { default: "Delivery Route Optimizer", @@ -20,14 +40,12 @@ export const viewport: Viewport = { themeColor: "#111827", }; -export default function RootLayout({ - children, -}: { - children: React.ReactNode; -}) { +export default function RootLayout({ children }: RootLayoutProps) { return ( - + {children} diff --git a/app/ui/src/app/page.tsx b/app/ui/src/app/page.tsx index fabcf972c..8f6487b3a 100644 --- a/app/ui/src/app/page.tsx +++ b/app/ui/src/app/page.tsx @@ -35,7 +35,6 @@ export default function LandingPage() { position: relative; z-index: 10; background: #ffffff; - border-bottom: 1px solid rgba(0,0,0,0.08); } .landing-content { diff --git a/app/ui/src/app/results/components/MobileResultsNavbar.tsx b/app/ui/src/app/results/components/MobileResultsNavbar.tsx index 89ac7ce53..2a1a9df2c 100644 --- a/app/ui/src/app/results/components/MobileResultsNavbar.tsx +++ b/app/ui/src/app/results/components/MobileResultsNavbar.tsx @@ -4,12 +4,12 @@ import { MOBILE_NAVBAR_LEFT_GROUP, MOBILE_NAVBAR_MENU_BTN, MOBILE_NAVBAR_ROOT, -} from "../../edit/formStyles.v2"; + MOBILE_NAVBAR_TITLE, +} from "@/app/edit/formStyles.v2"; import { RESULTS_MOBILE_NAV_CANCEL_BTN, RESULTS_MOBILE_NAV_SAVE_BTN, - RESULTS_MOBILE_NAV_TITLE, -} from "../formStyles.mobile"; +} from "@/app/results/formStyles.mobile"; type MobileResultsNavbarProps = { onMenuClick: () => void; @@ -49,7 +49,9 @@ export default function MobileResultsNavbar({ /> - Delivery Optimizer + + Delivery Optimizer +
{isEditMode && ( diff --git a/app/ui/src/app/results/components/RouteCard.tsx b/app/ui/src/app/results/components/RouteCard.tsx index c43f23db5..77cb10466 100644 --- a/app/ui/src/app/results/components/RouteCard.tsx +++ b/app/ui/src/app/results/components/RouteCard.tsx @@ -1,9 +1,11 @@ "use client"; +import { capitalize } from "@/app/edit/utils/deliveryHelpers"; import type { Route } from "../types"; import { routeColorHex } from "../utils/routeColors"; import EditableStopItem from "./EditableStopItem"; import RouteCardMenu from "./RouteCardMenu"; +import VehicleTypeIcon from "./VehicleTypeIcon"; type RouteCardProps = { route: Route; @@ -183,34 +185,12 @@ export default function RouteCard({

- - - - - - {route.vehicleType ?? "Vehicle"} + + + {route.vehicleType + ? capitalize(route.vehicleType) + : "Vehicle"} +

diff --git a/app/ui/src/app/results/components/VehicleTypeIcon.tsx b/app/ui/src/app/results/components/VehicleTypeIcon.tsx new file mode 100644 index 000000000..77fecf529 --- /dev/null +++ b/app/ui/src/app/results/components/VehicleTypeIcon.tsx @@ -0,0 +1,171 @@ +import type { JSX } from "react"; + +const KNOWN_VEHICLE_TYPES = ["truck", "car", "bicycle"] as const; + +type KnownVehicleType = (typeof KNOWN_VEHICLE_TYPES)[number]; + +type VehicleTypeIconProps = { + vehicleType?: string; + className?: string; +}; + +function isKnownVehicleType(value: string): value is KnownVehicleType { + return (KNOWN_VEHICLE_TYPES as readonly string[]).includes(value); +} + +function normalizedVehicleType( + vehicleType?: string, +): KnownVehicleType | "unknown" { + const normalized = vehicleType?.trim().toLowerCase() ?? ""; + return isKnownVehicleType(normalized) ? normalized : "unknown"; +} + +function TruckIcon(): JSX.Element { + return ( + <> + + + + + + ); +} + +function CarIcon(): JSX.Element { + return ( + <> + + + + + + ); +} + +function BicycleIcon(): JSX.Element { + return ( + <> + + + + + + ); +} + +function vehicleIconContent(vehicleType?: string): JSX.Element { + const kind = normalizedVehicleType(vehicleType); + switch (kind) { + case "truck": + return ; + case "bicycle": + return ; + case "car": + case "unknown": + return ; + } +} + +export default function VehicleTypeIcon({ + vehicleType, + className = "h-4 w-4 shrink-0 text-[var(--edit-text-secondary)]", +}: VehicleTypeIconProps): JSX.Element { + return ( + + ); +} diff --git a/app/ui/src/app/results/formStyles.mobile.ts b/app/ui/src/app/results/formStyles.mobile.ts index 42fab6622..5385c84e4 100644 --- a/app/ui/src/app/results/formStyles.mobile.ts +++ b/app/ui/src/app/results/formStyles.mobile.ts @@ -44,9 +44,6 @@ export const RESULTS_BOTTOM_SHEET_BTN_RECT_FILLED = export const RESULTS_BOTTOM_SHEET_BODY = "flex-1 min-h-0 overflow-y-auto px-4 pb-2"; -export const RESULTS_MOBILE_NAV_TITLE = - "min-w-0 truncate font-bold text-[14px] leading-5 uppercase tracking-[0.02em] text-[var(--edit-text-primary)] sm:text-[16px] sm:leading-[22px]"; - export const RESULTS_MOBILE_NAV_SAVE_BTN = "h-9 px-4 rounded-[6px] border border-[var(--edit-stone-700)] font-semibold text-[14px] leading-5 text-[var(--edit-text-primary)] whitespace-nowrap hover:bg-[var(--edit-secondary-btn-hover)] active:bg-[var(--edit-secondary-btn-pressed)] transition-colors disabled:opacity-50 disabled:cursor-not-allowed"; diff --git a/app/ui/src/app/results/page.tsx b/app/ui/src/app/results/page.tsx index 0ec18165e..e8e06289e 100644 --- a/app/ui/src/app/results/page.tsx +++ b/app/ui/src/app/results/page.tsx @@ -10,7 +10,7 @@ import { useState, useSyncExternalStore, } from "react"; -import { NAVBAR_V2_LOGO, NAVBAR_V2_ROOT } from "../edit/formStyles.v2"; +import { NAVBAR_V2_LOGO, NAVBAR_V2_ROOT } from "@/app/edit/formStyles.v2"; import styles from "../edit/edit.module.css"; import MobileSidebar from "../components/sidebar/MobileSidebar"; import ExportEditWarningModal from "./components/ExportEditWarningModal"; @@ -416,7 +416,7 @@ export default function ResultsPage() {