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,