Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
"use client";

import Link from "next/link";
import { usePathname } from "next/navigation";
import { useHasOptimizeResults } from "../../../utils/hasOptimizeResults";
import {
SIDEBAR_NAV_ITEM_ACTIVE,
SIDEBAR_NAV_ITEM_DISABLED,
SIDEBAR_NAV_ITEM_INACTIVE,
SIDEBAR_NAV_LABEL_ACTIVE,
SIDEBAR_NAV_LABEL_INACTIVE,
SIDEBAR_NAV_PILL_ACTIVE,
SIDEBAR_NAV_PILL_INACTIVE,
} from "@/app/edit/formStyles.v2";

Expand All @@ -12,26 +20,48 @@ const SIDEBAR_RESULTS_ICON = (
height="24"
viewBox="0 0 24 24"
fill="none"
aria-hidden
>
<path
d="M4.35 20.7C4.01667 20.8333 3.70833 20.7958 3.425 20.5875C3.14167 20.3792 3 20.1 3 19.75V5.75C3 5.53333 3.0625 5.34167 3.1875 5.175C3.3125 5.00833 3.48333 4.88333 3.7 4.8L9 3L15 5.1L19.65 3.3C19.9833 3.16667 20.2917 3.20417 20.575 3.4125C20.8583 3.62083 21 3.9 21 4.25V12.675C20.75 12.2917 20.4542 11.9417 20.1125 11.625C19.7708 11.3083 19.4 11.0333 19 10.8V5.7L16 6.85V10C15.65 10 15.3083 10.0292 14.975 10.0875C14.6417 10.1458 14.3167 10.2333 14 10.35V6.85L10 5.45V18.525L4.35 20.7ZM5 18.3L8 17.15V5.45L5 6.45V18.3ZM17.4125 17.5C17.7875 17.1667 17.9833 16.6667 18 16C18.0167 15.4333 17.8292 14.9583 17.4375 14.575C17.0458 14.1917 16.5667 14 16 14C15.4333 14 14.9583 14.1917 14.575 14.575C14.1917 14.9583 14 15.4333 14 16C14 16.5667 14.1917 17.0417 14.575 17.425C14.9583 17.8083 15.4333 18 16 18C16.5667 18 17.0375 17.8333 17.4125 17.5ZM16 20C14.9 20 13.9583 19.6083 13.175 18.825C12.3917 18.0417 12 17.1 12 16C12 14.9 12.3917 13.9583 13.175 13.175C13.9583 12.3917 14.9 12 16 12C17.1 12 18.0417 12.3917 18.825 13.175C19.6083 13.9583 20 14.9 20 16C20 16.3833 19.9542 16.7458 19.8625 17.0875C19.7708 17.4292 19.6333 17.75 19.45 18.05L22 20.6L20.6 22L18.05 19.45C17.75 19.6333 17.4292 19.7708 17.0875 19.8625C16.7458 19.9542 16.3833 20 16 20Z"
fill="var(--edit-text-primary)"
fill="currentColor"
/>
</svg>
);

export default function SidebarResultsButton() {
const pathname = usePathname();
const isResultsPage = pathname === "/results";
const hasStoredRoutes = useHasOptimizeResults();

if (isResultsPage) {
return (
<span className={SIDEBAR_NAV_ITEM_ACTIVE} aria-current="page">
<span
className={`${SIDEBAR_NAV_PILL_ACTIVE} text-[var(--edit-foreground)]`}
>
{SIDEBAR_RESULTS_ICON}
</span>
<span className={SIDEBAR_NAV_LABEL_ACTIVE}>Results</span>
</span>
);
}

if (hasStoredRoutes) {
return (
<Link href="/results" className={SIDEBAR_NAV_ITEM_INACTIVE}>
<span className={SIDEBAR_NAV_PILL_INACTIVE}>
{SIDEBAR_RESULTS_ICON}
</span>
<span className={SIDEBAR_NAV_LABEL_INACTIVE}>Results</span>
</Link>
);
}

return (
<Link
href="#"
className={SIDEBAR_NAV_ITEM_DISABLED}
aria-disabled="true"
tabIndex={-1}
>
{" "}
{/* TODO: add results page link when at least one route exists */}
<span className={SIDEBAR_NAV_ITEM_DISABLED} aria-disabled="true">
<span className={SIDEBAR_NAV_PILL_INACTIVE}>{SIDEBAR_RESULTS_ICON}</span>
<span className={SIDEBAR_NAV_LABEL_INACTIVE}>Results</span>
</Link>
</span>
);
}
3 changes: 2 additions & 1 deletion app/ui/src/app/edit/hooks/useOptimize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
vehicleRowToVehicleInput,
} from "@/app/edit/utils/optimizeMapper";
import { SUPPORTED_STATES } from "@/app/edit/constants/supportedRegions";
import { setOptimizeResults } from "@/app/edit/utils/hasOptimizeResults";
import { vroomToRoutes } from "@/app/edit/utils/vroomToRoutes";
import type {
AddressCard,
Expand Down Expand Up @@ -362,7 +363,7 @@ export function useOptimize(
lockedVehicles,
addresses,
);
sessionStorage.setItem("optimizeResults", JSON.stringify(routes));
setOptimizeResults(routes);
router.push("/results");
return;
}
Expand Down
68 changes: 68 additions & 0 deletions app/ui/src/app/edit/utils/hasOptimizeResults.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
"use client";

