Skip to content

Commit 99cc01d

Browse files
committed
fix(map): restore IP-based initial centre; assorted review tweaks
- MapView: restore the hasInitialPosition gate. MapLibre's initialViewState is consumed once at mount, so mounting before the IP lookup resolves locks the map to DEFAULT_COORDINATES (Brisbane) even after IP data arrives. The gate waits for either a selected listing or the IP fallback to resolve before mounting. - useIpInitialLocation: on timeout or error, fall back to DEFAULT_COORDINATES so the gate always eventually lifts (addresses the earlier concern about the map never rendering on IP failure). - mapUtils: drop the unused DEFAULT_COORDINATES.zoom; ZOOM_LEVEL_DEFAULT is already the canonical default zoom. - useMapListingUrl: memoize the Supabase client so fetchBySlug / selectListingById don't churn on every render. - MapPin: tighten isListingPinType with hasOwnProperty.call so inherited prototype keys like "toString" can't incorrectly narrow to ListingType. Made-with: Cursor
1 parent 64e7522 commit 99cc01d

5 files changed

Lines changed: 90 additions & 60 deletions

File tree

src/components/MapPin/MapPin.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,11 @@ const iconMap: Record<ListingType, React.ComponentType<{ size?: string }>> = {
154154
};
155155

156156
function isListingPinType(value: string | undefined): value is ListingType {
157-
return value !== undefined && value in iconMap;
157+
// Guard against inherited Object.prototype keys like `"toString"` that a
158+
// plain `value in iconMap` check would accept.
159+
return (
160+
value !== undefined && Object.prototype.hasOwnProperty.call(iconMap, value)
161+
);
158162
}
159163

