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:
@@ -1,6 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useState } from 'react';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
@@ -13,9 +14,9 @@ import {
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
import { usePlans, useBillingHistory, useQuota, subscriptionKeys } from '@/lib/hooks/use-subscription';
|
||||
import {
|
||||
subscriptionApi,
|
||||
type BillingHistoryDto,
|
||||
type PlanDto,
|
||||
type QuotaCheckResult,
|
||||
} from '@/lib/subscription-api';
|
||||
@@ -43,33 +44,24 @@ const STATUS_MAP: Record<string, { label: string; variant: 'default' | 'secondar
|
||||
};
|
||||
|
||||
export default function SubscriptionPage() {
|
||||
const [plans, setPlans] = useState<PlanDto[]>([]);
|
||||
const [billing, setBilling] = useState<BillingHistoryDto | null>(null);
|
||||
const [quotas, setQuotas] = useState<QuotaCheckResult[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const queryClient = useQueryClient();
|
||||
const { data: plansData, isLoading: plansLoading } = usePlans();
|
||||
const { data: billing, isLoading: billingLoading } = useBillingHistory();
|
||||
const { data: listingsQuota } = useQuota('listings');
|
||||
const { data: savedSearchesQuota } = useQuota('saved_searches');
|
||||
const [upgradeTarget, setUpgradeTarget] = useState<PlanDto | null>(null);
|
||||
const [billingCycle, setBillingCycle] = useState<'monthly' | 'yearly'>('monthly');
|
||||
const [processing, setProcessing] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [activeTab, setActiveTab] = useState('plan');
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(true);
|
||||
Promise.all([
|
||||
subscriptionApi.getPlans().catch(() => []),
|
||||
subscriptionApi.getBillingHistory().catch(() => null),
|
||||
Promise.all([
|
||||
subscriptionApi.checkQuota('listings').catch(() => null),
|
||||
subscriptionApi.checkQuota('saved_searches').catch(() => null),
|
||||
]),
|
||||
])
|
||||
.then(([plansData, billingData, quotaResults]) => {
|
||||
setPlans(plansData.sort((a, b) => PLAN_TIER_ORDER.indexOf(a.tier) - PLAN_TIER_ORDER.indexOf(b.tier)));
|
||||
setBilling(billingData);
|
||||
setQuotas(quotaResults.filter((q): q is QuotaCheckResult => q !== null));
|
||||
})
|
||||
.finally(() => setLoading(false));
|
||||
}, []);
|
||||
const loading = plansLoading || billingLoading;
|
||||
const plans = (plansData ?? []).slice().sort(
|
||||
(a, b) => PLAN_TIER_ORDER.indexOf(a.tier) - PLAN_TIER_ORDER.indexOf(b.tier),
|
||||
);
|
||||
const quotas = [listingsQuota, savedSearchesQuota].filter(
|
||||
(q): q is QuotaCheckResult => q != null,
|
||||
);
|
||||
|
||||
const currentTier = billing?.subscription?.planTier ?? 'FREE';
|
||||
const currentTierIndex = PLAN_TIER_ORDER.indexOf(currentTier);
|
||||
@@ -87,9 +79,7 @@ export default function SubscriptionPage() {
|
||||
} else {
|
||||
await subscriptionApi.createSubscription(upgradeTarget.tier, billingCycle);
|
||||
}
|
||||
// Reload billing data
|
||||
const newBilling = await subscriptionApi.getBillingHistory().catch(() => null);
|
||||
setBilling(newBilling);
|
||||
await queryClient.invalidateQueries({ queryKey: subscriptionKeys.billing() });
|
||||
setUpgradeTarget(null);
|
||||
} catch (e) {
|
||||
setError(e instanceof Error ? e.message : 'Nâng cấp thất bại');
|
||||
|
||||
Reference in New Issue
Block a user