import { useEffect, useState } from "react";
import type { Route } from "@/app/results/types";

export const OPTIMIZE_RESULTS_STORAGE_KEY = "optimizeResults";

export const OPTIMIZE_RESULTS_UPDATED_EVENT = "optimize-results-updated";

function isValidRoute(value: unknown): value is Route {
if (!value || typeof value !== "object") return false;
const route = value as Record<string, unknown>;
return typeof route.vehicleId === "string" && Array.isArray(route.stops);
}

function parseStoredRoutes(stored: string): Route[] | null {
try {
const parsed: unknown = JSON.parse(stored);
if (!Array.isArray(parsed) || parsed.length === 0) return null;
if (!parsed.every(isValidRoute)) return null;
return parsed;
} catch {
return null;
}
}

/** True when sessionStorage has at least one route ready for /results. */
export function readHasOptimizeResults(): boolean {
if (typeof window === "undefined") return false;

const stored = sessionStorage.getItem(OPTIMIZE_RESULTS_STORAGE_KEY);
if (!stored) return false;

return parseStoredRoutes(stored) != null;
}

export function notifyOptimizeResultsUpdated(): void {
if (typeof window === "undefined") return;
window.dispatchEvent(new Event(OPTIMIZE_RESULTS_UPDATED_EVENT));
}

export function setOptimizeResults(routes: Route[]): void {
sessionStorage.setItem(OPTIMIZE_RESULTS_STORAGE_KEY, JSON.stringify(routes));
notifyOptimizeResultsUpdated();
}

export function clearOptimizeResults(): void {
sessionStorage.removeItem(OPTIMIZE_RESULTS_STORAGE_KEY);
notifyOptimizeResultsUpdated();
}

