refactor: replace server actions with custom hooks for newsletter management and improve loading/error handling

This commit is contained in:
javayhu 2025-08-20 22:39:20 +08:00
parent d59be1044a
commit c00223c79a
3 changed files with 144 additions and 92 deletions

View File

@ -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<string | undefined>('');
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) {
</div>
<FormControl>
<div className="relative flex items-center">
{isLoading && (
{(isStatusLoading ||
subscribeMutation.isPending ||
unsubscribeMutation.isPending) && (
<Loader2Icon className="mr-2 size-4 animate-spin text-primary" />
)}
<Switch
@ -202,8 +148,16 @@ export function NewsletterFormCard({ className }: NewsletterFormCardProps) {
field.onChange(checked);
handleSubscriptionChange(checked);
}}
disabled={isLoading}
aria-readonly={isLoading}
disabled={
isStatusLoading ||
subscribeMutation.isPending ||
unsubscribeMutation.isPending
}
aria-readonly={
isStatusLoading ||
subscribeMutation.isPending ||
unsubscribeMutation.isPending
}
className="cursor-pointer"
/>
</div>
@ -211,7 +165,13 @@ export function NewsletterFormCard({ className }: NewsletterFormCardProps) {
</FormItem>
)}
/>
<FormError message={error} />
<FormError
message={
statusError?.message ||
subscribeMutation.error?.message ||
unsubscribeMutation.error?.message
}
/>
</CardContent>
<CardFooter className="mt-6 px-6 py-4 bg-background rounded-none">
<p className="text-sm text-muted-foreground">

View File

@ -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),
});
},
});
}

15
src/lib/query-client.ts Normal file
View File

@ -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,
},
},
});