refactor: replace useCredits hook with custom hooks for credit balance, consumption, and transactions management

This commit is contained in:
javayhu 2025-08-21 00:27:26 +08:00
parent d153ca655e
commit ff1e72df13
9 changed files with 178 additions and 129 deletions

View File

@ -2,7 +2,7 @@
import { CreditsBalanceButton } from '@/components/layout/credits-balance-button'; import { CreditsBalanceButton } from '@/components/layout/credits-balance-button';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { useCredits } from '@/hooks/use-credits'; import { useConsumeCredits, useCreditBalance } from '@/hooks/use-credits-query';
import { CoinsIcon } from 'lucide-react'; import { CoinsIcon } from 'lucide-react';
import { useState } from 'react'; import { useState } from 'react';
import { toast } from 'sonner'; import { toast } from 'sonner';
@ -10,24 +10,28 @@ import { toast } from 'sonner';
const CONSUME_CREDITS = 50; const CONSUME_CREDITS = 50;
export function ConsumeCreditCard() { export function ConsumeCreditCard() {
const { consumeCredits, hasEnoughCredits, isLoading } = useCredits(); const { data: balance = 0, isLoading: isLoadingBalance } = useCreditBalance();
const consumeCreditsMutation = useConsumeCredits();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const hasEnoughCredits = (amount: number) => balance >= amount;
const handleConsume = async () => { const handleConsume = async () => {
if (!hasEnoughCredits(CONSUME_CREDITS)) { if (!hasEnoughCredits(CONSUME_CREDITS)) {
toast.error('Insufficient credits, please buy more credits.'); toast.error('Insufficient credits, please buy more credits.');
return; return;
} }
setLoading(true); setLoading(true);
const success = await consumeCredits( try {
CONSUME_CREDITS, await consumeCreditsMutation.mutateAsync({
`AI Text Credit Consumption (${CONSUME_CREDITS} credits)` amount: CONSUME_CREDITS,
); description: `AI Text Credit Consumption (${CONSUME_CREDITS} credits)`,
setLoading(false); });
if (success) {
toast.success(`${CONSUME_CREDITS} credits have been consumed.`); toast.success(`${CONSUME_CREDITS} credits have been consumed.`);
} else { } catch (error) {
toast.error('Failed to consume credits, please try again later.'); toast.error('Failed to consume credits, please try again later.');
} finally {
setLoading(false);
} }
}; };
@ -40,7 +44,9 @@ export function ConsumeCreditCard() {
variant="outline" variant="outline"
size="sm" size="sm"
onClick={handleConsume} onClick={handleConsume}
disabled={isLoading || loading} disabled={
loading || isLoadingBalance || consumeCreditsMutation.isPending
}
className="w-full cursor-pointer" className="w-full cursor-pointer"
> >
<CoinsIcon className="size-4" /> <CoinsIcon className="size-4" />

View File

@ -1,7 +1,6 @@
'use client'; 'use client';
import { ActiveThemeProvider } from '@/components/layout/active-theme-provider'; import { ActiveThemeProvider } from '@/components/layout/active-theme-provider';
import { CreditsProvider } from '@/components/layout/credits-provider';
import { PaymentProvider } from '@/components/layout/payment-provider'; import { PaymentProvider } from '@/components/layout/payment-provider';
import { QueryProvider } from '@/components/providers/query-provider'; import { QueryProvider } from '@/components/providers/query-provider';
import { TooltipProvider } from '@/components/ui/tooltip'; import { TooltipProvider } from '@/components/ui/tooltip';
@ -65,9 +64,7 @@ export function Providers({ children, locale }: ProvidersProps) {
<ActiveThemeProvider> <ActiveThemeProvider>
<RootProvider theme={theme} i18n={{ locale, locales, translations }}> <RootProvider theme={theme} i18n={{ locale, locales, translations }}>
<TooltipProvider> <TooltipProvider>
<PaymentProvider> <PaymentProvider>{children}</PaymentProvider>
<CreditsProvider>{children}</CreditsProvider>
</PaymentProvider>
</TooltipProvider> </TooltipProvider>
</RootProvider> </RootProvider>
</ActiveThemeProvider> </ActiveThemeProvider>

View File

@ -2,7 +2,7 @@
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { websiteConfig } from '@/config/website'; import { websiteConfig } from '@/config/website';
import { useCredits } from '@/hooks/use-credits'; import { useCreditBalance } from '@/hooks/use-credits-query';
import { useLocaleRouter } from '@/i18n/navigation'; import { useLocaleRouter } from '@/i18n/navigation';
import { Routes } from '@/routes'; import { Routes } from '@/routes';
import { CoinsIcon, Loader2Icon } from 'lucide-react'; import { CoinsIcon, Loader2Icon } from 'lucide-react';
@ -15,8 +15,8 @@ export function CreditsBalanceButton() {
const router = useLocaleRouter(); const router = useLocaleRouter();
// Use the new useCredits hook // Use TanStack Query hook for credit balance
const { balance, isLoading } = useCredits(); const { data: balance = 0, isLoading } = useCreditBalance();
const handleClick = () => { const handleClick = () => {
router.push(Routes.SettingsCredits); router.push(Routes.SettingsCredits);

View File

@ -1,7 +1,7 @@
'use client'; 'use client';
import { websiteConfig } from '@/config/website'; import { websiteConfig } from '@/config/website';
import { useCredits } from '@/hooks/use-credits'; import { useCreditBalance } from '@/hooks/use-credits-query';
import { useLocaleRouter } from '@/i18n/navigation'; import { useLocaleRouter } from '@/i18n/navigation';
import { Routes } from '@/routes'; import { Routes } from '@/routes';
import { CoinsIcon, Loader2Icon } from 'lucide-react'; import { CoinsIcon, Loader2Icon } from 'lucide-react';
@ -16,8 +16,8 @@ export function CreditsBalanceMenu() {
const t = useTranslations('Marketing.avatar'); const t = useTranslations('Marketing.avatar');
const router = useLocaleRouter(); const router = useLocaleRouter();
// Use the new useCredits hook // Use TanStack Query hook for credit balance
const { balance, isLoading } = useCredits(); const { data: balance = 0, isLoading } = useCreditBalance();
const handleClick = () => { const handleClick = () => {
router.push(Routes.SettingsCredits); router.push(Routes.SettingsCredits);

View File

@ -1,31 +1,19 @@
'use client'; 'use client';
import { websiteConfig } from '@/config/website'; import { websiteConfig } from '@/config/website';
import { authClient } from '@/lib/auth-client';
import { useCreditsStore } from '@/stores/credits-store';
import { useEffect } from 'react';
/** /**
* Credits Provider Component * Credits Provider Component
* *
* This component initializes the credits store when the user is authenticated * This component is now simplified since TanStack Query handles data fetching automatically.
* and handles cleanup when the user logs out. * It's kept for potential future credits-related providers.
* Only renders when credits are enabled in the website configuration. * Only renders when credits are enabled in the website configuration.
*/ */
export function CreditsProvider({ children }: { children: React.ReactNode }) { export function CreditsProvider({ children }: { children: React.ReactNode }) {
// Only initialize credits store if credits are enabled // Only render when credits are enabled
if (!websiteConfig.credits.enableCredits) { if (!websiteConfig.credits.enableCredits) {
return <>{children}</>; return <>{children}</>;
} }
const { fetchCredits } = useCreditsStore();
const { data: session } = authClient.useSession();
useEffect(() => {
if (session?.user) {
fetchCredits(session.user);
}
}, [session?.user, fetchCredits]);
return <>{children}</>; return <>{children}</>;
} }

View File

@ -1,12 +1,10 @@
'use client'; 'use client';
import { getCreditTransactionsAction } from '@/actions/get-credit-transactions';
import type { CreditTransaction } from '@/components/settings/credits/credit-transactions-table';
import { CreditTransactionsTable } from '@/components/settings/credits/credit-transactions-table'; import { CreditTransactionsTable } from '@/components/settings/credits/credit-transactions-table';
import { useCreditTransactions } from '@/hooks/use-credits-query';
import type { SortingState } from '@tanstack/react-table'; import type { SortingState } from '@tanstack/react-table';
import { useTranslations } from 'next-intl'; import { useTranslations } from 'next-intl';
import { useCallback, useEffect, useState } from 'react'; import { useState } from 'react';
import { toast } from 'sonner';
/** /**
* Credit transactions component * Credit transactions component
@ -16,57 +14,25 @@ export function CreditTransactions() {
const [pageIndex, setPageIndex] = useState(0); const [pageIndex, setPageIndex] = useState(0);
const [pageSize, setPageSize] = useState(10); const [pageSize, setPageSize] = useState(10);
const [search, setSearch] = useState(''); const [search, setSearch] = useState('');
const [data, setData] = useState<CreditTransaction[]>([]);
const [total, setTotal] = useState(0);
const [sorting, setSorting] = useState<SortingState>([ const [sorting, setSorting] = useState<SortingState>([
{ id: 'createdAt', desc: true }, { id: 'createdAt', desc: true },
]); ]);
const [loading, setLoading] = useState(false);
const fetchData = useCallback(async () => { const { data, isLoading } = useCreditTransactions(
setLoading(true); pageIndex,
try { pageSize,
const result = await getCreditTransactionsAction({ search,
pageIndex, sorting
pageSize, );
search,
sorting,
});
if (result?.data?.success) {
setData(result.data.data?.items || []);
setTotal(result.data.data?.total || 0);
} else {
const errorMessage = result?.data?.error || t('error');
toast.error(errorMessage);
setData([]);
setTotal(0);
}
} catch (error) {
console.error(
'CreditTransactions, fetch credit transactions error:',
error
);
toast.error(t('error'));
setData([]);
setTotal(0);
} finally {
setLoading(false);
}
}, [pageIndex, pageSize, search, sorting]);
useEffect(() => {
fetchData();
}, [fetchData]);
return ( return (
<CreditTransactionsTable <CreditTransactionsTable
data={data} data={data?.items || []}
total={total} total={data?.total || 0}
pageIndex={pageIndex} pageIndex={pageIndex}
pageSize={pageSize} pageSize={pageSize}
search={search} search={search}
loading={loading} loading={isLoading}
onSearch={setSearch} onSearch={setSearch}
onPageChange={setPageIndex} onPageChange={setPageIndex}
onPageSizeChange={setPageSize} onPageSizeChange={setPageSize}

View File

@ -1,6 +1,5 @@
'use client'; 'use client';
import { getCreditStatsAction } from '@/actions/get-credit-stats';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { import {
Card, Card,
@ -12,7 +11,7 @@ import {
} from '@/components/ui/card'; } from '@/components/ui/card';
import { Skeleton } from '@/components/ui/skeleton'; import { Skeleton } from '@/components/ui/skeleton';
import { websiteConfig } from '@/config/website'; import { websiteConfig } from '@/config/website';
import { useCredits } from '@/hooks/use-credits'; import { useCreditBalance, useCreditStats } from '@/hooks/use-credits-query';
import { useMounted } from '@/hooks/use-mounted'; import { useMounted } from '@/hooks/use-mounted';
import { usePayment } from '@/hooks/use-payment'; import { usePayment } from '@/hooks/use-payment';
import { useLocaleRouter } from '@/i18n/navigation'; import { useLocaleRouter } from '@/i18n/navigation';
@ -22,7 +21,7 @@ import { Routes } from '@/routes';
import { RefreshCwIcon } from 'lucide-react'; import { RefreshCwIcon } from 'lucide-react';
import { useTranslations } from 'next-intl'; import { useTranslations } from 'next-intl';
import { useSearchParams } from 'next/navigation'; import { useSearchParams } from 'next/navigation';
import { useCallback, useEffect, useRef, useState } from 'react'; import { useCallback, useEffect, useRef } from 'react';
import { toast } from 'sonner'; import { toast } from 'sonner';
/** /**
@ -40,50 +39,24 @@ export default function CreditsBalanceCard() {
const hasHandledSession = useRef(false); const hasHandledSession = useRef(false);
const mounted = useMounted(); const mounted = useMounted();
// Use the credits hook to get balance // Use TanStack Query hooks for credits
const { const {
balance, data: balance = 0,
isLoading: isLoadingBalance, isLoading: isLoadingBalance,
error, error,
fetchCredits, refetch: refetchCredits,
} = useCredits(); } = useCreditBalance();
// Get payment info to check plan type // Get payment info to check plan type
const { currentPlan } = usePayment(); const { currentPlan } = usePayment();
// State for credit statistics // TanStack Query hook for credit statistics
const [creditStats, setCreditStats] = useState<{ const {
expiringCredits: { data: creditStats,
amount: number; isLoading: isLoadingStats,
earliestExpiration: string | Date | null; error: statsError,
}; refetch: refetchCreditStats,
subscriptionCredits: { amount: number }; } = useCreditStats();
lifetimeCredits: { amount: number };
} | null>(null);
const [isLoadingStats, setIsLoadingStats] = useState(true);
// Fetch credit statistics
const fetchCreditStats = useCallback(async () => {
console.log('fetchCreditStats, fetch start');
setIsLoadingStats(true);
try {
const result = await getCreditStatsAction();
if (result?.data?.success && result.data.data) {
setCreditStats(result.data.data);
} else {
console.error('fetchCreditStats, failed to fetch credit stats', result);
}
} catch (error) {
console.error('fetchCreditStats, error:', error);
} finally {
setIsLoadingStats(false);
}
}, []);
// Fetch stats on component mount
useEffect(() => {
fetchCreditStats();
}, []);
// Check for payment success and show success message // Check for payment success and show success message
useEffect(() => { useEffect(() => {
@ -96,9 +69,9 @@ export default function CreditsBalanceCard() {
toast.success(t('creditsAdded')); toast.success(t('creditsAdded'));
// Force refresh credits data to show updated balance // Force refresh credits data to show updated balance
fetchCredits(true); refetchCredits();
// Refresh credit stats // Refresh credit stats
fetchCreditStats(); refetchCreditStats();
}, 0); }, 0);
// Clean up URL parameters // Clean up URL parameters
@ -106,16 +79,16 @@ export default function CreditsBalanceCard() {
url.searchParams.delete('credits_session_id'); url.searchParams.delete('credits_session_id');
localeRouter.replace(Routes.SettingsCredits + url.search); localeRouter.replace(Routes.SettingsCredits + url.search);
} }
}, [searchParams, localeRouter, fetchCredits, fetchCreditStats, t]); }, [searchParams, localeRouter, refetchCredits, refetchCreditStats, t]);
// Retry all data fetching // Retry all data fetching
const handleRetry = useCallback(() => { const handleRetry = useCallback(() => {
// console.log('handleRetry, refetch credits data'); // console.log('handleRetry, refetch credits data');
// Force refresh credits balance (ignore cache) // Force refresh credits balance (ignore cache)
fetchCredits(true); refetchCredits();
// Refresh credit stats // Refresh credit stats
fetchCreditStats(); refetchCreditStats();
}, [fetchCredits, fetchCreditStats]); }, [refetchCredits, refetchCreditStats]);
// Render loading skeleton // Render loading skeleton
const isPageLoading = isLoadingBalance || isLoadingStats; const isPageLoading = isLoadingBalance || isLoadingStats;
@ -140,7 +113,7 @@ export default function CreditsBalanceCard() {
} }
// Render error state // Render error state
if (error) { if (error || statsError) {
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>
@ -148,7 +121,9 @@ export default function CreditsBalanceCard() {
<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="text-destructive text-sm">{error}</div> <div className="text-destructive text-sm">
{error?.message || statsError?.message}
</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">
<Button <Button

View File

@ -0,0 +1,117 @@
import { consumeCreditsAction } from '@/actions/consume-credits';
import { getCreditBalanceAction } from '@/actions/get-credit-balance';
import { getCreditStatsAction } from '@/actions/get-credit-stats';
import { getCreditTransactionsAction } from '@/actions/get-credit-transactions';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import type { SortingState } from '@tanstack/react-table';
// Query keys
export const creditsKeys = {
all: ['credits'] as const,
balance: () => [...creditsKeys.all, 'balance'] as const,
stats: () => [...creditsKeys.all, 'stats'] as const,
transactions: () => [...creditsKeys.all, 'transactions'] as const,
transactionsList: (filters: {
pageIndex: number;
pageSize: number;
search: string;
sorting: SortingState;
}) => [...creditsKeys.transactions(), filters] as const,
};
// Hook to fetch credit statistics
export function useCreditStats() {
return useQuery({
queryKey: creditsKeys.stats(),
queryFn: async () => {
const result = await getCreditStatsAction();
if (!result?.data?.success) {
throw new Error(result?.data?.error || 'Failed to fetch credit stats');
}
return result.data.data;
},
});
}
// Hook to fetch credit balance
export function useCreditBalance() {
return useQuery({
queryKey: creditsKeys.balance(),
queryFn: async () => {
const result = await getCreditBalanceAction();
if (!result?.data?.success) {
throw new Error('Failed to fetch credit balance');
}
return result.data.credits || 0;
},
});
}
// Hook to consume credits
export function useConsumeCredits() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({
amount,
description,
}: {
amount: number;
description: string;
}) => {
const result = await consumeCreditsAction({
amount,
description,
});
if (!result?.data?.success) {
throw new Error(result?.data?.error || 'Failed to consume credits');
}
return result.data;
},
onSuccess: () => {
// Invalidate credit balance and stats after consuming credits
queryClient.invalidateQueries({
queryKey: creditsKeys.balance(),
});
queryClient.invalidateQueries({
queryKey: creditsKeys.stats(),
});
},
});
}
// Hook to fetch credit transactions with pagination, search, and sorting
export function useCreditTransactions(
pageIndex: number,
pageSize: number,
search: string,
sorting: SortingState
) {
return useQuery({
queryKey: creditsKeys.transactionsList({
pageIndex,
pageSize,
search,
sorting,
}),
queryFn: async () => {
const result = await getCreditTransactionsAction({
pageIndex,
pageSize,
search,
sorting,
});
if (!result?.data?.success) {
throw new Error(
result?.data?.error || 'Failed to fetch credit transactions'
);
}
return {
items: result.data.data?.items || [],
total: result.data.data?.total || 0,
};
},
});
}

View File

@ -6,7 +6,7 @@ import type { SortingState } from '@tanstack/react-table';
// Query keys // Query keys
export const usersKeys = { export const usersKeys = {
all: ['users'] as const, all: ['users'] as const,
lists: () => [...usersKeys.all, 'list'] as const, lists: () => [...usersKeys.all, 'lists'] as const,
list: (filters: { list: (filters: {
pageIndex: number; pageIndex: number;
pageSize: number; pageSize: number;