Chapter 14 - Improve validation

This commit is contained in:
2024-01-04 08:00:38 +01:00
parent 1c23830272
commit 69f0e86915
3 changed files with 52 additions and 15 deletions

View File

@@ -7,9 +7,11 @@ import {redirect} from "next/navigation";
const FormSchema = z.object({ const FormSchema = z.object({
id: z.string(), id: z.string(),
customerId: z.string(), customerId: z.string({
amount: z.coerce.number(), invalid_type_error: 'Please select a customer.',
status: z.enum(['pending', 'paid']), }),
amount: z.coerce.number().gt(0, { message: 'Please enter an amount greater than $0.' }),
status: z.enum(['pending', 'paid'], { invalid_type_error: 'Please select an invoice status'}),
date: z.string(), date: z.string(),
}); });
@@ -17,14 +19,33 @@ const UpdateInvoice = FormSchema.omit({ id: true, date: true });
const CreateInvoice = FormSchema.omit({ id: true, date: true }); const CreateInvoice = FormSchema.omit({ id: true, date: true });
export async function createInvoice(formData: FormData) { export type State = {
errors?: {
customerId?: string[];
amount?: string[];
status?: string[];
};
message?: string | null;
};
const { customerId, amount, status } = CreateInvoice.parse({ export async function createInvoice(prevState: State, formData: FormData) {
const validatedFields = CreateInvoice.safeParse({
customerId: formData.get('customerId'), customerId: formData.get('customerId'),
amount: formData.get('amount'), amount: formData.get('amount'),
status: formData.get('status'), status: formData.get('status'),
}); });
// If form validation fails, return errors early. Otherwise, continue.
if (!validatedFields.success) {
return {
errors: validatedFields.error.flatten().fieldErrors,
message: 'Missing Fields. Failed to Create Invoice.',
};
}
const { customerId, amount, status } = validatedFields.data;
const amountInCents = amount * 100; const amountInCents = amount * 100;
const date = new Date().toISOString().split('T')[0]; const date = new Date().toISOString().split('T')[0];

View File

@@ -1,3 +1,4 @@
'use client';
import { CustomerField } from '@/app/lib/definitions'; import { CustomerField } from '@/app/lib/definitions';
import Link from 'next/link'; import Link from 'next/link';
import { import {
@@ -8,10 +9,14 @@ import {
} from '@heroicons/react/24/outline'; } from '@heroicons/react/24/outline';
import { Button } from '@/app/ui/button'; import { Button } from '@/app/ui/button';
import {createInvoice} from "@/app/lib/actions"; import {createInvoice} from "@/app/lib/actions";
import { useFormState } from 'react-dom';
export default function Form({ customers }: { customers: CustomerField[] }) { export default function Form({ customers }: { customers: CustomerField[] }) {
const initialState = { message: null, errors: {} };
const [state, dispatch] = useFormState(createInvoice, initialState);
console.log(state);
return ( return (
<form action={createInvoice}> <form action={dispatch}>
<div className="rounded-md bg-gray-50 p-4 md:p-6"> <div className="rounded-md bg-gray-50 p-4 md:p-6">
{/* Customer Name */} {/* Customer Name */}
<div className="mb-4"> <div className="mb-4">
@@ -20,21 +25,31 @@ export default function Form({ customers }: { customers: CustomerField[] }) {
</label> </label>
<div className="relative"> <div className="relative">
<select <select
id="customer" id="customer"
name="customerId" name="customerId"
className="peer block w-full cursor-pointer rounded-md border border-gray-200 py-2 pl-10 text-sm outline-2 placeholder:text-gray-500" className="peer block w-full cursor-pointer rounded-md border border-gray-200 py-2 pl-10 text-sm outline-2 placeholder:text-gray-500"
defaultValue="" defaultValue=""
aria-describedby="customer-error"
> >
<option value="" disabled> <option value="" disabled>
Select a customer Select a customer
</option> </option>
{customers.map((customer) => ( {customers.map((customer) => (
<option key={customer.id} value={customer.id}> <option key={customer.id} value={customer.id}>
{customer.name} {customer.name}
</option> </option>
))} ))}
</select> </select>
<UserCircleIcon className="pointer-events-none absolute left-3 top-1/2 h-[18px] w-[18px] -translate-y-1/2 text-gray-500" /> <UserCircleIcon
className="pointer-events-none absolute left-3 top-1/2 h-[18px] w-[18px] -translate-y-1/2 text-gray-500"/>
</div>
<div id="customer-error" aria-live="polite" aria-atomic="true">
{state.errors?.customerId &&
state.errors.customerId.map((error: string) => (
<p className="mt-2 text-sm text-red-500" key={error}>
{error}
</p>
))}
</div> </div>
</div> </div>

View File

@@ -6,7 +6,8 @@
"prettier": "prettier --write --ignore-unknown .", "prettier": "prettier --write --ignore-unknown .",
"prettier:check": "prettier --check --ignore-unknown .", "prettier:check": "prettier --check --ignore-unknown .",
"start": "next start", "start": "next start",
"seed": "node -r dotenv/config ./scripts/seed.js" "seed": "node -r dotenv/config ./scripts/seed.js",
"lint": "next lint"
}, },
"dependencies": { "dependencies": {
"@heroicons/react": "^2.0.18", "@heroicons/react": "^2.0.18",