refactor: improve loading state management in billing page
This commit is contained in:
parent
9fcfb3bdf7
commit
141b562307
@ -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",
|
||||||
|
@ -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": "订阅月度积分",
|
||||||
|
@ -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),
|
||||||
|
@ -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>
|
||||||
|
@ -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 &&
|
||||||
|
Loading…
Reference in New Issue
Block a user