Skip to content
This repository was archived by the owner on Jul 19, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
node_modules
.next
.env
.env
.eslintrc.json
62 changes: 38 additions & 24 deletions app-router/nextjs-dashboard/README.md
Original file line number Diff line number Diff line change
@@ -1,26 +1,40 @@
## Next.js App Router Course - Starter

This is the starter template for the Next.js App Router Course. It contains the starting code for the dashboard application.

For more information, see the [course curriculum](https://nextjs.org/learn) on the Next.js Website.


### [React Foundations](https://nextjs.org/learn/react-foundations) ✅

1. About React and Next.js (_theory - no code_)
2. Rendering User Interfaces (UI) (_theory - no code_)
3. Updating UI with JavaScript (_theory - no code_)
4. [Getting Started with React ♻️][1-4]
5. [Building UI with Components ♻️][1-4]
6. [Displaying Data with Props ♻️][1-6]
7. [Adding Interactivity with State ♻️][1-7]
8. [From React to Next.js ♻️][1-89]
9. [Installing Next.js ♻️][1-89]
10. [Server and Client Components ♻️][1-10]

[1-4]: https://github.com/treejamie/next-js-learn/pull/1
[1-5]: https://github.com/treejamie/next-js-learn/pull/2
[1-6]: https://github.com/treejamie/next-js-learn/pull/3
[1-7]: https://github.com/treejamie/next-js-learn/pull/4
[1-89]: https://github.com/treejamie/next-js-learn/pull/5
[1-10]: https://github.com/treejamie/next-js-learn/pull/6
### [App Router](https://nextjs.org/learn/dashboard-app/getting-started) 🚧



Part of this tutorial was to deploy it out to vercel. It was very slick.I took the liberty of putting it on a custom domain and you can find it there 👉 [next-js-dashboard.curle.io](https://next-js-dashboard.curle.io)


1. [Getting Started ♻️][2-1]
2. [CSS Styling ♻️][2-2]
3. [Optimizing Fonts and Images ♻️][2-3]
4. [Creating Layouts and Pages ♻️][2-4]
5. [Navigating Between Pages ♻️][2-5]
6. [Setting Up Your Database ♻️][2-6]
7. [Fetching Data ♻️][2-7]
8. [Static and Dynamic Rendering ♻️][2-8]
9. [Streaming ♻️][2-9]
10. Partial Prerendering ✅ (_no code, theory / beta functionality_)
11. [Adding Search and Pagination ♻️][2-11]
12. [Mutating Data️ ♻️][2-12]
13. [Handling Errors ♻️][2-13]
14. [Improving Accessibility ♻️][2-14]
15. Adding Authentication 🚧
16. Adding Metadata 🚧️


[2-1]: https://github.com/treejamie/next-js-learn/pull/7
[2-2]: https://github.com/treejamie/next-js-learn/pull/9
[2-3]: https://github.com/treejamie/next-js-learn/pull/10
[2-4]: https://github.com/treejamie/next-js-learn/pull/11
[2-5]: https://github.com/treejamie/next-js-learn/pull/12
[2-6]: https://github.com/treejamie/next-js-learn/pull/13
[2-7]: https://github.com/treejamie/next-js-learn/pull/15
[2-8]: https://github.com/treejamie/next-js-learn/pull/16
[2-9]: https://github.com/treejamie/next-js-learn/pull/17
[2-11]: https://github.com/treejamie/next-js-learn/pull/19
[2-12]: https://github.com/treejamie/next-js-learn/pull/20
[2-13]: https://github.com/treejamie/next-js-learn/pull/23
[2-14]: https://github.com/treejamie/next-js-learn/pull/25
94 changes: 66 additions & 28 deletions app-router/nextjs-dashboard/app/lib/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,24 @@ import { redirect } from 'next/navigation';
import postgres from 'postgres';

const sql = postgres(process.env.POSTGRES_URL!, { ssl: 'require' });

// maybe this will get solved later, but this is already
// defined in the definitions to an extent. Is this some
// stink?
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 });
Expand All @@ -31,52 +37,84 @@ export async function deleteInvoice(id: string) {
revalidatePath('/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}
`;
} catch (error){
console.log(error)
} catch (error) {
return { message: 'Database Error: Failed to Update Invoice.' };
}

revalidatePath('/dashboard/invoices');
redirect('/dashboard/invoices');
}

export type State = {
errors?: {
customerId?: string[];
amount?: string[];
status?: string[];
};
message?: string | null;
};

export async function createInvoice(prevState: 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.',
};
}

export async function createInvoice(formData: FormData){
const { customerId, amount, status } = CreateInvoice.parse({
customerId: formData.get('customerId'),
amount: formData.get('amount'),
status: formData.get('status'),
});

// no decimals, so store it all in cents and avoid
// floating point errors.
const amountInCents = amount * 100;
const date = new Date().toISOString().split('T')[0];
// no decimals, so store it all in cents and avoid
// floating point errors.
// 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];

try {
await sql`
try {
await sql`
INSERT INTO invoices (customer_id, amount, status, date)
VALUES (${customerId}, ${amountInCents}, ${status}, ${date})
`;
} catch(error){
console.log(error)
}
} catch (error) {
console.log(error);
return {
message: 'Database Error: Failed to Create Invoice.',
};
}

revalidatePath('/dashboard/invoices');
redirect('/dashboard/invoices');
revalidatePath('/dashboard/invoices');
redirect('/dashboard/invoices');
}
1 change: 0 additions & 1 deletion app-router/nextjs-dashboard/app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import AcmeLogo from '@/app/ui/acme-logo';
import { ArrowRightIcon } from '@heroicons/react/24/outline';
import Link from 'next/link';
import { lusitana } from '@/app/ui/fonts';
import Image from 'next/image';

