diff --git a/src/hooks/use-credits.ts b/src/hooks/use-credits.ts
index 2cfd0ee..7c4ed8c 100644
--- a/src/hooks/use-credits.ts
+++ b/src/hooks/use-credits.ts
@@ -1,66 +1,121 @@
-import { websiteConfig } from '@/config/website';
-import { authClient } from '@/lib/auth-client';
-import { useCreditsStore } from '@/stores/credits-store';
-import { useCallback, useEffect } from 'react';
+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';
-/**
- * Hook for accessing and managing credits state
- *
- * This hook provides access to the credits state and methods to manage it.
- * It also automatically fetches credits information when the user changes.
- * Only works when credits are enabled in the website configuration.
- */
-export function useCredits() {
- // Return default values if credits are disabled
- if (!websiteConfig.credits.enableCredits) {
- return {
- balance: 0,
- isLoading: false,
- error: null,
- fetchCredits: () => Promise.resolve(),
- consumeCredits: () => Promise.resolve(false),
- hasEnoughCredits: () => false,
- };
- }
+// 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,
+};
- const {
- balance,
- isLoading,
- error,
- fetchCredits: fetchCreditsFromStore,
- consumeCredits,
- } = useCreditsStore();
-
- const { data: session } = authClient.useSession();
-
- const fetchCredits = useCallback(
- (force = false) => {
- const currentUser = session?.user;
- if (currentUser) {
- fetchCreditsFromStore(currentUser, force);
+// Hook to fetch credit balance
+export function useCreditBalance() {
+ return useQuery({
+ queryKey: creditsKeys.balance(),
+ queryFn: async () => {
+ console.log('Fetching credit balance...');
+ const result = await getCreditBalanceAction();
+ if (!result?.data?.success) {
+ throw new Error('Failed to fetch credit balance');
}
+ console.log('Credit balance fetched:', result.data.credits);
+ return result.data.credits || 0;
},
- [session?.user, fetchCreditsFromStore]
- );
-
- useEffect(() => {
- const currentUser = session?.user;
- if (currentUser) {
- fetchCreditsFromStore(currentUser);
- }
- }, [session?.user, fetchCreditsFromStore]);
-
- return {
- // State
- balance,
- isLoading,
- error,
-
- // Methods
- fetchCredits,
- consumeCredits,
-
- // Helper methods
- hasEnoughCredits: (amount: number) => balance >= amount,
- };
+ });
+}
+
+// Hook to fetch credit statistics
+export function useCreditStats() {
+ return useQuery({
+ queryKey: creditsKeys.stats(),
+ queryFn: async () => {
+ console.log('Fetching credit stats...');
+ const result = await getCreditStatsAction();
+ if (!result?.data?.success) {
+ throw new Error(result?.data?.error || 'Failed to fetch credit stats');
+ }
+ console.log('Credit stats fetched:', result.data.data);
+ return result.data.data;
+ },
+ });
+}
+
+// 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,
+ };
+ },
+ });
}
diff --git a/src/hooks/use-newsletter.ts b/src/hooks/use-newsletter.ts
new file mode 100644
index 0000000..d4f80ce
--- /dev/null
+++ b/src/hooks/use-newsletter.ts
@@ -0,0 +1,77 @@
+import { checkNewsletterStatusAction } from '@/actions/check-newsletter-status';
+import { subscribeNewsletterAction } from '@/actions/subscribe-newsletter';
+import { unsubscribeNewsletterAction } from '@/actions/unsubscribe-newsletter';
+import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
+
+// Query keys
+export const newsletterKeys = {
+ all: ['newsletter'] as const,
+ status: (email: string) => [...newsletterKeys.all, 'status', email] as const,
+};
+
+// Hook to check newsletter subscription status
+export function useNewsletterStatus(email: string | undefined) {
+ return useQuery({
+ queryKey: newsletterKeys.status(email || ''),
+ queryFn: async () => {
+ if (!email) {
+ throw new Error('Email is required');
+ }
+ const result = await checkNewsletterStatusAction({ email });
+ if (!result?.data?.success) {
+ throw new Error(
+ result?.data?.error || 'Failed to check newsletter status'
+ );
+ }
+ return result.data;
+ },
+ enabled: !!email,
+ staleTime: 5 * 60 * 1000, // 5 minutes
+ });
+}
+
+// Hook to subscribe to newsletter
+export function useSubscribeNewsletter() {
+ const queryClient = useQueryClient();
+
+ return useMutation({
+ mutationFn: async (email: string) => {
+ const result = await subscribeNewsletterAction({ email });
+ if (!result?.data?.success) {
+ throw new Error(
+ result?.data?.error || 'Failed to subscribe to newsletter'
+ );
+ }
+ return result.data;
+ },
+ onSuccess: (_, email) => {
+ // Invalidate and refetch the newsletter status
+ queryClient.invalidateQueries({
+ queryKey: newsletterKeys.status(email),
+ });
+ },
+ });
+}
+
+// Hook to unsubscribe from newsletter
+export function useUnsubscribeNewsletter() {
+ const queryClient = useQueryClient();
+
+ return useMutation({
+ mutationFn: async (email: string) => {
+ const result = await unsubscribeNewsletterAction({ email });
+ if (!result?.data?.success) {
+ throw new Error(
+ result?.data?.error || 'Failed to unsubscribe from newsletter'
+ );
+ }
+ return result.data;
+ },
+ onSuccess: (_, email) => {
+ // Invalidate and refetch the newsletter status
+ queryClient.invalidateQueries({
+ queryKey: newsletterKeys.status(email),
+ });
+ },
+ });
+}
diff --git a/src/hooks/use-payment.ts b/src/hooks/use-payment.ts
index 259c020..a721841 100644
--- a/src/hooks/use-payment.ts
+++ b/src/hooks/use-payment.ts
@@ -1,49 +1,107 @@
-import { authClient } from '@/lib/auth-client';
-import { usePaymentStore } from '@/stores/payment-store';
-import { useCallback, useEffect } from 'react';
+import { getActiveSubscriptionAction } from '@/actions/get-active-subscription';
+import { getLifetimeStatusAction } from '@/actions/get-lifetime-status';
+import { getAllPricePlans } from '@/lib/price-plan';
+import type { PricePlan, Subscription } from '@/payment/types';
+import { useQuery } from '@tanstack/react-query';
-/**
- * Hook for accessing and managing payment state
- *
- * This hook provides access to the payment state and methods to manage it.
- * It also automatically fetches payment information when the user changes.
- */
-export function usePayment() {
- const {
- currentPlan,
- subscription,
- isLoading,
- error,
- fetchPayment: fetchPaymentFromStore,
- } = usePaymentStore();
+// Query keys
+export const paymentKeys = {
+ all: ['payment'] as const,
+ subscription: (userId: string) =>
+ [...paymentKeys.all, 'subscription', userId] as const,
+ lifetime: (userId: string) =>
+ [...paymentKeys.all, 'lifetime', userId] as const,
+ currentPlan: (userId: string) =>
+ [...paymentKeys.all, 'currentPlan', userId] as const,
+};
- const { data: session } = authClient.useSession();
-
- const fetchPayment = useCallback(
- (force = false) => {
- const currentUser = session?.user;
- if (currentUser) {
- fetchPaymentFromStore(currentUser, force);
+// Hook to fetch active subscription
+export function useActiveSubscription(userId: string | undefined) {
+ return useQuery({
+ queryKey: paymentKeys.subscription(userId || ''),
+ queryFn: async (): Promise => {
+ if (!userId) {
+ throw new Error('User ID is required');
}
+ const result = await getActiveSubscriptionAction({ userId });
+ if (!result?.data?.success) {
+ throw new Error(result?.data?.error || 'Failed to fetch subscription');
+ }
+ return result.data.data || null;
},
- [session?.user, fetchPaymentFromStore]
- );
-
- useEffect(() => {
- const currentUser = session?.user;
- if (currentUser) {
- fetchPaymentFromStore(currentUser);
- }
- }, [session?.user, fetchPaymentFromStore]);
-
- return {
- // State
- currentPlan,
- subscription,
- isLoading,
- error,
-
- // Methods
- fetchPayment,
- };
+ enabled: !!userId,
+ });
+}
+
+// Hook to fetch lifetime status
+export function useLifetimeStatus(userId: string | undefined) {
+ return useQuery({
+ queryKey: paymentKeys.lifetime(userId || ''),
+ queryFn: async (): Promise => {
+ if (!userId) {
+ throw new Error('User ID is required');
+ }
+ const result = await getLifetimeStatusAction({ userId });
+ if (!result?.data?.success) {
+ throw new Error(
+ result?.data?.error || 'Failed to fetch lifetime status'
+ );
+ }
+ return result.data.isLifetimeMember || false;
+ },
+ enabled: !!userId,
+ });
+}
+
+// Hook to get current plan based on subscription and lifetime status
+export function useCurrentPlan(userId: string | undefined) {
+ const {
+ data: subscription,
+ isLoading: isLoadingSubscription,
+ error: subscriptionError,
+ } = useActiveSubscription(userId);
+ const {
+ data: isLifetimeMember,
+ isLoading: isLoadingLifetime,
+ error: lifetimeError,
+ } = useLifetimeStatus(userId);
+
+ return useQuery({
+ queryKey: paymentKeys.currentPlan(userId || ''),
+ queryFn: async (): Promise<{
+ currentPlan: PricePlan | null;
+ subscription: Subscription | null;
+ }> => {
+ const plans: PricePlan[] = getAllPricePlans();
+ const freePlan = plans.find((plan) => plan.isFree);
+ const lifetimePlan = plans.find((plan) => plan.isLifetime);
+
+ // If lifetime member, return lifetime plan
+ if (isLifetimeMember) {
+ return {
+ currentPlan: lifetimePlan || null,
+ subscription: null,
+ };
+ }
+
+ // If has active subscription, find the corresponding plan
+ if (subscription) {
+ const plan =
+ plans.find((p) =>
+ p.prices.find((price) => price.priceId === subscription.priceId)
+ ) || null;
+ return {
+ currentPlan: plan,
+ subscription,
+ };
+ }
+
+ // Default to free plan
+ return {
+ currentPlan: freePlan || null,
+ subscription: null,
+ };
+ },
+ enabled: !!userId && !isLoadingSubscription && !isLoadingLifetime,
+ });
}
diff --git a/src/hooks/use-users.ts b/src/hooks/use-users.ts
new file mode 100644
index 0000000..f983685
--- /dev/null
+++ b/src/hooks/use-users.ts
@@ -0,0 +1,93 @@
+import { getUsersAction } from '@/actions/get-users';
+import { authClient } from '@/lib/auth-client';
+import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
+import type { SortingState } from '@tanstack/react-table';
+
+// Query keys
+export const usersKeys = {
+ all: ['users'] as const,
+ lists: () => [...usersKeys.all, 'lists'] as const,
+ list: (filters: {
+ pageIndex: number;
+ pageSize: number;
+ search: string;
+ sorting: SortingState;
+ }) => [...usersKeys.lists(), filters] as const,
+};
+
+// Hook to fetch users with pagination, search, and sorting
+export function useUsers(
+ pageIndex: number,
+ pageSize: number,
+ search: string,
+ sorting: SortingState
+) {
+ return useQuery({
+ queryKey: usersKeys.list({ pageIndex, pageSize, search, sorting }),
+ queryFn: async () => {
+ const result = await getUsersAction({
+ pageIndex,
+ pageSize,
+ search,
+ sorting,
+ });
+
+ if (!result?.data?.success) {
+ throw new Error(result?.data?.error || 'Failed to fetch users');
+ }
+
+ return {
+ items: result.data.data?.items || [],
+ total: result.data.data?.total || 0,
+ };
+ },
+ });
+}
+
+// Hook to ban user
+export function useBanUser() {
+ const queryClient = useQueryClient();
+
+ return useMutation({
+ mutationFn: async ({
+ userId,
+ banReason,
+ banExpiresIn,
+ }: {
+ userId: string;
+ banReason: string;
+ banExpiresIn?: number;
+ }) => {
+ return authClient.admin.banUser({
+ userId,
+ banReason,
+ banExpiresIn,
+ });
+ },
+ onSuccess: () => {
+ // Invalidate all users queries to refresh the data
+ queryClient.invalidateQueries({
+ queryKey: usersKeys.all,
+ });
+ },
+ });
+}
+
+// Hook to unban user
+export function useUnbanUser() {
+ const queryClient = useQueryClient();
+
+ return useMutation({
+ mutationFn: async ({ userId }: { userId: string }) => {
+ return authClient.admin.unbanUser({
+ userId,
+ });
+ },
+ onSuccess: () => {
+ // Invalidate all users queries to refresh the data
+ queryClient.invalidateQueries({
+ queryKey: usersKeys.all,
+ });
+ },
+ });
+}
diff --git a/src/stores/credits-store.ts b/src/stores/credits-store.ts
deleted file mode 100644
index 0f7325e..0000000
--- a/src/stores/credits-store.ts
+++ /dev/null
@@ -1,163 +0,0 @@
-import { consumeCreditsAction } from '@/actions/consume-credits';
-import { getCreditBalanceAction } from '@/actions/get-credit-balance';
-import type { Session } from '@/lib/auth-types';
-import { create } from 'zustand';
-
-// Cache duration: 2 minutes (optimized for better UX)
-const CACHE_DURATION = 2 * 60 * 1000;
-
-/**
- * Credits state interface
- */
-export interface CreditsState {
- // Current credit balance
- balance: number;
- // Loading state
- isLoading: boolean;
- // Error state
- error: string | null;
- // Last fetch timestamp to avoid frequent requests
- lastFetchTime: number | null;
-
- // Actions
- fetchCredits: (
- user: Session['user'] | null | undefined,
- force?: boolean
- ) => Promise;
- consumeCredits: (amount: number, description: string) => Promise;
-}
-
-/**
- * Credits store using Zustand
- * Manages the user's credit balance globally with caching and optimistic updates
- */
-export const useCreditsStore = create((set, get) => ({
- // Initial state
- balance: 0,
- isLoading: false,
- error: null,
- lastFetchTime: null,
-
- /**
- * Fetch credit balance for the current user with optional cache bypass
- * @param user Current user from auth session
- * @param force Whether to force refresh and ignore cache
- */
- fetchCredits: async (user, force = false) => {
- // Skip if already loading
- if (get().isLoading) return;
-
- // Skip if no user is provided
- if (!user) {
- set({
- balance: 0,
- error: null,
- lastFetchTime: null,
- });
- return;
- }
-
- // Check if we have recent data (within cache duration) unless force refresh
- if (!force) {
- const { lastFetchTime } = get();
- const now = Date.now();
- if (lastFetchTime && now - lastFetchTime < CACHE_DURATION) {
- return; // Use cached data
- }
- }
-
- console.log(`fetchCredits, ${force ? 'force fetch' : 'fetch'} credits`);
- set({
- isLoading: true,
- error: null,
- // Clear cache if force refresh
- lastFetchTime: force ? null : get().lastFetchTime,
- });
-
- try {
- const result = await getCreditBalanceAction();
- if (result?.data?.success && result.data.credits !== undefined) {
- const newBalance = result.data.credits || 0;
- console.log('fetchCredits, set new balance', newBalance);
- set({
- balance: newBalance,
- isLoading: false,
- error: null,
- lastFetchTime: Date.now(),
- });
- } else {
- console.warn('fetchCredits, failed to fetch credit balance', result);
- set({
- error:
- (result?.data as any)?.error || 'Failed to fetch credit balance',
- isLoading: false,
- });
- }
- } catch (error) {
- console.error('fetchCredits, error:', error);
- set({
- error: 'Failed to fetch credit balance',
- isLoading: false,
- });
- }
- },
-
- /**
- * Consume credits with optimistic updates
- * @param amount Amount of credits to consume
- * @param description Description for the transaction
- * @returns Promise Success status
- */
- consumeCredits: async (amount: number, description: string) => {
- const { balance } = get();
-
- // Check if we have enough credits
- if (balance < amount) {
- console.log('consumeCredits, insufficient credits', balance, amount);
- set({
- error: 'Insufficient credits',
- });
- return false;
- }
-
- // Optimistically update the balance
- set({
- balance: balance - amount,
- error: null,
- isLoading: true,
- });
-
- try {
- const result = await consumeCreditsAction({
- amount,
- description,
- });
-
- if (result?.data?.success) {
- set({
- isLoading: false,
- error: null,
- });
- return true;
- }
-
- // Revert optimistic update on failure
- console.warn('consumeCredits, reverting optimistic update');
- set({
- balance: balance, // Revert to original balance
- error: result?.data?.error || 'Failed to consume credits',
- isLoading: false,
- });
- return false;
- } catch (error) {
- console.error('consumeCredits, error:', error);
- // Revert optimistic update on error
- set({
- balance: balance, // Revert to original balance
- error: 'Failed to consume credits',
- isLoading: false,
- });
- return false;
- }
- },
-}));
diff --git a/src/stores/payment-store.ts b/src/stores/payment-store.ts
deleted file mode 100644
index ba8edd8..0000000
--- a/src/stores/payment-store.ts
+++ /dev/null
@@ -1,180 +0,0 @@
-import { getActiveSubscriptionAction } from '@/actions/get-active-subscription';
-import { getLifetimeStatusAction } from '@/actions/get-lifetime-status';
-import type { Session } from '@/lib/auth-types';
-import { getAllPricePlans } from '@/lib/price-plan';
-import type { PricePlan, Subscription } from '@/payment/types';
-import { create } from 'zustand';
-
-// Cache duration: 2 minutes (optimized for better UX)
-const CACHE_DURATION = 2 * 60 * 1000;
-
-/**
- * Payment state interface
- */
-export interface PaymentState {
- // Current plan
- currentPlan: PricePlan | null;
- // Active subscription
- subscription: Subscription | null;
- // Loading state
- isLoading: boolean;
- // Error state
- error: string | null;
- // Last fetch timestamp to avoid frequent requests
- lastFetchTime: number | null;
-
- // Actions
- fetchPayment: (
- user: Session['user'] | null | undefined,
- force?: boolean
- ) => Promise;
- resetState: () => void;
-}
-
-/**
- * Payment store using Zustand
- * Manages the user's payment and subscription data globally
- */
-export const usePaymentStore = create((set, get) => ({
- // Initial state
- currentPlan: null,
- subscription: null,
- isLoading: false,
- error: null,
- lastFetchTime: null,
-
- /**
- * Fetch payment and subscription data for the current user
- * @param user Current user from auth session
- */
- fetchPayment: async (user, force = false) => {
- // Skip if already loading
- if (get().isLoading) return;
-
- // Skip if no user is provided
- if (!user) {
- set({
- currentPlan: null,
- subscription: null,
- error: null,
- lastFetchTime: null,
- });
- return;
- }
-
- // Check if we have recent data (within cache duration) unless force refresh
- if (!force) {
- const { lastFetchTime } = get();
- const now = Date.now();
- if (lastFetchTime && now - lastFetchTime < CACHE_DURATION) {
- console.log('fetchPayment, use cached data');
- return; // Use cached data
- }
- }
-
- // Fetch subscription data
- set({ isLoading: true, error: null });
-
- // Get all price plans
- const plans: PricePlan[] = getAllPricePlans();
- const freePlan = plans.find((plan) => plan.isFree);
- const lifetimePlan = plans.find((plan) => plan.isLifetime);
-
- // Check if user is a lifetime member directly from the database
- let isLifetimeMember = false;
- try {
- const result = await getLifetimeStatusAction({ userId: user.id });
- if (result?.data?.success) {
- isLifetimeMember = result.data.isLifetimeMember || false;
- console.log('fetchPayment, lifetime status', isLifetimeMember);
- } else {
- console.warn(
- 'fetchPayment, lifetime status error',
- result?.data?.error
- );
- }
- } catch (error) {
- console.error('fetchPayment, lifetime status error:', error);
- }
-
- // If lifetime member, set the lifetime plan
- if (isLifetimeMember) {
- console.log('fetchPayment, set lifetime plan');
- set({
- currentPlan: lifetimePlan || null,
- subscription: null,
- isLoading: false,
- error: null,
- lastFetchTime: Date.now(),
- });
- return;
- }
-
- try {
- // Check if user has an active subscription
- const result = await getActiveSubscriptionAction({ userId: user.id });
- if (result?.data?.success) {
- const activeSubscription = result.data.data;
-
- // Set subscription state
- if (activeSubscription) {
- const plan =
- plans.find((p) =>
- p.prices.find(
- (price) => price.priceId === activeSubscription.priceId
- )
- ) || null;
- console.log('fetchPayment, subscription found, set pro plan');
- set({
- currentPlan: plan,
- subscription: activeSubscription,
- isLoading: false,
- error: null,
- lastFetchTime: Date.now(),
- });
- } else {
- // No subscription found - set to free plan
- console.log('fetchPayment, no subscription found, set free plan');
- set({
- currentPlan: freePlan || null,
- subscription: null,
- isLoading: false,
- error: null,
- lastFetchTime: Date.now(),
- });
- }
- } else {
- // Failed to fetch subscription
- console.error(
- 'fetchPayment, subscription for user failed',
- result?.data?.error
- );
- set({
- error: result?.data?.error || 'Failed to fetch payment data',
- isLoading: false,
- });
- }
- } catch (error) {
- console.error('fetchPayment, error:', error);
- set({
- error: 'Failed to fetch payment data',
- isLoading: false,
- });
- } finally {
- set({ isLoading: false });
- }
- },
-
- /**
- * Reset payment state
- */
- resetState: () => {
- set({
- currentPlan: null,
- subscription: null,
- isLoading: false,
- error: null,
- lastFetchTime: null,
- });
- },
-}));
diff --git a/src/stores/users-store.ts b/src/stores/users-store.ts
deleted file mode 100644
index e6b901e..0000000
--- a/src/stores/users-store.ts
+++ /dev/null
@@ -1,12 +0,0 @@
-import { create } from 'zustand';
-
-interface UsersState {
- refreshTrigger: number;
- triggerRefresh: () => void;
-}
-
-export const useUsersStore = create((set) => ({
- refreshTrigger: 0,
- triggerRefresh: () =>
- set((state) => ({ refreshTrigger: state.refreshTrigger + 1 })),
-}));