refactor: improve loading state management in billing page

This commit is contained in:
javayhu 2025-07-12 22:36:17 +08:00
parent 9fcfb3bdf7
commit 141b562307
5 changed files with 41 additions and 22 deletions

View File

@ -634,7 +634,7 @@
"types": { "types": {
"MONTHLY_REFRESH": "Monthly Refresh", "MONTHLY_REFRESH": "Monthly Refresh",
"REGISTER_GIFT": "Register Gift", "REGISTER_GIFT": "Register Gift",
"PURCHASE_PACKAGE": "Purchased Credits", "PURCHASE": "Purchased Credits",
"USAGE": "Consumed Credits", "USAGE": "Consumed Credits",
"EXPIRE": "Expired Credits", "EXPIRE": "Expired Credits",
"SUBSCRIPTION_RENEWAL": "Subscription Renewal", "SUBSCRIPTION_RENEWAL": "Subscription Renewal",

View File

@ -635,7 +635,7 @@
"types": { "types": {
"MONTHLY_REFRESH": "每月赠送", "MONTHLY_REFRESH": "每月赠送",
"REGISTER_GIFT": "注册赠送", "REGISTER_GIFT": "注册赠送",
"PURCHASE_PACKAGE": "购买积分", "PURCHASE": "购买积分",
"USAGE": "使用积分", "USAGE": "使用积分",
"EXPIRE": "过期积分", "EXPIRE": "过期积分",
"SUBSCRIPTION_RENEWAL": "订阅月度积分", "SUBSCRIPTION_RENEWAL": "订阅月度积分",

View File

@ -8,6 +8,9 @@ import { addDays } from 'date-fns';
import { and, eq, gte, isNotNull, lte, sql, sum } from 'drizzle-orm'; import { and, eq, gte, isNotNull, lte, sql, sum } from 'drizzle-orm';
import { createSafeActionClient } from 'next-safe-action'; import { createSafeActionClient } from 'next-safe-action';
const CREDITS_EXPIRATION_DAYS = 30;
const CREDITS_MONTHLY_DAYS = 30;
// Create a safe action client // Create a safe action client
const actionClient = createSafeActionClient(); const actionClient = createSafeActionClient();
@ -28,7 +31,7 @@ export const getCreditStatsAction = actionClient.action(async () => {
const userId = session.user.id; const userId = session.user.id;
// Get credits expiring in the next 30 days // Get credits expiring in the next 30 days
const thirtyDaysFromNow = addDays(new Date(), 30); const thirtyDaysFromNow = addDays(new Date(), CREDITS_EXPIRATION_DAYS);
const expiringCredits = await db const expiringCredits = await db
.select({ .select({
amount: sum(creditTransaction.remainingAmount), amount: sum(creditTransaction.remainingAmount),
@ -47,7 +50,7 @@ export const getCreditStatsAction = actionClient.action(async () => {
); );
// Get credits from subscription renewals (recent 30 days) // Get credits from subscription renewals (recent 30 days)
const thirtyDaysAgo = addDays(new Date(), -30); const thirtyDaysAgo = addDays(new Date(), -CREDITS_MONTHLY_DAYS);
const subscriptionCredits = await db const subscriptionCredits = await db
.select({ .select({
amount: sum(creditTransaction.amount), amount: sum(creditTransaction.amount),

View File

@ -92,7 +92,9 @@ export default function BillingCard() {
return ( return (
<Card className={cn('w-full overflow-hidden pt-6 pb-0 flex flex-col')}> <Card className={cn('w-full overflow-hidden pt-6 pb-0 flex flex-col')}>
<CardHeader> <CardHeader>
<CardTitle>{t('currentPlan.title')}</CardTitle> <CardTitle className="text-lg font-semibold">
{t('currentPlan.title')}
</CardTitle>
<CardDescription>{t('currentPlan.description')}</CardDescription> <CardDescription>{t('currentPlan.description')}</CardDescription>
</CardHeader> </CardHeader>
<CardContent className="space-y-4 flex-1"> <CardContent className="space-y-4 flex-1">
@ -103,7 +105,7 @@ export default function BillingCard() {
</div> </div>
</CardContent> </CardContent>
<CardFooter className="mt-2 px-6 py-4 flex justify-end items-center bg-background rounded-none"> <CardFooter className="mt-2 px-6 py-4 flex justify-end items-center bg-background rounded-none">
<Skeleton className="h-10 w-4/5" /> <Skeleton className="h-10 w-1/2" />
</CardFooter> </CardFooter>
</Card> </Card>
); );
@ -114,7 +116,9 @@ export default function BillingCard() {
return ( return (
<Card className={cn('w-full overflow-hidden pt-6 pb-0 flex flex-col')}> <Card className={cn('w-full overflow-hidden pt-6 pb-0 flex flex-col')}>
<CardHeader> <CardHeader>
<CardTitle>{t('currentPlan.title')}</CardTitle> <CardTitle className="text-lg font-semibold">
{t('currentPlan.title')}
</CardTitle>
<CardDescription>{t('currentPlan.description')}</CardDescription> <CardDescription>{t('currentPlan.description')}</CardDescription>
</CardHeader> </CardHeader>
<CardContent className="space-y-4 flex-1"> <CardContent className="space-y-4 flex-1">
@ -139,7 +143,9 @@ export default function BillingCard() {
return ( return (
<Card className={cn('w-full overflow-hidden pt-6 pb-0 flex flex-col')}> <Card className={cn('w-full overflow-hidden pt-6 pb-0 flex flex-col')}>
<CardHeader> <CardHeader>
<CardTitle>{t('currentPlan.title')}</CardTitle> <CardTitle className="text-lg font-semibold">
{t('currentPlan.title')}
</CardTitle>
<CardDescription>{t('currentPlan.description')}</CardDescription> <CardDescription>{t('currentPlan.description')}</CardDescription>
</CardHeader> </CardHeader>
<CardContent> <CardContent>

View File

@ -18,6 +18,7 @@ import { LocaleLink, useLocaleRouter } from '@/i18n/navigation';
import { formatDate } from '@/lib/formatter'; import { formatDate } from '@/lib/formatter';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { Routes } from '@/routes'; import { Routes } from '@/routes';
import { Loader2Icon } from 'lucide-react';
import { useTranslations } from 'next-intl'; import { useTranslations } from 'next-intl';
import { useSearchParams } from 'next/navigation'; import { useSearchParams } from 'next/navigation';
import { useEffect, useRef, useState } from 'react'; import { useEffect, useRef, useState } from 'react';
@ -30,7 +31,12 @@ export default function CreditsBalanceCard() {
const hasHandledSession = useRef(false); const hasHandledSession = useRef(false);
// Use the credits hook to get balance // Use the credits hook to get balance
const { balance, isLoading, error, refresh } = useCredits(); const {
balance,
isLoading: isLoadingBalance,
error,
refresh: refreshBalance,
} = useCredits();
// Get payment info to check plan type // Get payment info to check plan type
const { currentPlan } = usePayment(); const { currentPlan } = usePayment();
@ -44,7 +50,7 @@ export default function CreditsBalanceCard() {
subscriptionCredits: { amount: number }; subscriptionCredits: { amount: number };
lifetimeCredits: { amount: number }; lifetimeCredits: { amount: number };
} | null>(null); } | null>(null);
const [statsLoading, setStatsLoading] = useState(false); const [isLoadingStats, setIsLoadingStats] = useState(true);
// Don't render if credits are disabled // Don't render if credits are disabled
if (!websiteConfig.credits.enableCredits) { if (!websiteConfig.credits.enableCredits) {
@ -53,7 +59,7 @@ export default function CreditsBalanceCard() {
// Function to fetch credit statistics // Function to fetch credit statistics
const fetchCreditStats = async () => { const fetchCreditStats = async () => {
setStatsLoading(true); setIsLoadingStats(true);
try { try {
const result = await getCreditStatsAction(); const result = await getCreditStatsAction();
if (result?.data?.success && result.data.data) { if (result?.data?.success && result.data.data) {
@ -64,7 +70,7 @@ export default function CreditsBalanceCard() {
} catch (error) { } catch (error) {
console.error('Failed to fetch credit stats:', error); console.error('Failed to fetch credit stats:', error);
} finally { } finally {
setStatsLoading(false); setIsLoadingStats(false);
} }
}; };
@ -84,7 +90,7 @@ export default function CreditsBalanceCard() {
}, 0); }, 0);
// Refresh credits data to show updated balance // Refresh credits data to show updated balance
refresh(); refreshBalance();
// Refresh credit stats // Refresh credit stats
fetchCreditStats(); fetchCreditStats();
@ -93,24 +99,28 @@ export default function CreditsBalanceCard() {
url.searchParams.delete('session_id'); url.searchParams.delete('session_id');
localeRouter.replace(Routes.SettingsBilling + url.search); localeRouter.replace(Routes.SettingsBilling + url.search);
} }
}, [searchParams, localeRouter, refresh]); }, [searchParams, localeRouter, refreshBalance, fetchCreditStats]);
// Render loading skeleton // Render loading skeleton
if (isLoading) { const isPageLoading = isLoadingBalance || isLoadingStats;
if (isPageLoading) {
return ( return (
<Card className={cn('w-full overflow-hidden pt-6 pb-0 flex flex-col')}> <Card className={cn('w-full overflow-hidden pt-6 pb-0 flex flex-col')}>
<CardHeader> <CardHeader>
<CardTitle>{t('title')}</CardTitle> <CardTitle className="text-lg font-semibold">{t('title')}</CardTitle>
<CardDescription>{t('description')}</CardDescription> <CardDescription>{t('description')}</CardDescription>
</CardHeader> </CardHeader>
<CardContent className="space-y-4 flex-1"> <CardContent className="space-y-4 flex-1">
<div className="space-y-3"> <div className="flex items-center justify-start space-x-4">
<Skeleton className="h-6 w-1/2" /> <Skeleton className="h-6 w-1/5" />
<Skeleton className="h-6 w-4/5" /> </div>
<div className="text-sm text-muted-foreground space-y-2">
<Skeleton className="h-6 w-2/5" />
<Skeleton className="h-6 w-3/5" />
</div> </div>
</CardContent> </CardContent>
<CardFooter className="mt-2 px-6 py-4 flex justify-end items-center bg-background rounded-none"> <CardFooter className="mt-2 px-6 py-4 flex justify-end items-center bg-background rounded-none">
<Skeleton className="h-10 w-4/5" /> <Skeleton className="h-10 w-1/2" />
</CardFooter> </CardFooter>
</Card> </Card>
); );
@ -121,7 +131,7 @@ export default function CreditsBalanceCard() {
return ( return (
<Card className={cn('w-full overflow-hidden pt-6 pb-0 flex flex-col')}> <Card className={cn('w-full overflow-hidden pt-6 pb-0 flex flex-col')}>
<CardHeader> <CardHeader>
<CardTitle>{t('title')}</CardTitle> <CardTitle className="text-lg font-semibold">{t('title')}</CardTitle>
<CardDescription>{t('description')}</CardDescription> <CardDescription>{t('description')}</CardDescription>
</CardHeader> </CardHeader>
<CardContent className="space-y-4 flex-1"> <CardContent className="space-y-4 flex-1">
@ -159,7 +169,7 @@ export default function CreditsBalanceCard() {
{/* Balance information */} {/* Balance information */}
<div className="text-sm text-muted-foreground space-y-2"> <div className="text-sm text-muted-foreground space-y-2">
{/* Plan-based credits info */} {/* Plan-based credits info */}
{!statsLoading && creditStats && ( {!isLoadingStats && creditStats && (
<> <>
{/* Subscription credits (for paid plans) */} {/* Subscription credits (for paid plans) */}
{!currentPlan?.isFree && {!currentPlan?.isFree &&