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": {
"MONTHLY_REFRESH": "Monthly Refresh",
"REGISTER_GIFT": "Register Gift",
"PURCHASE_PACKAGE": "Purchased Credits",
"PURCHASE": "Purchased Credits",
"USAGE": "Consumed Credits",
"EXPIRE": "Expired Credits",
"SUBSCRIPTION_RENEWAL": "Subscription Renewal",

View File

@ -635,7 +635,7 @@
"types": {
"MONTHLY_REFRESH": "每月赠送",
"REGISTER_GIFT": "注册赠送",
"PURCHASE_PACKAGE": "购买积分",
"PURCHASE": "购买积分",
"USAGE": "使用积分",
"EXPIRE": "过期积分",
"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 { createSafeActionClient } from 'next-safe-action';
const CREDITS_EXPIRATION_DAYS = 30;
const CREDITS_MONTHLY_DAYS = 30;
// Create a safe action client
const actionClient = createSafeActionClient();
@ -28,7 +31,7 @@ export const getCreditStatsAction = actionClient.action(async () => {
const userId = session.user.id;
// 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
.select({
amount: sum(creditTransaction.remainingAmount),
@ -47,7 +50,7 @@ export const getCreditStatsAction = actionClient.action(async () => {
);
// 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
.select({
amount: sum(creditTransaction.amount),

View File

@ -92,7 +92,9 @@ export default function BillingCard() {
return (
<Card className={cn('w-full overflow-hidden pt-6 pb-0 flex flex-col')}>
<CardHeader>
<CardTitle>{t('currentPlan.title')}</CardTitle>
<CardTitle className="text-lg font-semibold">
{t('currentPlan.title')}
</CardTitle>
<CardDescription>{t('currentPlan.description')}</CardDescription>
</CardHeader>
<CardContent className="space-y-4 flex-1">
@ -103,7 +105,7 @@ export default function BillingCard() {
</div>
</CardContent>
<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>
</Card>
);
@ -114,7 +116,9 @@ export default function BillingCard() {
return (
<Card className={cn('w-full overflow-hidden pt-6 pb-0 flex flex-col')}>
<CardHeader>
<CardTitle>{t('currentPlan.title')}</CardTitle>
<CardTitle className="text-lg font-semibold">
{t('currentPlan.title')}
</CardTitle>
<CardDescription>{t('currentPlan.description')}</CardDescription>
</CardHeader>
<CardContent className="space-y-4 flex-1">
@ -139,7 +143,9 @@ export default function BillingCard() {
return (
<Card className={cn('w-full overflow-hidden pt-6 pb-0 flex flex-col')}>
<CardHeader>
<CardTitle>{t('currentPlan.title')}</CardTitle>
<CardTitle className="text-lg font-semibold">
{t('currentPlan.title')}
</CardTitle>
<CardDescription>{t('currentPlan.description')}</CardDescription>
</CardHeader>
<CardContent>

View File

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