From c00223c79a3530a2ab675b193a4a6c7b55572239 Mon Sep 17 00:00:00 2001 From: javayhu Date: Wed, 20 Aug 2025 22:39:20 +0800 Subject: [PATCH] refactor: replace server actions with custom hooks for newsletter management and improve loading/error handling --- .../notification/newsletter-form-card.tsx | 144 +++++++----------- src/hooks/use-newsletter.ts | 77 ++++++++++ src/lib/query-client.ts | 15 ++ 3 files changed, 144 insertions(+), 92 deletions(-) create mode 100644 src/hooks/use-newsletter.ts create mode 100644 src/lib/query-client.ts diff --git a/src/components/settings/notification/newsletter-form-card.tsx b/src/components/settings/notification/newsletter-form-card.tsx index 6786c30..5593383 100644 --- a/src/components/settings/notification/newsletter-form-card.tsx +++ b/src/components/settings/notification/newsletter-form-card.tsx @@ -1,8 +1,5 @@ 'use client'; -import { checkNewsletterStatusAction } from '@/actions/check-newsletter-status'; -import { subscribeNewsletterAction } from '@/actions/subscribe-newsletter'; -import { unsubscribeNewsletterAction } from '@/actions/unsubscribe-newsletter'; import { FormError } from '@/components/shared/form-error'; import { Card, @@ -21,12 +18,17 @@ import { } from '@/components/ui/form'; import { Switch } from '@/components/ui/switch'; import { websiteConfig } from '@/config/website'; +import { + useNewsletterStatus, + useSubscribeNewsletter, + useUnsubscribeNewsletter, +} from '@/hooks/use-newsletter'; import { authClient } from '@/lib/auth-client'; import { cn } from '@/lib/utils'; import { zodResolver } from '@hookform/resolvers/zod'; import { Loader2Icon } from 'lucide-react'; import { useTranslations } from 'next-intl'; -import { useEffect, useState } from 'react'; +import { useEffect } from 'react'; import { useForm } from 'react-hook-form'; import { toast } from 'sonner'; import { z } from 'zod'; @@ -47,12 +49,19 @@ export function NewsletterFormCard({ className }: NewsletterFormCardProps) { } const t = useTranslations('Dashboard.settings.notification'); - const [isLoading, setIsLoading] = useState(false); - const [error, setError] = useState(''); - const [isSubscriptionChecked, setIsSubscriptionChecked] = useState(false); const { data: session } = authClient.useSession(); const currentUser = session?.user; + // TanStack Query hooks + const { + data: newsletterStatus, + isLoading: isStatusLoading, + error: statusError, + } = useNewsletterStatus(currentUser?.email); + + const subscribeMutation = useSubscribeNewsletter(); + const unsubscribeMutation = useUnsubscribeNewsletter(); + // Create a schema for newsletter subscription const formSchema = z.object({ subscribed: z.boolean(), @@ -66,45 +75,12 @@ export function NewsletterFormCard({ className }: NewsletterFormCardProps) { }, }); - // Check subscription status on component mount + // Update form when newsletter status changes useEffect(() => { - const checkSubscriptionStatus = async () => { - if (currentUser?.email) { - try { - setIsLoading(true); - // Check if the user is already subscribed using server action - const statusResult = await checkNewsletterStatusAction({ - email: currentUser.email, - }); - - if (statusResult?.data?.success) { - const isCurrentlySubscribed = statusResult.data.subscribed; - setIsSubscriptionChecked(isCurrentlySubscribed); - form.setValue('subscribed', isCurrentlySubscribed); - } else { - // Handle error from server action - const errorMessage = statusResult?.data?.error; - if (errorMessage) { - console.error('check subscription status error:', errorMessage); - setError(errorMessage); - } - // Default to not subscribed if there's an error - setIsSubscriptionChecked(false); - form.setValue('subscribed', false); - } - } catch (error) { - console.error('check subscription status error:', error); - // Default to not subscribed if there's an error - setIsSubscriptionChecked(false); - form.setValue('subscribed', false); - } finally { - setIsLoading(false); - } - } - }; - - checkSubscriptionStatus(); - }, [currentUser?.email, form]); + if (newsletterStatus) { + form.setValue('subscribed', newsletterStatus.subscribed); + } + }, [newsletterStatus, form]); // Check if user exists after all hooks are initialized if (!currentUser) { @@ -114,59 +90,27 @@ export function NewsletterFormCard({ className }: NewsletterFormCardProps) { // Handle checkbox change const handleSubscriptionChange = async (value: boolean) => { if (!currentUser.email) { - setError(t('newsletter.emailRequired')); + toast.error(t('newsletter.emailRequired')); return; } - setIsLoading(true); - setError(''); - try { if (value) { - // Subscribe to newsletter using server action - const subscribeResult = await subscribeNewsletterAction({ - email: currentUser.email, - }); - - if (subscribeResult?.data?.success) { - toast.success(t('newsletter.subscribeSuccess')); - setIsSubscriptionChecked(true); - form.setValue('subscribed', true); - } else { - const errorMessage = - subscribeResult?.data?.error || t('newsletter.subscribeFail'); - toast.error(errorMessage); - setError(errorMessage); - // Reset checkbox if subscription failed - form.setValue('subscribed', false); - } + // Subscribe to newsletter + await subscribeMutation.mutateAsync(currentUser.email); + toast.success(t('newsletter.subscribeSuccess')); } else { - // Unsubscribe from newsletter using server action - const unsubscribeResult = await unsubscribeNewsletterAction({ - email: currentUser.email, - }); - - if (unsubscribeResult?.data?.success) { - toast.success(t('newsletter.unsubscribeSuccess')); - setIsSubscriptionChecked(false); - form.setValue('subscribed', false); - } else { - const errorMessage = - unsubscribeResult?.data?.error || t('newsletter.unsubscribeFail'); - toast.error(errorMessage); - setError(errorMessage); - // Reset checkbox if unsubscription failed - form.setValue('subscribed', true); - } + // Unsubscribe from newsletter + await unsubscribeMutation.mutateAsync(currentUser.email); + toast.success(t('newsletter.unsubscribeSuccess')); } } catch (error) { console.error('newsletter subscription error:', error); - setError(t('newsletter.error')); - toast.error(t('newsletter.error')); + const errorMessage = + error instanceof Error ? error.message : t('newsletter.error'); + toast.error(errorMessage); // Reset form to previous state on error - form.setValue('subscribed', isSubscriptionChecked); - } finally { - setIsLoading(false); + form.setValue('subscribed', newsletterStatus?.subscribed || false); } }; @@ -193,7 +137,9 @@ export function NewsletterFormCard({ className }: NewsletterFormCardProps) {
- {isLoading && ( + {(isStatusLoading || + subscribeMutation.isPending || + unsubscribeMutation.isPending) && ( )}
@@ -211,7 +165,13 @@ export function NewsletterFormCard({ className }: NewsletterFormCardProps) { )} /> - +

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/lib/query-client.ts b/src/lib/query-client.ts new file mode 100644 index 0000000..bb0486e --- /dev/null +++ b/src/lib/query-client.ts @@ -0,0 +1,15 @@ +import { QueryClient } from '@tanstack/react-query'; + +export const queryClient = new QueryClient({ + defaultOptions: { + queries: { + staleTime: 5 * 60 * 1000, // 5 minutes + gcTime: 10 * 60 * 1000, // 10 minutes (formerly cacheTime) + retry: 1, + refetchOnWindowFocus: false, + }, + mutations: { + retry: 1, + }, + }, +});