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..d5eda200 100644 --- a/app/ui/src/app/results/components/Sidebar.tsx +++ b/app/ui/src/app/results/components/Sidebar.tsx @@ -45,154 +45,169 @@ export default function Sidebar({ } return ( - ); diff --git a/app/ui/src/app/results/page.tsx b/app/ui/src/app/results/page.tsx index 342a582c..b087f378 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"; @@ -36,7 +41,7 @@ export default function ResultsPage() { } }, [initialRoutes.length]); - const [isSidebarOpen, setIsSidebarOpen] = useState(true); + const isSidebarOpen = true; const [isEditMode, setIsEditMode] = useState(false); const [pendingPinMove, setPendingPinMove] = useState( null, @@ -107,7 +112,9 @@ export default function ResultsPage() { const cancelPendingPinMove = useCallback(() => setPendingPinMove(null), []); return ( -
+
{error && (
@@ -122,53 +129,46 @@ 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 */} -
- + + + )} + -

- Results – Route map -

- {pendingPinMove != null && ( -
- - -
- )} + Export + +
+ + + + +
-
+
+ {isEditMode && ( +
+ You are now in editing mode +
+ )}