From b4aa7b33b976264d25e0b178c72fb3ae00627683 Mon Sep 17 00:00:00 2001 From: ndom91 Date: Sun, 17 Dec 2023 16:21:11 +0100 Subject: [PATCH] feat: add zod and begin server action migration --- auth.js | 16 +- package.json | 3 +- pnpm-lock.yaml | 98 ++++------- src/app/auth/signin/page.jsx | 161 ++++++------------ src/app/auth/signin/providerIcons.jsx | 73 ++++++++ src/app/categories/actions.jsx | 59 +++++++ src/app/categories/categoryTable.jsx | 123 ++++++++++++++ src/app/categories/page.jsx | 229 +++----------------------- src/app/categories/submitButton.jsx | 36 ++++ src/app/layout.jsx | 6 +- src/app/page.jsx | 134 +++++++-------- src/components/loadingSpinner.jsx | 22 +++ src/components/sidebar.jsx | 10 +- src/lib/store.js | 2 + 14 files changed, 514 insertions(+), 458 deletions(-) create mode 100644 src/app/auth/signin/providerIcons.jsx create mode 100644 src/app/categories/actions.jsx create mode 100644 src/app/categories/categoryTable.jsx create mode 100644 src/app/categories/submitButton.jsx create mode 100644 src/components/loadingSpinner.jsx diff --git a/auth.js b/auth.js index 41ddada..8978dab 100644 --- a/auth.js +++ b/auth.js @@ -9,8 +9,10 @@ import prisma from "@/lib/prisma" // import type { NextAuthConfig } from "next-auth" const providers = [ - process.env.GITHUB_ID ? GitHub : null, - process.env.GOOGLE_ID ? Google : null, + process.env.GITHUB_ID && + GitHub({ clientId: process.env.GITHUB_ID, clientSecret: process.env.GITHUB_SECRET }), + process.env.GOOGLE_ID && + Google({ clientId: process.env.GOOGLE_ID, clientSecret: process.env.GOOGLE_SECRET }), process.env.KEYCLOAK_ID && Keycloak({ clientId: process.env.KEYCLOAK_ID, @@ -43,10 +45,18 @@ const adapter = { const config = { providers, adapter: adapter, + callbacks: { + async session({ session, user }) { + session.user.userId = user.id + return session + }, + }, pages: { signIn: "/auth/signin", }, } // } satisfies NextAuthConfig -export const { handlers, auth, signIn, signOut } = NextAuth(config) +const { handlers, auth, signIn, signOut } = NextAuth(config) + +export { handlers, auth, signIn, signOut, providers } diff --git a/package.json b/package.json index f9d20f8..3b00164 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ "selfhost" ], "scripts": { - "dev": "next dev", + "dev": "next dev -p 3001", "build": "next build", "start": "next start", "lint": "next lint", @@ -38,6 +38,7 @@ "react-masonry-css": "^1.0.16", "react-use": "^17.4.2", "react-use-focus-trap": "1.1.7", + "zod": "^3.22.4", "zustand": "^4.4.7" }, "devDependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a65322e..2ee2310 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1,9 +1,5 @@ lockfileVersion: '6.0' -settings: - autoInstallPeers: true - excludeLinksFromLockfile: false - dependencies: '@auth/prisma-adapter': specifier: ^1.0.11 @@ -56,14 +52,17 @@ dependencies: react-use-focus-trap: specifier: 1.1.7 version: 1.1.7(react@18.2.0) + zod: + specifier: ^3.22.4 + version: 3.22.4 zustand: specifier: ^4.4.7 - version: 4.4.7(@types/react@18.0.35)(immer@10.0.3)(react@18.2.0) + version: 4.4.7(immer@10.0.3)(react@18.2.0) devDependencies: '@commitlint/cli': specifier: ^18.4.3 - version: 18.4.3(typescript@4.7.2) + version: 18.4.3 '@commitlint/config-conventional': specifier: ^18.4.3 version: 18.4.3 @@ -78,7 +77,7 @@ devDependencies: version: 8.55.0 eslint-config-next: specifier: 14.0.4 - version: 14.0.4(eslint@8.55.0)(typescript@4.7.2) + version: 14.0.4(eslint@8.55.0) eslint-config-prettier: specifier: ^9.1.0 version: 9.1.0(eslint@8.55.0) @@ -105,7 +104,7 @@ devDependencies: version: 2.9.0 simple-zustand-devtools: specifier: ^1.1.0 - version: 1.1.0(@types/react-dom@18.0.11)(@types/react@18.0.35)(react-dom@18.2.0)(react@18.2.0)(zustand@4.4.7) + version: 1.1.0(react-dom@18.2.0)(react@18.2.0)(zustand@4.4.7) tailwindcss: specifier: ^3.3.6 version: 3.3.6 @@ -204,14 +203,14 @@ packages: regenerator-runtime: 0.14.0 dev: true - /@commitlint/cli@18.4.3(typescript@4.7.2): + /@commitlint/cli@18.4.3: resolution: {integrity: sha512-zop98yfB3A6NveYAZ3P1Mb6bIXuCeWgnUfVNkH4yhIMQpQfzFwseadazOuSn0OOfTt0lWuFauehpm9GcqM5lww==} engines: {node: '>=v18'} hasBin: true dependencies: '@commitlint/format': 18.4.3 '@commitlint/lint': 18.4.3 - '@commitlint/load': 18.4.3(typescript@4.7.2) + '@commitlint/load': 18.4.3 '@commitlint/read': 18.4.3 '@commitlint/types': 18.4.3 execa: 5.1.1 @@ -281,7 +280,7 @@ packages: '@commitlint/types': 18.4.3 dev: true - /@commitlint/load@18.4.3(typescript@4.7.2): + /@commitlint/load@18.4.3: resolution: {integrity: sha512-v6j2WhvRQJrcJaj5D+EyES2WKTxPpxENmNpNG3Ww8MZGik3jWRXtph0QTzia5ZJyPh2ib5aC/6BIDymkUUM58Q==} engines: {node: '>=v18'} dependencies: @@ -291,8 +290,8 @@ packages: '@commitlint/types': 18.4.3 '@types/node': 18.19.3 chalk: 4.1.2 - cosmiconfig: 8.3.6(typescript@4.7.2) - cosmiconfig-typescript-loader: 5.0.0(@types/node@18.19.3)(cosmiconfig@8.3.6)(typescript@4.7.2) + cosmiconfig: 8.3.6 + cosmiconfig-typescript-loader: 5.0.0(@types/node@18.19.3)(cosmiconfig@8.3.6) lodash.isplainobject: 4.0.6 lodash.merge: 4.6.2 lodash.uniq: 4.5.0 @@ -867,32 +866,13 @@ packages: resolution: {integrity: sha512-L5eZmzw89eXBKkiqVBcJfU1QGx9y+wurRIEgt0cuLH0hwNtVUxtx+6cu0R2STwWj468sjXyBYPYDtGclUd1kjQ==} dev: false - /@types/prop-types@15.7.5: - resolution: {integrity: sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==} - - /@types/react-dom@18.0.11: - resolution: {integrity: sha512-O38bPbI2CWtgw/OoQoY+BRelw7uysmXbWvw3nLWO21H1HSh+GOlqPuXshJfjmpNlKiiSDG9cc1JZAaMmVdcTlw==} - dependencies: - '@types/react': 18.0.35 - dev: true - - /@types/react@18.0.35: - resolution: {integrity: sha512-6Laome31HpetaIUGFWl1VQ3mdSImwxtFZ39rh059a1MNnKGqBpC88J6NJ8n/Is3Qx7CefDGLgf/KhN/sYCf7ag==} - dependencies: - '@types/prop-types': 15.7.5 - '@types/scheduler': 0.16.3 - csstype: 3.1.3 - - /@types/scheduler@0.16.3: - resolution: {integrity: sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ==} - /@types/websocket@1.0.10: resolution: {integrity: sha512-svjGZvPB7EzuYS94cI7a+qhwgGU1y89wUgjT6E2wVUfmAGIvRfT7obBvRtnhXCSsoMdlG4gBFGE7MfkIXZLoww==} dependencies: '@types/node': 14.18.26 dev: false - /@typescript-eslint/parser@5.48.1(eslint@8.55.0)(typescript@4.7.2): + /@typescript-eslint/parser@5.48.1(eslint@8.55.0): resolution: {integrity: sha512-4yg+FJR/V1M9Xoq56SF9Iygqm+r5LMXvheo6DQ7/yUWynQ4YfCRnsKuRgqH4EQ5Ya76rVwlEpw4Xu+TgWQUcdA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: @@ -904,10 +884,9 @@ packages: dependencies: '@typescript-eslint/scope-manager': 5.48.1 '@typescript-eslint/types': 5.48.1 - '@typescript-eslint/typescript-estree': 5.48.1(typescript@4.7.2) + '@typescript-eslint/typescript-estree': 5.48.1 debug: 4.3.4 eslint: 8.55.0 - typescript: 4.7.2 transitivePeerDependencies: - supports-color dev: true @@ -925,7 +904,7 @@ packages: engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dev: true - /@typescript-eslint/typescript-estree@5.48.1(typescript@4.7.2): + /@typescript-eslint/typescript-estree@5.48.1: resolution: {integrity: sha512-Hut+Osk5FYr+sgFh8J/FHjqX6HFcDzTlWLrFqGoK5kVUN3VBHF/QzZmAsIXCQ8T/W9nQNBTqalxi1P3LSqWnRA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: @@ -940,8 +919,7 @@ packages: globby: 11.1.0 is-glob: 4.0.3 semver: 7.3.8 - tsutils: 3.21.0(typescript@4.7.2) - typescript: 4.7.2 + tsutils: 3.21.0 transitivePeerDependencies: - supports-color dev: true @@ -1597,7 +1575,7 @@ packages: toggle-selection: 1.0.6 dev: false - /cosmiconfig-typescript-loader@5.0.0(@types/node@18.19.3)(cosmiconfig@8.3.6)(typescript@4.7.2): + /cosmiconfig-typescript-loader@5.0.0(@types/node@18.19.3)(cosmiconfig@8.3.6): resolution: {integrity: sha512-+8cK7jRAReYkMwMiG+bxhcNKiHJDM6bR9FD/nGBXOWdMLuYawjF5cGrtLilJ+LGd3ZjCXnJjR5DkfWPoIVlqJA==} engines: {node: '>=v16'} peerDependencies: @@ -1606,12 +1584,11 @@ packages: typescript: '>=4' dependencies: '@types/node': 18.19.3 - cosmiconfig: 8.3.6(typescript@4.7.2) + cosmiconfig: 8.3.6 jiti: 1.21.0 - typescript: 4.7.2 dev: true - /cosmiconfig@8.3.6(typescript@4.7.2): + /cosmiconfig@8.3.6: resolution: {integrity: sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==} engines: {node: '>=14'} peerDependencies: @@ -1624,7 +1601,6 @@ packages: js-yaml: 4.1.0 parse-json: 5.2.0 path-type: 4.0.0 - typescript: 4.7.2 dev: true /cross-spawn@7.0.3: @@ -1679,6 +1655,7 @@ packages: /csstype@3.1.3: resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} + dev: false /damerau-levenshtein@1.0.8: resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==} @@ -2086,7 +2063,7 @@ packages: resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} engines: {node: '>=10'} - /eslint-config-next@14.0.4(eslint@8.55.0)(typescript@4.7.2): + /eslint-config-next@14.0.4(eslint@8.55.0): resolution: {integrity: sha512-9/xbOHEQOmQtqvQ1UsTQZpnA7SlDMBtuKJ//S4JnoyK3oGLhILKXdBgu/UO7lQo/2xOykQULS1qQ6p2+EpHgAQ==} peerDependencies: eslint: ^7.23.0 || ^8.0.0 @@ -2097,7 +2074,7 @@ packages: dependencies: '@next/eslint-plugin-next': 14.0.4 '@rushstack/eslint-patch': 1.6.1 - '@typescript-eslint/parser': 5.48.1(eslint@8.55.0)(typescript@4.7.2) + '@typescript-eslint/parser': 5.48.1(eslint@8.55.0) eslint: 8.55.0 eslint-import-resolver-node: 0.3.6 eslint-import-resolver-typescript: 3.5.3(eslint-plugin-import@2.29.1)(eslint@8.55.0) @@ -2105,7 +2082,6 @@ packages: eslint-plugin-jsx-a11y: 6.8.0(eslint@8.55.0) eslint-plugin-react: 7.33.2(eslint@8.55.0) eslint-plugin-react-hooks: 4.5.0(eslint@8.55.0) - typescript: 4.7.2 transitivePeerDependencies: - eslint-import-resolver-webpack - supports-color @@ -2180,7 +2156,7 @@ packages: eslint-import-resolver-webpack: optional: true dependencies: - '@typescript-eslint/parser': 5.48.1(eslint@8.55.0)(typescript@4.7.2) + '@typescript-eslint/parser': 5.48.1(eslint@8.55.0) debug: 3.2.7 eslint: 8.55.0 eslint-import-resolver-node: 0.3.9 @@ -2199,7 +2175,7 @@ packages: '@typescript-eslint/parser': optional: true dependencies: - '@typescript-eslint/parser': 5.48.1(eslint@8.55.0)(typescript@4.7.2) + '@typescript-eslint/parser': 5.48.1(eslint@8.55.0) array-includes: 3.1.7 array.prototype.findlastindex: 1.2.3 array.prototype.flat: 1.3.2 @@ -5074,7 +5050,7 @@ packages: requiresBuild: true dev: true - /simple-zustand-devtools@1.1.0(@types/react-dom@18.0.11)(@types/react@18.0.35)(react-dom@18.2.0)(react@18.2.0)(zustand@4.4.7): + /simple-zustand-devtools@1.1.0(react-dom@18.2.0)(react@18.2.0)(zustand@4.4.7): resolution: {integrity: sha512-Axfcfr9L3YL3kto7aschCQLY2VUlXXMnIVtaTe9Y0qWbNmPsX/y7KsNprmxBZoB0pww5ZGs1u/ohcrvQ3tE6jA==, tarball: https://registry.npmjs.com/simple-zustand-devtools/-/simple-zustand-devtools-1.1.0.tgz} peerDependencies: '@types/react': '>=18.0.0' @@ -5083,11 +5059,9 @@ packages: react-dom: '>=18.0.0' zustand: '>=1.0.2' dependencies: - '@types/react': 18.0.35 - '@types/react-dom': 18.0.11 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - zustand: 4.4.7(@types/react@18.0.35)(immer@10.0.3)(react@18.2.0) + zustand: 4.4.7(immer@10.0.3)(react@18.2.0) dev: true /slash@3.0.0: @@ -5627,14 +5601,13 @@ packages: resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==} dev: true - /tsutils@3.21.0(typescript@4.7.2): + /tsutils@3.21.0: resolution: {integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==} engines: {node: '>= 6'} peerDependencies: typescript: '>=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta' dependencies: tslib: 1.14.1 - typescript: 4.7.2 dev: true /type-check@0.4.0: @@ -5707,12 +5680,6 @@ packages: is-typed-array: 1.1.12 dev: true - /typescript@4.7.2: - resolution: {integrity: sha512-Mamb1iX2FDUpcTRzltPxgWMKy3fhg0TN378ylbktPGPK/99KbDtMQ4W1hwgsbPAsG3a0xKa1vmw4VKZQbkvz5A==} - engines: {node: '>=4.2.0'} - hasBin: true - dev: true - /unbox-primitive@1.0.2: resolution: {integrity: sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==} dependencies: @@ -6043,7 +6010,11 @@ packages: engines: {node: '>=10'} dev: true - /zustand@4.4.7(@types/react@18.0.35)(immer@10.0.3)(react@18.2.0): + /zod@3.22.4: + resolution: {integrity: sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==} + dev: false + + /zustand@4.4.7(immer@10.0.3)(react@18.2.0): resolution: {integrity: sha512-QFJWJMdlETcI69paJwhSMJz7PPWjVP8Sjhclxmxmxv/RYI7ZOvR5BHX+ktH0we9gTWQMxcne8q1OY8xxz604gw==} engines: {node: '>=12.7.0'} peerDependencies: @@ -6058,7 +6029,10 @@ packages: react: optional: true dependencies: - '@types/react': 18.0.35 immer: 10.0.3 react: 18.2.0 use-sync-external-store: 1.2.0(react@18.2.0) + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false diff --git a/src/app/auth/signin/page.jsx b/src/app/auth/signin/page.jsx index 010c052..5f2d424 100644 --- a/src/app/auth/signin/page.jsx +++ b/src/app/auth/signin/page.jsx @@ -1,90 +1,27 @@ -import { useState } from "react" -// import { useRouter } from "next/router" -import { signIn, getProviders, getCsrfToken } from "next-auth/react" +import { providers, signIn } from "../../../../auth" +import { ProviderIcons } from "./providerIcons" -const ProviderIcons = ({ provider }) => { - if (provider === "github") { - return ( - - - - ) - } else if (provider === "google") { - return ( - - - - ) - } else if (provider === "keycloak") { - return ( - - - - ) - } else if (provider === "authentik") { - return ( - - - - - - - - ) - } -} +const AuthPage = async () => { + const enabledProviders = providers.map((p) => { + const provider = typeof p === "function" ? p() : p + return { + name: provider.name, + id: provider.id, + } + }) + // console.log({ enabledProviders }) + const autoLoginFirstProvider = process.env.AUTOLOGIN_FIRST_PROVIDER -const Signin = ({ providers, csrfToken, autoLoginFirstProvider }) => { - const [email, setEmail] = useState("") + // const [email, setEmail] = useState("") // const { query } = useRouter() - const containsOauthProviders = Object.keys(providers).some((p) => - ["google", "github", "keycloak", "authentik"].includes(p), - ) + // const containsOauthProviders = enabledProviders.some((p) => + // ["google", "github", "keycloak", "authentik"].includes(p.id), + // ) // if (autoLoginFirstProvider && !query.error) { - // signIn(providers[0]) - // } + if (autoLoginFirstProvider) { + signIn(providers[0].id) + } return ( <> @@ -107,21 +44,41 @@ const Signin = ({ providers, csrfToken, autoLoginFirstProvider }) => { Briefkasten -
- {providers && containsOauthProviders && ( +
+ {enabledProviders.map((p) => ( +
{ + "use server" + await signIn(p.id) + }} + > + +
+ ))} + {/* providers && containsOauthProviders && (
{Object.values(providers).map((p) => p.type === "oauth" ? (
- )} - {Object.keys(providers).includes("email") ? ( + ) */} + {/* Object.keys(providers).includes("email") ? (
{ place="Continue with Magic Link Email" />
- ) : null} + ) : null */}
@@ -178,18 +135,4 @@ const Signin = ({ providers, csrfToken, autoLoginFirstProvider }) => { ) } -export default Signin - -export async function getServerSideProps(context) { - const providers = await getProviders() - const csrfToken = await getCsrfToken(context) - const autoLoginFirstProvider = process.env.AUTOLOGIN_FIRST_PROVIDER - - return { - props: { - providers, - csrfToken, - autoLoginFirstProvider, - }, - } -} +export default AuthPage diff --git a/src/app/auth/signin/providerIcons.jsx b/src/app/auth/signin/providerIcons.jsx new file mode 100644 index 0000000..172955f --- /dev/null +++ b/src/app/auth/signin/providerIcons.jsx @@ -0,0 +1,73 @@ +export const ProviderIcons = ({ provider }) => { + if (provider === "github") { + return ( + + + + ) + } else if (provider === "google") { + return ( + + + + ) + } else if (provider === "keycloak") { + return ( + + + + ) + } else if (provider === "authentik") { + return ( + + + + + + + + ) + } +} + diff --git a/src/app/categories/actions.jsx b/src/app/categories/actions.jsx new file mode 100644 index 0000000..f247704 --- /dev/null +++ b/src/app/categories/actions.jsx @@ -0,0 +1,59 @@ +"use server" + +import { z } from "zod" + +const schema = z.object({ + name: z + .string({ + invalid_type_error: "Invalid Name", + }) + .max(190), + description: z + .string({ + invalid_type_error: "Invalid Description", + }) + .max(190), +}) + +const createCategory = async (userId, formData) => { + const validatedFields = schema.safeParse({ + name: formData.get("name"), + description: formData.get("description"), + }) + + // Return early if the form data is invalid + if (!validatedFields.success) { + return { + errors: validatedFields.error.flatten().fieldErrors, + } + } + + try { + // if (categoryName.length > 190 || categoryDesc.length > 190) { + // toast(toastTypes.WARNING, "Category or name too long") + // return + // } + const addRes = await fetch("/api/categories", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + ...validatedFields, + userId, + }), + }) + if (addRes.ok) { + const addData = await addRes.json() + // addCategory({ ...addData.data, desc: addData.data.description }) + // setCategoryName("") + // setCategoryDesc("") + // toast(toastTypes.SUCCESS, `Successfully saved "${categoryName}"`) + } + } catch (error) { + console.error(error) + // toast(toastTypes.ERROR, `Error saving ${categoryName}`) + } +} + +export { createCategory } diff --git a/src/app/categories/categoryTable.jsx b/src/app/categories/categoryTable.jsx new file mode 100644 index 0000000..3092e6e --- /dev/null +++ b/src/app/categories/categoryTable.jsx @@ -0,0 +1,123 @@ +"use client" + +import { useState } from "react" + +import { SubmitButton } from "./submitButton" +import CategoryTableRow from "@/components/categoryTableRow" +import Breadcrumbs from "@/components/breadcrumbs" +import { createCategory } from "./actions" +import { useToast, toastTypes } from "@/lib/hooks" + +const breadcrumbs = [ + { + name: "Dashboard", + icon: ``, + }, + { + name: "Categories", + icon: ``, + }, +] + +export default function CategoryTable({ categories, userId }) { + const createCategoryWithUser = createCategory.bind(null, userId) + const [searchString, setSearchString] = useState("") + const toast = useToast(5000) + + return ( + <> +
+ +
+ +
+
+ + + +
+ setSearchString(e.target.value)} + className="w-2/3 rounded-md border-2 border-slate-200 px-2 py-1 pl-8 pr-8 text-base text-slate-600 outline-none placeholder:text-slate-200 focus:border-slate-200 focus:ring-2 focus:ring-slate-200 focus:ring-offset-transparent" + placeholder="Search for items" + /> +
+
+
+
+
+ + + + + + + + + + + + + {categories?.map((category) => ( + + ))} + + + + + + + +
+ ID + + Count + + Name + + Description + + Date Added + + Edit +
+ Add new Category + + + + + + + + +
+
+
+ + ) +} diff --git a/src/app/categories/page.jsx b/src/app/categories/page.jsx index 3a668a8..85e555c 100644 --- a/src/app/categories/page.jsx +++ b/src/app/categories/page.jsx @@ -1,211 +1,33 @@ -import Head from "next/head" -import { useState } from "react" -import { unstable_getServerSession } from "next-auth/next" - -import Layout from "@/components/layout" -import CategoryTableRow from "@/components/categoryTableRow" -import Breadcrumbs from "@/components/breadcrumbs" - +import { auth } from "../../../auth" +import { redirect } from "next/navigation" +import CategoryTable from "./categoryTable" +import Sidebar from "@/components/sidebar" import prisma from "@/lib/prisma" -import { useStore, initializeStore } from "@/lib/store" -import { useToast, toastTypes } from "@/lib/hooks" -import { authOptions } from "./api/auth/[...nextauth]" -const breadcrumbs = [ - { - name: "Dashboard", - icon: ``, - }, - { - name: "Categories", - icon: ``, - }, -] - -export default function Categories({ nextauth }) { - const [searchString, setSearchString] = useState("") - const categories = useStore((state) => { - if (!searchString) { - return state.categories - } else { - return state.categories.filter((cat) => - cat.name.toLowerCase().includes(searchString.toLowerCase()), - ) - } - }) - const addCategory = useStore((state) => state.addCategory) - const [categoryName, setCategoryName] = useState("") - const [categoryDesc, setCategoryDesc] = useState("") - const toast = useToast(5000) +export const metadata = { + title: "Briefkasten | Categories", +} - const saveNewCategory = async () => { - try { - if (categoryName.length > 190 || categoryDesc.length > 190) { - toast(toastTypes.WARNING, "Category or name too long") - return - } - const addRes = await fetch("/api/categories", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - name: categoryName, - desc: categoryDesc, - userId: nextauth.user?.userId, - }), - }) - if (addRes.status === 200) { - const addData = await addRes.json() - addCategory({ ...addData.data, desc: addData.data.description }) - setCategoryName("") - setCategoryDesc("") - toast(toastTypes.SUCCESS, `Successfully saved "${categoryName}"`) - } - } catch (error) { - console.error(error) - toast(toastTypes.ERROR, `Error saving ${categoryName}`) - } +export default async function Categories() { + const session = await auth() + if (!session) { + redirect("/auth/signin") } + const { categories, tags } = await getPageData(session) return ( - - - Briefkasten | Categories - -
-
- -
- -
-
- - - -
- setSearchString(e.target.value)} - className="w-2/3 rounded-md border-2 border-slate-200 px-2 py-1 pl-8 pr-8 text-base text-slate-600 outline-none placeholder:text-slate-200 focus:border-slate-200 focus:ring-2 focus:ring-slate-200 focus:ring-offset-transparent" - placeholder="Search for items" - /> -
-
+ <> + +
+
+
-
- - - - - - - - - - - - - {categories && - categories.map((category) => ( - - ))} - - - - - - - -
- ID - - Count - - Name - - Description - - Date Added - - Edit -
- Add new Category - - - setCategoryName(e.target.value)} - placeholder="Required" - className="block w-full rounded-md border-2 border-slate-200 bg-slate-50 p-2 py-1 text-sm text-slate-900 placeholder-slate-300 focus:border-slate-500 focus:ring-slate-500 " - /> - - setCategoryDesc(e.target.value)} - className="block w-full rounded-md border-2 border-slate-200 bg-slate-50 px-2 py-1 text-sm text-slate-900 placeholder-slate-300 focus:border-slate-500 focus:ring-slate-500" - /> - - - -
-
-
- + + ) } -export async function getServerSideProps(context) { - const session = await unstable_getServerSession(context.req, context.res, authOptions) - const zustandStore = initializeStore() - - if (!session) { - return { - redirect: { - destination: "/auth/signin", - permanent: false, - }, - } - } - +async function getPageData(session) { const categories = await prisma.category.findMany({ where: { userId: session.user.userId, @@ -223,14 +45,5 @@ export async function getServerSideProps(context) { }, }) - zustandStore.getState().setCategories(categories) - zustandStore.getState().setTags(tags) - - return { - props: { - session, - nextauth: session, - initialZustandState: JSON.parse(JSON.stringify(zustandStore.getState())), - }, - } + return { tags, categories } } diff --git a/src/app/categories/submitButton.jsx b/src/app/categories/submitButton.jsx new file mode 100644 index 0000000..7597367 --- /dev/null +++ b/src/app/categories/submitButton.jsx @@ -0,0 +1,36 @@ +"use client" + +import { useFormStatus } from "react-dom" +import { LoadingSpinner } from "@/components/loadingSpinner" + +export function SubmitButton() { + const { pending } = useFormStatus() + + return ( + + ) +} diff --git a/src/app/layout.jsx b/src/app/layout.jsx index f4e5871..cf0170e 100644 --- a/src/app/layout.jsx +++ b/src/app/layout.jsx @@ -1,7 +1,6 @@ "use client" import { SessionProvider } from "next-auth/react" -import Sidebar from "@/components/sidebar" import { StoreProvider } from "@/lib/store" import "./globals.css" @@ -42,10 +41,7 @@ export default function RootLayout({ children }) {
- - -
{children}
-
+ {children}
diff --git a/src/app/page.jsx b/src/app/page.jsx index ba1934e..6d68d85 100644 --- a/src/app/page.jsx +++ b/src/app/page.jsx @@ -1,7 +1,5 @@ "use client" -// import { auth } from "../../auth" -// import { authOptions } from "@/api/auth/[...nextauth]" import { useMemo, useState, useEffect, useRef } from "react" import { useDrop, usePrevious, useToggle } from "react-use" @@ -9,6 +7,7 @@ import { useStore } from "@/lib/store" import { initializeStore } from "@/lib/store" import { useToast, toastTypes } from "@/lib/hooks" +import Sidebar from "@/components/sidebar" import SlideOut from "@/components/slide-out" import Pagination from "@/components/pagination" import BookmarkCard from "@/components/bookmark-card" @@ -158,71 +157,76 @@ export default function Home() { return ( <> -
- - {bookmarks.length === 0 && } - {bookmarks.length > 0 && currentTableData.length === 0 && ( -
- {/* eslint-disable-next-line @next/next/no-img-element */} - No Results, Please Try Again - No results found, please try again! -
- )} -
-
- {currentTableData.length !== 0 && ( - <> - {activeView === viewTypes.CARD.name && ( - - {currentTableData.map((bookmark) => ( - initEdit(bookmark)} - /> - ))} - - )} + +
+
+ + {bookmarks.length === 0 && } + {bookmarks.length > 0 && currentTableData.length === 0 && ( +
+ {/* eslint-disable-next-line @next/next/no-img-element */} + No Results, Please Try Again + No results found, please try again! +
+ )} +
+
+ {currentTableData.length !== 0 && ( + <> + {activeView === viewTypes.CARD.name && ( + + {currentTableData.map((bookmark) => ( + initEdit(bookmark)} + /> + ))} + + )} - {activeView === viewTypes.LIST.name && ( - - )} - {activeView === viewTypes.DETAIL.name && ( -
- This view has not been implemented yet, please try Card or List view -
- )} - - )} -
-
- setCurrentPage(page)} - /> - - - {openModal && ( - + )} + {activeView === viewTypes.DETAIL.name && ( +
+ This view has not been implemented yet, please try Card or List view +
+ )} + + )} +
+
+ setCurrentPage(page)} /> - )} -
+ + + {openModal && ( + + )} +
+ ) } diff --git a/src/components/loadingSpinner.jsx b/src/components/loadingSpinner.jsx new file mode 100644 index 0000000..6fb9a3a --- /dev/null +++ b/src/components/loadingSpinner.jsx @@ -0,0 +1,22 @@ +export const LoadingSpinner = () => ( + + + + +) diff --git a/src/components/sidebar.jsx b/src/components/sidebar.jsx index 6d9d91e..c1c6c5f 100644 --- a/src/components/sidebar.jsx +++ b/src/components/sidebar.jsx @@ -56,7 +56,7 @@ export default function Sidebar() { "Content-Type": "application/json", }, body: JSON.stringify({ - userId: session?.user?.userId, + userId: session.data?.user?.userId, name: quickAddCategory, }), }) @@ -80,7 +80,7 @@ export default function Sidebar() { "Content-Type": "application/json", }, body: JSON.stringify({ - userId: session?.user?.userId, + userId: session.data?.user?.userId, name: quickAddTag, }), }) @@ -468,8 +468,8 @@ export default function Sidebar() { open ? "h-9 w-9" : "h-8 w-8 md:h-9 md:w-9" }`} src={ - session?.user?.image ?? - ` https://unavatar.io/${session?.user?.email ?? session?.user?.id}` + session.data?.user?.image ?? + ` https://unavatar.io/${session.data?.user?.email ?? session.data?.user?.id}` } alt="User Avatar" /> @@ -547,7 +547,7 @@ export default function Sidebar() { {open && (
-

{session?.user?.name}

+

{session.data?.user?.name}

)}
diff --git a/src/lib/store.js b/src/lib/store.js index 17f413a..05bd101 100644 --- a/src/lib/store.js +++ b/src/lib/store.js @@ -1,3 +1,5 @@ +"use client" + import { useRef, useContext, useLayoutEffect } from "react" import { produce } from "immer" import { createStore, useStore as useZustandStore } from "zustand"