Skip to content

Commit af68b50

Browse files
committed
feat: add pink route fetching banner and loading state management
- Introduced a new PinkRouteFetchingBanner component to display a loading state while fetching routes. - Updated MapPage and PinkLineMapPage to manage the fetching state and conditionally render the banner. - Enhanced CSS styles for the fetching banner to improve user experience during route computation.
1 parent 0514702 commit af68b50

6 files changed

Lines changed: 220 additions & 43 deletions

File tree

src/index.css

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -877,6 +877,54 @@ a:hover {
877877
z-index: 1002;
878878
}
879879

880+
.pink-route-fetch-container {
881+
z-index: 1003;
882+
pointer-events: none;
883+
}
884+
885+
.pink-route-fetch-inner {
886+
flex-direction: column;
887+
align-items: center;
888+
justify-content: center;
889+
gap: 6px;
890+
padding: 6px 10px;
891+
width: auto;
892+
min-width: 0;
893+
max-width: min(320px, calc(100vw - 24px));
894+
}
895+
896+
.pink-route-fetch-inner.shape-name-input-form {
897+
padding: 6px 10px;
898+
gap: 6px;
899+
align-items: center;
900+
}
901+
902+
.pink-route-fetch-spinner {
903+
flex-shrink: 0;
904+
box-sizing: border-box;
905+
width: 28px;
906+
height: 28px;
907+
border: 2px solid rgba(255, 255, 255, 0.22);
908+
border-top-color: #ff69b4;
909+
border-radius: 50%;
910+
animation: pink-route-fetch-spin 0.65s linear infinite;
911+
}
912+
913+
.pink-route-fetch-label {
914+
color: rgba(255, 255, 255, 0.92);
915+
font-size: 13px;
916+
font-weight: 500;
917+
line-height: 1.25;
918+
text-align: center;
919+
white-space: normal;
920+
}
921+
922+
@keyframes pink-route-fetch-spin {
923+
to {
924+
transform: rotate(360deg);
925+
}
926+
}
927+
880928
.shape-name-input-form {
881929
background: rgba(20, 20, 20, 0.4);
882930
backdrop-filter: blur(12px);

src/pages/MapPage/PinkLineMapPage.tsx

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useEffect, useRef, useState } from "react";
1+
import { useEffect, useMemo, useRef, useState } from "react";
22
import { useNavigate } from "react-router-dom";
33
import L from "leaflet";
44
import "leaflet/dist/leaflet.css";
@@ -17,6 +17,7 @@ import {
1717
} from "../../utils/pinkLineRoute";
1818
import supabase from "../../supabase";
1919
import PinkLineNodeForm from "./PinkLineNodeForm";
20+
import PinkRouteFetchingBanner from "./PinkRouteFetchingBanner";
2021
import { computeRouteViaEdgeFunction } from "../../services/googleRoutes";
2122
import { addParkingLotsLayer } from "../../utils/parkingLayer";
2223

