From e6c1f7d5f5f8102e97029d368d5f11a215394788 Mon Sep 17 00:00:00 2001 From: Benjamin Patton Date: Fri, 11 Aug 2023 16:37:24 -0400 Subject: [PATCH 1/2] moved application to app router --- .env.example | 6 - next.config.js | 7 +- package-lock.json | 96 ++++----- package.json | 6 +- src/app/actions.ts | 202 ++++++++++++++++++ src/app/api/auth/[...nextauth]/route.ts | 18 ++ .../api/uploadthing/core.ts} | 29 ++- src/app/api/uploadthing/route.ts | 7 + src/app/dashboard/[customerName]/page.tsx | 7 + src/app/dashboard/layout.tsx | 12 ++ src/app/dashboard/page.tsx | 10 + src/app/layout.tsx | 22 ++ src/app/login/page.tsx | 24 +++ src/app/page.tsx | 3 + src/app/sales/customers/[customerId]/page.tsx | 9 + src/app/sales/customers/layout.tsx | 15 ++ src/app/sales/customers/page.tsx | 7 + src/app/sales/deposits/[depositId]/page.tsx | 50 +++++ src/app/sales/invoices/[invoiceId]/page.tsx | 86 ++++++++ src/app/sales/invoices/layout.tsx | 7 + src/app/sales/invoices/page.tsx | 7 + src/app/sales/layout.tsx | 14 ++ src/app/sales/page.tsx | 3 + src/components/customer-id-page/index.tsx | 143 +++++++------ .../customer-layout/customer-link.tsx | 30 +++ src/components/customer-layout/index.tsx | 86 +++----- src/components/deposit/index.tsx | 171 ++++++++++----- src/components/error-boundary/index.tsx | 11 +- src/components/forms/create-deposit-form.tsx | 57 ++--- src/components/forms/create-invoice-form.tsx | 93 ++++---- src/components/forms/delete-deposit-form.tsx | 78 +++---- src/components/forms/image-upload.tsx | 13 +- .../full-stack-forms/add-customer.tsx | 145 ++++++------- .../full-stack-forms/create-invoice.tsx | 201 +++++------------ src/components/invoice-page/index.tsx | 180 ++++++++-------- src/components/login/index.tsx | 13 +- src/components/logo-renderer/index.tsx | 36 ---- src/components/modal/add-invoice-dialog.tsx | 11 +- src/components/modal/upload-logo-dialog.tsx | 11 +- src/components/navbar/index.tsx | 113 ++++------ src/components/navbar/navigation.tsx | 68 ++++++ src/components/rq-providers.tsx | 14 ++ src/components/sales-nav/index.tsx | 29 +-- src/models/customerserver.ts | 37 ++-- src/pages/_app.tsx | 40 ---- src/pages/_document.tsx | 13 -- src/pages/api/add-customer.ts | 36 ---- src/pages/api/auth/[...nextauth].ts | 16 -- src/pages/api/create-deposit.ts | 59 ----- src/pages/api/create-invoice.ts | 85 -------- src/pages/api/delete-deposit.ts | 40 ---- src/pages/api/get-customer/[params].ts | 32 --- src/pages/api/get-customers-list.ts | 13 -- src/pages/api/get-invoice-list-items.ts | 37 ---- src/pages/api/get-invoice/[params].ts | 42 ---- src/pages/api/get-user-info.ts | 17 -- src/pages/api/uploadthing.ts | 9 - src/pages/index.tsx | 11 - src/pages/login.tsx | 25 --- .../sales/customers/[customerId]/index.tsx | 48 ----- src/pages/sales/customers/index.tsx | 53 ----- .../sales/deposits/[depositId]/index.tsx | 69 ------ src/pages/sales/index.tsx | 52 ----- .../sales/invoices/[invoiceId]/index.tsx | 99 --------- src/pages/sales/invoices/index.tsx | 17 -- src/utils/index.ts | 63 +++--- src/utils/uploadthing.ts | 6 +- tsconfig.json | 28 ++- 68 files changed, 1403 insertions(+), 1694 deletions(-) delete mode 100644 .env.example create mode 100644 src/app/actions.ts create mode 100644 src/app/api/auth/[...nextauth]/route.ts rename src/{server/uploadthing.ts => app/api/uploadthing/core.ts} (73%) create mode 100644 src/app/api/uploadthing/route.ts create mode 100644 src/app/dashboard/[customerName]/page.tsx create mode 100644 src/app/dashboard/layout.tsx create mode 100644 src/app/dashboard/page.tsx create mode 100644 src/app/layout.tsx create mode 100644 src/app/login/page.tsx create mode 100644 src/app/page.tsx create mode 100644 src/app/sales/customers/[customerId]/page.tsx create mode 100644 src/app/sales/customers/layout.tsx create mode 100644 src/app/sales/customers/page.tsx create mode 100644 src/app/sales/deposits/[depositId]/page.tsx create mode 100644 src/app/sales/invoices/[invoiceId]/page.tsx create mode 100644 src/app/sales/invoices/layout.tsx create mode 100644 src/app/sales/invoices/page.tsx create mode 100644 src/app/sales/layout.tsx create mode 100644 src/app/sales/page.tsx create mode 100644 src/components/customer-layout/customer-link.tsx delete mode 100644 src/components/logo-renderer/index.tsx create mode 100644 src/components/navbar/navigation.tsx create mode 100644 src/components/rq-providers.tsx delete mode 100644 src/pages/_app.tsx delete mode 100644 src/pages/_document.tsx delete mode 100644 src/pages/api/add-customer.ts delete mode 100644 src/pages/api/auth/[...nextauth].ts delete mode 100644 src/pages/api/create-deposit.ts delete mode 100644 src/pages/api/create-invoice.ts delete mode 100644 src/pages/api/delete-deposit.ts delete mode 100644 src/pages/api/get-customer/[params].ts delete mode 100644 src/pages/api/get-customers-list.ts delete mode 100644 src/pages/api/get-invoice-list-items.ts delete mode 100644 src/pages/api/get-invoice/[params].ts delete mode 100644 src/pages/api/get-user-info.ts delete mode 100644 src/pages/api/uploadthing.ts delete mode 100644 src/pages/index.tsx delete mode 100644 src/pages/login.tsx delete mode 100644 src/pages/sales/customers/[customerId]/index.tsx delete mode 100644 src/pages/sales/customers/index.tsx delete mode 100644 src/pages/sales/deposits/[depositId]/index.tsx delete mode 100644 src/pages/sales/index.tsx delete mode 100644 src/pages/sales/invoices/[invoiceId]/index.tsx delete mode 100644 src/pages/sales/invoices/index.tsx diff --git a/.env.example b/.env.example deleted file mode 100644 index 5ffbb27..0000000 --- a/.env.example +++ /dev/null @@ -1,6 +0,0 @@ -DATABASE_URL='' -OAUTH_CLIENT_ID="" -OAUTH_CLIENT_SECRET="" -NEXTAUTH_URL="http://localhost:3000" -NEXTAUTH_SECRET="" -UPLOADTHING_SECRET="" \ No newline at end of file diff --git a/next.config.js b/next.config.js index e9c7864..7c70038 100644 --- a/next.config.js +++ b/next.config.js @@ -1,9 +1,10 @@ /** @type {import('next').NextConfig} */ const nextConfig = { - reactStrictMode: true, + // reactStrictMode: true, experimental: { esmExternals: false, // THIS IS THE FLAG THAT MATTERS + serverActions: true, }, -} +}; -module.exports = nextConfig +module.exports = nextConfig; diff --git a/package-lock.json b/package-lock.json index b5bdbd9..8047c00 100644 --- a/package-lock.json +++ b/package-lock.json @@ -30,12 +30,12 @@ "clsx": "^1.2.1", "eslint": "8.43.0", "eslint-config-next": "13.4.7", - "next": "13.4.7", + "next": "^13.4.13", "next-auth": "^4.22.1", "postcss": "8.4.24", "prisma": "^4.16.1", - "react": "18.2.0", - "react-dom": "18.2.0", + "react": "^18.2.0", + "react-dom": "^18.2.0", "react-dropzone": "^14.2.3", "react-error-boundary": "^4.0.10", "react-feather": "^2.0.10", @@ -246,9 +246,9 @@ } }, "node_modules/@next/env": { - "version": "13.4.7", - "resolved": "https://registry.npmjs.org/@next/env/-/env-13.4.7.tgz", - "integrity": "sha512-ZlbiFulnwiFsW9UV1ku1OvX/oyIPLtMk9p/nnvDSwI0s7vSoZdRtxXNsaO+ZXrLv/pMbXVGq4lL8TbY9iuGmVw==" + "version": "13.4.13", + "resolved": "https://registry.npmjs.org/@next/env/-/env-13.4.13.tgz", + "integrity": "sha512-fwz2QgVg08v7ZL7KmbQBLF2PubR/6zQdKBgmHEl3BCyWTEDsAQEijjw2gbFhI1tcKfLdOOJUXntz5vZ4S0Polg==" }, "node_modules/@next/eslint-plugin-next": { "version": "13.4.7", @@ -259,9 +259,9 @@ } }, "node_modules/@next/swc-darwin-arm64": { - "version": "13.4.7", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-13.4.7.tgz", - "integrity": "sha512-VZTxPv1b59KGiv/pZHTO5Gbsdeoxcj2rU2cqJu03btMhHpn3vwzEK0gUSVC/XW96aeGO67X+cMahhwHzef24/w==", + "version": "13.4.13", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-13.4.13.tgz", + "integrity": "sha512-ZptVhHjzUuivnXMNCJ6lER33HN7lC+rZ01z+PM10Ows21NHFYMvGhi5iXkGtBDk6VmtzsbqnAjnx4Oz5um0FjA==", "cpu": [ "arm64" ], @@ -274,9 +274,9 @@ } }, "node_modules/@next/swc-darwin-x64": { - "version": "13.4.7", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-13.4.7.tgz", - "integrity": "sha512-gO2bw+2Ymmga+QYujjvDz9955xvYGrWofmxTq7m70b9pDPvl7aDFABJOZ2a8SRCuSNB5mXU8eTOmVVwyp/nAew==", + "version": "13.4.13", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-13.4.13.tgz", + "integrity": "sha512-t9nTiWCLApw8W4G1kqJyYP7y6/7lyal3PftmRturIxAIBlZss9wrtVN8nci50StDHmIlIDxfguYIEGVr9DbFTg==", "cpu": [ "x64" ], @@ -289,9 +289,9 @@ } }, "node_modules/@next/swc-linux-arm64-gnu": { - "version": "13.4.7", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-13.4.7.tgz", - "integrity": "sha512-6cqp3vf1eHxjIDhEOc7Mh/s8z1cwc/l5B6ZNkOofmZVyu1zsbEM5Hmx64s12Rd9AYgGoiCz4OJ4M/oRnkE16/Q==", + "version": "13.4.13", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-13.4.13.tgz", + "integrity": "sha512-xEHUqC8eqR5DHe8SOmMnDU1K3ggrJ28uIKltrQAwqFSSSmzjnN/XMocZkcVhuncuxYrpbri0iMQstRyRVdQVWg==", "cpu": [ "arm64" ], @@ -304,9 +304,9 @@ } }, "node_modules/@next/swc-linux-arm64-musl": { - "version": "13.4.7", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-13.4.7.tgz", - "integrity": "sha512-T1kD2FWOEy5WPidOn1si0rYmWORNch4a/NR52Ghyp4q7KyxOCuiOfZzyhVC5tsLIBDH3+cNdB5DkD9afpNDaOw==", + "version": "13.4.13", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-13.4.13.tgz", + "integrity": "sha512-sNf3MnLAm8rquSSAoeD9nVcdaDeRYOeey4stOWOyWIgbBDtP+C93amSgH/LPTDoUV7gNiU6f+ghepTjTjRgIUQ==", "cpu": [ "arm64" ], @@ -319,9 +319,9 @@ } }, "node_modules/@next/swc-linux-x64-gnu": { - "version": "13.4.7", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-13.4.7.tgz", - "integrity": "sha512-zaEC+iEiAHNdhl6fuwl0H0shnTzQoAoJiDYBUze8QTntE/GNPfTYpYboxF5LRYIjBwETUatvE0T64W6SKDipvg==", + "version": "13.4.13", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-13.4.13.tgz", + "integrity": "sha512-WhcRaJJSHyx9OWmKjjz+OWHumiPZWRqmM/09Bt7Up4UqUJFFhGExeztR4trtv3rflvULatu9IH/nTV8fUUgaMA==", "cpu": [ "x64" ], @@ -334,9 +334,9 @@ } }, "node_modules/@next/swc-linux-x64-musl": { - "version": "13.4.7", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-13.4.7.tgz", - "integrity": "sha512-X6r12F8d8SKAtYJqLZBBMIwEqcTRvUdVm+xIq+l6pJqlgT2tNsLLf2i5Cl88xSsIytBICGsCNNHd+siD2fbWBA==", + "version": "13.4.13", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-13.4.13.tgz", + "integrity": "sha512-+Y4LLhOWWZQIDKVwr2R17lq2KSN0F1c30QVgGIWfnjjHpH8nrIWHEndhqYU+iFuW8It78CiJjQKTw4f51HD7jA==", "cpu": [ "x64" ], @@ -349,9 +349,9 @@ } }, "node_modules/@next/swc-win32-arm64-msvc": { - "version": "13.4.7", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-13.4.7.tgz", - "integrity": "sha512-NPnmnV+vEIxnu6SUvjnuaWRglZzw4ox5n/MQTxeUhb5iwVWFedolPFebMNwgrWu4AELwvTdGtWjqof53AiWHcw==", + "version": "13.4.13", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-13.4.13.tgz", + "integrity": "sha512-rWurdOR20uxjfqd1X9vDAgv0Jb26KjyL8akF9CBeFqX8rVaBAnW/Wf6A2gYEwyYY4Bai3T7p1kro6DFrsvBAAw==", "cpu": [ "arm64" ], @@ -364,9 +364,9 @@ } }, "node_modules/@next/swc-win32-ia32-msvc": { - "version": "13.4.7", - "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-13.4.7.tgz", - "integrity": "sha512-6Hxijm6/a8XqLQpOOf/XuwWRhcuc/g4rBB2oxjgCMuV9Xlr2bLs5+lXyh8w9YbAUMYR3iC9mgOlXbHa79elmXw==", + "version": "13.4.13", + "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-13.4.13.tgz", + "integrity": "sha512-E8bSPwRuY5ibJ3CzLQmJEt8qaWrPYuUTwnrwygPUEWoLzD5YRx9SD37oXRdU81TgGwDzCxpl7z5Nqlfk50xAog==", "cpu": [ "ia32" ], @@ -379,9 +379,9 @@ } }, "node_modules/@next/swc-win32-x64-msvc": { - "version": "13.4.7", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-13.4.7.tgz", - "integrity": "sha512-sW9Yt36Db1nXJL+mTr2Wo0y+VkPWeYhygvcHj1FF0srVtV+VoDjxleKtny21QHaG05zdeZnw2fCtf2+dEqgwqA==", + "version": "13.4.13", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-13.4.13.tgz", + "integrity": "sha512-4KlyC6jWRubPnppgfYsNTPeWfGCxtWLh5vaOAW/kdzAk9widqho8Qb5S4K2vHmal1tsURi7Onk2MMCV1phvyqA==", "cpu": [ "x64" ], @@ -3452,11 +3452,11 @@ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==" }, "node_modules/next": { - "version": "13.4.7", - "resolved": "https://registry.npmjs.org/next/-/next-13.4.7.tgz", - "integrity": "sha512-M8z3k9VmG51SRT6v5uDKdJXcAqLzP3C+vaKfLIAM0Mhx1um1G7MDnO63+m52qPdZfrTFzMZNzfsgvm3ghuVHIQ==", + "version": "13.4.13", + "resolved": "https://registry.npmjs.org/next/-/next-13.4.13.tgz", + "integrity": "sha512-A3YVbVDNeXLhWsZ8Nf6IkxmNlmTNz0yVg186NJ97tGZqPDdPzTrHotJ+A1cuJm2XfuWPrKOUZILl5iBQkIf8Jw==", "dependencies": { - "@next/env": "13.4.7", + "@next/env": "13.4.13", "@swc/helpers": "0.5.1", "busboy": "1.6.0", "caniuse-lite": "^1.0.30001406", @@ -3472,19 +3472,18 @@ "node": ">=16.8.0" }, "optionalDependencies": { - "@next/swc-darwin-arm64": "13.4.7", - "@next/swc-darwin-x64": "13.4.7", - "@next/swc-linux-arm64-gnu": "13.4.7", - "@next/swc-linux-arm64-musl": "13.4.7", - "@next/swc-linux-x64-gnu": "13.4.7", - "@next/swc-linux-x64-musl": "13.4.7", - "@next/swc-win32-arm64-msvc": "13.4.7", - "@next/swc-win32-ia32-msvc": "13.4.7", - "@next/swc-win32-x64-msvc": "13.4.7" + "@next/swc-darwin-arm64": "13.4.13", + "@next/swc-darwin-x64": "13.4.13", + "@next/swc-linux-arm64-gnu": "13.4.13", + "@next/swc-linux-arm64-musl": "13.4.13", + "@next/swc-linux-x64-gnu": "13.4.13", + "@next/swc-linux-x64-musl": "13.4.13", + "@next/swc-win32-arm64-msvc": "13.4.13", + "@next/swc-win32-ia32-msvc": "13.4.13", + "@next/swc-win32-x64-msvc": "13.4.13" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", - "fibers": ">= 3.1.0", "react": "^18.2.0", "react-dom": "^18.2.0", "sass": "^1.3.0" @@ -3493,9 +3492,6 @@ "@opentelemetry/api": { "optional": true }, - "fibers": { - "optional": true - }, "sass": { "optional": true } diff --git a/package.json b/package.json index 85c755a..44b5b61 100644 --- a/package.json +++ b/package.json @@ -31,12 +31,12 @@ "clsx": "^1.2.1", "eslint": "8.43.0", "eslint-config-next": "13.4.7", - "next": "13.4.7", + "next": "^13.4.13", "next-auth": "^4.22.1", "postcss": "8.4.24", "prisma": "^4.16.1", - "react": "18.2.0", - "react-dom": "18.2.0", + "react": "^18.2.0", + "react-dom": "^18.2.0", "react-dropzone": "^14.2.3", "react-error-boundary": "^4.0.10", "react-feather": "^2.0.10", diff --git a/src/app/actions.ts b/src/app/actions.ts new file mode 100644 index 0000000..37f2c1a --- /dev/null +++ b/src/app/actions.ts @@ -0,0 +1,202 @@ +"use server"; + +import { getServerSession } from "next-auth"; +import { authOptions } from "./api/auth/[...nextauth]/route"; +import { getUserByEmail } from "@/models/userserver"; +import { createCustomer } from "@/models/customerserver"; +import { revalidatePath } from "next/cache"; +import { + LineItemFields, + createInvoice, + findInvoiceByDepositId, +} from "@/models/invoiceserver"; +import { + parseDate, + validateAmount, + validateCustomerId, + validateDepositDate, + validateDueDate, + validateLineItemQuantity, + validateLineItemUnitPrice, +} from "@/utils"; +import { NextResponse } from "next/server"; +import invariant from "tiny-invariant"; +import { redirect } from "next/navigation"; +import { createDeposit, deleteDeposit } from "@/models/depositserver"; + +export async function addCustomer(name: string, email: string) { + const session = await getServerSession(authOptions); + if (!session) { + throw new Error("unauthorized"); + } + + if (!session?.user?.email) { + throw new Error( + "please add an email to your github profile & logout out and signin again" + ); + } + + const user = await getUserByEmail(session?.user?.email as string); + + if (!user) { + throw new Error( + "There is a server error and you shouldnt be getting this error" + ); + } + + const customer = await createCustomer({ + name, + email, + user_email: user?.email as string, + }); + console.log("customer", customer); + revalidatePath("/sales/customers"); +} + +export async function addInvoice(formData: FormData) { + const session = await getServerSession(authOptions); + if (!session) { + throw new Error("Unauthorized"); + } + + const intent = formData.get("intent"); + + switch (intent) { + case "create": { + const customerId = formData.get("customerId"); + const dueDateString = formData.get("dueDateString"); + invariant(typeof customerId === "string", "customerId is required"); + invariant(typeof dueDateString === "string", "dueDate is required"); + const dueDate = parseDate(dueDateString); + + const lineItemIds = formData.getAll("lineItemId"); + const lineItemQuantities = formData.getAll("quantity"); + const lineItemUnitPrices = formData.getAll("unitPrice"); + const lineItemDescriptions = formData.getAll("description"); + const lineItems: Array = []; + for (let i = 0; i < lineItemQuantities.length; i++) { + const quantity = +lineItemQuantities[i]; + const unitPrice = +lineItemUnitPrices[i]; + const description = lineItemDescriptions[i]; + invariant(typeof quantity === "number", "quantity is required"); + invariant(typeof unitPrice === "number", "unitPrice is required"); + invariant(typeof description === "string", "description is required"); + + lineItems.push({ quantity, unitPrice, description }); + } + + const errors = { + customerId: validateCustomerId(customerId), + dueDate: validateDueDate(dueDate), + lineItems: lineItems.reduce((acc, lineItem, index) => { + const id = lineItemIds[index]; + invariant(typeof id === "string", "lineItem ids are required"); + acc[id] = { + quantity: validateLineItemQuantity(lineItem.quantity), + unitPrice: validateLineItemUnitPrice(lineItem.unitPrice), + }; + return acc; + }, {} as Record), + }; + + const customerIdHasError = errors.customerId !== null; + const dueDateHasError = errors.dueDate !== null; + const lineItemsHaveErrors = Object.values(errors.lineItems).some( + (lineItem) => Object.values(lineItem).some(Boolean) + ); + const hasErrors = + dueDateHasError || customerIdHasError || lineItemsHaveErrors; + + if (hasErrors) { + return NextResponse.json(errors, { status: 400 }); + } + + const invoice = await createInvoice({ + dueDate, + customerId, + lineItems, + }); + redirect(`/sales/invoices/${invoice.id}`); + } + } + throw new Error(`Unsupported intent: ${intent}`); +} + +export async function removeDeposit({ + depositId, + intent, +}: { + depositId: string; + intent: string; +}) { + const session = await getServerSession(authOptions); + if (!session) { + throw new Error("Unauthorized"); + } + // let { intent } = await req.json(); + // const { depositId } = context.params; + + invariant(typeof depositId === "string", "params.depositId is not available"); + invariant(typeof intent === "string", "intent must be a string"); + + let redirectTo; + const associatedInvoiceId = await findInvoiceByDepositId(depositId); + + if (!associatedInvoiceId) { + redirectTo = "/sales/invoices"; + } else { + redirectTo = `/sales/invoices/${associatedInvoiceId.id}`; + } + + switch (intent) { + case "delete": { + await deleteDeposit(depositId); + redirect(redirectTo); + } + default: { + throw new Error(`Unsupported intent: ${intent}`); + } + } +} + +export async function addDeposit(formData: FormData, invoiceId: string) { + const session = await getServerSession(authOptions); + if (!session) { + throw new Error("Unauthorized"); + } + const intent = formData.get("intent"); + + invariant(typeof intent === "string", "intent required"); + + switch (intent) { + case "create-deposit": { + const amount = Number(formData.get("amount")); + const depositDateString = formData.get("depositDate"); + const note = formData.get("note"); + invariant(!Number.isNaN(amount), "amount must be a number"); + invariant(typeof depositDateString === "string", "dueDate is required"); + invariant(typeof note === "string", "dueDate is required"); + const depositDate = parseDate(depositDateString); + + const errors = { + amount: validateAmount(amount), + depositDate: validateDepositDate(depositDate), + }; + const hasErrors = Object.values(errors).some( + (errorMessage) => errorMessage + ); + if (hasErrors) { + return errors; + } + + const newDeposit = await createDeposit({ + invoiceId, + amount, + note, + depositDate, + }); + console.log("new deposit data", newDeposit); + revalidatePath("/sales/invoices/[invoiceId]"); + } + } +} diff --git a/src/app/api/auth/[...nextauth]/route.ts b/src/app/api/auth/[...nextauth]/route.ts new file mode 100644 index 0000000..9fcc0f9 --- /dev/null +++ b/src/app/api/auth/[...nextauth]/route.ts @@ -0,0 +1,18 @@ +import NextAuth, { NextAuthOptions } from "next-auth"; +import GithubProvider from "next-auth/providers/github"; +import { PrismaAdapter } from "@next-auth/prisma-adapter"; +import prisma from "@/utils/dbserver"; + +export const authOptions: NextAuthOptions = { + adapter: PrismaAdapter(prisma), + providers: [ + GithubProvider({ + clientId: process.env.OAUTH_CLIENT_ID as string, + clientSecret: process.env.OAUTH_CLIENT_SECRET as string, + }), + ], +}; + +const handler = NextAuth(authOptions); + +export { handler as GET, handler as POST }; diff --git a/src/server/uploadthing.ts b/src/app/api/uploadthing/core.ts similarity index 73% rename from src/server/uploadthing.ts rename to src/app/api/uploadthing/core.ts index a5d6ff6..a5bdf61 100644 --- a/src/server/uploadthing.ts +++ b/src/app/api/uploadthing/core.ts @@ -1,12 +1,11 @@ import { getServerSession } from "next-auth/next"; -import { authOptions } from "@/pages/api/auth/[...nextauth]"; - -import { createUploadthing, type FileRouter } from "uploadthing/next-legacy"; +import { authOptions } from "@/app/api/auth/[...nextauth]/route"; + +import { createUploadthing, type FileRouter } from "uploadthing/next"; import { getUserByEmail, updateUser } from "@/models/userserver"; - + const f = createUploadthing(); - - + // FileRouter for your app, can contain multiple FileRoutes export const ourFileRouter = { // Define as many FileRoutes as you like, each with a unique routeSlug @@ -14,21 +13,21 @@ export const ourFileRouter = { // Set permissions and file types for this FileRoute .middleware(async ({ req, res }) => { // This code runs on your server before upload - const session = await getServerSession(req, res, authOptions); - + const session = await getServerSession(authOptions); + // If you throw, the user will not be able to upload if (!session || !session.user) throw new Error("Unauthorized"); - + // Whatever is returned here is accessible in onUploadComplete as `metadata` return { user_email: session.user.email, user_name: session.user?.name }; }) .onUploadComplete(async ({ metadata, file }) => { // This code RUNS ON YOUR SERVER after upload - let user = await getUserByEmail(metadata.user_email as string); - if (user) { - await updateUser({ id: user.id, logoUrl: file.url }) - } + let user = await getUserByEmail(metadata.user_email as string); + if (user) { + await updateUser({ id: user.id, logoUrl: file.url }); + } }), } satisfies FileRouter; - -export type OurFileRouter = typeof ourFileRouter; \ No newline at end of file + +export type OurFileRouter = typeof ourFileRouter; diff --git a/src/app/api/uploadthing/route.ts b/src/app/api/uploadthing/route.ts new file mode 100644 index 0000000..3705a2b --- /dev/null +++ b/src/app/api/uploadthing/route.ts @@ -0,0 +1,7 @@ +import { createNextRouteHandler } from "uploadthing/next"; + +import { ourFileRouter } from "./core"; + +export const { GET, POST } = createNextRouteHandler({ + router: ourFileRouter, +}); diff --git a/src/app/dashboard/[customerName]/page.tsx b/src/app/dashboard/[customerName]/page.tsx new file mode 100644 index 0000000..f8a6a78 --- /dev/null +++ b/src/app/dashboard/[customerName]/page.tsx @@ -0,0 +1,7 @@ +export default function Customer({ + params, +}: { + params: { customerName: string }; +}) { + return

{params.customerName}

; +} diff --git a/src/app/dashboard/layout.tsx b/src/app/dashboard/layout.tsx new file mode 100644 index 0000000..0b79cbb --- /dev/null +++ b/src/app/dashboard/layout.tsx @@ -0,0 +1,12 @@ +export default function DashboardLayout({ + children, +}: { + children: React.ReactNode; +}) { + return ( + <> +

DashboardLayout

+ {children} + + ); +} diff --git a/src/app/dashboard/page.tsx b/src/app/dashboard/page.tsx new file mode 100644 index 0000000..14f3d8c --- /dev/null +++ b/src/app/dashboard/page.tsx @@ -0,0 +1,10 @@ +import Link from "next/link"; + +export default function DashboardPage() { + return ( +
+

Hello Dashboard page

+ Michael Scott +
+ ); +} diff --git a/src/app/layout.tsx b/src/app/layout.tsx new file mode 100644 index 0000000..0d63a94 --- /dev/null +++ b/src/app/layout.tsx @@ -0,0 +1,22 @@ +import * as React from "react"; +import "../styles/globals.css"; +import Navbar from "../components/navbar"; + +// export const metadata = { +// title: "Next.js", +// description: "Generated by Next.js", +// }; + +export default function RootLayout({ + children, +}: { + children: React.ReactNode; +}) { + return ( + + + {children} + + + ); +} diff --git a/src/app/login/page.tsx b/src/app/login/page.tsx new file mode 100644 index 0000000..0576374 --- /dev/null +++ b/src/app/login/page.tsx @@ -0,0 +1,24 @@ +import { FullFakebooksLogo } from "@/components"; +import { SignIn } from "@/components/login"; +import { authOptions } from "@/app/api/auth/[...nextauth]/route"; +import { getServerSession } from "next-auth"; +import { redirect } from "next/navigation"; + +async function LoginPage() { + const session = await getServerSession(authOptions); + if (session) { + redirect("/"); + } + return ( +
+

