Skip to content

Commit

Permalink
Class 14 acessibility & validate form
Browse files Browse the repository at this point in the history
  • Loading branch information
JorgeMadson committed Jan 25, 2024
1 parent bcf413b commit 3f35d25
Show file tree
Hide file tree
Showing 4 changed files with 86 additions and 16 deletions.
58 changes: 47 additions & 11 deletions app/lib/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,22 +7,46 @@ import { z } from 'zod';

const FormSchema = z.object({
id: z.string(),
customerId: z.string(),
amount: z.coerce.number(),
status: z.enum(['pending', 'paid']),
customerId: z.string({
invalid_type_error: 'Please select a customer.',
}),
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(),
});

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

export async function createInvoice(formData: FormData) {
const { customerId, amount, status } = CreateInvoice.parse({
// This is temporary until @types/react-dom is updated
export type State = {
errors?: {
customerId?: string[];
amount?: string[];
status?: string[];
};
message?: string | null;
}

export async function createInvoice(prevSate: State, formData: FormData) {
const validatedFields = CreateInvoice.safeParse({
customerId: formData.get('customerId'),
amount: formData.get('amount'),
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 date = new Date().toISOString().split('T')[0];

Expand All @@ -41,21 +65,33 @@ export async function createInvoice(formData: FormData) {
redirect('/dashboard/invoices');
}

export async function updateInvoice(id: string, formData: FormData) {
const { customerId, amount, status } = UpdateInvoice.parse({
export async function updateInvoice(
id: string,
prevState: State,
formData: FormData,
) {
const validatedFields = UpdateInvoice.safeParse({
customerId: formData.get('customerId'),
amount: formData.get('amount'),
status: formData.get('status'),
});

if (!validatedFields.success) {
return {
errors: validatedFields.error.flatten().fieldErrors,
message: 'Missing Fields. Failed to Update Invoice.',
};
}

const { customerId, amount, status } = validatedFields.data;
const amountInCents = amount * 100;

try {
await sql`
UPDATE invoices
SET customer_id = ${customerId}, amount = ${amountInCents}, status = ${status}
WHERE id = ${id}
`;
UPDATE invoices
SET customer_id = ${customerId}, amount = ${amountInCents}, status = ${status}
WHERE id = ${id}
`;
} catch (error) {
return { message: 'Database Error: Failed to Update Invoice.' };
}
Expand Down
35 changes: 33 additions & 2 deletions app/ui/invoices/create-form.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
"use client";
import { CustomerField } from '@/app/lib/definitions';
import Link from 'next/link';
import {
Expand All @@ -8,10 +9,14 @@ import {
} from '@heroicons/react/24/outline';
import { Button } from '@/app/ui/button';
import { createInvoice } from '@/app/lib/actions';
import { useFormState } from 'react-dom';

export default function Form({ customers }: { customers: CustomerField[] }) {

const initialState = { message: null, errors: {} };
const [state, dispatch] = useFormState(createInvoice, initialState);
return (
<form action={createInvoice}>
<form action={dispatch}>
<div className="rounded-md bg-gray-50 p-4 md:p-6">
{/* Customer Name */}
<div className="mb-4">
Expand All @@ -24,6 +29,7 @@ export default function Form({ customers }: { customers: CustomerField[] }) {
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"
defaultValue=""
aria-describedby="customer-error"
>
<option value="" disabled>
Select a customer
Expand All @@ -36,8 +42,15 @@ export default function Form({ customers }: { customers: CustomerField[] }) {
</select>
<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>

{/* Invoice Amount */}
<div className="mb-4">
<label htmlFor="amount" className="mb-2 block text-sm font-medium">
Expand All @@ -52,9 +65,18 @@ export default function Form({ customers }: { customers: CustomerField[] }) {
step="0.01"
placeholder="Enter USD amount"
className="peer block w-full rounded-md border border-gray-200 py-2 pl-10 text-sm outline-2 placeholder:text-gray-500"
aria-describedby="amount-error"
/>
<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" />
</div>
<div id="amount-error" aria-live="polite" aria-atomic="true">
{state.errors?.amount &&
state.errors.amount.map((error: string) => (
<p className="mt-2 text-sm text-red-500" key={error}>
{error}
</p>
))}
</div>
</div>
</div>

Expand All @@ -72,6 +94,7 @@ export default function Form({ customers }: { customers: CustomerField[] }) {
type="radio"
value="pending"
className="h-4 w-4 cursor-pointer border-gray-300 bg-gray-100 text-gray-600 focus:ring-2"
aria-describedby="status-error"
/>
<label
htmlFor="pending"
Expand All @@ -96,6 +119,14 @@ export default function Form({ customers }: { customers: CustomerField[] }) {
</label>
</div>
</div>
<div id="status-error" aria-live="polite" aria-atomic="true">
{state.errors?.status &&
state.errors.status.map((error: string) => (
<p className="mt-2 text-sm text-red-500" key={error}>
{error}
</p>
))}
</div>
</div>
</fieldset>
</div>
Expand Down
6 changes: 4 additions & 2 deletions app/ui/invoices/edit-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
import Link from 'next/link';
import { Button } from '@/app/ui/button';
import { updateInvoice } from '@/app/lib/actions';
import { useFormState } from 'react-dom';

export default function EditInvoiceForm({
invoice,
Expand All @@ -18,11 +19,12 @@ export default function EditInvoiceForm({
invoice: InvoiceForm;
customers: CustomerField[];
}) {

const initialState = { message: null, errors: {} };
const updateInvoiceWithId = updateInvoice.bind(null, invoice.id);
const [state, dispatch] = useFormState(updateInvoiceWithId, initialState);

return (
<form action={updateInvoiceWithId}>
<form action={dispatch}>
<div className="rounded-md bg-gray-50 p-4 md:p-6">
{/* Customer Name */}
<div className="mb-4">
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
"prettier": "prettier --write --ignore-unknown .",
"prettier:check": "prettier --check --ignore-unknown .",
"start": "next start",
"seed": "node -r dotenv/config ./scripts/seed.js"
"seed": "node -r dotenv/config ./scripts/seed.js",
"lint": "next lint"
},
"dependencies": {
"@heroicons/react": "^2.0.18",
Expand Down

0 comments on commit 3f35d25

Please sign in to comment.