-
Notifications
You must be signed in to change notification settings - Fork 7
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add contact form in contact section
- Loading branch information
1 parent
729f791
commit f636e6e
Showing
18 changed files
with
1,225 additions
and
14 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
RESEND_API_KEY=YOUR_API_KEY |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
import { Resend } from 'resend'; | ||
import { NextResponse } from 'next/server'; | ||
|
||
import { ContactEmailTemplate } from '@/components/contact-email-template'; | ||
|
||
export const runtime = 'edge'; | ||
export const dynamic = 'force-dynamic'; | ||
|
||
const resend = new Resend(process.env.RESEND_API_KEY); | ||
|
||
export async function POST(request: Request) { | ||
const { firstName, lastName, email, message } = await request.json(); | ||
|
||
try { | ||
const { data, error } = await resend.emails.send({ | ||
from: '[email protected]', | ||
to: '[email protected]', | ||
subject: 'Message from contact form', | ||
react: ContactEmailTemplate({ | ||
firstName, | ||
lastName, | ||
email, | ||
message | ||
}) | ||
}); | ||
|
||
if (error) { | ||
return NextResponse.json({ | ||
status: 500, | ||
body: { message: 'Error sending email' } | ||
}); | ||
} | ||
|
||
return NextResponse.json({ | ||
status: 200, | ||
body: { message: data } | ||
}); | ||
} catch (error) { | ||
return NextResponse.json({ | ||
status: 500, | ||
body: { message: error } | ||
}); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
import { Section } from '@/components/ui/section' | ||
import { Contact } from '@/components/contact' | ||
|
||
export default function CareerPage() { | ||
return ( | ||
<div className='flex flex-col flex-1'> | ||
<Section id='contact' className='pb-24'> | ||
<Contact /> | ||
</Section> | ||
</div> | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
interface ContactEmailTemplateProps { | ||
firstName: string; | ||
lastName: string; | ||
email: string; | ||
message: string; | ||
}; | ||
|
||
export function ContactEmailTemplate({ | ||
firstName, | ||
lastName, | ||
email, | ||
message | ||
}: ContactEmailTemplateProps) { | ||
return ( | ||
<div className='bg-white dark:bg-gray-950 rounded-lg shadow-lg overflow-hidden'> | ||
<header className='bg-gray-100 dark:bg-gray-800 px-6 py-4 flex items-center justify-between'> | ||
<div className='flex items-center gap-2'> | ||
<span className='text-lg font-semibold'>Portfolio | Emanuel Peire</span> | ||
</div> | ||
</header> | ||
<div className='p-6 space-y-4'> | ||
<div className='flex items-center gap-4'> | ||
<p className='text-lg font-medium'>{firstName} {lastName}</p> | ||
<p className='text-gray-500 dark:text-gray-400'>{email}</p> | ||
</div> | ||
<div className='text-gray-700 dark:text-gray-300 whitespace-pre-wrap'> | ||
{message} | ||
</div> | ||
</div> | ||
</div> | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,247 @@ | ||
'use client' | ||
|
||
import { useEffect } from 'react'; | ||
import { useForm } from 'react-hook-form'; | ||
import { zodResolver } from '@hookform/resolvers/zod'; | ||
|
||
import { Button } from '@/components/ui/button'; | ||
import { | ||
Card, | ||
CardContent, | ||
CardDescription, | ||
CardHeader | ||
} from '@/components/ui/card'; | ||
import { | ||
Form, | ||
FormControl, | ||
FormField, | ||
FormItem, | ||
FormLabel, | ||
FormMessage | ||
} from '@/components/ui/form'; | ||
import { Input } from '@/components/ui/input'; | ||
import { Textarea } from '@/components/ui/textarea'; | ||
import { useToast } from '@/components/ui/use-toast'; | ||
import { Loader2 } from 'lucide-react' | ||
|
||
import { getFormSchema, FormValues } from '@/lib/validation'; | ||
|
||
export function ContactForm() { | ||
const { toast } = useToast(); | ||
const formSchema = getFormSchema(); | ||
|
||
const form = useForm<FormValues>({ | ||
resolver: zodResolver(formSchema), | ||
mode: 'onChange', | ||
defaultValues: { | ||
firstName: '', | ||
lastName: '', | ||
email: '', | ||
message: '', | ||
honeypot: '', | ||
} | ||
}); | ||
|
||
const { | ||
handleSubmit, | ||
formState, | ||
control, | ||
watch, | ||
setError, | ||
clearErrors | ||
} = form; | ||
const { isSubmitting } = formState; | ||
|
||
/* Watches for changes in the form fields and checks if the message field contains any URLs. | ||
If a URL is found, it sets an error message for the message field. */ | ||
useEffect(() => { | ||
const subscription = watch((value, { name }) => { | ||
if (name === 'message' && /http|www|href/.test(value.message ?? '')) { | ||
setError('message', { | ||
type: 'manual', | ||
message: 'Message must not contain URLs', | ||
}); | ||
} else { | ||
clearErrors('message'); | ||
} | ||
}); | ||
return () => subscription.unsubscribe(); | ||
}, [watch, setError, clearErrors]); | ||
|
||
async function onSubmit(data: FormValues) { | ||
if (data.honeypot) { | ||
toast({ | ||
title: 'Spam detected', | ||
description: 'Please, fill out the form correctly and without spam. Thanks!', | ||
variant: 'destructive', | ||
}); | ||
return; | ||
} | ||
|
||
const formData = new FormData(); | ||
Object.entries(data).forEach(([key, value]) => { | ||
if (value) { | ||
formData.append(key, value); | ||
} | ||
}); | ||
|
||
try { | ||
const response = await fetch('/api/send', { | ||
method: 'POST', | ||
body: JSON.stringify({ | ||
firstName: data.firstName, | ||
lastName: data.lastName, | ||
email: data.email, | ||
message: data.message, | ||
}), | ||
}); | ||
|
||
const result = await response.json(); | ||
toast({ | ||
title: 'Email sent', | ||
description: 'I will get back to you as soon as possible!', | ||
variant: 'default', | ||
}); | ||
form.reset(); | ||
} catch (error) { | ||
toast({ | ||
title: 'Error', | ||
description: 'There was an error while submitting the email. Please try again later.', | ||
variant: 'destructive', | ||
}); | ||
} | ||
} | ||
|
||
return ( | ||
<div className='flex justify-center items-center'> | ||
<Card className='w-full max-w-md'> | ||
<CardHeader> | ||
<CardDescription className='font-mono text-center'> | ||
Please, fill out the form below and I'll get back to you as soon as possible. | ||
</CardDescription> | ||
</CardHeader> | ||
<CardContent> | ||
<Form {...form}> | ||
<form noValidate onSubmit={handleSubmit(onSubmit)}> | ||
<div className='space-y-4'> | ||
<div className='grid grid-cols-2 gap-4'> | ||
<div className='space-y-2'> | ||
<FormField | ||
control={control} | ||
name='firstName' | ||
render={({ field }) => ( | ||
<FormItem> | ||
<FormLabel> | ||
First Name | ||
</FormLabel> | ||
<FormControl> | ||
<Input | ||
{...field} | ||
id='first-name' | ||
placeholder='Your first name' | ||
/> | ||
</FormControl> | ||
<FormMessage /> | ||
</FormItem> | ||
)} | ||
/> | ||
</div> | ||
<div className='space-y-2'> | ||
<FormField | ||
control={control} | ||
name='lastName' | ||
render={({ field }) => ( | ||
<FormItem> | ||
<FormLabel> | ||
Last Name | ||
</FormLabel> | ||
<FormControl> | ||
<Input | ||
{...field} | ||
id='last-name' | ||
placeholder='Your last name' | ||
/> | ||
</FormControl> | ||
<FormMessage /> | ||
</FormItem> | ||
)} | ||
/> | ||
</div> | ||
</div> | ||
<div className='space-y-2'> | ||
<FormField | ||
control={control} | ||
name='email' | ||
render={({ field }) => ( | ||
<FormItem> | ||
<FormLabel> | ||
</FormLabel> | ||
<FormControl> | ||
<Input | ||
{...field} | ||
id='email' | ||
placeholder='Your email address' | ||
/> | ||
</FormControl> | ||
<FormMessage /> | ||
</FormItem> | ||
)} | ||
/> | ||
</div> | ||
<div className='space-y-2'> | ||
<FormField | ||
control={control} | ||
name='message' | ||
render={({ field }) => ( | ||
<FormItem> | ||
<FormLabel> | ||
Message | ||
</FormLabel> | ||
<FormControl> | ||
<Textarea | ||
{...field} | ||
id='message' | ||
placeholder='Enter your message here...' | ||
/> | ||
</FormControl> | ||
<FormMessage /> | ||
</FormItem> | ||
)} | ||
/> | ||
</div> | ||
{/* Honeypot Field */} | ||
<div style={{ display: 'none' }}> | ||
<FormField | ||
control={control} | ||
name='honeypot' | ||
render={({ field }) => ( | ||
<FormItem> | ||
<Input | ||
{...field} | ||
id='honeypot' | ||
tabIndex={-1} | ||
autoComplete='off' | ||
/> | ||
</FormItem> | ||
)} | ||
/> | ||
</div> | ||
</div> | ||
<Button | ||
type='submit' | ||
disabled={isSubmitting} | ||
className='w-full mt-6' | ||
> | ||
{isSubmitting && ( | ||
<Loader2 className='mr-2 h-4 w-4 animate-spin' /> | ||
)} | ||
Submit | ||
</Button> | ||
</form> | ||
</Form> | ||
</CardContent> | ||
</Card> | ||
</div> | ||
) | ||
} |
Oops, something went wrong.