Skip to content

Commit

Permalink
Merge branch 'main' into example-34ht
Browse files Browse the repository at this point in the history
  • Loading branch information
delbaoliveira committed Oct 2, 2023
2 parents 24d91af + da17909 commit b7d8bd3
Show file tree
Hide file tree
Showing 18 changed files with 876 additions and 939 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,12 @@ export default function NotFound() {
<FaceFrownIcon className="w-12 text-gray-400" />
<h2 className="text-lg font-semibold">Not Found</h2>
<p>Could not find the requested invoice.</p>
<button className="mt-4 rounded-md bg-black px-4 py-2 text-sm text-white">
<Link href="/dashboard/invoices">Go Back</Link>
</button>
<Link
href="/dashboard/invoices"
className="mt-4 rounded-md bg-black px-4 py-2 text-sm text-white"
>
Go Back
</Link>
</main>
);
}
93 changes: 5 additions & 88 deletions dashboard/15-final/app/dashboard/invoices/[id]/edit/page.tsx
Original file line number Diff line number Diff line change
@@ -1,102 +1,19 @@
import { fetchInvoiceById, fetchAllCustomers } from '@/app/lib/data';
import { updateInvoice } from '@/app/lib/actions';
import { fetchInvoiceById, fetchCustomerNames } from '@/app/lib/data';
import { notFound } from 'next/navigation';
import Form from '@/app/ui/invoices/edit-form';

export default async function Page({ params }: { params: { id: string } }) {
const id = params.id;
const invoice = await fetchInvoiceById(id);
const customers = await fetchAllCustomers();
const customerNames = await fetchCustomerNames();

if (!invoice) {
notFound();
}

return (
<main role="main">
<div className="mx-auto max-w-sm rounded-lg border px-6 py-8 shadow-sm">
<h2
className="mb-6 text-xl font-semibold text-gray-900"
role="heading"
aria-level={2}
>
Edit Invoice
</h2>
<form action={updateInvoice}>
<input type="hidden" name="id" value={id} />
{/* Customer selection */}
<div className="mb-4" role="group">
<label
htmlFor="customer"
className="mb-2 block text-sm font-semibold"
>
Customer
</label>
<select
id="customer"
name="customerId"
className="block w-full rounded-md border-0 py-1.5 pl-3 text-sm ring-1 ring-inset ring-gray-200 placeholder:text-gray-200 focus:ring-blue-200"
defaultValue={invoice.name}
aria-label="Select Customer"
>
{customers.map((customer) => (
<option key={customer.id} value={customer.id}>
{customer.name}
</option>
))}
</select>
</div>

{/* Invoice amount */}
<div className="mb-4" role="group">
<label className="mb-2 block text-sm font-semibold">Amount</label>
<div className="relative mt-2 rounded-md shadow-sm">
<div className="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
<span className="text-gray-600 sm:text-sm" aria-hidden="true">
$
</span>
</div>
<input
name="amount"
type="number"
step="0.01"
defaultValue={invoice.amount}
placeholder="00.00"
className="block w-full rounded-md border-0 py-1.5 pl-7 text-sm leading-6 ring-1 ring-inset ring-gray-200 placeholder:text-gray-400 focus:ring-blue-200"
aria-label="Enter Amount"
/>
</div>
</div>

{/* Invoice status */}
<div className="mb-4" role="group">
<label
className="mb-2 block text-sm font-semibold"
htmlFor="status"
>
Status
</label>
<select
id="status"
name="status"
className="block w-full rounded-md border-0 py-1.5 pl-3 text-sm ring-1 ring-inset ring-gray-200 placeholder:text-gray-200 focus:ring-blue-200"
defaultValue={invoice.status}
aria-label="Select Status"
>
<option value="pending">Pending</option>
<option value="paid">Paid</option>
</select>
</div>

{/* Submit button */}
<button
type="submit"
className="rounded-md bg-blue-500 px-4 py-2 text-center text-sm font-semibold text-white hover:bg-blue-600 focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
aria-label="Update Invoice"
>
Update Invoice
</button>
</form>
</div>
<main>
<Form invoice={invoice} customerNames={customerNames} id={id} />
</main>
);
}
101 changes: 6 additions & 95 deletions dashboard/15-final/app/dashboard/invoices/create/page.tsx
Original file line number Diff line number Diff line change
@@ -1,101 +1,12 @@
import { createInvoice } from '@/app/lib/actions';
import { fetchAllCustomers } from '@/app/lib/data';
import { fetchCustomerNames } from '@/app/lib/data';
import Form from '@/app/ui/invoices/create-form';