+ +

+
+ +
+
+ ); +} + +export default LoginPage; diff --git a/src/app/page.tsx b/src/app/page.tsx new file mode 100644 index 0000000..c746a0b --- /dev/null +++ b/src/app/page.tsx @@ -0,0 +1,3 @@ +export default function Page({ children }: { children: React.ReactNode }) { + return <>{children}; +} diff --git a/src/app/sales/customers/[customerId]/page.tsx b/src/app/sales/customers/[customerId]/page.tsx new file mode 100644 index 0000000..db7843f --- /dev/null +++ b/src/app/sales/customers/[customerId]/page.tsx @@ -0,0 +1,9 @@ +import CustomerIdPage from "@/components/customer-id-page"; + +async function CustomerIdRoute({ params }: { params: { customerId: string } }) { + const { customerId } = params; + + return ; +} + +export default CustomerIdRoute; diff --git a/src/app/sales/customers/layout.tsx b/src/app/sales/customers/layout.tsx new file mode 100644 index 0000000..279e7b6 --- /dev/null +++ b/src/app/sales/customers/layout.tsx @@ -0,0 +1,15 @@ +import CustomerLayout from "@/components/customer-layout"; +import { getServerSession } from "next-auth"; +import { redirect } from "next/navigation"; +import { authOptions } from "@/app/api/auth/[...nextauth]/route"; + +async function CustomersLayout({ children }: { children: React.ReactNode }) { + const session = await getServerSession(authOptions); + if (!session) { + redirect("/login"); + } + + return {children}; +} + +export default CustomersLayout; diff --git a/src/app/sales/customers/page.tsx b/src/app/sales/customers/page.tsx new file mode 100644 index 0000000..ddb25eb --- /dev/null +++ b/src/app/sales/customers/page.tsx @@ -0,0 +1,7 @@ +export default function CustomersPage({ + children, +}: { + children: React.ReactNode; +}) { + return <>{children}; +} diff --git a/src/app/sales/deposits/[depositId]/page.tsx b/src/app/sales/deposits/[depositId]/page.tsx new file mode 100644 index 0000000..6264e14 --- /dev/null +++ b/src/app/sales/deposits/[depositId]/page.tsx @@ -0,0 +1,50 @@ +import { getDepositDetails } from "@/models/depositserver"; +import invariant from "tiny-invariant"; +import { TrashIcon } from "@/components"; +import { getServerSession } from "next-auth"; +import { authOptions } from "@/app/api/auth/[...nextauth]/route"; +import { redirect } from "next/navigation"; +import DeleteDepositForm from "@/components/forms/delete-deposit-form"; + +export default async function DepositRoute({ + params, +}: { + params: { depositId: string }; +}) { + const user = await getServerSession(authOptions); + if (!user) { + redirect("/login"); + } + + const { depositId } = params; + invariant(typeof depositId === "string", "params.depositId is not available"); + const depositDetails = await getDepositDetails(depositId); + if (!depositDetails) { + throw new Response("not found", { status: 404 }); + } + + const data = { + depositNote: depositDetails.note, + depositId, + }; + return ( +
+
+ {data.depositNote ? ( + + Note: +
+ {data.depositNote} +
+ ) : ( + + No note + + )} +
+ +
+
+
+ ); +} diff --git a/src/app/sales/invoices/[invoiceId]/page.tsx b/src/app/sales/invoices/[invoiceId]/page.tsx new file mode 100644 index 0000000..38de2f8 --- /dev/null +++ b/src/app/sales/invoices/[invoiceId]/page.tsx @@ -0,0 +1,86 @@ +import Link from "next/link"; +import { LabelText } from "@/components"; +import { currencyFormatter } from "@/utils"; +import { Deposits, LineItemDisplay } from "@/components/deposit"; +import { LineItem } from "@prisma/client"; +import { redirect } from "next/navigation"; +import { getInvoiceDetails } from "@/models/invoiceserver"; + +export const lineItemClassName = + "flex justify-between border-t border-gray-100 py-4 text-[14px] leading-[24px]"; + +async function InvoiceRoute({ params }: { params: { invoiceId: string } }) { + const { invoiceId } = params; + + const invoiceDetails = await getInvoiceDetails(invoiceId); + if (!invoiceDetails) { + return redirect("/login"); + } + const invoiceData = { + customerName: invoiceDetails.invoice.customer.name, + customerId: invoiceDetails.invoice.customer.id, + totalAmount: invoiceDetails.totalAmount, + dueStatus: invoiceDetails.dueStatus, + dueDisplay: invoiceDetails.dueStatusDisplay, + invoiceDateDisplay: invoiceDetails.invoice.invoiceDate.toLocaleDateString(), + lineItems: invoiceDetails.invoice.lineItems.map((li) => ({ + id: li.id, + description: li.description, + quantity: li.quantity, + unitPrice: li.unitPrice, + })), + deposits: invoiceDetails.invoice.deposits.map((deposit) => ({ + id: deposit.id, + amount: deposit.amount, + depositDateFormatted: deposit.depositDate.toLocaleDateString(), + })), + invoiceId: invoiceId, + }; + + return ( + <> +
+ + {invoiceData.customerName} + +
+ {currencyFormatter.format(invoiceData.totalAmount)} +
+ + + {invoiceData.dueDisplay} + + {` • Invoiced ${invoiceData.invoiceDateDisplay}`} + +
+ {invoiceData.lineItems.map((item) => ( + + ))} +
+
Net Total
+
{currencyFormatter.format(invoiceData.totalAmount)}
+
+
+ +
+ + ); +} + +export default InvoiceRoute; diff --git a/src/app/sales/invoices/layout.tsx b/src/app/sales/invoices/layout.tsx new file mode 100644 index 0000000..6a4ccfc --- /dev/null +++ b/src/app/sales/invoices/layout.tsx @@ -0,0 +1,7 @@ +import InvoicesPage from "@/components/invoice-page"; + +function InvoicesLayout({ children }: { children: React.ReactNode }) { + return {children}; +} + +export default InvoicesLayout; diff --git a/src/app/sales/invoices/page.tsx b/src/app/sales/invoices/page.tsx new file mode 100644 index 0000000..e7b7b84 --- /dev/null +++ b/src/app/sales/invoices/page.tsx @@ -0,0 +1,7 @@ +export default function InvoicesPage({ + children, +}: { + children: React.ReactNode; +}) { + return <>{children}; +} diff --git a/src/app/sales/layout.tsx b/src/app/sales/layout.tsx new file mode 100644 index 0000000..1cbfe3a --- /dev/null +++ b/src/app/sales/layout.tsx @@ -0,0 +1,14 @@ +import SalesNav from "@/components/sales-nav"; +import { authOptions } from "@/app/api/auth/[...nextauth]/route"; +import { getServerSession } from "next-auth"; +import { redirect } from "next/navigation"; + +function SalesLayout({ children }: { children: React.ReactNode }) { + const session = getServerSession(authOptions); + if (!session) { + redirect("/login"); + } + return {children}; +} + +export default SalesLayout; diff --git a/src/app/sales/page.tsx b/src/app/sales/page.tsx new file mode 100644 index 0000000..74460e1 --- /dev/null +++ b/src/app/sales/page.tsx @@ -0,0 +1,3 @@ +export default function SalesPage({ children }: { children: React.ReactNode }) { + return <>{children}; +} diff --git a/src/components/customer-id-page/index.tsx b/src/components/customer-id-page/index.tsx index ca79abe..17e780e 100644 --- a/src/components/customer-id-page/index.tsx +++ b/src/components/customer-id-page/index.tsx @@ -1,71 +1,88 @@ -import { currencyFormatter } from '../../utils' -import Link from 'next/link' -import { CustomerSkeleton } from '../customer-layout' -import { ErrorBoundaryComponent } from '../error-boundary' +import { currencyFormatter } from "../../utils"; +import Link from "next/link"; +import { CustomerSkeleton } from "../customer-layout"; +import { ErrorBoundaryComponent } from "../error-boundary"; +import { getCustomerDetails, getCustomerInfo } from "@/models/customerserver"; +import { redirect } from "next/navigation"; +import { Suspense } from "react"; -const lineItemClassName = 'border-t border-gray-100 text-[14px] h-[56px]' - -export default function CustomerIdPage({ - customerInfo, -}: { - customerInfo: any -}) { - const { data, isLoading, isError, isSuccess, error } = customerInfo - - if (isError) throw error - - // const showSkeleton = useSpinDelay(Boolean(isLoading)) +const lineItemClassName = "border-t border-gray-100 text-[14px] h-[56px]"; +export default function CustomerIdPage({ customerId }: { customerId: string }) { return ( <> - {isLoading && } - {isSuccess && data && ( -
-
- {data.customerInfo.email} -
-
- {data.customerInfo.name} -
-
-
Invoices
-
- - - {data.customerDetails?.invoiceDetails.map((details: any) => ( - - - - - - ))} - -
- - {details.number} - - - {details.dueStatusDisplay} - - {currencyFormatter.format(details.totalAmount)} -
-
- )} +
+ }> + {/* @ts-expect-error */} + + + }> + {/* @ts-expect-error */} + + +
- ) + ); +} + +async function CustomerInfo({ customerId }: { customerId: string }) { + const customerInfo = await getCustomerInfo(customerId); + if (!customerInfo) { + return redirect("/login"); + } + + return ( + <> +
+ {customerInfo.email} +
+
+ {customerInfo.name} +
+
+
Invoices
+
+ + ); +} + +async function CustomerDetails({ customerId }: { customerId: string }) { + const customerDetails = await getCustomerDetails(customerId); + + return ( + + + {customerDetails?.invoiceDetails.map((details: any) => ( + + + + + + ))} + +
+ + {details.number} + + + {details.dueStatusDisplay} + + {currencyFormatter.format(details.totalAmount)} +
+ ); } diff --git a/src/components/customer-layout/customer-link.tsx b/src/components/customer-layout/customer-link.tsx new file mode 100644 index 0000000..89f204a --- /dev/null +++ b/src/components/customer-layout/customer-link.tsx @@ -0,0 +1,30 @@ +"use client"; +import { Customer } from "@prisma/client"; +import Link from "next/link"; +import { usePathname } from "next/navigation"; + +export default function CustomerLink({ + children, + customer, +}: { + children: React.ReactNode; + customer: Pick; +}) { + const pathname = usePathname(); + + const individualCustomerPathisActive = + pathname === `/sales/customers/[customerId]`; + return ( + + {children} + + ); +} diff --git a/src/components/customer-layout/index.tsx b/src/components/customer-layout/index.tsx index d030473..dec6499 100644 --- a/src/components/customer-layout/index.tsx +++ b/src/components/customer-layout/index.tsx @@ -1,77 +1,37 @@ -import Link from 'next/link' -import { InvoiceDetailsFallback } from '../index' -import { Customer } from '../../models/customerserver' -import { usePathname } from 'next/navigation' -import { useQuery } from '@tanstack/react-query' -import { fetcher } from '@/utils' -import BetterAddCustomerForm from '../full-stack-forms/add-customer' -import { ErrorBoundaryComponent } from '../error-boundary' +import { InvoiceDetailsFallback } from "../index"; +import { Customer, getCustomerListItems } from "../../models/customerserver"; +import BetterAddCustomerForm from "../full-stack-forms/add-customer"; +import { ErrorBoundaryComponent } from "../error-boundary"; +import CustomerLink from "./customer-link"; +import { Suspense } from "react"; export default function CustomerLayout({ children, }: { - children: React.ReactNode + children: React.ReactNode; }) { - const pathname = usePathname() - - const { data, isLoading, isError, error } = useQuery( - ['customers'], - () => fetcher('/api/get-customers-list'), - { useErrorBoundary: true } - ) - - if (isLoading) { - return
Loading...
- } - - if (isError) throw error - - const newCustomerPathisActive = pathname === '/sales/customers/newCustomer' - const individualCustomerPathisActive = - pathname === `/sales/customers/[customerId]` - return (
-
- {data?.customers?.map( - (customer: Pick) => ( - -
-
{customer.name}
-
-
-
{customer.email}
-
- - ) - )} -
+ }> + {/* @ts-expect-error */} + +
<>{children}
- ) + ); } export function CustomerSkeleton() { @@ -79,5 +39,23 @@ export function CustomerSkeleton() {
- ) + ); +} + +async function Customers() { + const customers = await getCustomerListItems(); + return ( +
+ {customers?.map((customer: Pick) => ( + +
+
{customer.name}
+
+
+
{customer.email}
+
+
+ ))} +
+ ); } diff --git a/src/components/deposit/index.tsx b/src/components/deposit/index.tsx index 0969fdb..b7d99b2 100644 --- a/src/components/deposit/index.tsx +++ b/src/components/deposit/index.tsx @@ -1,65 +1,49 @@ -import { currencyFormatter } from '../../utils' -import Link from 'next/link' -import CreateDepositForm from '../forms/create-deposit-form' +"use client"; +import { currencyFormatter, parseDate } from "../../utils"; +import Link from "next/link"; +import * as z from "zod"; +import { LabelText, inputClasses, submitButtonClasses, SpinnerIcon } from ".."; +import { addDeposit } from "@/app/actions"; +import { useRef, experimental_useOptimistic as useOptimistic } from "react"; +import { experimental_useFormStatus as useFormStatus } from "react-dom"; +import invariant from "tiny-invariant"; interface DepositFormControlsCollection extends HTMLFormControlsCollection { - amount?: HTMLInputElement - depositDate?: HTMLInputElement - note?: HTMLInputElement - intent?: HTMLButtonElement + amount?: HTMLInputElement; + depositDate?: HTMLInputElement; + note?: HTMLInputElement; + intent?: HTMLButtonElement; } interface DepositData { - customerName: string - customerId: string - totalAmount: number - dueStatus: 'paid' | 'overpaid' | 'overdue' | 'due' - dueDisplay: string - invoiceDateDisplay: string - lineItems: { - id: string - description: string - quantity: number - unitPrice: number - }[] deposits: { - id: string - amount: number - depositDateFormatted: string - }[] - invoiceId: string + id: string; + amount: number; + depositDateFormatted: string; + }[]; + invoiceId: string; } const lineItemClassName = - 'flex justify-between border-t border-gray-100 py-4 text-[14px] leading-[24px]' + "flex justify-between border-t border-gray-100 py-4 text-[14px] leading-[24px]"; export function Deposits({ data }: { data: DepositData }) { + const [optimisticDeposits, addOptimisticDeposits] = useOptimistic( + data.deposits, + ( + state: DepositData["deposits"], + newDeposit: DepositData["deposits"][number] + ) => [...state, newDeposit] + ); + const formRef = useRef(); // this is purely for helping the user have a better experience. - // if (newDepositFetcher.submission) { - // const amount = Number(newDepositFetcher.submission.formData.get('amount')) - // const depositDateVal = - // newDepositFetcher.submission.formData.get('depositDate') - // const depositDate = - // typeof depositDateVal === 'string' ? parseDate(depositDateVal) : null - // if ( - // !validateAmount(amount) && - // depositDate && - // !validateDepositDate(depositDate) - // ) { - // deposits.push({ - // id: 'new', - // amount, - // depositDateFormatted: depositDate.toLocaleDateString(), - // }) - // } - // } - + console.log("optimisticDeposits", optimisticDeposits); return (
Deposits
- {data.deposits.length > 0 ? ( - data.deposits.map((deposit) => ( + {optimisticDeposits.length > 0 ? ( + optimisticDeposits.map((deposit) => (
None yet
)} - +
{ + const amount = Number(formData.get("amount")); + const depositDateString = formData.get("depositDate"); + + invariant( + typeof depositDateString === "string", + "dueDate is required" + ); + const depositDate = parseDate(depositDateString); + const newDeposit = { + id: "new", + amount, + depositDateFormatted: depositDate.toLocaleDateString(), + }; + addOptimisticDeposits(newDeposit); + formRef.current.reset(); + await addDeposit(formData, data.invoiceId); + }} + ref={formRef} + className="grid grid-cols-1 gap-x-4 gap-y-2 lg:grid-cols-2" + > +
+
+ + + +
+ +
+
+
+ + + +
+ +
+
+
+ + + + +
+
+ +
+
+
- ) + ); } export function LineItemDisplay({ @@ -83,9 +135,9 @@ export function LineItemDisplay({ quantity, unitPrice, }: { - description: string - quantity: number - unitPrice: number + description: string; + quantity: number; + unitPrice: number; }) { return (
@@ -93,5 +145,20 @@ export function LineItemDisplay({ {quantity === 1 ? null :
({quantity}x)
}
{currencyFormatter.format(unitPrice)}
- ) + ); +} + +function CreateDepositButton() { + const { pending } = useFormStatus(); + return ( + + ); } diff --git a/src/components/error-boundary/index.tsx b/src/components/error-boundary/index.tsx index bc3bcca..443681f 100644 --- a/src/components/error-boundary/index.tsx +++ b/src/components/error-boundary/index.tsx @@ -1,20 +1,21 @@ -import { ErrorBoundary } from 'react-error-boundary' +"use client"; +import { ErrorBoundary } from "react-error-boundary"; function ErrorFallback({ error }: any) { return (

Something went wrong:

-
{error.message}
+
{error.message}
- ) + ); } export function ErrorBoundaryComponent({ children, }: { - children: React.ReactNode + children: React.ReactNode; }) { return ( {children} - ) + ); } diff --git a/src/components/forms/create-deposit-form.tsx b/src/components/forms/create-deposit-form.tsx index 0d53fe3..caf1eb3 100644 --- a/src/components/forms/create-deposit-form.tsx +++ b/src/components/forms/create-deposit-form.tsx @@ -1,10 +1,11 @@ -import { LabelText, inputClasses, submitButtonClasses } from '..' -import { useForm } from 'react-hook-form' -import { zodResolver } from '@hookform/resolvers/zod' -import * as z from 'zod' -import React from 'react' -import { useRouter } from 'next/router' -import { useMutation } from '@tanstack/react-query' +"use client"; +import { LabelText, inputClasses, submitButtonClasses } from ".."; +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import * as z from "zod"; +import React from "react"; +import { useRouter } from "next/navigation"; +import { useMutation } from "@tanstack/react-query"; const createDepositSchema = z.object({ formAmount: z.string(), @@ -12,12 +13,12 @@ const createDepositSchema = z.object({ invoiceId: z.string(), formNote: z.string(), intent: z.string(), -}) +}); -type CreateDepositFormData = z.infer +type CreateDepositFormData = z.infer; export default function CreateDepositForm({ data }: { data: any }) { - const [fetchingError, setFetchingError] = React.useState(null) + const [fetchingError, setFetchingError] = React.useState(null); const { register, handleSubmit, @@ -25,28 +26,28 @@ export default function CreateDepositForm({ data }: { data: any }) { formState: { errors }, } = useForm({ resolver: zodResolver(createDepositSchema), - }) + }); - const router = useRouter() + const router = useRouter(); const { mutateAsync, error, isError } = useMutation({ mutationFn: (formData: any) => { - return fetch('/api/create-deposit', { - method: 'POST', + return fetch("/api/deposits", { + method: "POST", body: JSON.stringify(formData), - }) + }); }, - }) + }); async function _createDepositAction(data: CreateDepositFormData) { - await new Promise((resolve) => setTimeout(resolve, 200)) - const result = await mutateAsync(data) - const newDeposit = await result.json() + await new Promise((resolve) => setTimeout(resolve, 200)); + const result = await mutateAsync(data); + const newDeposit = await result.json(); if (newDeposit.status !== 200) { - setFetchingError(error) + setFetchingError(error); } - reset() - router.push(`/sales/invoices/${data.invoiceId}`) + reset(); + router.push(`/sales/invoices/${data.invoiceId}`); } return ( @@ -62,7 +63,7 @@ export default function CreateDepositForm({ data }: { data: any }) {
@@ -104,7 +105,7 @@ export default function CreateDepositForm({ data }: { data: any }) { @@ -116,7 +117,7 @@ export default function CreateDepositForm({ data }: { data: any }) {
- ) + ); } diff --git a/src/components/forms/create-invoice-form.tsx b/src/components/forms/create-invoice-form.tsx index 9bd8483..062d1fa 100644 --- a/src/components/forms/create-invoice-form.tsx +++ b/src/components/forms/create-invoice-form.tsx @@ -1,25 +1,26 @@ -import { useState, useId } from 'react' +"use client"; +import { useState, useId } from "react"; import { inputClasses, LabelText, PlusIcon, MinusIcon, submitButtonClasses, -} from '@/components' +} from "@/components"; // import { CustomerCombobox } from "@/app/resources/customers"; -import { Customer } from '@/models/customerserver' -import { useQuery } from '@tanstack/react-query' +import { Customer } from "@/models/customerserver"; +import { useQuery } from "@tanstack/react-query"; import { useForm, FormProvider, useFormContext, useFieldArray, -} from 'react-hook-form' -import { zodResolver } from '@hookform/resolvers/zod' -import * as z from 'zod' -import { useRouter } from 'next/router' +} from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import * as z from "zod"; +import { useRouter } from "next/navigation"; -const generateRandomId = () => Math.random().toString(32).slice(2) +const generateRandomId = () => Math.random().toString(32).slice(2); export const createInvoiceSchema = z.object({ customerId: z.string(), @@ -33,57 +34,57 @@ export const createInvoiceSchema = z.object({ }) ), intent: z.string(), -}) +}); -export type CreateInvoiceFormData = z.infer +export type CreateInvoiceFormData = z.infer; -const fetcher = (url: string) => fetch(url).then((res) => res.json()) +const fetcher = (url: string) => fetch(url).then((res) => res.json()); export default function CreateInvoiceForm() { - const [loading, setLoading] = useState(false) - const [error, setError] = useState(null) + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); const methods = useForm({ resolver: zodResolver(createInvoiceSchema), defaultValues: { invoiceLineItems: [ { lineItemId: generateRandomId(), - description: '', + description: "", quantity: 1, unitPrice: 0, }, ], }, - }) + }); const { register, handleSubmit, formState: { errors }, - } = methods + } = methods; - const { data, isLoading, isError } = useQuery(['customers'], () => - fetcher('/api/get-customers-list') - ) + const { data, isLoading, isError } = useQuery(["customers"], () => + fetcher("/api/customers") + ); - const router = useRouter() + const router = useRouter(); async function _createInvoiceAction(data: CreateInvoiceFormData) { - await new Promise((resolve) => setTimeout(resolve, 200)) - setLoading(true) - const response = await fetch('/api/create-invoice', { - method: 'POST', + await new Promise((resolve) => setTimeout(resolve, 200)); + setLoading(true); + const response = await fetch("/api/invoices", { + method: "POST", headers: { - 'Content-Type': 'application/json', + "Content-Type": "application/json", }, body: JSON.stringify(data), - }) - const result = await response.json() - console.log('result', result) + }); + const result = await response.json(); + console.log("result", result); if (result.status !== 200) { - setError(null) + setError(null); } - router.push(`/sales/invoices/${result}`) + router.push(`/sales/invoices/${result}`); } return ( @@ -102,7 +103,7 @@ export default function CreateInvoiceForm() { {isLoading && Loading...} {data && ( - {data?.customers?.map((customer: Customer) => (
- ) + ); } function LineItems() { - const data = useFormContext() + const data = useFormContext(); const { fields, append, remove } = useFieldArray({ control: data.control, - name: 'invoiceLineItems', - }) + name: "invoiceLineItems", + }); return (
{fields.map((field, index) => ( @@ -174,7 +175,7 @@ function LineItems() { lineItemId: generateRandomId(), quantity: 1, unitPrice: 0, - description: 'your new item added', + description: "your new item added", }, ], }) @@ -184,7 +185,7 @@ function LineItems() {
- ) + ); } function LineItemFormFields({ @@ -192,11 +193,11 @@ function LineItemFormFields({ index, onRemoveClick, }: { - lineItemClientId: string - index: number - onRemoveClick: () => void + lineItemClientId: string; + index: number; + onRemoveClick: () => void; }) { - const data = useFormContext() + const data = useFormContext(); return (
@@ -264,7 +265,7 @@ function LineItemFormFields({ Boolean(data.formState.errors?.unitPrice) || undefined } aria-errormessage={ - data.formState.errors?.unitPrice ? 'name-error' : undefined + data.formState.errors?.unitPrice ? "name-error" : undefined } />
@@ -283,5 +284,5 @@ function LineItemFormFields({ - ) + ); } diff --git a/src/components/forms/delete-deposit-form.tsx b/src/components/forms/delete-deposit-form.tsx index 7d124af..2ecdb00 100644 --- a/src/components/forms/delete-deposit-form.tsx +++ b/src/components/forms/delete-deposit-form.tsx @@ -1,66 +1,44 @@ -import { zodResolver } from '@hookform/resolvers/zod' -import React from 'react' -import { useRouter } from 'next/router' -import { useForm } from 'react-hook-form' -import * as z from 'zod' -import { TrashIcon } from '..' +"use client"; +import React from "react"; +import * as z from "zod"; +import { SpinnerIcon, TrashIcon } from ".."; +import { removeDeposit } from "@/app/actions"; +import { experimental_useFormStatus as useFormStatus } from "react-dom"; const deleteDepositSchema = z.object({ - depositId: z.string(), intent: z.string(), -}) - -type DeleteDepositFormData = z.infer +}); export default function DeleteDepositForm({ depositId, }: { - depositId: string + depositId: string; }) { - const [loading, setLoading] = React.useState(false) - const [error, setError] = React.useState(null) - const { - register, - handleSubmit, - formState: { errors }, - } = useForm({ - resolver: zodResolver(deleteDepositSchema), - }) - const router = useRouter() - - async function _deleteDepositAction(data: DeleteDepositFormData) { - await new Promise((resolve) => setTimeout(resolve, 200)) - setLoading(true) - const response = await fetch('/api/delete-deposit', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(data), - }) - const result = await response.json() - if (result.status !== 200) { - setError(null) - } - router.push(result) - } - return ( -
+ { + const intentFromForm = Object.fromEntries(formData.entries()); + const { intent } = deleteDepositSchema.parse(intentFromForm); + await removeDeposit({ depositId, intent }); + }} + > - - + - ) + ); +} + +function DeleteDepositButton() { + const { pending } = useFormStatus(); + return ( + + ); } diff --git a/src/components/forms/image-upload.tsx b/src/components/forms/image-upload.tsx index 6763685..83dbb50 100644 --- a/src/components/forms/image-upload.tsx +++ b/src/components/forms/image-upload.tsx @@ -1,6 +1,7 @@ -import { UploadButton } from '@/utils/uploadthing' +"use client"; +import { UploadButton } from "@/utils/uploadthing"; // You need to import our styles for the button to look right. Best to import in the root /_app.tsx but this is fine -import '@uploadthing/react/styles.css' +import "@uploadthing/react/styles.css"; export function UploadImage() { return ( @@ -9,14 +10,14 @@ export function UploadImage() { endpoint="imageUploader" onClientUploadComplete={(res) => { // Do something with the response - console.log('Files: ', res) - alert('Upload Completed') + console.log("Files: ", res); + alert("Upload Completed"); }} onUploadError={(error: Error) => { // Do something with the error. - alert(`ERROR! ${error.message}`) + alert(`ERROR! ${error.message}`); }} /> - ) + ); } diff --git a/src/components/full-stack-forms/add-customer.tsx b/src/components/full-stack-forms/add-customer.tsx index b16ecfe..994a082 100644 --- a/src/components/full-stack-forms/add-customer.tsx +++ b/src/components/full-stack-forms/add-customer.tsx @@ -1,10 +1,10 @@ -import { FilePlusIcon, inputClasses, LabelText } from '..' -import { useRouter } from 'next/router' -import { useForm } from 'react-hook-form' -import { zodResolver } from '@hookform/resolvers/zod' -import * as z from 'zod' -import * as React from 'react' -import { useMutation } from '@tanstack/react-query' +"use client"; +import { FilePlusIcon, inputClasses, LabelText, SpinnerIcon } from ".."; +import { useRouter } from "next/navigation"; +import * as z from "zod"; +import * as React from "react"; +import { addCustomer } from "@/app/actions"; +import { experimental_useFormStatus as useFormStatus } from "react-dom"; import { Dialog, @@ -13,42 +13,17 @@ import { DialogHeader, DialogTitle, DialogTrigger, -} from '../../../shadui/ui/dialog' +} from "../../../shadui/ui/dialog"; +import { ErrorBoundaryComponent } from "../error-boundary"; const newCustomerSchema = z.object({ - name: z.string().nonempty({ message: 'Name is required' }), - email: z.string().email({ message: 'Invalid email' }), -}) - -type NewCustomerFormData = z.infer + name: z.string().nonempty({ message: "Name is required" }), + email: z.string().email().nonempty({ message: "Invalid email" }), +}); export default function BetterAddCustomerForm() { - const [open, setOpen] = React.useState(false) - const { - register, - handleSubmit, - formState: { errors }, - } = useForm({ - resolver: zodResolver(newCustomerSchema), - }) - - const mutation = useMutation({ - mutationFn: (formData: any) => { - return fetch('/api/add-customer', { - method: 'POST', - body: JSON.stringify(formData), - }) - }, - }) - - const router = useRouter() - - async function _addCustomerAction(data: NewCustomerFormData) { - const result = await mutation.mutateAsync(data) - const newCustomer = await result.json() - setOpen(false) - router.push(`/sales/customers/${newCustomer.id}`) - } + const [open, setOpen] = React.useState(false); + const customerFormRef = React.useRef(); return ( @@ -62,44 +37,60 @@ export default function BetterAddCustomerForm() { Add a Customer -
-

