From 7d2af2d780f28d3fccf73b7c3c814fc7957e2685 Mon Sep 17 00:00:00 2001 From: Mohd Kaif Ansari <155880242+techykaif@users.noreply.github.com> Date: Fri, 21 Aug 2026 23:20:53 +0530 Subject: [PATCH 1/4] fix(map): guard clipboard access on insecure contexts --- src/components/map/places-map.tsx | 50 +++++++++---------------------- 1 file changed, 14 insertions(+), 36 deletions(-) diff --git a/src/components/map/places-map.tsx b/src/components/map/places-map.tsx index f337cfa..90f9d81 100644 --- a/src/components/map/places-map.tsx +++ b/src/components/map/places-map.tsx @@ -63,8 +63,6 @@ export function PlacesMap({ places }: PlacesMapProps) { if (typeof window === "undefined") return null; return parseMapState(window.location.search).placeId ?? null; }); - // A viewport in the URL restores that exact center/zoom on load instead of - // fitting all places; it is then kept in sync as the user pans (see #118). const [viewport, setViewport] = React.useState(() => { if (typeof window === "undefined") return null; const state = parseMapState(window.location.search); @@ -110,8 +108,6 @@ export function PlacesMap({ places }: PlacesMapProps) { return labels; }, [filters]); - // Clear stale saved-places/home data as soon as the signed-in user changes, - // during render rather than an effect, so there's no stale-data flash. const userId = user?.id ?? null; if (userId !== lastUserId) { setLastUserId(userId); @@ -126,7 +122,7 @@ export function PlacesMap({ places }: PlacesMapProps) { React.useEffect(() => { const supabase = createClient(); - if (!supabase) return; // self-host / preview mode: no auth, no private layer + if (!supabase) return; supabase.auth.getUser().then(({ data }) => setUser(data.user)); const { data: { subscription }, @@ -152,8 +148,6 @@ export function PlacesMap({ places }: PlacesMapProps) { fetchUserHome().then(setHome).catch(() => setHome(null)); }, [user]); - // Debounce the search query so filtering doesn't run on every keystroke. - // Clearing the box applies immediately (0 ms); typing waits 250 ms. React.useEffect(() => { const timer = setTimeout( () => setDebouncedQuery(filters.query), @@ -162,9 +156,6 @@ export function PlacesMap({ places }: PlacesMapProps) { return () => clearTimeout(timer); }, [filters.query]); - // Mirror filter, focus, and viewport state back into the URL so it stays - // shareable. replaceState keeps panning out of browser history, so the back - // button still leaves the map in one press (see #118). React.useEffect(() => { if (!hydrated.current) return; const search = mapStateToSearch({ @@ -201,8 +192,6 @@ export function PlacesMap({ places }: PlacesMapProps) { [places, filters.city], ); - // Private layer: saved places always render on top of the public set, - // independent of the public search/type/city filters above. const privatePlaces = React.useMemo( () => savedPlaces.map(userPlaceToPlace), [savedPlaces], @@ -219,7 +208,6 @@ export function PlacesMap({ places }: PlacesMapProps) { return placesByDistance(visible, userLocation); }, [visible, userLocation]); - // Build the results list: nearest-first when located, otherwise all visible. const { rows, resultsHeader, resultsToggle } = React.useMemo(() => { if (userLocation && byDistance.length > 0) { const shown = sortByDistance ? byDistance : byDistance.slice(0, 5); @@ -253,6 +241,10 @@ export function PlacesMap({ places }: PlacesMapProps) { lng: viewport?.lng ?? null, zoom: viewport?.zoom ?? null, }); + if (!("clipboard" in navigator)) { + toast.error("Copying isn't supported in this browser"); + return; + } navigator.clipboard .writeText(url) .then(() => toast.success("Link copied")) @@ -305,7 +297,7 @@ export function PlacesMap({ places }: PlacesMapProps) { function selectPlace(place: Place) { setFocusId(place.id); - setSheetOpen(false); // collapse the mobile sheet so the pin is visible + setSheetOpen(false); } const panelProps = { @@ -342,12 +334,10 @@ export function PlacesMap({ places }: PlacesMapProps) { return (
- {/* Desktop sidebar */} - {/* Map + mobile overlays */}
- {/* Mobile top bar: persistent search + filters trigger */}
@@ -387,42 +376,31 @@ export function PlacesMap({ places }: PlacesMapProps) {
- {/* Mobile near-me FAB, lifted above the peek bar */} - {/* Mobile peek bar: always-visible results summary; tap to open sheet */} - {/* Mobile bottom sheet */} - - + {...panelProps} + />
{user && ( From 37f2c8cc7c5fe27fbe320576e902ec0d3fcf2d89 Mon Sep 17 00:00:00 2001 From: Mohd Kaif Ansari <155880242+techykaif@users.noreply.github.com> Date: Fri, 21 Aug 2026 23:21:02 +0530 Subject: [PATCH 2/4] fix(map): guard clipboard access in pin popup --- src/components/pins/pin-popup.tsx | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/components/pins/pin-popup.tsx b/src/components/pins/pin-popup.tsx index d7321c1..9689eec 100644 --- a/src/components/pins/pin-popup.tsx +++ b/src/components/pins/pin-popup.tsx @@ -32,6 +32,10 @@ export function PinPopup({ place }: PinPopupProps) { lng: place.lng, zoom: 15, }); + if (!("clipboard" in navigator)) { + toast.error("Copying isn't supported in this browser"); + return; + } navigator.clipboard .writeText(url) .then(() => toast.success("Link copied")) @@ -40,7 +44,6 @@ export function PinPopup({ place }: PinPopupProps) { return (
- {/* Header */}

{place.name} @@ -59,12 +62,10 @@ export function PinPopup({ place }: PinPopupProps) {

- {/* Address */} {place.address && (

{place.address}

)} - {/* Exam centre validity */} {place.exam && (
{place.exam} centre @@ -74,7 +75,6 @@ export function PinPopup({ place }: PinPopupProps) {
)} - {/* Actions */}
- {/* Footer badge */}

Added by {place.added_by}

From 848f99286c441b383ca285179fc57c1343bdeaa2 Mon Sep 17 00:00:00 2001 From: Mohd Kaif Ansari <155880242+techykaif@users.noreply.github.com> Date: Fri, 21 Aug 2026 23:26:41 +0530 Subject: [PATCH 3/4] fix(clipboard): preserve pin popup structure --- src/components/pins/pin-popup.tsx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/components/pins/pin-popup.tsx b/src/components/pins/pin-popup.tsx index 9689eec..21063cd 100644 --- a/src/components/pins/pin-popup.tsx +++ b/src/components/pins/pin-popup.tsx @@ -44,6 +44,7 @@ export function PinPopup({ place }: PinPopupProps) { return (
+ {/* Header */}

{place.name} @@ -62,10 +63,12 @@ export function PinPopup({ place }: PinPopupProps) {

+ {/* Address */} {place.address && (

{place.address}

)} + {/* Exam centre validity */} {place.exam && (
{place.exam} centre @@ -75,6 +78,7 @@ export function PinPopup({ place }: PinPopupProps) {
)} + {/* Actions */}
+ {/* Footer badge */}

Added by {place.added_by}

From caa5950bd990cdd2dfb9f12cb66aa757e14189c3 Mon Sep 17 00:00:00 2001 From: Mohd Kaif Ansari <155880242+techykaif@users.noreply.github.com> Date: Fri, 21 Aug 2026 23:27:04 +0530 Subject: [PATCH 4/4] fix(clipboard): restore map structure --- src/components/map/places-map.tsx | 46 ++++++++++++++++++++++++------- 1 file changed, 36 insertions(+), 10 deletions(-) diff --git a/src/components/map/places-map.tsx b/src/components/map/places-map.tsx index 90f9d81..1a57708 100644 --- a/src/components/map/places-map.tsx +++ b/src/components/map/places-map.tsx @@ -63,6 +63,8 @@ export function PlacesMap({ places }: PlacesMapProps) { if (typeof window === "undefined") return null; return parseMapState(window.location.search).placeId ?? null; }); + // A viewport in the URL restores that exact center/zoom on load instead of + // fitting all places; it is then kept in sync as the user pans (see #118). const [viewport, setViewport] = React.useState(() => { if (typeof window === "undefined") return null; const state = parseMapState(window.location.search); @@ -108,6 +110,8 @@ export function PlacesMap({ places }: PlacesMapProps) { return labels; }, [filters]); + // Clear stale saved-places/home data as soon as the signed-in user changes, + // during render rather than an effect, so there's no stale-data flash. const userId = user?.id ?? null; if (userId !== lastUserId) { setLastUserId(userId); @@ -122,7 +126,7 @@ export function PlacesMap({ places }: PlacesMapProps) { React.useEffect(() => { const supabase = createClient(); - if (!supabase) return; + if (!supabase) return; // self-host / preview mode: no auth, no private layer supabase.auth.getUser().then(({ data }) => setUser(data.user)); const { data: { subscription }, @@ -148,6 +152,8 @@ export function PlacesMap({ places }: PlacesMapProps) { fetchUserHome().then(setHome).catch(() => setHome(null)); }, [user]); + // Debounce the search query so filtering doesn't run on every keystroke. + // Clearing the box applies immediately (0 ms); typing waits 250 ms. React.useEffect(() => { const timer = setTimeout( () => setDebouncedQuery(filters.query), @@ -156,6 +162,9 @@ export function PlacesMap({ places }: PlacesMapProps) { return () => clearTimeout(timer); }, [filters.query]); + // Mirror filter, focus, and viewport state back into the URL so it stays + // shareable. replaceState keeps panning out of browser history, so the back + // button still leaves the map in one press (see #118). React.useEffect(() => { if (!hydrated.current) return; const search = mapStateToSearch({ @@ -192,6 +201,8 @@ export function PlacesMap({ places }: PlacesMapProps) { [places, filters.city], ); + // Private layer: saved places always render on top of the public set, + // independent of the public search/type/city filters above. const privatePlaces = React.useMemo( () => savedPlaces.map(userPlaceToPlace), [savedPlaces], @@ -208,6 +219,7 @@ export function PlacesMap({ places }: PlacesMapProps) { return placesByDistance(visible, userLocation); }, [visible, userLocation]); + // Build the results list: nearest-first when located, otherwise all visible. const { rows, resultsHeader, resultsToggle } = React.useMemo(() => { if (userLocation && byDistance.length > 0) { const shown = sortByDistance ? byDistance : byDistance.slice(0, 5); @@ -297,7 +309,7 @@ export function PlacesMap({ places }: PlacesMapProps) { function selectPlace(place: Place) { setFocusId(place.id); - setSheetOpen(false); + setSheetOpen(false); // collapse the mobile sheet so the pin is visible } const panelProps = { @@ -334,10 +346,12 @@ export function PlacesMap({ places }: PlacesMapProps) { return (
+ {/* Desktop sidebar */} + {/* Map + mobile overlays */}
+ {/* Mobile top bar: persistent search + filters trigger */}
@@ -376,31 +391,42 @@ export function PlacesMap({ places }: PlacesMapProps) {
+ {/* Mobile near-me FAB, lifted above the peek bar */} + {/* Mobile peek bar: always-visible results summary; tap to open sheet */} + {/* Mobile bottom sheet */} + > + +
{user && (