export default async function Page() {
const customers = await fetchAllCustomers();
const customerNames = await fetchCustomerNames();

return (
<div className="mx-auto max-w-sm rounded-lg border px-6 py-8 shadow-sm">
<h2 className="mb-6 text-xl font-semibold text-gray-900">
Create Invoice
</h2>

<form action={createInvoice}>
{/* Customer */}
<div className="mb-4">
<label
htmlFor="customer"
className="mb-2 block text-sm font-semibold"
aria-label="Select Customer"
>
Customer
</label>
<select
id="customer"
name="customerId"
className="block w-full rounded-md border-0 py-1.5 pl-3 text-sm ring-1 ring-inset ring-gray-200 placeholder:text-gray-200"
defaultValue=""
aria-label="Select Customer"
aria-required="true"
required
>
<option value="" disabled>
Select a customer
</option>
{customers.map((customer) => (
<option key={customer.id} value={customer.id}>
{customer.name}
</option>
))}
</select>
</div>

{/* Amount */}
<div className="mb-4">
<label
className="mb-2 block text-sm font-semibold"
htmlFor="amount"
id="amount-label"
>
Amount
</label>
<div className="relative mt-2 rounded-md shadow-sm">
<div className="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
<span className="text-gray-600 sm:text-sm" aria-hidden="true">
$
</span>
</div>
<input
id="amount"
name="amount"
type="number"
step="0.01"
placeholder="00.00"
className="block w-full rounded-md border-0 py-1.5 pl-7 text-sm leading-6 ring-1 ring-inset ring-gray-200 placeholder:text-gray-400"
aria-describedby="amount-label"
/>
</div>
</div>

{/* Invoice Status */}
<div className="mb-4">
<label
className="mb-2 block text-sm font-semibold"
htmlFor="status"
id="status-label"
>
Status
</label>
<select
id="status"
name="status"
className="block w-full rounded-md border-0 py-1.5 pl-3 text-sm ring-1 ring-inset ring-gray-200 placeholder:text-gray-200"
defaultValue="pending"
aria-describedby="status-label"
>
<option value="pending">Pending</option>
<option value="paid">Paid</option>
</select>
</div>

{/* Submit Button */}
<button
type="submit"
className="rounded-md bg-blue-500 px-4 py-2 text-center text-sm font-semibold text-white hover:bg-blue-600 focus:border-blue-700 focus:outline-none focus:ring focus:ring-blue-200"
>
Create
</button>
</form>
</div>
<main>
<Form customerNames={customerNames} />
</main>
);
}
70 changes: 54 additions & 16 deletions dashboard/15-final/app/lib/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,48 +5,86 @@ import { sql } from '@vercel/postgres';
import { revalidatePath } from 'next/cache';
import { redirect } from 'next/navigation';

const InvoiceSchema = z.object({
const FormSchema = z.object({
id: z.string(),
customerId: z.string(),
amount: z.coerce.number(),
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']),
date: z.string(),
});
const CreateInvoice = InvoiceSchema.omit({ id: true, date: true });
const UpdateInvoice = InvoiceSchema.omit({ date: true });
const DeleteInvoice = InvoiceSchema.pick({ id: true });

export async function createInvoice(formData: FormData) {
const { customerId, amount, status } = CreateInvoice.parse({
const CreateInvoice = FormSchema.omit({ id: true, date: true });
const UpdateInvoice = FormSchema.omit({ date: true });
const DeleteInvoice = FormSchema.pick({ id: true });

// This is temporary
export type State = {
errors?: {
customerId?: string[];
amount?: string[];
status?: string[];
};
message: string;
};

export async function createInvoice(prevState: State, formData: FormData) {
// Validate form fields using Zod
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.',
};
}

// Prepare data for insertion into the database
const { customerId, amount, status } = validatedFields.data;
const amountInCents = amount * 100;
const date = new Date().toISOString().split('T')[0];

// Insert data into the database
try {
await sql`
INSERT INTO invoices (customer_id, amount, status, date)
VALUES (${customerId}, ${amountInCents}, ${status}, ${date})
`;
INSERT INTO invoices (customer_id, amount, status, date)
VALUES (${customerId}, ${amountInCents}, ${status}, ${date})
`;

revalidatePath('/dashboard/invoices');
redirect('/dashboard/invoices');
} catch (error) {
throw new Error('Failed to Create Invoice');
// If a database error occurs, return a more specific error.
return {
message: 'Database error: Failed to create invoice.',
};
}
}

export async function updateInvoice(formData: FormData) {
const { id, customerId, amount, status } = UpdateInvoice.parse({
export async function updateInvoice(prevState: State, formData: FormData) {
const validatedFields = UpdateInvoice.safeParse({
id: formData.get('id'),
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 { id, customerId, amount, status } = validatedFields.data;
const amountInCents = amount * 100;

try {
Expand All @@ -59,7 +97,7 @@ export async function updateInvoice(formData: FormData) {
revalidatePath('/dashboard/invoices');
redirect('/dashboard/invoices');
} catch (error) {
throw new Error('Failed to Update Invoice');
return { message: 'Database error: Failed to update invoice.' };
}
}

Expand All @@ -73,6 +111,6 @@ export async function deleteInvoice(formData: FormData) {
revalidatePath('/dashboard/invoices');
return { message: 'Deleted Invoice' };
} catch (error) {
throw new Error('Failed to Delete Invoice');
return { message: 'Database error: Failed to delete invoice.' };
}
}
Loading

0 comments on commit b7d8bd3

Please sign in to comment.