From 276e73412abef28c222c52a84334d49f5e414f3c Mon Sep 17 00:00:00 2001
From: Copilot <198982749+Copilot@users.noreply.github.com>
Date: Mon, 17 Nov 2025 23:39:08 +0100
Subject: Use consolidated data API in map sheet with shared card component
 (#100)

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: arielcostas <94913521+arielcostas@users.noreply.github.com>
Co-authored-by: Ariel Costas Guerrero <ariel@costas.dev>
---
 src/frontend/app/components/StopMapSheet.tsx | 151 ++++++++++++++++++++++-----
 1 file changed, 122 insertions(+), 29 deletions(-)

(limited to 'src/frontend/app/components/StopMapSheet.tsx')

diff --git a/src/frontend/app/components/StopMapSheet.tsx b/src/frontend/app/components/StopMapSheet.tsx
index e87e8c8..b3a1666 100644
--- a/src/frontend/app/components/StopMapSheet.tsx
+++ b/src/frontend/app/components/StopMapSheet.tsx
@@ -1,6 +1,10 @@
 import maplibregl from "maplibre-gl";
 import React, { useEffect, useMemo, useRef, useState } from "react";
-import Map, { AttributionControl, Marker, type MapRef } from "react-map-gl/maplibre";
+import Map, {
+  AttributionControl,
+  Marker,
+  type MapRef,
+} from "react-map-gl/maplibre";
 import { useApp } from "~/AppContext";
 import { getLineColor } from "~/data/LineColors";
 import type { RegionId } from "~/data/RegionConfig";
@@ -53,24 +57,38 @@ export const StopMap: React.FC<StopMapProps> = ({
     const lat2 = (b.lat * Math.PI) / 180;
     const sinDLat = Math.sin(dLat / 2);
     const sinDLon = Math.sin(dLon / 2);
-    const h = sinDLat * sinDLat + Math.cos(lat1) * Math.cos(lat2) * sinDLon * sinDLon;
+    const h =
+      sinDLat * sinDLat + Math.cos(lat1) * Math.cos(lat2) * sinDLon * sinDLon;
     return 2 * R * Math.asin(Math.min(1, Math.sqrt(h)));
   };
   const computeFocusPoints = (): Pt[] => {
     const buses: Pt[] = [];
     for (const c of busPositions) {
-      if (c.currentPosition) buses.push({ lat: c.currentPosition.latitude, lon: c.currentPosition.longitude });
+      if (c.currentPosition)
+        buses.push({
+          lat: c.currentPosition.latitude,
+          lon: c.currentPosition.longitude,
+        });
     }
-    const stopPt = stop.latitude && stop.longitude ? { lat: stop.latitude, lon: stop.longitude } : null;
-    const userPt = userPosition ? { lat: userPosition.latitude, lon: userPosition.longitude } : null;
+    const stopPt =
+      stop.latitude && stop.longitude
+        ? { lat: stop.latitude, lon: stop.longitude }
+        : null;
+    const userPt = userPosition
+      ? { lat: userPosition.latitude, lon: userPosition.longitude }
+      : null;
 
     if (buses.length === 0 && !stopPt && !userPt) return [];
 
     // Choose anchor for proximity: stop > user > average of buses
     let anchor: Pt | null = stopPt || userPt || null;
     if (!anchor && buses.length) {
-      let lat = 0, lon = 0;
-      for (const b of buses) { lat += b.lat; lon += b.lon; }
+      let lat = 0,
+        lon = 0;
+      for (const b of buses) {
+        lat += b.lat;
+        lon += b.lon;
+      }
       anchor = { lat: lat / buses.length, lon: lon / buses.length };
     }
 
@@ -122,7 +140,7 @@ export const StopMap: React.FC<StopMapProps> = ({
           });
         },
         () => {},
-        { enableHighAccuracy: true, maximumAge: 15000, timeout: 5000 },
+        { enableHighAccuracy: true, maximumAge: 15000, timeout: 5000 }
       );
       geoWatchId.current = navigator.geolocation.watchPosition(
         (pos) => {
@@ -133,7 +151,7 @@ export const StopMap: React.FC<StopMapProps> = ({
           });
         },
         () => {},
-        { enableHighAccuracy: true, maximumAge: 30000, timeout: 10000 },
+        { enableHighAccuracy: true, maximumAge: 30000, timeout: 10000 }
       );
     } catch {}
     return () => {
@@ -158,7 +176,7 @@ export const StopMap: React.FC<StopMapProps> = ({
 
   const busPositions = useMemo(
     () => circulations.filter((c) => !!c.currentPosition),
-    [circulations],
+    [circulations]
   );
 
   // Fit bounds to stop + buses, with ~1km padding each side, with a modest animation
@@ -185,13 +203,17 @@ export const StopMap: React.FC<StopMapProps> = ({
     const bounds = new maplibregl.LngLatBounds(sw, ne);
 
     // Determine predominant bus quadrant relative to stop to bias padding.
-    const padding: number | { top: number; right: number; bottom: number; left: number } = 24;
+    const padding:
+      | number
+      | { top: number; right: number; bottom: number; left: number } = 24;
 
     // If the diagonal is huge (likely outliers sneaked in), clamp via zoom fallback
     try {
       if (points.length === 1) {
         const only = points[0];
-        mapRef.current.getMap().jumpTo({ center: [only.lon, only.lat], zoom: 16 });
+        mapRef.current
+          .getMap()
+          .jumpTo({ center: [only.lon, only.lat], zoom: 16 });
       } else {
         mapRef.current.fitBounds(bounds, {
           padding: padding as any,
@@ -208,7 +230,10 @@ export const StopMap: React.FC<StopMapProps> = ({
     const pts = computeFocusPoints();
     if (pts.length === 0) return;
 
-    let minLat = pts[0].lat, maxLat = pts[0].lat, minLon = pts[0].lon, maxLon = pts[0].lon;
+    let minLat = pts[0].lat,
+      maxLat = pts[0].lat,
+      minLon = pts[0].lon,
+      maxLon = pts[0].lon;
     for (const p of pts) {
       if (p.lat < minLat) minLat = p.lat;
       if (p.lat > maxLat) maxLat = p.lat;
@@ -220,14 +245,22 @@ export const StopMap: React.FC<StopMapProps> = ({
     const ne = [maxLon, maxLat] as [number, number];
     const bounds = new maplibregl.LngLatBounds(sw, ne);
 
-    const padding: number | { top: number; right: number; bottom: number; left: number } = 24;
+    const padding:
+      | number
+      | { top: number; right: number; bottom: number; left: number } = 24;
 
     try {
       if (pts.length === 1) {
         const only = pts[0];
-        mapRef.current.getMap().easeTo({ center: [only.lon, only.lat], zoom: 16, duration: 450 });
+        mapRef.current
+          .getMap()
+          .easeTo({ center: [only.lon, only.lat], zoom: 16, duration: 450 });
       } else {
-        mapRef.current.fitBounds(bounds, { padding: padding as any, duration: 500, maxZoom: 17 } as any);
+        mapRef.current.fitBounds(bounds, {
+          padding: padding as any,
+          duration: 500,
+          maxZoom: 17,
+        } as any);
       }
     } catch {}
   };
@@ -256,15 +289,36 @@ export const StopMap: React.FC<StopMapProps> = ({
 
           {/* Stop marker (center) */}
           {stop.latitude && stop.longitude && (
-            <Marker longitude={stop.longitude} latitude={stop.latitude} anchor="bottom">
+            <Marker
+              longitude={stop.longitude}
+              latitude={stop.latitude}
+              anchor="bottom"
+            >
               <div title={`Stop ${stop.stopId}`}>
                 <svg width="28" height="36" viewBox="0 0 28 36">
                   <defs>
-                    <filter id="drop" x="-20%" y="-20%" width="140%" height="140%">
-                      <feDropShadow dx="0" dy="1" stdDeviation="1" flood-opacity="0.35" />
+                    <filter
+                      id="drop"
+                      x="-20%"
+                      y="-20%"
+                      width="140%"
+                      height="140%"
+                    >
+                      <feDropShadow
+                        dx="0"
+                        dy="1"
+                        stdDeviation="1"
+                        floodOpacity={0.35}
+                      />
                     </filter>
                   </defs>
-                  <path d="M14 0C6.82 0 1 5.82 1 13c0 8.5 11 23 13 23s13-14.5 13-23C27 5.82 21.18 0 14 0z" fill="#1976d2" stroke="#fff" strokeWidth="2" filter="url(#drop)" />
+                  <path
+                    d="M14 0C6.82 0 1 5.82 1 13c0 8.5 11 23 13 23s13-14.5 13-23C27 5.82 21.18 0 14 0z"
+                    fill="#1976d2"
+                    stroke="#fff"
+                    strokeWidth="2"
+                    filter="url(#drop)"
+                  />
                   <circle cx="14" cy="13" r="5" fill="#fff" />
                   <circle cx="14" cy="13" r="3" fill="#1976d2" />
                 </svg>
@@ -274,7 +328,11 @@ export const StopMap: React.FC<StopMapProps> = ({
 
           {/* User position marker (if available) */}
           {userPosition && (
-            <Marker longitude={userPosition.longitude} latitude={userPosition.latitude} anchor="center">
+            <Marker
+              longitude={userPosition.longitude}
+              latitude={userPosition.latitude}
+              anchor="center"
+            >
               <div className="user-dot" title="Your location">
                 <div className="user-dot__pulse" />
                 <div className="user-dot__core" />
@@ -290,7 +348,12 @@ export const StopMap: React.FC<StopMapProps> = ({
             const gaps: number[] = new Array(busPositions.length).fill(baseGap);
             if (map && zoom >= 14.5 && busPositions.length > 1) {
               const pts = busPositions.map((c) =>
-                c.currentPosition ? map.project([c.currentPosition.longitude, c.currentPosition.latitude]) : null,
+                c.currentPosition
+                  ? map.project([
+                      c.currentPosition.longitude,
+                      c.currentPosition.latitude,
+                    ])
+                  : null
               );
               for (let i = 0; i < pts.length; i++) {
                 const pi = pts[i];
@@ -314,7 +377,12 @@ export const StopMap: React.FC<StopMapProps> = ({
               const showLabel = zoom >= 13;
               const labelGap = gaps[idx] ?? baseGap;
               return (
-                <Marker key={idx} longitude={p.longitude} latitude={p.latitude} anchor="center">
+                <Marker
+                  key={idx}
+                  longitude={p.longitude}
+                  latitude={p.latitude}
+                  anchor="center"
+                >
                   <div
                     style={{
                       display: "flex",
@@ -329,9 +397,16 @@ export const StopMap: React.FC<StopMapProps> = ({
                       width="20"
                       height="20"
                       viewBox="0 0 24 24"
-                      style={{ filter: "drop-shadow(0 1px 2px rgba(0,0,0,0.3))" }}
+                      style={{
+                        filter: "drop-shadow(0 1px 2px rgba(0,0,0,0.3))",
+                      }}
                     >
-                      <path d="M12 2 L20 22 L12 18 L4 22 Z" fill={lineColor.background} stroke="#fff" strokeWidth="1.5" />
+                      <path
+                        d="M12 2 L20 22 L12 18 L4 22 Z"
+                        fill={lineColor.background}
+                        stroke="#fff"
+                        strokeWidth="1.5"
+                      />
                     </svg>
                     {showLabel && (
                       <div
@@ -362,11 +437,29 @@ export const StopMap: React.FC<StopMapProps> = ({
       )}
       {/* Floating controls */}
       <div className="map-floating-controls">
-        <button type="button" aria-label="Center" className="center-btn" onClick={handleCenter} title="Center view">
+        <button
+          type="button"
+          aria-label="Center"
+          className="center-btn"
+          onClick={handleCenter}
+          title="Center view"
+        >
           <svg width="20" height="20" viewBox="0 0 24 24" aria-hidden="true">
-            <circle cx="12" cy="12" r="3" fill="currentColor"/>
-            <path d="M12 2v3M12 19v3M2 12h3M19 12h3" stroke="currentColor" strokeWidth="2" strokeLinecap="round"/>
-            <circle cx="12" cy="12" r="8" fill="none" stroke="currentColor" strokeWidth="1.5"/>
+            <circle cx="12" cy="12" r="3" fill="currentColor" />
+            <path
+              d="M12 2v3M12 19v3M2 12h3M19 12h3"
+              stroke="currentColor"
+              strokeWidth="2"
+              strokeLinecap="round"
+            />
+            <circle
+              cx="12"
+              cy="12"
+              r="8"
+              fill="none"
+              stroke="currentColor"
+              strokeWidth="1.5"
+            />
           </svg>
         </button>
       </div>
-- 
cgit v1.3