export function useHasOptimizeResults(): boolean {
const [hasResults, setHasResults] = useState(false);

useEffect(() => {
const sync = () => setHasResults(readHasOptimizeResults());
sync();

window.addEventListener(OPTIMIZE_RESULTS_UPDATED_EVENT, sync);
window.addEventListener("storage", sync);
return () => {
window.removeEventListener(OPTIMIZE_RESULTS_UPDATED_EVENT, sync);
window.removeEventListener("storage", sync);
};
}, []);

return hasResults;
}
4 changes: 2 additions & 2 deletions app/ui/src/app/results/components/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ export default function Sidebar({
<li
key={route.vehicleId}
className="rounded-xl border border-zinc-200 bg-zinc-50 shadow-sm overflow-hidden"
style={{ boxShadow: `inset 4px 0 0 ${accent}` }}
style={{ boxShadow: `inset 4px 0 0 0 ${accent}` }}
>
<button
type="button"
Expand All @@ -102,7 +102,7 @@ export default function Sidebar({
<div className="min-w-0 flex-1">
<div className="flex items-start gap-2 min-w-0">
<span
className="mt-0.5 h-5 w-5 shrink-0 rounded-md"
className="mt-0.5 h-8 w-8 shrink-0 rounded-md"
style={{ backgroundColor: accent }}
aria-hidden
/>
Expand Down
55 changes: 19 additions & 36 deletions app/ui/src/app/results/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@
"use client";

import { useCallback, useEffect, useState } from "react";
import { NAVBAR_V2_LOGO, NAVBAR_V2_ROOT } from "../edit/formStyles.v2";
import styles from "../edit/edit.module.css";
import EditSidebar from "../edit/components/layout/sidebar/Sidebar";
import SidebarEditButton from "../edit/components/layout/sidebar/SidebarEditButton";
import SidebarResultsButton from "../edit/components/layout/sidebar/SidebarResultsButton";
import MapComponent from "./components/Map";
import Sidebar from "./components/Sidebar";
import type { PendingPinMove, Route } from "./types";
Expand Down Expand Up @@ -36,7 +41,6 @@ export default function ResultsPage() {
}
}, [initialRoutes.length]);

const [isSidebarOpen, setIsSidebarOpen] = useState(true);
const [isEditMode, setIsEditMode] = useState(false);
const [pendingPinMove, setPendingPinMove] = useState<PendingPinMove | null>(
null,
Expand Down Expand Up @@ -107,7 +111,9 @@ export default function ResultsPage() {
const cancelPendingPinMove = useCallback(() => setPendingPinMove(null), []);

return (
<main className="h-screen flex flex-col overflow-hidden">
<main
className={`h-screen flex flex-col overflow-hidden font-sans-manrope ${styles.root}`}
>
{error && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40">
<div className="rounded-lg border border-zinc-200 bg-white p-6 shadow-sm w-80 space-y-4">
Expand All @@ -122,35 +128,9 @@ export default function ResultsPage() {
</div>
)}{" "}
{/* Map container switched to h-screen and added overflow hidden so the page is forced to be exactly one screen tall, whereas before the page was allowed to get taller than browser window leading to a long scroll */}
<header className="flex items-center justify-between gap-3 p-4 shrink-0 border-b border-zinc-200 bg-white">
<header className={`${NAVBAR_V2_ROOT} shrink-0 border-b border-zinc-200`}>
<div className="flex items-center gap-3 min-w-0">
<button
type="button"
onClick={() => setIsSidebarOpen((prev) => !prev)}
className="flex h-9 w-9 items-center justify-center rounded-lg border border-zinc-200 bg-white text-zinc-700 hover:bg-zinc-50"
aria-label={isSidebarOpen ? "Close sidebar" : "Open sidebar"}
>
<svg
className="h-5 w-5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
aria-hidden
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M4 6h16M4 12h16M4 18h16"
/>
</svg>
</button>
<div className="min-w-0">
<span className="block truncate text-sm font-semibold tracking-wide text-zinc-800 uppercase">
Delivery Optimizer
</span>
<h1 className="sr-only">Results</h1>
</div>
<p className={NAVBAR_V2_LOGO}>DELIVERY OPTIMIZER</p>
</div>

<div className="ml-auto flex items-center gap-2">
Expand All @@ -159,14 +139,14 @@ export default function ResultsPage() {
<button
type="button"
onClick={cancelPendingPinMove}
className="rounded-full border border-zinc-300 bg-white px-3 py-1 text-xs font-medium text-zinc-700 hover:bg-zinc-50"
className="h-9 px-6 rounded-[80px] border border-[var(--edit-foreground)] font-medium text-[14px] leading-5 text-[var(--edit-foreground)] whitespace-nowrap hover:bg-black/5 transition-colors"
>
Cancel
</button>
<button
type="button"
onClick={savePendingPinMove}
className="rounded-full bg-amber-500 px-3 py-1 text-xs font-medium text-white hover:bg-amber-600 focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-amber-500"
className="h-9 px-6 rounded-[80px] border border-[var(--edit-foreground)] font-medium text-[14px] leading-5 text-[var(--edit-foreground)] whitespace-nowrap hover:bg-black/5 transition-colors"
>
Save
</button>
Expand All @@ -177,16 +157,19 @@ export default function ResultsPage() {
disabled
aria-disabled="true"
title="Export coming soon"
className="rounded-full bg-emerald-500 px-3 py-1 text-xs font-medium text-white opacity-50 cursor-not-allowed"
className="h-9 px-6 rounded-[80px] bg-[var(--edit-teal-500)] font-medium text-[14px] leading-5 text-[var(--edit-foreground)] whitespace-nowrap opacity-50 cursor-not-allowed"
>
Export
</button>
</div>
</header>
<div className="flex flex-1 min-h-0">
<div
className={`shrink-0 h-full overflow-hidden transition-[width] duration-300 ease-in-out ${isSidebarOpen ? "w-72" : "w-0"}`}
>
<EditSidebar>
<SidebarEditButton />
<SidebarResultsButton />
</EditSidebar>

<div className="shrink-0 h-full w-72 overflow-hidden">
<Sidebar
routes={routes}
isEditMode={isEditMode}
Expand Down
Loading