generated from theodorusclarence/ts-nextjs-tailwind-starter
-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: create ContactForm with Formik and custom fields
- Loading branch information
Showing
9 changed files
with
987 additions
and
411 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 @@ | ||
!.storybook |
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
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,43 @@ | ||
import { Field, useField } from 'formik'; | ||
|
||
import clsxm from '@/lib/clsxm'; | ||
|
||
export interface InputProps { | ||
id: string; | ||
type?: 'text' | 'number' | 'email'; | ||
label: string; | ||
placeholder?: string; | ||
} | ||
|
||
export const Input = ({ | ||
id, | ||
type = 'text', | ||
label, | ||
placeholder, | ||
}: InputProps) => { | ||
const fieldProps = { id, name: id, type, label, placeholder }; | ||
const [_, meta] = useField(fieldProps); | ||
|
||
return ( | ||
<label htmlFor='firstName' className='mb-2 mt-6 flex flex-col font-bold'> | ||
{label} | ||
<Field | ||
placeholder={placeholder} | ||
name={id} | ||
id={id} | ||
type={type} | ||
className={clsxm( | ||
'my-2 rounded-md px-4 py-2 ring-1 dark:bg-transparent font-normal', | ||
{ | ||
'ring-red-600': meta.touched && meta.error, | ||
'ring-grey-400 dark:ring-slate-500': !meta.touched || !meta.error, | ||
}, | ||
)} | ||
/> | ||
|
||
<div className='font-sm font-normal text-red-600'> | ||
{meta.touched && meta.error} | ||
</div> | ||
</label> | ||
); | ||
}; |
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,37 @@ | ||
import { Field, useField } from 'formik'; | ||
|
||
import clsxm from '@/lib/clsxm'; | ||
|
||
export interface TextAreaProps { | ||
id: string; | ||
label: string; | ||
placeholder?: string; | ||
} | ||
|
||
export const TextArea = ({ id, label, placeholder }: TextAreaProps) => { | ||
const fieldProps = { id, name: id, label, placeholder }; | ||
const [_, meta] = useField(fieldProps); | ||
|
||
return ( | ||
<label htmlFor='firstName' className='mb-2 mt-6 flex flex-col font-bold'> | ||
{label} | ||
<Field | ||
as='textarea' | ||
placeholder={placeholder} | ||
name={id} | ||
id={id} | ||
className={clsxm( | ||
'my-2 rounded-md px-4 py-2 ring-1 dark:bg-transparent font-normal', | ||
{ | ||
'ring-red-600': meta.touched && meta.error, | ||
'ring-grey-400 dark:ring-slate-500': !meta.touched || !meta.error, | ||
}, | ||
)} | ||
rows={7} | ||
/> | ||
<div className='font-sm font-normal text-red-600'> | ||
{meta.touched && meta.error} | ||
</div> | ||
</label> | ||
); | ||
}; |
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,48 @@ | ||
import { Field, useField } from 'formik'; | ||
|
||
import clsxm from '@/lib/clsxm'; | ||
|
||
export interface SelectProps { | ||
id: string; | ||
label: string; | ||
options: Option[]; | ||
} | ||
|
||
export interface Option { | ||
key: string; | ||
value: string; | ||
} | ||
|
||
export const Select = ({ id, label, options }: SelectProps) => { | ||
const fieldProps = { label, id, name: id, options }; | ||
const [_, meta] = useField(fieldProps); | ||
|
||
return ( | ||
<label htmlFor={id} className='mb-2 mt-6 flex flex-col font-bold'> | ||
{label} | ||
<Field | ||
as='select' | ||
name={id} | ||
id={id} | ||
className={clsxm( | ||
'my-2 rounded-md px-4 py-2 ring-1 dark:bg-transparent font-normal', | ||
{ | ||
'ring-red-600': meta.touched && meta.error, | ||
'ring-grey-400 dark:ring-slate-500': !meta.touched || !meta.error, | ||
}, | ||
)} | ||
defaultValue='none' | ||
> | ||
<option value='none'>Please select</option> | ||
{options.map((option) => ( | ||
<option value={option.value} key={option.key}> | ||
{option.value} | ||
</option> | ||
))} | ||
</Field> | ||
<div className='font-sm font-normal text-red-600'> | ||
{meta.touched && meta.error} | ||
</div> | ||
</label> | ||
); | ||
}; |
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,162 @@ | ||
'use client'; | ||
|
||
import { Form, Formik } from 'formik'; | ||
import { useState } from 'react'; | ||
import * as Yup from 'yup'; | ||
|
||
import { Input } from '@/components/atoms/Input/Input'; | ||
import { Select } from '@/components/atoms/select/Select'; | ||
import { TextArea } from '@/components/atoms/TextArea/TextArea'; | ||
import Button from '@/components/buttons/Button'; | ||
|
||
const ContactForm = () => { | ||
const [success, setSuccess] = useState(false); | ||
const [error, setError] = useState(false); | ||
|
||
const subjects = [ | ||
{ | ||
key: 'dev', | ||
value: '👩🏻💻 I need a website / app developed', | ||
}, | ||
{ | ||
key: 'work', | ||
value: "🤝 Let's work together on something", | ||
}, | ||
{ | ||
key: 'recruiter', | ||
value: "👔 I'm a recruiter and want to hire you", | ||
}, | ||
{ | ||
key: 'feedback', | ||
value: "💡 I've got feedback on this website", | ||
}, | ||
{ | ||
key: 'problem', | ||
value: '🤕 Reporting a problem', | ||
}, | ||
{ | ||
key: 'general', | ||
value: '🙋 General inquiry', | ||
}, | ||
{ | ||
key: 'other', | ||
value: '❓ Other', | ||
}, | ||
]; | ||
|
||
const validationSchema = Yup.object({ | ||
name: Yup.string().required("Don't be a stranger :)"), | ||
email: Yup.string() | ||
.email( | ||
'Something seems off with the address you entered 🤔 Please double-check and try again.', | ||
) | ||
.required( | ||
"Required - otherwise it's kinda hard for me to reply to you 🤷♀️", | ||
), | ||
subject: Yup.mixed() | ||
.oneOf(subjects.map((subject) => subject.value)) | ||
.required( | ||
"Don't leave me hanging! I need a subject to know what's up 🧐", | ||
), | ||
message: Yup.string().required( | ||
'One does not simply submit a contact form without a message.', | ||
), | ||
}); | ||
|
||
const handleSubmit = async ( | ||
formValues: Record<string, string>, | ||
setSubmitting: (arg: boolean) => void, | ||
resetForm: () => void, | ||
) => { | ||
// eslint-disable-next-line no-console | ||
console.log(JSON.stringify(formValues, null, 2)); | ||
|
||
setError(false); | ||
setSuccess(false); | ||
|
||
const res = await fetch('/api/contact/send', { | ||
body: JSON.stringify(formValues), | ||
headers: { | ||
'Content-Type': 'application/json', | ||
}, | ||
method: 'POST', | ||
}); | ||
|
||
const { error } = await res.json(); | ||
|
||
if (error) { | ||
setError(true); | ||
setSubmitting(false); | ||
return; | ||
} | ||
|
||
setSubmitting(false); | ||
setSuccess(true); | ||
resetForm(); | ||
}; | ||
|
||
return ( | ||
<Formik | ||
initialValues={{ | ||
name: '', | ||
email: '', | ||
subject: '', | ||
message: '', | ||
}} | ||
validationSchema={validationSchema} | ||
onSubmit={(values, { setSubmitting, resetForm }) => { | ||
handleSubmit(values, setSubmitting, resetForm); | ||
}} | ||
> | ||
{({ isSubmitting }) => { | ||
return ( | ||
<Form role='form' className='mt-4'> | ||
{success && ( | ||
<div className='rounded-md bg-green-100 px-4 py-2 font-bold text-green-600 ring-1 ring-green-600'> | ||
Thanks for your message. I will get back to you as soon as | ||
possible. | ||
</div> | ||
)} | ||
{error && ( | ||
<div className='rounded-md bg-red-100 px-4 py-2 font-bold text-red-600 ring-1 ring-red-600'> | ||
Whoops, something went wrong on our side! 😟 Please try again - | ||
if the issue persists, try reaching out directly at | ||
marta_panc@me.com | ||
</div> | ||
)} | ||
|
||
<Input id='name' label='Name' placeholder='Bilbo Baggins' /> | ||
|
||
<Input | ||
id='company' | ||
label='Company' | ||
placeholder='CyberWizards Ltd.' | ||
/> | ||
|
||
<Input | ||
id='email' | ||
label='Email Address' | ||
placeholder='hr@hogwarts.edu' | ||
/> | ||
|
||
<Select id='subject' label='Subject' options={subjects} /> | ||
|
||
<TextArea | ||
id='message' | ||
label='Message' | ||
placeholder="What's on your mind? The email's the limit! 🪄✨" | ||
/> | ||
|
||
<div className='mt-6 flex justify-end'> | ||
<Button type='submit' disabled={isSubmitting} className='group'> | ||
{isSubmitting ? 'Working on it...' : 'Send message'} | ||
</Button> | ||
</div> | ||
</Form> | ||
); | ||
}} | ||
</Formik> | ||
); | ||
}; | ||
|
||
export default ContactForm; |
15 changes: 15 additions & 0 deletions
15
src/components/molecules/ContactForm/__stories__/ContactForm.stories.tsx
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,15 @@ | ||
import { Meta } from '@storybook/react'; | ||
|
||
import ContactForm from '@/components/molecules/ContactForm/ContactForm'; | ||
|
||
const meta: Meta<typeof ContactForm> = { | ||
title: 'ContactForm', | ||
component: ContactForm, | ||
tags: ['autodocs'], | ||
}; | ||
|
||
export default meta; | ||
|
||
export const SampleStory = () => { | ||
return <ContactForm />; | ||
}; |
Oops, something went wrong.