Add contact form, cookie banner, and update packages
This commit includes the addition of a contact form and a cookie banner to improve user experience and comply with regulations. The contact form involves email submission functionality. Several packages have also been updated and new routes have been added to the sitemap for better SEO. Environment variables have also been adjusted for email and contact form functionality.
This commit is contained in:
161
apps/web/app/(marketing)/contact/_components/contact-form.tsx
Normal file
161
apps/web/app/(marketing)/contact/_components/contact-form.tsx
Normal file
@@ -0,0 +1,161 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useTransition } from 'react';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useForm } from 'react-hook-form';
|
||||
|
||||
import { Alert, AlertDescription, AlertTitle } from '@kit/ui/alert';
|
||||
import { Button } from '@kit/ui/button';
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from '@kit/ui/form';
|
||||
import { Input } from '@kit/ui/input';
|
||||
import { Textarea } from '@kit/ui/textarea';
|
||||
import { Trans } from '@kit/ui/trans';
|
||||
|
||||
import { ContactEmailSchema } from '~/(marketing)/contact/_lib/contact-email.schema';
|
||||
import { sendContactEmail } from '~/(marketing)/contact/_lib/server/server-actions';
|
||||
|
||||
export function ContactForm() {
|
||||
const [pending, startTransition] = useTransition();
|
||||
|
||||
const [state, setState] = useState({
|
||||
success: false,
|
||||
error: false,
|
||||
});
|
||||
|
||||
const form = useForm({
|
||||
resolver: zodResolver(ContactEmailSchema),
|
||||
defaultValues: {
|
||||
name: '',
|
||||
email: '',
|
||||
message: '',
|
||||
},
|
||||
});
|
||||
|
||||
if (state.success) {
|
||||
return <SuccessAlert />;
|
||||
}
|
||||
|
||||
if (state.error) {
|
||||
return <ErrorAlert />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form
|
||||
className={'flex flex-col space-y-4'}
|
||||
onSubmit={form.handleSubmit((data) => {
|
||||
startTransition(async () => {
|
||||
try {
|
||||
await sendContactEmail(data);
|
||||
|
||||
setState({ success: true, error: false });
|
||||
} catch (error) {
|
||||
setState({ error: true, success: false });
|
||||
}
|
||||
});
|
||||
})}
|
||||
>
|
||||
<FormField
|
||||
name={'name'}
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
<Trans i18nKey={'marketing:contactName'} />
|
||||
</FormLabel>
|
||||
|
||||
<FormControl>
|
||||
<Input maxLength={200} {...field} />
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
name={'email'}
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
<Trans i18nKey={'marketing:contactEmail'} />
|
||||
</FormLabel>
|
||||
|
||||
<FormControl>
|
||||
<Input type={'email'} {...field} />
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
name={'message'}
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
<Trans i18nKey={'marketing:contactMessage'} />
|
||||
</FormLabel>
|
||||
|
||||
<FormControl>
|
||||
<Textarea
|
||||
className={'min-h-36'}
|
||||
maxLength={5000}
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
|
||||
<Button disabled={pending} type={'submit'}>
|
||||
<Trans i18nKey={'marketing:sendMessage'} />
|
||||
</Button>
|
||||
</form>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
||||
function SuccessAlert() {
|
||||
return (
|
||||
<Alert variant={'success'}>
|
||||
<AlertTitle>
|
||||
<Trans i18nKey={'marketing:contactSuccess'} />
|
||||
</AlertTitle>
|
||||
|
||||
<AlertDescription>
|
||||
<Trans i18nKey={'marketing:contactSuccessDescription'} />
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
function ErrorAlert() {
|
||||
return (
|
||||
<Alert variant={'destructive'}>
|
||||
<AlertTitle>
|
||||
<Trans i18nKey={'marketing:contactError'} />
|
||||
</AlertTitle>
|
||||
|
||||
<AlertDescription>
|
||||
<Trans i18nKey={'marketing:contactErrorDescription'} />
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const ContactEmailSchema = z.object({
|
||||
name: z.string().min(1).max(200),
|
||||
email: z.string().email(),
|
||||
message: z.string().min(1).max(5000),
|
||||
});
|
||||
@@ -0,0 +1,51 @@
|
||||
'use server';
|
||||
|
||||
import { z } from 'zod';
|
||||
|
||||
import { getMailer } from '@kit/mailers';
|
||||
import { enhanceAction } from '@kit/next/actions';
|
||||
|
||||
import { ContactEmailSchema } from '../contact-email.schema';
|
||||
|
||||
const contactEmail = z
|
||||
.string({
|
||||
description: `The email where you want to receive the contact form submissions.`,
|
||||
required_error:
|
||||
'Contact email is required. Please use the environment variable CONTACT_EMAIL.',
|
||||
})
|
||||
.parse(process.env.CONTACT_EMAIL);
|
||||
|
||||
const emailFrom = z
|
||||
.string({
|
||||
description: `The email sending address.`,
|
||||
required_error:
|
||||
'Sender email is required. Please use the environment variable EMAIL_SENDER.',
|
||||
})
|
||||
.parse(process.env.EMAIL_SENDER);
|
||||
|
||||
export const sendContactEmail = enhanceAction(
|
||||
async (data) => {
|
||||
const mailer = await getMailer();
|
||||
|
||||
await mailer.sendEmail({
|
||||
to: contactEmail,
|
||||
from: emailFrom,
|
||||
subject: 'Contact Form Submission',
|
||||
html: `
|
||||
<p>
|
||||
You have received a new contact form submission.
|
||||
</p>
|
||||
|
||||
<p>Name: ${data.name}</p>
|
||||
<p>Email: ${data.email}</p>
|
||||
<p>Message: ${data.message}</p>
|
||||
`,
|
||||
});
|
||||
|
||||
return {};
|
||||
},
|
||||
{
|
||||
schema: ContactEmailSchema,
|
||||
auth: false,
|
||||
},
|
||||
);
|
||||
@@ -1,4 +1,8 @@
|
||||
import { Heading } from '@kit/ui/heading';
|
||||
import { Trans } from '@kit/ui/trans';
|
||||
|
||||
import { SitePageHeader } from '~/(marketing)/_components/site-page-header';
|
||||
import { ContactForm } from '~/(marketing)/contact/_components/contact-form';
|
||||
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
|
||||
import { withI18n } from '~/lib/i18n/with-i18n';
|
||||
|
||||
@@ -15,8 +19,33 @@ async function ContactPage() {
|
||||
|
||||
return (
|
||||
<div className={'mt-8'}>
|
||||
<SitePageHeader
|
||||
title={t(`marketing:contact`)}
|
||||
subtitle={t(`marketing:contactDescription`)}
|
||||
/>
|
||||
|
||||
<div className={'container mx-auto'}>
|
||||
<SitePageHeader title={t(`marketing:contact`)} subtitle={``} />
|
||||
<div
|
||||
className={'flex flex-1 flex-col items-center justify-center py-12'}
|
||||
>
|
||||
<div
|
||||
className={
|
||||
'flex w-full max-w-lg flex-col space-y-4 rounded-lg border p-8'
|
||||
}
|
||||
>
|
||||
<div>
|
||||
<Heading level={3}>
|
||||
<Trans i18nKey={'marketing:contactHeading'} />
|
||||
</Heading>
|
||||
|
||||
<p className={'text-muted-foreground'}>
|
||||
<Trans i18nKey={'marketing:contactSubheading'} />
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<ContactForm />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -24,8 +24,11 @@ function getSiteUrls() {
|
||||
const urls = [
|
||||
'/',
|
||||
'/faq',
|
||||
'/blog',
|
||||
'/docs',
|
||||
'/pricing',
|
||||
'/contact',
|
||||
'/cookie-policy',
|
||||
'/terms-of-service',
|
||||
'/privacy-policy',
|
||||
];
|
||||
|
||||
Reference in New Issue
Block a user