diff --git a/app/ui/public/b2-logo.png b/app/ui/public/b2-logo.png new file mode 100644 index 00000000..7227f21a Binary files /dev/null and b/app/ui/public/b2-logo.png differ diff --git a/app/ui/src/app/edit/components/layout/sidebar/SidebarResultsButton.tsx b/app/ui/src/app/edit/components/layout/sidebar/SidebarResultsButton.tsx index b3fb88cf..406de4f0 100644 --- a/app/ui/src/app/edit/components/layout/sidebar/SidebarResultsButton.tsx +++ b/app/ui/src/app/edit/components/layout/sidebar/SidebarResultsButton.tsx @@ -1,7 +1,15 @@ +"use client"; + import Link from "next/link"; +import { usePathname } from "next/navigation"; +import { readHasOptimizeResults } 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"; @@ -12,26 +20,48 @@ const SIDEBAR_RESULTS_ICON = ( height="24" viewBox="0 0 24 24" fill="none" + aria-hidden > ); export default function SidebarResultsButton() { + const pathname = usePathname(); + const isResultsPage = pathname === "/results"; + const hasStoredRoutes = !isResultsPage && readHasOptimizeResults(); + + if (isResultsPage) { + return ( + + + {SIDEBAR_RESULTS_ICON} + + Results + + ); + } + + if (hasStoredRoutes) { + return ( + + + {SIDEBAR_RESULTS_ICON} + + Results + + ); + } + return ( - - {" "} - {/* TODO: add results page link when at least one route exists */} + {SIDEBAR_RESULTS_ICON} Results - + ); } diff --git a/app/ui/src/app/edit/utils/hasOptimizeResults.ts b/app/ui/src/app/edit/utils/hasOptimizeResults.ts new file mode 100644 index 00000000..583f388c --- /dev/null +++ b/app/ui/src/app/edit/utils/hasOptimizeResults.ts @@ -0,0 +1,16 @@ +import type { Route } from "@/app/results/types"; + +/** 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("optimizeResults"); + if (!stored) return false; + + try { + const parsed = JSON.parse(stored) as Route[]; + return Array.isArray(parsed) && parsed.length > 0; + } catch { + return false; + } +} diff --git a/app/ui/src/app/results/components/Map.tsx b/app/ui/src/app/results/components/Map.tsx index 6d03ad37..36e05fc5 100644 --- a/app/ui/src/app/results/components/Map.tsx +++ b/app/ui/src/app/results/components/Map.tsx @@ -20,6 +20,43 @@ import type { PendingPinMove, Route } from "../types"; import { routeColorHex } from "../utils/routeColors"; const DAVIS_CENTER = { lat: 38.5449, lng: -121.7405 }; +const MARKER_ICON_WIDTH = 28; +const MARKER_ICON_HEIGHT = 40; + +let markerScaledSize: google.maps.Size | undefined; + +function getMarkerScaledSize(): google.maps.Size | undefined { + if (typeof google === "undefined") return undefined; + markerScaledSize ??= new google.maps.Size( + MARKER_ICON_WIDTH, + MARKER_ICON_HEIGHT, + ); + return markerScaledSize; +} + +// fillColor is always a route palette hex from routeColorHex, never user input. +function markerSvgDataUrl(fillColor: string): string { + const svg = ``; + return `data:image/svg+xml;charset=UTF-8,${encodeURIComponent(svg)}`; +} + +function createRoutePinElement(fillColor: string): HTMLElement { + const wrapper = document.createElement("div"); + wrapper.style.width = `${MARKER_ICON_WIDTH}px`; + wrapper.style.height = `${MARKER_ICON_HEIGHT}px`; + // Anchor bottom-center of the pin on the stop (tip is at y=40 in the SVG). + wrapper.style.transform = "translate(-50%, -100%)"; + + const img = document.createElement("img"); + img.src = markerSvgDataUrl(fillColor); + img.width = MARKER_ICON_WIDTH; + img.height = MARKER_ICON_HEIGHT; + img.style.display = "block"; + img.draggable = false; + img.alt = ""; + wrapper.appendChild(img); + return wrapper; +} function routePolylineOptions( strokeColor: string, @@ -307,7 +344,8 @@ function AdvancedMarkers({ if (cancelled) return; - routes.forEach((route) => { + routes.forEach((route, routeIndex) => { + const accentColor = routeColorHex(routeIndex); const sorted = [...route.stops].sort( (a, b) => a.sequence - b.sequence, ); @@ -319,6 +357,7 @@ function AdvancedMarkers({ position, title: stop.address, gmpDraggable: isEditMode, + content: createRoutePinElement(accentColor), }); m.addListener("dragend", () => { @@ -466,7 +505,10 @@ export default function MapComponent({ /> )} {!mapId && - routes.map((route) => { + routes.map((route, routeIndex) => { + const accentColor = routeColorHex(routeIndex); + const iconUrl = markerSvgDataUrl(accentColor); + const scaledSize = getMarkerScaledSize(); const sorted = [...route.stops].sort( (a, b) => a.sequence - b.sequence, ); @@ -485,6 +527,18 @@ export default function MapComponent({ key={stop.id} position={position} title={stop.address} + icon={ + scaledSize + ? { + url: iconUrl, + scaledSize, + anchor: new google.maps.Point( + MARKER_ICON_WIDTH / 2, + MARKER_ICON_HEIGHT, + ), + } + : { url: iconUrl } + } draggable={isEditMode} onDragEnd={(e) => { const latLng = e.latLng; diff --git a/app/ui/src/app/results/components/Sidebar.tsx b/app/ui/src/app/results/components/Sidebar.tsx index 76a6bed9..0fb4f138 100644 --- a/app/ui/src/app/results/components/Sidebar.tsx +++ b/app/ui/src/app/results/components/Sidebar.tsx @@ -1,5 +1,6 @@ // Sidebar: route cards, expand/collapse stops, edit-mode toggle. +import Image from "next/image"; import { useMemo, useState } from "react"; import type { Route } from "../types"; import { routeColorHex } from "../utils/routeColors"; @@ -45,154 +46,234 @@ export default function Sidebar({ } return ( - ); diff --git a/app/ui/src/app/results/page.tsx b/app/ui/src/app/results/page.tsx index 8ec3ad7d..eed75126 100644 --- a/app/ui/src/app/results/page.tsx +++ b/app/ui/src/app/results/page.tsx @@ -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"; @@ -107,7 +112,9 @@ export default function ResultsPage() { const cancelPendingPinMove = useCallback(() => setPendingPinMove(null), []); return ( -
+
{error && (
@@ -122,12 +129,12 @@ export default function ResultsPage() {
)}{" "} {/* 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 */} -
+
-
- - Delivery Optimizer - -

Results

-
+

DELIVERY OPTIMIZER

@@ -159,14 +161,14 @@ export default function ResultsPage() { @@ -177,15 +179,20 @@ 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
+ + + + +
-
+
+ {isEditMode && ( +
+ You are now in editing mode +
+ )}