Skip to content

Commit

Permalink
feat: create ContactForm with Formik and custom fields
Browse files Browse the repository at this point in the history
  • Loading branch information
martapanc committed Sep 16, 2023
1 parent 3acb598 commit f3ac559
Show file tree
Hide file tree
Showing 9 changed files with 987 additions and 411 deletions.
1 change: 1 addition & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
!.storybook
1 change: 1 addition & 0 deletions .storybook/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ const config: StorybookConfig = {
'@storybook/addon-links',
'@storybook/addon-essentials',
'@storybook/addon-interactions',
'@storybook/addon-mdx-gfm',
],
framework: {
name: '@storybook/nextjs',
Expand Down
21 changes: 12 additions & 9 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
"classnames": "^2.3.2",
"clsx": "^2.0.0",
"focus-trap-react": "^10.1.4",
"formik": "^2.4.4",
"framer-motion": "^10.16.1",
"graphql": "^16.8.0",
"i18next": "^23.4.5",
Expand All @@ -51,18 +52,20 @@
"strapi-flatten-graphql": "^0.1.0",
"styled-components": "^6.0.7",
"tailwind-merge": "^1.12.0",
"typed.js": "^2.0.16"
"typed.js": "^2.0.16",
"yup": "^1.2.0"
},
"devDependencies": {
"@commitlint/cli": "^17.7.1",
"@commitlint/config-conventional": "^17.7.0",
"@storybook/addon-essentials": "^7.3.2",
"@storybook/addon-interactions": "^7.3.2",
"@storybook/addon-links": "^7.3.2",
"@storybook/blocks": "^7.3.2",
"@storybook/nextjs": "^7.3.2",
"@storybook/react": "^7.3.2",
"@storybook/testing-library": "^0.2.0",
"@storybook/addon-essentials": "^7.4.2",
"@storybook/addon-interactions": "^7.4.2",
"@storybook/addon-links": "^7.4.2",
"@storybook/addon-mdx-gfm": "^7.4.2",
"@storybook/blocks": "^7.4.2",
"@storybook/nextjs": "^7.4.2",
"@storybook/react": "^7.4.2",
"@storybook/testing-library": "^0.2.1",
"@svgr/webpack": "^8.1.0",
"@tailwindcss/forms": "^0.5.3",
"@testing-library/jest-dom": "^5.16.5",
Expand All @@ -89,7 +92,7 @@
"postcss": "^8.4.23",
"prettier": "^3.0.2",
"prettier-plugin-tailwindcss": "^0.5.3",
"storybook": "^7.3.2",
"storybook": "^7.4.2",
"tailwindcss": "^3.3.2",
"typescript": "^5.2.2"
},
Expand Down
43 changes: 43 additions & 0 deletions src/components/atoms/Input/Input.tsx
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>
);
};
37 changes: 37 additions & 0 deletions src/components/atoms/TextArea/TextArea.tsx
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>
);
};
48 changes: 48 additions & 0 deletions src/components/atoms/select/Select.tsx
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>
);
};
162 changes: 162 additions & 0 deletions src/components/molecules/ContactForm/ContactForm.tsx
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;
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 />;
};
Loading

0 comments on commit f3ac559

Please sign in to comment.