feat: implement newsletter subscription and unsubscription actions

- Added `subscribeAction` and `unsubscribeAction` in `newsletter.ts` to handle newsletter subscriptions and unsubscriptions with validation using Zod.
- Introduced new localization messages for email validation in English and Chinese localization files.
- Updated `WaitlistFormCard` to utilize the new subscription action, improving user feedback with toast notifications.
- Replaced the alert icon in `FormError` component for better visual representation of errors.
This commit is contained in:
javayhu 2025-03-16 10:42:33 +08:00
parent 9b221f4583
commit 9ddf1a3c20
9 changed files with 170 additions and 15 deletions

View File

@ -72,6 +72,7 @@
"formTitle": "Join Our Waitlist",
"formDescription": "We will send you only one email every week, and no spam",
"email": "Email",
"emailValidation": "Please enter a valid email address",
"subscribe": "Subscribe",
"subscribing": "Subscribing...",
"success": "Subscribed successfully",

View File

@ -66,6 +66,7 @@
"formTitle": "加入我们的邮件列表",
"formDescription": "我们每周只发送一封邮件,并且不会发送垃圾邮件",
"email": "邮箱",
"emailValidation": "请输入有效的邮箱地址",
"subscribe": "订阅",
"subscribing": "订阅中...",
"success": "订阅成功",

68
src/actions/newsletter.ts Normal file
View File

@ -0,0 +1,68 @@
'use server';
import {
subscribe as subscribeToNewsletter,
unsubscribe as unsubscribeFromNewsletter
} from '@/newsletter';
import { createSafeActionClient } from 'next-safe-action';
import { z } from 'zod';
// Create a safe action client
const actionClient = createSafeActionClient();
// Newsletter schema for validation
const newsletterSchema = z.object({
email: z.string().email({ message: 'Please enter a valid email address' }),
});
// Create a safe action for newsletter subscription
export const subscribeAction = actionClient
.schema(newsletterSchema)
.action(async ({ parsedInput: { email } }) => {
try {
const subscribed = await subscribeToNewsletter(email);
if (!subscribed) {
return {
success: false,
error: 'Failed to subscribe to the newsletter',
};
}
return {
success: true,
};
} catch (error) {
console.error('Newsletter subscription error:', error);
return {
success: false,
error: 'An unexpected error occurred',
};
}
});
// Create a safe action for newsletter unsubscription
export const unsubscribeAction = actionClient
.schema(newsletterSchema)
.action(async ({ parsedInput: { email } }) => {
try {
const unsubscribed = await unsubscribeFromNewsletter(email);
if (!unsubscribed) {
return {
success: false,
error: 'Failed to unsubscribe from the newsletter',
};
}
return {
success: true,
};
} catch (error) {
console.error('Newsletter unsubscription error:', error);
return {
success: false,
error: 'An unexpected error occurred',
};
}
});

View File

@ -1,4 +1,4 @@
import { TriangleAlertIcon } from 'lucide-react';
import { BugIcon } from 'lucide-react';
interface FormErrorProps {
message?: string;
@ -9,7 +9,7 @@ export const FormError = ({ message }: FormErrorProps) => {
return (
<div className="bg-destructive/15 p-3 rounded-md flex items-center gap-x-2 text-sm text-destructive">
<TriangleAlertIcon className="h-4 w-4" />
<BugIcon className="h-4 w-4" />
<p>{message}</p>
</div>
);

View File

@ -1,5 +1,6 @@
"use client";
import { subscribeAction } from '@/actions/newsletter';
import { FormError } from '@/components/shared/form-error';
import { Button } from '@/components/ui/button';
import {
@ -39,7 +40,7 @@ export function WaitlistFormCard() {
const formSchema = z.object({
email: z
.string()
.email({ message: 'Please enter a valid email address' }),
.email({ message: t('emailValidation') }),
});
// Initialize the form
@ -52,21 +53,21 @@ export function WaitlistFormCard() {
// Handle form submission
const onSubmit = async (values: z.infer<typeof formSchema>) => {
setIsSubmitting(true);
setError('');
try {
// Here you would typically send the form data to your API
console.log('Form submitted:', values);
setError('');
setIsSubmitting(true);
// Simulate API call
await new Promise(resolve => setTimeout(resolve, 1000));
const result = await subscribeAction({
email: values.email,
});
// Show success message
toast.success(t('success'));
// Reset form
form.reset();
if (result?.data?.success) {
toast.success(t('success'));
form.reset();
} else {
setError(t('fail'));
toast.error(t('fail'));
}
} catch (err) {
console.error('Form submission error:', err);
setError(t('fail'));

3
src/newsletter/index.ts Normal file
View File

@ -0,0 +1,3 @@
// export the subscribe and unsubscribe functions
export { subscribe } from './newsletter';
export { unsubscribe } from './newsletter';

View File

@ -0,0 +1,11 @@
import { subscribeNewsletter, unsubscribeNewsletter } from './provider/resend';
export const subscribe = async (email: string) => {
const subscribed = await subscribeNewsletter({ email });
return subscribed;
};
export const unsubscribe = async (email: string) => {
const unsubscribed = await unsubscribeNewsletter({ email });
return unsubscribed;
};

View File

@ -0,0 +1,51 @@
import { SubscribeNewsletterHandler, UnsubscribeNewsletterHandler } from '@/newsletter/types';
import { Resend } from 'resend';
const apiKey = process.env.RESEND_API_KEY || 'test_api_key';
const audienceId = process.env.RESEND_AUDIENCE_ID || 'test_audience_id';
const resend = new Resend(apiKey);
export const subscribeNewsletter: SubscribeNewsletterHandler = async ({ email }) => {
if (!process.env.RESEND_API_KEY || !process.env.RESEND_AUDIENCE_ID) {
console.warn('RESEND_API_KEY or RESEND_AUDIENCE_ID not set, skipping subscribe newsletter');
return false;
}
const result = await resend.contacts.create({
email,
audienceId,
unsubscribed: false,
});
const subscribed = !result.error;
if (!subscribed) {
console.error('Error subscribing newsletter', result.error);
return false;
} else {
console.log('Subscribed newsletter', email);
return true;
}
};
export const unsubscribeNewsletter: UnsubscribeNewsletterHandler = async ({ email }) => {
if (!process.env.RESEND_API_KEY || !process.env.RESEND_AUDIENCE_ID) {
console.warn('RESEND_API_KEY or RESEND_AUDIENCE_ID not set, skipping unsubscribe newsletter');
return false;
}
const result = await resend.contacts.update({
email,
audienceId,
unsubscribed: true,
});
const unsubscribed = !result.error;
if (!unsubscribed) {
console.error('Error unsubscribing newsletter', result.error);
return false;
} else {
console.log('Unsubscribed newsletter', email);
return true;
}
};

19
src/newsletter/types.ts Normal file
View File

@ -0,0 +1,19 @@
export interface SubscribeNewsletterProps {
email: string;
}
export interface UnsubscribeNewsletterProps {
email: string;
}
export type SubscribeNewsletterHandler = (params: SubscribeNewsletterProps) => Promise<boolean>;
export type UnsubscribeNewsletterHandler = (params: UnsubscribeNewsletterProps) => Promise<boolean>;
/**
* Newsletter provider, currently only Resend is supported
*/
export interface NewsletterProvider {
subscribe: SubscribeNewsletterHandler;
unsubscribe: UnsubscribeNewsletterHandler;
}