diff --git a/.gitignore b/.gitignore index d9ee72c..4fa944a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ node_modules .next -.env \ No newline at end of file +.env +.eslintrc.json \ No newline at end of file diff --git a/app-router/nextjs-dashboard/README.md b/app-router/nextjs-dashboard/README.md index 89f3eb8..d60a318 100644 --- a/app-router/nextjs-dashboard/README.md +++ b/app-router/nextjs-dashboard/README.md @@ -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 \ No newline at end of file +### [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 \ No newline at end of file diff --git a/app-router/nextjs-dashboard/app/lib/actions.ts b/app-router/nextjs-dashboard/app/lib/actions.ts index 1272502..d963d54 100644 --- a/app-router/nextjs-dashboard/app/lib/actions.ts +++ b/app-router/nextjs-dashboard/app/lib/actions.ts @@ -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 }); @@ -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'); } \ No newline at end of file diff --git a/app-router/nextjs-dashboard/app/page.tsx b/app-router/nextjs-dashboard/app/page.tsx index 52dd456..a46a9cb 100644 --- a/app-router/nextjs-dashboard/app/page.tsx +++ b/app-router/nextjs-dashboard/app/page.tsx @@ -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() { diff --git a/app-router/nextjs-dashboard/app/ui/customers/table.tsx b/app-router/nextjs-dashboard/app/ui/customers/table.tsx index fce2f55..95c947f 100644 --- a/app-router/nextjs-dashboard/app/ui/customers/table.tsx +++ b/app-router/nextjs-dashboard/app/ui/customers/table.tsx @@ -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'; diff --git a/app-router/nextjs-dashboard/app/ui/invoices/create-form.tsx b/app-router/nextjs-dashboard/app/ui/invoices/create-form.tsx index 23a23ba..d1c1fe4 100644 --- a/app-router/nextjs-dashboard/app/ui/invoices/create-form.tsx +++ b/app-router/nextjs-dashboard/app/ui/invoices/create-form.tsx @@ -1,3 +1,6 @@ +'use client' + +import { useActionState } from 'react'; import { CustomerField } from '@/app/lib/definitions'; import Link from 'next/link'; import { @@ -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 ( -
+
{/* Customer Name */}
@@ -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" >
+
+ {state.errors?.customerId && + state.errors.customerId.map((error: string) => ( +

+ {error} +

+ ))} +
{/* Invoice Amount */} @@ -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" /> +
+ {state.errors?.amount && + state.errors.amount.map((error: string) => ( +

+ {error} +

+ ))} +
@@ -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" />