Chapter 14 - Improve validation
This commit is contained in:
@@ -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];
|
||||||
|
|
||||||
|
|||||||
@@ -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">
|
||||||
@@ -24,6 +29,7 @@ export default function Form({ customers }: { customers: CustomerField[] }) {
|
|||||||
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
|
||||||
@@ -34,7 +40,16 @@ export default function Form({ customers }: { customers: CustomerField[] }) {
|
|||||||
</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>
|
||||||
|
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
Reference in New Issue
Block a user