Skip to content

Commit a49d29c

Browse files
authored
fix map interaction regressions (#51)
* fix map interaction regressions * address copilot review feedback * address latest copilot feedback
1 parent 76ed2c6 commit a49d29c

11 files changed

Lines changed: 360 additions & 199 deletions

File tree

messages/de.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -385,6 +385,7 @@
385385
"searchError": "Etwas ist schiefgelaufen. Erneut versuchen?",
386386
"searchNoResults": "Keine Ergebnisse. Tippe weiter oder verfeinere deine Suche",
387387
"returnToListing": "Zurück zum Eintrag",
388+
"loadingListing": "Eintrag wird geladen…",
388389
"loadingPins": "Wird geladen…",
389390
"didYouKnow": "Wusstest du schon?",
390391
"steps": {

messages/en.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -385,6 +385,7 @@
385385
"searchError": "Something went wrong. Try again?",
386386
"searchNoResults": "No results. Keep typing or refine your search",
387387
"returnToListing": "Return to listing",
388+
"loadingListing": "Loading listing…",
388389
"loadingPins": "Loading…",
389390
"didYouKnow": "Did you know?",
390391
"steps": {

messages/es.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -385,6 +385,7 @@
385385
"searchError": "Algo salió mal. ¿Intentarlo de nuevo?",
386386
"searchNoResults": "Sin resultados. Sigue escribiendo o ajusta tu búsqueda",
387387
"returnToListing": "Volver al anuncio",
388+
"loadingListing": "Cargando anuncio…",
388389
"loadingPins": "Cargando…",
389390
"didYouKnow": "¿Sabías que?",
390391
"steps": {

src/contexts/UnreadMessagesContext.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
"use client";
2-
import { createContext, useContext, useState, useEffect } from "react";
2+
import { createContext, useContext, useState, useEffect, useMemo } from "react";
33
import { createClient } from "@/utils/supabase/client";
44

55
const UnreadMessagesContext = createContext();
@@ -9,7 +9,7 @@ export function UnreadMessagesProvider({ children }) {
99
const [unreadCount, setUnreadCount] = useState(0);
1010
const [threadReadStatus, setThreadReadStatus] = useState({});
1111
const [hasViewedChats, setHasViewedChats] = useState(false);
12-
const supabase = createClient();
12+
const supabase = useMemo(() => createClient(), []);
1313

1414
// Check for unread messages on mount
1515
useEffect(() => {
@@ -84,7 +84,7 @@ export function UnreadMessagesProvider({ children }) {
8484
return () => {
8585
subscription.unsubscribe();
8686
};
87-
}, []);
87+
}, [supabase]);
8888

8989
// Real-time subscription for new messages
9090
useEffect(() => {

src/features/map/components/MapListingDrawerPanel.tsx

Lines changed: 72 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import * as VisuallyHidden from "@radix-ui/react-visually-hidden";
44
import { Drawer } from "vaul";
5-
import { styled } from "@pigment-css/react";
5+
import { keyframes, styled } from "@pigment-css/react";
66
import { useTranslations } from "next-intl";
77
import type { User } from "@supabase/supabase-js";
88

@@ -20,6 +20,7 @@ import { SIDEBAR_WIDTH } from "../lib/mapUtils";
2020
type MapListingDrawerPanelProps = {
2121
user: User | null;
2222
selectedListing: SelectedListing | null;
23+
isSelectedListingLoading: boolean;
2324
isDesktop: boolean;
2425
hasTouch: boolean;
2526
isDrawerHeaderShown: boolean;
@@ -193,9 +194,42 @@ const NoListingFound = styled("div")(({ theme }) => ({
193194
},
194195
}));
195196

197+
const LoadingState = styled("div")({
198+
display: "flex",
199+
flexDirection: "column",
200+
gap: "1.5rem",
201+
padding: "0 1rem",
202+
});
203+
204+
const mapDrawerPulse = keyframes({
205+
"0%": { opacity: 0.55 },
206+
"50%": { opacity: 1 },
207+
"100%": { opacity: 0.55 },
208+
});
209+
210+
const SkeletonBlock = styled("div")(({ theme }) => ({
211+
borderRadius: theme.corners.base,
212+
background: theme.colors.background.slight,
213+
opacity: 0.85,
214+
animation: `${mapDrawerPulse} 1.2s ease-in-out infinite`,
215+
}));
216+
217+
const SkeletonHeader = styled("div")({
218+
display: "flex",
219+
flexDirection: "column",
220+
gap: "0.5rem",
221+
});
222+
223+
const SkeletonText = styled("div")({
224+
display: "flex",
225+
flexDirection: "column",
226+
gap: "0.75rem",
227+
});
228+
196229
export default function MapListingDrawerPanel({
197230
user,
198231
selectedListing,
232+
isSelectedListingLoading,
199233
isDesktop,
200234
hasTouch,
201235
isDrawerHeaderShown,
@@ -208,9 +242,10 @@ export default function MapListingDrawerPanel({
208242
const t = useTranslations();
209243

210244
const showErrorPanel = isListingError(selectedListing);
211-
const listingForDisplay = isListingError(selectedListing)
212-
? null
213-
: selectedListing;
245+
const listingForDisplay =
246+
isSelectedListingLoading || isListingError(selectedListing)
247+
? null
248+
: selectedListing;
214249

215250
return (
216251
<Drawer.Portal>
@@ -239,16 +274,25 @@ export default function MapListingDrawerPanel({
239274
}}
240275
>
241276
<StyledHeaderText>
242-
<h3 style={{ fontSize: "0.85rem" }}>
243-
{listingForDisplay
244-
? getListingDisplayName(listingForDisplay, user)
245-
: ""}
246-
</h3>
247-
<p>
248-
{listingForDisplay
249-
? getListingDisplayType(listingForDisplay)
250-
: ""}
251-
</p>
277+
{isSelectedListingLoading ? (
278+
<SkeletonHeader>
279+
<SkeletonBlock style={{ height: "0.85rem", width: "65%" }} />
280+
<SkeletonBlock style={{ height: "0.8rem", width: "40%" }} />
281+
</SkeletonHeader>
282+
) : (
283+
<>
284+
<h3 style={{ fontSize: "0.85rem" }}>
285+
{listingForDisplay
286+
? getListingDisplayName(listingForDisplay, user)
287+
: ""}
288+
</h3>
289+
<p>
290+
{listingForDisplay
291+
? getListingDisplayType(listingForDisplay)
292+
: ""}
293+
</p>
294+
</>
295+
)}
252296
</StyledHeaderText>
253297
</StyledDrawerHeaderInner>
254298

@@ -276,7 +320,20 @@ export default function MapListingDrawerPanel({
276320
</StyledDrawerHeader>
277321

278322
<StyledDrawerInner>
279-
{showErrorPanel ? (
323+
{isSelectedListingLoading ? (
324+
<LoadingState aria-busy={true}>
325+
<p>{t("Map.loadingListing")}</p>
326+
<SkeletonBlock style={{ height: "3rem", width: "100%" }} />
327+
<SkeletonText>
328+
<SkeletonBlock style={{ height: "1rem", width: "45%" }} />
329+
<SkeletonBlock style={{ height: "4.5rem", width: "100%" }} />
330+
<SkeletonBlock style={{ height: "1rem", width: "35%" }} />
331+
<SkeletonBlock style={{ height: "7rem", width: "100%" }} />
332+
<SkeletonBlock style={{ height: "1rem", width: "30%" }} />
333+
<SkeletonBlock style={{ height: "5rem", width: "100%" }} />
334+
</SkeletonText>
335+
</LoadingState>
336+
) : showErrorPanel ? (
280337
<NoListingFound>
281338
<header>
282339
<h2>{t("Map.emptyTitle")}</h2>

src/features/map/components/MapPageClient.tsx

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,8 @@ export default function MapPageClient({
5757
selectedListing,
5858
selectedListingId,
5959
isListingSelected,
60-
selectListingById,
60+
isSelectedListingLoading,
61+
selectListing,
6162
closeListing,
6263
} = useMapListingUrl({ user, initialListingSlug, initialListing });
6364

@@ -72,38 +73,45 @@ export default function MapPageClient({
7273
isFullSnap,
7374
isPartialSnap,
7475
isDrawerHeaderShown,
76+
resetDrawer,
7577
handleSnapChange,
7678
} = useMapDrawerState({ isDesktop, listingSlug, isListingSelected });
7779

7880
const handleMarkerClick = useCallback(
7981
(listing: ListingMarker) => {
8082
if (listing.id === selectedListingId && isListingSelected) return;
8183

82-
// Optimistic pin grow + single fetch (selectListingById sets the
83-
// optimistic id synchronously, fetches by id, then pushes the URL).
84-
void selectListingById(listing.id);
84+
resetDrawer();
85+
selectListing(listing);
8586
},
86-
[isListingSelected, selectListingById, selectedListingId]
87+
[isListingSelected, resetDrawer, selectListing, selectedListingId]
8788
);
8889

8990
const handleMapClick = useCallback(() => {
9091
if (isListingSelected) {
92+
resetDrawer();
9193
closeListing();
9294
}
93-
}, [closeListing, isListingSelected]);
95+
}, [closeListing, isListingSelected, resetDrawer]);
9496

9597
const handleDrawerOpenChange = useCallback(
9698
(open: boolean) => {
9799
// Drawer-driven close (e.g. escape key on desktop) should also update
98100
// the URL. We only act on the close transition — `open === true` is
99101
// already reflected by `isListingSelected` from the URL.
100102
if (!open && isListingSelected) {
103+
resetDrawer();
101104
closeListing();
102105
}
103106
},
104-
[closeListing, isListingSelected]
107+
[closeListing, isListingSelected, resetDrawer]
105108
);
106109

110+
const handlePanelClose = useCallback(() => {
111+
resetDrawer();
112+
closeListing();
113+
}, [closeListing, resetDrawer]);
114+
107115
return (
108116
<StyledMapPage>
109117
<StyledMapWrapper>
@@ -120,6 +128,7 @@ export default function MapPageClient({
120128
selectedListing={selectedListing}
121129
selectedListingId={selectedListingId}
122130
listingSlug={listingSlug}
131+
isListingSelected={isListingSelected}
123132
initialCoordinates={initialCoordinates}
124133
onMapClick={handleMapClick}
125134
onMarkerClick={handleMarkerClick}
@@ -131,13 +140,14 @@ export default function MapPageClient({
131140
<MapListingDrawerPanel
132141
user={user}
133142
selectedListing={selectedListing}
143+
isSelectedListingLoading={isSelectedListingLoading}
134144
isDesktop={isDesktop}
135145
hasTouch={hasTouch}
136146
isDrawerHeaderShown={isDrawerHeaderShown}
137147
isFullSnap={isFullSnap}
138148
isPartialSnap={isPartialSnap}
139149
onToggleSnap={handleSnapChange}
140-
onClose={closeListing}
150+
onClose={handlePanelClose}
141151
drawerContentRef={drawerContentRef}
142152
/>
143153
</Drawer.Root>

src/features/map/components/MapView.tsx

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ type MapViewProps = {
4545
selectedListing: SelectedListing | null;
4646
selectedListingId: number | null;
4747
listingSlug: string | null;
48+
isListingSelected: boolean;
4849
initialCoordinates: (ListingCoordinates & { zoom: number }) | null;
4950
onMapClick: () => void;
5051
onMarkerClick: (listing: ListingMarker) => void;
@@ -76,8 +77,9 @@ const ReturnToListingButton = styled(Button)({
7677
const LoadingChip = styled("div")(({ theme }) => ({
7778
position: "absolute",
7879
top: "0.75rem",
79-
right: "0.75rem",
80-
zIndex: 1,
80+
left: "50%",
81+
transform: "translateX(-50%)",
82+
zIndex: 2,
8183
padding: "0.25rem 0.75rem",
8284
borderRadius: "999px",
8385
background: theme.colors.background.top,
@@ -155,6 +157,7 @@ export default function MapView({
155157
selectedListing,
156158
selectedListingId,
157159
listingSlug,
160+
isListingSelected,
158161
initialCoordinates,
159162
onMapClick,
160163
onMarkerClick,
@@ -222,11 +225,11 @@ export default function MapView({
222225

223226
const handleMapClickInternal = useCallback(
224227
(_event: MapLayerMouseEvent) => {
225-
if (selectedListingId !== null || listingSlug) {
228+
if (selectedListingId !== null || isListingSelected || listingSlug) {
226229
onMapClick();
227230
}
228231
},
229-
[listingSlug, onMapClick, selectedListingId]
232+
[isListingSelected, listingSlug, onMapClick, selectedListingId]
230233
);
231234

232235
const handleSearchPick = useCallback(
@@ -245,7 +248,7 @@ export default function MapView({
245248
);
246249

247250
const showReturnButton = Boolean(
248-
selectedListing && listingSlug && !isSelectedInView
251+
selectedListing && isListingSelected && !isSelectedInView
249252
);
250253

251254
return (

src/features/map/hooks/useMapDrawerState.ts

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ type UseMapDrawerStateResult = {
1919
isFullSnap: boolean;
2020
isPartialSnap: boolean;
2121
isDrawerHeaderShown: boolean;
22+
resetDrawer: () => void;
2223
handleSnapChange: () => void;
2324
};
2425

@@ -48,6 +49,15 @@ export function useMapDrawerState({
4849
const isFullSnap = snap === SNAP_POINTS.full;
4950
const isPartialSnap = snap === SNAP_POINTS.partial;
5051

52+
const resetDrawer = useCallback(() => {
53+
document.documentElement.classList.remove("drawer-fully-open");
54+
setSnap(SNAP_POINTS.partial);
55+
setIsDrawerHeaderShown(false);
56+
if (drawerContentRef.current) {
57+
drawerContentRef.current.scrollTop = 0;
58+
}
59+
}, []);
60+
5161
// Snap to full on desktop. On mobile we stay at the partial snap until the
5262
// user explicitly expands or a new listing is opened.
5363
useEffect(() => {
@@ -79,17 +89,12 @@ export function useMapDrawerState({
7989
// changes (including browser back/forward).
8090
useEffect(() => {
8191
if (!listingSlug) {
82-
document.documentElement.classList.remove("drawer-fully-open");
83-
setSnap(SNAP_POINTS.partial);
92+
resetDrawer();
8493
return;
8594
}
8695

87-
setSnap(SNAP_POINTS.partial);
88-
setIsDrawerHeaderShown(false);
89-
if (drawerContentRef.current) {
90-
drawerContentRef.current.scrollTop = 0;
91-
}
92-
}, [listingSlug]);
96+
resetDrawer();
97+
}, [listingSlug, resetDrawer]);
9398

9499
// Mobile: we can attach the scroll handler directly when the drawer is
95100
// fully snapped because the drawer content is the scroll container.
@@ -174,6 +179,7 @@ export function useMapDrawerState({
174179
isFullSnap,
175180
isPartialSnap,
176181
isDrawerHeaderShown,
182+
resetDrawer,
177183
handleSnapChange,
178184
};
179185
}

0 commit comments

Comments
 (0)