diff --git a/src/components/CreateBreweryPostForm.tsx b/src/components/CreateBreweryPostForm.tsx new file mode 100644 index 0000000..9f8123f --- /dev/null +++ b/src/components/CreateBreweryPostForm.tsx @@ -0,0 +1,284 @@ +import sendUploadBreweryImagesRequest from '@/requests/BreweryImage/sendUploadBreweryImageRequest'; +import sendCreateBreweryPostRequest from '@/requests/BreweryPost/sendCreateBreweryPostRequest'; +import CreateBreweryPostSchema from '@/services/BreweryPost/types/CreateBreweryPostSchema'; +import UploadImageValidationSchema from '@/services/types/ImageSchema/UploadImageValidationSchema'; +import createErrorToast from '@/util/createErrorToast'; +import { Tab } from '@headlessui/react'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { AddressAutofillRetrieveResponse } from '@mapbox/search-js-core'; +import dynamic from 'next/dynamic'; +import { useRouter } from 'next/router'; +import { FC, Fragment } from 'react'; + +import { + useForm, + SubmitHandler, + FieldError, + UseFormRegister, + FieldErrors, + UseFormSetValue, +} from 'react-hook-form'; +import toast from 'react-hot-toast'; +import { z } from 'zod'; +import FormError from './ui/forms/FormError'; +import FormInfo from './ui/forms/FormInfo'; +import FormLabel from './ui/forms/FormLabel'; +import FormSegment from './ui/forms/FormSegment'; +import FormTextArea from './ui/forms/FormTextArea'; +import FormTextInput from './ui/forms/FormTextInput'; +import Button from './ui/forms/Button'; + +const AddressAutofill = dynamic( + // @ts-ignore + () => import('@mapbox/search-js-react').then((mod) => mod.AddressAutofill), + { ssr: false }, +); +const CreateBreweryPostWithImagesSchema = CreateBreweryPostSchema.merge( + UploadImageValidationSchema, +); + +const InfoSection: FC<{ + register: UseFormRegister>; + errors: FieldErrors>; + isSubmitting: boolean; +}> = ({ register, errors, isSubmitting }) => { + return ( + <> + + Name + {errors.name?.message} + + + + + + Description + {errors.description?.message} + + + + + + Date Established + {errors.dateEstablished?.message} + + + + + + Images + {(errors.images as FieldError | undefined)?.message} + + + + + + ); +}; + +const LocationSection: FC<{ + register: UseFormRegister>; + errors: FieldErrors>; + isSubmitting: boolean; + setValue: UseFormSetValue>; +}> = ({ register, errors, isSubmitting, setValue }) => { + const onAutoCompleteChange = (address: string) => { + setValue('address', address); + }; + + const onAutoCompleteRetrieve = (address: AddressAutofillRetrieveResponse) => { + const { country, region, place } = address.features[0].properties as unknown as { + country?: string; + region?: string; + place?: string; + }; + + setValue('country', country); + setValue('region', region); + setValue('city', place!); + }; + + return ( + <> + + Address + {errors.address?.message} + + + + + + +
+
+ + City + {errors.city?.message} + + + + +
+
+ + Region + {errors.region?.message} + + + + +
+
+ + Country + {errors.country?.message} + + + + + + ); +}; + +const CreateBreweryPostForm: FC = () => { + const { + register, + handleSubmit, + reset, + setValue, + formState: { errors, isSubmitting }, + } = useForm>({ + resolver: zodResolver(CreateBreweryPostWithImagesSchema), + }); + + const router = useRouter(); + + const onSubmit: SubmitHandler< + z.infer + > = async (data) => { + const loadingToast = toast.loading('Creating brewery...'); + try { + if (!(data.images instanceof FileList)) { + return; + } + const breweryPost = await sendCreateBreweryPostRequest(data); + await sendUploadBreweryImagesRequest({ breweryPost, images: data.images }); + await router.push(`/breweries/${breweryPost.id}`); + toast.remove(loadingToast); + toast.success('Created brewery.'); + } catch (error) { + toast.remove(loadingToast); + createErrorToast(error); + reset(); + } + }; + + return ( +
{ + const fieldErrors = Object.keys(error).length; + + toast.error(`Form submission failed.`); + toast.error(`You have ${fieldErrors} errors in your form.`); + })} + className="form-control" + autoComplete="off" + > + + + + Information + + + Location + + + + + + + + + + + +
+ +
+
+ ); +}; + +export default CreateBreweryPostForm; diff --git a/src/pages/breweries/create.tsx b/src/pages/breweries/create.tsx index 7e32d09..a5894e3 100644 --- a/src/pages/breweries/create.tsx +++ b/src/pages/breweries/create.tsx @@ -1,115 +1,12 @@ -import Button from '@/components/ui/forms/Button'; -import FormError from '@/components/ui/forms/FormError'; -import FormInfo from '@/components/ui/forms/FormInfo'; -import FormLabel from '@/components/ui/forms/FormLabel'; import FormPageLayout from '@/components/ui/forms/FormPageLayout'; -import FormSegment from '@/components/ui/forms/FormSegment'; -import FormTextArea from '@/components/ui/forms/FormTextArea'; -import FormTextInput from '@/components/ui/forms/FormTextInput'; - -import createErrorToast from '@/util/createErrorToast'; import withPageAuthRequired from '@/util/withPageAuthRequired'; -import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema'; -import { zodResolver } from '@hookform/resolvers/zod'; -import type { AddressAutofillRetrieveResponse } from '@mapbox/search-js-core'; import { GetServerSideProps, NextPage } from 'next'; import Head from 'next/head'; -import { FieldError, SubmitHandler, useForm } from 'react-hook-form'; -import toast from 'react-hot-toast'; + import { FaBeer } from 'react-icons/fa'; -import { z } from 'zod'; -import dynamic from 'next/dynamic'; -import { useRouter } from 'next/router'; -import BreweryPostQueryResult from '@/services/BreweryPost/types/BreweryPostQueryResult'; -import CreateBreweryPostSchema from '@/services/BreweryPost/types/CreateBreweryPostSchema'; -import UploadImageValidationSchema from '@/services/types/ImageSchema/UploadImageValidationSchema'; -import sendUploadBreweryImagesRequest from '@/requests/BreweryImage/sendUploadBreweryImageRequest'; - -const AddressAutofill = dynamic( - () => import('@mapbox/search-js-react').then((mod) => mod.AddressAutofill), - { ssr: false }, -); -const CreateBreweryPostWithImagesSchema = CreateBreweryPostSchema.merge( - UploadImageValidationSchema, -); - -const sendCreateBreweryPostRequest = async ( - data: z.infer, -) => { - const response = await fetch('/api/breweries/create', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(data), - }); - - if (!response.ok) { - throw new Error(response.statusText); - } - - const json = await response.json(); - - const parsed = APIResponseValidationSchema.safeParse(json); - if (!parsed.success) { - throw new Error('API response parsing failed'); - } - - const parsedPayload = BreweryPostQueryResult.safeParse(parsed.data.payload); - if (!parsedPayload.success) { - throw new Error('API response payload parsing failed'); - } - - return parsedPayload.data; -}; +import CreateBreweryPostForm from '@/components/CreateBreweryPostForm'; const CreateBreweryPage: NextPage = () => { - const { - register, - handleSubmit, - reset, - setValue, - formState: { errors, isSubmitting }, - } = useForm>({ - resolver: zodResolver(CreateBreweryPostWithImagesSchema), - }); - - const router = useRouter(); - - const onSubmit: SubmitHandler< - z.infer - > = async (data) => { - const loadingToast = toast.loading('Creating brewery...'); - try { - if (!(data.images instanceof FileList)) { - return; - } - const breweryPost = await sendCreateBreweryPostRequest(data); - await sendUploadBreweryImagesRequest({ breweryPost, images: data.images }); - await router.push(`/breweries/${breweryPost.id}`); - toast.remove(loadingToast); - toast.success('Created brewery.'); - } catch (error) { - toast.remove(loadingToast); - reset(); - createErrorToast(error); - } - }; - - const onAutoCompleteChange = (address: string) => { - setValue('address', address); - }; - - const onAutoCompleteRetrieve = (address: AddressAutofillRetrieveResponse) => { - const { country, region, place } = address.features[0].properties as unknown as { - country?: string; - region?: string; - place?: string; - }; - - setValue('country', country); - setValue('region', region); - setValue('city', place!); - }; - return ( <> @@ -123,150 +20,7 @@ const CreateBreweryPage: NextPage = () => { headingText="Create Brewery" headingIcon={FaBeer} > -
-
- - Name - {errors.name?.message} - - - - - - Description - {errors.description?.message} - - - - - - Date Established - {errors.dateEstablished?.message} - - - - - - Images - - {(errors.images as FieldError | undefined)?.message} - - - - - -
- -
- - Address - {errors.address?.message} - - - - - - - -
-
- - City - {errors.city?.message} - - - - -
-
- - Region - {errors.region?.message} - - - - -
-
- - Country - {errors.country?.message} - - - - -
-
- -
-
+ diff --git a/src/requests/BreweryPost/sendCreateBreweryPostRequest.ts b/src/requests/BreweryPost/sendCreateBreweryPostRequest.ts new file mode 100644 index 0000000..192a8be --- /dev/null +++ b/src/requests/BreweryPost/sendCreateBreweryPostRequest.ts @@ -0,0 +1,34 @@ +import BreweryPostQueryResult from '@/services/BreweryPost/types/BreweryPostQueryResult'; +import CreateBreweryPostSchema from '@/services/BreweryPost/types/CreateBreweryPostSchema'; +import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema'; +import { z } from 'zod'; + +const sendCreateBreweryPostRequest = async ( + data: z.infer, +) => { + const response = await fetch('/api/breweries/create', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(data), + }); + + if (!response.ok) { + throw new Error(response.statusText); + } + + const json = await response.json(); + + const parsed = APIResponseValidationSchema.safeParse(json); + if (!parsed.success) { + throw new Error('API response parsing failed'); + } + + const parsedPayload = BreweryPostQueryResult.safeParse(parsed.data.payload); + if (!parsedPayload.success) { + throw new Error('API response payload parsing failed'); + } + + return parsedPayload.data; +}; + +export default sendCreateBreweryPostRequest;