Skip to content

HI-FI: Route Colors for Results Map Pins#152

Open
KesarSidhu wants to merge 9 commits into
benevolentbandwidth:mainfrom
KesarSidhu:step2-pin-colors
Open

HI-FI: Route Colors for Results Map Pins#152
KesarSidhu wants to merge 9 commits into
benevolentbandwidth:mainfrom
KesarSidhu:step2-pin-colors

Conversation

@KesarSidhu
Copy link
Copy Markdown
Contributor

@KesarSidhu KesarSidhu commented May 8, 2026

Summary

Makes map pins route-colored and updates marker visuals to match the design: each stop uses its route palette color with a consistent teardrop pin on Advanced and fallback markers.

Motivation

Map stops should match sidebar/route polyline colors so drivers can tie pins to routes at a glance. This PR stacks on #150 (step2-routes-panel) and only changes marker rendering in Map.tsx.

Changes

Frontend

  • app/ui/src/app/results/components/Map.tsx: Route-colored markers via routeColorHex; shared SVG teardrop for Advanced Markers and classic Marker fallback; tip anchored on the stop lat/lng; module-level google.maps.Size for fallback icons.

Validation

Frontend

  • npm --prefix app/ui run lint
  • npm --prefix app/ui run format:check
  • npm --prefix app/ui run typecheck
  • npm --prefix app/ui run test
  • npm --prefix app/ui run build

Manual: On /results with multiple routes, confirm pin colors match route cards/polylines; with and without NEXT_PUBLIC_GOOGLE_MAPS_MAP_ID, pins look the same shape and sit on the stop.

Risk

Low. UI-only marker changes in Map.tsx. No API or data model changes beyond what #150 already includes.

Rollout and Recovery

Ship after #150 merges (or with base step2-routes-panel). Revert this PR to restore default map markers.

@markboenigk
Copy link
Copy Markdown
Collaborator

Same branch-strategy note as #149 and #150 — this PR shares 5+ files with both of those and all three target main. Whichever lands last will conflict. Worth pausing to stack these sequentially before continuing the series.

New code specific to this PR

The custom pin markers are a nice touch, but two issues:

  • Visual inconsistency: The AdvancedMarkers path uses a CSS rotated-square (borderRadius: "9999px 9999px 9999px 0", transform: "rotate(-45deg)"), while the fallback Marker path renders a proper SVG teardrop. Users without a mapId get a different-looking pin. The SVG approach is the better shape — worth unifying, or at least making them match visually.

  • Pin anchors at the wrong coordinate: transformOrigin: "center" means the element rotates around its center, so the visual tip of the diamond doesn't point at the actual stop coordinate — it'll be offset. The tip needs to align with the lat/lng, which requires either adjusting transformOrigin or adding a position offset to the marker.

Carried over from #149/#150 (still unresolved)

  • SidebarResultsButton links unconditionally to /results — the removed TODO guard was load-bearing, navigating there with no sessionStorage data hits an error modal.
  • Export button still has no onClick.
  • Save button still always visible and silently no-ops with no pending pin move.

Nit: new google.maps.Size(28, 40) is constructed inside routes.map() on every render — could be a module-level constant. Also, markerSvgDataUrl embeds fillColor directly into the SVG string; safe here since it only ever receives palette values, but worth a short comment noting that assumption.

Kesar Sidhu and others added 8 commits May 24, 2026 18:23
Align the results header with the mock by using a compact app label plus Save/Export actions while preserving the existing sidebar toggle and pending-edit cancel behavior.

Co-authored-by: Cursor <cursoragent@cursor.com>
Show Save/Cancel only when a pin move is pending, disable Export until
export modal ships, and restore a screen-reader Results heading.

Co-authored-by: Cursor <cursoragent@cursor.com>
Replay the full pre-rebase results stack (route colors, map polylines,
top bar, left strip, routes panel) on current upstream/main in one
commit to avoid conflicts with directions work already merged upstream.

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Avoid setState in useEffect (react-hooks/set-state-in-effect) by
reading sessionStorage when rendering SidebarResultsButton.

Co-authored-by: Cursor <cursoragent@cursor.com>
Use SIDEBAR_NAV_ITEM_INACTIVE (exported from formStyles.v2) and restore
optional phoneNumber on Stop for vroomToRoutes.

Co-authored-by: Cursor <cursoragent@cursor.com>
Advanced and fallback markers share the same SVG icon, tip-aligned
coordinates, and a module-level scaledSize singleton.

Co-authored-by: Cursor <cursoragent@cursor.com>
Point results page and SidebarResultsButton at layout/sidebar and
edit/utils after folder restructure on upstream/main.

Co-authored-by: Cursor <cursoragent@cursor.com>
Copy link
Copy Markdown
Collaborator