160164
function MapPin({ selected = false, type }: MapPinProps) {

src/features/map/components/MapView.tsx

Lines changed: 61 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,14 @@ export default function MapView({
178178
selectedListing,
179179
});
180180

181+
// MapLibre's `initialViewState` is only consumed once at mount, so we wait
182+
// for either a selected listing or the IP-based (or fallback) initial
183+
// centre to resolve before mounting the Map. `useIpInitialLocation`
184+
// always resolves (to DEFAULT_COORDINATES on failure), so this cannot
185+
// stall indefinitely.
186+
const hasInitialPosition =
187+
hasValidCoordinates(selectedListing) || Boolean(initialCoordinates);
188+
181189
useEffect(() => {
182190
const protocol = new Protocol();
183191
maplibregl.addProtocol("pmtiles", protocol.tile);
@@ -242,59 +250,61 @@ export default function MapView({
242250

243251
return (
244252
<MapContainer>
245-
<>
246-
<Map
247-
ref={mapRef}
248-
attributionControl={false}
249-
mapStyle={MAP_STYLE}
250-
renderWorldCopies={true}
251-
initialViewState={resolveInitialViewState(
252-
selectedListing,
253-
initialCoordinates
254-
)}
255-
onMoveEnd={handleMoveEnd}
256-
onLoad={handleLoad}
257-
onClick={handleMapClickInternal}
258-
>
259-
<GeolocateControl showUserLocation={true} />
260-
<NavigationControl showZoom={true} showCompass={false} />
261-
262-
<AttributionControl
263-
compact={true}
264-
style={
265-
!isDesktop
266-
? attributionControlMobileStyle
267-
: attributionControlDesktopStyle
268-
}
253+
{hasInitialPosition && (
254+
<>
255+
<Map
256+
ref={mapRef}
257+
attributionControl={false}
258+
mapStyle={MAP_STYLE}
259+
renderWorldCopies={true}
260+
initialViewState={resolveInitialViewState(
261+
selectedListing,
262+
initialCoordinates
263+
)}
264+
onMoveEnd={handleMoveEnd}
265+
onLoad={handleLoad}
266+
onClick={handleMapClickInternal}
267+
>
268+
<GeolocateControl showUserLocation={true} />
269+
<NavigationControl showZoom={true} showCompass={false} />
270+
271+
<AttributionControl
272+
compact={true}
273+
style={
274+
!isDesktop
275+
? attributionControlMobileStyle
276+
: attributionControlDesktopStyle
277+
}
278+
/>
279+
280+
<MapPinLayer
281+
listings={listings}
282+
selectedListingId={selectedListingId}
283+
DrawerTrigger={DrawerTrigger}
284+
onMarkerClick={onMarkerClick}
285+
/>
286+
</Map>
287+
288+
<MapSearch
289+
onPick={handleSearchPick}
290+
countryCode={countryCode}
291+
style={searchStyle}
269292
/>
270293

271-
<MapPinLayer
272-
listings={listings}
273-
selectedListingId={selectedListingId}
274-
DrawerTrigger={DrawerTrigger}
275-
onMarkerClick={onMarkerClick}
276-
/>
277-
</Map>
278-
279-
<MapSearch
280-
onPick={handleSearchPick}
281-
countryCode={countryCode}
282-
style={searchStyle}
283-
/>
284-
285-
{isFetching && <LoadingChip>{t("loadingPins")}</LoadingChip>}
286-
287-
{showReturnButton && (
288-
<ReturnToListingButton
289-
onClick={flyToSelected}
290-
variant="secondary"
291-
size="small"
292-
width="contained"
293-
>
294-
{t("returnToListing")}
295-
</ReturnToListingButton>
296-
)}
297-
</>
294+
{isFetching && <LoadingChip>{t("loadingPins")}</LoadingChip>}
295+
296+
{showReturnButton && (
297+
<ReturnToListingButton
298+
onClick={flyToSelected}
299+
variant="secondary"
300+
size="small"
301+
width="contained"
302+
>
303+
{t("returnToListing")}
304+
</ReturnToListingButton>
305+
)}
306+
</>
307+
)}
298308
</MapContainer>
299309
);
300310
}

src/features/map/hooks/useIpInitialLocation.ts

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { config, geolocation } from "@maptiler/client";
55

66
import type { ListingCoordinates } from "@/types/listing";
77

8-
import { ZOOM_LEVEL_DEFAULT } from "../lib/mapUtils";
8+
import { DEFAULT_COORDINATES, ZOOM_LEVEL_DEFAULT } from "../lib/mapUtils";
99

1010
type UseIpInitialLocationArgs = {
1111
// Skip when the page already has a listing slug (deep-linked selections
@@ -30,8 +30,9 @@ function ensureMapTilerConfig() {
3030
hasConfiguredMapTiler = true;
3131
}
3232

33-
// One-time IP-based initial centre. MapView falls back to
34-
// DEFAULT_COORDINATES if this fails or is skipped.
33+
// One-time IP-based initial centre. On timeout or error we still resolve to
34+
// `DEFAULT_COORDINATES` so MapView, which gates on `initialCoordinates`
35+
// being set, always eventually mounts.
3536
export function useIpInitialLocation({
3637
skip = false,
3738
}: UseIpInitialLocationArgs = {}): UseIpInitialLocationResult {
@@ -48,6 +49,14 @@ export function useIpInitialLocation({
4849
let cancelled = false;
4950
let timeoutId: ReturnType<typeof setTimeout> | null = null;
5051

52+
const applyFallback = () => {
53+
if (cancelled) return;
54+
setInitialCoordinates({
55+
...DEFAULT_COORDINATES,
56+
zoom: ZOOM_LEVEL_DEFAULT,
57+
});
58+
};
59+
5160
async function initializeLocation() {
5261
// Race the network call against a 3s timeout. We track the timeout id
5362
// so we can clear it once the race settles — otherwise the losing
@@ -85,13 +94,16 @@ export function useIpInitialLocation({
8594
longitude: lng,
8695
zoom: ZOOM_LEVEL_DEFAULT,
8796
});
97+
} else {
98+
applyFallback();
8899
}
89100
} catch (error) {
90101
if (cancelled) return;
91102
console.warn(
92103
"Could not determine location from MapTiler:",
93104
(error as Error).message
94105
);
106+
applyFallback();
95107
} finally {
96108
if (timeoutId !== null) {
97109
clearTimeout(timeoutId);

src/features/map/hooks/useMapListingUrl.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"use client";
22

3-
import { useCallback, useEffect, useRef, useState } from "react";
3+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
44
import { useRouter, useSearchParams } from "next/navigation";
55
import { useTranslations } from "next-intl";
66

@@ -49,7 +49,10 @@ export function useMapListingUrl({
4949
const t = useTranslations();
5050
const router = useRouter();
5151
const searchParams = useSearchParams();
52-
const supabase = createClient();
52+
// `createClient()` builds a new Supabase browser client each call, so
53+
// memoize to keep the reference stable — otherwise `fetchBySlug` /
54+
// `selectListingById` churn on every render.
55+
const supabase = useMemo(() => createClient(), []);
5356

5457
const listingSlug = searchParams.get("listing");
5558

src/features/map/lib/mapUtils.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,12 @@ export type BoundingBox = {
1515
east: number;
1616
};
1717

18-
// Default coordinates for Brisbane, Australia
19-
export const DEFAULT_COORDINATES: ListingCoordinates & { zoom: number } = {
18+
// Default coordinates for Brisbane, Australia — the absolute fallback when
19+
// IP-based geolocation is unavailable. Pair with `ZOOM_LEVEL_DEFAULT` for
20+
// the zoom level.
21+
export const DEFAULT_COORDINATES: ListingCoordinates = {
2022
latitude: -27.4683,
2123
longitude: 153.0322,
22-
zoom: 9,
2324
};
2425

2526
export const ZOOM_LEVEL_DEFAULT = 11;

0 commit comments

Comments
 (0)