aboutsummaryrefslogtreecommitdiffstats
path: root/src/components/organisms/forms/contact-form
diff options
context:
space:
mode:
authorArmand Philippot <git@armandphilippot.com>2023-09-22 19:34:01 +0200
committerArmand Philippot <git@armandphilippot.com>2023-10-24 12:23:48 +0200
commita6ff5eee45215effb3344cb5d631a27a7c0369aa (patch)
tree5051747acf72318b4fc5c18d603e3757fbefdfdb /src/components/organisms/forms/contact-form
parent651ea4fc992e77d2f36b3c68f8e7a70644246067 (diff)
refactor(components): rewrite form components
Diffstat (limited to 'src/components/organisms/forms/contact-form')
-rw-r--r--src/components/organisms/forms/contact-form/contact-form.module.scss15
-rw-r--r--src/components/organisms/forms/contact-form/contact-form.stories.tsx65
-rw-r--r--src/components/organisms/forms/contact-form/contact-form.test.tsx48
-rw-r--r--src/components/organisms/forms/contact-form/contact-form.tsx210
-rw-r--r--src/components/organisms/forms/contact-form/index.ts1
5 files changed, 339 insertions, 0 deletions
diff --git a/src/components/organisms/forms/contact-form/contact-form.module.scss b/src/components/organisms/forms/contact-form/contact-form.module.scss
new file mode 100644
index 0000000..c106fb1
--- /dev/null
+++ b/src/components/organisms/forms/contact-form/contact-form.module.scss
@@ -0,0 +1,15 @@
+.form {
+ display: flex;
+ flex-flow: column wrap;
+ gap: var(--spacing-xs);
+ max-width: 45ch;
+}
+
+.field {
+ width: 100%;
+}
+
+.button {
+ display: block;
+ margin: var(--spacing-sm) auto 0;
+}
diff --git a/src/components/organisms/forms/contact-form/contact-form.stories.tsx b/src/components/organisms/forms/contact-form/contact-form.stories.tsx
new file mode 100644
index 0000000..4df3db0
--- /dev/null
+++ b/src/components/organisms/forms/contact-form/contact-form.stories.tsx
@@ -0,0 +1,65 @@
+import { ComponentMeta, ComponentStory } from '@storybook/react';
+import { ContactForm } from './contact-form';
+
+/**
+ * ContactForm - Storybook Meta
+ */
+export default {
+ title: 'Organisms/Forms',
+ component: ContactForm,
+ argTypes: {
+ className: {
+ control: {
+ type: 'text',
+ },
+ description: 'Set additional classnames to the form wrapper.',
+ table: {
+ category: 'Styles',
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ Notice: {
+ control: {
+ type: null,
+ },
+ description: 'A component to display a success or error message.',
+ table: {
+ category: 'Options',
+ },
+ type: {
+ name: 'function',
+ required: false,
+ },
+ },
+ sendMail: {
+ control: {
+ type: null,
+ },
+ description: 'A callback function to process the contact form data.',
+ table: {
+ category: 'Events',
+ },
+ type: {
+ name: 'function',
+ required: true,
+ },
+ },
+ },
+} as ComponentMeta<typeof ContactForm>;
+
+const Template: ComponentStory<typeof ContactForm> = (args) => (
+ <ContactForm {...args} />
+);
+
+/**
+ * Forms Stories - Contact
+ */
+export const Contact = Template.bind({});
+Contact.args = {
+ sendMail: async (_data, reset: () => void) => {
+ reset();
+ },
+};
diff --git a/src/components/organisms/forms/contact-form/contact-form.test.tsx b/src/components/organisms/forms/contact-form/contact-form.test.tsx
new file mode 100644
index 0000000..59d69fa
--- /dev/null
+++ b/src/components/organisms/forms/contact-form/contact-form.test.tsx
@@ -0,0 +1,48 @@
+import { render, screen } from '../../../../../tests/utils';
+import { ContactForm } from './contact-form';
+
+const props = {
+ sendMail: async () => {
+ /** Do nothing. */
+ },
+};
+
+describe('ContactForm', () => {
+ it('renders a contact form', () => {
+ render(<ContactForm {...props} />);
+ expect(
+ screen.getByRole('form', { name: 'Contact form' })
+ ).toBeInTheDocument();
+ });
+
+ it('renders a name field', () => {
+ render(<ContactForm {...props} />);
+ expect(screen.getByRole('textbox', { name: /^Name:/ })).toBeInTheDocument();
+ });
+
+ it('renders an email field', () => {
+ render(<ContactForm {...props} />);
+ expect(
+ screen.getByRole('textbox', { name: /^Email:/ })
+ ).toBeInTheDocument();
+ });
+
+ it('renders an object field', () => {
+ render(<ContactForm {...props} />);
+ expect(
+ screen.getByRole('textbox', { name: /^Object:/ })
+ ).toBeInTheDocument();
+ });
+
+ it('renders a message field', () => {
+ render(<ContactForm {...props} />);
+ expect(
+ screen.getByRole('textbox', { name: /^Message:/ })
+ ).toBeInTheDocument();
+ });
+
+ it('renders a submit button', () => {
+ render(<ContactForm {...props} />);
+ expect(screen.getByRole('button', { name: /^Send/ })).toBeInTheDocument();
+ });
+});
diff --git a/src/components/organisms/forms/contact-form/contact-form.tsx b/src/components/organisms/forms/contact-form/contact-form.tsx
new file mode 100644
index 0000000..6208b94
--- /dev/null
+++ b/src/components/organisms/forms/contact-form/contact-form.tsx
@@ -0,0 +1,210 @@
+import { ChangeEvent, FC, FormEvent, ReactNode, useState } from 'react';
+import { useIntl } from 'react-intl';
+import { Button, Form, Input, Label, Spinner, TextArea } from '../../../atoms';
+import { LabelledField } from '../../../molecules';
+import styles from './contact-form.module.scss';
+
+export type ContactFormData = {
+ email: string;
+ message: string;
+ name: string;
+ object: string;
+};
+
+export type ContactFormProps = {
+ /**
+ * Set additional classnames to the form wrapper.
+ */
+ className?: string;
+ /**
+ * Pass a component to print a success/error message.
+ */
+ Notice?: ReactNode;
+ /**
+ * A callback function to send mail.
+ */
+ sendMail: (data: ContactFormData, reset: () => void) => Promise<void>;
+};
+
+/**
+ * ContactForm component
+ *
+ * Render a contact form.
+ */
+export const ContactForm: FC<ContactFormProps> = ({
+ className = '',
+ Notice,
+ sendMail,
+}) => {
+ const formClass = `${styles.form} ${className}`;
+ const intl = useIntl();
+ const emptyForm: ContactFormData = {
+ email: '',
+ message: '',
+ name: '',
+ object: '',
+ };
+ const [data, setData] = useState(emptyForm);
+ const [isSubmitting, setIsSubmitting] = useState<boolean>(false);
+
+ /**
+ * Reset all the form fields.
+ */
+ const resetForm = () => {
+ setData(emptyForm);
+ setIsSubmitting(false);
+ };
+
+ const updateForm = (
+ e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
+ ) => {
+ switch (e.target.name) {
+ case 'email':
+ setData((prevData) => {
+ return { ...prevData, email: e.target.value };
+ });
+ break;
+ case 'message':
+ setData((prevData) => {
+ return { ...prevData, message: e.target.value };
+ });
+ break;
+ case 'name':
+ setData((prevData) => {
+ return { ...prevData, name: e.target.value };
+ });
+ break;
+ case 'object':
+ setData((prevData) => {
+ return { ...prevData, object: e.target.value };
+ });
+ break;
+ default:
+ break;
+ }
+ };
+
+ const formName = intl.formatMessage({
+ defaultMessage: 'Contact form',
+ description: 'ContactForm: form accessible name',
+ id: 'HFdzae',
+ });
+
+ const nameLabel = intl.formatMessage({
+ defaultMessage: 'Name:',
+ description: 'ContactForm: name label',
+ id: '1dCuCx',
+ });
+
+ const emailLabel = intl.formatMessage({
+ defaultMessage: 'Email:',
+ description: 'ContactForm: email label',
+ id: 'w4B5PA',
+ });
+
+ const objectLabel = intl.formatMessage({
+ defaultMessage: 'Object:',
+ description: 'ContactForm: object label',
+ id: 's8/tyz',
+ });
+
+ const messageLabel = intl.formatMessage({
+ defaultMessage: 'Message:',
+ description: 'ContactForm: message label',
+ id: 'yN5P+m',
+ });
+
+ const submitHandler = async (e: FormEvent) => {
+ e.preventDefault();
+ setIsSubmitting(true);
+ sendMail(data, resetForm).then(() => setIsSubmitting(false));
+ };
+
+ return (
+ <Form aria-label={formName} className={formClass} onSubmit={submitHandler}>
+ <LabelledField
+ className={styles.field}
+ field={
+ <Input
+ id="contact-name"
+ isRequired
+ name="name"
+ onChange={updateForm}
+ type="text"
+ value={data.name}
+ />
+ }
+ label={
+ <Label htmlFor="contact-name" isRequired>
+ {nameLabel}
+ </Label>
+ }
+ />
+ <LabelledField
+ className={styles.field}
+ field={
+ <Input
+ id="contact-email"
+ isRequired
+ name="email"
+ onChange={updateForm}
+ type="email"
+ value={data.email}
+ />
+ }
+ label={
+ <Label htmlFor="contact-email" isRequired>
+ {emailLabel}
+ </Label>
+ }
+ />
+ <LabelledField
+ className={styles.field}
+ field={
+ <Input
+ id="contact-object"
+ name="object"
+ onChange={updateForm}
+ type="text"
+ value={data.object}
+ />
+ }
+ label={<Label htmlFor="contact-object">{objectLabel}</Label>}
+ />
+ <LabelledField
+ className={styles.field}
+ field={
+ <TextArea
+ id="contact-message"
+ isRequired
+ name="message"
+ onChange={updateForm}
+ value={data.message}
+ />
+ }
+ label={
+ <Label htmlFor="contact-message" isRequired>
+ {messageLabel}
+ </Label>
+ }
+ />
+ <Button type="submit" kind="primary" className={styles.button}>
+ {intl.formatMessage({
+ defaultMessage: 'Send',
+ description: 'ContactForm: send button',
+ id: 'VkAnvv',
+ })}
+ </Button>
+ {isSubmitting && (
+ <Spinner
+ message={intl.formatMessage({
+ defaultMessage: 'Sending mail...',
+ description: 'ContactForm: spinner message on submit',
+ id: 'xaqaYQ',
+ })}
+ />
+ )}
+ {Notice}
+ </Form>
+ );
+};
diff --git a/src/components/organisms/forms/contact-form/index.ts b/src/components/organisms/forms/contact-form/index.ts
new file mode 100644
index 0000000..c72af3d
--- /dev/null
+++ b/src/components/organisms/forms/contact-form/index.ts
@@ -0,0 +1 @@
+export * from './contact-form';