@kirillakovalenko kirillakovalenko left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here is my review of PR #152HI-FI: Route Colors for Results Map Pins.


Overview

This PR introduces route-colored map pins (teardrop SVG) for both the Advanced Markers and classic Marker fallback paths, and makes several surrounding UI changes: a refactored results sidebar, a shared edit nav sidebar on the results page, and smart SidebarResultsButton state (active / navigable / disabled).

Scope is broader than the title suggests. In addition to map pins, the PR restructures the results page header, results sidebar layout, and adds the edit-page's sidebar to the results page. This scope creep is worth noting.


Bugs / Correctness

1. Advanced Marker tip anchor is wrong

Map.tsx

wrapper.style.transform = "translate(-50%, -50%)";

The teardrop SVG has its tip at the bottom of the image (y=40 in a 0 0 28 40 viewBox). translate(-50%, -50%) places the center of the wrapper at the stop coordinate, not the tip. The result is the pin floats ~20 px above the actual stop.

For the tip to sit on the stop, you need the bottom-center of the image to land on the coordinate:

wrapper.style.transform = "translate(-50%, -100%)";

The classic Marker fallback gets this right with anchor: new google.maps.Point(MARKER_ICON_WIDTH / 2, MARKER_ICON_HEIGHT).

2. Hydration mismatch in SidebarResultsButton

SidebarResultsButton.tsx

readHasOptimizeResults() reads sessionStorage synchronously during render. On the server it returns false (SSR guard), so the component renders as disabled, but on the client it may render as a link. Next.js will warn about (or silently mis-hydrate) this.

Fix with the standard pattern:

const [hasStoredRoutes, setHasStoredRoutes] = useState(false);
useEffect(() => { setHasStoredRoutes(readHasOptimizeResults()); }, []);

3. Save and Cancel buttons look identical

page.tsx

The pending-pin-move Save button was previously styled bg-amber-500 (clearly the primary action). Now both Save and Cancel share rounded-full border border-zinc-300 bg-white — visually indistinguishable. Users may click Cancel when intending to Save.


Design / UX Issues

4. Hardcoded branding footer in Sidebar

results/components/Sidebar.tsx

<p className="text-3xl leading-none font-semibold text-[var(--edit-teal-500)]">b²</p>
<p>Built with ❤️ for Humanity.</p>
<p>The Benevolent Bandwidth Foundation</p>

This looks like placeholder/demo content that will be awkward in production. If this is intentional branding, it should be in a shared constant or config, not inline JSX in a sidebar component.

5. Visually hidden <h1>

page.tsx

<h1 className="sr-only">Results</h1>

The visible heading is replaced by the brand label "DELIVERY OPTIMIZER". This is an accessibility tradeoff — screen readers still get an h1, but sighted users lose page-title context.


Code Quality

6. Results page imports edit-page internals

page.tsx

import styles from "../edit/edit.module.css";
import EditSidebar from "../edit/components/layout/sidebar/Sidebar";

The results page now reaches into the edit page's module-level CSS and sidebar component. These components are not abstracted into a shared layout. If the edit page's sidebar changes, the results page breaks. Consider extracting the shared nav sidebar into app/ui/src/app/components/ or a layout/ subtree.

7. Weak type validation in hasOptimizeResults.ts

hasOptimizeResults.ts

const parsed = JSON.parse(stored) as Route[];
return Array.isArray(parsed) && parsed.length > 0;

The as Route[] cast is unchecked — only array length is validated, not the Route shape. Stale/corrupted sessionStorage with a non-empty array of wrong shape would still return true and show the Results button as navigable. At minimum validate one required field (e.g., vehicleId).

8. Module-level mutable state

Map.tsx

let markerScaledSize: google.maps.Size | undefined;

Module-level mutable state leaks across tests and could cause issues in future SSR/RSC environments. The lazy init pattern is fine in production, but a factory function returning a local value (or useRef if it were a hook) would be cleaner.


Validation Gaps

The PR checklist shows npm run test and npm run build unchecked. Given the hydration change and advanced-marker anchor issue, running both before merge is important.


