mirror of
https://github.com/michivonah/nextjs.git
synced 2025-12-22 22:16:28 +01:00
add form validation
This commit is contained in:
parent
7d2dea886c
commit
eb75a90936
4 changed files with 93 additions and 14 deletions
|
|
@ -9,21 +9,42 @@ const sql = postgres(process.env.POSTGRES_URL!, {ssl: 'require'});
|
|||
|
||||
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 async function createInvoice(formData: FormData){
|
||||
const { customerId, amount, status } = CreateInvoice.parse({
|
||||
export type State = {
|
||||
errors?: {
|
||||
customerId?: string[];
|
||||
amount?: string[];
|
||||
status?: string[];
|
||||
};
|
||||
message?: string | null;
|
||||
};
|
||||
|
||||
export async function createInvoice(prevState: State, formData: FormData){
|
||||
const validateFields = CreateInvoice.safeParse({
|
||||
customerId: formData.get('customerId'),
|
||||
amount: formData.get('amount'),
|
||||
status: formData.get('status'),
|
||||
});
|
||||
|
||||
if(!validateFields.success){
|
||||
return{
|
||||
errors: validateFields.error.flatten().fieldErrors,
|
||||
message: 'Missing fields. Failed to create invoice.',
|
||||
}
|
||||
}
|
||||
|
||||
const { customerId, amount, status } = validateFields.data;
|
||||
const amountInCents = amount * 100;
|
||||
const date = new Date().toISOString().split('T')[0];
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
'use client';
|
||||
|
||||
import { CustomerField } from '@/app/lib/definitions';
|
||||
import Link from 'next/link';
|
||||
import {
|
||||
|
|
@ -7,11 +9,15 @@ import {
|
|||
UserCircleIcon,
|
||||
} from '@heroicons/react/24/outline';
|
||||
import { Button } from '@/app/ui/button';
|
||||
import { createInvoice } from '@/app/lib/actions';
|
||||
import { createInvoice, State } from '@/app/lib/actions';
|
||||
import { useActionState } from 'react';
|
||||
|
||||
export default function Form({ customers }: { customers: CustomerField[] }) {
|
||||
const initialState: State = { message: null, errors: {}};
|
||||
const [state, formAction] = useActionState(createInvoice, initialState);
|
||||
|
||||
return (
|
||||
<form action={createInvoice}>
|
||||
<form action={formAction}>
|
||||
<div className="rounded-md bg-gray-50 p-4 md:p-6">
|
||||
{/* Customer Name */}
|
||||
<div className="mb-4">
|
||||
|
|
@ -24,6 +30,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
|
||||
|
|
@ -36,6 +43,14 @@ 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 */}
|
||||
|
|
@ -52,10 +67,20 @@ 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"
|
||||
required
|
||||
/>
|
||||
<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 &&
|
||||
state.errors.amount.map((error: string) => (
|
||||
<p className="mt-2 text-sm text-red-500" key={error}>
|
||||
{error}
|
||||
</p>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Invoice Status */}
|
||||
|
|
@ -72,6 +97,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"
|
||||
|
|
@ -95,6 +121,14 @@ export default function Form({ customers }: { customers: CustomerField[] }) {
|
|||
Paid <CheckIcon className="h-4 w-4" />
|
||||
</label>
|
||||
</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>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
|
|
|||
|
|
@ -3,7 +3,8 @@
|
|||
"scripts": {
|
||||
"build": "next build",
|
||||
"dev": "next dev --turbopack",
|
||||
"start": "next start"
|
||||
"start": "next start",
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@heroicons/react": "^2.2.0",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue