Skip to content

Commit 233611a

Browse files
committed
feat: create invoice error handling
1 parent be307f3 commit 233611a

File tree

3 files changed

+78
-20
lines changed

3 files changed

+78
-20
lines changed

app/lib/actions.ts

Lines changed: 36 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,14 @@
11
'use server';
22

3+
export type State = {
4+
errors?: {
5+
customerId?: string[];
6+
amount?: string[];
7+
status?: string[];
8+
};
9+
message?: string | null;
10+
};
11+
312
import { z } from 'zod';
413
import { sql } from '@vercel/postgres';
514
import { revalidatePath } from 'next/cache';
@@ -8,33 +17,42 @@ import { Edu_NSW_ACT_Foundation } from 'next/font/google';
817

918
const FormSchema = z.object({
1019
id: z.string(),
11-
customerId: z.string(),
12-
amount: z.coerce.number(),
13-
status: z.enum(['pending', 'paid']),
20+
customerId: z.string({
21+
invalid_type_error: 'Please select a customer.',
22+
}),
23+
amount: z.coerce
24+
.number()
25+
.gt(0, { message: 'Please enter an amount greater than $0.' }),
26+
27+
status: z.enum(['pending', 'paid'], {
28+
invalid_type_error: 'Please select an invoice status.',
29+
}),
1430
date: z.string(),
1531
});
1632

1733
const CreateInvoice = FormSchema.omit({ id: true, date: true });
1834

19-
export async function createInvoice(formData: FormData) {
20-
try {
35+
export async function createInvoice(prevState: State, formData: FormData) {
2136

22-
console.log('formData', formData);
23-
const rawFormData = {
24-
customerId: formData.get('customerId'),
25-
amount: formData.get('amount'),
26-
status: formData.get('status'),
37+
const validatedFields = CreateInvoice.safeParse({
38+
customerId: formData.get('customerId'),
39+
amount: formData.get('amount'),
40+
status: formData.get('status'),
41+
});
42+
43+
if (!validatedFields.success) {
44+
return {
45+
errors: validatedFields.error.flatten().fieldErrors,
46+
message: 'Missing Fields. Failed to Create Invoice.',
2747
};
28-
const { customerId, amount, status } = CreateInvoice.parse({
29-
customerId: formData.get('customerId'),
30-
amount: formData.get('amount'),
31-
status: formData.get('status'),
32-
});
33-
const amountInCents = amount * 100;
34-
const date = new Date().toISOString().split('T')[0];
48+
}
3549

36-
console.log('customerId, amount, status', customerId, amount, status);
50+
// Prepare data for insertion into the database
51+
const { customerId, amount, status } = validatedFields.data;
52+
const amountInCents = amount * 100;
53+
const date = new Date().toISOString().split('T')[0];
3754

55+
try {
3856
await sql`
3957
INSERT INTO invoices (customer_id, amount, status, date)
4058
VALUES (${customerId}, ${amountInCents}, ${status}, ${date})

app/ui/invoices/create-form.tsx

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
'use client';
2+
13
import { CustomerField } from '@/app/lib/definitions';
24
import Link from 'next/link';
35
import {
@@ -8,10 +10,14 @@ import {
810
} from '@heroicons/react/24/outline';
911
import { Button } from '@/app/ui/button';
1012
import { createInvoice } from '@/app/lib/actions';
13+
import { useFormState } from 'react-dom';
1114

1215
export default function Form({ customers }: { customers: CustomerField[] }) {
16+
const initialState = { message: null, errors: {} };
17+
const [state, dispatch] = useFormState(createInvoice, initialState);
18+
console.log('state', state);
1319
return (
14-
<form action={createInvoice}>
20+
<form action={dispatch}>
1521
<div className="rounded-md bg-gray-50 p-4 md:p-6">
1622
{/* Customer Name */}
1723
<div className="mb-4">
@@ -24,6 +30,7 @@ export default function Form({ customers }: { customers: CustomerField[] }) {
2430
name="customerId"
2531
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"
2632
defaultValue=""
33+
aria-describedby="customer-error"
2734
>
2835
<option value="" disabled>
2936
Select a customer
@@ -36,6 +43,14 @@ export default function Form({ customers }: { customers: CustomerField[] }) {
3643
</select>
3744
<UserCircleIcon className="pointer-events-none absolute left-3 top-1/2 h-[18px] w-[18px] -translate-y-1/2 text-gray-500" />
3845
</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>
3954
</div>
4055

4156
{/* Invoice Amount */}
@@ -52,10 +67,19 @@ export default function Form({ customers }: { customers: CustomerField[] }) {
5267
step="0.01"
5368
placeholder="Enter USD amount"
5469
className="peer block w-full rounded-md border border-gray-200 py-2 pl-10 text-sm outline-2 placeholder:text-gray-500"
70+
aria-describedby="amount-error"
5571
/>
5672
<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" />
5773
</div>
5874
</div>
75+
<div id="amount-error" aria-live="polite" aria-atomic="true">
76+
{state.errors?.amount &&
77+
state.errors.amount.map((error: string) => (
78+
<p className="mt-2 text-sm text-red-500" key={error}>
79+
{error}
80+
</p>
81+
))}
82+
</div>
5983
</div>
6084

6185
{/* Invoice Status */}
@@ -72,6 +96,7 @@ export default function Form({ customers }: { customers: CustomerField[] }) {
7296
type="radio"
7397
value="pending"
7498
className="h-4 w-4 cursor-pointer border-gray-300 bg-gray-100 text-gray-600 focus:ring-2"
99+
aria-describedby="status-error"
75100
/>
76101
<label
77102
htmlFor="pending"
@@ -87,6 +112,7 @@ export default function Form({ customers }: { customers: CustomerField[] }) {
87112
type="radio"
88113
value="paid"
89114
className="h-4 w-4 cursor-pointer border-gray-300 bg-gray-100 text-gray-600 focus:ring-2"
115+
aria-describedby="status-error"
90116
/>
91117
<label
92118
htmlFor="paid"
@@ -97,7 +123,20 @@ export default function Form({ customers }: { customers: CustomerField[] }) {
97123
</div>
98124
</div>
99125
</div>
126+
<div id="status-error" aria-live="polite" aria-atomic="true">
127+
{state.errors?.status &&
128+
state.errors.status.map((error: string) => (
129+
<p className="mt-2 text-sm text-red-500" key={error}>
130+
{error}
131+
</p>
132+
))}
133+
</div>
100134
</fieldset>
135+
<div aria-live="polite" aria-atomic="true">
136+
{state.message && (
137+
<p className="mt-2 text-sm text-red-500">{state.message}</p>
138+
)}
139+
</div>
101140
</div>
102141
<div className="mt-6 flex justify-end gap-4">
103142
<Link

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@
66
"prettier": "prettier --write --ignore-unknown .",
77
"prettier:check": "prettier --check --ignore-unknown .",
88
"start": "next start",
9-
"seed": "node -r dotenv/config ./scripts/seed.js"
9+
"seed": "node -r dotenv/config ./scripts/seed.js",
10+
"lint": "next lint"
1011
},
1112
"dependencies": {
1213
"@heroicons/react": "^2.0.18",

0 commit comments

Comments
 (0)