Skip to content
Open
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
5 changes: 5 additions & 0 deletions app/ui/src/app/constants/b2Branding.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export const B2_LOGO_MARK = "b²";

export const B2_FOOTER_TAGLINE = "Built with ❤️ for Humanity.";

export const B2_FOUNDATION_NAME = "The Benevolent Bandwidth Foundation";
7 changes: 5 additions & 2 deletions app/ui/src/app/edit/components/layout/sidebar/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,14 @@ import { SIDEBAR_ROOT, SIDEBAR_NAV } from "@/app/edit/formStyles.v2";

type SidebarProps = {
children: React.ReactNode;
className?: string;
};

export default function Sidebar({ children }: SidebarProps) {
export default function Sidebar({ children, className }: SidebarProps) {
return (
<aside className={SIDEBAR_ROOT}>
<aside
className={className ? `${SIDEBAR_ROOT} ${className}` : SIDEBAR_ROOT}
>
<div className={SIDEBAR_NAV}>{children}</div>
</aside>
);
Expand Down
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>
);
}
90 changes: 3 additions & 87 deletions app/ui/src/app/edit/edit.module.css
Original file line number Diff line number Diff line change
@@ -1,93 +1,9 @@
/**
* Scoped design tokens for the edit page.
* Import this only in app/edit/page.tsx — does not affect other pages.
*
* Usage in page.tsx:
* import styles from './edit.module.css';
* <div className={styles.root}>...</div>
*
* All CSS variables defined on .root are available to every descendant,
* and can be referenced in formStyles.v2.ts as Tailwind arbitrary values:
* bg-[var(--edit-page-bg)]
* text-[var(--edit-foreground)]
* etc.
*/

/* tokens in shared/editDesignTokens.module.css */
.root {
/* ── Teal brand ──────────────────────────────────────────────── */
--edit-teal-300: #57ac91;
--edit-teal-500: #397461;
--edit-teal-600: #267b67;
--edit-teal-700: #265749;
--edit-teal-alpha: #39746126;
--edit-pagination-active-bg: rgba(26, 173, 144, 0.24);
--edit-pagination-mobile-active-bg: #c3e1d8;

/* ── Stone neutrals (Figma Stone scale) ─────────────────────── */
--edit-bg-primary: #fdfdfc;
--edit-stone-50: #f8f7f5;
--edit-stone-200: #dcdbd8;
--edit-stone-500: #908f8c;
--edit-stone-600: #777673;
--edit-stone-700: #5d5c59;

/* ── Nav ─────────────────────────────────────────────────────── */
--edit-container-active: #d5f2e8;
--edit-text-primary: #272725;
--edit-text-secondary: #464544;

/* ── Buttons ──────────────────────────────────────────────────── */
--edit-btn-primary: #4cb599;

--edit-secondary-btn-hover: rgba(0, 0, 0, 0.02);
--edit-secondary-btn-pressed: rgba(0, 0, 0, 0.04);

--edit-tertiary-btn-hover: #f6f5f2;
--edit-tertiary-btn-pressed: #f6f5f2;

/* ── Semantic status ─────────────────────────────────────────── */
--edit-container-success: #daf1db;
--edit-text-success: #2a7e3b;
--edit-required-asterisk: #da1b0b;
--edit-error-border: #da1b0b;
--edit-error-bg: #fef2f2;
--edit-footer-logo-bg: url("/b2logo.png") -25.852px -10.077px / 293.436%
265.161% no-repeat;

/* ── Delete button ──────────────────────────────────────────── */
--edit-btn-delete: #da1b0b;
--edit-text-invert: #fdfdfc;

/* ── Icons ──────────────────────────────────────────────────── */
--edit-icon-vehicle: #1c1b1f;
--edit-icon-location: #1c1b1f;
--edit-primary-icon: #3d3d3c;

/* ── Empty state illustrations ──────────────────────────────── */
--edit-empty-state-shadow: #e8e8e8;

/* Vehicle illustration */
--edit-vehicle-body: #4f46e5;
--edit-vehicle-window: #a5b4fc;
--edit-vehicle-accent: #3730a3;
--edit-vehicle-detail: #c7d2fe;
--edit-vehicle-wheel-outer: #1e1e2e;
--edit-vehicle-wheel-mid: #6b7280;
--edit-vehicle-wheel-inner: #d1d5db;
--edit-vehicle-light: #fcd34d;
--edit-vehicle-back-panel: #312e81;

/* Address illustration */
--edit-address-accent: #f97316;
--edit-address-on-accent: #ffffff;
--edit-address-card-bg: #fff7ed;
--edit-address-card-border: #fed7aa;
--edit-address-card-header: #ffedd5;
--edit-address-card-line-dark: #e5e7eb;
--edit-address-card-line-light: #f3f4f6;
composes: root from "../shared/editDesignTokens.module.css";
}

/* ── Primary Button Hover and Pressed States Overlay ──────────────────────── */
/* primary button hover overlay */
.primaryBtnOverlay {
position: relative;
overflow: hidden;
Expand Down
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);
}

export 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;
}
52 changes: 50 additions & 2 deletions app/ui/src/app/results/components/Map.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,37 @@ 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;

function getMarkerScaledSize(): google.maps.Size | undefined {
if (typeof google === "undefined") return undefined;
return new google.maps.Size(MARKER_ICON_WIDTH, MARKER_ICON_HEIGHT);
}

// fillColor is always a route palette hex from routeColorHex, never user input.
function markerSvgDataUrl(fillColor: string): string {
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${MARKER_ICON_WIDTH}" height="${MARKER_ICON_HEIGHT}" viewBox="0 0 28 40"><path d="M14 1C7.373 1 2 6.373 2 13c0 9.246 12 24 12 24s12-14.754 12-24C26 6.373 20.627 1 14 1z" fill="${fillColor}" stroke="#ffffff" stroke-width="2"/><circle cx="14" cy="13" r="4.25" fill="#ffffff"/></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,
Expand Down Expand Up @@ -307,7 +338,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,
);
Expand All @@ -319,6 +351,7 @@ function AdvancedMarkers({
position,
title: stop.address,
gmpDraggable: isEditMode,
content: createRoutePinElement(accentColor),
});

m.addListener("dragend", () => {
Expand Down Expand Up @@ -466,7 +499,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,
);
Expand All @@ -485,6 +521,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;
Expand Down
Loading
Loading