New Customer

-
-
- - - {errors.name && ( -

{errors.name.message}

- )} -
-
- - -
- - {errors.email && ( -

{errors.email.message}

- )} -
-
+ +
+

New Customer

+
{ + const customerForm = Object.fromEntries(formData.entries()); + // const name = formData.get("name") as string; + // const email = formData.get("email") as string; + const { name, email } = newCustomerSchema.parse(customerForm); + customerFormRef.current.reset; + + await addCustomer(name, email); + + setOpen(false); + }} + ref={customerFormRef} + className="flex flex-col gap-4" + > +
+ + +
+
+ + +
+ + +
+
- ) + ); +} + +function SubmitButton() { + const { pending } = useFormStatus(); + return ( + + ); } diff --git a/src/components/full-stack-forms/create-invoice.tsx b/src/components/full-stack-forms/create-invoice.tsx index 18041a2..478d61d 100644 --- a/src/components/full-stack-forms/create-invoice.tsx +++ b/src/components/full-stack-forms/create-invoice.tsx @@ -1,4 +1,5 @@ -import { useState, useId } from 'react' +"use client"; +import { useId, useRef, useState } from "react"; import { inputClasses, LabelText, @@ -6,19 +7,9 @@ import { MinusIcon, submitButtonClasses, FilePlusIcon, -} from '@/components' -// import { CustomerCombobox } from "@/app/resources/customers"; -import { Customer } from '@/models/customerserver' -import { useMutation, useQuery } from '@tanstack/react-query' -import { - useForm, - FormProvider, - useFormContext, - useFieldArray, -} from 'react-hook-form' -import { zodResolver } from '@hookform/resolvers/zod' -import * as z from 'zod' -import { useRouter } from 'next/router' +} from "@/components"; +import { Customer } from "@/models/customerserver"; +import * as z from "zod"; import { Dialog, @@ -27,9 +18,11 @@ import { DialogHeader, DialogTitle, DialogTrigger, -} from '../../../shadui/ui/dialog' +} from "../../../shadui/ui/dialog"; +import { addInvoice } from "@/app/actions"; +import { ErrorBoundaryComponent } from "../error-boundary"; -const generateRandomId = () => Math.random().toString(32).slice(2) +const generateRandomId = () => Math.random().toString(32).slice(2); export const createInvoiceSchema = z.object({ customerId: z.string(), @@ -43,58 +36,15 @@ export const createInvoiceSchema = z.object({ }) ), intent: z.string(), -}) - -export type CreateInvoiceFormData = z.infer - -const fetcher = (url: string) => fetch(url).then((res) => res.json()) - -export default function CreateInvoiceForm() { - const [open, setOpen] = useState(false) - const router = useRouter() - - const methods = useForm({ - resolver: zodResolver(createInvoiceSchema), - defaultValues: { - invoiceLineItems: [ - { - lineItemId: generateRandomId(), - description: '', - quantity: 1, - unitPrice: 0, - }, - ], - }, - }) - - const { - register, - handleSubmit, - formState: { errors }, - } = methods - - const { data, isLoading, isError } = useQuery(['customers'], () => - fetcher('/api/get-customers-list') - ) +}); - const mutation = useMutation({ - mutationFn: (formData: any) => { - return fetch('/api/create-invoice', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(formData), - }) - }, - }) - - async function _createInvoiceAction(data: CreateInvoiceFormData) { - const sendInvoice = await mutation.mutateAsync(data) - const result = await sendInvoice.json() - setOpen(false) - router.push(`/sales/invoices/${result}`) - } +export default function CreateInvoiceForm({ + customers, +}: { + customers: Pick[]; +}) { + const [open, setOpen] = useState(false); + const invoiceFormRef = useRef(); return ( @@ -108,11 +58,17 @@ export default function CreateInvoiceForm() { Create New Invoice - +

New Invoice

{ + invoiceFormRef.current.reset; + await addInvoice(formData); + + setOpen(false); + }} + ref={invoiceFormRef} className="flex flex-col gap-4" > {/* */} @@ -121,16 +77,13 @@ export default function CreateInvoiceForm() { - {isLoading && Loading...} - {data && ( - - )} +
{/* Replace all bracketed content with the combobox once that's figured out */} @@ -142,7 +95,7 @@ export default function CreateInvoiceForm() { - {errors.intent && ( - {errors.intent.message} - )} -
+
- ) + ); } function LineItems() { - const data = useFormContext() - const { fields, append, remove } = useFieldArray({ - control: data.control, - name: 'invoiceLineItems', - }) + const firstId = useId(); + const [lineItems, setLineItems] = useState(() => [firstId]); return (
- {fields.map((field, index) => ( + {lineItems.map((lineItemClientId, index) => ( remove(index)} + onRemoveClick={() => { + setLineItems((lis) => + lis.filter((id, i) => id !== lineItemClientId) + ); + }} /> ))}
- ) + ); } function LineItemFormFields({ @@ -215,12 +155,10 @@ function LineItemFormFields({ index, onRemoveClick, }: { - lineItemClientId: string - index: number - onRemoveClick: () => void + lineItemClientId: string; + index: number; + onRemoveClick: () => void; }) { - const data = useFormContext() - return (
@@ -229,12 +167,7 @@ function LineItemFormFields({ Line Item {index + 1}
- - +
@@ -244,21 +177,12 @@ function LineItemFormFields({ Quantity: - {/* {errors?.quantity ? ( - - {errors.quantity} - - ) : null} */}
@@ -268,27 +192,14 @@ function LineItemFormFields({ Unit Price: - {/* {data.formState.errors?.unitPrice ? ( - - {data.formState.errors.unitPrice} - - ) : null} */}
@@ -300,11 +211,11 @@ function LineItemFormFields({
- ) + ); } diff --git a/src/components/invoice-page/index.tsx b/src/components/invoice-page/index.tsx index 353bcc7..af7fdae 100644 --- a/src/components/invoice-page/index.tsx +++ b/src/components/invoice-page/index.tsx @@ -1,54 +1,78 @@ -import Link from 'next/link' -import { FilePlusIcon, LabelText } from '../index' -import { currencyFormatter, fetcher } from '../../utils' -import { usePathname } from 'next/navigation' -import { AddInvoiceDialog } from '../modal/add-invoice-dialog' -import { useQuery } from '@tanstack/react-query' +import Link from "next/link"; +import { FilePlusIcon, LabelText } from "../index"; +import { currencyFormatter } from "../../utils"; +import CreateInvoiceForm from "../full-stack-forms/create-invoice"; +import { getInvoiceListItems } from "@/models/invoiceserver"; +import { Suspense } from "react"; +import { CustomerSkeleton } from "../customer-layout"; +import { getCustomerListItems } from "@/models/customerserver"; export default function InvoicesPage({ children, }: { - children?: React.ReactNode + children?: React.ReactNode; }) { - const { data, isLoading, isError } = useQuery(['invoices'], () => - fetcher('/api/get-invoice-list-items') - ) + return ( +
+ }> + {/* @ts-expect-error */} + + +
+ Invoice List +
- if (isLoading) { - return
Loading...
- } + }> + {/* @ts-expect-error */} + {children} + +
+ ); +} - if (isError) { - return
Error
- } +async function OurInvoices() { + const invoiceListItems = await getInvoiceListItems(); + const dueSoonAmount = invoiceListItems.reduce((sum, li) => { + if (li.dueStatus !== "due") { + return sum; + } + const remainingBalance = li.totalAmount - li.totalDeposits; + return sum + remainingBalance; + }, 0); + const overdueAmount = invoiceListItems.reduce((sum, li) => { + if (li.dueStatus !== "overdue") { + return sum; + } + const remainingBalance = li.totalAmount - li.totalDeposits; + return sum + remainingBalance; + }, 0); + const allInvoicesData = { + overdueAmount, + dueSoonAmount, + }; - const { allInvoicesData, dueSoonPercent } = data + const hundo = allInvoicesData.dueSoonAmount + allInvoicesData.overdueAmount; + const dueSoonPercent = Math.floor( + (allInvoicesData.dueSoonAmount / hundo) * 100 + ); return ( -
-
- -
-
-
-
- + +
+
+
-
- Invoice List -
- - {children} - +
- ) + ); } function InvoicesInfo({ @@ -56,51 +80,33 @@ function InvoicesInfo({ amount, right, }: { - label: string - amount: number - right?: boolean + label: string; + amount: number; + right?: boolean; }) { return ( -
+
{label}
{currencyFormatter.format(amount)}
- ) + ); } -function InvoiceList({ - children, - invoiceListItems, -}: { - children: React.ReactNode - invoiceListItems: InvoiceData['invoiceListItems'] -}) { - const pathname = usePathname() - - const newInvoiceRouteIsActive = pathname === '/sales/invoices/newInvoice' - const singleInvoiceRouteIsActive = pathname === '/sales/invoices/[invoiceId]' +async function InvoiceList({ children }: { children: React.ReactNode }) { + const invoiceListItems = await getInvoiceListItems(); + const customers = await getCustomerListItems(); return (
- { - - Create new invoice - - } - /> - } +
@@ -109,9 +115,7 @@ function InvoiceList({ key={invoice.id} href={`/sales/invoices/${invoice.id}`} className={ - 'block border-b border-gray-50 py-3 px-4 hover:bg-gray-50' + - ' ' + - (singleInvoiceRouteIsActive ? 'bg-gray-50' : '') + "block border-b border-gray-50 py-3 px-4 hover:bg-gray-50" } >
@@ -122,13 +126,13 @@ function InvoiceList({
{invoice.number}
{invoice.dueStatusDisplay} @@ -140,20 +144,20 @@ function InvoiceList({
{children}
- ) + ); } interface InvoiceData { invoiceListItems: { - totalAmount: number - totalDeposits: number - daysToDueDate: number - dueStatus: 'paid' | 'overpaid' | 'overdue' | 'due' - dueStatusDisplay: string - id: string - name: string - number: number - }[] - overdueAmount: number - dueSoonAmount: number + totalAmount: number; + totalDeposits: number; + daysToDueDate: number; + dueStatus: "paid" | "overpaid" | "overdue" | "due"; + dueStatusDisplay: string; + id: string; + name: string; + number: number; + }[]; + overdueAmount: number; + dueSoonAmount: number; } diff --git a/src/components/login/index.tsx b/src/components/login/index.tsx index d8be023..9156956 100644 --- a/src/components/login/index.tsx +++ b/src/components/login/index.tsx @@ -1,25 +1,26 @@ -import { GitHubIcon } from '../Icon' -import { signIn, signOut } from 'next-auth/react' +"use client"; +import { GitHubIcon } from "../Icon"; +import { signIn, signOut } from "next-auth/react"; export function SignOut() { return ( - ) + ); } export function SignIn() { return ( - ) + ); } diff --git a/src/components/logo-renderer/index.tsx b/src/components/logo-renderer/index.tsx deleted file mode 100644 index 1a1c0bd..0000000 --- a/src/components/logo-renderer/index.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import { fetcher } from '@/utils' -import { useQuery } from '@tanstack/react-query' -import Link from 'next/link' -import { FullFakebooksLogo, FullUserLogo, SpinnerIcon } from '..' - -export function LogoRenderer({}) { - const { data, isLoading, isError } = useQuery(['user'], () => - fetcher('/api/get-user-info/') - ) - - console.log('data', data) - - console.log('fetching logo info') - - return ( -
- {isLoading ? ( - - ) : data && data.user?.logoUrl ? ( - - - - ) : ( - - - - )} - {/* Fallback if there is an error user info in the db */} - {/* {isError && ( - - - - )} */} -
- ) -} diff --git a/src/components/modal/add-invoice-dialog.tsx b/src/components/modal/add-invoice-dialog.tsx index 888ded9..bf796bb 100644 --- a/src/components/modal/add-invoice-dialog.tsx +++ b/src/components/modal/add-invoice-dialog.tsx @@ -1,16 +1,17 @@ -import * as React from 'react' -import { DialogComponent } from '.' -import CreateInvoiceForm from '../forms/create-invoice-form' +"use client"; +import * as React from "react"; +import { DialogComponent } from "."; +import CreateInvoiceForm from "../forms/create-invoice-form"; export function AddInvoiceDialog({ children, trigger }: any) { return ( } > {/* @ts-ignore */} {children ? ({ openModal }) => children({ openModal }) : null} - ) + ); } diff --git a/src/components/modal/upload-logo-dialog.tsx b/src/components/modal/upload-logo-dialog.tsx index 1bce97e..06e3571 100644 --- a/src/components/modal/upload-logo-dialog.tsx +++ b/src/components/modal/upload-logo-dialog.tsx @@ -1,18 +1,19 @@ -import * as React from 'react' +"use client"; +import * as React from "react"; -import { DialogComponent } from '.' +import { DialogComponent } from "."; -import { UploadImage } from '../forms/image-upload' +import { UploadImage } from "../forms/image-upload"; export function UploadCompanyLogo({ children, trigger }: any) { return ( } > {/* @ts-ignore */} {children ? ({ openModal }) => children({ openModal }) : null} - ) + ); } diff --git a/src/components/navbar/index.tsx b/src/components/navbar/index.tsx index 4d45487..4a7ec7f 100644 --- a/src/components/navbar/index.tsx +++ b/src/components/navbar/index.tsx @@ -1,90 +1,55 @@ -import Link from 'next/link' -import { usePathname } from 'next/navigation' -import clsx from 'clsx' -import { UpRightArrowIcon, SpinnerIcon, FilePlusIcon } from '..' -import { SignOut } from '../login' -import { UploadCompanyLogo } from '../modal/upload-logo-dialog' -import { LogoRenderer } from '../logo-renderer' +import { authOptions } from "@/app/api/auth/[...nextauth]/route"; +import { getUserByEmail } from "@/models/userserver"; +import { getServerSession } from "next-auth"; +import Link from "next/link"; +import { Suspense } from "react"; +import { FullFakebooksLogo, FullUserLogo, SpinnerIcon } from ".."; +import Navigation from "./navigation"; export default function Navbar({ children }: { children: React.ReactNode }) { - const pathname = usePathname() - return (
- + }> + {/* @ts-expect-error */} + +
-
- - Dashboard - - - Accounts - - - Sales - - - Expenses - - - Reports - -
- - Upload Logo - - } - /> -
- - Github Repo - -
- -
-
+
{children}
- ) + ); } -function NavItem({ - to, - children, - isActive, -}: { - to: string - children: React.ReactNode - isActive: boolean -}) { - return ( - - {children} - - ) -} +async function LogoRenderer() { + const session = await getServerSession(authOptions); + console.log(session); + if (!session) { + return ( +
+ + + +
+ ); + } + + const user = await getUserByEmail(session?.user?.email as string); -function Spinner({ visible }: { visible: boolean }) { return ( - - ) +
+ {user?.logoUrl ? ( + + + + ) : ( + + + + )} +
+ ); } diff --git a/src/components/navbar/navigation.tsx b/src/components/navbar/navigation.tsx new file mode 100644 index 0000000..1e4b63c --- /dev/null +++ b/src/components/navbar/navigation.tsx @@ -0,0 +1,68 @@ +"use client"; +import Link from "next/link"; +import { usePathname } from "next/navigation"; +import { SignOut } from "../login"; +import { UploadCompanyLogo } from "../modal/upload-logo-dialog"; + +export default function Navigation() { + const pathname = usePathname(); + + return ( +
+ + Dashboard + + + Accounts + + + Sales + + + Expenses + + + Reports + +
+ + Upload Logo + + } + /> +
+ + Github Repo + +
+ +
+
+ ); +} + +function NavItem({ + to, + children, + isActive, +}: { + to: string; + children: React.ReactNode; + isActive: boolean; +}) { + return ( + + {children} + + ); +} diff --git a/src/components/rq-providers.tsx b/src/components/rq-providers.tsx new file mode 100644 index 0000000..898cba0 --- /dev/null +++ b/src/components/rq-providers.tsx @@ -0,0 +1,14 @@ +"use client"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; +import * as React from "react"; + +export function RQProviders({ children }: { children: React.ReactNode }) { + const [queryClient] = React.useState(() => new QueryClient()); + return ( + + {children} + + + ); +} diff --git a/src/components/sales-nav/index.tsx b/src/components/sales-nav/index.tsx index c500f76..0f00ce9 100644 --- a/src/components/sales-nav/index.tsx +++ b/src/components/sales-nav/index.tsx @@ -1,19 +1,20 @@ -import Link from 'next/link' -import { useRouter } from 'next/router' +"use client"; +import Link from "next/link"; +import { usePathname } from "next/navigation"; const linkClassName = ({ isActive }: { isActive: boolean }) => - isActive ? 'font-bold text-black' : '' + isActive ? "font-bold text-black" : ""; export default function SalesNav({ children }: { children: React.ReactNode }) { - const router = useRouter() - if (!router.pathname) { + const pathname = usePathname(); + if (!pathname) { throw new Error( - 'You should never reach this error but if you do, pathname is undefined. Contact site owner' - ) + "You should never reach this error but if you do, pathname is undefined. Contact site owner" + ); } - const invoiceMatches = router.pathname.includes('/sales/invoices') + const invoiceMatches = pathname.includes("/sales/invoices"); - const customerMatches = router.pathname.includes('/sales/customers') + const customerMatches = pathname.includes("/sales/customers"); return (
@@ -22,14 +23,14 @@ export default function SalesNav({ children }: { children: React.ReactNode }) {
Overview Subscriptions @@ -41,7 +42,7 @@ export default function SalesNav({ children }: { children: React.ReactNode }) { Invoices @@ -50,7 +51,7 @@ export default function SalesNav({ children }: { children: React.ReactNode }) { Deposits @@ -59,5 +60,5 @@ export default function SalesNav({ children }: { children: React.ReactNode }) {
{children}
- ) + ); } diff --git a/src/models/customerserver.ts b/src/models/customerserver.ts index 26fc54a..5b027ed 100644 --- a/src/models/customerserver.ts +++ b/src/models/customerserver.ts @@ -1,8 +1,9 @@ -import prisma from '@/utils/dbserver' -import type { Customer, User } from '@prisma/client' -import { getInvoiceDerivedData } from './invoiceserver' +"server only"; +import prisma from "@/utils/dbserver"; +import type { Customer, User } from "@prisma/client"; +import { getInvoiceDerivedData } from "./invoiceserver"; -export type { Customer } +export type { Customer }; export async function searchCustomers(query: string) { const customers = await prisma.customer.findMany({ @@ -11,18 +12,18 @@ export async function searchCustomers(query: string) { name: true, email: true, }, - }) - const lowerQuery = query.toLowerCase() + }); + const lowerQuery = query.toLowerCase(); return customers.filter((c) => { return ( c.name.toLowerCase().includes(lowerQuery) || c.email.toLowerCase().includes(lowerQuery) - ) - }) + ); + }); } export async function getFirstCustomer() { - return prisma.customer.findFirst() + return prisma.customer.findFirst(); } export async function getCustomerListItems() { @@ -32,20 +33,20 @@ export async function getCustomerListItems() { name: true, email: true, }, - }) + }); } export async function getCustomerInfo(customerId: string) { return prisma.customer.findUnique({ where: { id: customerId }, select: { name: true, email: true }, - }) + }); } export async function getCustomerDetails(customerId: string) { await new Promise((resolve) => setTimeout(resolve, Math.random() * 3000 + 1500) - ) + ); const customer = await prisma.customer.findUnique({ where: { id: customerId }, select: { @@ -69,24 +70,24 @@ export async function getCustomerDetails(customerId: string) { }, }, }, - }) - if (!customer) return null + }); + if (!customer) return null; const invoiceDetails = customer.invoices.map((invoice) => ({ id: invoice.id, number: invoice.number, ...getInvoiceDerivedData(invoice), - })) + })); - return { name: customer.name, email: customer.email, invoiceDetails } + return { name: customer.name, email: customer.email, invoiceDetails }; } export async function createCustomer({ name, email, user_email, -}: Pick & { user_email: User['email'] }) { +}: Pick & { user_email: User["email"] }) { return prisma.customer.create({ data: { email, name, user: { connect: { email: user_email as string } } }, - }) + }); } diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx deleted file mode 100644 index 71e894a..0000000 --- a/src/pages/_app.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import '@/styles/globals.css' -import * as React from 'react' -import type { AppProps } from 'next/app' -import type { ReactElement, ReactNode } from 'react' -import type { NextPage } from 'next' -import { SessionProvider } from 'next-auth/react' -import { - Hydrate, - QueryClient, - QueryClientProvider, -} from '@tanstack/react-query' -import { ReactQueryDevtools } from '@tanstack/react-query-devtools' - -export type NextPageWithLayout

