diff --git a/package.json b/package.json index 10c74d3f6..d82816c08 100644 --- a/package.json +++ b/package.json @@ -67,13 +67,13 @@ "@sindresorhus/slugify": "^2.2.1", "@stripe/stripe-js": "^4.1.0", "@t3-oss/env-nextjs": "^0.10.1", - "@tanstack/react-query": "^4.36.1", + "@tanstack/react-query": "^5.53.3", "@tanstack/react-table": "^8.20.1", "@tremor/react": "^3.17.4", - "@trpc/client": "^10.43.6", - "@trpc/next": "^10.45.2", - "@trpc/react-query": "^10.43.6", - "@trpc/server": "^10.43.6", + "@trpc/client": "11.0.0-rc.498", + "@trpc/next": "11.0.0-rc.498", + "@trpc/react-query": "11.0.0-rc.498", + "@trpc/server": "11.0.0-rc.498", "@types/bcryptjs": "^2.4.6", "@types/papaparse": "^5.3.14", "@wojtekmaj/react-hooks": "^1.20.0", @@ -94,6 +94,7 @@ "next-auth": "^4.24.7", "next-nprogress-bar": "^2.3.13", "nodemailer": "^6.9.14", + "nuqs": "^1.17.8", "papaparse": "^5.4.1", "pdf-lib": "^1.17.1", "pg-boss": "^9.0.3", @@ -106,6 +107,7 @@ "react-dom": "18.2.0", "react-dropzone": "^14.2.3", "react-email": "2.1.6", + "react-error-boundary": "^4.0.13", "react-hook-form": "^7.52.1", "react-number-format": "^5.3.4", "react-pdf": "^8.0.2", @@ -146,7 +148,7 @@ "prisma": "^5.13.0", "tailwindcss": "^3.4.3", "tsx": "^4.7.0", - "typescript": "^5.4.5", + "typescript": "^5.5.3", "vitest": "^1.6.0" }, "ct3aMetadata": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 61b4824d3..b1599e973 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -100,7 +100,7 @@ importers: version: 4.2.0(react@18.3.1) '@scalar/nextjs-api-reference': specifier: ^0.4.18 - version: 0.4.18(@babel/core@7.24.5)(@opentelemetry/api@1.9.0)(postcss@8.4.40)(react-dom@18.2.0(react@18.3.1))(tailwindcss@3.4.3)(typescript@5.4.5)(vitest@1.6.0(@types/node@20.12.12)(terser@5.31.1)) + version: 0.4.18(@babel/core@7.24.5)(@opentelemetry/api@1.9.0)(postcss@8.4.40)(react-dom@18.2.0(react@18.3.1))(tailwindcss@3.4.3)(typescript@5.5.4)(vitest@1.6.0(@types/node@20.12.12)(terser@5.31.1)) '@sentry/nextjs': specifier: ^8.19.0 version: 8.19.0(@opentelemetry/api@1.9.0)(@opentelemetry/core@1.25.1(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.52.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.25.1(@opentelemetry/api@1.9.0))(next@14.2.4(@babel/core@7.24.5)(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.3.1))(react@18.3.1))(react@18.3.1)(webpack@5.92.0) @@ -118,10 +118,10 @@ importers: version: 4.1.0 '@t3-oss/env-nextjs': specifier: ^0.10.1 - version: 0.10.1(typescript@5.4.5)(zod@3.23.8) + version: 0.10.1(typescript@5.5.4)(zod@3.23.8) '@tanstack/react-query': - specifier: ^4.36.1 - version: 4.36.1(react-dom@18.2.0(react@18.3.1))(react@18.3.1) + specifier: ^5.53.3 + version: 5.53.3(react@18.3.1) '@tanstack/react-table': specifier: ^8.20.1 version: 8.20.1(react-dom@18.2.0(react@18.3.1))(react@18.3.1) @@ -129,17 +129,17 @@ importers: specifier: ^3.17.4 version: 3.17.4(react-dom@18.2.0(react@18.3.1))(react@18.3.1)(tailwindcss@3.4.3) '@trpc/client': - specifier: ^10.43.6 - version: 10.45.2(@trpc/server@10.45.2) + specifier: 11.0.0-rc.498 + version: 11.0.0-rc.498(@trpc/server@11.0.0-rc.498) '@trpc/next': - specifier: ^10.45.2 - version: 10.45.2(@tanstack/react-query@4.36.1(react-dom@18.2.0(react@18.3.1))(react@18.3.1))(@trpc/client@10.45.2(@trpc/server@10.45.2))(@trpc/react-query@10.45.2(@tanstack/react-query@4.36.1(react-dom@18.2.0(react@18.3.1))(react@18.3.1))(@trpc/client@10.45.2(@trpc/server@10.45.2))(@trpc/server@10.45.2)(react-dom@18.2.0(react@18.3.1))(react@18.3.1))(@trpc/server@10.45.2)(next@14.2.4(@babel/core@7.24.5)(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.3.1))(react@18.3.1))(react-dom@18.2.0(react@18.3.1))(react@18.3.1) + specifier: 11.0.0-rc.498 + version: 11.0.0-rc.498(@tanstack/react-query@5.53.3(react@18.3.1))(@trpc/client@11.0.0-rc.498(@trpc/server@11.0.0-rc.498))(@trpc/react-query@11.0.0-rc.498(@tanstack/react-query@5.53.3(react@18.3.1))(@trpc/client@11.0.0-rc.498(@trpc/server@11.0.0-rc.498))(@trpc/server@11.0.0-rc.498)(react-dom@18.2.0(react@18.3.1))(react@18.3.1))(@trpc/server@11.0.0-rc.498)(next@14.2.4(@babel/core@7.24.5)(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.3.1))(react@18.3.1))(react-dom@18.2.0(react@18.3.1))(react@18.3.1) '@trpc/react-query': - specifier: ^10.43.6 - version: 10.45.2(@tanstack/react-query@4.36.1(react-dom@18.2.0(react@18.3.1))(react@18.3.1))(@trpc/client@10.45.2(@trpc/server@10.45.2))(@trpc/server@10.45.2)(react-dom@18.2.0(react@18.3.1))(react@18.3.1) + specifier: 11.0.0-rc.498 + version: 11.0.0-rc.498(@tanstack/react-query@5.53.3(react@18.3.1))(@trpc/client@11.0.0-rc.498(@trpc/server@11.0.0-rc.498))(@trpc/server@11.0.0-rc.498)(react-dom@18.2.0(react@18.3.1))(react@18.3.1) '@trpc/server': - specifier: ^10.43.6 - version: 10.45.2 + specifier: 11.0.0-rc.498 + version: 11.0.0-rc.498 '@types/bcryptjs': specifier: ^2.4.6 version: 2.4.6 @@ -200,6 +200,9 @@ importers: nodemailer: specifier: ^6.9.14 version: 6.9.14 + nuqs: + specifier: ^1.17.8 + version: 1.17.8(next@14.2.4(@babel/core@7.24.5)(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.3.1))(react@18.3.1)) papaparse: specifier: ^5.4.1 version: 5.4.1 @@ -220,7 +223,7 @@ importers: version: 0.7.4(@prisma/client@5.14.0(prisma@5.14.0)) prisma-json-types-generator: specifier: ^3.0.4 - version: 3.0.4(prisma@5.14.0)(typescript@5.4.5) + version: 3.0.4(prisma@5.14.0)(typescript@5.5.4) pushmodal: specifier: ^1.0.4 version: 1.0.4(@radix-ui/react-dialog@1.0.5(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.2.0(react@18.3.1))(react@18.3.1))(react-dom@18.2.0(react@18.3.1))(react@18.3.1) @@ -236,6 +239,9 @@ importers: react-email: specifier: 2.1.6 version: 2.1.6(@opentelemetry/api@1.9.0)(@swc/helpers@0.5.11)(eslint@9.8.0) + react-error-boundary: + specifier: ^4.0.13 + version: 4.0.13(react@18.3.1) react-hook-form: specifier: ^7.52.1 version: 7.52.1(react@18.3.1) @@ -335,7 +341,7 @@ importers: version: 9.2.22 knip: specifier: ^5.17.2 - version: 5.17.4(@types/node@20.12.12)(typescript@5.4.5) + version: 5.17.4(@types/node@20.12.12)(typescript@5.5.4) lint-staged: specifier: ^15.2.2 version: 15.2.2 @@ -352,8 +358,8 @@ importers: specifier: ^4.7.0 version: 4.10.4 typescript: - specifier: ^5.4.5 - version: 5.4.5 + specifier: ^5.5.3 + version: 5.5.4 vitest: specifier: ^1.6.0 version: 1.6.0(@types/node@20.12.12)(terser@5.31.1) @@ -3598,20 +3604,13 @@ packages: peerDependencies: tailwindcss: '>=3.0.0 || insiders' - '@tanstack/query-core@4.36.1': - resolution: {integrity: sha512-DJSilV5+ytBP1FbFcEJovv4rnnm/CokuVvrBEtW/Va9DvuJ3HksbXUJEpI0aV1KtuL4ZoO9AVE6PyNLzF7tLeA==} + '@tanstack/query-core@5.53.3': + resolution: {integrity: sha512-ZfjAgd7NpqDx0e4aYBt7EmS2enbulPrJwowTy+mayRE93WUUH+sIYHun1TdRjpGwDPMNNZ5D6goh7n3CwoO+HA==} - '@tanstack/react-query@4.36.1': - resolution: {integrity: sha512-y7ySVHFyyQblPl3J3eQBWpXZkliroki3ARnBKsdJchlgt7yJLRDUcf4B8soufgiYt3pEQIkBWBx1N9/ZPIeUWw==} + '@tanstack/react-query@5.53.3': + resolution: {integrity: sha512-286mN/91CeM7vC6CZFLKYDHSw+WyMX6ekIvzoTbpM4xyPb99VSyCKPLyPgaOatKqYm6ooMBquSq9NGRdKgsJfg==} peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 - react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 - react-native: '*' - peerDependenciesMeta: - react-dom: - optional: true - react-native: - optional: true + react: ^18 || ^19 '@tanstack/react-table@8.20.1': resolution: {integrity: sha512-PJK+07qbengObe5l7c8vCdtefXm8cyR4i078acWrHbdm8JKw1ES7YpmOtVt9ALUVEEFAHscdVpGRhRgikgFMbQ==} @@ -3804,33 +3803,38 @@ packages: react: ^18.0.0 react-dom: '>=16.6.0' - '@trpc/client@10.45.2': - resolution: {integrity: sha512-ykALM5kYWTLn1zYuUOZ2cPWlVfrXhc18HzBDyRhoPYN0jey4iQHEFSEowfnhg1RvYnrAVjNBgHNeSAXjrDbGwg==} + '@trpc/client@11.0.0-rc.498': + resolution: {integrity: sha512-a1VjvLHCo8gPENMfzI8lVF1ys6kOGI3f/cIAZUMB0d2TlkDERlFYANnSnzFvmiKy6ICh3lsQQ3OIMmIfyd8OtQ==} peerDependencies: - '@trpc/server': 10.45.2 + '@trpc/server': 11.0.0-rc.498+5714423cc - '@trpc/next@10.45.2': - resolution: {integrity: sha512-RSORmfC+/nXdmRY1pQ0AalsVgSzwNAFbZLYHiTvPM5QQ8wmMEHilseCYMXpu0se/TbPt9zVR6Ka2d7O6zxKkXg==} + '@trpc/next@11.0.0-rc.498': + resolution: {integrity: sha512-GoaRReGOd949aZbzJI8xv+QFcLUpp7+BOw5NdgLjEk47Nin2TcLl19Tk8JhXImFzwDLF8JQFrHvk91mnb6ka0Q==} peerDependencies: - '@tanstack/react-query': ^4.18.0 - '@trpc/client': 10.45.2 - '@trpc/react-query': 10.45.2 - '@trpc/server': 10.45.2 + '@tanstack/react-query': ^5.49.2 + '@trpc/client': 11.0.0-rc.498+5714423cc + '@trpc/react-query': 11.0.0-rc.498+5714423cc + '@trpc/server': 11.0.0-rc.498+5714423cc next: '*' react: '>=16.8.0' react-dom: '>=16.8.0' + peerDependenciesMeta: + '@tanstack/react-query': + optional: true + '@trpc/react-query': + optional: true - '@trpc/react-query@10.45.2': - resolution: {integrity: sha512-BAqb9bGZIscroradlNx+Cc9522R+idY3BOSf5z0jHUtkxdMbjeGKxSSMxxu7JzoLqSIEC+LVzL3VvF8sdDWaZQ==} + '@trpc/react-query@11.0.0-rc.498': + resolution: {integrity: sha512-il/fgO9DSTHmrJbDInGNl8PymLo19CEfIjBXmerTm4hg/p438fQb4ZKDVbpBxEThgAaaP6D0HTELWn5VDWG3VQ==} peerDependencies: - '@tanstack/react-query': ^4.18.0 - '@trpc/client': 10.45.2 - '@trpc/server': 10.45.2 - react: '>=16.8.0' - react-dom: '>=16.8.0' + '@tanstack/react-query': ^5.49.2 + '@trpc/client': 11.0.0-rc.498+5714423cc + '@trpc/server': 11.0.0-rc.498+5714423cc + react: '>=18.2.0' + react-dom: '>=18.2.0' - '@trpc/server@10.45.2': - resolution: {integrity: sha512-wOrSThNNE4HUnuhJG6PfDRp4L2009KDVxsd+2VYH8ro6o/7/jwYZ8Uu5j+VaW+mOmc8EHerHzGcdbGNQSAUPgg==} + '@trpc/server@11.0.0-rc.498': + resolution: {integrity: sha512-LECCOz8JNB67CBasPA6fkLbhJIbUQRP1wq02xUfUMeaSEZFWJL+7fcTgLaWFQSHMMnMsvkLuDuDDkUSCF2SXmg==} '@types/aria-query@5.0.4': resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==} @@ -6623,6 +6627,11 @@ packages: nprogress@0.2.0: resolution: {integrity: sha512-I19aIingLgR1fmhftnbWWO3dXc0hSxqHQHQb3H8m+K3TnEn/iSeTZZOyvKXWqQESMwuUVnatlCnZdLBZZt2VSA==} + nuqs@1.17.8: + resolution: {integrity: sha512-JqsnzO+hJyjJE7ebuhpHMLA2iGY48e2xr0oJQFhj7kjUmDABL2XOup47rxF5TL/5b9jEsmU2t0lAKin1VdK1/A==} + peerDependencies: + next: '>=13.4 <14.0.2 || ^14.0.3' + oauth@0.9.15: resolution: {integrity: sha512-a5ERWK1kh38ExDEfoO6qUHJb32rd7aYmPHuyCu3Fta/cnICvYmgd2uhuKXvPD+PXB+gCEYYEaQdIRAjCOwAKNA==} @@ -7165,6 +7174,11 @@ packages: engines: {node: '>=18.0.0'} hasBin: true + react-error-boundary@4.0.13: + resolution: {integrity: sha512-b6PwbdSv8XeOSYvjt8LpgpKrZ0yGdtZokYwkwV2wlcZbxgopHX/hgPl5VgpnoVOWd868n1hktM8Qm4b+02MiLQ==} + peerDependencies: + react: '>=16.13.1' + react-hook-form@7.52.1: resolution: {integrity: sha512-uNKIhaoICJ5KQALYZ4TOaOLElyM+xipord+Ha3crEFhTntdLvWZqVY49Wqd/0GiVCA/f9NjemLeiNPjG7Hpurg==} engines: {node: '>=12.22.0'} @@ -7922,8 +7936,8 @@ packages: engines: {node: '>=14.17'} hasBin: true - typescript@5.4.5: - resolution: {integrity: sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==} + typescript@5.5.4: + resolution: {integrity: sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==} engines: {node: '>=14.17'} hasBin: true @@ -8061,11 +8075,6 @@ packages: '@types/react': optional: true - use-sync-external-store@1.2.2: - resolution: {integrity: sha512-PElTlVMwpblvbNqQ82d2n6RjStvdSoNe9FG28kNfz3WiXilJm4DdNkEzRhCZuIDwY8U08WVihhGR5iRqAwfDiw==} - peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 - usehooks-ts@3.1.0: resolution: {integrity: sha512-bBIa7yUyPhE1BCc0GmR96VU/15l/9gP1Ch5mYdLcFBaFGQsdmXkvjV0TtOqW1yUd6VjIwDunm+flSciCQXujiw==} engines: {node: '>=16.15.0'} @@ -8575,7 +8584,7 @@ snapshots: '@aws-sdk/client-sso-oidc': 3.577.0(@aws-sdk/client-sts@3.577.0) '@aws-sdk/client-sts': 3.577.0 '@aws-sdk/core': 3.576.0 - '@aws-sdk/credential-provider-node': 3.577.0(@aws-sdk/client-sso-oidc@3.577.0(@aws-sdk/client-sts@3.577.0))(@aws-sdk/client-sts@3.577.0) + '@aws-sdk/credential-provider-node': 3.577.0(@aws-sdk/client-sso-oidc@3.577.0)(@aws-sdk/client-sts@3.577.0) '@aws-sdk/middleware-bucket-endpoint': 3.577.0 '@aws-sdk/middleware-expect-continue': 3.577.0 '@aws-sdk/middleware-flexible-checksums': 3.577.0 @@ -8636,7 +8645,7 @@ snapshots: '@aws-crypto/sha256-js': 3.0.0 '@aws-sdk/client-sts': 3.577.0 '@aws-sdk/core': 3.576.0 - '@aws-sdk/credential-provider-node': 3.577.0(@aws-sdk/client-sso-oidc@3.577.0(@aws-sdk/client-sts@3.577.0))(@aws-sdk/client-sts@3.577.0) + '@aws-sdk/credential-provider-node': 3.577.0(@aws-sdk/client-sso-oidc@3.577.0)(@aws-sdk/client-sts@3.577.0) '@aws-sdk/middleware-host-header': 3.577.0 '@aws-sdk/middleware-logger': 3.577.0 '@aws-sdk/middleware-recursion-detection': 3.577.0 @@ -8725,7 +8734,7 @@ snapshots: '@aws-crypto/sha256-js': 3.0.0 '@aws-sdk/client-sso-oidc': 3.577.0(@aws-sdk/client-sts@3.577.0) '@aws-sdk/core': 3.576.0 - '@aws-sdk/credential-provider-node': 3.577.0(@aws-sdk/client-sso-oidc@3.577.0(@aws-sdk/client-sts@3.577.0))(@aws-sdk/client-sts@3.577.0) + '@aws-sdk/credential-provider-node': 3.577.0(@aws-sdk/client-sso-oidc@3.577.0)(@aws-sdk/client-sts@3.577.0) '@aws-sdk/middleware-host-header': 3.577.0 '@aws-sdk/middleware-logger': 3.577.0 '@aws-sdk/middleware-recursion-detection': 3.577.0 @@ -8793,12 +8802,12 @@ snapshots: '@smithy/util-stream': 3.0.1 tslib: 2.6.2 - '@aws-sdk/credential-provider-ini@3.577.0(@aws-sdk/client-sso-oidc@3.577.0(@aws-sdk/client-sts@3.577.0))(@aws-sdk/client-sts@3.577.0)': + '@aws-sdk/credential-provider-ini@3.577.0(@aws-sdk/client-sso-oidc@3.577.0)(@aws-sdk/client-sts@3.577.0)': dependencies: '@aws-sdk/client-sts': 3.577.0 '@aws-sdk/credential-provider-env': 3.577.0 '@aws-sdk/credential-provider-process': 3.577.0 - '@aws-sdk/credential-provider-sso': 3.577.0(@aws-sdk/client-sso-oidc@3.577.0(@aws-sdk/client-sts@3.577.0)) + '@aws-sdk/credential-provider-sso': 3.577.0(@aws-sdk/client-sso-oidc@3.577.0) '@aws-sdk/credential-provider-web-identity': 3.577.0(@aws-sdk/client-sts@3.577.0) '@aws-sdk/types': 3.577.0 '@smithy/credential-provider-imds': 3.0.0 @@ -8810,13 +8819,13 @@ snapshots: - '@aws-sdk/client-sso-oidc' - aws-crt - '@aws-sdk/credential-provider-node@3.577.0(@aws-sdk/client-sso-oidc@3.577.0(@aws-sdk/client-sts@3.577.0))(@aws-sdk/client-sts@3.577.0)': + '@aws-sdk/credential-provider-node@3.577.0(@aws-sdk/client-sso-oidc@3.577.0)(@aws-sdk/client-sts@3.577.0)': dependencies: '@aws-sdk/credential-provider-env': 3.577.0 '@aws-sdk/credential-provider-http': 3.577.0 - '@aws-sdk/credential-provider-ini': 3.577.0(@aws-sdk/client-sso-oidc@3.577.0(@aws-sdk/client-sts@3.577.0))(@aws-sdk/client-sts@3.577.0) + '@aws-sdk/credential-provider-ini': 3.577.0(@aws-sdk/client-sso-oidc@3.577.0)(@aws-sdk/client-sts@3.577.0) '@aws-sdk/credential-provider-process': 3.577.0 - '@aws-sdk/credential-provider-sso': 3.577.0(@aws-sdk/client-sso-oidc@3.577.0(@aws-sdk/client-sts@3.577.0)) + '@aws-sdk/credential-provider-sso': 3.577.0(@aws-sdk/client-sso-oidc@3.577.0) '@aws-sdk/credential-provider-web-identity': 3.577.0(@aws-sdk/client-sts@3.577.0) '@aws-sdk/types': 3.577.0 '@smithy/credential-provider-imds': 3.0.0 @@ -8837,10 +8846,10 @@ snapshots: '@smithy/types': 3.0.0 tslib: 2.6.2 - '@aws-sdk/credential-provider-sso@3.577.0(@aws-sdk/client-sso-oidc@3.577.0(@aws-sdk/client-sts@3.577.0))': + '@aws-sdk/credential-provider-sso@3.577.0(@aws-sdk/client-sso-oidc@3.577.0)': dependencies: '@aws-sdk/client-sso': 3.577.0 - '@aws-sdk/token-providers': 3.577.0(@aws-sdk/client-sso-oidc@3.577.0(@aws-sdk/client-sts@3.577.0)) + '@aws-sdk/token-providers': 3.577.0(@aws-sdk/client-sso-oidc@3.577.0) '@aws-sdk/types': 3.577.0 '@smithy/property-provider': 3.0.0 '@smithy/shared-ini-file-loader': 3.0.0 @@ -8977,7 +8986,7 @@ snapshots: '@smithy/types': 3.0.0 tslib: 2.6.2 - '@aws-sdk/token-providers@3.577.0(@aws-sdk/client-sso-oidc@3.577.0(@aws-sdk/client-sts@3.577.0))': + '@aws-sdk/token-providers@3.577.0(@aws-sdk/client-sso-oidc@3.577.0)': dependencies: '@aws-sdk/client-sso-oidc': 3.577.0(@aws-sdk/client-sts@3.577.0) '@aws-sdk/types': 3.577.0 @@ -9728,11 +9737,11 @@ snapshots: '@floating-ui/utils@0.2.3': {} - '@floating-ui/vue@1.0.7(vue@3.4.30(typescript@5.4.5))': + '@floating-ui/vue@1.0.7(vue@3.4.30(typescript@5.5.4))': dependencies: '@floating-ui/dom': 1.6.5 '@floating-ui/utils': 0.2.3 - vue-demi: 0.14.8(vue@3.4.30(typescript@5.4.5)) + vue-demi: 0.14.8(vue@3.4.30(typescript@5.5.4)) transitivePeerDependencies: - '@vue/composition-api' - vue @@ -9756,10 +9765,10 @@ snapshots: dependencies: tailwindcss: 3.4.3 - '@headlessui/vue@1.7.22(vue@3.4.30(typescript@5.4.5))': + '@headlessui/vue@1.7.22(vue@3.4.30(typescript@5.5.4))': dependencies: - '@tanstack/vue-virtual': 3.7.0(vue@3.4.30(typescript@5.4.5)) - vue: 3.4.30(typescript@5.4.5) + '@tanstack/vue-virtual': 3.7.0(vue@3.4.30(typescript@5.5.4)) + vue: 3.4.30(typescript@5.5.4) '@hexagon/base64@1.1.28': {} @@ -11784,14 +11793,14 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.18.0': optional: true - '@scalar/api-client-modal@0.0.16(tailwindcss@3.4.3)(typescript@5.4.5)(vitest@1.6.0(@types/node@20.12.12)(terser@5.31.1))': + '@scalar/api-client-modal@0.0.16(tailwindcss@3.4.3)(typescript@5.5.4)(vitest@1.6.0(@types/node@20.12.12)(terser@5.31.1))': dependencies: - '@scalar/client-app': 0.1.14(tailwindcss@3.4.3)(typescript@5.4.5)(vitest@1.6.0(@types/node@20.12.12)(terser@5.31.1)) - '@scalar/components': 0.12.4(typescript@5.4.5)(vitest@1.6.0(@types/node@20.12.12)(terser@5.31.1)) + '@scalar/client-app': 0.1.14(tailwindcss@3.4.3)(typescript@5.5.4)(vitest@1.6.0(@types/node@20.12.12)(terser@5.31.1)) + '@scalar/components': 0.12.4(typescript@5.5.4)(vitest@1.6.0(@types/node@20.12.12)(terser@5.31.1)) '@scalar/oas-utils': 0.2.5 '@scalar/object-utils': 1.1.2 - vue: 3.4.30(typescript@5.4.5) - vue-router: 4.4.0(vue@3.4.30(typescript@5.4.5)) + vue: 3.4.30(typescript@5.5.4) + vue-router: 4.4.0(vue@3.4.30(typescript@5.5.4)) transitivePeerDependencies: - '@jest/globals' - '@types/bun' @@ -11804,24 +11813,24 @@ snapshots: - typescript - vitest - '@scalar/api-client@1.3.19(typescript@5.4.5)(vitest@1.6.0(@types/node@20.12.12)(terser@5.31.1))': + '@scalar/api-client@1.3.19(typescript@5.5.4)(vitest@1.6.0(@types/node@20.12.12)(terser@5.31.1))': dependencies: - '@floating-ui/vue': 1.0.7(vue@3.4.30(typescript@5.4.5)) - '@headlessui/vue': 1.7.22(vue@3.4.30(typescript@5.4.5)) - '@scalar/components': 0.12.4(typescript@5.4.5)(vitest@1.6.0(@types/node@20.12.12)(terser@5.31.1)) + '@floating-ui/vue': 1.0.7(vue@3.4.30(typescript@5.5.4)) + '@headlessui/vue': 1.7.22(vue@3.4.30(typescript@5.5.4)) + '@scalar/components': 0.12.4(typescript@5.5.4)(vitest@1.6.0(@types/node@20.12.12)(terser@5.31.1)) '@scalar/oas-utils': 0.2.5 '@scalar/openapi-parser': 0.7.1 - '@scalar/themes': 0.9.8(typescript@5.4.5) - '@scalar/use-codemirror': 0.11.4(typescript@5.4.5) - '@scalar/use-toasts': 0.7.4(typescript@5.4.5) - '@scalar/use-tooltip': 1.0.1(typescript@5.4.5) - '@vueuse/core': 10.11.0(vue@3.4.30(typescript@5.4.5)) + '@scalar/themes': 0.9.8(typescript@5.5.4) + '@scalar/use-codemirror': 0.11.4(typescript@5.5.4) + '@scalar/use-toasts': 0.7.4(typescript@5.5.4) + '@scalar/use-tooltip': 1.0.1(typescript@5.5.4) + '@vueuse/core': 10.11.0(vue@3.4.30(typescript@5.5.4)) axios: 1.7.2 httpsnippet-lite: 3.0.5 nanoid: 5.0.7 pretty-bytes: 6.1.1 pretty-ms: 8.0.0 - vue: 3.4.30(typescript@5.4.5) + vue: 3.4.30(typescript@5.5.4) transitivePeerDependencies: - '@jest/globals' - '@types/bun' @@ -11833,21 +11842,21 @@ snapshots: - typescript - vitest - '@scalar/api-reference@1.24.20(postcss@8.4.40)(tailwindcss@3.4.3)(typescript@5.4.5)(vitest@1.6.0(@types/node@20.12.12)(terser@5.31.1))': + '@scalar/api-reference@1.24.20(postcss@8.4.40)(tailwindcss@3.4.3)(typescript@5.5.4)(vitest@1.6.0(@types/node@20.12.12)(terser@5.31.1))': dependencies: - '@headlessui/vue': 1.7.22(vue@3.4.30(typescript@5.4.5)) - '@scalar/api-client': 1.3.19(typescript@5.4.5)(vitest@1.6.0(@types/node@20.12.12)(terser@5.31.1)) - '@scalar/api-client-modal': 0.0.16(tailwindcss@3.4.3)(typescript@5.4.5)(vitest@1.6.0(@types/node@20.12.12)(terser@5.31.1)) - '@scalar/components': 0.12.4(typescript@5.4.5)(vitest@1.6.0(@types/node@20.12.12)(terser@5.31.1)) + '@headlessui/vue': 1.7.22(vue@3.4.30(typescript@5.5.4)) + '@scalar/api-client': 1.3.19(typescript@5.5.4)(vitest@1.6.0(@types/node@20.12.12)(terser@5.31.1)) + '@scalar/api-client-modal': 0.0.16(tailwindcss@3.4.3)(typescript@5.5.4)(vitest@1.6.0(@types/node@20.12.12)(terser@5.31.1)) + '@scalar/components': 0.12.4(typescript@5.5.4)(vitest@1.6.0(@types/node@20.12.12)(terser@5.31.1)) '@scalar/oas-utils': 0.2.5 '@scalar/openapi-parser': 0.7.1 '@scalar/snippetz': 0.1.6 - '@scalar/themes': 0.9.8(typescript@5.4.5) - '@scalar/use-toasts': 0.7.4(typescript@5.4.5) - '@scalar/use-tooltip': 1.0.1(typescript@5.4.5) + '@scalar/themes': 0.9.8(typescript@5.5.4) + '@scalar/use-toasts': 0.7.4(typescript@5.5.4) + '@scalar/use-tooltip': 1.0.1(typescript@5.5.4) '@unhead/schema': 1.9.14 - '@unhead/vue': 1.9.14(vue@3.4.30(typescript@5.4.5)) - '@vueuse/core': 10.11.0(vue@3.4.30(typescript@5.4.5)) + '@unhead/vue': 1.9.14(vue@3.4.30(typescript@5.5.4)) + '@vueuse/core': 10.11.0(vue@3.4.30(typescript@5.5.4)) axios: 1.7.2 fuse.js: 7.0.0 github-slugger: 2.0.0 @@ -11855,7 +11864,7 @@ snapshots: postcss-nested: 6.0.1(postcss@8.4.40) unhead: 1.9.14 unified: 11.0.4 - vue: 3.4.30(typescript@5.4.5) + vue: 3.4.30(typescript@5.5.4) transitivePeerDependencies: - '@jest/globals' - '@types/bun' @@ -11869,26 +11878,26 @@ snapshots: - typescript - vitest - '@scalar/client-app@0.1.14(tailwindcss@3.4.3)(typescript@5.4.5)(vitest@1.6.0(@types/node@20.12.12)(terser@5.31.1))': + '@scalar/client-app@0.1.14(tailwindcss@3.4.3)(typescript@5.5.4)(vitest@1.6.0(@types/node@20.12.12)(terser@5.31.1))': dependencies: '@headlessui/tailwindcss': 0.2.1(tailwindcss@3.4.3) - '@headlessui/vue': 1.7.22(vue@3.4.30(typescript@5.4.5)) - '@scalar/components': 0.12.4(typescript@5.4.5)(vitest@1.6.0(@types/node@20.12.12)(terser@5.31.1)) - '@scalar/draggable': 0.1.3(typescript@5.4.5) + '@headlessui/vue': 1.7.22(vue@3.4.30(typescript@5.5.4)) + '@scalar/components': 0.12.4(typescript@5.5.4)(vitest@1.6.0(@types/node@20.12.12)(terser@5.31.1)) + '@scalar/draggable': 0.1.3(typescript@5.5.4) '@scalar/oas-utils': 0.2.5 '@scalar/object-utils': 1.1.2 '@scalar/openapi-parser': 0.7.1 - '@scalar/use-toasts': 0.7.4(typescript@5.4.5) - '@scalar/use-tooltip': 1.0.1(typescript@5.4.5) - '@vueuse/core': 10.11.0(vue@3.4.30(typescript@5.4.5)) + '@scalar/use-toasts': 0.7.4(typescript@5.5.4) + '@scalar/use-tooltip': 1.0.1(typescript@5.5.4) + '@vueuse/core': 10.11.0(vue@3.4.30(typescript@5.5.4)) axios: 1.7.2 - cva: 1.0.0-beta.1(typescript@5.4.5) + cva: 1.0.0-beta.1(typescript@5.5.4) js-cookie: 3.0.5 nanoid: 5.0.7 pretty-bytes: 6.1.1 pretty-ms: 8.0.0 - vue: 3.4.30(typescript@5.4.5) - vue-router: 4.4.0(vue@3.4.30(typescript@5.4.5)) + vue: 3.4.30(typescript@5.5.4) + vue-router: 4.4.0(vue@3.4.30(typescript@5.5.4)) zod: 3.23.8 transitivePeerDependencies: - '@jest/globals' @@ -11924,20 +11933,20 @@ snapshots: transitivePeerDependencies: - supports-color - '@scalar/components@0.12.4(typescript@5.4.5)(vitest@1.6.0(@types/node@20.12.12)(terser@5.31.1))': + '@scalar/components@0.12.4(typescript@5.5.4)(vitest@1.6.0(@types/node@20.12.12)(terser@5.31.1))': dependencies: '@floating-ui/utils': 0.2.2 - '@floating-ui/vue': 1.0.7(vue@3.4.30(typescript@5.4.5)) - '@headlessui/vue': 1.7.22(vue@3.4.30(typescript@5.4.5)) + '@floating-ui/vue': 1.0.7(vue@3.4.30(typescript@5.5.4)) + '@headlessui/vue': 1.7.22(vue@3.4.30(typescript@5.5.4)) '@scalar/code-highlight': 0.0.5 '@scalar/oas-utils': 0.2.5 '@storybook/test': 8.1.11(vitest@1.6.0(@types/node@20.12.12)(terser@5.31.1)) - '@vueuse/core': 10.11.0(vue@3.4.30(typescript@5.4.5)) - cva: 1.0.0-beta.1(typescript@5.4.5) + '@vueuse/core': 10.11.0(vue@3.4.30(typescript@5.5.4)) + cva: 1.0.0-beta.1(typescript@5.5.4) nanoid: 5.0.7 - radix-vue: 1.8.5(vue@3.4.30(typescript@5.4.5)) + radix-vue: 1.8.5(vue@3.4.30(typescript@5.5.4)) tailwind-merge: 2.4.0 - vue: 3.4.30(typescript@5.4.5) + vue: 3.4.30(typescript@5.5.4) transitivePeerDependencies: - '@jest/globals' - '@types/bun' @@ -11949,15 +11958,15 @@ snapshots: - typescript - vitest - '@scalar/draggable@0.1.3(typescript@5.4.5)': + '@scalar/draggable@0.1.3(typescript@5.5.4)': dependencies: - vue: 3.4.30(typescript@5.4.5) + vue: 3.4.30(typescript@5.5.4) transitivePeerDependencies: - typescript - '@scalar/nextjs-api-reference@0.4.18(@babel/core@7.24.5)(@opentelemetry/api@1.9.0)(postcss@8.4.40)(react-dom@18.2.0(react@18.3.1))(tailwindcss@3.4.3)(typescript@5.4.5)(vitest@1.6.0(@types/node@20.12.12)(terser@5.31.1))': + '@scalar/nextjs-api-reference@0.4.18(@babel/core@7.24.5)(@opentelemetry/api@1.9.0)(postcss@8.4.40)(react-dom@18.2.0(react@18.3.1))(tailwindcss@3.4.3)(typescript@5.5.4)(vitest@1.6.0(@types/node@20.12.12)(terser@5.31.1))': dependencies: - '@scalar/api-reference': 1.24.20(postcss@8.4.40)(tailwindcss@3.4.3)(typescript@5.4.5)(vitest@1.6.0(@types/node@20.12.12)(terser@5.31.1)) + '@scalar/api-reference': 1.24.20(postcss@8.4.40)(tailwindcss@3.4.3)(typescript@5.5.4)(vitest@1.6.0(@types/node@20.12.12)(terser@5.31.1)) next: 14.2.4(@babel/core@7.24.5)(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.3.1))(react@18.3.1) react: 18.3.1 transitivePeerDependencies: @@ -12034,13 +12043,13 @@ snapshots: '@scalar/snippetz-plugin-node-ofetch': 0.1.1 '@scalar/snippetz-plugin-node-undici': 0.1.6 - '@scalar/themes@0.9.8(typescript@5.4.5)': + '@scalar/themes@0.9.8(typescript@5.5.4)': dependencies: - vue: 3.4.30(typescript@5.4.5) + vue: 3.4.30(typescript@5.5.4) transitivePeerDependencies: - typescript - '@scalar/use-codemirror@0.11.4(typescript@5.4.5)': + '@scalar/use-codemirror@0.11.4(typescript@5.5.4)': dependencies: '@codemirror/autocomplete': 6.16.3(@codemirror/language@6.10.2)(@codemirror/state@6.4.1)(@codemirror/view@6.28.2)(@lezer/common@1.2.1) '@codemirror/commands': 6.6.0 @@ -12057,25 +12066,25 @@ snapshots: '@replit/codemirror-css-color-picker': 6.1.1(@codemirror/language@6.10.2)(@codemirror/state@6.4.1)(@codemirror/view@6.28.2) '@uiw/codemirror-themes': 4.22.2(@codemirror/language@6.10.2)(@codemirror/state@6.4.1)(@codemirror/view@6.28.2) codemirror: 6.0.1(@lezer/common@1.2.1) - vue: 3.4.30(typescript@5.4.5) + vue: 3.4.30(typescript@5.5.4) optionalDependencies: y-codemirror.next: 0.3.5(@codemirror/state@6.4.1)(@codemirror/view@6.28.2)(yjs@13.6.15) yjs: 13.6.15 transitivePeerDependencies: - typescript - '@scalar/use-toasts@0.7.4(typescript@5.4.5)': + '@scalar/use-toasts@0.7.4(typescript@5.5.4)': dependencies: nanoid: 5.0.7 - vue: 3.4.30(typescript@5.4.5) + vue: 3.4.30(typescript@5.5.4) vue-sonner: 1.1.3 transitivePeerDependencies: - typescript - '@scalar/use-tooltip@1.0.1(typescript@5.4.5)': + '@scalar/use-tooltip@1.0.1(typescript@5.5.4)': dependencies: - radix-vue: 1.8.5(vue@3.4.30(typescript@5.4.5)) - vue: 3.4.30(typescript@5.4.5) + radix-vue: 1.8.5(vue@3.4.30(typescript@5.5.4)) + vue: 3.4.30(typescript@5.5.4) transitivePeerDependencies: - '@vue/composition-api' - typescript @@ -12792,18 +12801,18 @@ snapshots: dependencies: '@swc/counter': 0.1.3 - '@t3-oss/env-core@0.10.1(typescript@5.4.5)(zod@3.23.8)': + '@t3-oss/env-core@0.10.1(typescript@5.5.4)(zod@3.23.8)': dependencies: zod: 3.23.8 optionalDependencies: - typescript: 5.4.5 + typescript: 5.5.4 - '@t3-oss/env-nextjs@0.10.1(typescript@5.4.5)(zod@3.23.8)': + '@t3-oss/env-nextjs@0.10.1(typescript@5.5.4)(zod@3.23.8)': dependencies: - '@t3-oss/env-core': 0.10.1(typescript@5.4.5)(zod@3.23.8) + '@t3-oss/env-core': 0.10.1(typescript@5.5.4)(zod@3.23.8) zod: 3.23.8 optionalDependencies: - typescript: 5.4.5 + typescript: 5.5.4 '@tailwindcss/typography@0.5.13(tailwindcss@3.4.3)': dependencies: @@ -12813,15 +12822,12 @@ snapshots: postcss-selector-parser: 6.0.10 tailwindcss: 3.4.3 - '@tanstack/query-core@4.36.1': {} + '@tanstack/query-core@5.53.3': {} - '@tanstack/react-query@4.36.1(react-dom@18.2.0(react@18.3.1))(react@18.3.1)': + '@tanstack/react-query@5.53.3(react@18.3.1)': dependencies: - '@tanstack/query-core': 4.36.1 + '@tanstack/query-core': 5.53.3 react: 18.3.1 - use-sync-external-store: 1.2.2(react@18.3.1) - optionalDependencies: - react-dom: 18.2.0(react@18.3.1) '@tanstack/react-table@8.20.1(react-dom@18.2.0(react@18.3.1))(react@18.3.1)': dependencies: @@ -12841,10 +12847,10 @@ snapshots: '@tanstack/virtual-core@3.7.0': {} - '@tanstack/vue-virtual@3.7.0(vue@3.4.30(typescript@5.4.5))': + '@tanstack/vue-virtual@3.7.0(vue@3.4.30(typescript@5.5.4))': dependencies: '@tanstack/virtual-core': 3.7.0 - vue: 3.4.30(typescript@5.4.5) + vue: 3.4.30(typescript@5.5.4) '@testing-library/dom@10.1.0': dependencies: @@ -13016,29 +13022,30 @@ snapshots: transitivePeerDependencies: - tailwindcss - '@trpc/client@10.45.2(@trpc/server@10.45.2)': + '@trpc/client@11.0.0-rc.498(@trpc/server@11.0.0-rc.498)': dependencies: - '@trpc/server': 10.45.2 + '@trpc/server': 11.0.0-rc.498 - '@trpc/next@10.45.2(@tanstack/react-query@4.36.1(react-dom@18.2.0(react@18.3.1))(react@18.3.1))(@trpc/client@10.45.2(@trpc/server@10.45.2))(@trpc/react-query@10.45.2(@tanstack/react-query@4.36.1(react-dom@18.2.0(react@18.3.1))(react@18.3.1))(@trpc/client@10.45.2(@trpc/server@10.45.2))(@trpc/server@10.45.2)(react-dom@18.2.0(react@18.3.1))(react@18.3.1))(@trpc/server@10.45.2)(next@14.2.4(@babel/core@7.24.5)(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.3.1))(react@18.3.1))(react-dom@18.2.0(react@18.3.1))(react@18.3.1)': + '@trpc/next@11.0.0-rc.498(@tanstack/react-query@5.53.3(react@18.3.1))(@trpc/client@11.0.0-rc.498(@trpc/server@11.0.0-rc.498))(@trpc/react-query@11.0.0-rc.498(@tanstack/react-query@5.53.3(react@18.3.1))(@trpc/client@11.0.0-rc.498(@trpc/server@11.0.0-rc.498))(@trpc/server@11.0.0-rc.498)(react-dom@18.2.0(react@18.3.1))(react@18.3.1))(@trpc/server@11.0.0-rc.498)(next@14.2.4(@babel/core@7.24.5)(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.3.1))(react@18.3.1))(react-dom@18.2.0(react@18.3.1))(react@18.3.1)': dependencies: - '@tanstack/react-query': 4.36.1(react-dom@18.2.0(react@18.3.1))(react@18.3.1) - '@trpc/client': 10.45.2(@trpc/server@10.45.2) - '@trpc/react-query': 10.45.2(@tanstack/react-query@4.36.1(react-dom@18.2.0(react@18.3.1))(react@18.3.1))(@trpc/client@10.45.2(@trpc/server@10.45.2))(@trpc/server@10.45.2)(react-dom@18.2.0(react@18.3.1))(react@18.3.1) - '@trpc/server': 10.45.2 + '@trpc/client': 11.0.0-rc.498(@trpc/server@11.0.0-rc.498) + '@trpc/server': 11.0.0-rc.498 next: 14.2.4(@babel/core@7.24.5)(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.3.1))(react@18.3.1) react: 18.3.1 react-dom: 18.2.0(react@18.3.1) + optionalDependencies: + '@tanstack/react-query': 5.53.3(react@18.3.1) + '@trpc/react-query': 11.0.0-rc.498(@tanstack/react-query@5.53.3(react@18.3.1))(@trpc/client@11.0.0-rc.498(@trpc/server@11.0.0-rc.498))(@trpc/server@11.0.0-rc.498)(react-dom@18.2.0(react@18.3.1))(react@18.3.1) - '@trpc/react-query@10.45.2(@tanstack/react-query@4.36.1(react-dom@18.2.0(react@18.3.1))(react@18.3.1))(@trpc/client@10.45.2(@trpc/server@10.45.2))(@trpc/server@10.45.2)(react-dom@18.2.0(react@18.3.1))(react@18.3.1)': + '@trpc/react-query@11.0.0-rc.498(@tanstack/react-query@5.53.3(react@18.3.1))(@trpc/client@11.0.0-rc.498(@trpc/server@11.0.0-rc.498))(@trpc/server@11.0.0-rc.498)(react-dom@18.2.0(react@18.3.1))(react@18.3.1)': dependencies: - '@tanstack/react-query': 4.36.1(react-dom@18.2.0(react@18.3.1))(react@18.3.1) - '@trpc/client': 10.45.2(@trpc/server@10.45.2) - '@trpc/server': 10.45.2 + '@tanstack/react-query': 5.53.3(react@18.3.1) + '@trpc/client': 11.0.0-rc.498(@trpc/server@11.0.0-rc.498) + '@trpc/server': 11.0.0-rc.498 react: 18.3.1 react-dom: 18.2.0(react@18.3.1) - '@trpc/server@10.45.2': {} + '@trpc/server@11.0.0-rc.498': {} '@types/aria-query@5.0.4': {} @@ -13269,13 +13276,13 @@ snapshots: dependencies: '@unhead/schema': 1.9.14 - '@unhead/vue@1.9.14(vue@3.4.30(typescript@5.4.5))': + '@unhead/vue@1.9.14(vue@3.4.30(typescript@5.5.4))': dependencies: '@unhead/schema': 1.9.14 '@unhead/shared': 1.9.14 hookable: 5.5.3 unhead: 1.9.14 - vue: 3.4.30(typescript@5.4.5) + vue: 3.4.30(typescript@5.5.4) '@vitest/expect@1.6.0': dependencies: @@ -13354,29 +13361,29 @@ snapshots: '@vue/shared': 3.4.30 csstype: 3.1.3 - '@vue/server-renderer@3.4.30(vue@3.4.30(typescript@5.4.5))': + '@vue/server-renderer@3.4.30(vue@3.4.30(typescript@5.5.4))': dependencies: '@vue/compiler-ssr': 3.4.30 '@vue/shared': 3.4.30 - vue: 3.4.30(typescript@5.4.5) + vue: 3.4.30(typescript@5.5.4) '@vue/shared@3.4.30': {} - '@vueuse/core@10.11.0(vue@3.4.30(typescript@5.4.5))': + '@vueuse/core@10.11.0(vue@3.4.30(typescript@5.5.4))': dependencies: '@types/web-bluetooth': 0.0.20 '@vueuse/metadata': 10.11.0 - '@vueuse/shared': 10.11.0(vue@3.4.30(typescript@5.4.5)) - vue-demi: 0.14.8(vue@3.4.30(typescript@5.4.5)) + '@vueuse/shared': 10.11.0(vue@3.4.30(typescript@5.5.4)) + vue-demi: 0.14.8(vue@3.4.30(typescript@5.5.4)) transitivePeerDependencies: - '@vue/composition-api' - vue '@vueuse/metadata@10.11.0': {} - '@vueuse/shared@10.11.0(vue@3.4.30(typescript@5.4.5))': + '@vueuse/shared@10.11.0(vue@3.4.30(typescript@5.5.4))': dependencies: - vue-demi: 0.14.8(vue@3.4.30(typescript@5.4.5)) + vue-demi: 0.14.8(vue@3.4.30(typescript@5.5.4)) transitivePeerDependencies: - '@vue/composition-api' - vue @@ -14446,11 +14453,11 @@ snapshots: csstype@3.1.3: {} - cva@1.0.0-beta.1(typescript@5.4.5): + cva@1.0.0-beta.1(typescript@5.5.4): dependencies: clsx: 2.0.0 optionalDependencies: - typescript: 5.4.5 + typescript: 5.5.4 d3-array@3.2.4: dependencies: @@ -15688,7 +15695,7 @@ snapshots: klona@2.0.6: {} - knip@5.17.4(@types/node@20.12.12)(typescript@5.4.5): + knip@5.17.4(@types/node@20.12.12)(typescript@5.5.4): dependencies: '@ericcornelissen/bash-parser': 0.5.2 '@nodelib/fs.walk': 2.0.0 @@ -15707,7 +15714,7 @@ snapshots: smol-toml: 1.1.4 strip-json-comments: 5.0.1 summary: 2.1.0 - typescript: 5.4.5 + typescript: 5.5.4 zod: 3.23.8 zod-validation-error: 3.3.0(zod@3.23.8) @@ -16708,6 +16715,11 @@ snapshots: nprogress@0.2.0: {} + nuqs@1.17.8(next@14.2.4(@babel/core@7.24.5)(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.3.1))(react@18.3.1)): + dependencies: + mitt: 3.0.1 + next: 14.2.4(@babel/core@7.24.5)(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.3.1))(react@18.3.1) + oauth@0.9.15: {} object-assign@4.1.1: {} @@ -17084,12 +17096,12 @@ snapshots: dependencies: '@prisma/client': 5.14.0(prisma@5.14.0) - prisma-json-types-generator@3.0.4(prisma@5.14.0)(typescript@5.4.5): + prisma-json-types-generator@3.0.4(prisma@5.14.0)(typescript@5.5.4): dependencies: '@prisma/generator-helper': 5.9.1 prisma: 5.14.0 tslib: 2.6.2 - typescript: 5.4.5 + typescript: 5.5.4 prisma@5.14.0: dependencies: @@ -17253,20 +17265,20 @@ snapshots: quick-format-unescaped@4.0.4: {} - radix-vue@1.8.5(vue@3.4.30(typescript@5.4.5)): + radix-vue@1.8.5(vue@3.4.30(typescript@5.5.4)): dependencies: '@floating-ui/dom': 1.6.5 - '@floating-ui/vue': 1.0.7(vue@3.4.30(typescript@5.4.5)) + '@floating-ui/vue': 1.0.7(vue@3.4.30(typescript@5.5.4)) '@internationalized/date': 3.5.4 '@internationalized/number': 3.5.3 - '@tanstack/vue-virtual': 3.7.0(vue@3.4.30(typescript@5.4.5)) - '@vueuse/core': 10.11.0(vue@3.4.30(typescript@5.4.5)) - '@vueuse/shared': 10.11.0(vue@3.4.30(typescript@5.4.5)) + '@tanstack/vue-virtual': 3.7.0(vue@3.4.30(typescript@5.5.4)) + '@vueuse/core': 10.11.0(vue@3.4.30(typescript@5.5.4)) + '@vueuse/shared': 10.11.0(vue@3.4.30(typescript@5.5.4)) aria-hidden: 1.2.4 defu: 6.1.4 fast-deep-equal: 3.1.3 nanoid: 5.0.7 - vue: 3.4.30(typescript@5.4.5) + vue: 3.4.30(typescript@5.5.4) transitivePeerDependencies: - '@vue/composition-api' @@ -17349,6 +17361,11 @@ snapshots: - utf-8-validate - webpack-cli + react-error-boundary@4.0.13(react@18.3.1): + dependencies: + '@babel/runtime': 7.24.5 + react: 18.3.1 + react-hook-form@7.52.1(react@18.3.1): dependencies: react: 18.3.1 @@ -18308,7 +18325,7 @@ snapshots: typescript@5.1.6: {} - typescript@5.4.5: {} + typescript@5.5.4: {} ua-parser-js@1.0.37: {} @@ -18487,10 +18504,6 @@ snapshots: optionalDependencies: '@types/react': 18.3.3 - use-sync-external-store@1.2.2(react@18.3.1): - dependencies: - react: 18.3.1 - usehooks-ts@3.1.0(react@18.3.1): dependencies: lodash.debounce: 4.0.8 @@ -18637,26 +18650,26 @@ snapshots: vlq@0.2.3: {} - vue-demi@0.14.8(vue@3.4.30(typescript@5.4.5)): + vue-demi@0.14.8(vue@3.4.30(typescript@5.5.4)): dependencies: - vue: 3.4.30(typescript@5.4.5) + vue: 3.4.30(typescript@5.5.4) - vue-router@4.4.0(vue@3.4.30(typescript@5.4.5)): + vue-router@4.4.0(vue@3.4.30(typescript@5.5.4)): dependencies: '@vue/devtools-api': 6.6.3 - vue: 3.4.30(typescript@5.4.5) + vue: 3.4.30(typescript@5.5.4) vue-sonner@1.1.3: {} - vue@3.4.30(typescript@5.4.5): + vue@3.4.30(typescript@5.5.4): dependencies: '@vue/compiler-dom': 3.4.30 '@vue/compiler-sfc': 3.4.30 '@vue/runtime-dom': 3.4.30 - '@vue/server-renderer': 3.4.30(vue@3.4.30(typescript@5.4.5)) + '@vue/server-renderer': 3.4.30(vue@3.4.30(typescript@5.5.4)) '@vue/shared': 3.4.30 optionalDependencies: - typescript: 5.4.5 + typescript: 5.5.4 w3c-keyname@2.2.8: {} diff --git a/prisma/seeds/companies.ts b/prisma/seeds/companies.ts index 4f24b99bf..1c02b7195 100644 --- a/prisma/seeds/companies.ts +++ b/prisma/seeds/companies.ts @@ -1,5 +1,5 @@ import { generatePublicId } from "@/common/id"; -import { db } from "@/server/db"; +import type { TPrismaOrTransaction } from "@/server/db"; import { faker } from "@faker-js/faker"; import colors from "colors"; import { sample } from "lodash-es"; @@ -19,7 +19,7 @@ type CompanyType = { zipcode: string; country: string; }; -const seedCompanies = async (count = 4) => { +const seedCompanies = async (tx: TPrismaOrTransaction, count = 4) => { const companies: CompanyType[] = []; for (let i = 0; i < count; i++) { @@ -41,11 +41,15 @@ const seedCompanies = async (count = 4) => { console.log(`Seeding ${companies.length} companies`.blue); - const records = await db.company.createMany({ + const records = await tx.company.createManyAndReturn({ data: companies, + select: { + id: true, + name: true, + }, }); - console.log(`🎉 Seeded ${records.count} companies`.green); + console.log(`🎉 Seeded ${records.length} companies`.green); return records; }; diff --git a/prisma/seeds/index.ts b/prisma/seeds/index.ts index b7688b9df..471291d7e 100644 --- a/prisma/seeds/index.ts +++ b/prisma/seeds/index.ts @@ -3,8 +3,10 @@ import colors from "colors"; import inquirer from "inquirer"; colors.enable(); +import { Prisma } from "@prisma/client"; import type { QuestionCollection } from "inquirer"; import seedCompanies from "./companies"; +import { seedStakeholders } from "./stakeholder"; import seedTeam from "./team"; if (process.env.NODE_ENV === "production") { @@ -23,47 +25,86 @@ const seed = async () => { const answer = inquiry.answer as boolean; if (answer) { - await nuke(); + await cleanupDb(); console.log("Seeding database".underline.cyan); - return db.$transaction(async () => { - await seedCompanies(); - await seedTeam(); + return db.$transaction(async (tx) => { + const companies = await seedCompanies(tx); + await seedTeam(tx); + + for (const company of companies) { + await seedStakeholders(tx, company.id, company.name, 500); + } }); - } else { - throw new Error("Seeding aborted"); } -}; - -const nuke = async () => { - console.log("🚀 Nuking database records".yellow); - return db.$transaction(async (db) => { - await db.user.deleteMany(); - await db.member.deleteMany(); - await db.company.deleteMany(); - await db.shareClass.deleteMany(); - await db.equityPlan.deleteMany(); - await db.document.deleteMany(); - await db.bucket.deleteMany(); - await db.audit.deleteMany(); - await db.session.deleteMany(); - }); + throw new Error("Seeding aborted"); }; await seed() - .then(async () => { + .then(() => { console.log("✅ Database seeding completed".green); console.log( - `💌 We have created four admin accounts for you. Please login with one of these emails:\n` + "💌 We have created four admin accounts for you. Please login with one of these emails:\n" .cyan, - `ceo@example.com\n`.underline.yellow, - `cto@example.com\n`.underline.yellow, - `cfo@example.com\n`.underline.yellow, - `lawyer@example.com\n`.underline.yellow, + "ceo@example.com\n".underline.yellow, + "cto@example.com\n".underline.yellow, + "cfo@example.com\n".underline.yellow, + "lawyer@example.com\n".underline.yellow, ); - await db.$disconnect(); }) - .catch(async (error: Error) => { + .catch((error: Error) => { console.log(`❌ ${error.message}`.red); + }) + .finally(async () => { await db.$disconnect(); }); + +export async function cleanupDb() { + console.log("🚀 Nuking database records".yellow); + + try { + const tables = await db.$queryRaw<{ tablename: string }[]>` + SELECT tablename FROM pg_tables + WHERE schemaname = 'public' AND tablename != '_prisma_migrations' + `; + + // Disable foreign key checks + await db.$executeRaw`SET CONSTRAINTS ALL DEFERRED`; + + for (const { tablename } of tables) { + try { + // Check if the table exists before attempting to truncate + const tableExists = await db.$queryRaw<[{ exists: boolean }]>` + SELECT EXISTS ( + SELECT FROM information_schema.tables + WHERE table_schema = 'public' AND table_name = ${tablename} + ) + `; + + if (tableExists[0].exists) { + await db.$executeRaw( + Prisma.sql`TRUNCATE TABLE "${Prisma.raw(tablename)}" CASCADE`, + ); + console.log(`Table ${tablename} truncated successfully`.green); + } else { + console.log(`Table ${tablename} doesn't exist, skipping`.yellow); + } + } catch (err) { + if (err instanceof Prisma.PrismaClientKnownRequestError) { + console.error(`Error truncating table ${tablename}:`, err.message); + } else { + console.error(`Unexpected error truncating table ${tablename}:`, err); + } + } + } + + // Re-enable foreign key checks + await db.$executeRaw`SET CONSTRAINTS ALL IMMEDIATE`; + + console.log( + "All tables reset successfully, except _prisma_migrations".yellow, + ); + } catch (error) { + console.error("Error resetting tables:", error); + } +} diff --git a/prisma/seeds/stakeholder.ts b/prisma/seeds/stakeholder.ts new file mode 100644 index 000000000..61500dd9f --- /dev/null +++ b/prisma/seeds/stakeholder.ts @@ -0,0 +1,45 @@ +import type { TPrismaOrTransaction } from "@/server/db"; +import { faker } from "@faker-js/faker"; +import colors from "colors"; +import { StakeholderRelationshipEnum, StakeholderTypeEnum } from "../enums"; +colors.enable(); + +export async function seedStakeholders( + tx: TPrismaOrTransaction, + companyId: string, + companyName: string, + count = 100, +) { + const stakeholders = []; + + for (let index = 0; index < count; index++) { + stakeholders.push({ + name: faker.person.fullName(), + email: faker.internet.email(), + institutionName: faker.company.name(), + stakeholderType: faker.helpers.arrayElement( + Object.values(StakeholderTypeEnum), + ), + currentRelationship: faker.helpers.arrayElement( + Object.values(StakeholderRelationshipEnum), + ), + taxId: faker.finance.accountNumber(), + + streetAddress: faker.location.streetAddress(), + city: faker.location.city(), + state: faker.location.state(), + zipcode: faker.location.zipCode(), + country: faker.location.country(), + companyId, + }); + } + console.log( + `Seeding ${stakeholders.length} stakeholders for ${companyName}`.blue, + ); + + const record = await tx.stakeholder.createMany({ data: stakeholders }); + + console.log( + `🎉 Seeded ${record.count} stakeholders for ${companyName}`.green, + ); +} diff --git a/prisma/seeds/team.ts b/prisma/seeds/team.ts index 13596205e..bd75861a7 100644 --- a/prisma/seeds/team.ts +++ b/prisma/seeds/team.ts @@ -1,5 +1,6 @@ import type { MemberStatusEnum } from "@/prisma/enums"; -import { db } from "@/server/db"; +import type { TPrismaOrTransaction } from "@/server/db"; + import { faker } from "@faker-js/faker"; import bcrypt from "bcryptjs"; import colors from "colors"; @@ -14,7 +15,7 @@ type UserType = { status?: MemberStatusEnum; }; -const seedTeam = async () => { +const seedTeam = async (tx: TPrismaOrTransaction) => { const team = [ { name: faker.person.fullName(), @@ -60,14 +61,15 @@ const seedTeam = async () => { ]; console.log(`Seeding ${team.length} team members`.blue); - const companies = await db.company.findMany(); + const companies = await tx.company.findMany(); + // biome-ignore lint/complexity/noForEach: team.forEach(async (t) => { // const { name, email, image, title, status, isOnboarded } = t const salt = await bcrypt.genSalt(10); const hashedPassword = await bcrypt.hash("P@ssw0rd!", salt); const { name, email, title, status, isOnboarded } = t; - const user = await db.user.create({ + const user = await tx.user.create({ data: { name, email, @@ -77,8 +79,9 @@ const seedTeam = async () => { }, }); + // biome-ignore lint/complexity/noForEach: companies.forEach(async (company) => { - await db.member.create({ + await tx.member.create({ data: { title, isOnboarded, diff --git a/src/app/(authenticated)/(dashboard)/[publicId]/audits/page.tsx b/src/app/(authenticated)/(dashboard)/[publicId]/audits/page.tsx index 587c79371..f38cadb8c 100644 --- a/src/app/(authenticated)/(dashboard)/[publicId]/audits/page.tsx +++ b/src/app/(authenticated)/(dashboard)/[publicId]/audits/page.tsx @@ -18,7 +18,7 @@ const AuditsPage = async () => { return ; } - const audits = await api.audit.getAudits.query({}); + const audits = await api.audit.getAudits({}); return (
diff --git a/src/app/(authenticated)/(dashboard)/[publicId]/documents/data-rooms/[dataRoomPublicId]/page.tsx b/src/app/(authenticated)/(dashboard)/[publicId]/documents/data-rooms/[dataRoomPublicId]/page.tsx index 958d421d8..195e80eb1 100644 --- a/src/app/(authenticated)/(dashboard)/[publicId]/documents/data-rooms/[dataRoomPublicId]/page.tsx +++ b/src/app/(authenticated)/(dashboard)/[publicId]/documents/data-rooms/[dataRoomPublicId]/page.tsx @@ -10,7 +10,7 @@ const DataRoomSettinsPage = async ({ }: { params: { publicId: string; dataRoomPublicId: string }; }) => { - const { dataRoom, documents } = await api.dataRoom.getDataRoom.query({ + const { dataRoom, documents } = await api.dataRoom.getDataRoom({ dataRoomPublicId, include: { company: false, @@ -18,7 +18,7 @@ const DataRoomSettinsPage = async ({ documents: true, }, }); - const contacts = await api.common.getContacts.query(); + const contacts = await api.common.getContacts(); if (!dataRoom) { return notFound(); diff --git a/src/app/(authenticated)/(dashboard)/[publicId]/documents/esign/[templatePublicId]/page.tsx b/src/app/(authenticated)/(dashboard)/[publicId]/documents/esign/[templatePublicId]/page.tsx index 55fc8dbbc..772f80b05 100644 --- a/src/app/(authenticated)/(dashboard)/[publicId]/documents/esign/[templatePublicId]/page.tsx +++ b/src/app/(authenticated)/(dashboard)/[publicId]/documents/esign/[templatePublicId]/page.tsx @@ -13,11 +13,10 @@ const EsignTemplateDetailPage = async ({ }) => { const session = await withServerComponentSession(); - const { name, status, url, fields, recipients } = - await api.template.get.query({ - publicId: templatePublicId, - isDraftOnly: true, - }); + const { name, status, url, fields, recipients } = await api.template.get({ + publicId: templatePublicId, + isDraftOnly: true, + }); return ( diff --git a/src/app/(authenticated)/(dashboard)/[publicId]/documents/esign/page.tsx b/src/app/(authenticated)/(dashboard)/[publicId]/documents/esign/page.tsx index 2a423f3a5..0f40e167e 100644 --- a/src/app/(authenticated)/(dashboard)/[publicId]/documents/esign/page.tsx +++ b/src/app/(authenticated)/(dashboard)/[publicId]/documents/esign/page.tsx @@ -14,7 +14,7 @@ export const metadata: Metadata = { const EsignDocumentPage = async () => { const session = await withServerComponentSession(); - const { documents } = await api.template.all.query(); + const { documents } = await api.template.all(); if (documents.length === 0) { return ( diff --git a/src/app/(authenticated)/(dashboard)/[publicId]/documents/esign/v/[templatePublicId]/page.tsx b/src/app/(authenticated)/(dashboard)/[publicId]/documents/esign/v/[templatePublicId]/page.tsx index 648983984..957568a10 100644 --- a/src/app/(authenticated)/(dashboard)/[publicId]/documents/esign/v/[templatePublicId]/page.tsx +++ b/src/app/(authenticated)/(dashboard)/[publicId]/documents/esign/v/[templatePublicId]/page.tsx @@ -22,13 +22,13 @@ export default async function TemplateDetailViewPage({ const { allow } = await serverAccessControl(); const [{ name, status, url, fields }, auditsData] = await Promise.all([ - api.template.get.query({ + api.template.get({ publicId: templatePublicId, isDraftOnly: false, }), allow( - api.audit.allEsignAudits.query({ + api.audit.allEsignAudits({ templatePublicId: templatePublicId, }), ["audits", "read"], diff --git a/src/app/(authenticated)/(dashboard)/[publicId]/documents/page.tsx b/src/app/(authenticated)/(dashboard)/[publicId]/documents/page.tsx index 1306b2bcc..e7386751f 100644 --- a/src/app/(authenticated)/(dashboard)/[publicId]/documents/page.tsx +++ b/src/app/(authenticated)/(dashboard)/[publicId]/documents/page.tsx @@ -18,10 +18,7 @@ const DocumentsPage = async () => { const { allow } = await serverAccessControl(); const session = await withServerComponentSession(); - const documents = await allow(api.document.getAll.query(), [ - "documents", - "read", - ]); + const documents = await allow(api.document.getAll(), ["documents", "read"]); const canUpload = allow(true, ["documents", "read"]); diff --git a/src/app/(authenticated)/(dashboard)/[publicId]/documents/share/_page.tsx b/src/app/(authenticated)/(dashboard)/[publicId]/documents/share/_page.tsx index fb635f74e..6f89f0d9c 100644 --- a/src/app/(authenticated)/(dashboard)/[publicId]/documents/share/_page.tsx +++ b/src/app/(authenticated)/(dashboard)/[publicId]/documents/share/_page.tsx @@ -14,7 +14,7 @@ export const metadata: Metadata = { }; const DocumentsPage = async () => { - const documents = await api.document.getAll.query(); + const documents = await api.document.getAll(); const session = await withServerComponentSession(); if (documents.length === 0) { diff --git a/src/app/(authenticated)/(dashboard)/[publicId]/fundraise/safes/page.tsx b/src/app/(authenticated)/(dashboard)/[publicId]/fundraise/safes/page.tsx index d81acf197..c864677f2 100644 --- a/src/app/(authenticated)/(dashboard)/[publicId]/fundraise/safes/page.tsx +++ b/src/app/(authenticated)/(dashboard)/[publicId]/fundraise/safes/page.tsx @@ -12,7 +12,7 @@ export const metadata: Metadata = { }; const SafePage = async () => { - const safes = await api.safe.getSafes.query(); + const safes = await api.safe.getSafes(); if (!safes?.data?.length) { return ( diff --git a/src/app/(authenticated)/(dashboard)/[publicId]/securities/options/page.tsx b/src/app/(authenticated)/(dashboard)/[publicId]/securities/options/page.tsx index 03e7cadcc..0ace13a45 100644 --- a/src/app/(authenticated)/(dashboard)/[publicId]/securities/options/page.tsx +++ b/src/app/(authenticated)/(dashboard)/[publicId]/securities/options/page.tsx @@ -12,9 +12,9 @@ export const metadata: Metadata = { }; const OptionsPage = async () => { - const options = await api.securities.getOptions.query(); - const stakeholders = await api.stakeholder.getStakeholders.query(); - const equityPlans = await api.equityPlan.getPlans.query(); + const options = await api.securities.getOptions(); + const stakeholders = await api.stakeholder.getStakeholders(); + const equityPlans = await api.equityPlan.getPlans(); if (options?.data?.length === 0) { return ( diff --git a/src/app/(authenticated)/(dashboard)/[publicId]/securities/shares/page.tsx b/src/app/(authenticated)/(dashboard)/[publicId]/securities/shares/page.tsx index fbd24edb0..a8d9a25a8 100644 --- a/src/app/(authenticated)/(dashboard)/[publicId]/securities/shares/page.tsx +++ b/src/app/(authenticated)/(dashboard)/[publicId]/securities/shares/page.tsx @@ -13,9 +13,9 @@ export const metadata: Metadata = { }; const SharesPage = async () => { - const shares = await api.securities.getShares.query(); - const stakeholders = await api.stakeholder.getStakeholders.query(); - const shareClasses = await api.shareClass.get.query(); + const shares = await api.securities.getShares(); + const stakeholders = await api.stakeholder.getStakeholders(); + const shareClasses = await api.shareClass.get(); if (shares?.data?.length === 0) { return ( diff --git a/src/app/(authenticated)/(dashboard)/[publicId]/settings/bank-accounts/page.tsx b/src/app/(authenticated)/(dashboard)/[publicId]/settings/bank-accounts/page.tsx index 357d32436..d6f24244e 100644 --- a/src/app/(authenticated)/(dashboard)/[publicId]/settings/bank-accounts/page.tsx +++ b/src/app/(authenticated)/(dashboard)/[publicId]/settings/bank-accounts/page.tsx @@ -14,7 +14,7 @@ export const metadata: Metadata = { const ApiSettingsPage = async () => { const { allow } = await serverAccessControl(); - const data = await allow(api.bankAccounts.getAll.query(), [ + const data = await allow(api.bankAccounts.getAll(), [ "bank-accounts", "read", ]); diff --git a/src/app/(authenticated)/(dashboard)/[publicId]/settings/billing/page.tsx b/src/app/(authenticated)/(dashboard)/[publicId]/settings/billing/page.tsx index 84937e1a7..2fd7a8331 100644 --- a/src/app/(authenticated)/(dashboard)/[publicId]/settings/billing/page.tsx +++ b/src/app/(authenticated)/(dashboard)/[publicId]/settings/billing/page.tsx @@ -8,8 +8,8 @@ export const metadata: Metadata = { }; const BillingPage = async () => { const [{ products }, { subscription }] = await Promise.all([ - api.billing.getProducts.query(), - api.billing.getSubscription.query(), + api.billing.getProducts(), + api.billing.getSubscription(), ]); return ( diff --git a/src/app/(authenticated)/(dashboard)/[publicId]/settings/company/page.tsx b/src/app/(authenticated)/(dashboard)/[publicId]/settings/company/page.tsx index 4deb24116..f56eb5d8f 100644 --- a/src/app/(authenticated)/(dashboard)/[publicId]/settings/company/page.tsx +++ b/src/app/(authenticated)/(dashboard)/[publicId]/settings/company/page.tsx @@ -10,10 +10,7 @@ export const metadata: Metadata = { const CompanySettingsPage = async () => { const { allow } = await serverAccessControl(); - const data = await allow(api.company.getCompany.query(), [ - "company", - "update", - ]); + const data = await allow(api.company.getCompany(), ["company", "update"]); if (!data?.company) { return ; diff --git a/src/app/(authenticated)/(dashboard)/[publicId]/settings/developer/page.tsx b/src/app/(authenticated)/(dashboard)/[publicId]/settings/developer/page.tsx index c5a338ce6..07e6c188c 100644 --- a/src/app/(authenticated)/(dashboard)/[publicId]/settings/developer/page.tsx +++ b/src/app/(authenticated)/(dashboard)/[publicId]/settings/developer/page.tsx @@ -11,7 +11,7 @@ export const metadata: Metadata = { title: "Developer settings", }; const AccessTokenPage = async () => { - const data = await api.accessToken.listAll.query({ + const data = await api.accessToken.listAll({ typeEnum: "api", }); diff --git a/src/app/(authenticated)/(dashboard)/[publicId]/settings/profile/page.tsx b/src/app/(authenticated)/(dashboard)/[publicId]/settings/profile/page.tsx index 4cc38559e..d9799c19c 100644 --- a/src/app/(authenticated)/(dashboard)/[publicId]/settings/profile/page.tsx +++ b/src/app/(authenticated)/(dashboard)/[publicId]/settings/profile/page.tsx @@ -1,13 +1,13 @@ import { ProfileSettings } from "@/components/member/member-profile"; import { api } from "@/trpc/server"; -import { type Metadata } from "next"; +import type { Metadata } from "next"; export const metadata: Metadata = { title: "Profile", }; const ProfileSettingsPage = async () => { - const memberProfile = await api.member.getProfile.query(); + const memberProfile = await api.member.getProfile(); return ; }; diff --git a/src/app/(authenticated)/(dashboard)/[publicId]/settings/roles/page.tsx b/src/app/(authenticated)/(dashboard)/[publicId]/settings/roles/page.tsx index a46af7f32..3586b6659 100644 --- a/src/app/(authenticated)/(dashboard)/[publicId]/settings/roles/page.tsx +++ b/src/app/(authenticated)/(dashboard)/[publicId]/settings/roles/page.tsx @@ -8,7 +8,7 @@ import { api } from "@/trpc/server"; export default async function RolesPage() { const { allow } = await serverAccessControl(); - const data = await allow(api.rbac.listRoles.query(), ["roles", "read"]); + const data = await allow(api.rbac.listRoles(), ["roles", "read"]); const canCreate = allow(true, ["roles", "create"]); diff --git a/src/app/(authenticated)/(dashboard)/[publicId]/settings/security/passkey/page.tsx b/src/app/(authenticated)/(dashboard)/[publicId]/settings/security/passkey/page.tsx index 7d4683a84..aaae7ac9c 100644 --- a/src/app/(authenticated)/(dashboard)/[publicId]/settings/security/passkey/page.tsx +++ b/src/app/(authenticated)/(dashboard)/[publicId]/settings/security/passkey/page.tsx @@ -10,7 +10,7 @@ export const metadata: Metadata = { title: "Security", }; export default async function PasskeyPage() { - const passkeys = await api.passkey.find.query(); + const passkeys = await api.passkey.find(); return (
diff --git a/src/app/(authenticated)/(dashboard)/[publicId]/settings/team/page.tsx b/src/app/(authenticated)/(dashboard)/[publicId]/settings/team/page.tsx index a9b32339a..531299df5 100644 --- a/src/app/(authenticated)/(dashboard)/[publicId]/settings/team/page.tsx +++ b/src/app/(authenticated)/(dashboard)/[publicId]/settings/team/page.tsx @@ -11,8 +11,8 @@ export const metadata: Metadata = { const TeamMembersPage = async () => { const [members, roles] = await Promise.all([ - api.member.getMembers.query(), - api.rbac.listRoles.query(), + api.member.getMembers(), + api.rbac.listRoles(), ]); return (
diff --git a/src/app/(authenticated)/(dashboard)/[publicId]/stakeholders/page.tsx b/src/app/(authenticated)/(dashboard)/[publicId]/stakeholders/page.tsx index e2d358d57..2709fdee4 100644 --- a/src/app/(authenticated)/(dashboard)/[publicId]/stakeholders/page.tsx +++ b/src/app/(authenticated)/(dashboard)/[publicId]/stakeholders/page.tsx @@ -1,23 +1,41 @@ import EmptyState from "@/components/common/empty-state"; import StakeholderDropdown from "@/components/stakeholder/stakeholder-dropdown"; import StakeholderTable from "@/components/stakeholder/stakeholder-table"; -import { Card } from "@/components/ui/card"; import { UnAuthorizedState } from "@/components/ui/un-authorized-state"; import { serverAccessControl } from "@/lib/rbac/access-control"; -import { api } from "@/trpc/server"; +import { getManyStakeholder } from "@/server/api/client-handlers/stakeholder"; +import { ManyStakeholderQuerySchema } from "@/server/api/schema/stakeholder"; +import { withServerSession } from "@/server/auth"; import { RiGroup2Fill } from "@remixicon/react"; import type { Metadata } from "next"; +import { headers } from "next/headers"; export const metadata: Metadata = { title: "Stakeholders", }; -const StakeholdersPage = async () => { +const StakeholdersPage = async ({ + searchParams, +}: { + searchParams: Record; +}) => { + const session = await withServerSession(); + const { allow } = await serverAccessControl(); - const stakeholders = await allow(api.stakeholder.getStakeholders.query(), [ - "stakeholder", - "read", - ]); + + const { limit, page, sort, name } = + ManyStakeholderQuerySchema.parse(searchParams); + + const stakeholders = await allow( + getManyStakeholder({ + searchParams: { limit, page, sort, ...(name && { name }) }, + urlParams: { + companyId: session.user.companyId, + }, + headers: headers(), + }), + ["stakeholder", "read"], + ); const stakeholderDropdown = allow( , @@ -29,7 +47,11 @@ const StakeholdersPage = async () => { return ; } - if (stakeholders.length === 0) { + if ( + stakeholders?.data && + stakeholders?.data.length === 0 && + Object.keys(searchParams).length === 0 + ) { return ( } @@ -54,9 +76,13 @@ const StakeholdersPage = async () => {
{stakeholderDropdown}
- - - +
); }; diff --git a/src/app/(authenticated)/(dashboard)/[publicId]/updates/page.tsx b/src/app/(authenticated)/(dashboard)/[publicId]/updates/page.tsx index 982e28c3e..2209dcc48 100644 --- a/src/app/(authenticated)/(dashboard)/[publicId]/updates/page.tsx +++ b/src/app/(authenticated)/(dashboard)/[publicId]/updates/page.tsx @@ -16,7 +16,7 @@ const UpdatesPage = async ({ }: { params: { publicId: string }; }) => { - const updates = await api.update.get.query(); + const updates = await api.update.get(); if (updates.data.length === 0) { return ( diff --git a/src/app/(documents)/esign/[token]/page.tsx b/src/app/(documents)/esign/[token]/page.tsx index 6e0e1ed84..9ad376a43 100644 --- a/src/app/(documents)/esign/[token]/page.tsx +++ b/src/app/(documents)/esign/[token]/page.tsx @@ -26,7 +26,7 @@ export default async function SigningPage(props: SigningPageProps) { templateId, signableFields, status: templateStatus, - } = await api.template.getSigningFields.query({ + } = await api.template.getSigningFields({ token, }); diff --git a/src/components/audit/audit-table/audit-table-toolbar.tsx b/src/components/audit/audit-table/audit-table-toolbar.tsx index 9f05b0582..39e8da13f 100644 --- a/src/components/audit/audit-table/audit-table-toolbar.tsx +++ b/src/components/audit/audit-table/audit-table-toolbar.tsx @@ -1,11 +1,11 @@ -import { useDataTable } from "@/components/ui/data-table/data-table"; +import { useTable } from "@/components/ui/data-table/data-table"; import { ResetButton } from "@/components/ui/data-table/data-table-buttons"; import { DataTableFacetedFilter } from "@/components/ui/data-table/data-table-faceted-filter"; import { DataTableViewOptions } from "@/components/ui/data-table/data-table-view-options"; import { getActions } from "@/server/audit/schema"; export function AuditTableToolbar() { - const { table } = useDataTable(); + const { table } = useTable(); const isFiltered = table.getState().columnFilters.length > 0; const actions = getActions(); diff --git a/src/components/audit/audit-table/index.tsx b/src/components/audit/audit-table/index.tsx index 87e26a9d4..599dfd12a 100644 --- a/src/components/audit/audit-table/index.tsx +++ b/src/components/audit/audit-table/index.tsx @@ -1,33 +1,17 @@ "use client"; -import * as React from "react"; - -import { - type ColumnDef, - type ColumnFiltersState, - type SortingState, - type VisibilityState, - getCoreRowModel, - getFacetedRowModel, - getFacetedUniqueValues, - getFilteredRowModel, - getPaginationRowModel, - getSortedRowModel, - useReactTable, -} from "@tanstack/react-table"; - -import { Checkbox } from "@/components/ui/checkbox"; - -import { type RouterOutputs } from "@/trpc/shared"; - import { dayjsExt } from "@/common/dayjs"; import { Badge } from "@/components/ui/badge"; +import { Checkbox } from "@/components/ui/checkbox"; import { DataTable } from "@/components/ui/data-table/data-table"; import { DataTableBody } from "@/components/ui/data-table/data-table-body"; -import { SortButton } from "@/components/ui/data-table/data-table-buttons"; import { DataTableContent } from "@/components/ui/data-table/data-table-content"; import { DataTableHeader } from "@/components/ui/data-table/data-table-header"; import { DataTablePagination } from "@/components/ui/data-table/data-table-pagination"; +import { useDataTable } from "@/hooks/use-data-table"; +import type { RouterOutputs } from "@/trpc/shared"; +import { type ColumnDef, createColumnHelper } from "@tanstack/react-table"; +import * as React from "react"; import { AuditTableToolbar } from "./audit-table-toolbar"; type Audit = RouterOutputs["audit"]["getAudits"]["data"]; @@ -36,8 +20,10 @@ interface AuditTableProps { audits: Audit; } -export const columns: ColumnDef[] = [ - { +const columnHelper = createColumnHelper(); + +export const columns = [ + columnHelper.display({ id: "select", header: ({ table }) => ( [] = [ ), enableSorting: false, enableHiding: false, - }, - { - id: "action", - accessorKey: "action", - header: () => { - return
Action
; - }, - cell: ({ row }) => ( + }), + + columnHelper.accessor("action", { + header: "Action", + cell: (row) => (
- {row.getValue("action")} + {row.getValue()}
), - filterFn: (row, id, value: string[]) => { - // eslint-disable-next-line @typescript-eslint/no-unsafe-return + filterFn: (row, id, value) => { return value.includes(row.getValue(id)); }, - }, + }), - { - id: "occurredAt", - accessorKey: "occurredAt", - header: ({ column }) => { + columnHelper.accessor("occurredAt", { + header: "Time", + cell: (row) => { + const date = new Date(row.getValue()); + const formattedDate = dayjsExt(date).format("lll"); return ( - column.toggleSorting(column.getIsSorted() === "asc")} - /> + ); }, - cell: ({ row }) => { - const date = new Date(row.getValue("occurredAt")); - const formattedDate = dayjsExt(date).format("lll"); - return ; - }, - }, + }), - { - id: "summary", - accessorKey: "summary", - header: () => { - return
Summary
; - }, - cell: ({ row }) => { - return

{row.getValue("summary")}

; - }, - }, + columnHelper.accessor("summary", { + header: "Summary", + cell: (row) => row.getValue(), + }), ]; export function AuditTable({ audits }: AuditTableProps) { - const [sorting, setSorting] = React.useState([]); - const [columnFilters, setColumnFilters] = React.useState( - [], - ); - const [columnVisibility, setColumnVisibility] = - React.useState({}); - const [rowSelection, setRowSelection] = React.useState({}); - - const table = useReactTable({ + const table = useDataTable({ data: audits, columns: columns, - enableRowSelection: true, - onRowSelectionChange: setRowSelection, - onSortingChange: setSorting, - onColumnFiltersChange: setColumnFilters, - onColumnVisibilityChange: setColumnVisibility, - getCoreRowModel: getCoreRowModel(), - getFilteredRowModel: getFilteredRowModel(), - getPaginationRowModel: getPaginationRowModel(), - getSortedRowModel: getSortedRowModel(), - getFacetedRowModel: getFacetedRowModel(), - getFacetedUniqueValues: getFacetedUniqueValues(), - state: { - sorting, - columnFilters, - columnVisibility, - rowSelection, - }, }); return ( diff --git a/src/components/billing/pricing-modal/index.tsx b/src/components/billing/pricing-modal/index.tsx index ccb2a1fd4..311819487 100644 --- a/src/components/billing/pricing-modal/index.tsx +++ b/src/components/billing/pricing-modal/index.tsx @@ -44,7 +44,7 @@ function Plans({ products, subscription }: PricingProps) { const [billingInterval, setBillingInterval] = useState("month"); - const { mutateAsync: checkoutWithStripe, isLoading: checkoutLoading } = + const { mutateAsync: checkoutWithStripe, isPending: checkoutLoading } = api.billing.checkout.useMutation({ onSuccess: async ({ stripeSessionId }) => { const stripe = await getStripeClient(); @@ -52,7 +52,7 @@ function Plans({ products, subscription }: PricingProps) { }, }); - const { mutateAsync: stripePortal, isLoading: stripePortalLoading } = + const { mutateAsync: stripePortal, isPending: stripePortalLoading } = api.billing.stripePortal.useMutation({ onSuccess: ({ url }) => { router.push(url); diff --git a/src/components/dashboard/overview/activities-card.tsx b/src/components/dashboard/overview/activities-card.tsx index b9dd3b1f3..4c20c3451 100644 --- a/src/components/dashboard/overview/activities-card.tsx +++ b/src/components/dashboard/overview/activities-card.tsx @@ -16,7 +16,7 @@ type Props = { }; const ActivityCard = async ({ className, publicId }: Props) => { - const activity = await api.audit.getAudits.query({ take: 4 }); + const activity = await api.audit.getAudits({ take: 4 }); return ( diff --git a/src/components/member/member-table-toolbar.tsx b/src/components/member/member-table-toolbar.tsx index d9a045222..e4a57ba47 100644 --- a/src/components/member/member-table-toolbar.tsx +++ b/src/components/member/member-table-toolbar.tsx @@ -1,4 +1,4 @@ -import { useDataTable } from "../ui/data-table/data-table"; +import { useTable } from "../ui/data-table/data-table"; import { ResetButton } from "../ui/data-table/data-table-buttons"; import { DataTableFacetedFilter } from "../ui/data-table/data-table-faceted-filter"; import { DataTableViewOptions } from "../ui/data-table/data-table-view-options"; @@ -6,7 +6,7 @@ import { Input } from "../ui/input"; import { statusValues } from "./data"; export function MemberTableToolbar() { - const { table } = useDataTable(); + const { table } = useTable(); const isFiltered = table.getState().columnFilters.length > 0; return ( diff --git a/src/components/member/member-table.tsx b/src/components/member/member-table.tsx index 65ebc7dca..05f7e408d 100644 --- a/src/components/member/member-table.tsx +++ b/src/components/member/member-table.tsx @@ -1,20 +1,8 @@ "use client"; -import * as React from "react"; +import type * as React from "react"; -import { - type ColumnDef, - type ColumnFiltersState, - type SortingState, - type VisibilityState, - getCoreRowModel, - getFacetedRowModel, - getFacetedUniqueValues, - getFilteredRowModel, - getPaginationRowModel, - getSortedRowModel, - useReactTable, -} from "@tanstack/react-table"; +import { type ColumnDef, createColumnHelper } from "@tanstack/react-table"; import { Button } from "@/components/ui/button"; import { Checkbox } from "@/components/ui/checkbox"; @@ -31,7 +19,9 @@ import { api } from "@/trpc/react"; import { Avatar, AvatarImage } from "@/components/ui/avatar"; +import { useDataTable } from "@/hooks/use-data-table"; import { getRoleId } from "@/lib/rbac/access-control-utils"; +import type { MemberStatusEnum } from "@/prisma/enums"; import type { RouterOutputs } from "@/trpc/shared"; import { RiMore2Fill } from "@remixicon/react"; import { useSession } from "next-auth/react"; @@ -39,7 +29,6 @@ import { useRouter } from "next/navigation"; import { pushModal } from "../modals"; import { DataTable } from "../ui/data-table/data-table"; import { DataTableBody } from "../ui/data-table/data-table-body"; -import { SortButton } from "../ui/data-table/data-table-buttons"; import { DataTableContent } from "../ui/data-table/data-table-content"; import { DataTableHeader } from "../ui/data-table/data-table-header"; import { DataTablePagination } from "../ui/data-table/data-table-pagination"; @@ -53,33 +42,28 @@ type MembersType = { roles: Roles; }; -const humanizeStatus = (status: string) => { - if (status === "PENDING") { - return ( - - Pending - - ); - } - if (status === "ACTIVE") { - return ( - - Active - - ); - } - if (status === "INACTIVE") { - return ( - - Inactive - - ); - } - return "Unknown"; +const humanizeStatus: Record = { + ACTIVE: ( + + Active + + ), + INACTIVE: ( + + Inactive + + ), + PENDING: ( + + Pending + + ), }; -export const columns: ColumnDef[] = [ - { +const columnHelper = createColumnHelper(); + +export const columns = [ + columnHelper.display({ id: "select", header: ({ table }) => ( [] = [ ), enableSorting: false, enableHiding: false, - }, - { - id: "name", - header: ({ column }) => { - return ( - column.toggleSorting(column.getIsSorted() === "asc")} - /> - ); - }, - accessorFn: (row) => row.user?.name, - cell: ({ row }) => ( + }), + columnHelper.accessor("user.name", { + header: "Name", + cell: (row) => (
-

{row.original?.user?.name}

-

{row.original?.user?.email}

+

{row.getValue()}

+

{row.row.original?.user?.email}

), - }, - { - accessorKey: "title", - header: ({ column }) => { - return ( - column.toggleSorting(column.getIsSorted() === "asc")} - /> - ); - }, - cell: ({ row }) =>
{row.getValue("title")}
, - }, - { - accessorKey: "status", - header: ({ column }) => { - return ( - column.toggleSorting(column.getIsSorted() === "asc")} - /> - ); - }, - cell: ({ row }) =>
{humanizeStatus(row.original.status)}
, + }), + columnHelper.accessor("title", { + header: "Title", + cell: (row) =>
{row.getValue()}
, + }), + columnHelper.accessor("status", { + header: "Status", + cell: (row) =>
{humanizeStatus[row.getValue()]}
, filterFn: (row, id, value: string[]) => { - // eslint-disable-next-line @typescript-eslint/no-unsafe-return return value.includes(row.getValue(id)); }, - }, - { + }), + + columnHelper.display({ id: "actions", enableHiding: false, + enableSorting: false, cell: ({ row, table }) => { // eslint-disable-next-line react-hooks/rules-of-hooks const router = useRouter(); @@ -278,38 +238,13 @@ export const columns: ColumnDef[] = [ ); }, - }, + }), ]; const MemberTable = ({ members, roles }: MembersType) => { - const [sorting, setSorting] = React.useState([]); - const [columnFilters, setColumnFilters] = React.useState( - [], - ); - const [columnVisibility, setColumnVisibility] = - React.useState({}); - const [rowSelection, setRowSelection] = React.useState({}); - - const table = useReactTable({ + const table = useDataTable({ data: members, columns: columns, - enableRowSelection: true, - onRowSelectionChange: setRowSelection, - onSortingChange: setSorting, - onColumnFiltersChange: setColumnFilters, - onColumnVisibilityChange: setColumnVisibility, - getCoreRowModel: getCoreRowModel(), - getFilteredRowModel: getFilteredRowModel(), - getPaginationRowModel: getPaginationRowModel(), - getSortedRowModel: getSortedRowModel(), - getFacetedRowModel: getFacetedRowModel(), - getFacetedUniqueValues: getFacetedUniqueValues(), - state: { - sorting, - columnFilters, - columnVisibility, - rowSelection, - }, meta: { roles, }, diff --git a/src/components/modals/stakeholder/single-stake-holder-form.tsx b/src/components/modals/stakeholder/single-stake-holder-form.tsx index f49147992..be8468db3 100644 --- a/src/components/modals/stakeholder/single-stake-holder-form.tsx +++ b/src/components/modals/stakeholder/single-stake-holder-form.tsx @@ -23,13 +23,13 @@ import { StakeholderRelationshipEnum, StakeholderTypeEnum, } from "@/prisma/enums"; +import type { TGetManyStakeholderRes } from "@/server/api/client-handlers/stakeholder"; import { api } from "@/trpc/react"; import type { RouterOutputs } from "@/trpc/shared"; import { useRouter } from "next/navigation"; import { toast } from "sonner"; -export type TStakeholder = - RouterOutputs["stakeholder"]["getStakeholders"][number]; +export type TStakeholder = TGetManyStakeholderRes["data"][number]; type TSingleStakeholderForm = | { diff --git a/src/components/onboarding/check-email/index.tsx b/src/components/onboarding/check-email/index.tsx index 0c25d5d7d..099fa63e0 100644 --- a/src/components/onboarding/check-email/index.tsx +++ b/src/components/onboarding/check-email/index.tsx @@ -10,16 +10,17 @@ const CheckEmailComponent = () => { const searchParams = useSearchParams(); const email = searchParams.get("email"); - const { mutateAsync, isLoading } = api.auth.resendEmail.useMutation({ - onSuccess: () => { - toast.success("🎉 Email successfully re-sent."); - }, - onError: () => { - toast.error( - "Uh oh! Something went wrong, please try again or contact support.", - ); - }, - }); + const { mutateAsync, isPending: isLoading } = + api.auth.resendEmail.useMutation({ + onSuccess: () => { + toast.success("🎉 Email successfully re-sent."); + }, + onError: () => { + toast.error( + "Uh oh! Something went wrong, please try again or contact support.", + ); + }, + }); async function Resend() { try { diff --git a/src/components/rbac/role-table.tsx b/src/components/rbac/role-table.tsx index 5c5e357d2..608e2adf7 100644 --- a/src/components/rbac/role-table.tsx +++ b/src/components/rbac/role-table.tsx @@ -1,23 +1,12 @@ "use client"; +import { useDataTable } from "@/hooks/use-data-table"; import { ADMIN_PERMISSION } from "@/lib/rbac/constants"; import type { TPermission } from "@/lib/rbac/schema"; import { api } from "@/trpc/react"; import type { RouterOutputs } from "@/trpc/shared"; import { RiMore2Fill } from "@remixicon/react"; -import { - type ColumnDef, - type ColumnFiltersState, - type SortingState, - type VisibilityState, - getCoreRowModel, - getFacetedRowModel, - getFacetedUniqueValues, - getFilteredRowModel, - getPaginationRowModel, - getSortedRowModel, - useReactTable, -} from "@tanstack/react-table"; +import { type ColumnDef, createColumnHelper } from "@tanstack/react-table"; import { useRouter } from "next/navigation"; import { useState } from "react"; import { pushModal } from "../modals"; @@ -27,7 +16,6 @@ import { Button } from "../ui/button"; import { Checkbox } from "../ui/checkbox"; import { DataTable } from "../ui/data-table/data-table"; import { DataTableBody } from "../ui/data-table/data-table-body"; -import { SortButton } from "../ui/data-table/data-table-buttons"; import { DataTableContent } from "../ui/data-table/data-table-content"; import { DataTableHeader } from "../ui/data-table/data-table-header"; import { DataTablePagination } from "../ui/data-table/data-table-pagination"; @@ -47,8 +35,10 @@ interface RoleTableProps { roles: Role[]; } -export const columns: ColumnDef[] = [ - { +const columnHelper = createColumnHelper(); + +export const columns = [ + columnHelper.display({ id: "select", header: ({ table }) => ( [] = [ ), enableSorting: false, enableHiding: false, - }, - { - id: "name", - header: ({ column }) => { - return ( - column.toggleSorting(column.getIsSorted() === "asc")} - /> - ); - }, - accessorKey: "name", - cell: ({ row }) => { - return
{row.getValue("name")}
; - }, - }, - { - accessorKey: "type", + }), + + columnHelper.accessor("name", { + header: "Name", + cell: (row) => row.getValue(), + }), + + columnHelper.accessor("type", { header: () =>
Type
, - cell: ({ row }) => { - const type = row.original.type; + cell: (row) => { + const type = row.getValue(); return (
@@ -98,9 +78,11 @@ export const columns: ColumnDef[] = [
); }, - }, - { + }), + + columnHelper.display({ id: "actions", + enableSorting: false, enableHiding: false, cell: ({ row }) => { const role = row.original; @@ -177,7 +159,7 @@ export const columns: ColumnDef[] = [ ); }, - }, + }), ]; function getPermission(permissions_: TPermission[]) { @@ -197,31 +179,9 @@ function getPermission(permissions_: TPermission[]) { } export function RoleTable({ roles }: RoleTableProps) { - const [sorting, setSorting] = useState([]); - const [columnFilters, setColumnFilters] = useState([]); - const [columnVisibility, setColumnVisibility] = useState({}); - const [rowSelection, setRowSelection] = useState({}); - - const table = useReactTable({ + const table = useDataTable({ data: roles, columns: columns, - enableRowSelection: true, - onRowSelectionChange: setRowSelection, - onSortingChange: setSorting, - onColumnFiltersChange: setColumnFilters, - onColumnVisibilityChange: setColumnVisibility, - getCoreRowModel: getCoreRowModel(), - getFilteredRowModel: getFilteredRowModel(), - getPaginationRowModel: getPaginationRowModel(), - getSortedRowModel: getSortedRowModel(), - getFacetedRowModel: getFacetedRowModel(), - getFacetedUniqueValues: getFacetedUniqueValues(), - state: { - sorting, - columnFilters, - columnVisibility, - rowSelection, - }, }); return ( diff --git a/src/components/safe/safe-table/index.tsx b/src/components/safe/safe-table/index.tsx index f57ec74bb..17e5d13d2 100644 --- a/src/components/safe/safe-table/index.tsx +++ b/src/components/safe/safe-table/index.tsx @@ -1,18 +1,6 @@ "use client"; -import { - type ColumnDef, - type ColumnFiltersState, - type SortingState, - type VisibilityState, - getCoreRowModel, - getFacetedRowModel, - getFacetedUniqueValues, - getFilteredRowModel, - getPaginationRowModel, - getSortedRowModel, - useReactTable, -} from "@tanstack/react-table"; +import { createColumnHelper } from "@tanstack/react-table"; import * as React from "react"; import { Button } from "@/components/ui/button"; @@ -31,10 +19,10 @@ import { api } from "@/trpc/react"; import { Avatar, AvatarImage } from "@/components/ui/avatar"; import { DataTable } from "@/components/ui/data-table/data-table"; import { DataTableBody } from "@/components/ui/data-table/data-table-body"; -import { SortButton } from "@/components/ui/data-table/data-table-buttons"; import { DataTableContent } from "@/components/ui/data-table/data-table-content"; import { DataTableHeader } from "@/components/ui/data-table/data-table-header"; import { DataTablePagination } from "@/components/ui/data-table/data-table-pagination"; +import { useDataTable } from "@/hooks/use-data-table"; import { getPresignedGetUrl } from "@/server/file-uploads"; import type { RouterOutputs } from "@/trpc/shared"; import { RiFileDownloadLine, RiMore2Fill } from "@remixicon/react"; @@ -48,8 +36,10 @@ type SafesType = { safes: Safe; }; -export const columns: ColumnDef[] = [ - { +const columnHelper = createColumnHelper(); + +export const columns = [ + columnHelper.display({ id: "select", header: ({ table }) => ( [] = [ ), enableSorting: false, enableHiding: false, - }, - { - id: "stakeholderName", - header: ({ column }) => { - return ( - column.toggleSorting(column.getIsSorted() === "asc")} - /> - ); - }, - accessorFn: (row) => row?.stakeholder?.name, - cell: ({ row }) => ( + }), + + columnHelper.accessor("stakeholder.name", { + header: "Stakeholder", + cell: (row) => (
-

{row?.original?.stakeholder?.name}

+

{row.getValue()}

), - }, - { - accessorKey: "capital", - header: ({ column }) => { - return ( - column.toggleSorting(column.getIsSorted() === "asc")} - /> - ); - }, - cell: ({ row }) => ( -
{row.getValue("capital")}
- ), - }, - { - accessorKey: "valuationCap", - header: ({ column }) => { - return ( - column.toggleSorting(column.getIsSorted() === "asc")} - /> - ); - }, - cell: ({ row }) => ( -
{row.getValue("valuationCap")}
- ), - }, - { - accessorKey: "type", - header: ({ column }) => { - return ( - column.toggleSorting(column.getIsSorted() === "asc")} - /> - ); - }, - cell: ({ row }) =>
{row.getValue("type")}
, - }, - { - accessorKey: "status", - header: ({ column }) => { - return ( - column.toggleSorting(column.getIsSorted() === "asc")} - /> - ); - }, - cell: ({ row }) => ( -
{row.getValue("status")}
- ), - }, - { - accessorKey: "discountRate", - header: ({ column }) => { - return ( - column.toggleSorting(column.getIsSorted() === "asc")} - /> - ); - }, - cell: ({ row }) => ( -
{row.getValue("discountRate")}
- ), - }, - { - accessorKey: "issueDate", - header: ({ column }) => { - return ( - column.toggleSorting(column.getIsSorted() === "asc")} - /> - ); - }, - cell: ({ row }) => ( -
- {new Date().toLocaleDateString(row.getValue("issueDate"))} + }), + + columnHelper.accessor("capital", { + header: "Capital", + cell: (row) =>
{row.getValue()}
, + }), + + columnHelper.accessor("valuationCap", { + header: "Valuation Cap", + cell: (row) =>
{row.getValue()}
, + }), + + columnHelper.accessor("type", { + header: "Type", + cell: (row) =>
{row.getValue()}
, + }), + + columnHelper.accessor("status", { + header: "Status", + cell: (row) =>
{row.getValue()}
, + }), + + columnHelper.accessor("discountRate", { + header: "Discount Rate", + cell: (row) =>
{row.getValue()}
, + }), + columnHelper.accessor("issueDate", { + header: "Issue Date", + cell: (row) => ( +
+ {new Date(row.getValue()).toLocaleDateString()}
), - }, - { + }), + columnHelper.display({ id: "Documents", + enableSorting: false, enableHiding: false, - header: ({ column }) => { - return ( - column.toggleSorting(column.getIsSorted() === "asc")} - /> - ); - }, + header: "Documents", cell: ({ row }) => { const openFileOnTab = async (key: string) => { const fileUrl = await getPresignedGetUrl(key); @@ -235,9 +160,10 @@ export const columns: ColumnDef[] = [ ); }, - }, - { + }), + columnHelper.display({ id: "actions", + enableSorting: false, enableHiding: false, cell: ({ row }) => { const router = useRouter(); @@ -289,38 +215,13 @@ export const columns: ColumnDef[] = [ ); }, - }, + }), ]; export const SafeTable = ({ safes }: SafesType) => { - const [sorting, setSorting] = React.useState([]); - const [columnFilters, setColumnFilters] = React.useState( - [], - ); - const [columnVisibility, setColumnVisibility] = - React.useState({}); - const [rowSelection, setRowSelection] = React.useState({}); - - const table = useReactTable({ + const table = useDataTable({ data: safes ?? [], columns: columns, - enableRowSelection: true, - onRowSelectionChange: setRowSelection, - onSortingChange: setSorting, - onColumnFiltersChange: setColumnFilters, - onColumnVisibilityChange: setColumnVisibility, - getCoreRowModel: getCoreRowModel(), - getFilteredRowModel: getFilteredRowModel(), - getPaginationRowModel: getPaginationRowModel(), - getSortedRowModel: getSortedRowModel(), - getFacetedRowModel: getFacetedRowModel(), - getFacetedUniqueValues: getFacetedUniqueValues(), - state: { - sorting, - columnFilters, - columnVisibility, - rowSelection, - }, }); return ( diff --git a/src/components/safe/safe-table/safe-table-toolbar.tsx b/src/components/safe/safe-table/safe-table-toolbar.tsx index d011eff02..b84e26c83 100644 --- a/src/components/safe/safe-table/safe-table-toolbar.tsx +++ b/src/components/safe/safe-table/safe-table-toolbar.tsx @@ -1,4 +1,4 @@ -import { useDataTable } from "@/components/ui/data-table/data-table"; +import { useTable } from "@/components/ui/data-table/data-table"; import { ResetButton } from "@/components/ui/data-table/data-table-buttons"; import { DataTableFacetedFilter } from "@/components/ui/data-table/data-table-faceted-filter"; import { DataTableViewOptions } from "@/components/ui/data-table/data-table-view-options"; @@ -6,7 +6,7 @@ import { Input } from "@/components/ui/input"; import { statusValues } from "./data"; export function SafeTableToolbar() { - const { table } = useDataTable(); + const { table } = useTable(); const isFiltered = table.getState().columnFilters.length > 0; return ( diff --git a/src/components/safe/steps/investor-details/index.tsx b/src/components/safe/steps/investor-details/index.tsx index 2a952eb61..9f299ce1f 100644 --- a/src/components/safe/steps/investor-details/index.tsx +++ b/src/components/safe/steps/investor-details/index.tsx @@ -5,6 +5,6 @@ import { InvestorDetailsForm } from "./form"; export { type TFormSchema } from "./form"; export async function InvestorDetails() { - const stakeholders = await api.stakeholder.getStakeholders.query(); + const stakeholders = await api.stakeholder.getStakeholders(); return ; } diff --git a/src/components/securities/options/option-table-toolbar.tsx b/src/components/securities/options/option-table-toolbar.tsx index 09dca0dc3..81d391532 100644 --- a/src/components/securities/options/option-table-toolbar.tsx +++ b/src/components/securities/options/option-table-toolbar.tsx @@ -1,4 +1,4 @@ -import { useDataTable } from "@/components/ui/data-table/data-table"; +import { useTable } from "@/components/ui/data-table/data-table"; import { ResetButton } from "@/components/ui/data-table/data-table-buttons"; import { DataTableFacetedFilter } from "@/components/ui/data-table/data-table-faceted-filter"; import { DataTableViewOptions } from "@/components/ui/data-table/data-table-view-options"; @@ -6,7 +6,7 @@ import { Input } from "@/components/ui/input"; import { statusValues } from "./data"; export function OptionTableToolbar() { - const { table } = useDataTable(); + const { table } = useTable(); const isFiltered = table.getState().columnFilters.length > 0; return ( diff --git a/src/components/securities/options/option-table.tsx b/src/components/securities/options/option-table.tsx index d27612608..2e84e78cd 100644 --- a/src/components/securities/options/option-table.tsx +++ b/src/components/securities/options/option-table.tsx @@ -1,18 +1,6 @@ "use client"; -import { - type ColumnDef, - type ColumnFiltersState, - type SortingState, - type VisibilityState, - getCoreRowModel, - getFacetedRowModel, - getFacetedUniqueValues, - getFilteredRowModel, - getPaginationRowModel, - getSortedRowModel, - useReactTable, -} from "@tanstack/react-table"; +import { type ColumnDef, createColumnHelper } from "@tanstack/react-table"; import * as React from "react"; import { Button } from "@/components/ui/button"; @@ -31,10 +19,10 @@ import { api } from "@/trpc/react"; import { Avatar, AvatarImage } from "@/components/ui/avatar"; import { DataTable } from "@/components/ui/data-table/data-table"; import { DataTableBody } from "@/components/ui/data-table/data-table-body"; -import { SortButton } from "@/components/ui/data-table/data-table-buttons"; import { DataTableContent } from "@/components/ui/data-table/data-table-content"; import { DataTableHeader } from "@/components/ui/data-table/data-table-header"; import { DataTablePagination } from "@/components/ui/data-table/data-table-pagination"; +import { useDataTable } from "@/hooks/use-data-table"; import { getPresignedGetUrl } from "@/server/file-uploads"; import type { RouterOutputs } from "@/trpc/shared"; import { RiFileDownloadLine, RiMore2Fill } from "@remixicon/react"; @@ -48,8 +36,10 @@ type OptionsType = { options: Option; }; -export const columns: ColumnDef[] = [ - { +const columnHelper = createColumnHelper(); + +export const columns = [ + columnHelper.display({ id: "select", header: ({ table }) => ( [] = [ ), enableSorting: false, enableHiding: false, - }, - { - id: "stakeholderName", - header: ({ column }) => { - return ( - column.toggleSorting(column.getIsSorted() === "asc")} - /> - ); - }, - accessorFn: (row) => row?.stakeholder?.name, - cell: ({ row }) => ( + }), + columnHelper.accessor("stakeholder.name", { + header: "Stakeholder", + cell: (row) => (
-

{row?.original?.stakeholder?.name}

+

{row.getValue()}

), - }, - { - accessorKey: "quantity", - header: ({ column }) => { - return ( - column.toggleSorting(column.getIsSorted() === "asc")} - /> - ); - }, - cell: ({ row }) => ( -
{row.getValue("quantity")}
- ), - }, - { - accessorKey: "grantId", - header: ({ column }) => { - return ( - column.toggleSorting(column.getIsSorted() === "asc")} - /> - ); - }, - cell: ({ row }) => ( -
{row.getValue("grantId")}
- ), - }, - { - accessorKey: "exercisePrice", - header: ({ column }) => { - return ( - column.toggleSorting(column.getIsSorted() === "asc")} - /> - ); - }, - cell: ({ row }) => ( -
{row.getValue("exercisePrice")}
- ), - }, - { - accessorKey: "type", - header: ({ column }) => { - return ( - column.toggleSorting(column.getIsSorted() === "asc")} - /> - ); - }, - cell: ({ row }) =>
{row.getValue("type")}
, - }, - { - accessorKey: "status", - header: ({ column }) => { - return ( - column.toggleSorting(column.getIsSorted() === "asc")} - /> - ); - }, - cell: ({ row }) => ( -
{row.getValue("status")}
- ), - }, - { - accessorKey: "issueDate", - header: ({ column }) => { - return ( - column.toggleSorting(column.getIsSorted() === "asc")} - /> - ); - }, - cell: ({ row }) => ( -
- {new Date().toLocaleDateString(row.getValue("issueDate"))} + }), + columnHelper.accessor("quantity", { + header: "Quantity", + cell: (row) =>
{row.getValue()}
, + }), + columnHelper.accessor("grantId", { + header: "GrantId", + cell: (row) =>
{row.getValue()}
, + }), + columnHelper.accessor("exercisePrice", { + header: "Exercise Price", + cell: (row) =>
{row.getValue()}
, + }), + columnHelper.accessor("type", { + header: "Type", + cell: (row) =>
{row.getValue()}
, + }), + columnHelper.accessor("status", { + header: "Status", + cell: (row) =>
{row.getValue()}
, + }), + columnHelper.accessor("issueDate", { + header: "Issue Date", + cell: (row) => ( +
+ {new Date(row.getValue()).toLocaleDateString()}
), - }, - { + }), + columnHelper.display({ id: "Documents", enableHiding: false, - header: ({ column }) => { - return ( - column.toggleSorting(column.getIsSorted() === "asc")} - /> - ); - }, + enableSorting: false, + header: "Documents", cell: ({ row }) => { const openFileOnTab = async (key: string) => { const fileUrl = await getPresignedGetUrl(key); @@ -227,10 +146,11 @@ export const columns: ColumnDef[] = [ ); }, - }, - { + }), + columnHelper.display({ id: "actions", enableHiding: false, + enableSorting: false, cell: ({ row }) => { const router = useRouter(); const option = row.original; @@ -281,38 +201,13 @@ export const columns: ColumnDef[] = [ ); }, - }, + }), ]; const OptionTable = ({ options }: OptionsType) => { - const [sorting, setSorting] = React.useState([]); - const [columnFilters, setColumnFilters] = React.useState( - [], - ); - const [columnVisibility, setColumnVisibility] = - React.useState({}); - const [rowSelection, setRowSelection] = React.useState({}); - - const table = useReactTable({ + const table = useDataTable({ data: options ?? [], columns: columns, - enableRowSelection: true, - onRowSelectionChange: setRowSelection, - onSortingChange: setSorting, - onColumnFiltersChange: setColumnFilters, - onColumnVisibilityChange: setColumnVisibility, - getCoreRowModel: getCoreRowModel(), - getFilteredRowModel: getFilteredRowModel(), - getPaginationRowModel: getPaginationRowModel(), - getSortedRowModel: getSortedRowModel(), - getFacetedRowModel: getFacetedRowModel(), - getFacetedUniqueValues: getFacetedUniqueValues(), - state: { - sorting, - columnFilters, - columnVisibility, - rowSelection, - }, }); return ( diff --git a/src/components/securities/shares/share-modal.tsx b/src/components/securities/shares/share-modal.tsx index cfebd90d0..3605d220d 100644 --- a/src/components/securities/shares/share-modal.tsx +++ b/src/components/securities/shares/share-modal.tsx @@ -12,12 +12,12 @@ import { GeneralDetails } from "./steps/general-details"; import { RelevantDates } from "./steps/relevant-dates"; async function ContributionDetailsStep() { - const stakeholders = await api.stakeholder.getStakeholders.query(); + const stakeholders = await api.stakeholder.getStakeholders(); return ; } async function GeneralDetailsStep() { - const shareClasses = await api.shareClass.get.query(); + const shareClasses = await api.shareClass.get(); return ; } diff --git a/src/components/securities/shares/share-table-toolbar.tsx b/src/components/securities/shares/share-table-toolbar.tsx index 7836bd58c..ea636e4c4 100644 --- a/src/components/securities/shares/share-table-toolbar.tsx +++ b/src/components/securities/shares/share-table-toolbar.tsx @@ -1,4 +1,4 @@ -import { useDataTable } from "@/components/ui/data-table/data-table"; +import { useTable } from "@/components/ui/data-table/data-table"; import { ResetButton } from "@/components/ui/data-table/data-table-buttons"; import { DataTableFacetedFilter } from "@/components/ui/data-table/data-table-faceted-filter"; import { DataTableViewOptions } from "@/components/ui/data-table/data-table-view-options"; @@ -6,7 +6,7 @@ import { Input } from "@/components/ui/input"; import { statusValues } from "./data"; export function ShareTableToolbar() { - const { table } = useDataTable(); + const { table } = useTable(); const isFiltered = table.getState().columnFilters.length > 0; return ( diff --git a/src/components/securities/shares/share-table.tsx b/src/components/securities/shares/share-table.tsx index d511337ab..72768750d 100644 --- a/src/components/securities/shares/share-table.tsx +++ b/src/components/securities/shares/share-table.tsx @@ -1,18 +1,6 @@ "use client"; -import { - type ColumnDef, - type ColumnFiltersState, - type SortingState, - type VisibilityState, - getCoreRowModel, - getFacetedRowModel, - getFacetedUniqueValues, - getFilteredRowModel, - getPaginationRowModel, - getSortedRowModel, - useReactTable, -} from "@tanstack/react-table"; +import { type ColumnDef, createColumnHelper } from "@tanstack/react-table"; import * as React from "react"; import { dayjsExt } from "@/common/dayjs"; @@ -27,14 +15,15 @@ import { DataTablePagination } from "@/components/ui/data-table/data-table-pagin import type { RouterOutputs } from "@/trpc/shared"; import { Button } from "@/components/ui/button"; -import { SortButton } from "@/components/ui/data-table/data-table-buttons"; import { DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, } from "@/components/ui/dropdown-menu"; -import { formatCurrency, formatNumber } from "@/lib/utils"; +import { useDataTable } from "@/hooks/use-data-table"; +import { cn, formatCurrency, formatNumber } from "@/lib/utils"; +import type { SecuritiesStatusEnum } from "@/prisma/enums"; import { getPresignedGetUrl } from "@/server/file-uploads"; import { api } from "@/trpc/react"; import { @@ -52,38 +41,24 @@ type SharesType = { shares: Share; }; -const humanizeShareStatus = (type: string) => { - switch (type) { - case "ACTIVE": - return "Active"; - case "DRAFT": - return "Draft"; - case "SIGNED": - return "Signed"; - case "PENDING": - return "Pending"; - default: - return ""; - } +const statusHumanizeMap: Record = { + ACTIVE: "Active", + DRAFT: "Draft", + SIGNED: "Signed", + PENDING: "Pending", }; -const StatusColorProvider = (type: string) => { - switch (type) { - case "ACTIVE": - return "bg-green-50 text-green-600 ring-green-600/20"; - case "DRAFT": - return "bg-yellow-50 text-yellow-600 ring-yellow-600/20"; - case "SIGNED": - return "bg-blue-50 text-blue-600 ring-blue-600/20"; - case "PENDING": - return "bg-gray-50 text-gray-600 ring-gray-600/20"; - default: - return ""; - } +const statusColorMap: Record = { + ACTIVE: "bg-green-50 text-green-600 ring-green-600/20", + DRAFT: "bg-yellow-50 text-yellow-600 ring-yellow-600/20", + SIGNED: "bg-blue-50 text-blue-600 ring-blue-600/20", + PENDING: "bg-gray-50 text-gray-600 ring-gray-600/20", }; -export const columns: ColumnDef[] = [ - { +const columnHelper = createColumnHelper(); + +const columns = [ + columnHelper.display({ id: "select", header: ({ table }) => ( [] = [ ), enableSorting: false, enableHiding: false, - }, - { - id: "stakeholderName", - accessorKey: "stakeholder.name", - header: ({ column }) => { - return ( - column.toggleSorting(column.getIsSorted() === "asc")} - /> - ); - }, - cell: ({ row }) => ( + }), + columnHelper.accessor("stakeholder.name", { + header: "Stakeholder", + cell: (row) => (
-

{row?.original?.stakeholder?.name}

+

{row.getValue()}

), - }, - { - id: "status", - accessorKey: "status", - header: ({ column }) => ( - column.toggleSorting(column?.getIsSorted() === "asc")} - /> - ), - cell: ({ row }) => { - const status = row.original?.status; + }), + columnHelper.accessor("status", { + header: "Status", + cell: (row) => { + const status = row.getValue(); return ( - {humanizeShareStatus(status)} + {statusHumanizeMap[status]} ); }, - }, - { - id: "shareClass", - accessorKey: "shareClass.classType", - - header: ({ column }) => ( - column.toggleSorting(column.getIsSorted() === "asc")} - /> - ), - cell: ({ row }) => ( -
{row.original.shareClass.classType}
- ), - }, - { - id: "quantity", - accessorKey: "quantity", - header: ({ column }) => ( -
- column.toggleSorting(column.getIsSorted() === "asc")} - /> -
- ), - cell: ({ row }) => { - const quantity = row.original.quantity; + }), + columnHelper.accessor("shareClass.classType", { + header: "Share class", + cell: (row) =>
{row.getValue()}
, + }), + columnHelper.accessor("quantity", { + header: "Quantity", + cell: (row) => { + const quantity = row.getValue(); return (
{quantity ? formatNumber(quantity) : null}
); }, - }, - { - id: "pricePerShare", - accessorKey: "pricePerShare", - header: ({ column }) => ( -
- column.toggleSorting(column.getIsSorted() === "asc")} - /> -
- ), - cell: ({ row }) => { - const price = row.original.pricePerShare; + }), + columnHelper.accessor("pricePerShare", { + header: "Unit price", + cell: (row) => { + const price = row.getValue(); return (
{price ? formatCurrency(price, "USD") : null}
); }, - }, - { - id: "issueDate", - accessorKey: "issueDate", - header: ({ column }) => ( - column.toggleSorting(column.getIsSorted() === "asc")} - /> - ), - cell: ({ row }) => ( -
- {dayjsExt(row.original.issueDate).format("DD/MM/YYYY")} + }), + columnHelper.accessor("issueDate", { + header: "Issued", + cell: (row) => ( +
+ {dayjsExt(row.getValue()).format("DD/MM/YYYY")}
), - }, - { - id: "boardApprovalDate", - accessorKey: "boardApprovalDate", - header: ({ column }) => ( -
- column.toggleSorting(column.getIsSorted() === "asc")} - /> + }), + columnHelper.accessor("boardApprovalDate", { + header: "Board Approved", + cell: (row) => ( +
+ {dayjsExt(row.getValue()).format("DD/MM/YYYY")}
), - cell: ({ row }) => ( -
- {dayjsExt(row.original.boardApprovalDate).format("DD/MM/YYYY")} -
- ), - }, - { + }), + columnHelper.display({ id: "Documents", enableHiding: false, - header: ({ column }) => { - return ( - column.toggleSorting(column.getIsSorted() === "asc")} - /> - ); - }, + enableSorting: false, + header: "Board Approved", cell: ({ row }) => { const documents = row?.original?.documents; @@ -287,10 +198,12 @@ export const columns: ColumnDef[] = [ ); }, - }, - { + }), + + columnHelper.display({ id: "actions", enableHiding: false, + enableSorting: false, cell: ({ row }) => { // eslint-disable-next-line react-hooks/rules-of-hooks const router = useRouter(); @@ -343,38 +256,13 @@ export const columns: ColumnDef[] = [ ); }, - }, + }), ]; const ShareTable = ({ shares }: SharesType) => { - const [sorting, setSorting] = React.useState([]); - const [columnFilters, setColumnFilters] = React.useState( - [], - ); - const [columnVisibility, setColumnVisibility] = - React.useState({}); - const [rowSelection, setRowSelection] = React.useState({}); - - const table = useReactTable({ + const table = useDataTable({ data: shares ?? [], columns: columns, - enableRowSelection: true, - onRowSelectionChange: setRowSelection, - onSortingChange: setSorting, - onColumnFiltersChange: setColumnFilters, - onColumnVisibilityChange: setColumnVisibility, - getCoreRowModel: getCoreRowModel(), - getFilteredRowModel: getFilteredRowModel(), - getPaginationRowModel: getPaginationRowModel(), - getSortedRowModel: getSortedRowModel(), - getFacetedRowModel: getFacetedRowModel(), - getFacetedUniqueValues: getFacetedUniqueValues(), - state: { - sorting, - columnFilters, - columnVisibility, - rowSelection, - }, }); return ( diff --git a/src/components/security/passkey/passkey-table-toolbar.tsx b/src/components/security/passkey/passkey-table-toolbar.tsx index 74f0d24ac..160a3a502 100644 --- a/src/components/security/passkey/passkey-table-toolbar.tsx +++ b/src/components/security/passkey/passkey-table-toolbar.tsx @@ -1,4 +1,4 @@ -import { useDataTable } from "@/components/ui/data-table/data-table"; +import { useTable } from "@/components/ui/data-table/data-table"; import { ResetButton } from "@/components/ui/data-table/data-table-buttons"; import { DataTableFacetedFilter } from "@/components/ui/data-table/data-table-faceted-filter"; import { DataTableViewOptions } from "@/components/ui/data-table/data-table-view-options"; @@ -6,7 +6,7 @@ import { Input } from "@/components/ui/input"; import { statusValues } from "./data"; export function PasskeyTableToolbar() { - const { table } = useDataTable(); + const { table } = useTable(); const isFiltered = table.getState().columnFilters.length > 0; return ( diff --git a/src/components/security/passkey/user-passkeys-data-table.tsx b/src/components/security/passkey/user-passkeys-data-table.tsx index ee09f23be..dd2e315d6 100644 --- a/src/components/security/passkey/user-passkeys-data-table.tsx +++ b/src/components/security/passkey/user-passkeys-data-table.tsx @@ -1,33 +1,19 @@ "use client"; +import { dayjsExt } from "@/common/dayjs"; import { Badge } from "@/components/ui/badge"; +import { DataTable } from "@/components/ui/data-table/data-table"; import { DataTableBody } from "@/components/ui/data-table/data-table-body"; -import { SortButton } from "@/components/ui/data-table/data-table-buttons"; import { DataTableContent } from "@/components/ui/data-table/data-table-content"; import { DataTableHeader } from "@/components/ui/data-table/data-table-header"; -import type { RouterOutputs } from "@/trpc/shared"; -import * as React from "react"; - -import { - type ColumnDef, - type ColumnFiltersState, - type SortingState, - type VisibilityState, - getCoreRowModel, - getFacetedRowModel, - getFacetedUniqueValues, - getFilteredRowModel, - getPaginationRowModel, - getSortedRowModel, - useReactTable, -} from "@tanstack/react-table"; - -import { dayjsExt } from "@/common/dayjs"; -import { DataTable } from "@/components/ui/data-table/data-table"; import { DataTablePagination } from "@/components/ui/data-table/data-table-pagination"; +import { useDataTable } from "@/hooks/use-data-table"; import { api } from "@/trpc/react"; +import type { RouterOutputs } from "@/trpc/shared"; import { RiMore2Fill } from "@remixicon/react"; +import { type ColumnDef, createColumnHelper } from "@tanstack/react-table"; import { useRouter } from "next/navigation"; +import * as React from "react"; import { toast } from "sonner"; import { Button } from "../../ui/button"; import { @@ -60,94 +46,52 @@ const humanizeDeviceType = (type: string) => { } }; -export const columns: ColumnDef[] = [ - { - accessorKey: "name", - header: ({ column }) => { - return ( - column.toggleSorting(column.getIsSorted() === "asc")} - /> - ); - }, - cell: ({ row }) =>
{row.getValue("name")}
, - }, - { - accessorKey: "credentialDeviceType", - header: ({ column }) => { - return ( - column.toggleSorting(column.getIsSorted() === "asc")} - /> - ); - }, - cell: ({ row }) => ( -
- {humanizeDeviceType(row.getValue("credentialDeviceType"))} -
+const columnHelper = createColumnHelper(); + +const columns = [ + columnHelper.accessor("name", { + header: "Device Name", + cell: (row) =>
{row.getValue()}
, + }), + columnHelper.accessor("credentialDeviceType", { + header: "Device type", + cell: (row) => ( +
{humanizeDeviceType(row.getValue())}
), - }, - { - accessorKey: "credentialBackedUp", - header: ({ column }) => { - return ( - column.toggleSorting(column.getIsSorted() === "asc")} - /> - ); - }, - cell: ({ row }) => ( + }), + columnHelper.accessor("credentialBackedUp", { + header: "Backup", + cell: (row) => (
- {row.getValue("credentialBackedUp") ? "Done" : "Pending"} + {row.getValue() ? "Done" : "Pending"}
), - }, - { - accessorKey: "createdAt", - header: ({ column }) => { - return ( - column.toggleSorting(column.getIsSorted() === "asc")} - /> - ); - }, - cell: ({ row }) => ( -
- {dayjsExt(row.getValue("createdAt")).fromNow()} + }), + columnHelper.accessor("createdAt", { + header: "Created", + cell: (row) => ( +
+ {dayjsExt(row.getValue()).fromNow()}
), - }, - - { - accessorKey: "lastUsedAt", - header: ({ column }) => { - return ( - column.toggleSorting(column.getIsSorted() === "asc")} - /> - ); - }, - cell: ({ row }) => ( -
- {row.getValue("lastUsedAt") - ? dayjsExt(row.getValue("lastUsedAt")).fromNow() - : "Not yet"} + }), + columnHelper.accessor("lastUsedAt", { + header: "Used", + cell: (row) => ( +
+ {row.getValue() ? dayjsExt(row.getValue()).fromNow() : "Not yet"}
), - }, - { + }), + columnHelper.display({ id: "actions", enableHiding: false, + enableSorting: false, cell: ({ row }) => { const [open, setOpen] = React.useState(false); const router = useRouter(); @@ -213,38 +157,13 @@ export const columns: ColumnDef[] = [ ); }, - }, + }), ]; const PasskeyTable = ({ passkey }: PasskeyType) => { - const [sorting, setSorting] = React.useState([]); - const [columnFilters, setColumnFilters] = React.useState( - [], - ); - const [columnVisibility, setColumnVisibility] = - React.useState({}); - const [rowSelection, setRowSelection] = React.useState({}); - - const table = useReactTable({ + const table = useDataTable({ data: passkey ?? [], columns: columns, - enableRowSelection: true, - onRowSelectionChange: setRowSelection, - onSortingChange: setSorting, - onColumnFiltersChange: setColumnFilters, - onColumnVisibilityChange: setColumnVisibility, - getCoreRowModel: getCoreRowModel(), - getFilteredRowModel: getFilteredRowModel(), - getPaginationRowModel: getPaginationRowModel(), - getSortedRowModel: getSortedRowModel(), - getFacetedRowModel: getFacetedRowModel(), - getFacetedUniqueValues: getFacetedUniqueValues(), - state: { - sorting, - columnFilters, - columnVisibility, - rowSelection, - }, }); return ( diff --git a/src/components/stakeholder/stakeholder-table-toolbar.tsx b/src/components/stakeholder/stakeholder-table-toolbar.tsx index 4e57ff3e3..38a1b100f 100644 --- a/src/components/stakeholder/stakeholder-table-toolbar.tsx +++ b/src/components/stakeholder/stakeholder-table-toolbar.tsx @@ -1,23 +1,22 @@ -import { statusValues } from "@/components/member/data"; -import { useDataTable } from "@/components/ui/data-table/data-table"; +"use client"; + +import { useTable } from "@/components/ui/data-table/data-table"; import { ResetButton } from "@/components/ui/data-table/data-table-buttons"; -import { DataTableFacetedFilter } from "@/components/ui/data-table/data-table-faceted-filter"; import { DataTableViewOptions } from "@/components/ui/data-table/data-table-view-options"; -import { Input } from "@/components/ui/input"; +import { DebouncedInput } from "../ui/debounced-input"; export const StakeholderTableToolbar = () => { - const { table } = useDataTable(); + const { table } = useTable(); const isFiltered = table.getState().columnFilters.length > 0; + const value = table.getColumn("name")?.getFilterValue() as string; return (
- - table.getColumn("name")?.setFilterValue(event.target.value) - } + table.getColumn("name")?.setFilterValue(value)} className="h-8 w-64" />
diff --git a/src/components/stakeholder/stakeholder-table.tsx b/src/components/stakeholder/stakeholder-table.tsx index 5d72cacb8..05686a01d 100644 --- a/src/components/stakeholder/stakeholder-table.tsx +++ b/src/components/stakeholder/stakeholder-table.tsx @@ -2,31 +2,20 @@ import { pushModal } from "@/components/modals"; import { Badge } from "@/components/ui/badge"; -import { Checkbox } from "@/components/ui/checkbox"; import { DataTable } from "@/components/ui/data-table/data-table"; import { DataTableBody } from "@/components/ui/data-table/data-table-body"; -import { SortButton } from "@/components/ui/data-table/data-table-buttons"; import { DataTableContent } from "@/components/ui/data-table/data-table-content"; import { DataTableHeader } from "@/components/ui/data-table/data-table-header"; import { DataTablePagination } from "@/components/ui/data-table/data-table-pagination"; -import type { RouterOutputs } from "@/trpc/shared"; +import { usePaginatedTable } from "@/hooks/use-paginated-data-table"; +import type { TGetManyStakeholderRes } from "@/server/api/client-handlers/stakeholder"; +import type { TManyStakeholderQuerySchema } from "@/server/api/schema/stakeholder"; import { RiMore2Fill } from "@remixicon/react"; -import { - type ColumnDef, - type ColumnFiltersState, - type SortingState, - type VisibilityState, - getCoreRowModel, - getFacetedRowModel, - getFacetedUniqueValues, - getFilteredRowModel, - getPaginationRowModel, - getSortedRowModel, - useReactTable, -} from "@tanstack/react-table"; -import React from "react"; +import { createColumnHelper } from "@tanstack/react-table"; + import { Allow } from "../rbac/allow"; import { Button } from "../ui/button"; +import { Card } from "../ui/card"; import { DropdownMenu, DropdownMenuContent, @@ -37,11 +26,9 @@ import { } from "../ui/dropdown-menu"; import { StakeholderTableToolbar } from "./stakeholder-table-toolbar"; -type Stakeholder = RouterOutputs["stakeholder"]["getStakeholders"]; - type StakeholderTableType = { - stakeholders: Stakeholder; -}; + stakeholders: TGetManyStakeholderRes; +} & TManyStakeholderQuerySchema; const getStakeholderType = (type: string) => { switch (type) { @@ -83,105 +70,65 @@ const getCurrentRelationship = (relationship: string) => { } }; -export const columns: ColumnDef[] = [ - { - id: "select", - header: ({ table }) => ( - table.toggleAllPageRowsSelected(!!value)} - aria-label="Select all" - /> - ), - cell: ({ row }) => ( - row.toggleSelected(!!value)} - aria-label="Select row" - /> - ), +const columnHelper = + createColumnHelper(); + +const columns = [ + // columnHelper.display({ + // id: "select", + // header: ({ table }) => ( + // table.toggleAllPageRowsSelected(!!value)} + // aria-label="Select all" + // /> + // ), + // cell: ({ row }) => ( + // row.toggleSelected(!!value)} + // aria-label="Select row" + // /> + // ), + // enableHiding: false, + // enableSorting: false, + // }), + columnHelper.accessor("name", { + header: "Name", + cell: (row) =>
{row.getValue()}
, + }), + columnHelper.accessor("email", { + header: "Email", + cell: (info) => info.getValue(), + }), + columnHelper.accessor("institutionName", { + header: "Institute name", + cell: (info) => info.getValue(), enableSorting: false, - enableHiding: false, - }, - { - id: "name", - header: ({ column }) => { - return ( - column.toggleSorting(column.getIsSorted() === "asc")} - /> - ); - }, - accessorFn: (row) => row.name, - cell: ({ row }) => ( -
{row.getValue("name")}
- ), - }, - { - accessorKey: "email", - header: ({ column }) => { - return ( - column.toggleSorting(column.getIsSorted() === "asc")} - /> - ); - }, - cell: ({ row }) =>
{row.getValue("email")}
, - }, - { - accessorKey: "Institution name", - header: ({ column }) => { - return ( - column.toggleSorting(column.getIsSorted() === "asc")} - /> - ); - }, - cell: ({ row }) =>
{row.original.institutionName ?? ""}
, - }, - { - accessorKey: "Type", - header: ({ column }) => { - return ( - column.toggleSorting(column.getIsSorted() === "asc")} - /> - ); - }, - cell: ({ row }) => { - const type = row.original.stakeholderType as string; + }), + columnHelper.accessor("stakeholderType", { + header: "Type", + cell: (info) => { + const type = info.getValue(); return ( {getStakeholderType(type)} ); }, - }, - { - accessorKey: "Association", - header: ({ column }) => { - return ( - column.toggleSorting(column.getIsSorted() === "asc")} - /> - ); - }, - cell: ({ row }) => { - return ( -
{getCurrentRelationship(row.original.currentRelationship)}
- ); - }, - }, - { - id: "actions", + enableSorting: false, + }), + columnHelper.accessor("currentRelationship", { + header: "Association", + cell: (info) => getCurrentRelationship(info.getValue()), + enableSorting: false, + }), + + columnHelper.display({ + id: "Actions", enableHiding: false, cell: ({ row }) => { const singleStakeholder = row.original; @@ -214,51 +161,39 @@ export const columns: ColumnDef[] = [ ); }, - }, + }), ]; -const StakeholderTable = ({ stakeholders }: StakeholderTableType) => { - const [sorting, setSorting] = React.useState([]); - const [columnFilters, setColumnFilters] = React.useState( - [], - ); - const [columnVisibility, setColumnVisibility] = - React.useState({}); - const [rowSelection, setRowSelection] = React.useState({}); - - const table = useReactTable({ - data: stakeholders, - columns: columns, - enableRowSelection: true, - onRowSelectionChange: setRowSelection, - onSortingChange: setSorting, - onColumnFiltersChange: setColumnFilters, - onColumnVisibilityChange: setColumnVisibility, - getCoreRowModel: getCoreRowModel(), - getFilteredRowModel: getFilteredRowModel(), - getPaginationRowModel: getPaginationRowModel(), - getSortedRowModel: getSortedRowModel(), - getFacetedRowModel: getFacetedRowModel(), - getFacetedUniqueValues: getFacetedUniqueValues(), - state: { - sorting, - columnFilters, - columnVisibility, - rowSelection, - }, +const StakeholderTable = ({ + stakeholders, + page, + limit, + sort, + name, +}: StakeholderTableType) => { + const table = usePaginatedTable({ + pageCount: stakeholders?.meta?.pageCount ?? -1, + columns, + data: stakeholders?.data ?? [], + limit, + page, + sort, + filterFields: [{ id: "name", value: name }], }); return ( -
- - - - - - - - -
+ +
+ + + + + + + + +
+
); }; diff --git a/src/components/stakeholder/stakeholder-uploader.tsx b/src/components/stakeholder/stakeholder-uploader.tsx index aafa4d023..d5194f78e 100644 --- a/src/components/stakeholder/stakeholder-uploader.tsx +++ b/src/components/stakeholder/stakeholder-uploader.tsx @@ -17,7 +17,7 @@ const StakeholderUploader = () => { const router = useRouter(); - const { mutateAsync, isLoading } = + const { mutateAsync, isPending: isLoading } = api.stakeholder.addStakeholders.useMutation({ onSuccess: ({ success, message }) => { if (success) { diff --git a/src/components/ui/data-table/data-table-body.tsx b/src/components/ui/data-table/data-table-body.tsx index 1323bc106..9a77727ba 100644 --- a/src/components/ui/data-table/data-table-body.tsx +++ b/src/components/ui/data-table/data-table-body.tsx @@ -1,9 +1,9 @@ import { flexRender } from "@tanstack/react-table"; import { TableBody, TableCell, TableRow } from "../table"; -import { useDataTable } from "./data-table"; +import { useTable } from "./data-table"; export function DataTableBody() { - const { table } = useDataTable(); + const { table } = useTable(); const columnLength = table.getAllColumns().length; diff --git a/src/components/ui/data-table/data-table-buttons.tsx b/src/components/ui/data-table/data-table-buttons.tsx index e3123a8f3..16fb2294d 100644 --- a/src/components/ui/data-table/data-table-buttons.tsx +++ b/src/components/ui/data-table/data-table-buttons.tsx @@ -1,21 +1,7 @@ import { cn } from "@/lib/utils"; -import { RiCloseLine, RiExpandUpDownLine } from "@remixicon/react"; +import { RiCloseLine } from "@remixicon/react"; import { Button, type ButtonProps } from "../button"; -interface SortButtonProps - extends Omit, "children"> { - label: string; -} - -export function SortButton({ label, className, ...rest }: SortButtonProps) { - return ( - - ); -} - type ResetButtonProps = Omit; export function ResetButton({ className, ...rest }: ResetButtonProps) { diff --git a/src/components/ui/data-table/data-table-header.tsx b/src/components/ui/data-table/data-table-header.tsx index 34cf2de31..5d66d3d1c 100644 --- a/src/components/ui/data-table/data-table-header.tsx +++ b/src/components/ui/data-table/data-table-header.tsx @@ -1,10 +1,47 @@ -import { flexRender } from "@tanstack/react-table"; +import { type Header, flexRender } from "@tanstack/react-table"; import { TableHead, TableHeader, TableRow } from "../table"; -import { useDataTable } from "./data-table"; +import { + RiArrowDownSLine, + RiArrowUpSLine, + RiExpandUpDownLine, +} from "@remixicon/react"; +import { useTable } from "./data-table"; + +// biome-ignore lint/suspicious/noExplicitAny: +function HeaderItem({ header }: { header: Header }) { + const canSort = header.column.getCanSort(); + const Element = canSort ? "button" : "div"; + + const label = canSort + ? header.column.getNextSortingOrder() === "asc" + ? "Sort ascending" + : header.column.getNextSortingOrder() === "desc" + ? "Sort descending" + : "Clear sort" + : undefined; + return ( + + {flexRender(header.column.columnDef.header, header.getContext())} + {{ + asc: , + desc: , + }[header.column.getIsSorted() as string] ?? null} + + {canSort && !header.column.getIsSorted() ? ( + + ) : null} + + ); +} export function DataTableHeader() { - const { table } = useDataTable(); + const { table } = useTable(); return ( {table.getHeaderGroups().map((headerGroup) => ( @@ -12,12 +49,7 @@ export function DataTableHeader() { {headerGroup.headers.map((header) => { return ( - {header.isPlaceholder - ? null - : flexRender( - header.column.columnDef.header, - header.getContext(), - )} + {header.isPlaceholder ? null : } ); })} diff --git a/src/components/ui/data-table/data-table-pagination.tsx b/src/components/ui/data-table/data-table-pagination.tsx index 8e170fbe6..1e47a60a5 100644 --- a/src/components/ui/data-table/data-table-pagination.tsx +++ b/src/components/ui/data-table/data-table-pagination.tsx @@ -1,8 +1,8 @@ import { Button } from "../button"; -import { useDataTable } from "./data-table"; +import { useTable } from "./data-table"; export function DataTablePagination() { - const { table } = useDataTable(); + const { table } = useTable(); return (
diff --git a/src/components/ui/data-table/data-table-view-options.tsx b/src/components/ui/data-table/data-table-view-options.tsx index bb9703714..ac9b71d6a 100644 --- a/src/components/ui/data-table/data-table-view-options.tsx +++ b/src/components/ui/data-table/data-table-view-options.tsx @@ -9,10 +9,10 @@ import { } from "../dropdown-menu"; import { RiArrowDownSLine } from "@remixicon/react"; -import { useDataTable } from "./data-table"; +import { useTable } from "./data-table"; export function DataTableViewOptions() { - const { table } = useDataTable(); + const { table } = useTable(); return ( diff --git a/src/components/ui/data-table/data-table.tsx b/src/components/ui/data-table/data-table.tsx index dced417f7..5b9af7fbf 100644 --- a/src/components/ui/data-table/data-table.tsx +++ b/src/components/ui/data-table/data-table.tsx @@ -1,6 +1,6 @@ import { type ReactNode, createContext, useContext } from "react"; -import { type Table } from "@tanstack/react-table"; +import type { Table } from "@tanstack/react-table"; interface DataTableRootProps { children: ReactNode; @@ -12,13 +12,13 @@ interface TDataTableContext { } // biome-ignore lint/suspicious/noExplicitAny: -const dataTableContext = createContext | null>(null); +const TableStateContext = createContext | null>(null); -export const useDataTable = () => { - const context = useContext(dataTableContext); +export const useTable = () => { + const context = useContext(TableStateContext); if (!context) { - throw new Error("useDataTable should be called inside DataTable"); + throw new Error("useTable should be called inside DataTable"); } return context; @@ -29,8 +29,8 @@ export function DataTable({ table, }: DataTableRootProps) { return ( - + {children} - + ); } diff --git a/src/components/ui/debounced-input.tsx b/src/components/ui/debounced-input.tsx new file mode 100644 index 000000000..620cfd72c --- /dev/null +++ b/src/components/ui/debounced-input.tsx @@ -0,0 +1,36 @@ +import { useEffect, useState } from "react"; +import { Input, type InputProps } from "./input"; + +export function DebouncedInput({ + value: initialValue, + onChange, + debounce = 500, + ...props +}: { + value: string | number; + onChange: (value: string | number) => void; + debounce?: number; +} & Omit) { + const [value, setValue] = useState(initialValue); + + useEffect(() => { + setValue(initialValue); + }, [initialValue]); + + // biome-ignore lint/correctness/useExhaustiveDependencies: + useEffect(() => { + const timeout = setTimeout(() => { + onChange(value); + }, debounce); + + return () => clearTimeout(timeout); + }, [value]); + + return ( + setValue(e.target.value)} + /> + ); +} diff --git a/src/components/update/update-table-toolbar.tsx b/src/components/update/update-table-toolbar.tsx index f7b8fb042..647c92e44 100644 --- a/src/components/update/update-table-toolbar.tsx +++ b/src/components/update/update-table-toolbar.tsx @@ -1,10 +1,10 @@ -import { useDataTable } from "@/components/ui/data-table/data-table"; +import { useTable } from "@/components/ui/data-table/data-table"; import { ResetButton } from "@/components/ui/data-table/data-table-buttons"; import { DataTableViewOptions } from "@/components/ui/data-table/data-table-view-options"; import { Input } from "@/components/ui/input"; export const UpdateTableToolbar = () => { - const { table } = useDataTable(); + const { table } = useTable(); const isFiltered = table.getState().columnFilters.length > 0; return ( diff --git a/src/components/update/update-table.tsx b/src/components/update/update-table.tsx index 15f28b4a9..454b658e1 100644 --- a/src/components/update/update-table.tsx +++ b/src/components/update/update-table.tsx @@ -5,7 +5,6 @@ import { Button } from "@/components/ui/button"; import { Checkbox } from "@/components/ui/checkbox"; import { DataTable } from "@/components/ui/data-table/data-table"; import { DataTableBody } from "@/components/ui/data-table/data-table-body"; -import { SortButton } from "@/components/ui/data-table/data-table-buttons"; import { DataTableContent } from "@/components/ui/data-table/data-table-content"; import { DataTableHeader } from "@/components/ui/data-table/data-table-header"; import { DataTablePagination } from "@/components/ui/data-table/data-table-pagination"; @@ -15,24 +14,14 @@ import { DropdownMenuItem, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; +import { useDataTable } from "@/hooks/use-data-table"; +import type { UpdateStatusEnum } from "@/prisma/enums"; import type { RouterOutputs } from "@/trpc/shared"; import { RiAddCircleLine } from "@remixicon/react"; -import { - type ColumnDef, - type ColumnFiltersState, - type SortingState, - type VisibilityState, - getCoreRowModel, - getFacetedRowModel, - getFacetedUniqueValues, - getFilteredRowModel, - getPaginationRowModel, - getSortedRowModel, - useReactTable, -} from "@tanstack/react-table"; +import { type ColumnDef, createColumnHelper } from "@tanstack/react-table"; import { useSession } from "next-auth/react"; import Link from "next/link"; -import React, { useState } from "react"; +import React, { type ReactElement, useState } from "react"; import { pushModal } from "../modals"; import { ChangeUpdateVisibilityAlertDialog } from "./change-update-visibility-alert-dialog"; import { UpdateTableToolbar } from "./update-table-toolbar"; @@ -43,15 +32,10 @@ type UpdateTableType = { updates: Update; }; -const getUpdateStatus = (status: string) => { - switch (status) { - case "DRAFT": - return Draft; - case "PUBLIC": - return Public; - case "PRIVATE": - return Private; - } +const UpdateStatus: Record = { + DRAFT: Draft, + PUBLIC: Public, + PRIVATE: Private, }; const UpdateActions = (row: { original: Update[number] }) => { @@ -119,8 +103,10 @@ const UpdateActions = (row: { original: Update[number] }) => { ); }; -export const columns: ColumnDef[] = [ - { +const columnHelper = createColumnHelper(); + +const columns = [ + columnHelper.display({ id: "select", header: ({ table }) => ( [] = [ ), enableSorting: false, enableHiding: false, - }, - { - id: "title", - header: ({ column }) => { - return ( - column.toggleSorting(column.getIsSorted() === "asc")} - /> - ); - }, - accessorFn: (row) => row.title, - cell: ({ row }) => ( -
{row.getValue("title")}
- ), - }, - { - accessorKey: "status", - header: () => { - return
Sharing status
; - }, - cell: ({ row }) =>
{getUpdateStatus(row.original.status)}
, - }, - { - accessorKey: "actions", - header: () => { - return
Actions
; - }, - cell: ({ row }) =>
{UpdateActions(row)}
, - }, + }), + + columnHelper.accessor("title", { + header: "Title", + cell: (row) =>
{row.getValue()}
, + }), + columnHelper.accessor("status", { + header: "Status", + cell: (row) =>
{UpdateStatus[row.getValue()]}
, + }), + columnHelper.display({ + id: "actions", + header: "Title", + cell: ({ row }) => UpdateActions(row), + enableSorting: false, + enableHiding: false, + }), ]; const UpdateTable = ({ updates }: UpdateTableType) => { - const [sorting, setSorting] = React.useState([]); - const [columnFilters, setColumnFilters] = React.useState( - [], - ); - const [columnVisibility, setColumnVisibility] = - React.useState({}); - const [rowSelection, setRowSelection] = React.useState({}); - - const table = useReactTable({ - data: updates, - columns: columns, - enableRowSelection: true, - onRowSelectionChange: setRowSelection, - onSortingChange: setSorting, - onColumnFiltersChange: setColumnFilters, - onColumnVisibilityChange: setColumnVisibility, - getCoreRowModel: getCoreRowModel(), - getFilteredRowModel: getFilteredRowModel(), - getPaginationRowModel: getPaginationRowModel(), - getSortedRowModel: getSortedRowModel(), - getFacetedRowModel: getFacetedRowModel(), - getFacetedUniqueValues: getFacetedUniqueValues(), - state: { - sorting, - columnFilters, - columnVisibility, - rowSelection, - }, - }); + const table = useDataTable({ data: updates, columns: columns }); return (
diff --git a/src/hooks/use-data-table.tsx b/src/hooks/use-data-table.tsx new file mode 100644 index 000000000..6dcb2df9a --- /dev/null +++ b/src/hooks/use-data-table.tsx @@ -0,0 +1,53 @@ +import { + type ColumnFiltersState, + type RowData, + type SortingState, + type TableOptions, + type VisibilityState, + getCoreRowModel, + getFacetedRowModel, + getFacetedUniqueValues, + getFilteredRowModel, + getPaginationRowModel, + getSortedRowModel, + useReactTable, +} from "@tanstack/react-table"; +import { useState } from "react"; + +type MakeOptional = Omit & Partial>; + +export type TDataTableOptions = MakeOptional< + TableOptions, + "getCoreRowModel" +>; + +export function useDataTable( + options: TDataTableOptions, +) { + const [sorting, setSorting] = useState([]); + const [columnFilters, setColumnFilters] = useState([]); + const [columnVisibility, setColumnVisibility] = useState({}); + const [rowSelection, setRowSelection] = useState({}); + + return useReactTable({ + enableRowSelection: true, + onRowSelectionChange: setRowSelection, + onSortingChange: setSorting, + onColumnFiltersChange: setColumnFilters, + onColumnVisibilityChange: setColumnVisibility, + getCoreRowModel: getCoreRowModel(), + getFilteredRowModel: getFilteredRowModel(), + getPaginationRowModel: getPaginationRowModel(), + getSortedRowModel: getSortedRowModel(), + getFacetedRowModel: getFacetedRowModel(), + getFacetedUniqueValues: getFacetedUniqueValues(), + state: { + sorting, + columnFilters, + columnVisibility, + rowSelection, + ...(options?.state && { ...options.state }), + }, + ...options, + }); +} diff --git a/src/hooks/use-paginated-data-table.ts b/src/hooks/use-paginated-data-table.ts new file mode 100644 index 000000000..b9dc01b57 --- /dev/null +++ b/src/hooks/use-paginated-data-table.ts @@ -0,0 +1,121 @@ +"use client"; + +import type { + ColumnFiltersState, + RowData, + SortingState, + Updater, +} from "@tanstack/react-table"; + +import { useRouter } from "next/navigation"; +import { type TDataTableOptions, useDataTable } from "./use-data-table"; +import { useUpdateSearchParams } from "./use-update-search-params"; + +function parseSortingState(sort: string) { + const [field, direction] = sort.split("."); + + const state: SortingState = [{ id: field ?? "", desc: direction === "desc" }]; + + return state; +} + +function parseFilteringState( + filterFields?: { id: string; value: string | undefined }[], +) { + const columnFilters: ColumnFiltersState = []; + + if (filterFields) { + for (const filter of filterFields) { + if (filter.value) { + columnFilters.push({ id: filter.id, value: [filter.value] }); + } + } + } + + return columnFilters; +} + +export function usePaginatedTable({ + page, + limit, + sort, + filterFields = [], + ...options +}: TDataTableOptions & { + page: number; + limit: number; + sort: string; + filterFields?: { id: string; value: string | undefined }[]; +}) { + const router = useRouter(); + + const updateSearchParams = useUpdateSearchParams(); + + const pageIndex = page - 1; + const pageSize = limit; + const sorting = parseSortingState(sort); + + const columnFilters = parseFilteringState(filterFields); + + return useDataTable({ + manualPagination: true, + manualSorting: true, + manualFiltering: true, + pageCount: options.pageCount ?? -1, + state: { + pagination: { + pageIndex, + pageSize, + ...(options?.state?.pagination && { ...options.state.pagination }), + }, + sorting, + columnFilters, + ...(options?.state && { ...options.state }), + }, + onPaginationChange: (updater) => { + if (typeof updater === "function") { + const updateValue = updater({ pageIndex, pageSize }); + updateSearchParams({ + limit: updateValue.pageSize, + page: updateValue.pageIndex + 1, + }); + } + }, + + onSortingChange: (updater: Updater) => { + if (typeof updater === "function") { + const updateValue = updater(sorting); + + const sortValue = updateValue[0] + ? `${updateValue[0]?.id}.${updateValue[0]?.desc ? "desc" : "asc"}` + : undefined; + + updateSearchParams({ sort: sortValue }); + } + }, + + onColumnFiltersChange: (updater: Updater) => { + if (typeof updater === "function") { + const updateValue = updater(columnFilters); + if (updateValue.length) { + const state = updateValue.reduce>( + (prev, curr) => { + prev[curr.id] = curr.value; + + return prev; + }, + {}, + ); + + //@ts-expect-error + updateSearchParams({ ...state }); + } else { + const path = window.location.pathname; + router.push(path); + } + } + }, + + ...options, + }); +} diff --git a/src/hooks/use-update-search-params.ts b/src/hooks/use-update-search-params.ts new file mode 100644 index 000000000..4710f0c02 --- /dev/null +++ b/src/hooks/use-update-search-params.ts @@ -0,0 +1,28 @@ +"use client"; + +import { usePathname, useRouter, useSearchParams } from "next/navigation"; + +export const useUpdateSearchParams = () => { + const router = useRouter(); + const pathname = usePathname(); + const searchParams = useSearchParams(); + + return ( + params: Record, + ) => { + const nextSearchParams = new URLSearchParams( + searchParams?.toString() ?? "", + ); + + // biome-ignore lint/complexity/noForEach: + Object.entries(params).forEach(([key, value]) => { + if (value === undefined || value === null) { + nextSearchParams.delete(key); + } else { + nextSearchParams.set(key, String(value)); + } + }); + + router.push(`${pathname}?${nextSearchParams.toString()}`); + }; +}; diff --git a/src/server/api/api-client.ts b/src/server/api/api-client.ts new file mode 100644 index 000000000..7293ce3db --- /dev/null +++ b/src/server/api/api-client.ts @@ -0,0 +1,122 @@ +import { env } from "@/env"; +import type { + RouteConfigToTypedResponse, + createRoute, + z, +} from "@hono/zod-openapi"; + +// biome-ignore lint/suspicious/noExplicitAny: +type ExtractParams = T extends { params: z.ZodType } + ? z.infer + : never; +// biome-ignore lint/suspicious/noExplicitAny: +type ExtractQuery = T extends { query: z.ZodType } + ? z.infer + : never; + +type ExtractRequestBody = T extends { + body: { + content: { + "application/json": { + // biome-ignore lint/suspicious/noExplicitAny: + schema: z.ZodType; + }; + }; + }; +} + ? z.infer + : never; + +// biome-ignore lint/complexity/noBannedTypes: +type IfNever = [T] extends [never] ? {} : Obj; + +export type APIClientParams = IfNever< + ExtractParams, + { urlParams: ExtractParams } +> & + IfNever< + ExtractQuery, + { searchParams: ExtractQuery } + > & + IfNever< + ExtractRequestBody, + { json: ExtractRequestBody } + > & { + headers?: Headers; + }; + +type RouteConfig = Parameters[0]; + +export async function createClient( + method: U["method"], + url: U["path"], + params: APIClientParams, +) { + const path = buildPath(url, params); + const headers = buildHeaders(params.headers); + const requestOptions = buildRequestOptions(method, headers, params); + + const response = await fetch(path, requestOptions); + + return response.json() as RouteConfigToTypedResponse["_data"]; +} + +function interpolatePath( + path: string, + params: Record, +): string { + return path.replace(/{([^}]+)}/g, (_, key) => + encodeURIComponent(String(params[key])), + ); +} + +function buildHeaders(customHeaders?: HeadersInit): Headers { + const headers = new Headers(customHeaders); + headers.set("Content-Type", "application/json"); + return headers; +} + +function buildPath( + url: string, + params: APIClientParams, +): string { + let path = interpolatePath( + `${env.NEXT_PUBLIC_BASE_URL}/api${url}`, + "urlParams" in params ? params.urlParams : {}, + ); + + if ("searchParams" in params) { + const queryString = new URLSearchParams(params.searchParams).toString(); + path += `?${queryString}`; + } + + return path; +} + +function buildRequestOptions( + method: string, + headers: Headers, + params: APIClientParams, +): RequestInit { + const requestOptions: RequestInit = { + method: method.toUpperCase(), + credentials: "include", + headers, + cache: "no-store", + }; + + if ("json" in params) { + requestOptions.body = JSON.stringify(params.json); + } + + return requestOptions; +} + +async function getErrorMessage(response: Response): Promise { + try { + const data = await response.json(); + return data.message || "Unknown error occurred."; + } catch { + return response.statusText || "Unknown error occurred."; + } +} diff --git a/src/server/api/client-handlers/stakeholder.ts b/src/server/api/client-handlers/stakeholder.ts new file mode 100644 index 000000000..48080bac6 --- /dev/null +++ b/src/server/api/client-handlers/stakeholder.ts @@ -0,0 +1,18 @@ +import { type APIClientParams, createClient } from "../api-client"; + +import type { getMany } from "../routes/stakeholder/getMany"; + +type GetManyRoute = typeof getMany.route; + +export const getManyStakeholder = (params: TGetManyStakeholderParams) => { + return createClient( + "get", + "/v1/{companyId}/stakeholders", + params, + ); +}; + +export type TGetManyStakeholderParams = APIClientParams; +export type TGetManyStakeholderRes = Awaited< + ReturnType +>; diff --git a/src/server/api/const.ts b/src/server/api/const.ts index 86b6666b0..627c17186 100644 --- a/src/server/api/const.ts +++ b/src/server/api/const.ts @@ -1,2 +1,3 @@ -export const DEFAULT_PAGINATION_LIMIT = 50; +export const DEFAULT_PAGINATION_LIMIT = 10 as const; +export const OFFSET_MAXIMUM_LIMIT = 50 as const; export const SECURITY_SCHEME_NAME = "Bearer"; diff --git a/src/server/api/error.ts b/src/server/api/error.ts index 685b9c006..c960a316e 100644 --- a/src/server/api/error.ts +++ b/src/server/api/error.ts @@ -200,11 +200,14 @@ export class ApiError extends HTTPException { export function handleError(err: Error, c: Context): Response { if (err instanceof ApiError) { if (err.status >= 500) { - log.error("ApiError", { - name: err.name, - code: err.code, - status: err.status, - }); + log.error( + { + name: err.name, + code: err.code, + status: err.status, + }, + "ApiError", + ); } return c.json( { @@ -219,11 +222,14 @@ export function handleError(err: Error, c: Context): Response { if (err instanceof HTTPException) { if (err.status >= 500) { - log.error("HTTPException", { - name: err.name, - status: err.status, - message: err.message, - }); + log.error( + { + name: err.name, + status: err.status, + message: err.message, + }, + "HTTPException", + ); } const code = statusToCode(err.status); return c.json( @@ -241,12 +247,15 @@ export function handleError(err: Error, c: Context): Response { * This is a generic error, we should log it and return a 500 */ - log.error("UnhandledError", { - name: err.name, - message: err.message, - cause: err.cause, - stack: err.stack, - }); + log.error( + { + name: err.name, + message: err.message, + cause: err.cause, + stack: err.stack, + }, + "UnhandledError", + ); return c.json( { diff --git a/src/server/api/middlewares/session-token.ts b/src/server/api/middlewares/session-token.ts index 4cc0ecf89..470f2ec57 100644 --- a/src/server/api/middlewares/session-token.ts +++ b/src/server/api/middlewares/session-token.ts @@ -74,7 +74,6 @@ async function fetchSessionFromAuthUrl( new Request(newUrl, { method: "GET", headers: clonedRequest.headers, - body: clonedRequest.body, }), ); diff --git a/src/server/api/routes/stakeholder/getMany.ts b/src/server/api/routes/stakeholder/getMany.ts index c34554d41..5c437d955 100644 --- a/src/server/api/routes/stakeholder/getMany.ts +++ b/src/server/api/routes/stakeholder/getMany.ts @@ -1,14 +1,15 @@ import { z } from "@hono/zod-openapi"; +import { OffsetPaginationResponseSchema } from "../../schema/pagination"; import { - PaginationQuerySchema, - PaginationResponseSchema, -} from "../../schema/pagination"; -import { StakeholderSchema } from "../../schema/stakeholder"; + ManyStakeholderQuerySchema, + StakeholderSchema, + parseManyStakeholderSortParam, +} from "../../schema/stakeholder"; import { authMiddleware, withAuthApiV1 } from "../../utils/endpoint-creator"; const ResponseSchema = z.object({ data: z.array(StakeholderSchema), - meta: PaginationResponseSchema, + meta: OffsetPaginationResponseSchema, }); const ParamsSchema = z.object({ @@ -33,7 +34,7 @@ export const getMany = withAuthApiV1 path: "/v1/{companyId}/stakeholders", middleware: [authMiddleware()], request: { - query: PaginationQuerySchema, + query: ManyStakeholderQuerySchema, params: ParamsSchema, }, responses: { @@ -50,13 +51,25 @@ export const getMany = withAuthApiV1 .handler(async (c) => { const { membership } = c.get("session"); const { db } = c.get("services"); - const query = c.req.valid("query"); + const { limit, page, sort, name: query } = c.req.valid("query"); const [data, meta] = await db.stakeholder - .paginate({ where: { companyId: membership.companyId } }) - .withCursor({ - limit: query.limit, - after: query.cursor, + .paginate({ + where: { + companyId: membership.companyId, + ...(query && { + OR: [ + { name: { contains: query, mode: "insensitive" } }, + { email: { contains: query, mode: "insensitive" } }, + ], + }), + }, + orderBy: parseManyStakeholderSortParam(sort), + }) + .withPages({ + includePageCount: true, + limit, + page, }); const response: z.infer = { diff --git a/src/server/api/schema/pagination.ts b/src/server/api/schema/pagination.ts index 36e608c72..8b580bbe3 100644 --- a/src/server/api/schema/pagination.ts +++ b/src/server/api/schema/pagination.ts @@ -1,5 +1,44 @@ import { z } from "@hono/zod-openapi"; -import { DEFAULT_PAGINATION_LIMIT } from "../const"; +import { DEFAULT_PAGINATION_LIMIT, OFFSET_MAXIMUM_LIMIT } from "../const"; + +const sortDirections = ["asc", "desc"] as const; +type SortDirection = (typeof sortDirections)[number]; + +export const OffsetPaginationQuerySchema = z.object({ + limit: z.coerce + .number() + .max(OFFSET_MAXIMUM_LIMIT) + .positive() + .optional() + .default(DEFAULT_PAGINATION_LIMIT) + .openapi({ + description: `The number of items to return per page. The maximum value is ${OFFSET_MAXIMUM_LIMIT}.`, + param: { + name: "limit", + in: "query", + }, + example: DEFAULT_PAGINATION_LIMIT, + default: DEFAULT_PAGINATION_LIMIT, + maximum: OFFSET_MAXIMUM_LIMIT, + minimum: 1, + }), + page: z.coerce + .number() + .positive() + .optional() + .default(1) + .openapi({ + description: "The page number to retrieve. Starts from 1.", + param: { + name: "page", + in: "query", + }, + example: 1, + default: 1, + minimum: 1, + }) + .openapi("Offset Pagination query"), +}); export const PaginationQuerySchema = z.object({ limit: z @@ -53,7 +92,6 @@ export const PaginationResponseSchema = z hasNextPage: z.boolean().openapi({ description: "Indicates if there is a next page available in the pagination. `true` if there are more pages after the current one, `false` otherwise.", - example: false, }), startCursor: z.string().nullable().openapi({ @@ -68,4 +106,63 @@ export const PaginationResponseSchema = z example: "cly151kxq0000i7ngb3erchgo", }), }) - .openapi("Pagination"); + .openapi("Pagination Cursor"); + +export const OffsetPaginationResponseSchema = z + .object({ + isFirstPage: z.boolean().openapi({ + description: + "Indicates whether the current page is the first page of the pagination.", + }), + isLastPage: z.boolean().openapi({ + description: + "Indicates whether the current page is the last page of the pagination.", + }), + currentPage: z.number().openapi({ + description: "The current page number in the pagination.", + }), + previousPage: z.number().nullable().openapi({ + description: + "The previous page number in the pagination. Null if the current page is the first page.", + }), + nextPage: z.number().nullable().openapi({ + description: + "The next page number in the pagination. Null if the current page is the last page.", + }), + pageCount: z.number().openapi({ + description: "The total number of pages available.", + }), + totalCount: z.number().openapi({ + description: "The total number of items across all pages.", + }), + }) + .openapi("Offset Pagination", { + description: + "A schema representing the pagination details of an offset-based pagination system.", + }); + +export function generateSortParam(sortFields: T) { + type SortField = T[number]; + type SortParam = `${SortField}.${SortDirection}`; + + const sortParams = sortFields.flatMap((field) => + sortDirections.map((direction) => `${field}.${direction}` as const), + ) as readonly SortParam[]; + + const parseSortParam = (sort: S) => { + type Field = S extends `${infer F}.${SortDirection}` ? F : never; + const [field, direction] = sort.split(".") as [Field, SortDirection]; + return { [field]: direction } as { [K in Field]: SortDirection }; + }; + return { + schema: z.enum(sortParams as [SortParam, ...SortParam[]]).openapi({ + description: "sort by", + param: { + name: "sort", + in: "query", + }, + }), + sortParams, + parseSortParam, + }; +} diff --git a/src/server/api/schema/stakeholder.ts b/src/server/api/schema/stakeholder.ts index 854ff7470..073f1fb4c 100644 --- a/src/server/api/schema/stakeholder.ts +++ b/src/server/api/schema/stakeholder.ts @@ -4,6 +4,8 @@ import { } from "@/prisma/enums"; import { z } from "@hono/zod-openapi"; +import { OffsetPaginationQuerySchema, generateSortParam } from "./pagination"; + const StakeholderTypeArray = Object.values(StakeholderTypeEnum) as [ StakeholderTypeEnum, ...StakeholderTypeEnum[], @@ -34,6 +36,11 @@ export const StakeholderSchema = z example: "ACME Corp", }), + taxId: z.string().nullish().openapi({ + description: "Tax id", + example: "12-3456789", + }), + stakeholderType: z.enum(StakeholderTypeArray).openapi({ description: "Stakeholder type", example: "INDIVIDUAL", @@ -106,6 +113,24 @@ export const UpdateStakeholderSchema = StakeholderSchema.omit({ description: "Update a stakeholder by ID", }); +const sortFields = generateSortParam(["createdAt", "name", "email"] as const); + +export const parseManyStakeholderSortParam = sortFields.parseSortParam; +export const ManyStakeholderSortParams = sortFields.sortParams; +export const ManyStakeholderSortParamsSchema = sortFields.schema; + +export const ManyStakeholderQuerySchema = z + .object({ + sort: ManyStakeholderSortParamsSchema.optional().default("createdAt.desc"), + name: z.string().optional(), + }) + .merge(OffsetPaginationQuerySchema); + export type TStakeholderSchema = z.infer; export type TCreateStakeholderSchema = z.infer; export type TUpdateStakeholderSchema = z.infer; +export type TManyStakeholderSortParams = + (typeof ManyStakeholderSortParams)[number]; +export type TManyStakeholderQuerySchema = z.infer< + typeof ManyStakeholderQuerySchema +>; diff --git a/src/server/api/utils/endpoint-creator.ts b/src/server/api/utils/endpoint-creator.ts index d22d9f158..592914bf2 100644 --- a/src/server/api/utils/endpoint-creator.ts +++ b/src/server/api/utils/endpoint-creator.ts @@ -28,7 +28,8 @@ const AuthHeaderSchema = z.object({ .openapi({ description: "Bearer token to authorize the request", example: "Bearer api_x0X0x0X0x0X0x0X0x0X0x0X", - }), + }) + .optional(), }); type AuthHeaders = { @@ -92,7 +93,7 @@ const createApi = ( }; export const authMiddleware = (option?: accessTokenAuthMiddlewareOptions) => - some(sessionCookieAuthMiddleware(), accessTokenAuthMiddleware(option)); + some(accessTokenAuthMiddleware(option), sessionCookieAuthMiddleware()); export const ApiV1 = createApi("v1"); diff --git a/src/trpc/api/trpc.ts b/src/trpc/api/trpc.ts index 2bdfaec0b..55b0a9089 100644 --- a/src/trpc/api/trpc.ts +++ b/src/trpc/api/trpc.ts @@ -21,6 +21,7 @@ import { import { getServerAuthSession } from "@/server/auth"; import { db } from "@/server/db"; import * as Sentry from "@sentry/nextjs"; +import { cache } from "react"; interface Meta { policies: addPolicyOption; @@ -38,7 +39,7 @@ interface Meta { * * @see https://trpc.io/docs/server/context */ -export const createTRPCContext = async (opts: { headers: Headers }) => { +export const createTRPCContext = cache(async (opts: { headers: Headers }) => { const session = await getServerAuthSession(); return { @@ -48,12 +49,11 @@ export const createTRPCContext = async (opts: { headers: Headers }) => { userAgent: getUserAgent(opts.headers), ...opts, }; -}; +}); export type CreateTRPCContextType = Awaited< ReturnType >; - const withAuthTrpcContext = ({ session, ...rest }: CreateTRPCContextType) => { if (!session || !session.user) { throw new TRPCError({ code: "UNAUTHORIZED" }); @@ -126,22 +126,12 @@ export type withAccessControlTrpcContextType = ReturnType< * ZodErrors so that you get typesafety on the frontend if your procedure fails due to validation * errors on the backend. */ -const t = initTRPC - .context() - .meta() - .create({ - transformer: superjson, - errorFormatter({ shape, error }) { - return { - ...shape, - data: { - ...shape.data, - zodError: - error.cause instanceof ZodError ? error.cause.flatten() : null, - }, - }; - }, - }); +const t = initTRPC.context().meta().create({ + /** + * @see https://trpc.io/docs/server/data-transformers + */ + transformer: superjson, +}); /** * 3. ROUTER & PROCEDURE (THE IMPORTANT BIT) @@ -155,7 +145,6 @@ const t = initTRPC * * @see https://trpc.io/docs/router */ -export const createTRPCRouter = t.router; const sentryMiddleware = t.middleware( Sentry.trpcMiddleware({ @@ -213,3 +202,6 @@ export const withAccessControl = t.procedure.use( }); }), ); + +export const createTRPCRouter = t.router; +export const createCallerFactory = t.createCallerFactory; diff --git a/src/trpc/react.tsx b/src/trpc/react.tsx index cadb403ee..a6f516355 100644 --- a/src/trpc/react.tsx +++ b/src/trpc/react.tsx @@ -1,48 +1,56 @@ "use client"; -import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; -import { loggerLink, unstable_httpBatchStreamLink } from "@trpc/client"; +import type { QueryClient } from "@tanstack/react-query"; +import { QueryClientProvider } from "@tanstack/react-query"; +import { httpBatchLink } from "@trpc/client"; import { createTRPCReact } from "@trpc/react-query"; import { useState } from "react"; -import { type AppRouter } from "@/trpc/api/root"; -import { getUrl, transformer } from "./shared"; +import type { AppRouter } from "@/trpc/api/root"; + +import { getUrl, makeQueryClient, transformer } from "./shared"; export const api = createTRPCReact(); -export function TRPCReactProvider(props: { - children: React.ReactNode; - cookies: string; -}) { - const [queryClient] = useState(() => new QueryClient()); +let clientQueryClientSingleton: QueryClient; +function getQueryClient() { + if (typeof window === "undefined") { + // Server: always make a new query client + return makeQueryClient(); + } + // Browser: use singleton pattern to keep the same query client + // biome-ignore lint/suspicious/noAssignInExpressions: + return (clientQueryClientSingleton ??= makeQueryClient()); +} + +export function TRPCReactProvider( + props: Readonly<{ + children: React.ReactNode; + cookies: string; + }>, +) { + // NOTE: Avoid useState when initializing the query client if you don't + // have a suspense boundary between this and the code that may + // suspend because React will throw away the client on the initial + // render if it suspends and there is no boundary + const queryClient = getQueryClient(); const [trpcClient] = useState(() => api.createClient({ - transformer, links: [ - loggerLink({ - enabled: (op) => - process.env.NODE_ENV === "development" || - (op.direction === "down" && op.result instanceof Error), - }), - unstable_httpBatchStreamLink({ + httpBatchLink({ + transformer, url: getUrl(), - headers() { - return { - cookie: props.cookies, - "x-trpc-source": "react", - }; - }, }), ], }), ); return ( - - + + {props.children} - - + + ); } diff --git a/src/trpc/server.ts b/src/trpc/server.ts index 56418bd1e..5dd70c688 100644 --- a/src/trpc/server.ts +++ b/src/trpc/server.ts @@ -1,25 +1,13 @@ import "server-only"; -import { - TRPCClientError, - createTRPCProxyClient, - loggerLink, -} from "@trpc/client"; -import { callProcedure } from "@trpc/server"; -import { observable } from "@trpc/server/observable"; -import type { TRPCErrorResponse } from "@trpc/server/rpc"; -import { cookies } from "next/headers"; +import { createHydrationHelpers } from "@trpc/react-query/rsc"; import { cache } from "react"; -import { env } from "@/env"; -import { type AppRouter, appRouter } from "@/trpc/api/root"; -import { createTRPCContext } from "@/trpc/api/trpc"; -import { transformer } from "./shared"; +import { cookies } from "next/headers"; +import { appRouter } from "./api/root"; +import { createCallerFactory, createTRPCContext } from "./api/trpc"; +import { makeQueryClient } from "./shared"; -/** - * This wraps the `createTRPCContext` helper and provides the required context for the tRPC API when - * handling a tRPC call from a React Server Component. - */ const createContext = cache(() => { return createTRPCContext({ headers: new Headers({ @@ -29,37 +17,9 @@ const createContext = cache(() => { }); }); -export const api = createTRPCProxyClient({ - transformer, - links: [ - loggerLink({ - enabled: (op) => - env.LOGS || (op.direction === "down" && op.result instanceof Error), - }), - /** - * Custom RSC link that lets us invoke procedures without using http requests. Since Server - * Components always run on the server, we can just call the procedure as a function. - */ - () => - ({ op }) => - observable((observer) => { - createContext() - .then((ctx) => { - return callProcedure({ - procedures: appRouter._def.procedures, - path: op.path, - rawInput: op.input, - ctx, - type: op.type, - }); - }) - .then((data) => { - observer.next({ result: { data } }); - observer.complete(); - }) - .catch((cause: TRPCErrorResponse) => { - observer.error(TRPCClientError.from(cause)); - }); - }), - ], -}); +export const getQueryClient = cache(makeQueryClient); +const caller = createCallerFactory(appRouter)(createContext); + +export const { trpc: api, HydrateClient } = createHydrationHelpers< + typeof appRouter +>(caller, getQueryClient); diff --git a/src/trpc/shared.ts b/src/trpc/shared.ts index 3b2e54ada..49dbc3c9a 100644 --- a/src/trpc/shared.ts +++ b/src/trpc/shared.ts @@ -3,6 +3,10 @@ import superjson from "superjson"; import { env } from "@/env"; import type { AppRouter } from "@/trpc/api/root"; +import { + QueryClient, + defaultShouldDehydrateQuery, +} from "@tanstack/react-query"; export const transformer = superjson; @@ -29,3 +33,22 @@ export type RouterInputs = inferRouterInputs; * @example type HelloOutput = RouterOutputs['example']['hello'] */ export type RouterOutputs = inferRouterOutputs; + +export function makeQueryClient() { + return new QueryClient({ + defaultOptions: { + queries: { + staleTime: 30 * 1000, + }, + dehydrate: { + serializeData: transformer.serialize, + shouldDehydrateQuery: (query) => + defaultShouldDehydrateQuery(query) || + query.state.status === "pending", + }, + hydrate: { + deserializeData: transformer.deserialize, + }, + }, + }); +}