Summary

  |   -- | -- Must fix before merge | Advanced marker tip anchor (#1), Save/Cancel button parity (#3) Should fix before merge | Hydration mismatch (#2), edit-page coupling (#6) Low priority / follow-up | Branding footer (#4), hidden h1 (#5), weak type validation (#7) Tests | Uncheck boxes must be addressed — build in particular can reveal type errors from the new imports Here is my review of PR #152 — HI-FI: Route Colors for Results Map Pins.

Overview
This PR introduces route-colored map pins (teardrop SVG) for both the Advanced Markers and classic Marker fallback paths, and makes several surrounding UI changes: a refactored results sidebar, a shared edit nav sidebar on the results page, and smart SidebarResultsButton state (active / navigable / disabled).

Scope is broader than the title suggests. In addition to map pins, the PR restructures the results page header, results sidebar layout, and adds the edit-page's sidebar to the results page. This scope creep is worth noting.

Bugs / Correctness

  1. Advanced Marker tip anchor is wrong
    Map.tsx

wrapper.style.transform = "translate(-50%, -50%)";
The teardrop SVG has its tip at the bottom of the image (y=40 in a 0 0 28 40 viewBox). translate(-50%, -50%) places the center of the wrapper at the stop coordinate, not the tip. The result is the pin floats ~20 px above the actual stop.

For the tip to sit on the stop, you need the bottom-center of the image to land on the coordinate:

wrapper.style.transform = "translate(-50%, -100%)";
The classic Marker fallback gets this right with anchor: new google.maps.Point(MARKER_ICON_WIDTH / 2, MARKER_ICON_HEIGHT).

  1. Hydration mismatch in SidebarResultsButton
    SidebarResultsButton.tsx

readHasOptimizeResults() reads sessionStorage synchronously during render. On the server it returns false (SSR guard), so the component renders as disabled, but on the client it may render as a link. Next.js will warn about (or silently mis-hydrate) this.

Fix with the standard pattern:

const [hasStoredRoutes, setHasStoredRoutes] = useState(false);
useEffect(() => { setHasStoredRoutes(readHasOptimizeResults()); }, []);
3. Save and Cancel buttons look identical
page.tsx

The pending-pin-move Save button was previously styled bg-amber-500 (clearly the primary action). Now both Save and Cancel share rounded-full border border-zinc-300 bg-white — visually indistinguishable. Users may click Cancel when intending to Save.

Design / UX Issues
4. Hardcoded branding footer in Sidebar
results/components/Sidebar.tsx

Built with ❤️ for Humanity.

The Benevolent Bandwidth Foundation

This looks like placeholder/demo content that will be awkward in production. If this is intentional branding, it should be in a shared constant or config, not inline JSX in a sidebar component.
  1. Visually hidden


    page.tsx

Results

The visible heading is replaced by the brand label "DELIVERY OPTIMIZER". This is an accessibility tradeoff — screen readers still get an h1, but sighted users lose page-title context.

Code Quality
6. Results page imports edit-page internals
page.tsx

import styles from "../edit/edit.module.css";
import EditSidebar from "../edit/components/layout/sidebar/Sidebar";
The results page now reaches into the edit page's module-level CSS and sidebar component. These components are not abstracted into a shared layout. If the edit page's sidebar changes, the results page breaks. Consider extracting the shared nav sidebar into app/ui/src/app/components/ or a layout/ subtree.

  1. Weak type validation in hasOptimizeResults.ts
    hasOptimizeResults.ts

const parsed = JSON.parse(stored) as Route[];
return Array.isArray(parsed) && parsed.length > 0;
The as Route[] cast is unchecked — only array length is validated, not the Route shape. Stale/corrupted sessionStorage with a non-empty array of wrong shape would still return true and show the Results button as navigable. At minimum validate one required field (e.g., vehicleId).

  1. Module-level mutable state
    Map.tsx

let markerScaledSize: google.maps.Size | undefined;
Module-level mutable state leaks across tests and could cause issues in future SSR/RSC environments. The lazy init pattern is fine in production, but a factory function returning a local value (or useRef if it were a hook) would be cleaner.

Validation Gaps
The PR checklist shows npm run test and npm run build unchecked. Given the hydration change and advanced-marker anchor issue, running both before merge is important.

Summary
Must fix before merge Advanced marker tip anchor (#1), Save/Cancel button parity (#3)
Should fix before merge Hydration mismatch (#2), edit-page coupling (#6)
Low priority / follow-up Branding footer (#4), hidden h1 (#5), weak type validation (#7)
Tests Uncheck boxes must be addressed — build in particular can reveal type errors from the new imports

Co-authored-by: Cursor <cursoragent@cursor.com>
@KesarSidhu
Copy link
Copy Markdown
Contributor Author

@markboenigk

@markboenigk
Copy link
Copy Markdown
Collaborator

Good fixes since the last round — the pin anchor, SVG unification, Save/Cancel styling, and top bar conditional rendering are all sorted.

Two items before this is mergeable:

  • Run npm --prefix app/ui run test and npm --prefix app/ui run build and check them off. The CSS Modules composes refactor is a build-time change and needs a clean build to confirm.
  • Merge HI-FI: Left Navigation Strip on Results #151 first, then rebase this branch onto the updated main. Both PRs modify results/page.tsx, SidebarResultsButton.tsx, useOptimize.ts, and hasOptimizeResults.ts — they'll conflict if merged independently.

Once those are done this is ready to go.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants