refactor: replace useCredits hook with custom hooks for credit balance, consumption, and transactions management
This commit is contained in:
parent
d153ca655e
commit
ff1e72df13
@ -2,7 +2,7 @@
|
||||
|
||||
import { CreditsBalanceButton } from '@/components/layout/credits-balance-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 { useState } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
@ -10,24 +10,28 @@ import { toast } from 'sonner';
|
||||
const CONSUME_CREDITS = 50;
|
||||
|
||||
export function ConsumeCreditCard() {
|
||||
const { consumeCredits, hasEnoughCredits, isLoading } = useCredits();
|
||||
const { data: balance = 0, isLoading: isLoadingBalance } = useCreditBalance();
|
||||
const consumeCreditsMutation = useConsumeCredits();
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const hasEnoughCredits = (amount: number) => balance >= amount;
|
||||
|
||||
const handleConsume = async () => {
|
||||
if (!hasEnoughCredits(CONSUME_CREDITS)) {
|
||||
toast.error('Insufficient credits, please buy more credits.');
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
const success = await consumeCredits(
|
||||
CONSUME_CREDITS,
|
||||
`AI Text Credit Consumption (${CONSUME_CREDITS} credits)`
|
||||
);
|
||||
setLoading(false);
|
||||
if (success) {
|
||||
try {
|
||||
await consumeCreditsMutation.mutateAsync({
|
||||
amount: CONSUME_CREDITS,
|
||||
description: `AI Text Credit Consumption (${CONSUME_CREDITS} credits)`,
|
||||
});
|
||||
toast.success(`${CONSUME_CREDITS} credits have been consumed.`);
|
||||
} else {
|
||||
} catch (error) {
|
||||
toast.error('Failed to consume credits, please try again later.');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
@ -40,7 +44,9 @@ export function ConsumeCreditCard() {
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleConsume}
|
||||
disabled={isLoading || loading}
|
||||
disabled={
|
||||
loading || isLoadingBalance || consumeCreditsMutation.isPending
|
||||
}
|
||||
className="w-full cursor-pointer"
|
||||
>
|
||||
<CoinsIcon className="size-4" />
|
||||
|
@ -1,7 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { ActiveThemeProvider } from '@/components/layout/active-theme-provider';
|
||||
import { CreditsProvider } from '@/components/layout/credits-provider';
|
||||
import { PaymentProvider } from '@/components/layout/payment-provider';
|
||||
import { QueryProvider } from '@/components/providers/query-provider';
|
||||
import { TooltipProvider } from '@/components/ui/tooltip';
|
||||
@ -65,9 +64,7 @@ export function Providers({ children, locale }: ProvidersProps) {
|
||||
<ActiveThemeProvider>
|
||||
<RootProvider theme={theme} i18n={{ locale, locales, translations }}>
|
||||
<TooltipProvider>
|
||||
<PaymentProvider>
|
||||
<CreditsProvider>{children}</CreditsProvider>
|
||||
</PaymentProvider>
|
||||
<PaymentProvider>{children}</PaymentProvider>
|
||||
</TooltipProvider>
|
||||
</RootProvider>
|
||||
</ActiveThemeProvider>
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { websiteConfig } from '@/config/website';
|
||||
import { useCredits } from '@/hooks/use-credits';
|
||||
import { useCreditBalance } from '@/hooks/use-credits-query';
|
||||
import { useLocaleRouter } from '@/i18n/navigation';
|
||||
import { Routes } from '@/routes';
|
||||
import { CoinsIcon, Loader2Icon } from 'lucide-react';
|
||||
@ -15,8 +15,8 @@ export function CreditsBalanceButton() {
|
||||
|
||||
const router = useLocaleRouter();
|
||||
|
||||
// Use the new useCredits hook
|
||||
const { balance, isLoading } = useCredits();
|
||||
// Use TanStack Query hook for credit balance
|
||||
const { data: balance = 0, isLoading } = useCreditBalance();
|
||||
|
||||
const handleClick = () => {
|
||||
router.push(Routes.SettingsCredits);
|
||||
|
@ -1,7 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { websiteConfig } from '@/config/website';
|
||||
import { useCredits } from '@/hooks/use-credits';
|
||||
import { useCreditBalance } from '@/hooks/use-credits-query';
|
||||
import { useLocaleRouter } from '@/i18n/navigation';
|
||||
import { Routes } from '@/routes';
|
||||
import { CoinsIcon, Loader2Icon } from 'lucide-react';
|
||||
@ -16,8 +16,8 @@ export function CreditsBalanceMenu() {
|
||||
const t = useTranslations('Marketing.avatar');
|
||||
const router = useLocaleRouter();
|
||||
|
||||
// Use the new useCredits hook
|
||||
const { balance, isLoading } = useCredits();
|
||||
// Use TanStack Query hook for credit balance
|
||||
const { data: balance = 0, isLoading } = useCreditBalance();
|
||||
|
||||
const handleClick = () => {
|
||||
router.push(Routes.SettingsCredits);
|
||||
|
@ -1,31 +1,19 @@
|
||||
'use client';
|
||||
|
||||
import { websiteConfig } from '@/config/website';
|
||||
import { authClient } from '@/lib/auth-client';
|
||||
import { useCreditsStore } from '@/stores/credits-store';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
/**
|
||||
* Credits Provider Component
|
||||
*
|
||||
* This component initializes the credits store when the user is authenticated
|
||||
* and handles cleanup when the user logs out.
|
||||
* This component is now simplified since TanStack Query handles data fetching automatically.
|
||||
* It's kept for potential future credits-related providers.
|
||||
* Only renders when credits are enabled in the website configuration.
|
||||
*/
|
||||
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) {
|
||||
return <>{children}</>;
|
||||
}
|
||||
|
||||
const { fetchCredits } = useCreditsStore();
|
||||
const { data: session } = authClient.useSession();
|
||||
|
||||
useEffect(() => {
|
||||
if (session?.user) {
|
||||
fetchCredits(session.user);
|
||||
}
|
||||
}, [session?.user, fetchCredits]);
|
||||
|
||||
return <>{children}</>;
|
||||
}
|
||||
|
@ -1,12 +1,10 @@
|
||||
'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 { useCreditTransactions } from '@/hooks/use-credits-query';
|
||||
import type { SortingState } from '@tanstack/react-table';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
import { useState } from 'react';
|
||||
|
||||
/**
|
||||
* Credit transactions component
|
||||
@ -16,57 +14,25 @@ export function CreditTransactions() {
|
||||
const [pageIndex, setPageIndex] = useState(0);
|
||||
const [pageSize, setPageSize] = useState(10);
|
||||
const [search, setSearch] = useState('');
|
||||
const [data, setData] = useState<CreditTransaction[]>([]);
|
||||
const [total, setTotal] = useState(0);
|
||||
const [sorting, setSorting] = useState<SortingState>([
|
||||
{ id: 'createdAt', desc: true },
|
||||
]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const fetchData = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const result = await getCreditTransactionsAction({
|
||||
pageIndex,
|
||||
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]);
|
||||
const { data, isLoading } = useCreditTransactions(
|
||||
pageIndex,
|
||||
pageSize,
|
||||
search,
|
||||
sorting
|
||||
);
|
||||
|
||||
return (
|
||||
<CreditTransactionsTable
|
||||
data={data}
|
||||
total={total}
|
||||
data={data?.items || []}
|
||||
total={data?.total || 0}
|
||||
pageIndex={pageIndex}
|
||||
pageSize={pageSize}
|
||||
search={search}
|
||||
loading={loading}
|
||||
loading={isLoading}
|
||||
onSearch={setSearch}
|
||||
onPageChange={setPageIndex}
|
||||
onPageSizeChange={setPageSize}
|
||||
|
@ -1,6 +1,5 @@
|
||||
'use client';
|
||||
|
||||
import { getCreditStatsAction } from '@/actions/get-credit-stats';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
Card,
|
||||
@ -12,7 +11,7 @@ import {
|
||||
} from '@/components/ui/card';
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
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 { usePayment } from '@/hooks/use-payment';
|
||||
import { useLocaleRouter } from '@/i18n/navigation';
|
||||
@ -22,7 +21,7 @@ import { Routes } from '@/routes';
|
||||
import { RefreshCwIcon } from 'lucide-react';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useCallback, useEffect, useRef } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
/**
|
||||
@ -40,50 +39,24 @@ export default function CreditsBalanceCard() {
|
||||
const hasHandledSession = useRef(false);
|
||||
const mounted = useMounted();
|
||||
|
||||
// Use the credits hook to get balance
|
||||
// Use TanStack Query hooks for credits
|
||||
const {
|
||||
balance,
|
||||
data: balance = 0,
|
||||
isLoading: isLoadingBalance,
|
||||
error,
|
||||
fetchCredits,
|
||||
} = useCredits();
|
||||
refetch: refetchCredits,
|
||||
} = useCreditBalance();
|
||||
|
||||
// Get payment info to check plan type
|
||||
const { currentPlan } = usePayment();
|
||||
|
||||
// State for credit statistics
|
||||
const [creditStats, setCreditStats] = useState<{
|
||||
expiringCredits: {
|
||||
amount: number;
|
||||
earliestExpiration: string | Date | null;
|
||||
};
|
||||
subscriptionCredits: { amount: number };
|
||||
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();
|
||||
}, []);
|
||||
// TanStack Query hook for credit statistics
|
||||
const {
|
||||
data: creditStats,
|
||||
isLoading: isLoadingStats,
|
||||
error: statsError,
|
||||
refetch: refetchCreditStats,
|
||||
} = useCreditStats();
|
||||
|
||||
// Check for payment success and show success message
|
||||
useEffect(() => {
|
||||
@ -96,9 +69,9 @@ export default function CreditsBalanceCard() {
|
||||
toast.success(t('creditsAdded'));
|
||||
|
||||
// Force refresh credits data to show updated balance
|
||||
fetchCredits(true);
|
||||
refetchCredits();
|
||||
// Refresh credit stats
|
||||
fetchCreditStats();
|
||||
refetchCreditStats();
|
||||
}, 0);
|
||||
|
||||
// Clean up URL parameters
|
||||
@ -106,16 +79,16 @@ export default function CreditsBalanceCard() {
|
||||
url.searchParams.delete('credits_session_id');
|
||||
localeRouter.replace(Routes.SettingsCredits + url.search);
|
||||
}
|
||||
}, [searchParams, localeRouter, fetchCredits, fetchCreditStats, t]);
|
||||
}, [searchParams, localeRouter, refetchCredits, refetchCreditStats, t]);
|
||||
|
||||
// Retry all data fetching
|
||||
const handleRetry = useCallback(() => {
|
||||
// console.log('handleRetry, refetch credits data');
|
||||
// Force refresh credits balance (ignore cache)
|
||||
fetchCredits(true);
|
||||
refetchCredits();
|
||||
// Refresh credit stats
|
||||
fetchCreditStats();
|
||||
}, [fetchCredits, fetchCreditStats]);
|
||||
refetchCreditStats();
|
||||
}, [refetchCredits, refetchCreditStats]);
|
||||
|
||||
// Render loading skeleton
|
||||
const isPageLoading = isLoadingBalance || isLoadingStats;
|
||||
@ -140,7 +113,7 @@ export default function CreditsBalanceCard() {
|
||||
}
|
||||
|
||||
// Render error state
|
||||
if (error) {
|
||||
if (error || statsError) {
|
||||
return (
|
||||
<Card className={cn('w-full overflow-hidden pt-6 pb-0 flex flex-col')}>
|
||||
<CardHeader>
|
||||
@ -148,7 +121,9 @@ export default function CreditsBalanceCard() {
|
||||
<CardDescription>{t('description')}</CardDescription>
|
||||
</CardHeader>
|
||||
<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>
|
||||
<CardFooter className="mt-2 px-6 py-4 flex justify-end items-center bg-background rounded-none">
|
||||
<Button
|
||||
|
117
src/hooks/use-credits-query.ts
Normal file
117
src/hooks/use-credits-query.ts
Normal 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,
|
||||
};
|
||||
},
|
||||
});
|
||||
}
|
@ -6,7 +6,7 @@ import type { SortingState } from '@tanstack/react-table';
|
||||
// Query keys
|
||||
export const usersKeys = {
|
||||
all: ['users'] as const,
|
||||
lists: () => [...usersKeys.all, 'list'] as const,
|
||||
lists: () => [...usersKeys.all, 'lists'] as const,
|
||||
list: (filters: {
|
||||
pageIndex: number;
|
||||
pageSize: number;
|
||||
|
Loading…
Reference in New Issue
Block a user