Skip to content

Commit

Permalink
🦺 added validation for create- and update-invoice forms
Browse files Browse the repository at this point in the history
  • Loading branch information
MammaSonnim committed Jul 23, 2024
1 parent 1847a39 commit d43a117
Show file tree
Hide file tree
Showing 4 changed files with 100 additions and 14 deletions.
58 changes: 47 additions & 11 deletions app/lib/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,28 +4,50 @@ import { z } from 'zod';
import { sql } from '@vercel/postgres';
import { revalidatePath } from 'next/cache';
import { redirect } from 'next/navigation';
import { ServerActionState } from './definitions';

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 });

export interface CreateInvoiceResponseState {
errors?: {
customerId?: string[];
amount?: string[];
status?: string[];
};
message?: string | null;
}

export async function createInvoice(
_prevState: ServerActionState,
_prevState: CreateInvoiceResponseState,
formData: FormData,
) {
const { customerId, amount, status } = CreateInvoice.parse({
const validatedFields = CreateInvoice.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 Create Invoice.',
};
}

const { customerId, amount, status } = validatedFields.data;
const amountInCents = amount * 100;
const date = new Date().toISOString().split('T')[0];

Expand All @@ -36,7 +58,6 @@ export async function createInvoice(
`;
} catch (error) {
return {
success: false,
message: 'Database Error: Failed to Create Invoice.',
};
}
Expand All @@ -47,17 +68,34 @@ export async function createInvoice(

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

export interface UpdateInvoiceResponseState {
errors?: {
customerId?: string[];
amount?: string[];
status?: string[];
};
message?: string | null;
}

export async function updateInvoice(
id: string,
_prevState: ServerActionState,
_prevState: UpdateInvoiceResponseState,
formData: FormData,
) {
const { customerId, amount, status } = UpdateInvoice.parse({
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 {
Expand All @@ -68,7 +106,6 @@ export async function updateInvoice(
`;
} catch (error) {
return {
success: false,
message: 'Database Error: Failed to Update Invoice.',
};
}
Expand All @@ -82,7 +119,6 @@ export async function deleteInvoice(id: string) {
await sql`DELETE FROM invoices WHERE id = ${id}`;
} catch (error) {
return {
success: false,
message: 'Database Error: Failed to Delete Invoice.',
};
}
Expand Down
1 change: 0 additions & 1 deletion app/ui/invoices/buttons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ export function UpdateInvoice({ id }: { id: string }) {
export function DeleteInvoice({ id }: { id: string }) {
const deleteInvoiceWithId = deleteInvoice.bind(null, id);
const [state, formAction] = useActionState(deleteInvoiceWithId, {
success: false,
message: '',
});
const { setMessage } = useNotification();
Expand Down
28 changes: 27 additions & 1 deletion app/ui/invoices/create-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ import { useNotification } from '../notification-context';

export default function Form({ customers }: { customers: CustomerField[] }) {
const [state, formAction] = useActionState(createInvoice, {
success: false,
message: '',
errors: {},
});
const { setMessage } = useNotification();

Expand All @@ -25,6 +25,7 @@ export default function Form({ customers }: { customers: CustomerField[] }) {
setMessage(state.message);
}
}, [state, setMessage]);

return (
<form action={formAction}>
<div className="rounded-md bg-gray-50 p-4 md:p-6">
Expand All @@ -39,6 +40,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 @@ -51,6 +53,13 @@ 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?.map((error: string) => (
<p className="mt-2 text-sm text-red-500" key={error}>
{error}
</p>
))}
</div>
</div>

{/* Invoice Amount */}
Expand All @@ -67,10 +76,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>
<div id="amount-error" aria-live="polite" aria-atomic="true">
{state.errors?.amount?.map((error: string) => (
<p className="mt-2 text-sm text-red-500" key={error}>
{error}
</p>
))}
</div>
</div>

{/* Invoice Status */}
Expand All @@ -87,6 +104,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 @@ -102,6 +120,7 @@ export default function Form({ customers }: { customers: CustomerField[] }) {
type="radio"
value="paid"
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="paid"
Expand All @@ -112,6 +131,13 @@ export default function Form({ customers }: { customers: CustomerField[] }) {
</div>
</div>
</div>
<div id="status-error" aria-live="polite" aria-atomic="true">
{state.errors?.status?.map((error: string) => (
<p className="mt-2 text-sm text-red-500" key={error}>
{error}
</p>
))}
</div>
</fieldset>
</div>
<div className="mt-6 flex justify-end gap-4">
Expand Down
27 changes: 26 additions & 1 deletion app/ui/invoices/edit-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ export default function EditInvoiceForm({
}) {
const updateInvoiceWithId = updateInvoice.bind(null, invoice.id);
const [state, formAction] = useActionState(updateInvoiceWithId, {
success: false,
message: '',
errors: {},
});
const { setMessage } = useNotification();

Expand All @@ -47,6 +47,7 @@ export default function EditInvoiceForm({
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={invoice.customer_id}
aria-describedby="customer-error"
>
<option value="" disabled>
Select a customer
Expand All @@ -59,6 +60,13 @@ export default function EditInvoiceForm({
</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?.map((error: string) => (
<p className="mt-2 text-sm text-red-500" key={error}>
{error}
</p>
))}
</div>
</div>

{/* Invoice Amount */}
Expand All @@ -76,10 +84,18 @@ export default function EditInvoiceForm({
defaultValue={invoice.amount}
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>
<div id="amount-error" aria-live="polite" aria-atomic="true">
{state.errors?.amount?.map((error: string) => (
<p className="mt-2 text-sm text-red-500" key={error}>
{error}
</p>
))}
</div>
</div>

{/* Invoice Status */}
Expand All @@ -97,6 +113,7 @@ export default function EditInvoiceForm({
value="pending"
defaultChecked={invoice.status === '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 @@ -113,6 +130,7 @@ export default function EditInvoiceForm({
value="paid"
defaultChecked={invoice.status === 'paid'}
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="paid"
Expand All @@ -123,6 +141,13 @@ export default function EditInvoiceForm({
</div>
</div>
</div>
<div id="status-error" aria-live="polite" aria-atomic="true">
{state.errors?.status?.map((error: string) => (
<p className="mt-2 text-sm text-red-500" key={error}>
{error}
</p>
))}
</div>
</fieldset>
</div>
<div className="mt-6 flex justify-end gap-4">
Expand Down

0 comments on commit d43a117

Please sign in to comment.