Skip to content

Commit

Permalink
add form error representation #9
Browse files Browse the repository at this point in the history
  • Loading branch information
ukorvl committed Feb 23, 2024
1 parent 53a8e59 commit d29c35f
Show file tree
Hide file tree
Showing 12 changed files with 135 additions and 31 deletions.
2 changes: 1 addition & 1 deletion app/contact/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export default function Contact() {
<AppearInViewport
className={titleCn}
variants={titleVariants}
transition={{duration: TransitionDuration.VERY_LONG, type: 'spring', bounce: 0.5}}
transition={{duration: TransitionDuration.LONG, type: 'spring', bounce: 0}}
>
CONTACT US
</AppearInViewport>
Expand Down
51 changes: 41 additions & 10 deletions components/pages/Contact/DynamicFormBg/DynamicFormBg.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,56 @@
import React from 'react';
'use client';

import React, {ReactNode, useCallback, useState} from 'react';
import clsx from 'clsx';
import {FormStepsContext} from './FormStepsContext';

/**
*
* Props.
*/
type DynamicFormBgProps = {
steps: number;
maxSteps: number;
children: ReactNode;
};

/**
* @param {DynamicFormBgProps} props Props.
* @returns {React.JSX.Element} Background for the contact form.
*/
export const DynamicFormBg = ({steps, maxSteps}: DynamicFormBgProps) => {
const DynamicFormBg = ({children}: DynamicFormBgProps) => {
const containerCn = 'relative';
const bgTextCn = clsx(
'absolute left-0 top-0 bg-gradient-to-r text-[500px] font-extrabold opacity-40',
'from-accent0 to-accent3 bg-clip-text text-transparent',
);
const maxSteps = 3;
const minSteps = 0;
const [steps, setSteps] = useState(0);
// eslint-disable-next-line jsdoc/require-jsdoc
const stepsSetter = useCallback(
(steps: number) => {
if (steps > maxSteps) {
setSteps(maxSteps);
}
if (steps < minSteps) {
setSteps(minSteps);
}
setSteps(steps);
},
[setSteps],
);

return (
<div className="bg-gray-100 py-16">
<div className="container mx-auto">
<div className="flex flex-col items-center">
<h2 className="mb-4 text-center text-4xl font-bold text-gray-900">SV</h2>
</div>
<div className={containerCn}>
<FormStepsContext.Provider value={{steps, setSteps: stepsSetter}}>
{children}
</FormStepsContext.Provider>
<div
aria-hidden="true"
className={bgTextCn}
>
SV {steps}
</div>
</div>
);
};

export default DynamicFormBg;
19 changes: 19 additions & 0 deletions components/pages/Contact/DynamicFormBg/FormStepsContext.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import {createContext} from 'react';

/**
* Type of the context.
*/
type FormStepsContextType = {
steps: number;
setSteps: (steps: number) => void;
};

const FormStepsContext = createContext({
steps: 0,
// eslint-disable-next-line jsdoc/require-jsdoc
setSteps: () => {},
} as FormStepsContextType);

FormStepsContext.displayName = 'FormStepsContext';

export {FormStepsContext};
21 changes: 13 additions & 8 deletions components/pages/Contact/Form/Form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import FormField from './FormField';
*/
type FormProps = {
onSubmit?: () => void;
onErrorSubmit?: () => void;
};

const initialValues = {
Expand All @@ -26,15 +27,19 @@ const {NEXT_PUBLIC_FORMSPREE_ID} = env;
* @param {FormProps} props Props.
* @returns React element.
*/
export default function Form({onSubmit: onSuccessSubmit}: FormProps) {
export default function Form({onSubmit: onSuccessSubmit, onErrorSubmit}: FormProps) {
const mottoCn = 'text-center text-gray-300 mt-16';
const btnCn = 'mt-8';
const [{status}, submit] = useFormspree<ContactFormData>(NEXT_PUBLIC_FORMSPREE_ID!);
// eslint-disable-next-line jsdoc/require-jsdoc
const onSubmit = async (values: ContactFormData) => {
await submit(values);
resetForm();
onSuccessSubmit?.();
try {
await submit(values);
resetForm();
onSuccessSubmit?.();
} catch {
onErrorSubmit?.();
}
};

const formikData = useFormik<ContactFormData>({
Expand All @@ -45,8 +50,8 @@ export default function Form({onSubmit: onSuccessSubmit}: FormProps) {
const {handleSubmit, isSubmitting, isValid, dirty, resetForm} = formikData;

return (
<AppearInViewport transition={{delay: 1, duration: 1.5}}>
<FormikContext.Provider value={formikData}>
<FormikContext.Provider value={formikData}>
<AppearInViewport transition={{delay: 1, staggerChildren: 0.2, delayChildren: 0.5}}>
<form onSubmit={handleSubmit}>
<FormField name="name" />
<FormField name="message" />
Expand All @@ -63,7 +68,7 @@ export default function Form({onSubmit: onSuccessSubmit}: FormProps) {
</Button>
{status === 'error' && <div>Something went wrong. Please try again.</div>}
</form>
</FormikContext.Provider>
</AppearInViewport>
</AppearInViewport>
</FormikContext.Provider>
);
}
8 changes: 8 additions & 0 deletions components/pages/Contact/FormWrapper/ErrorSubmitForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/**
* @returns React component.
*/
function ErrorSubmitForm() {
return <div>There was an error submitting the form. Please try again.</div>;
}

export default ErrorSubmitForm;
29 changes: 25 additions & 4 deletions components/pages/Contact/FormWrapper/FormWrapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,38 @@ import {useState} from 'react';
import clsx from 'clsx';
import Form from '../Form/Form';
import SubmittedForm from './SubmittedForm';
import ErrorSubmitForm from './ErrorSubmitForm';
import DynamicFormBg from '../DynamicFormBg/DynamicFormBg';

/**
* @returns React element.
*/
export default function FormWrapper() {
const [isSubmitted, setIsSubmitted] = useState(false);
const wrapperCn = clsx('w-96', 'sm:w-100', 'mx-auto', 'mt-8');
const [isError, setIsError] = useState(false);
const wrapperCn = clsx('w-96', 'sm:w-100', 'mx-auto', 'mt-8', 'z-10');

// eslint-disable-next-line jsdoc/require-jsdoc
const render = () => {
if (isError) {
return <ErrorSubmitForm />;
}

if (isSubmitted) {
return <SubmittedForm />;
}

return (
<Form
onErrorSubmit={() => setIsError(true)}
onSubmit={() => setIsSubmitted(true)}
/>
);
};

return (
<div className={wrapperCn}>
{isSubmitted ? <SubmittedForm /> : <Form onSubmit={() => setIsSubmitted(true)} />}
</div>
<DynamicFormBg>
<div className={wrapperCn}>{render()}</div>
</DynamicFormBg>
);
}
2 changes: 2 additions & 0 deletions components/shared/Menu/Menu.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
'use client';

import {ReactNode, useState} from 'react';
import FocusTrap from 'focus-trap-react';
import useDisableBodyScroll from '@/lib/shared/useDisableBodyScroll';
Expand Down
4 changes: 2 additions & 2 deletions components/shared/Menu/MenuDynamicBg.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {useContext} from 'react';
import clsx from 'clsx';
import {m, AnimatePresence} from 'framer-motion';
import {useContextSafeSafe} from '@/utils/useContextSafe';
import {MenuContext} from './MenuContext';
import ImageWrapper from '../ImageWrapper/ImageWrapper';

Expand All @@ -11,7 +11,7 @@ const dynamicBgCn = clsx('absolute', 'top-0', 'left-0', 'w-full', 'h-full', 'z-3
* @returns React component.
*/
export default function MenuDynamicBg() {
const {menuBg} = useContext(MenuContext);
const {menuBg} = useContextSafeSafe(MenuContext);

return (
<AnimatePresence mode="popLayout">
Expand Down
5 changes: 3 additions & 2 deletions components/shared/Menu/MenuItem.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import {m} from 'framer-motion';
import {ReactNode, forwardRef, useContext, useLayoutEffect} from 'react';
import {ReactNode, forwardRef, useLayoutEffect} from 'react';
import clsx from 'clsx';
import {twMerge} from 'tailwind-merge';
import {useHover} from '@/lib/shared/useHover';
import {useAssignRefs} from '@/lib/shared/useAssignRefs';
import {useContextSafeSafe} from '@/utils/useContextSafe';
import {MenuContext} from './MenuContext';

/**
Expand Down Expand Up @@ -42,7 +43,7 @@ const MenuItem = forwardRef<HTMLLIElement, MenuItemProps>(function MenuItem(
{children, className, bgImgPath},
forwardedRef,
) {
const {setIsOpen, setMenuBg} = useContext(MenuContext);
const {setIsOpen, setMenuBg} = useContextSafeSafe(MenuContext);
const [r, isHover] = useHover();
const ref = useAssignRefs(r, forwardedRef);

Expand Down
5 changes: 3 additions & 2 deletions components/shared/Menu/MenuList.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
'use client';

import {ReactNode, useCallback, useContext, useEffect} from 'react';
import {ReactNode, useCallback, useEffect} from 'react';
import {m} from 'framer-motion';
import {useHotkeys} from 'react-hotkeys-hook';
import clsx from 'clsx';
import useWindowDimensions from '@/lib/shared/useWindowDimensions';
import {useContextSafeSafe} from '@/utils/useContextSafe';
import {MenuContext} from './MenuContext';
import {MenuPosition} from './MenuPosition';
import menuButtonSize from './menuButtonSize';
Expand Down Expand Up @@ -59,7 +60,7 @@ const navCn = clsx(
* @returns React component.
*/
export default function MenuList({children}: MenuListProps) {
const {isOpen, setIsOpen, position} = useContext(MenuContext);
const {isOpen, setIsOpen, position} = useContextSafeSafe(MenuContext);
const close = useCallback(() => setIsOpen(false), [setIsOpen]);
const {height, width} = useWindowDimensions();
const bodyHasOverflow = height ? height < document.body.clientHeight : undefined;
Expand Down
5 changes: 3 additions & 2 deletions components/shared/Menu/MenuToggle.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {forwardRef, memo, useContext} from 'react';
import {forwardRef, memo} from 'react';
import clsx from 'clsx';
import {useContextSafeSafe} from '@/utils/useContextSafe';
import {MenuContext} from './MenuContext';
import menuButtonSize from './menuButtonSize';
import Button from '../Button/Button';
Expand All @@ -25,7 +26,7 @@ const MenuToggle = forwardRef<HTMLButtonElement, MenuToggleProps>(function MenuT
{onToggle},
ref,
) {
const {setIsOpen, isOpen, position} = useContext(MenuContext);
const {setIsOpen, isOpen, position} = useContextSafeSafe(MenuContext);
// eslint-disable-next-line jsdoc/require-jsdoc
const onClick = () => {
setIsOpen(!isOpen);
Expand Down
15 changes: 15 additions & 0 deletions utils/useContextSafe.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import React from 'react';

/**
* @param context - React context.
* @returns Context value.
*/
export const useContextSafeSafe = <T>(context: React.Context<T>) => {
const contextValue = React.useContext(context);

if (contextValue === undefined) {
throw new Error(context.displayName + 'must be used within a Provider with a value');
}

return contextValue;
};

0 comments on commit d29c35f

Please sign in to comment.