export default function Page() {
Expand Down
1 change: 0 additions & 1 deletion app-router/nextjs-dashboard/app/ui/customers/table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import Image from 'next/image';
import { lusitana } from '@/app/ui/fonts';
import Search from '@/app/ui/search';
import {
CustomersTableType,
FormattedCustomersTable,
} from '@/app/lib/definitions';

Expand Down
51 changes: 48 additions & 3 deletions app-router/nextjs-dashboard/app/ui/invoices/create-form.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
'use client'

import { useActionState } from 'react';
import { CustomerField } from '@/app/lib/definitions';
import Link from 'next/link';
import {
Expand All @@ -7,11 +10,14 @@ 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';

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">
Expand All @@ -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
Expand All @@ -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 */}
Expand All @@ -52,9 +67,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 +96,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 @@ -87,6 +112,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 @@ -95,9 +121,28 @@ export default function Form({ customers }: { customers: CustomerField[] }) {
Paid <CheckIcon className="h-4 w-4" />
</label>
</div>

</div>

</div>
</fieldset>
</fieldset>
<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>


{state.message &&
<div id="error" aria-live="polite" aria-atomic="true">
<p className="mt-2 text-sm text-red-500" key="error">
{state.message}
</p>
</div>
}
</div>
<div className="mt-6 flex justify-end gap-4">
<Link
Expand Down
9 changes: 6 additions & 3 deletions app-router/nextjs-dashboard/app/ui/invoices/edit-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,22 @@ import {
} from '@heroicons/react/24/outline';
import Link from 'next/link';
import { Button } from '@/app/ui/button';
import { updateInvoice } from '@/app/lib/actions';

import { updateInvoice, State } from '@/app/lib/actions';
import { useActionState } from 'react';

export default function EditInvoiceForm({
invoice,
customers,
}: {
invoice: InvoiceForm;
customers: CustomerField[];
}) {
const initialState: State = { message: null, errors: {} };
const updateInvoiceWithId = updateInvoice.bind(null, invoice.id);
const [state, formAction] = useActionState(updateInvoiceWithId, initialState);

return (
<form action={updateInvoiceWithId}>
<form action={formAction}>
<div className="rounded-md bg-gray-50 p-4 md:p-6">
{/* Customer Name */}
<div className="mb-4">
Expand Down
7 changes: 5 additions & 2 deletions app-router/nextjs-dashboard/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -26,6 +27,8 @@
"@types/bcrypt": "^5.0.2",
"@types/node": "22.10.7",
"@types/react": "19.0.7",
"@types/react-dom": "19.0.3"
"@types/react-dom": "19.0.3",
"eslint": "^9",
"eslint-config-next": "15.3.0"
}
}
Loading