diff --git a/apps/web/components/map/listing-map.tsx b/apps/web/components/map/listing-map.tsx index 2d1dd97..b3b93db 100644 --- a/apps/web/components/map/listing-map.tsx +++ b/apps/web/components/map/listing-map.tsx @@ -38,6 +38,9 @@ const DEFAULT_CENTER: [number, number] = [106.6297, 10.8231]; // HCMC [lng, lat] const DEFAULT_ZOOM = 12; function getMarkerCoords(listing: ListingDetail, index: number): { lat: number; lng: number } { + if (listing.property.latitude != null && listing.property.longitude != null) { + return { lat: listing.property.latitude, lng: listing.property.longitude }; + } const base = CITY_COORDS[listing.property.city] || [10.8231, 106.6297]; const seed = listing.id.charCodeAt(0) + index; return { diff --git a/apps/web/components/providers/query-provider.tsx b/apps/web/components/providers/query-provider.tsx index 0eec0dc..aa03482 100644 --- a/apps/web/components/providers/query-provider.tsx +++ b/apps/web/components/providers/query-provider.tsx @@ -1,9 +1,78 @@ 'use client'; -import { QueryClientProvider } from '@tanstack/react-query'; +import { QueryClientProvider, QueryErrorResetBoundary } from '@tanstack/react-query'; +import { useTranslations } from 'next-intl'; +import { Component, type ErrorInfo, type ReactNode } from 'react'; import { getQueryClient } from '@/lib/query-client'; +interface FallbackProps { + error: Error; + reset: () => void; +} + +function QueryErrorFallback({ error, reset }: FallbackProps) { + const t = useTranslations(); + + return ( +
+

{t('error.description')}

+ {error.message && ( +

{error.message}

+ )} + +
+ ); +} + +interface ErrorBoundaryState { + error: Error | null; +} + +class QueryErrorBoundaryInner extends Component< + { children: ReactNode; onReset: () => void }, + ErrorBoundaryState +> { + state: ErrorBoundaryState = { error: null }; + + static getDerivedStateFromError(error: Error) { + return { error }; + } + + componentDidCatch(error: Error, info: ErrorInfo) { + if (process.env.NODE_ENV !== 'production') { + console.error('Query error boundary caught:', error, info); + } + } + + reset = () => { + this.props.onReset(); + this.setState({ error: null }); + }; + + render() { + if (this.state.error) { + return ; + } + return this.props.children; + } +} + export function QueryProvider({ children }: { children: React.ReactNode }) { const queryClient = getQueryClient(); - return {children}; + return ( + + + {({ reset }) => ( + + {children} + + )} + + + ); } diff --git a/apps/web/lib/listings-api.ts b/apps/web/lib/listings-api.ts index 6dff0bc..4f33fe3 100644 --- a/apps/web/lib/listings-api.ts +++ b/apps/web/lib/listings-api.ts @@ -64,6 +64,8 @@ export interface ListingDetail { legalStatus: string | null; amenities: string[] | null; projectName: string | null; + latitude: number | null; + longitude: number | null; media: PropertyMedia[]; }; seller: { diff --git a/apps/web/lib/query-client.ts b/apps/web/lib/query-client.ts index ec702dd..ced4918 100644 --- a/apps/web/lib/query-client.ts +++ b/apps/web/lib/query-client.ts @@ -11,6 +11,7 @@ function makeQueryClient() { retry: 3, retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000), refetchOnWindowFocus: false, + throwOnError: true, }, mutations: { retry: 1,