@@ -70,7 +71,13 @@ const PinkLineMapPage = () => {
7071
const [integratedRoute, setIntegratedRoute] = useState<IntegratedRoute | null>(null);
7172
const [routeForPersistence, setRouteForPersistence] = useState<Array<[number, number]>>([]);
7273
const [routeError, setRouteError] = useState<string | null>(null);
74+
const [isFetchingPinkRoute, setIsFetchingPinkRoute] = useState(false);
7375
const busyRef = useRef(false);
76+
77+
const pinkUserPointsKey = useMemo(
78+
() => JSON.stringify(nodes.map((n) => [n.lat, n.lng])),
79+
[nodes]
80+
);
7481
const pendingMarkerRef = useRef<L.Marker | null>(null);
7582
const parkingLayerRef = useRef<L.LayerGroup | null>(null);
7683

@@ -214,14 +221,23 @@ const PinkLineMapPage = () => {
214221
const rebuildRoute = async () => {
215222
const basePaths = defaultLinePathsRef.current;
216223
if (basePaths.length === 0) {
224+
setIsFetchingPinkRoute(false);
217225
setIntegratedRoute(null);
218226
setRouteForPersistence([]);
219227
setRouteError(null);
220228
return;
221229
}
222230

223-
const userPoints = nodes.map((n) => [n.lat, n.lng] as [number, number]);
231+
const userPoints = JSON.parse(pinkUserPointsKey) as [number, number][];
232+
const willCallGoogle = userPoints.length > 0;
233+
if (!willCallGoogle) {
234+
setIsFetchingPinkRoute(false);
235+
}
236+
224237
try {
238+
if (willCallGoogle) {
239+
setIsFetchingPinkRoute(true);
240+
}
225241
const route = await buildIntegratedRouteWithGoogleDetours(basePaths, userPoints, {
226242
computeRoute: async (waypoints) => {
227243
const computed = await computeRouteViaEdgeFunction(waypoints);
@@ -239,6 +255,10 @@ const PinkLineMapPage = () => {
239255
setIntegratedRoute(null);
240256
setRouteForPersistence([]);
241257
setRouteError("Failed to compute route using Google Routes. Please adjust points and try again.");
258+
} finally {
259+
if (!cancelled && willCallGoogle) {
260+
setIsFetchingPinkRoute(false);
261+
}
242262
}
243263
};
244264

@@ -247,7 +267,7 @@ const PinkLineMapPage = () => {
247267
return () => {
248268
cancelled = true;
249269
};
250-
}, [nodes, defaultLineLoaded, project?.id]);
270+
}, [pinkUserPointsKey, defaultLineLoaded, project?.id]);
251271

252272
// Single rendering effect: clears ALL visuals, then redraws from scratch.
253273
// Every pink polyline on the map comes from routeLayersRef — nothing else.
@@ -440,6 +460,9 @@ const PinkLineMapPage = () => {
440460
onCancel={() => setPendingNode(null)}
441461
/>
442462
)}
463+
{!pendingNode && (isSubmitting || isFetchingPinkRoute) && (
464+
<PinkRouteFetchingBanner variant={isSubmitting ? "submit" : "route"} />
465+
)}
443466
<div
444467
style={{
445468
position: "absolute",
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
type PinkRouteFetchingBannerVariant = "route" | "submit";
2+
3+
const LABELS: Record<PinkRouteFetchingBannerVariant, { text: string; aria: string }> = {
4+
route: { text: "טוען מסלול…", aria: "טוען מסלול" },
5+
submit: { text: "שומר במסד הנתונים…", aria: "שומר במסד הנתונים" },
6+
};
7+
8+
interface PinkRouteFetchingBannerProps {
9+
variant: PinkRouteFetchingBannerVariant;
10+
}
11+
12+
const PinkRouteFetchingBanner = ({ variant }: PinkRouteFetchingBannerProps) => {
13+
const { text, aria } = LABELS[variant];
14+
return (
15+
<div
16+
className="shape-name-input-container pink-route-fetch-container"
17+
role="status"
18+
aria-live="polite"
19+
aria-label={aria}
20+
>
21+
<div className="shape-name-input-form pink-route-fetch-inner">
22+
<span className="pink-route-fetch-spinner" aria-hidden />
23+
<span className="pink-route-fetch-label" dir="rtl">
24+
{text}
25+
</span>
26+
</div>
27+
</div>
28+
);
29+
};
30+
31+
export default PinkRouteFetchingBanner;

src/pages/MapPage/index.tsx

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { useNavigate } from "react-router-dom";
33
import L from "leaflet";
44
import "leaflet/dist/leaflet.css";
55
import PinkLineNodeForm from "./PinkLineNodeForm";
6+
import PinkRouteFetchingBanner from "./PinkRouteFetchingBanner";
67
import MemorialSiteForm from "./MemorialSiteForm";
78
import localMemorialIconUrl from "../../assets/memorial-sites/local-memorial-site.png";
89
import regionalMemorialIconUrl from "../../assets/memorial-sites/regional-memorial-site.png";
@@ -100,6 +101,12 @@ const MapPage = () => {
100101
const [integratedPinkRoute, setIntegratedPinkRoute] = useState<IntegratedRoute | null>(null);
101102
const [pinkRouteForPersistence, setPinkRouteForPersistence] = useState<Array<[number, number]>>([]);
102103
const [pinkRouteError, setPinkRouteError] = useState<string | null>(null);
104+
const [isFetchingPinkRoute, setIsFetchingPinkRoute] = useState(false);
105+
106+
const pinkUserPointsKey = useMemo(
107+
() => JSON.stringify(pinkNodes.map((n) => [n.lat, n.lng])),
108+
[pinkNodes]
109+
);
103110

104111
const [submissionBatches, setSubmissionBatches] = useState<SubmissionBatchSummary[]>([]);
105112
const [submissionBatchesLoading, setSubmissionBatchesLoading] = useState(false);
@@ -306,14 +313,23 @@ const MapPage = () => {
306313

307314
const rebuildPinkRoute = async () => {
308315
if (!defaultLineLoaded || defaultLinePathsRef.current.length === 0) {
316+
setIsFetchingPinkRoute(false);
309317
setIntegratedPinkRoute(null);
310318
setPinkRouteForPersistence([]);
311319
setPinkRouteError(null);
312320
return;
313321
}
314322

315-
const userPoints = pinkNodes.map((n) => [n.lat, n.lng] as [number, number]);
323+
const userPoints = JSON.parse(pinkUserPointsKey) as [number, number][];
324+
const willCallGoogle = userPoints.length > 0;
325+
if (!willCallGoogle) {
326+
setIsFetchingPinkRoute(false);
327+
}
328+
316329
try {
330+
if (willCallGoogle) {
331+
setIsFetchingPinkRoute(true);
332+
}
317333
const route = await buildIntegratedRouteWithGoogleDetours(defaultLinePathsRef.current, userPoints, {
318334
computeRoute: async (waypoints) => {
319335
const computed = await computeRouteViaEdgeFunction(waypoints);
@@ -331,6 +347,10 @@ const MapPage = () => {
331347
setIntegratedPinkRoute(null);
332348
setPinkRouteForPersistence([]);
333349
setPinkRouteError("Failed to compute route using Google Routes. Please adjust points and try again.");
350+
} finally {
351+
if (!cancelled && willCallGoogle) {
352+
setIsFetchingPinkRoute(false);
353+
}
334354
}
335355
};
336356

@@ -339,7 +359,7 @@ const MapPage = () => {
339359
return () => {
340360
cancelled = true;
341361
};
342-
}, [pinkNodes, defaultLineLoaded, pinkProjectId]);
362+
}, [pinkUserPointsKey, defaultLineLoaded, pinkProjectId]);
343363

344364
useEffect(() => {
345365
if (!mapRef.current) return;
@@ -851,6 +871,12 @@ const MapPage = () => {
851871
<>
852872
<div id="map" style={{ height: "100vh", width: "100%" }} />
853873

874+
{!pendingPinkTarget &&
875+
!pendingMemorialTarget &&
876+
(submitting || isFetchingPinkRoute) && (
877+
<PinkRouteFetchingBanner variant={submitting ? "submit" : "route"} />
878+
)}
879+
854880
{pendingPinkTarget && (
855881
<PinkLineNodeForm
856882
onSubmit={(name, description) => {

0 commit comments

Comments
 (0)