feat(web): add QueryErrorBoundary and use real map coordinates
Add global QueryErrorResetBoundary wrapping the app so TanStack Query errors are caught with a retry UI instead of crashing. Enable throwOnError in QueryClient defaults. Update ListingMap to use real latitude/longitude from API when available, falling back to city-based jitter for listings without coordinates. Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
@@ -38,6 +38,9 @@ const DEFAULT_CENTER: [number, number] = [106.6297, 10.8231]; // HCMC [lng, lat]
|
|||||||
const DEFAULT_ZOOM = 12;
|
const DEFAULT_ZOOM = 12;
|
||||||
|
|
||||||
function getMarkerCoords(listing: ListingDetail, index: number): { lat: number; lng: number } {
|
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 base = CITY_COORDS[listing.property.city] || [10.8231, 106.6297];
|
||||||
const seed = listing.id.charCodeAt(0) + index;
|
const seed = listing.id.charCodeAt(0) + index;
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -1,9 +1,78 @@
|
|||||||
'use client';
|
'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';
|
import { getQueryClient } from '@/lib/query-client';
|
||||||
|
|
||||||
|
interface FallbackProps {
|
||||||
|
error: Error;
|
||||||
|
reset: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function QueryErrorFallback({ error, reset }: FallbackProps) {
|
||||||
|
const t = useTranslations();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex min-h-[200px] flex-col items-center justify-center px-4" role="alert">
|
||||||
|
<p className="text-sm text-destructive">{t('error.description')}</p>
|
||||||
|
{error.message && (
|
||||||
|
<p className="mt-1 text-xs text-muted-foreground">{error.message}</p>
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
onClick={reset}
|
||||||
|
className="mt-3 inline-flex h-8 items-center rounded-md bg-primary px-4 text-xs font-medium text-primary-foreground hover:bg-primary/90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||||
|
>
|
||||||
|
{t('common.retry')}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 <QueryErrorFallback error={this.state.error} reset={this.reset} />;
|
||||||
|
}
|
||||||
|
return this.props.children;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function QueryProvider({ children }: { children: React.ReactNode }) {
|
export function QueryProvider({ children }: { children: React.ReactNode }) {
|
||||||
const queryClient = getQueryClient();
|
const queryClient = getQueryClient();
|
||||||
return <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>;
|
return (
|
||||||
|
<QueryClientProvider client={queryClient}>
|
||||||
|
<QueryErrorResetBoundary>
|
||||||
|
{({ reset }) => (
|
||||||
|
<QueryErrorBoundaryInner onReset={reset}>
|
||||||
|
{children}
|
||||||
|
</QueryErrorBoundaryInner>
|
||||||
|
)}
|
||||||
|
</QueryErrorResetBoundary>
|
||||||
|
</QueryClientProvider>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -64,6 +64,8 @@ export interface ListingDetail {
|
|||||||
legalStatus: string | null;
|
legalStatus: string | null;
|
||||||
amenities: string[] | null;
|
amenities: string[] | null;
|
||||||
projectName: string | null;
|
projectName: string | null;
|
||||||
|
latitude: number | null;
|
||||||
|
longitude: number | null;
|
||||||
media: PropertyMedia[];
|
media: PropertyMedia[];
|
||||||
};
|
};
|
||||||
seller: {
|
seller: {
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ function makeQueryClient() {
|
|||||||
retry: 3,
|
retry: 3,
|
||||||
retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000),
|
retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000),
|
||||||
refetchOnWindowFocus: false,
|
refetchOnWindowFocus: false,
|
||||||
|
throwOnError: true,
|
||||||
},
|
},
|
||||||
mutations: {
|
mutations: {
|
||||||
retry: 1,
|
retry: 1,
|
||||||
|
|||||||
Reference in New Issue
Block a user