Skip to content

Commit

Permalink
Merge branch 'main' of github.com:maurer2/run-types-run
Browse files Browse the repository at this point in the history
  • Loading branch information
maurer2 committed Aug 23, 2024
2 parents 9924502 + 823b1df commit 3e39518
Show file tree
Hide file tree
Showing 5 changed files with 168 additions and 57 deletions.
57 changes: 49 additions & 8 deletions src/app/actions/handleFormValuesSubmit/handleFormValuesSubmit.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,61 @@
'use server';

import { redirect } from 'next/navigation';
import type { FieldError, FieldErrors, FieldValues } from 'react-hook-form';

import type { FormValues } from '../../../types/pizza';
import { redirect } from 'next/navigation';
import z from 'zod';

import { pizzaFormValidationSchema } from '../../../schema/pizza/validation';

export async function handleFormValuesSubmit(formValues: FormValues) {
const formValueParsingResult = pizzaFormValidationSchema.safeParse(formValues);
async function getMaxAvailableAmount(): Promise<number> {
return new Promise((resolve) => {
setTimeout(() => resolve(5), 1000);
});
}

// returns undefined when redirect happens
export async function handleFormValuesSubmit(formValues: FieldValues): Promise<FieldErrors | undefined> {
// debug
// const formValuesTest = structuredClone(formValues);
// formValuesTest.amount = 'test';

console.log(`Form values received: ${JSON.stringify(formValues, null, 4)}`);
const pizzaFormValidationSchemaAugmented = pizzaFormValidationSchema.superRefine(
async ({ amount }, ctx) => {
const maxAvailableAmount = await getMaxAvailableAmount();

if (amount > maxAvailableAmount) {
ctx.addIssue({
code: z.ZodIssueCode.too_big,
inclusive: true, // ??
maximum: maxAvailableAmount,
message: `Amount of ${amount} exceeds currently available amount of ${maxAvailableAmount}`,
path: ['amount'],
type: 'number',
});
}

return true;
},
);

const formValueParsingResult = await pizzaFormValidationSchemaAugmented.safeParseAsync(
formValues,
);
// const formValueParsingResult = pizzaFormValidationSchema.safeParse(formValues);

if (!formValueParsingResult.success) {
return {
errors: formValueParsingResult.error.flatten().fieldErrors,
}
const errorsList = Object.entries(formValueParsingResult.error.flatten().fieldErrors).map(
([name, messages]) => {
const error: FieldError = {
message: messages[0],
type: 'server',
};
return [name, error];
},
);
const errors: FieldErrors = Object.fromEntries(errorsList);

return errors;
}

return redirect('/pizza/success');
Expand Down
13 changes: 13 additions & 0 deletions src/app/api/pizza/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,16 @@ export async function POST() {
);
return response;
}

export async function GET() {
const response: NextResponse<{ message: string }> = new NextResponse(
JSON.stringify({ message: getReasonPhrase(StatusCodes.NOT_FOUND) }),
{
headers: {
'Content-Type': 'application/json',
},
status: StatusCodes.NOT_FOUND,
},
);
return response;
}
64 changes: 64 additions & 0 deletions src/app/api/pizza/validate-form-values/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import type { NextRequest } from 'next/server';
import type { FieldError, FieldErrors } from 'react-hook-form';

import { StatusCodes } from 'http-status-codes';
import { NextResponse } from 'next/server';
import z from 'zod';

import type { FormValues } from '../../../../types/pizza';

import { pizzaFormValidationSchema } from '../../../../schema/pizza/validation';

async function getMaxAvailableAmount(): Promise<number> {
return new Promise((resolve) => {
setTimeout(() => resolve(5), 1000);
});
}

// todo: change to GET
export async function POST(request: NextRequest) {
const pizzaFormValidationSchemaAugmented = pizzaFormValidationSchema.superRefine(
async ({ amount }, ctx) => {
const maxAvailableAmount = await getMaxAvailableAmount();

if (amount > maxAvailableAmount) {
ctx.addIssue({
code: z.ZodIssueCode.too_big,
inclusive: true, // ??
maximum: maxAvailableAmount,
message: `Amount of ${amount} exceeds currently available amount of ${maxAvailableAmount}`,
path: ['amount'],
type: 'number',
});
}

return true;
},
);

try {
const payload: FormValues = await request.json();
const formValueParsingResult = await pizzaFormValidationSchemaAugmented.safeParseAsync(payload);

if (!formValueParsingResult.success) {
const errorsList = Object.entries(formValueParsingResult.error.flatten().fieldErrors).map(
([name, messages]) => {
const error: FieldError = {
message: messages[0],
type: 'server',
};
return [name, error];
},
);
const errors: FieldErrors = Object.fromEntries(errorsList);

// one or more fields invalid
return NextResponse.json(errors, { status: StatusCodes.OK });
}
} catch {
return NextResponse.json(null, { status: StatusCodes.BAD_REQUEST });
}

// fields valid
return NextResponse.json({}, { status: StatusCodes.OK });
}
89 changes: 41 additions & 48 deletions src/components/PizzaForm/PizzaForm.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,43 @@
'use client'
'use client';

import type { FormEvent } from 'react';

// import { DevTool } from "@hookform/devtools";
import { zodResolver } from '@hookform/resolvers/zod';
import { useMutation } from '@tanstack/react-query';
import { clsx } from 'clsx';
import React, { useEffect , useState } from 'react';
import { FormProvider, useForm } from 'react-hook-form';
import { useRouter } from 'next/navigation';
import React, { useEffect, useState } from 'react';
import { type FieldErrors, FormProvider, useForm } from 'react-hook-form';

import type { FormValues } from '../../types/pizza';
import type { PizzaFormProps } from './types';

import { handleFormValuesSubmit } from '../../app/actions/handleFormValuesSubmit/handleFormValuesSubmit';
import { formLabels } from '../../constants/pizza/labels'
import { formLabels } from '../../constants/pizza/labels';
import { sendValues } from '../../hooks/useSendValues/helpers';
import { pizzaFormValidationSchema } from '../../schema/pizza/validation';
import UncontrolledInput from '../UncontrolledInput';
import UncontrolledRadioCheckbox from '../UncontrolledRadioCheckbox';

const PizzaForm = ({ defaultValues, formSettings }: PizzaFormProps) => {
const router = useRouter();
const {
data: validationErrors,
mutateAsync,
reset: resetValidationErrors,
} = useMutation({
mutationFn: async (formValues: FormValues) => sendValues('/api/pizza/validate-form-values', formValues),
mutationKey: ['validation-errors'],
onSuccess: (errors: FieldErrors) => {
// no validation errors
if (Object.keys(errors).length === 0) {
router.push('/pizza/success');
}
},
});
const formMethods = useForm<FormValues>({
defaultValues,
errors: validationErrors, // https://github.com/react-hook-form/react-hook-form/pull/11188
mode: 'onChange',
resolver: zodResolver(pizzaFormValidationSchema),
});
Expand All @@ -33,8 +51,6 @@ const PizzaForm = ({ defaultValues, formSettings }: PizzaFormProps) => {
watch,
} = formMethods;
const [isPending, setIsPending] = useState(false);
const [serverSideErrors, setServerSideErrors] = useState<Record<string, string[]>|undefined>(undefined); // todo: improve typings

const priceRangeClassValue = watch('priceRangeClass');

// custom validation trigger for dough and toppings when price range changes
Expand All @@ -45,44 +61,25 @@ const PizzaForm = ({ defaultValues, formSettings }: PizzaFormProps) => {
const onSubmit = async (formValues: FormValues): Promise<void> => {
// https://github.com/vercel/next.js/discussions/51371#discussioncomment-7152123
setIsPending(true);

const currentServerSideErrors = await handleFormValuesSubmit(formValues);

await mutateAsync(formValues);
setIsPending(false);
setServerSideErrors(currentServerSideErrors?.errors);
};

const handleReset = (event: FormEvent<HTMLFormElement>): void => {
event.preventDefault();

reset({ ...defaultValues });
resetValidationErrors();
setIsPending(false);
setServerSideErrors(undefined);
};

const hasServerSideErrors = serverSideErrors !== undefined && (
Object.hasOwn(serverSideErrors, 'amount')
|| Object.hasOwn(serverSideErrors, 'id')
|| Object.hasOwn(serverSideErrors, 'priceRangeClass')
|| Object.hasOwn(serverSideErrors, 'selectedDough')
|| Object.hasOwn(serverSideErrors, 'selectedToppings')
);

return (
<FormProvider {...formMethods}>
<form onReset={handleReset} onSubmit={handleSubmit(onSubmit)}>
<UncontrolledInput
label={formLabels.id}
name="id"
type="text"
/>
<UncontrolledInput label={formLabels.id} name="id" type="text" />
<div className="divider" />

<UncontrolledInput
label={formLabels.amount}
name="amount"
type="text"
/>
<UncontrolledInput label={formLabels.amount} name="amount" type="text" />
<div className="divider" />

<UncontrolledRadioCheckbox
Expand Down Expand Up @@ -127,24 +124,20 @@ const PizzaForm = ({ defaultValues, formSettings }: PizzaFormProps) => {
</button>
</div>

{hasServerSideErrors && (
<div className="alert alert-warning shadow-lg mt-8">
<svg
className="stroke-current shrink-0 h-6 w-6"
fill="none"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
/>
</svg>
<code className="whitespace-pre">{JSON.stringify(serverSideErrors, null, 4)}</code>
</div>
)}
<pre className="mt-4 mockup-code bg-primary text-primary-content">
<code className="pl-6 whitespace-pre">
{JSON.stringify(
errors,
(key, value) => {
if (key === 'ref') {
return undefined;
}
return value;
},
4,
)}
</code>
</pre>
</form>
{/* <DevTool control={control} /> */}
</FormProvider>
Expand Down
2 changes: 1 addition & 1 deletion src/schema/pizza/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export const pizzaSettingsSchema = z
// #region doughs
doughs: z.array(
z.enum([...DOUGH], {
invalid_type_error: `Dough must contain ${listFormatterAnd([...DOUGH])}}`,
invalid_type_error: `Dough must contain ${listFormatterAnd([...DOUGH])}}`,
required_error: 'Dough is required',
}))
.length(DOUGH.length, `Dough must have exactly ${DOUGH.length} entries`)
Expand Down

0 comments on commit 3e39518

Please sign in to comment.