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

@@ -1,6 +1,6 @@
'use client';
import { useEffect, useState } from 'react';
import { useState } from 'react';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
@@ -13,7 +13,7 @@ import {
TableHeader,
TableRow,
} from '@/components/ui/table';
import { paymentApi, type TransactionListDto } from '@/lib/payment-api';
import { useTransactions } from '@/lib/hooks/use-payments';
function formatVND(amount: string | number): string {
const num = typeof amount === 'string' ? Number(amount) : amount;
@@ -45,24 +45,15 @@ const PROVIDER_LABELS: Record<string, string> = {
};
export default function PaymentsPage() {
const [transactions, setTransactions] = useState<TransactionListDto | null>(null);
const [loading, setLoading] = useState(true);
const [statusFilter, setStatusFilter] = useState('');
const [page, setPage] = useState(0);
const limit = 20;
useEffect(() => {
setLoading(true);
paymentApi
.getTransactions({
status: statusFilter || undefined,
limit,
offset: page * limit,
})
.then((data) => setTransactions(data))
.catch(() => setTransactions(null))
.finally(() => setLoading(false));
}, [statusFilter, page]);
const { data: transactions, isLoading: loading } = useTransactions({
status: statusFilter || undefined,
limit,
offset: page * limit,
});
const totalPages = transactions ? Math.ceil(transactions.total / limit) : 0;