Skip to content

Commit e353dc4

Browse files
Add form validation and error handling
1 parent 8e540f2 commit e353dc4

File tree

2 files changed

+64
-7
lines changed

2 files changed

+64
-7
lines changed

app/lib/actions.ts

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,22 +5,48 @@ import { sql } from '@vercel/postgres';
55
import { revalidatePath } from 'next/cache';
66
import { redirect } from 'next/navigation';
77

8+
export type State = {
9+
errors?: {
10+
customerId?: string[];
11+
amount?: string[];
12+
status?: string[];
13+
};
14+
message?: string | null;
15+
};
16+
817
const formSchema = z.object({
918
id: z.string(),
10-
customerId: z.string(),
11-
amount: z.coerce.number(),
12-
status: z.enum(['paid', 'pending']),
19+
customerId: z.string({
20+
invalid_type_error: 'Please select a customer',
21+
}),
22+
amount: z.coerce
23+
.number()
24+
.gt(0, { message: 'Please enter an amount greater than $0.' }),
25+
status: z.enum(['paid', 'pending'], {
26+
invalid_type_error: 'Please select an invoice status.',
27+
}),
1328
date: z.string(),
1429
});
1530

1631
const CreateInvoice = formSchema.omit({ id: true, date: true });
1732

18-
export async function createInvoice(formData: FormData) {
19-
const { customerId, amount, status } = CreateInvoice.parse({
33+
export async function createInvoice(prevState: State, formData: FormData) {
34+
// Validate form using Zod
35+
const validateFields = CreateInvoice.safeParse({
2036
customerId: formData.get('customerId'),
2137
amount: formData.get('amount'),
2238
status: formData.get('status'),
2339
});
40+
41+
if (!validateFields.success) {
42+
return {
43+
errors: validateFields.error.flatten().fieldErrors,
44+
message: 'Missing fields. Failed to create invoice.',
45+
};
46+
}
47+
48+
// Prepare data for insertion into the database
49+
const { customerId, amount, status } = validateFields.data;
2450
const amountInCents = amount * 100;
2551
const date = new Date().toISOString().split('T')[0];
2652

@@ -35,6 +61,7 @@ export async function createInvoice(formData: FormData) {
3561
};
3662
}
3763

64+
// Revalidate the cache for the invoices page and redirect the user.
3865
revalidatePath('/dashboard/invoices');
3966
redirect('/dashboard/invoices');
4067
}
@@ -66,7 +93,6 @@ export async function updateInvoice(id: string, formData: FormData) {
6693
}
6794

6895
export async function deleteInvoice(id: string) {
69-
7096
try {
7197
await sql`
7298
DELETE FROM invoices

app/ui/invoices/create-form.tsx

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
'use client';
22

33
import { CustomerField } from '@/app/lib/definitions';
4+
import { useFormState } from 'react-dom';
45
import Link from 'next/link';
56
import {
67
CheckIcon,
@@ -12,8 +13,11 @@ import { Button } from '@/app/ui/button';
1213
import { createInvoice } from '@/app/lib/actions';
1314

1415
export default function Form({ customers }: { customers: CustomerField[] }) {
16+
const initialState = { message: null, errors: {} };
17+
const [state, dispatch] = useFormState(createInvoice, initialState);
18+
1519
return (
16-
<form action={createInvoice}>
20+
<form action={dispatch}>
1721
<div className="rounded-md bg-gray-50 p-4 md:p-6">
1822
{/* Customer Name */}
1923
<div className="mb-4">
@@ -26,6 +30,7 @@ export default function Form({ customers }: { customers: CustomerField[] }) {
2630
name="customerId"
2731
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"
2832
defaultValue=""
33+
aria-describedby="customer-error"
2934
>
3035
<option value="" disabled>
3136
Select a customer
@@ -38,6 +43,14 @@ export default function Form({ customers }: { customers: CustomerField[] }) {
3843
</select>
3944
<UserCircleIcon className="pointer-events-none absolute left-3 top-1/2 h-[18px] w-[18px] -translate-y-1/2 text-gray-500" />
4045
</div>
46+
<div id="customer-error" aria-live="polite" aria-atomic="true">
47+
{state.errors?.customerId &&
48+
state.errors.customerId.map((error: string) => (
49+
<p className="mt-2 text-sm text-red-500" key={error}>
50+
{error}
51+
</p>
52+
))}
53+
</div>
4154
</div>
4255

4356
{/* Invoice Amount */}
@@ -53,10 +66,19 @@ export default function Form({ customers }: { customers: CustomerField[] }) {
5366
type="number"
5467
step="0.01"
5568
placeholder="Enter USD amount"
69+
aria-describedby='amount-error'
5670
className="peer block w-full rounded-md border border-gray-200 py-2 pl-10 text-sm outline-2 placeholder:text-gray-500"
5771
/>
5872
<CurrencyDollarIcon className="pointer-events-none absolute left-3 top-1/2 h-[18px] w-[18px] -translate-y-1/2 text-gray-500 peer-focus:text-gray-900" />
5973
</div>
74+
<div id="amount-error" aria-live="polite" aria-atomic="true">
75+
{state.errors?.amount &&
76+
state.errors.amount.map((error: string) => (
77+
<p className="mt-2 text-sm text-red-500" key={error}>
78+
{error}
79+
</p>
80+
))}
81+
</div>
6082
</div>
6183
</div>
6284

@@ -73,6 +95,7 @@ export default function Form({ customers }: { customers: CustomerField[] }) {
7395
name="status"
7496
type="radio"
7597
value="pending"
98+
aria-describedby='status-error'
7699
className="h-4 w-4 cursor-pointer border-gray-300 bg-gray-100 text-gray-600 focus:ring-2"
77100
/>
78101
<label
@@ -97,6 +120,14 @@ export default function Form({ customers }: { customers: CustomerField[] }) {
97120
Paid <CheckIcon className="h-4 w-4" />
98121
</label>
99122
</div>
123+
<div id="status-error" aria-live="polite" aria-atomic="true">
124+
{state.errors?.status &&
125+
state.errors.status.map((error: string) => (
126+
<p className="mt-2 text-sm text-red-500" key={error}>
127+
{error}
128+
</p>
129+
))}
130+
</div>
100131
</div>
101132
</div>
102133
</fieldset>

0 commit comments

Comments
 (0)