refactor: replace server actions with custom hooks for newsletter management and improve loading/error handling
This commit is contained in:
parent
d59be1044a
commit
c00223c79a
@ -1,8 +1,5 @@
|
|||||||
'use client';
|
'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 { FormError } from '@/components/shared/form-error';
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
@ -21,12 +18,17 @@ import {
|
|||||||
} from '@/components/ui/form';
|
} from '@/components/ui/form';
|
||||||
import { Switch } from '@/components/ui/switch';
|
import { Switch } from '@/components/ui/switch';
|
||||||
import { websiteConfig } from '@/config/website';
|
import { websiteConfig } from '@/config/website';
|
||||||
|
import {
|
||||||
|
useNewsletterStatus,
|
||||||
|
useSubscribeNewsletter,
|
||||||
|
useUnsubscribeNewsletter,
|
||||||
|
} from '@/hooks/use-newsletter';
|
||||||
import { authClient } from '@/lib/auth-client';
|
import { authClient } from '@/lib/auth-client';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { Loader2Icon } from 'lucide-react';
|
import { Loader2Icon } from 'lucide-react';
|
||||||
import { useTranslations } from 'next-intl';
|
import { useTranslations } from 'next-intl';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
@ -47,12 +49,19 @@ export function NewsletterFormCard({ className }: NewsletterFormCardProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const t = useTranslations('Dashboard.settings.notification');
|
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 { data: session } = authClient.useSession();
|
||||||
const currentUser = session?.user;
|
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
|
// Create a schema for newsletter subscription
|
||||||
const formSchema = z.object({
|
const formSchema = z.object({
|
||||||
subscribed: z.boolean(),
|
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(() => {
|
useEffect(() => {
|
||||||
const checkSubscriptionStatus = async () => {
|
if (newsletterStatus) {
|
||||||
if (currentUser?.email) {
|
form.setValue('subscribed', newsletterStatus.subscribed);
|
||||||
try {
|
}
|
||||||
setIsLoading(true);
|
}, [newsletterStatus, form]);
|
||||||
// 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]);
|
|
||||||
|
|
||||||
// Check if user exists after all hooks are initialized
|
// Check if user exists after all hooks are initialized
|
||||||
if (!currentUser) {
|
if (!currentUser) {
|
||||||
@ -114,59 +90,27 @@ export function NewsletterFormCard({ className }: NewsletterFormCardProps) {
|
|||||||
// Handle checkbox change
|
// Handle checkbox change
|
||||||
const handleSubscriptionChange = async (value: boolean) => {
|
const handleSubscriptionChange = async (value: boolean) => {
|
||||||
if (!currentUser.email) {
|
if (!currentUser.email) {
|
||||||
setError(t('newsletter.emailRequired'));
|
toast.error(t('newsletter.emailRequired'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setIsLoading(true);
|
|
||||||
setError('');
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (value) {
|
if (value) {
|
||||||
// Subscribe to newsletter using server action
|
// Subscribe to newsletter
|
||||||
const subscribeResult = await subscribeNewsletterAction({
|
await subscribeMutation.mutateAsync(currentUser.email);
|
||||||
email: currentUser.email,
|
toast.success(t('newsletter.subscribeSuccess'));
|
||||||
});
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
// Unsubscribe from newsletter using server action
|
// Unsubscribe from newsletter
|
||||||
const unsubscribeResult = await unsubscribeNewsletterAction({
|
await unsubscribeMutation.mutateAsync(currentUser.email);
|
||||||
email: currentUser.email,
|
toast.success(t('newsletter.unsubscribeSuccess'));
|
||||||
});
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('newsletter subscription error:', error);
|
console.error('newsletter subscription error:', error);
|
||||||
setError(t('newsletter.error'));
|
const errorMessage =
|
||||||
toast.error(t('newsletter.error'));
|
error instanceof Error ? error.message : t('newsletter.error');
|
||||||
|
toast.error(errorMessage);
|
||||||
// Reset form to previous state on error
|
// Reset form to previous state on error
|
||||||
form.setValue('subscribed', isSubscriptionChecked);
|
form.setValue('subscribed', newsletterStatus?.subscribed || false);
|
||||||
} finally {
|
|
||||||
setIsLoading(false);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -193,7 +137,9 @@ export function NewsletterFormCard({ className }: NewsletterFormCardProps) {
|
|||||||
</div>
|
</div>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<div className="relative flex items-center">
|
<div className="relative flex items-center">
|
||||||
{isLoading && (
|
{(isStatusLoading ||
|
||||||
|
subscribeMutation.isPending ||
|
||||||
|
unsubscribeMutation.isPending) && (
|
||||||
<Loader2Icon className="mr-2 size-4 animate-spin text-primary" />
|
<Loader2Icon className="mr-2 size-4 animate-spin text-primary" />
|
||||||
)}
|
)}
|
||||||
<Switch
|
<Switch
|
||||||
@ -202,8 +148,16 @@ export function NewsletterFormCard({ className }: NewsletterFormCardProps) {
|
|||||||
field.onChange(checked);
|
field.onChange(checked);
|
||||||
handleSubscriptionChange(checked);
|
handleSubscriptionChange(checked);
|
||||||
}}
|
}}
|
||||||
disabled={isLoading}
|
disabled={
|
||||||
aria-readonly={isLoading}
|
isStatusLoading ||
|
||||||
|
subscribeMutation.isPending ||
|
||||||
|
unsubscribeMutation.isPending
|
||||||
|
}
|
||||||
|
aria-readonly={
|
||||||
|
isStatusLoading ||
|
||||||
|
subscribeMutation.isPending ||
|
||||||
|
unsubscribeMutation.isPending
|
||||||
|
}
|
||||||
className="cursor-pointer"
|
className="cursor-pointer"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -211,7 +165,13 @@ export function NewsletterFormCard({ className }: NewsletterFormCardProps) {
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<FormError message={error} />
|
<FormError
|
||||||
|
message={
|
||||||
|
statusError?.message ||
|
||||||
|
subscribeMutation.error?.message ||
|
||||||
|
unsubscribeMutation.error?.message
|
||||||
|
}
|
||||||
|
/>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
<CardFooter className="mt-6 px-6 py-4 bg-background rounded-none">
|
<CardFooter className="mt-6 px-6 py-4 bg-background rounded-none">
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">
|
||||||
|
77
src/hooks/use-newsletter.ts
Normal file
77
src/hooks/use-newsletter.ts
Normal 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
15
src/lib/query-client.ts
Normal 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,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
Loading…
Reference in New Issue
Block a user