= NextPage & { - getLayout?: (page: ReactElement) => ReactNode -} - -type AppPropsWithLayout = AppProps & { - Component: NextPageWithLayout -} - -export default function App({ - Component, - pageProps: { session, ...pageProps }, -}: AppPropsWithLayout) { - const [queryClient] = React.useState(() => new QueryClient()) - - const getLayout = Component.getLayout ?? ((page) => page) - - return ( - - - - {getLayout()} - - - - - ) -} diff --git a/src/pages/_document.tsx b/src/pages/_document.tsx deleted file mode 100644 index 54e8bf3..0000000 --- a/src/pages/_document.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import { Html, Head, Main, NextScript } from 'next/document' - -export default function Document() { - return ( - - - -

- - - - ) -} diff --git a/src/pages/api/add-customer.ts b/src/pages/api/add-customer.ts deleted file mode 100644 index 1f90baf..0000000 --- a/src/pages/api/add-customer.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { NextApiRequest, NextApiResponse } from 'next' -import { getServerSession } from 'next-auth' -import { authOptions } from './auth/[...nextauth]' -import { createCustomer } from '@/models/customerserver' -import { getUserByEmail } from '@/models/userserver' - -export default async (req: NextApiRequest, res: NextApiResponse) => { - const session = await getServerSession(req, res, authOptions) - if (!session) { - res.status(401).send('Unauthorized') - } - - if (!session?.user?.email) { - res.status(401).send({ - message: - 'please add an email to your github profile & logout out and signin again', - }) - } - - const user = await getUserByEmail(session?.user?.email as string) - - if (!user) { - res.status(401).send({ - message: 'There is a server error and you shouldnt be getting this error', - }) - } - - const { name, email } = JSON.parse(req.body) - const customer = await createCustomer({ - name, - email, - user_email: user?.email as string, - }) - console.log('customer', customer) - res.status(200).json(customer) -} diff --git a/src/pages/api/auth/[...nextauth].ts b/src/pages/api/auth/[...nextauth].ts deleted file mode 100644 index af7e152..0000000 --- a/src/pages/api/auth/[...nextauth].ts +++ /dev/null @@ -1,16 +0,0 @@ -import NextAuth, { NextAuthOptions } from 'next-auth' -import GithubProvider from 'next-auth/providers/github' -import { PrismaAdapter } from '@next-auth/prisma-adapter' -import prisma from '@/utils/dbserver' - -export const authOptions: NextAuthOptions = { - adapter: PrismaAdapter(prisma), - providers: [ - GithubProvider({ - clientId: process.env.OAUTH_CLIENT_ID as string, - clientSecret: process.env.OAUTH_CLIENT_SECRET as string, - }), - ], -} - -export default NextAuth(authOptions) diff --git a/src/pages/api/create-deposit.ts b/src/pages/api/create-deposit.ts deleted file mode 100644 index 4b8b593..0000000 --- a/src/pages/api/create-deposit.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { createDeposit } from '@/models/depositserver' -import { parseDate } from '@/utils' -import { NextApiRequest, NextApiResponse } from 'next' -import { getServerSession } from 'next-auth/next' -import invariant from 'tiny-invariant' -import { - validateAmount, - validateDepositDate, -} from '../sales/invoices/[invoiceId]' -import { authOptions } from './auth/[...nextauth]' - -export default async (req: NextApiRequest, res: NextApiResponse) => { - const session = await getServerSession(req, res, authOptions) - if (!session) { - return new Response('Unauthorized', { status: 401 }) - } - - const { invoiceId, intent, formAmount, formDepositDate, formNote } = - JSON.parse(req.body) - - if (typeof invoiceId !== 'string') { - throw new Error('This should be impossible.') - } - - invariant(typeof intent === 'string', 'intent required') - switch (intent) { - case 'create-deposit': { - const amount = Number(formAmount) - const note = formNote - invariant(!Number.isNaN(amount), 'amount must be a number') - invariant(typeof formDepositDate === 'string', 'dueDate is required') - invariant(typeof note === 'string', 'dueDate is required') - const depositDate = parseDate(formDepositDate) - - const errors = { - amount: validateAmount(amount), - depositDate: validateDepositDate(depositDate), - } - const hasErrors = Object.values(errors).some( - (errorMessage) => errorMessage - ) - if (hasErrors) { - return errors - } - - const newDeposit = await createDeposit({ - invoiceId, - amount, - note, - depositDate, - }) - console.log('new deposit data', newDeposit) - return res.status(200).json(newDeposit) - } - default: { - throw new Error(`Unsupported intent: ${intent}`) - } - } -} diff --git a/src/pages/api/create-invoice.ts b/src/pages/api/create-invoice.ts deleted file mode 100644 index 1e3a3b2..0000000 --- a/src/pages/api/create-invoice.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { getServerSession } from 'next-auth/next' -import { NextApiRequest, NextApiResponse } from 'next/types' -import { authOptions } from './auth/[...nextauth]' -import invariant from 'tiny-invariant' -import { - parseDate, - validateCustomerId, - validateDueDate, - validateLineItemQuantity, - validateLineItemUnitPrice, -} from '@/utils' -import { LineItemFields, createInvoice } from '@/models/invoiceserver' -import { NextResponse } from 'next/server' - -export default async (req: NextApiRequest, res: NextApiResponse) => { - const session = await getServerSession(req, res, authOptions) - if (!session) { - return new Response('Unauthorized', { status: 401 }) - } - const { intent, customerId, invoiceDueDate, invoiceLineItems } = - await req.body - - switch (intent) { - case 'create': { - const dueDateString = invoiceDueDate - invariant(typeof customerId === 'string', 'customerId is required') - invariant(typeof dueDateString === 'string', 'dueDate is required') - const dueDate = parseDate(dueDateString) - - const lineItemIds = invoiceLineItems.map( - (lineItem: any) => lineItem.lineItemId - ) - const lineItemQuantities = invoiceLineItems.map( - (lineItem: any) => lineItem.quantity - ) - const lineItemUnitPrices = invoiceLineItems.map( - (lineItem: any) => lineItem.unitPrice - ) - const lineItemDescriptions = invoiceLineItems.map( - (lineItem: any) => lineItem.description - ) - const lineItems: Array = [] - for (let i = 0; i < lineItemQuantities.length; i++) { - const quantity = +lineItemQuantities[i] - const unitPrice = +lineItemUnitPrices[i] - const description = lineItemDescriptions[i] - invariant(typeof quantity === 'number', 'quantity is required') - invariant(typeof unitPrice === 'number', 'unitPrice is required') - invariant(typeof description === 'string', 'description is required') - - lineItems.push({ quantity, unitPrice, description }) - } - - const errors = { - customerId: validateCustomerId(customerId), - dueDate: validateDueDate(dueDate), - lineItems: lineItems.reduce((acc, lineItem, index) => { - const id = lineItemIds[index] - invariant(typeof id === 'string', 'lineItem ids are required') - acc[id] = { - quantity: validateLineItemQuantity(lineItem.quantity), - unitPrice: validateLineItemUnitPrice(lineItem.unitPrice), - } - return acc - }, {} as Record), - } - - const customerIdHasError = errors.customerId !== null - const dueDateHasError = errors.dueDate !== null - const lineItemsHaveErrors = Object.values(errors.lineItems).some( - (lineItem) => Object.values(lineItem).some(Boolean) - ) - const hasErrors = - dueDateHasError || customerIdHasError || lineItemsHaveErrors - - if (hasErrors) { - res.status(400).json(errors) - } - - const invoice = await createInvoice({ dueDate, customerId, lineItems }) - - res.status(200).json(invoice.id) - } - } -} diff --git a/src/pages/api/delete-deposit.ts b/src/pages/api/delete-deposit.ts deleted file mode 100644 index 9e8a4d0..0000000 --- a/src/pages/api/delete-deposit.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { NextApiRequest, NextApiResponse } from 'next' -import { getServerSession } from 'next-auth' -import { authOptions } from './auth/[...nextauth]' -import { deleteDeposit } from '@/models/depositserver' - -import invariant from 'tiny-invariant' -import { findInvoiceByDepositId } from '@/models/invoiceserver' - -export default async (req: NextApiRequest, res: NextApiResponse) => { - const session = await getServerSession(req, res, authOptions) - if (!session) { - return res.status(401).send('Unauthorized') - } - let { - query: { params }, - } = req - let { intent, depositId } = req.body - - invariant(typeof depositId === 'string', 'params.depositId is not available') - invariant(typeof intent === 'string', 'intent must be a string') - - let redirect - const associatedInvoiceId = await findInvoiceByDepositId(depositId) - - if (!associatedInvoiceId) { - redirect = '/sales/invoices' - } else { - redirect = `/sales/invoices/${associatedInvoiceId.id}` - } - - switch (intent) { - case 'delete': { - await deleteDeposit(depositId) - res.status(200).json(redirect) - } - default: { - throw new Error(`Unsupported intent: ${intent}`) - } - } -} diff --git a/src/pages/api/get-customer/[params].ts b/src/pages/api/get-customer/[params].ts deleted file mode 100644 index b0f8fa3..0000000 --- a/src/pages/api/get-customer/[params].ts +++ /dev/null @@ -1,32 +0,0 @@ -import { NextApiRequest, NextApiResponse } from 'next' -import { getServerSession } from 'next-auth' -import { authOptions } from '../auth/[...nextauth]' -import { - getCustomerDetails, - getCustomerInfo, - getCustomerListItems, -} from '@/models/customerserver' -import invariant from 'tiny-invariant' - -export default async (req: NextApiRequest, res: NextApiResponse) => { - const session = await getServerSession(req, res, authOptions) - if (!session) { - return res.status(401).send('Unauthorized') - } - const customerId = req.query.params as string - - invariant( - typeof customerId === 'string', - 'params.customerId is not available' - ) - const customerInfo = await getCustomerInfo(customerId) - if (!customerInfo) { - throw new Response('not found', { status: 404 }) - } - const customerDetails = await getCustomerDetails(customerId) - const customers = await getCustomerListItems() - - const data = { customers, customerInfo, customerDetails } - - res.status(200).json(data) -} diff --git a/src/pages/api/get-customers-list.ts b/src/pages/api/get-customers-list.ts deleted file mode 100644 index cb1e04f..0000000 --- a/src/pages/api/get-customers-list.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { getCustomerListItems } from '@/models/customerserver' -import { NextApiRequest, NextApiResponse } from 'next' -import { getServerSession } from 'next-auth' -import { authOptions } from './auth/[...nextauth]' - -export default async (req: NextApiRequest, res: NextApiResponse) => { - const session = await getServerSession(req, res, authOptions) - if (!session) { - return res.status(401).send('Unauthorized') - } - const customers = await getCustomerListItems() - res.status(200).json({ customers }) -} diff --git a/src/pages/api/get-invoice-list-items.ts b/src/pages/api/get-invoice-list-items.ts deleted file mode 100644 index 9c8547b..0000000 --- a/src/pages/api/get-invoice-list-items.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { getInvoiceListItems } from '@/models/invoiceserver' -import { NextApiRequest, NextApiResponse } from 'next' -import { getServerSession } from 'next-auth' -import { authOptions } from './auth/[...nextauth]' - -export default async (req: NextApiRequest, res: NextApiResponse) => { - const session = await getServerSession(req, res, authOptions) - if (!session) { - return res.status(401).send('Unauthorized') - } - const invoiceListItems = await getInvoiceListItems() - const dueSoonAmount = invoiceListItems.reduce((sum, li) => { - if (li.dueStatus !== 'due') { - return sum - } - const remainingBalance = li.totalAmount - li.totalDeposits - return sum + remainingBalance - }, 0) - const overdueAmount = invoiceListItems.reduce((sum, li) => { - if (li.dueStatus !== 'overdue') { - return sum - } - const remainingBalance = li.totalAmount - li.totalDeposits - return sum + remainingBalance - }, 0) - const allInvoicesData = { - invoiceListItems, - overdueAmount, - dueSoonAmount, - } - - const hundo = allInvoicesData.dueSoonAmount + allInvoicesData.overdueAmount - const dueSoonPercent = Math.floor( - (allInvoicesData.dueSoonAmount / hundo) * 100 - ) - res.status(200).json({ allInvoicesData, dueSoonPercent }) -} diff --git a/src/pages/api/get-invoice/[params].ts b/src/pages/api/get-invoice/[params].ts deleted file mode 100644 index 36cd1f0..0000000 --- a/src/pages/api/get-invoice/[params].ts +++ /dev/null @@ -1,42 +0,0 @@ -import { NextApiRequest, NextApiResponse } from 'next' -import { getServerSession } from 'next-auth' -import { authOptions } from '../auth/[...nextauth]' -import { getInvoiceDetails, getInvoiceListItems } from '@/models/invoiceserver' - -export default async (req: NextApiRequest, res: NextApiResponse) => { - const session = await getServerSession(req, res, authOptions) - if (!session) { - return res.status(401).send('Unauthorized') - } - const invoiceId = req.query.params as string - - if (typeof invoiceId !== 'string') { - throw new Error('This should be impossible.') - } - const invoiceDetails = await getInvoiceDetails(invoiceId) - if (!invoiceDetails) { - throw new Response('not found', { status: 404 }) - } - const invoiceData = { - customerName: invoiceDetails.invoice.customer.name, - customerId: invoiceDetails.invoice.customer.id, - totalAmount: invoiceDetails.totalAmount, - dueStatus: invoiceDetails.dueStatus, - dueDisplay: invoiceDetails.dueStatusDisplay, - invoiceDateDisplay: invoiceDetails.invoice.invoiceDate.toLocaleDateString(), - lineItems: invoiceDetails.invoice.lineItems.map((li) => ({ - id: li.id, - description: li.description, - quantity: li.quantity, - unitPrice: li.unitPrice, - })), - deposits: invoiceDetails.invoice.deposits.map((deposit) => ({ - id: deposit.id, - amount: deposit.amount, - depositDateFormatted: deposit.depositDate.toLocaleDateString(), - })), - invoiceId: invoiceId, - } - - res.status(200).json({ invoiceData }) -} diff --git a/src/pages/api/get-user-info.ts b/src/pages/api/get-user-info.ts deleted file mode 100644 index 7b228f7..0000000 --- a/src/pages/api/get-user-info.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { getUserByEmail } from '@/models/userserver' -import { getServerSession } from 'next-auth/next' -import { authOptions } from './auth/[...nextauth]' -import { NextApiRequest, NextApiResponse } from 'next/types' - -export default async (req: NextApiRequest, res: NextApiResponse) => { - const session = await getServerSession(req, res, authOptions) - console.log(session) - if (!session) { - return res.status(401).send('Unauthorized') - } - const user = await getUserByEmail(session?.user?.email as string) - console.log(user) - const data = { user } - - res.status(200).json(data) -} diff --git a/src/pages/api/uploadthing.ts b/src/pages/api/uploadthing.ts deleted file mode 100644 index b3acd72..0000000 --- a/src/pages/api/uploadthing.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { createNextPageApiHandler } from 'uploadthing/next-legacy' - -import { ourFileRouter } from '@/server/uploadthing' - -const handler = createNextPageApiHandler({ - router: ourFileRouter, -}) - -export default handler diff --git a/src/pages/index.tsx b/src/pages/index.tsx deleted file mode 100644 index b9ea400..0000000 --- a/src/pages/index.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import Layout from '@/components/layouts' - -function Home() { - return

Home Page

-} - -Home.getLayout = function getLayout(page: React.ReactNode) { - return {page} -} - -export default Home diff --git a/src/pages/login.tsx b/src/pages/login.tsx deleted file mode 100644 index af8b933..0000000 --- a/src/pages/login.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { FullFakebooksLogo } from '@/components' -import Layout from '@/components/layouts' -import { SignIn } from '@/components/login' -import { useSession } from 'next-auth/react' - -function LoginPage() { - const session = useSession() - console.log('session', session) - return ( -
-

- -

-
- -
-
- ) -} - -LoginPage.getLayout = function getLayout(page: React.ReactNode) { - return {page} -} - -export default LoginPage diff --git a/src/pages/sales/customers/[customerId]/index.tsx b/src/pages/sales/customers/[customerId]/index.tsx deleted file mode 100644 index 96e1f2e..0000000 --- a/src/pages/sales/customers/[customerId]/index.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import CustomerIdPage from '@/components/customer-id-page' -import { useRouter } from 'next/router' -import CustomerLayout from '@/components/customer-layout' -import Layout from '@/components/layouts' -import SalesNav from '@/components/sales-nav' -import { useQuery } from '@tanstack/react-query' -import { fetcher } from '@/utils' - -function CustomerIdRoute() { - const router = useRouter() - const customerId = router.query.customerId - - const customerQueryData = useQuery( - ['customer', customerId], - () => fetcher(`/api/get-customer/${customerId}`), - { useErrorBoundary: true, enabled: !!customerId } - ) - return -} - -CustomerIdRoute.getLayout = function getLayout(page: React.ReactNode) { - return ( - - - {page} - - - ) -} - -export default CustomerIdRoute - -// export async function getServerSideProps( -// context: GetServerSidePropsContext & { params: { customerId: string } } -// ) { -// const user = await getServerSession(context.req, context.res, authOptions) -// if (!user) { -// redirect('/login') -// } -// const customers = await getCustomerListItems() - -// const data = { customers } -// return { -// props: { -// data, -// }, -// } -// } diff --git a/src/pages/sales/customers/index.tsx b/src/pages/sales/customers/index.tsx deleted file mode 100644 index 20037d1..0000000 --- a/src/pages/sales/customers/index.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import { getCustomerListItems } from '@/models/customerserver' -import { getServerSession } from 'next-auth' -import { authOptions } from '@/pages/api/auth/[...nextauth]' -import CustomerLayout from '@/components/customer-layout' -import { redirect } from 'next/navigation' -import { - GetServerSidePropsContext, - InferGetServerSidePropsType, -} from 'next/types' -import SalesNav from '@/components/sales-nav' -import Layout from '@/components/layouts' -import { useSession } from 'next-auth/react' -import { useRouter } from 'next/router' -import { fetcher } from '@/utils' -import { useQuery } from '@tanstack/react-query' - -function CustomersPage({ children }: { children: React.ReactNode }) { - const session = useSession() - const router = useRouter() - if (session.status == 'unauthenticated') { - router.push('/login') - } - - return {children} -} - -CustomersPage.getLayout = function getLayout(page: React.ReactNode) { - return ( - - {page} - - ) -} - -export default CustomersPage - -// export async function getServerSideProps(context: GetServerSidePropsContext) { -// const user = await getServerSession(context.req, context.res, authOptions) -// if (!user) { -// return { -// redirect: { -// destination: '/login', -// permanent: false, -// }, -// } -// } -// const customers = await getCustomerListItems() -// return { -// props: { -// customers, -// }, -// } -// } diff --git a/src/pages/sales/deposits/[depositId]/index.tsx b/src/pages/sales/deposits/[depositId]/index.tsx deleted file mode 100644 index fe4763e..0000000 --- a/src/pages/sales/deposits/[depositId]/index.tsx +++ /dev/null @@ -1,69 +0,0 @@ -import { getDepositDetails } from '@/models/depositserver' -import invariant from 'tiny-invariant' -import { TrashIcon } from '@/components' -import { getServerSession } from 'next-auth' -import { authOptions } from '@/pages/api/auth/[...nextauth]' -import { redirect } from 'next/navigation' -import { - GetServerSidePropsContext, - InferGetServerSidePropsType, -} from 'next/types' -import DeleteDepositForm from '@/components/forms/delete-deposit-form' -import Layout from '@/components/layouts' -import SalesNav from '@/components/sales-nav' - -export default function DepositRoute({ - data, -}: InferGetServerSidePropsType) { - return ( - - -
-
- {data.depositNote ? ( - - Note: -
- {data.depositNote} -
- ) : ( - - No note - - )} -
- -
-
-
-
-
- ) -} - -export async function getServerSideProps( - context: GetServerSidePropsContext & { params: { depositId: string } } -) { - const user = await getServerSession(context.req, context.res, authOptions) - if (!user) { - redirect('/login') - } - - const { depositId } = context.params - invariant(typeof depositId === 'string', 'params.depositId is not available') - const depositDetails = await getDepositDetails(depositId) - if (!depositDetails) { - throw new Response('not found', { status: 404 }) - } - - const data = { - depositNote: depositDetails.note, - depositId, - } - - return { - props: { - data, - }, - } -} diff --git a/src/pages/sales/index.tsx b/src/pages/sales/index.tsx deleted file mode 100644 index 270070f..0000000 --- a/src/pages/sales/index.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import { useSession } from 'next-auth/react' -import SalesNav from '@/components/sales-nav' -// import { getFirstCustomer } from '@/models/customerserver' -// import { getFirstInvoice } from '@/models/invoiceserver' -// import { authOptions } from '@/pages/api/auth/[...nextauth]' -// import { getServerSession } from 'next-auth' -// import { -// GetServerSidePropsContext, -// InferGetServerSidePropsType, -// } from 'next/types' -import Layout from '@/components/layouts' -import { useRouter } from 'next/router' - -function SalesPage({ children }: { children: React.ReactNode }) { - const session = useSession() - const router = useRouter() - if (session.status == 'unauthenticated') { - router.push('/login') - } - return {children} -} - -SalesPage.getLayout = function getLayout(page: React.ReactNode) { - return {page} -} - -export default SalesPage - -// export async function getServerSideProps(context: GetServerSidePropsContext) { -// const session = await getServerSession(context.req, context.res, authOptions) -// if (!session) { -// return { -// redirect: { -// destination: '/login', -// permanent: false, -// }, -// } -// } -// const [firstInvoice, firstCustomer] = await Promise.all([ -// getFirstInvoice(), -// getFirstCustomer(), -// ]) -// const data = { -// firstInvoiceId: firstInvoice?.id, -// firstCustomerId: firstCustomer?.id, -// } -// return { -// props: { -// data, -// }, -// } -// } diff --git a/src/pages/sales/invoices/[invoiceId]/index.tsx b/src/pages/sales/invoices/[invoiceId]/index.tsx deleted file mode 100644 index 432429e..0000000 --- a/src/pages/sales/invoices/[invoiceId]/index.tsx +++ /dev/null @@ -1,99 +0,0 @@ -import Link from 'next/link' -import { LabelText } from '@/components' -import { currencyFormatter, fetcher } from '@/utils' -import { Deposits, LineItemDisplay } from '@/components/deposit' -import InvoicesPage from '@/components/invoice-page' -import Layout from '@/components/layouts' -import SalesNav from '@/components/sales-nav' -import { useRouter } from 'next/router' -import { useQuery } from '@tanstack/react-query' -import { CustomerSkeleton } from '@/components/customer-layout' -import { LineItem } from '@prisma/client' - -export function validateAmount(amount: number) { - if (amount <= 0) return 'Must be greater than 0' - if (Number(amount.toFixed(2)) !== amount) { - return 'Must only have two decimal places' - } - return null -} - -export function validateDepositDate(date: Date) { - if (Number.isNaN(date.getTime())) { - return 'Please enter a valid date' - } - return null -} - -export const lineItemClassName = - 'flex justify-between border-t border-gray-100 py-4 text-[14px] leading-[24px]' - -function InvoiceRoute() { - const router = useRouter() - const invoiceId = router.query.invoiceId - - const { data, isLoading, isSuccess, isError } = useQuery( - ['invoices', invoiceId], - () => fetcher(`/api/get-invoice/${invoiceId}`) - ) - - return ( - <> - {isLoading && } - {isSuccess && ( -
- - {data?.invoiceData.customerName} - -
- {currencyFormatter.format(data?.invoiceData.totalAmount)} -
- - - {data?.invoiceData.dueDisplay} - - {` • Invoiced ${data?.invoiceData.invoiceDateDisplay}`} - -
- {data?.invoiceData.lineItems.map((item: LineItem) => ( - - ))} -
-
Net Total
-
{currencyFormatter.format(data?.invoiceData.totalAmount)}
-
-
- -
- )} - - ) -} - -InvoiceRoute.getLayout = function getLayout(page: React.ReactNode) { - return ( - - - {page} - - - ) -} - -export default InvoiceRoute diff --git a/src/pages/sales/invoices/index.tsx b/src/pages/sales/invoices/index.tsx deleted file mode 100644 index 25b7c27..0000000 --- a/src/pages/sales/invoices/index.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import InvoicesPage from '@/components/invoice-page' -import SalesNav from '@/components/sales-nav' -import Layout from '@/components/layouts' - -function InvoicesLayout() { - return -} - -InvoicesLayout.getLayout = function getLayout(page: React.ReactNode) { - return ( - - {page} - - ) -} - -export default InvoicesLayout diff --git a/src/utils/index.ts b/src/utils/index.ts index 2262880..4388ae7 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,66 +1,81 @@ -const DEFAULT_REDIRECT = '/' +const DEFAULT_REDIRECT = "/"; export function safeRedirect( to: FormDataEntryValue | string | null | undefined, defaultRedirect: string = DEFAULT_REDIRECT ) { - if (!to || typeof to !== 'string') { - return defaultRedirect + if (!to || typeof to !== "string") { + return defaultRedirect; } - if (!to.startsWith('/') || to.startsWith('//')) { - return defaultRedirect + if (!to.startsWith("/") || to.startsWith("//")) { + return defaultRedirect; } - return to + return to; } export function validateEmail(email: unknown): email is string { - return typeof email === 'string' && email.length > 3 && email.includes('@') + return typeof email === "string" && email.length > 3 && email.includes("@"); } -export const currencyFormatter = new Intl.NumberFormat('en-US', { - style: 'currency', - currency: 'USD', +export const currencyFormatter = new Intl.NumberFormat("en-US", { + style: "currency", + currency: "USD", maximumFractionDigits: 2, -}) +}); export function asUTC(date: Date) { - return new Date(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate()) + return new Date(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate()); } export function parseDate(dateString: string) { - const [year, month, day] = dateString.split('-').map(Number) - return asUTC(new Date(year, month - 1, day)) + const [year, month, day] = dateString.split("-").map(Number); + return asUTC(new Date(year, month - 1, day)); } export function validateCustomerId(customerId: string) { // the database won't let us create an invoice without a customer // so all we need to do is make sure this is not an empty string - return customerId === '' ? 'Please select a customer' : null + return customerId === "" ? "Please select a customer" : null; } export function validateDueDate(date: Date) { if (Number.isNaN(date.getTime())) { - return 'Please enter a valid date' + return "Please enter a valid date"; } - return null + return null; } export function validateLineItemQuantity(quantity: number) { - if (quantity <= 0) return 'Must be greater than 0' + if (quantity <= 0) return "Must be greater than 0"; if (Number(quantity.toFixed(0)) !== quantity) { - return 'Fractional quantities are not allowed' + return "Fractional quantities are not allowed"; } - return null + return null; } export function validateLineItemUnitPrice(unitPrice: number) { - if (unitPrice <= 0) return 'Must be greater than 0' + if (unitPrice <= 0) return "Must be greater than 0"; if (Number(unitPrice.toFixed(2)) !== unitPrice) { - return 'Must only have two decimal places' + return "Must only have two decimal places"; } - return null + return null; } -export const fetcher = (url: string) => fetch(url).then((res) => res.json()) +export function validateAmount(amount: number) { + if (amount <= 0) return "Must be greater than 0"; + if (Number(amount.toFixed(2)) !== amount) { + return "Must only have two decimal places"; + } + return null; +} + +export function validateDepositDate(date: Date) { + if (Number.isNaN(date.getTime())) { + return "Please enter a valid date"; + } + return null; +} + +export const fetcher = (url: string) => fetch(url).then((res) => res.json()); diff --git a/src/utils/uploadthing.ts b/src/utils/uploadthing.ts index 8de9fb9..7607acc 100644 --- a/src/utils/uploadthing.ts +++ b/src/utils/uploadthing.ts @@ -1,6 +1,6 @@ -import { generateComponents } from '@uploadthing/react' +import { generateComponents } from "@uploadthing/react"; -import type { OurFileRouter } from '@/server/uploadthing' +import type { OurFileRouter } from "@/app/api/uploadthing/core"; export const { UploadButton, UploadDropzone, Uploader } = - generateComponents() + generateComponents(); diff --git a/tsconfig.json b/tsconfig.json index 61c19ab..31eebad 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,7 +1,11 @@ { "compilerOptions": { "target": "es5", - "lib": ["dom", "dom.iterable", "esnext"], + "lib": [ + "dom", + "dom.iterable", + "esnext" + ], "allowJs": true, "skipLibCheck": true, "strict": true, @@ -15,9 +19,23 @@ "jsx": "preserve", "incremental": true, "paths": { - "@/*": ["./src/*"] - } + "@/*": [ + "./src/*" + ] + }, + "plugins": [ + { + "name": "next" + } + ] }, - "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], - "exclude": ["node_modules"] + "include": [ + "next-env.d.ts", + "**/*.ts", + "**/*.tsx", + ".next/types/**/*.ts" + ], + "exclude": [ + "node_modules" + ] } From a4a20710bea87a0fa07f4d120653cf317d3dd47c Mon Sep 17 00:00:00 2001 From: Benjamin Patton Date: Fri, 11 Aug 2023 17:10:33 -0400 Subject: [PATCH 2/2] fixed linting errors --- src/components/customer-layout/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/customer-layout/index.tsx b/src/components/customer-layout/index.tsx index dec6499..cb0a060 100644 --- a/src/components/customer-layout/index.tsx +++ b/src/components/customer-layout/index.tsx @@ -47,7 +47,7 @@ async function Customers() { return (
{customers?.map((customer: Pick) => ( - +
{customer.name}