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;
|
||||
|
||||
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 {
|
||||
|
||||
@@ -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 (
|
||||
<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 }) {
|
||||
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;
|
||||
amenities: string[] | null;
|
||||
projectName: string | null;
|
||||
latitude: number | null;
|
||||
longitude: number | null;
|
||||
media: PropertyMedia[];
|
||||
};
|
||||
seller: {
|
||||
|
||||
@@ -11,6 +11,7 @@ function makeQueryClient() {
|
||||
retry: 3,
|
||||
retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000),
|
||||
refetchOnWindowFocus: false,
|
||||
throwOnError: true,
|
||||
},
|
||||
mutations: {
|
||||
retry: 1,
|
||||
|
||||
Reference in New Issue
Block a user