feat(web): add React Query, dark mode toggle, and error retry UX

- Install @tanstack/react-query with exponential backoff retry config
- Create QueryClientProvider and custom hooks for listings, analytics,
  payments, and subscription API calls
- Migrate 5 dashboard pages from useState/useEffect to React Query hooks
- Add dark mode CSS variables and ThemeProvider with localStorage persistence
- Add theme toggle button in dashboard header (sun/moon icon)
- Enhance error boundaries with auto-retry, retry count, and loading state

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Ho Ngoc Hai
2026-04-08 23:02:44 +07:00
parent ccb82fddf8
commit 9d120dd21f
20 changed files with 481 additions and 155 deletions

View File

@@ -8,11 +8,8 @@ import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Select } from '@/components/ui/select';
import {
listingsApi,
type ListingDetail,
type PaginatedResult,
} from '@/lib/listings-api';
import { useListingsSearch } from '@/lib/hooks/use-listings';
import { type ListingDetail } from '@/lib/listings-api';
import { PROPERTY_TYPES, TRANSACTION_TYPES, LISTING_STATUSES } from '@/lib/validations/listings';
function formatPrice(priceVND: string): string {
const num = Number(priceVND);
@@ -33,8 +30,6 @@ function formatDate(dateStr: string | null): string {
type ViewMode = 'grid' | 'table';
export default function ListingsPage() {
const [result, setResult] = React.useState<PaginatedResult<ListingDetail> | null>(null);
const [loading, setLoading] = React.useState(true);
const [viewMode, setViewMode] = React.useState<ViewMode>('grid');
const [filters, setFilters] = React.useState({
transactionType: '',
@@ -43,23 +38,15 @@ export default function ListingsPage() {
page: 1,
});
const fetchListings = React.useCallback(() => {
setLoading(true);
const searchParams = React.useMemo(() => {
const params: Record<string, string | number> = { page: filters.page, limit: 12 };
if (filters.transactionType) params['transactionType'] = filters.transactionType;
if (filters.propertyType) params['propertyType'] = filters.propertyType;
if (filters.status) params['status'] = filters.status;
listingsApi
.search(params)
.then(setResult)
.catch(() => setResult(null))
.finally(() => setLoading(false));
return params;
}, [filters]);
React.useEffect(() => {
fetchListings();
}, [fetchListings]);
const { data: result, isLoading: loading } = useListingsSearch(searchParams);
// Stats from current page data
const stats = React.useMemo(() => {