From 9365c1f5b8cf8331a383091e38c18a551cea80c1 Mon Sep 17 00:00:00 2001 From: Lewechi Date: Thu, 30 Apr 2026 18:51:14 +0100 Subject: [PATCH 01/21] fix: fixes --- next.config.ts | 2 +- pnpm-lock.yaml | 1524 ++++++----------- src/app/(dashboard)/dashboard/page.tsx | 3 +- src/app/(dashboard)/layout.tsx | 14 +- src/app/api/v1/auth/apple/route.ts | 8 + src/app/api/v1/auth/google/route.ts | 8 + src/app/api/v1/auth/login/route.ts | 15 +- src/app/api/v1/auth/logout/route.ts | 1 + src/app/api/v1/auth/refresh/route.ts | 1 + src/app/api/v1/auth/verify-login-otp/route.ts | 1 + src/server/services/attention.service.ts | 12 +- src/server/services/jwt.service.ts | 18 +- src/server/services/login-otp.service.ts | 13 +- src/server/services/onboarding.service.ts | 2 +- src/server/services/token-refresh.service.ts | 16 +- src/server/utils/auth.ts | 129 +- 16 files changed, 673 insertions(+), 1094 deletions(-) diff --git a/next.config.ts b/next.config.ts index ed33a3c7..20421862 100644 --- a/next.config.ts +++ b/next.config.ts @@ -6,7 +6,7 @@ const nextConfig: NextConfig = { compress: true, serverExternalPackages: ["ioredis"], turbopack: { - root: join(__dirname), // Set the root to the current directory dynamically + root: process.cwd(), rules: { "*.svg": { loaders: ["@svgr/webpack"], diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d7bc9201..d59c6e86 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -75,8 +75,8 @@ importers: specifier: ^1.5.5 version: 1.5.6 '@web3-react/core': - specifier: ^8.2.3 - version: 8.2.3(@types/react@19.2.14)(immer@11.1.4)(react@19.1.0) + specifier: ^6.1.9 + version: 6.1.9(react@19.1.0) '@web3-react/injected-connector': specifier: ^6.0.7 version: 6.0.7 @@ -99,8 +99,8 @@ importers: specifier: ^2.6.0 version: 2.6.0 drizzle-kit: - specifier: ^0.31.8 - version: 0.31.10 + specifier: ^0.18.1 + version: 0.18.1 drizzle-orm: specifier: ^0.45.1 version: 0.45.1(@types/pg@8.20.0)(pg@8.20.0) @@ -129,8 +129,8 @@ importers: specifier: ^9.0.3 version: 9.0.3 jspdf: - specifier: ^3.0.3 - version: 3.0.4 + specifier: ^4.2.1 + version: 4.2.1 lodash: specifier: ^4.17.23 version: 4.17.23 @@ -138,8 +138,8 @@ importers: specifier: ^0.544.0 version: 0.544.0(react@19.1.0) next: - specifier: 16.2.4 - version: 16.2.4(@babel/core@7.29.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + specifier: ^15.5.3 + version: 15.5.15(@babel/core@7.29.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) next-themes: specifier: ^0.4.6 version: 0.4.6(react-dom@19.1.0(react@19.1.0))(react@19.1.0) @@ -251,7 +251,7 @@ importers: version: 8.57.2(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) '@vitest/coverage-v8': specifier: ^4.1.2 - version: 4.1.2(vitest@4.1.1(@types/node@20.19.37)(vite@8.0.2(@emnapi/core@1.9.1)(@emnapi/runtime@1.10.0)(@types/node@20.19.37)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0))) + version: 4.1.2(vitest@4.1.1(@types/node@20.19.37)(vite@8.0.2(@emnapi/core@1.9.1)(@emnapi/runtime@1.10.0)(@types/node@20.19.37)(esbuild@0.15.18)(jiti@2.6.1)(tsx@4.21.0))) eslint: specifier: ^9 version: 9.39.4(jiti@2.6.1) @@ -278,7 +278,7 @@ importers: version: 5.9.3 vitest: specifier: ^4.0.18 - version: 4.1.1(@types/node@20.19.37)(vite@8.0.2(@emnapi/core@1.9.1)(@emnapi/runtime@1.10.0)(@types/node@20.19.37)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)) + version: 4.1.1(@types/node@20.19.37)(vite@8.0.2(@emnapi/core@1.9.1)(@emnapi/runtime@1.10.0)(@types/node@20.19.37)(esbuild@0.15.18)(jiti@2.6.1)(tsx@4.21.0)) packages: @@ -1040,467 +1040,177 @@ packages: '@date-fns/tz@1.4.1': resolution: {integrity: sha512-P5LUNhtbj6YfI3iJjw5EL9eUAG6OitD0W3fWQcpQjDRc/QIsL0tRNuO1PcDvPccWL1fSTXXdE1ds+l95DV/OFA==} - '@drizzle-team/brocli@0.10.2': - resolution: {integrity: sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w==} - '@emnapi/core@1.9.1': resolution: {integrity: sha512-mukuNALVsoix/w1BJwFzwXBN/dHeejQtuVzcDsfOEsdpCumXb/E9j8w11h5S54tT1xhifGfbbSm/ICrObRb3KA==} '@emnapi/runtime@1.10.0': resolution: {integrity: sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==} - '@emnapi/runtime@1.9.1': - resolution: {integrity: sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA==} - '@emnapi/wasi-threads@1.2.0': resolution: {integrity: sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==} - '@esbuild-kit/core-utils@3.3.2': - resolution: {integrity: sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ==} - deprecated: 'Merged into tsx: https://tsx.is' - - '@esbuild-kit/esm-loader@2.6.5': - resolution: {integrity: sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA==} - deprecated: 'Merged into tsx: https://tsx.is' - - '@esbuild/aix-ppc64@0.25.12': - resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==} - engines: {node: '>=18'} - cpu: [ppc64] - os: [aix] - '@esbuild/aix-ppc64@0.27.4': resolution: {integrity: sha512-cQPwL2mp2nSmHHJlCyoXgHGhbEPMrEEU5xhkcy3Hs/O7nGZqEpZ2sUtLaL9MORLtDfRvVl2/3PAuEkYZH0Ty8Q==} engines: {node: '>=18'} cpu: [ppc64] os: [aix] - '@esbuild/android-arm64@0.18.20': - resolution: {integrity: sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==} - engines: {node: '>=12'} - cpu: [arm64] - os: [android] - - '@esbuild/android-arm64@0.25.12': - resolution: {integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==} - engines: {node: '>=18'} - cpu: [arm64] - os: [android] - '@esbuild/android-arm64@0.27.4': resolution: {integrity: sha512-gdLscB7v75wRfu7QSm/zg6Rx29VLdy9eTr2t44sfTW7CxwAtQghZ4ZnqHk3/ogz7xao0QAgrkradbBzcqFPasw==} engines: {node: '>=18'} cpu: [arm64] os: [android] - '@esbuild/android-arm@0.18.20': - resolution: {integrity: sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==} + '@esbuild/android-arm@0.15.18': + resolution: {integrity: sha512-5GT+kcs2WVGjVs7+boataCkO5Fg0y4kCjzkB5bAip7H4jfnOS3dA6KPiww9W1OEKTKeAcUVhdZGvgI65OXmUnw==} engines: {node: '>=12'} cpu: [arm] os: [android] - '@esbuild/android-arm@0.25.12': - resolution: {integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==} - engines: {node: '>=18'} - cpu: [arm] - os: [android] - '@esbuild/android-arm@0.27.4': resolution: {integrity: sha512-X9bUgvxiC8CHAGKYufLIHGXPJWnr0OCdR0anD2e21vdvgCI8lIfqFbnoeOz7lBjdrAGUhqLZLcQo6MLhTO2DKQ==} engines: {node: '>=18'} cpu: [arm] os: [android] - '@esbuild/android-x64@0.18.20': - resolution: {integrity: sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==} - engines: {node: '>=12'} - cpu: [x64] - os: [android] - - '@esbuild/android-x64@0.25.12': - resolution: {integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==} - engines: {node: '>=18'} - cpu: [x64] - os: [android] - '@esbuild/android-x64@0.27.4': resolution: {integrity: sha512-PzPFnBNVF292sfpfhiyiXCGSn9HZg5BcAz+ivBuSsl6Rk4ga1oEXAamhOXRFyMcjwr2DVtm40G65N3GLeH1Lvw==} engines: {node: '>=18'} cpu: [x64] os: [android] - '@esbuild/darwin-arm64@0.18.20': - resolution: {integrity: sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==} - engines: {node: '>=12'} - cpu: [arm64] - os: [darwin] - - '@esbuild/darwin-arm64@0.25.12': - resolution: {integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==} - engines: {node: '>=18'} - cpu: [arm64] - os: [darwin] - '@esbuild/darwin-arm64@0.27.4': resolution: {integrity: sha512-b7xaGIwdJlht8ZFCvMkpDN6uiSmnxxK56N2GDTMYPr2/gzvfdQN8rTfBsvVKmIVY/X7EM+/hJKEIbbHs9oA4tQ==} engines: {node: '>=18'} cpu: [arm64] os: [darwin] - '@esbuild/darwin-x64@0.18.20': - resolution: {integrity: sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==} - engines: {node: '>=12'} - cpu: [x64] - os: [darwin] - - '@esbuild/darwin-x64@0.25.12': - resolution: {integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==} - engines: {node: '>=18'} - cpu: [x64] - os: [darwin] - '@esbuild/darwin-x64@0.27.4': resolution: {integrity: sha512-sR+OiKLwd15nmCdqpXMnuJ9W2kpy0KigzqScqHI3Hqwr7IXxBp3Yva+yJwoqh7rE8V77tdoheRYataNKL4QrPw==} engines: {node: '>=18'} cpu: [x64] os: [darwin] - '@esbuild/freebsd-arm64@0.18.20': - resolution: {integrity: sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==} - engines: {node: '>=12'} - cpu: [arm64] - os: [freebsd] - - '@esbuild/freebsd-arm64@0.25.12': - resolution: {integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==} - engines: {node: '>=18'} - cpu: [arm64] - os: [freebsd] - '@esbuild/freebsd-arm64@0.27.4': resolution: {integrity: sha512-jnfpKe+p79tCnm4GVav68A7tUFeKQwQyLgESwEAUzyxk/TJr4QdGog9sqWNcUbr/bZt/O/HXouspuQDd9JxFSw==} engines: {node: '>=18'} cpu: [arm64] os: [freebsd] - '@esbuild/freebsd-x64@0.18.20': - resolution: {integrity: sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==} - engines: {node: '>=12'} - cpu: [x64] - os: [freebsd] - - '@esbuild/freebsd-x64@0.25.12': - resolution: {integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==} - engines: {node: '>=18'} - cpu: [x64] - os: [freebsd] - '@esbuild/freebsd-x64@0.27.4': resolution: {integrity: sha512-2kb4ceA/CpfUrIcTUl1wrP/9ad9Atrp5J94Lq69w7UwOMolPIGrfLSvAKJp0RTvkPPyn6CIWrNy13kyLikZRZQ==} engines: {node: '>=18'} cpu: [x64] os: [freebsd] - '@esbuild/linux-arm64@0.18.20': - resolution: {integrity: sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==} - engines: {node: '>=12'} - cpu: [arm64] - os: [linux] - - '@esbuild/linux-arm64@0.25.12': - resolution: {integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==} - engines: {node: '>=18'} - cpu: [arm64] - os: [linux] - '@esbuild/linux-arm64@0.27.4': resolution: {integrity: sha512-7nQOttdzVGth1iz57kxg9uCz57dxQLHWxopL6mYuYthohPKEK0vU0C3O21CcBK6KDlkYVcnDXY099HcCDXd9dA==} engines: {node: '>=18'} cpu: [arm64] os: [linux] - '@esbuild/linux-arm@0.18.20': - resolution: {integrity: sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==} - engines: {node: '>=12'} - cpu: [arm] - os: [linux] - - '@esbuild/linux-arm@0.25.12': - resolution: {integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==} - engines: {node: '>=18'} - cpu: [arm] - os: [linux] - '@esbuild/linux-arm@0.27.4': resolution: {integrity: sha512-aBYgcIxX/wd5n2ys0yESGeYMGF+pv6g0DhZr3G1ZG4jMfruU9Tl1i2Z+Wnj9/KjGz1lTLCcorqE2viePZqj4Eg==} engines: {node: '>=18'} cpu: [arm] os: [linux] - '@esbuild/linux-ia32@0.18.20': - resolution: {integrity: sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==} - engines: {node: '>=12'} - cpu: [ia32] - os: [linux] - - '@esbuild/linux-ia32@0.25.12': - resolution: {integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==} - engines: {node: '>=18'} - cpu: [ia32] - os: [linux] - '@esbuild/linux-ia32@0.27.4': resolution: {integrity: sha512-oPtixtAIzgvzYcKBQM/qZ3R+9TEUd1aNJQu0HhGyqtx6oS7qTpvjheIWBbes4+qu1bNlo2V4cbkISr8q6gRBFA==} engines: {node: '>=18'} cpu: [ia32] os: [linux] - '@esbuild/linux-loong64@0.18.20': - resolution: {integrity: sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==} + '@esbuild/linux-loong64@0.15.18': + resolution: {integrity: sha512-L4jVKS82XVhw2nvzLg/19ClLWg0y27ulRwuP7lcyL6AbUWB5aPglXY3M21mauDQMDfRLs8cQmeT03r/+X3cZYQ==} engines: {node: '>=12'} cpu: [loong64] os: [linux] - '@esbuild/linux-loong64@0.25.12': - resolution: {integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==} - engines: {node: '>=18'} - cpu: [loong64] - os: [linux] - '@esbuild/linux-loong64@0.27.4': resolution: {integrity: sha512-8mL/vh8qeCoRcFH2nM8wm5uJP+ZcVYGGayMavi8GmRJjuI3g1v6Z7Ni0JJKAJW+m0EtUuARb6Lmp4hMjzCBWzA==} engines: {node: '>=18'} cpu: [loong64] os: [linux] - '@esbuild/linux-mips64el@0.18.20': - resolution: {integrity: sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==} - engines: {node: '>=12'} - cpu: [mips64el] - os: [linux] - - '@esbuild/linux-mips64el@0.25.12': - resolution: {integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==} - engines: {node: '>=18'} - cpu: [mips64el] - os: [linux] - '@esbuild/linux-mips64el@0.27.4': resolution: {integrity: sha512-1RdrWFFiiLIW7LQq9Q2NES+HiD4NyT8Itj9AUeCl0IVCA459WnPhREKgwrpaIfTOe+/2rdntisegiPWn/r/aAw==} engines: {node: '>=18'} cpu: [mips64el] os: [linux] - '@esbuild/linux-ppc64@0.18.20': - resolution: {integrity: sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==} - engines: {node: '>=12'} - cpu: [ppc64] - os: [linux] - - '@esbuild/linux-ppc64@0.25.12': - resolution: {integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==} - engines: {node: '>=18'} - cpu: [ppc64] - os: [linux] - '@esbuild/linux-ppc64@0.27.4': resolution: {integrity: sha512-tLCwNG47l3sd9lpfyx9LAGEGItCUeRCWeAx6x2Jmbav65nAwoPXfewtAdtbtit/pJFLUWOhpv0FpS6GQAmPrHA==} engines: {node: '>=18'} cpu: [ppc64] os: [linux] - '@esbuild/linux-riscv64@0.18.20': - resolution: {integrity: sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==} - engines: {node: '>=12'} - cpu: [riscv64] - os: [linux] - - '@esbuild/linux-riscv64@0.25.12': - resolution: {integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==} - engines: {node: '>=18'} - cpu: [riscv64] - os: [linux] - '@esbuild/linux-riscv64@0.27.4': resolution: {integrity: sha512-BnASypppbUWyqjd1KIpU4AUBiIhVr6YlHx/cnPgqEkNoVOhHg+YiSVxM1RLfiy4t9cAulbRGTNCKOcqHrEQLIw==} engines: {node: '>=18'} cpu: [riscv64] os: [linux] - '@esbuild/linux-s390x@0.18.20': - resolution: {integrity: sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==} - engines: {node: '>=12'} - cpu: [s390x] - os: [linux] - - '@esbuild/linux-s390x@0.25.12': - resolution: {integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==} - engines: {node: '>=18'} - cpu: [s390x] - os: [linux] - '@esbuild/linux-s390x@0.27.4': resolution: {integrity: sha512-+eUqgb/Z7vxVLezG8bVB9SfBie89gMueS+I0xYh2tJdw3vqA/0ImZJ2ROeWwVJN59ihBeZ7Tu92dF/5dy5FttA==} engines: {node: '>=18'} cpu: [s390x] os: [linux] - '@esbuild/linux-x64@0.18.20': - resolution: {integrity: sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==} - engines: {node: '>=12'} - cpu: [x64] - os: [linux] - - '@esbuild/linux-x64@0.25.12': - resolution: {integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==} - engines: {node: '>=18'} - cpu: [x64] - os: [linux] - '@esbuild/linux-x64@0.27.4': resolution: {integrity: sha512-S5qOXrKV8BQEzJPVxAwnryi2+Iq5pB40gTEIT69BQONqR7JH1EPIcQ/Uiv9mCnn05jff9umq/5nqzxlqTOg9NA==} engines: {node: '>=18'} cpu: [x64] os: [linux] - '@esbuild/netbsd-arm64@0.25.12': - resolution: {integrity: sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==} - engines: {node: '>=18'} - cpu: [arm64] - os: [netbsd] - '@esbuild/netbsd-arm64@0.27.4': resolution: {integrity: sha512-xHT8X4sb0GS8qTqiwzHqpY00C95DPAq7nAwX35Ie/s+LO9830hrMd3oX0ZMKLvy7vsonee73x0lmcdOVXFzd6Q==} engines: {node: '>=18'} cpu: [arm64] os: [netbsd] - '@esbuild/netbsd-x64@0.18.20': - resolution: {integrity: sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==} - engines: {node: '>=12'} - cpu: [x64] - os: [netbsd] - - '@esbuild/netbsd-x64@0.25.12': - resolution: {integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==} - engines: {node: '>=18'} - cpu: [x64] - os: [netbsd] - '@esbuild/netbsd-x64@0.27.4': resolution: {integrity: sha512-RugOvOdXfdyi5Tyv40kgQnI0byv66BFgAqjdgtAKqHoZTbTF2QqfQrFwa7cHEORJf6X2ht+l9ABLMP0dnKYsgg==} engines: {node: '>=18'} cpu: [x64] os: [netbsd] - '@esbuild/openbsd-arm64@0.25.12': - resolution: {integrity: sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==} - engines: {node: '>=18'} - cpu: [arm64] - os: [openbsd] - '@esbuild/openbsd-arm64@0.27.4': resolution: {integrity: sha512-2MyL3IAaTX+1/qP0O1SwskwcwCoOI4kV2IBX1xYnDDqthmq5ArrW94qSIKCAuRraMgPOmG0RDTA74mzYNQA9ow==} engines: {node: '>=18'} cpu: [arm64] os: [openbsd] - '@esbuild/openbsd-x64@0.18.20': - resolution: {integrity: sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==} - engines: {node: '>=12'} - cpu: [x64] - os: [openbsd] - - '@esbuild/openbsd-x64@0.25.12': - resolution: {integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==} - engines: {node: '>=18'} - cpu: [x64] - os: [openbsd] - '@esbuild/openbsd-x64@0.27.4': resolution: {integrity: sha512-u8fg/jQ5aQDfsnIV6+KwLOf1CmJnfu1ShpwqdwC0uA7ZPwFws55Ngc12vBdeUdnuWoQYx/SOQLGDcdlfXhYmXQ==} engines: {node: '>=18'} cpu: [x64] os: [openbsd] - '@esbuild/openharmony-arm64@0.25.12': - resolution: {integrity: sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==} - engines: {node: '>=18'} - cpu: [arm64] - os: [openharmony] - '@esbuild/openharmony-arm64@0.27.4': resolution: {integrity: sha512-JkTZrl6VbyO8lDQO3yv26nNr2RM2yZzNrNHEsj9bm6dOwwu9OYN28CjzZkH57bh4w0I2F7IodpQvUAEd1mbWXg==} engines: {node: '>=18'} cpu: [arm64] os: [openharmony] - '@esbuild/sunos-x64@0.18.20': - resolution: {integrity: sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==} - engines: {node: '>=12'} - cpu: [x64] - os: [sunos] - - '@esbuild/sunos-x64@0.25.12': - resolution: {integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==} - engines: {node: '>=18'} - cpu: [x64] - os: [sunos] - '@esbuild/sunos-x64@0.27.4': resolution: {integrity: sha512-/gOzgaewZJfeJTlsWhvUEmUG4tWEY2Spp5M20INYRg2ZKl9QPO3QEEgPeRtLjEWSW8FilRNacPOg8R1uaYkA6g==} engines: {node: '>=18'} cpu: [x64] os: [sunos] - '@esbuild/win32-arm64@0.18.20': - resolution: {integrity: sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==} - engines: {node: '>=12'} - cpu: [arm64] - os: [win32] - - '@esbuild/win32-arm64@0.25.12': - resolution: {integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==} - engines: {node: '>=18'} - cpu: [arm64] - os: [win32] - '@esbuild/win32-arm64@0.27.4': resolution: {integrity: sha512-Z9SExBg2y32smoDQdf1HRwHRt6vAHLXcxD2uGgO/v2jK7Y718Ix4ndsbNMU/+1Qiem9OiOdaqitioZwxivhXYg==} engines: {node: '>=18'} cpu: [arm64] os: [win32] - '@esbuild/win32-ia32@0.18.20': - resolution: {integrity: sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==} - engines: {node: '>=12'} - cpu: [ia32] - os: [win32] - - '@esbuild/win32-ia32@0.25.12': - resolution: {integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==} - engines: {node: '>=18'} - cpu: [ia32] - os: [win32] - '@esbuild/win32-ia32@0.27.4': resolution: {integrity: sha512-DAyGLS0Jz5G5iixEbMHi5KdiApqHBWMGzTtMiJ72ZOLhbu/bzxgAe8Ue8CTS3n3HbIUHQz/L51yMdGMeoxXNJw==} engines: {node: '>=18'} cpu: [ia32] os: [win32] - '@esbuild/win32-x64@0.18.20': - resolution: {integrity: sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==} - engines: {node: '>=12'} - cpu: [x64] - os: [win32] - - '@esbuild/win32-x64@0.25.12': - resolution: {integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==} - engines: {node: '>=18'} - cpu: [x64] - os: [win32] - '@esbuild/win32-x64@0.27.4': resolution: {integrity: sha512-+knoa0BDoeXgkNvvV1vvbZX4+hizelrkwmGJBdT17t8FNPwG2lKemmuMZlmaNQ3ws3DKKCxpb4zRZEIp3UxFCg==} engines: {node: '>=18'} @@ -1555,69 +1265,15 @@ packages: engines: {node: '>=18'} hasBin: true - '@ethersproject/abstract-provider@5.8.0': - resolution: {integrity: sha512-wC9SFcmh4UK0oKuLJQItoQdzS/qZ51EJegK6EmAWlh+OptpQ/npECOR3QqECd8iGHC0RJb4WKbVdSfif4ammrg==} - - '@ethersproject/abstract-signer@5.8.0': - resolution: {integrity: sha512-N0XhZTswXcmIZQdYtUnd79VJzvEwXQw6PK0dTl9VoYrEBxxCPXqS0Eod7q5TNKRxe1/5WUMuR0u0nqTF/avdCA==} - - '@ethersproject/address@5.8.0': - resolution: {integrity: sha512-GhH/abcC46LJwshoN+uBNoKVFPxUuZm6dA257z0vZkKmU1+t8xTn8oK7B9qrj8W2rFRMch4gbJl6PmVxjxBEBA==} - - '@ethersproject/base64@5.8.0': - resolution: {integrity: sha512-lN0oIwfkYj9LbPx4xEkie6rAMJtySbpOAFXSDVQaBnAzYfB4X2Qr+FXJGxMoc3Bxp2Sm8OwvzMrywxyw0gLjIQ==} - - '@ethersproject/basex@5.8.0': - resolution: {integrity: sha512-PIgTszMlDRmNwW9nhS6iqtVfdTAKosA7llYXNmGPw4YAI1PUyMv28988wAb41/gHF/WqGdoLv0erHaRcHRKW2Q==} - - '@ethersproject/bignumber@5.8.0': - resolution: {integrity: sha512-ZyaT24bHaSeJon2tGPKIiHszWjD/54Sz8t57Toch475lCLljC6MgPmxk7Gtzz+ddNN5LuHea9qhAe0x3D+uYPA==} - '@ethersproject/bytes@5.8.0': resolution: {integrity: sha512-vTkeohgJVCPVHu5c25XWaWQOZ4v+DkGoC42/TS2ond+PARCxTJvgTFUNDZovyQ/uAQ4EcpqqowKydcdmRKjg7A==} - '@ethersproject/constants@5.8.0': - resolution: {integrity: sha512-wigX4lrf5Vu+axVTIvNsuL6YrV4O5AXl5ubcURKMEME5TnWBouUh0CDTWxZ2GpnRn1kcCgE7l8O5+VbV9QTTcg==} - - '@ethersproject/hash@5.8.0': - resolution: {integrity: sha512-ac/lBcTbEWW/VGJij0CNSw/wPcw9bSRgCB0AIBz8CvED/jfvDoV9hsIIiWfvWmFEi8RcXtlNwp2jv6ozWOsooA==} - '@ethersproject/keccak256@5.8.0': resolution: {integrity: sha512-A1pkKLZSz8pDaQ1ftutZoaN46I6+jvuqugx5KYNeQOPqq+JZ0Txm7dlWesCHB5cndJSu5vP2VKptKf7cksERng==} '@ethersproject/logger@5.8.0': resolution: {integrity: sha512-Qe6knGmY+zPPWTC+wQrpitodgBfH7XoceCGL5bJVejmH+yCS3R8jJm8iiWuvWbG76RUmyEG53oqv6GMVWqunjA==} - '@ethersproject/networks@5.8.0': - resolution: {integrity: sha512-egPJh3aPVAzbHwq8DD7Po53J4OUSsA1MjQp8Vf/OZPav5rlmWUaFLiq8cvQiGK0Z5K6LYzm29+VA/p4RL1FzNg==} - - '@ethersproject/properties@5.8.0': - resolution: {integrity: sha512-PYuiEoQ+FMaZZNGrStmN7+lWjlsoufGIHdww7454FIaGdbe/p5rnaCXTr5MtBYl3NkeoVhHZuyzChPeGeKIpQw==} - - '@ethersproject/providers@5.8.0': - resolution: {integrity: sha512-3Il3oTzEx3o6kzcg9ZzbE+oCZYyY+3Zh83sKkn4s1DZfTUjIegHnN2Cm0kbn9YFy45FDVcuCLLONhU7ny0SsCw==} - - '@ethersproject/random@5.8.0': - resolution: {integrity: sha512-E4I5TDl7SVqyg4/kkA/qTfuLWAQGXmSOgYyO01So8hLfwgKvYK5snIlzxJMk72IFdG/7oh8yuSqY2KX7MMwg+A==} - - '@ethersproject/rlp@5.8.0': - resolution: {integrity: sha512-LqZgAznqDbiEunaUvykH2JAoXTT9NV0Atqk8rQN9nx9SEgThA/WMx5DnW8a9FOufo//6FZOCHZ+XiClzgbqV9Q==} - - '@ethersproject/sha2@5.8.0': - resolution: {integrity: sha512-dDOUrXr9wF/YFltgTBYS0tKslPEKr6AekjqDW2dbn1L1xmjGR+9GiKu4ajxovnrDbwxAKdHjW8jNcwfz8PAz4A==} - - '@ethersproject/signing-key@5.8.0': - resolution: {integrity: sha512-LrPW2ZxoigFi6U6aVkFN/fa9Yx/+4AtIUe4/HACTvKJdhm0eeb107EVCIQcrLZkxaSIgc/eCrX8Q1GtbH+9n3w==} - - '@ethersproject/strings@5.8.0': - resolution: {integrity: sha512-qWEAk0MAvl0LszjdfnZ2uC8xbR2wdv4cDabyHiBh3Cldq/T8dPH3V4BbBsAYJUeonwD+8afVXld274Ls+Y1xXg==} - - '@ethersproject/transactions@5.8.0': - resolution: {integrity: sha512-UglxSDjByHG0TuU17bDfCemZ3AnKO2vYrL5/2n2oXvKzvb7Cz+W9gOWXKARjp2URVwcWlQlPOEQyAviKwT4AHg==} - - '@ethersproject/web@5.8.0': - resolution: {integrity: sha512-j7+Ksi/9KfGviws6Qtf9Q7KCqRhpwrYKQPs+JBA/rKVFF/yaWLHJEH3zfVP2plVu+eys0d2DlFmhoQJayFewcw==} - '@faker-js/faker@10.4.0': resolution: {integrity: sha512-sDBWI3yLy8EcDzgobvJTWq1MJYzAkQdpjXuPukga9wXonhpMRvd1Izuo2Qgwey2OiEoRIBr35RMU9HJRoOHzpw==} engines: {node: ^20.19.0 || ^22.13.0 || ^23.5.0 || >=24.0.0, npm: '>=10'} @@ -1844,59 +1500,59 @@ packages: '@emnapi/core': ^1.7.1 '@emnapi/runtime': ^1.7.1 + '@next/env@15.5.15': + resolution: {integrity: sha512-vcmyu5/MyFzN7CdqRHO3uHO44p/QPCZkuTUXroeUmhNP8bL5PHFEhik22JUazt+CDDoD6EpBYRCaS2pISL+/hg==} + '@next/env@16.2.1': resolution: {integrity: sha512-n8P/HCkIWW+gVal2Z8XqXJ6aB3J0tuM29OcHpCsobWlChH/SITBs1DFBk/HajgrwDkqqBXPbuUuzgDvUekREPg==} - '@next/env@16.2.4': - resolution: {integrity: sha512-dKkkOzOSwFYe5RX6y26fZgkSpVAlIOJKQHIiydQcrWH6y/97+RceSOAdjZ14Qa3zLduVUy0TXcn+EiM6t4rPgw==} - '@next/eslint-plugin-next@15.5.3': resolution: {integrity: sha512-SdhaKdko6dpsSr0DldkESItVrnPYB1NS2NpShCSX5lc7SSQmLZt5Mug6t2xbiuVWEVDLZSuIAoQyYVBYp0dR5g==} - '@next/swc-darwin-arm64@16.2.4': - resolution: {integrity: sha512-OXTFFox5EKN1Ym08vfrz+OXxmCcEjT4SFMbNRsWZE99dMqt2Kcusl5MqPXcW232RYkMLQTy0hqgAMEsfEd/l2A==} + '@next/swc-darwin-arm64@15.5.15': + resolution: {integrity: sha512-6PvFO2Tzt10GFK2Ro9tAVEtacMqRmTarYMFKAnV2vYMdwWc73xzmDQyAV7SwEdMhzmiRoo7+m88DuiXlJlGeaw==} engines: {node: '>= 10'} cpu: [arm64] os: [darwin] - '@next/swc-darwin-x64@16.2.4': - resolution: {integrity: sha512-XhpVnUfmYWvD3YrXu55XdcAkQtOnvaI6wtQa8fuF5fGoKoxIUZ0kWPtcOfqJEWngFF/lOS9l3+O9CcownhiQxQ==} + '@next/swc-darwin-x64@15.5.15': + resolution: {integrity: sha512-G+YNV+z6FDZTp/+IdGyIMFqalBTaQSnvAA+X/hrt+eaTRFSznRMz9K7rTmzvM6tDmKegNtyzgufZW0HwVzEqaQ==} engines: {node: '>= 10'} cpu: [x64] os: [darwin] - '@next/swc-linux-arm64-gnu@16.2.4': - resolution: {integrity: sha512-Mx/tjlNA3G8kg14QvuGAJ4xBwPk1tUHq56JxZ8CXnZwz1Etz714soCEzGQQzVMz4bEnGPowzkV6Xrp6wAkEWOQ==} + '@next/swc-linux-arm64-gnu@15.5.15': + resolution: {integrity: sha512-eVkrMcVIBqGfXB+QUC7jjZ94Z6uX/dNStbQFabewAnk13Uy18Igd1YZ/GtPRzdhtm7QwC0e6o7zOQecul4iC1w==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - '@next/swc-linux-arm64-musl@16.2.4': - resolution: {integrity: sha512-iVMMp14514u7Nup2umQS03nT/bN9HurK8ufylC3FZNykrwjtx7V1A7+4kvhbDSCeonTVqV3Txnv0Lu+m2oDXNg==} + '@next/swc-linux-arm64-musl@15.5.15': + resolution: {integrity: sha512-RwSHKMQ7InLy5GfkY2/n5PcFycKA08qI1VST78n09nN36nUPqCvGSMiLXlfUmzmpQpF6XeBYP2KRWHi0UW3uNg==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - '@next/swc-linux-x64-gnu@16.2.4': - resolution: {integrity: sha512-EZOvm1aQWgnI/N/xcWOlnS3RQBk0VtVav5Zo7n4p0A7UKyTDx047k8opDbXgBpHl4CulRqRfbw3QrX2w5UOXMQ==} + '@next/swc-linux-x64-gnu@15.5.15': + resolution: {integrity: sha512-nplqvY86LakS+eeiuWsNWvfmK8pFcOEW7ZtVRt4QH70lL+0x6LG/m1OpJ/tvrbwjmR8HH9/fH2jzW1GlL03TIg==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - '@next/swc-linux-x64-musl@16.2.4': - resolution: {integrity: sha512-h9FxsngCm9cTBf71AR4fGznDEDx1hS7+kSEiIRjq5kO1oXWm07DxVGZjCvk0SGx7TSjlUqhI8oOyz7NfwAdPoA==} + '@next/swc-linux-x64-musl@15.5.15': + resolution: {integrity: sha512-eAgl9NKQ84/sww0v81DQINl/vL2IBxD7sMybd0cWRw6wqgouVI53brVRBrggqBRP/NWeIAE1dm5cbKYoiMlqDQ==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - '@next/swc-win32-arm64-msvc@16.2.4': - resolution: {integrity: sha512-3NdJV5OXMSOeJYijX+bjaLge3mJBlh4ybydbT4GFoB/2hAojWHtMhl3CYlYoMrjPuodp0nzFVi4Tj2+WaMg+Ow==} + '@next/swc-win32-arm64-msvc@15.5.15': + resolution: {integrity: sha512-GJVZC86lzSquh0MtvZT+L7G8+jMnJcldloOjA8Kf3wXvBrvb6OGe2MzPuALxFshSm/IpwUtD2mIoof39ymf52A==} engines: {node: '>= 10'} cpu: [arm64] os: [win32] - '@next/swc-win32-x64-msvc@16.2.4': - resolution: {integrity: sha512-kMVGgsqhO5YTYODD9IPGGhA6iprWidQckK3LmPeW08PIFENRmgfb4MjXHO+p//d+ts2rpjvK5gXWzXSMrPl9cw==} + '@next/swc-win32-x64-msvc@15.5.15': + resolution: {integrity: sha512-nFucjVdwlFqxh/JG3hWSJ4p8+YJV7Ii8aPDuBQULB6DzUF4UNZETXLfEUk+oI2zEznWWULPt7MeuTE6xtK1HSA==} engines: {node: '>= 10'} cpu: [x64] os: [win32] @@ -3445,23 +3101,17 @@ packages: '@web3-react/abstract-connector@6.0.7': resolution: {integrity: sha512-RhQasA4Ox8CxUC0OENc1AJJm8UTybu/oOCM61Zjg6y0iF7Z0sqv1Ai1VdhC33hrQpA8qSBgoXN9PaP8jKmtdqg==} - '@web3-react/core@8.2.3': - resolution: {integrity: sha512-0ezmRKhqQpoa9ct2/3erg60zBXfC/f/liYR1mfSGKtIroRkLnPARigZSV6pI+fi8bhfGJ0RKtFWyTCCWZzdq1w==} + '@web3-react/core@6.1.9': + resolution: {integrity: sha512-P877DslsbAkWIlMANpWiK7pCvNwlz0kJC0EGckuVh0wlA23J4UnFxq6xyOaxkxaDCu14rA/tAO0NbwjcXTQgSA==} peerDependencies: react: '>=16.8' '@web3-react/injected-connector@6.0.7': resolution: {integrity: sha512-Y7aJSz6pg+MWKtvdyuqyy6LWuH+4Tqtph1LWfiyVms9II9ar/9B/de4R8wh4wjg91wmHkU+D75yP09E/Soh2RA==} - '@web3-react/store@8.2.3': - resolution: {integrity: sha512-qUJQ5pDsYYDra+/+glq2BmIS43HYAiEZ22sLLVh6E75WiZKRNOOqUxBDPe33KTIn718DLt51j+wd2FT+oT/kJQ==} - '@web3-react/types@6.0.7': resolution: {integrity: sha512-ofGmfDhxmNT1/P/MgVa8IKSkCStFiyvXe+U5tyZurKdrtTDFU+wJ/LxClPDtFerWpczNFPUSrKcuhfPX1sI6+A==} - '@web3-react/types@8.2.3': - resolution: {integrity: sha512-kSG90QkN+n7IOtp10nQ44oS8J7jzfH9EmqnruwBpCGybh1FM/ohyRvUKWYZNfNE4wsjTSpKsINR0/VdDsZMHyg==} - abitype@0.7.1: resolution: {integrity: sha512-VBkRHTDZf9Myaek/dO3yMmOzB/y2s3Zo6nVU7yaw1G+TvCHAjwaJzNGN9yo4K5D8bU/VZXKP1EJpRhFr862PlQ==} peerDependencies: @@ -3621,27 +3271,13 @@ packages: engines: {node: '>=6.0.0'} hasBin: true - baseline-browser-mapping@2.10.21: - resolution: {integrity: sha512-Q+rUQ7Uz8AHM7DEaNdwvfFCTq7a43lNTzuS94eiWqwyxfV/wJv+oUivef51T91mmRY4d4A1u9rcSvkeufCVXlA==} - engines: {node: '>=6.0.0'} - hasBin: true - bcryptjs@3.0.3: resolution: {integrity: sha512-GlF5wPWnSa/X5LKM1o0wz0suXIINz1iHRLvTS+sLyi7XPbe5ycmYI3DlZqVGZZtDgl4DmasFg7gOB3JYbphV5g==} hasBin: true - bech32@1.1.4: - resolution: {integrity: sha512-s0IrSOzLlbvX7yp4WBfPITzpAU8sqQcpsmwXDiKwrG4r491vwCO/XpejasRNl0piBMe/DvP4Tz0mIS/X1DPJBQ==} - bignumber.js@9.3.1: resolution: {integrity: sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==} - bn.js@4.12.3: - resolution: {integrity: sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g==} - - bn.js@5.2.3: - resolution: {integrity: sha512-EAcmnPkxpntVL+DS7bO1zhcZNvCkxqtkd0ZY53h06GNQ3DEkkGZ/gKgmDv6DdZQGj9BgfSPKtJJ7Dp1GPP8f7w==} - boolbase@1.0.0: resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} @@ -3651,6 +3287,9 @@ packages: brace-expansion@1.1.12: resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} + brace-expansion@2.1.0: + resolution: {integrity: sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==} + brace-expansion@5.0.5: resolution: {integrity: sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==} engines: {node: 18 || 20 || >=22} @@ -3659,9 +3298,6 @@ packages: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} - brorand@1.1.0: - resolution: {integrity: sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w==} - browserslist@4.28.1: resolution: {integrity: sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} @@ -3670,9 +3306,6 @@ packages: buffer-equal-constant-time@1.0.1: resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} - buffer-from@1.1.2: - resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} - buffer@6.0.3: resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} @@ -3703,8 +3336,9 @@ packages: resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} engines: {node: '>=10'} - caniuse-lite@1.0.30001781: - resolution: {integrity: sha512-RdwNCyMsNBftLjW6w01z8bKEvT6e/5tpPVEgtn22TiLGlstHOVecsX2KHFkD5e/vRnIE4EGzpuIODb3mtswtkw==} + camelcase@7.0.1: + resolution: {integrity: sha512-xlx1yCK2Oc1APsPXDL2LdlNP6+uu8OCDdhOBSVT279M/S+y75O30C2VuD8T2ogdePBBl7PfPF4504tnLgX3zfw==} + engines: {node: '>=14.16'} caniuse-lite@1.0.30001790: resolution: {integrity: sha512-bOoxfJPyYo+ds6W0YfptaCWbFnJYjh2Y1Eow5lRv+vI2u8ganPZqNm1JwNh0t2ELQCqIWg4B3dWEusgAmsoyOw==} @@ -3721,6 +3355,10 @@ packages: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} + chalk@5.6.2: + resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==} + engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + character-entities-legacy@3.0.0: resolution: {integrity: sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==} @@ -3736,6 +3374,10 @@ packages: classnames@2.5.1: resolution: {integrity: sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==} + cli-color@2.0.4: + resolution: {integrity: sha512-zlnpg0jNcibNrO7GG9IeHH7maWFeCz+Ja1wx/7tZNU5ASSSSZ+/qZciM0/LHCYxSdqv5h2sdbQ/PXYdOuetXvA==} + engines: {node: '>=0.10'} + client-only@0.0.1: resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} @@ -3895,6 +3537,10 @@ packages: resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==} engines: {node: '>=12'} + d@1.0.2: + resolution: {integrity: sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw==} + engines: {node: '>=0.12'} + damerau-levenshtein@1.0.8: resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==} @@ -3984,6 +3630,9 @@ packages: diacritics@1.3.0: resolution: {integrity: sha512-wlwEkqcsaxvPJML+rDh/2iS824jbREk6DUMUKkEaSlxdYHeS43cClJtsWglvw2RfeXGm6ohKDqsXteJ5sP5enA==} + difflib@0.2.4: + resolution: {integrity: sha512-9YVwmMb0wQHQNr5J9m6BSj6fk4pfGITGQOOs+D9Fl+INODWFOfvhIU1hNv6GgR1RBoC/9NJcwu77zShxV0kT7w==} + dijkstrajs@1.0.3: resolution: {integrity: sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==} @@ -4021,8 +3670,12 @@ packages: resolution: {integrity: sha512-pYxfDYpued//QpnLIm4Avk7rsNtAtQkUES2cwAYSvD/wd2pKD71gN2Ebj3e7klzXwjocvE8c5vx/1fxwpqmSxA==} engines: {node: '>=4'} - drizzle-kit@0.31.10: - resolution: {integrity: sha512-7OZcmQUrdGI+DUNNsKBn1aW8qSoKuTH7d0mYgSP8bAzdFzKoovxEFnoGQp2dVs82EOJeYycqRtciopszwUf8bw==} + dreamopt@0.8.0: + resolution: {integrity: sha512-vyJTp8+mC+G+5dfgsY+r3ckxlz+QMX40VjPQsZc5gxVAxLmi64TBoVkP54A/pRAXMXsbu2GMMBrZPxNv23waMg==} + engines: {node: '>=0.4.0'} + + drizzle-kit@0.18.1: + resolution: {integrity: sha512-Oqie227W2Dd7FuqX4pvQWeClSvnoPCIn2cO9JueeLWZqj3tpdBhnbgt4nLHhBbOdWRlTLYwXnkTDW3hYym/gGQ==} hasBin: true drizzle-orm@0.45.1: @@ -4127,9 +3780,6 @@ packages: electron-to-chromium@1.5.322: resolution: {integrity: sha512-vFU34OcrvMcH66T+dYC3G4nURmgfDVewMIu6Q2urXpumAPSMmzvcn04KVVV8Opikq8Vs5nUbO/8laNhNRqSzYw==} - elliptic@6.6.1: - resolution: {integrity: sha512-RaddvvMatK2LJHqFJ+YA4WysVN5Ita9E35botqIYspQ4TkRAlCicdzKOjlyv/1Za5RyTNn7di//eEV0uTAfe3g==} - emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} @@ -4185,14 +3835,148 @@ packages: es-toolkit@1.45.1: resolution: {integrity: sha512-/jhoOj/Fx+A+IIyDNOvO3TItGmlMKhtX8ISAHKE90c4b/k1tqaqEZ+uUqfpU8DMnW5cgNJv606zS55jGvza0Xw==} - esbuild@0.18.20: - resolution: {integrity: sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==} + es5-ext@0.10.64: + resolution: {integrity: sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg==} + engines: {node: '>=0.10'} + + es6-iterator@2.0.3: + resolution: {integrity: sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==} + + es6-symbol@3.1.4: + resolution: {integrity: sha512-U9bFFjX8tFiATgtkJ1zg25+KviIXpgRvRHS8sau3GfhVzThRQrOeksPeT0BWW2MNZs1OEWJ1DPXOQMn0KKRkvg==} + engines: {node: '>=0.12'} + + es6-weak-map@2.0.3: + resolution: {integrity: sha512-p5um32HOTO1kP+w7PRnB+5lQ43Z6muuMuIMffvDN8ZB4GcnjLBV6zGStpbASIMk4DCAvEaamhe2zhyCb/QXXsA==} + + esbuild-android-64@0.15.18: + resolution: {integrity: sha512-wnpt3OXRhcjfIDSZu9bnzT4/TNTDsOUvip0foZOUBG7QbSt//w3QV4FInVJxNhKc/ErhUxc5z4QjHtMi7/TbgA==} engines: {node: '>=12'} - hasBin: true + cpu: [x64] + os: [android] - esbuild@0.25.12: - resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==} - engines: {node: '>=18'} + esbuild-android-arm64@0.15.18: + resolution: {integrity: sha512-G4xu89B8FCzav9XU8EjsXacCKSG2FT7wW9J6hOc18soEHJdtWu03L3TQDGf0geNxfLTtxENKBzMSq9LlbjS8OQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + + esbuild-darwin-64@0.15.18: + resolution: {integrity: sha512-2WAvs95uPnVJPuYKP0Eqx+Dl/jaYseZEUUT1sjg97TJa4oBtbAKnPnl3b5M9l51/nbx7+QAEtuummJZW0sBEmg==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + + esbuild-darwin-arm64@0.15.18: + resolution: {integrity: sha512-tKPSxcTJ5OmNb1btVikATJ8NftlyNlc8BVNtyT/UAr62JFOhwHlnoPrhYWz09akBLHI9nElFVfWSTSRsrZiDUA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + + esbuild-freebsd-64@0.15.18: + resolution: {integrity: sha512-TT3uBUxkteAjR1QbsmvSsjpKjOX6UkCstr8nMr+q7zi3NuZ1oIpa8U41Y8I8dJH2fJgdC3Dj3CXO5biLQpfdZA==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + + esbuild-freebsd-arm64@0.15.18: + resolution: {integrity: sha512-R/oVr+X3Tkh+S0+tL41wRMbdWtpWB8hEAMsOXDumSSa6qJR89U0S/PpLXrGF7Wk/JykfpWNokERUpCeHDl47wA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + + esbuild-linux-32@0.15.18: + resolution: {integrity: sha512-lphF3HiCSYtaa9p1DtXndiQEeQDKPl9eN/XNoBf2amEghugNuqXNZA/ZovthNE2aa4EN43WroO0B85xVSjYkbg==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + + esbuild-linux-64@0.15.18: + resolution: {integrity: sha512-hNSeP97IviD7oxLKFuii5sDPJ+QHeiFTFLoLm7NZQligur8poNOWGIgpQ7Qf8Balb69hptMZzyOBIPtY09GZYw==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + + esbuild-linux-arm64@0.15.18: + resolution: {integrity: sha512-54qr8kg/6ilcxd+0V3h9rjT4qmjc0CccMVWrjOEM/pEcUzt8X62HfBSeZfT2ECpM7104mk4yfQXkosY8Quptug==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + + esbuild-linux-arm@0.15.18: + resolution: {integrity: sha512-UH779gstRblS4aoS2qpMl3wjg7U0j+ygu3GjIeTonCcN79ZvpPee12Qun3vcdxX+37O5LFxz39XeW2I9bybMVA==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + + esbuild-linux-mips64le@0.15.18: + resolution: {integrity: sha512-Mk6Ppwzzz3YbMl/ZZL2P0q1tnYqh/trYZ1VfNP47C31yT0K8t9s7Z077QrDA/guU60tGNp2GOwCQnp+DYv7bxQ==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + + esbuild-linux-ppc64le@0.15.18: + resolution: {integrity: sha512-b0XkN4pL9WUulPTa/VKHx2wLCgvIAbgwABGnKMY19WhKZPT+8BxhZdqz6EgkqCLld7X5qiCY2F/bfpUUlnFZ9w==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + + esbuild-linux-riscv64@0.15.18: + resolution: {integrity: sha512-ba2COaoF5wL6VLZWn04k+ACZjZ6NYniMSQStodFKH/Pu6RxzQqzsmjR1t9QC89VYJxBeyVPTaHuBMCejl3O/xg==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + + esbuild-linux-s390x@0.15.18: + resolution: {integrity: sha512-VbpGuXEl5FCs1wDVp93O8UIzl3ZrglgnSQ+Hu79g7hZu6te6/YHgVJxCM2SqfIila0J3k0csfnf8VD2W7u2kzQ==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + + esbuild-netbsd-64@0.15.18: + resolution: {integrity: sha512-98ukeCdvdX7wr1vUYQzKo4kQ0N2p27H7I11maINv73fVEXt2kyh4K4m9f35U1K43Xc2QGXlzAw0K9yoU7JUjOg==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + + esbuild-openbsd-64@0.15.18: + resolution: {integrity: sha512-yK5NCcH31Uae076AyQAXeJzt/vxIo9+omZRKj1pauhk3ITuADzuOx5N2fdHrAKPxN+zH3w96uFKlY7yIn490xQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + + esbuild-register@3.6.0: + resolution: {integrity: sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg==} + peerDependencies: + esbuild: '>=0.12 <1' + + esbuild-sunos-64@0.15.18: + resolution: {integrity: sha512-On22LLFlBeLNj/YF3FT+cXcyKPEI263nflYlAhz5crxtp3yRG1Ugfr7ITyxmCmjm4vbN/dGrb/B7w7U8yJR9yw==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + + esbuild-windows-32@0.15.18: + resolution: {integrity: sha512-o+eyLu2MjVny/nt+E0uPnBxYuJHBvho8vWsC2lV61A7wwTWC3jkN2w36jtA+yv1UgYkHRihPuQsL23hsCYGcOQ==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + + esbuild-windows-64@0.15.18: + resolution: {integrity: sha512-qinug1iTTaIIrCorAUjR0fcBk24fjzEedFYhhispP8Oc7SFvs+XeW3YpAKiKp8dRpizl4YYAhxMjlftAMJiaUw==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + + esbuild-windows-arm64@0.15.18: + resolution: {integrity: sha512-q9bsYzegpZcLziq0zgUi5KqGVtfhjxGbnksaBFYmWLxeV/S1fK4OLdq2DFYnXcLMjlZw2L0jLsk1eGoB522WXQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + + esbuild@0.15.18: + resolution: {integrity: sha512-x/R72SmW3sSFRm5zrrIjAhCeQSAWoni3CmHEqfQrZIQTM3lVCdehdwuIqaOtfC2slvpdlLa62GYoN8SxT23m6Q==} + engines: {node: '>=12'} hasBin: true esbuild@0.27.4: @@ -4308,6 +4092,10 @@ packages: jiti: optional: true + esniff@2.0.1: + resolution: {integrity: sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg==} + engines: {node: '>=0.10'} + espree@10.4.0: resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -4338,6 +4126,9 @@ packages: resolution: {integrity: sha512-U1wulmetNymijEhpSEQ7Ct/P/Jw9/e7R1j5XIbPRydgV2DjLVMsULDlNksq3RQnFgKoLlZf88ijYtWEXcPa07A==} engines: {node: '>=14.0.0'} + event-emitter@0.3.5: + resolution: {integrity: sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA==} + eventemitter3@5.0.4: resolution: {integrity: sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==} @@ -4349,6 +4140,9 @@ packages: resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} engines: {node: '>=12.0.0'} + ext@1.7.0: + resolution: {integrity: sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==} + extend@3.0.2: resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} @@ -4534,6 +4328,11 @@ packages: resolution: {integrity: sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==} deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + glob@8.1.0: + resolution: {integrity: sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==} + engines: {node: '>=12'} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + globals@14.0.0: resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} engines: {node: '>=18'} @@ -4557,6 +4356,9 @@ packages: graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + hanji@0.0.5: + resolution: {integrity: sha512-Abxw1Lq+TnYiL4BueXqMau222fPSPMFtya8HdpWsz/xVAhifXou71mPh/kY2+08RgFcVccjG3uZHs6K5HAe3zw==} + has-bigints@1.1.0: resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==} engines: {node: '>= 0.4'} @@ -4580,9 +4382,6 @@ packages: resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} engines: {node: '>= 0.4'} - hash.js@1.1.7: - resolution: {integrity: sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==} - hasown@2.0.2: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} @@ -4593,15 +4392,15 @@ packages: hastscript@9.0.1: resolution: {integrity: sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==} + heap@0.2.7: + resolution: {integrity: sha512-2bsegYkkHO+h/9MGbn6KWcE45cHZgPANo5LXF7EvWdT0yT2EguSVO1nDgU5c8+ZOPwp2vMNa7YFsJhVcDR9Sdg==} + highlight.js@10.7.3: resolution: {integrity: sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==} highlightjs-vue@1.0.0: resolution: {integrity: sha512-PDEfEF102G23vHmPhLyPboFCD+BkMGu+GuJe2d9/eH4FsCwvgBpnc9n0pGE+ffKdph38s6foEZiEjdgHdzp+IA==} - hmac-drbg@1.0.1: - resolution: {integrity: sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg==} - html-escaper@2.0.2: resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} @@ -4761,6 +4560,9 @@ packages: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} + is-promise@2.2.2: + resolution: {integrity: sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==} + is-regex@1.2.1: resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} engines: {node: '>= 0.4'} @@ -4862,6 +4664,10 @@ packages: json-buffer@3.0.1: resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + json-diff@0.9.0: + resolution: {integrity: sha512-cVnggDrVkAAA3OvFfHpFEhOnmcsUpleEKq4d4O8sQWWSH40MBrWstKigVB1kGrgLWzuom+7rRdaCsnBD6VyObQ==} + hasBin: true + json-parse-even-better-errors@2.3.1: resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} @@ -4884,8 +4690,8 @@ packages: resolution: {integrity: sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==} engines: {node: '>=12', npm: '>=6'} - jspdf@3.0.4: - resolution: {integrity: sha512-dc6oQ8y37rRcHn316s4ngz/nOjayLF/FFxBF4V9zamQKRqXxyiH1zagkCdktdWhtoQId5K20xt1lB90XzkB+hQ==} + jspdf@4.2.1: + resolution: {integrity: sha512-YyAXyvnmjTbR4bHQRLzex3CuINCDlQnBqoSYyjJwTP2x9jDLuKDzy7aKUl0hgx3uhcl7xzg32agn5vlie6HIlQ==} jsx-ast-utils@3.3.5: resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==} @@ -5036,6 +4842,9 @@ packages: lodash.once@4.1.1: resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==} + lodash.throttle@4.1.1: + resolution: {integrity: sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==} + lodash@4.17.23: resolution: {integrity: sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==} @@ -5052,6 +4861,9 @@ packages: lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + lru-queue@0.1.0: + resolution: {integrity: sha512-BpdYkt9EvGl8OfWHDQPISVpcl5xZthb+XPsbELj5AQXxIC8IriDZIQYjBJPEm5rS420sjZ0TLEzRcq5KdBhYrQ==} + lucide-react@0.544.0: resolution: {integrity: sha512-t5tS44bqd825zAW45UQxpG2CvcC4urOwn2TrwSH8u+MjeE+1NnWl6QqeQ/6NdjMqdOygyiT9p3Ev0p1NJykxjw==} peerDependencies: @@ -5077,6 +4889,10 @@ packages: mdn-data@2.0.30: resolution: {integrity: sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==} + memoizee@0.4.17: + resolution: {integrity: sha512-DGqD7Hjpi/1or4F/aYAspXKNm5Yili0QDAFAY4QYvpqpgiY6+1jOfqpmByzjxbWd/T9mChbCArXAbDAsTm5oXA==} + engines: {node: '>=0.12'} + merge2@1.4.1: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} @@ -5097,12 +4913,6 @@ packages: resolution: {integrity: sha512-bjdr2xW1dBCMsMGGsUeqM4eFI60m94+szhxWys+B1ztIt6gWSfeGBdSVCIawezeHYLYn0j6zrsXdQS/JllBzww==} engines: {node: '>=6'} - minimalistic-assert@1.0.1: - resolution: {integrity: sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==} - - minimalistic-crypto-utils@1.0.1: - resolution: {integrity: sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg==} - minimatch@10.2.4: resolution: {integrity: sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==} engines: {node: 18 || 20 || >=22} @@ -5110,6 +4920,14 @@ packages: minimatch@3.1.5: resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==} + minimatch@5.1.9: + resolution: {integrity: sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==} + engines: {node: '>=10'} + + minimatch@7.4.9: + resolution: {integrity: sha512-Brg/fp/iAVDOQoHxkuN5bEYhyQlZhxddI78yWsCbeEwTHXQjlNLtiJDUsp1GIptVqMI7/gkJMz4vVAc01mpoBw==} + engines: {node: '>=10'} + minimist@1.2.8: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} @@ -5145,9 +4963,12 @@ packages: react: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc react-dom: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc - next@16.2.4: - resolution: {integrity: sha512-kPvz56wF5frc+FxlHI5qnklCzbq53HTwORaWBGdT0vNoKh1Aya9XC8aPauH4NJxqtzbWsS5mAbctm4cr+EkQ2Q==} - engines: {node: '>=20.9.0'} + next-tick@1.1.0: + resolution: {integrity: sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==} + + next@15.5.15: + resolution: {integrity: sha512-VSqCrJwtLVGwAVE0Sb/yikrQfkwkZW9p+lL/J4+xe+G3ZA+QnWPqgcfH1tDUEuk9y+pthzzVFp4L/U8JerMfMQ==} + engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0} hasBin: true peerDependencies: '@opentelemetry/api': ^1.1.0 @@ -5881,6 +5702,9 @@ packages: siginfo@2.0.0: resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + sisteransi@1.0.5: + resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} + snake-case@3.0.4: resolution: {integrity: sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==} @@ -5888,13 +5712,6 @@ packages: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} - source-map-support@0.5.21: - resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} - - source-map@0.6.1: - resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} - engines: {node: '>=0.10.0'} - space-separated-tokens@2.0.2: resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} @@ -6034,6 +5851,10 @@ packages: text-segmentation@1.0.3: resolution: {integrity: sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==} + timers-ext@0.1.8: + resolution: {integrity: sha512-wFH7+SEAcKfJpfLPkrgMPvvwnEtj8W4IurvEyrKsDleXnKLCDw71w8jltvfLa8Rm4qQxxT4jmDBYbJG/z7qoww==} + engines: {node: '>=0.12'} + tiny-invariant@1.3.3: resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} @@ -6127,6 +5948,9 @@ packages: resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} engines: {node: '>=10'} + type@2.7.3: + resolution: {integrity: sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ==} + typed-array-buffer@1.0.3: resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==} engines: {node: '>= 0.4'} @@ -6218,11 +6042,6 @@ packages: '@types/react': optional: true - use-sync-external-store@1.2.0: - resolution: {integrity: sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==} - peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 - use-sync-external-store@1.6.0: resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==} peerDependencies: @@ -6441,6 +6260,9 @@ packages: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} + wordwrap@1.0.0: + resolution: {integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==} + wrap-ansi@6.2.0: resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} engines: {node: '>=8'} @@ -6460,18 +6282,6 @@ packages: utf-8-validate: optional: true - ws@8.18.0: - resolution: {integrity: sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==} - engines: {node: '>=10.0.0'} - peerDependencies: - bufferutil: ^4.0.1 - utf-8-validate: '>=5.0.2' - peerDependenciesMeta: - bufferutil: - optional: true - utf-8-validate: - optional: true - ws@8.20.0: resolution: {integrity: sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==} engines: {node: '>=10.0.0'} @@ -6530,21 +6340,6 @@ packages: zod@4.3.6: resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==} - zustand@4.4.0: - resolution: {integrity: sha512-2dq6wq4dSxbiPTamGar0NlIG/av0wpyWZJGeQYtUOLegIUvhM2Bf86ekPlmgpUtS5uR7HyetSiktYrGsdsyZgQ==} - engines: {node: '>=12.7.0'} - peerDependencies: - '@types/react': '>=16.8' - immer: '>=9.0' - react: '>=16.8' - peerDependenciesMeta: - '@types/react': - optional: true - immer: - optional: true - react: - optional: true - zustand@5.0.12: resolution: {integrity: sha512-i77ae3aZq4dhMlRhJVCYgMLKuSiZAaUPAct2AksxQ+gOtimhGMdXljRT21P5BNpeT4kXlLIckvkPM029OljD7g==} engines: {node: '>=12.20.0'} @@ -7794,8 +7589,6 @@ snapshots: '@date-fns/tz@1.4.1': {} - '@drizzle-team/brocli@0.10.2': {} - '@emnapi/core@1.9.1': dependencies: '@emnapi/wasi-threads': 1.2.0 @@ -7807,243 +7600,90 @@ snapshots: tslib: 2.8.1 optional: true - '@emnapi/runtime@1.9.1': - dependencies: - tslib: 2.8.1 - optional: true - '@emnapi/wasi-threads@1.2.0': dependencies: tslib: 2.8.1 optional: true - '@esbuild-kit/core-utils@3.3.2': - dependencies: - esbuild: 0.18.20 - source-map-support: 0.5.21 - - '@esbuild-kit/esm-loader@2.6.5': - dependencies: - '@esbuild-kit/core-utils': 3.3.2 - get-tsconfig: 4.13.7 - - '@esbuild/aix-ppc64@0.25.12': - optional: true - '@esbuild/aix-ppc64@0.27.4': optional: true - '@esbuild/android-arm64@0.18.20': - optional: true - - '@esbuild/android-arm64@0.25.12': - optional: true - '@esbuild/android-arm64@0.27.4': optional: true - '@esbuild/android-arm@0.18.20': - optional: true - - '@esbuild/android-arm@0.25.12': + '@esbuild/android-arm@0.15.18': optional: true '@esbuild/android-arm@0.27.4': optional: true - '@esbuild/android-x64@0.18.20': - optional: true - - '@esbuild/android-x64@0.25.12': - optional: true - '@esbuild/android-x64@0.27.4': optional: true - '@esbuild/darwin-arm64@0.18.20': - optional: true - - '@esbuild/darwin-arm64@0.25.12': - optional: true - '@esbuild/darwin-arm64@0.27.4': optional: true - '@esbuild/darwin-x64@0.18.20': - optional: true - - '@esbuild/darwin-x64@0.25.12': - optional: true - '@esbuild/darwin-x64@0.27.4': optional: true - '@esbuild/freebsd-arm64@0.18.20': - optional: true - - '@esbuild/freebsd-arm64@0.25.12': - optional: true - '@esbuild/freebsd-arm64@0.27.4': optional: true - '@esbuild/freebsd-x64@0.18.20': - optional: true - - '@esbuild/freebsd-x64@0.25.12': - optional: true - '@esbuild/freebsd-x64@0.27.4': optional: true - '@esbuild/linux-arm64@0.18.20': - optional: true - - '@esbuild/linux-arm64@0.25.12': - optional: true - '@esbuild/linux-arm64@0.27.4': optional: true - '@esbuild/linux-arm@0.18.20': - optional: true - - '@esbuild/linux-arm@0.25.12': - optional: true - '@esbuild/linux-arm@0.27.4': optional: true - '@esbuild/linux-ia32@0.18.20': - optional: true - - '@esbuild/linux-ia32@0.25.12': - optional: true - '@esbuild/linux-ia32@0.27.4': optional: true - '@esbuild/linux-loong64@0.18.20': - optional: true - - '@esbuild/linux-loong64@0.25.12': + '@esbuild/linux-loong64@0.15.18': optional: true '@esbuild/linux-loong64@0.27.4': optional: true - '@esbuild/linux-mips64el@0.18.20': - optional: true - - '@esbuild/linux-mips64el@0.25.12': - optional: true - '@esbuild/linux-mips64el@0.27.4': optional: true - '@esbuild/linux-ppc64@0.18.20': - optional: true - - '@esbuild/linux-ppc64@0.25.12': - optional: true - '@esbuild/linux-ppc64@0.27.4': optional: true - '@esbuild/linux-riscv64@0.18.20': - optional: true - - '@esbuild/linux-riscv64@0.25.12': - optional: true - '@esbuild/linux-riscv64@0.27.4': optional: true - '@esbuild/linux-s390x@0.18.20': - optional: true - - '@esbuild/linux-s390x@0.25.12': - optional: true - '@esbuild/linux-s390x@0.27.4': optional: true - '@esbuild/linux-x64@0.18.20': - optional: true - - '@esbuild/linux-x64@0.25.12': - optional: true - '@esbuild/linux-x64@0.27.4': optional: true - '@esbuild/netbsd-arm64@0.25.12': - optional: true - '@esbuild/netbsd-arm64@0.27.4': optional: true - '@esbuild/netbsd-x64@0.18.20': - optional: true - - '@esbuild/netbsd-x64@0.25.12': - optional: true - '@esbuild/netbsd-x64@0.27.4': optional: true - '@esbuild/openbsd-arm64@0.25.12': - optional: true - '@esbuild/openbsd-arm64@0.27.4': optional: true - '@esbuild/openbsd-x64@0.18.20': - optional: true - - '@esbuild/openbsd-x64@0.25.12': - optional: true - '@esbuild/openbsd-x64@0.27.4': optional: true - '@esbuild/openharmony-arm64@0.25.12': - optional: true - '@esbuild/openharmony-arm64@0.27.4': optional: true - '@esbuild/sunos-x64@0.18.20': - optional: true - - '@esbuild/sunos-x64@0.25.12': - optional: true - '@esbuild/sunos-x64@0.27.4': optional: true - '@esbuild/win32-arm64@0.18.20': - optional: true - - '@esbuild/win32-arm64@0.25.12': - optional: true - - '@esbuild/win32-arm64@0.27.4': - optional: true - - '@esbuild/win32-ia32@0.18.20': - optional: true - - '@esbuild/win32-ia32@0.25.12': - optional: true - - '@esbuild/win32-ia32@0.27.4': - optional: true - - '@esbuild/win32-x64@0.18.20': + '@esbuild/win32-arm64@0.27.4': optional: true - '@esbuild/win32-x64@0.25.12': + '@esbuild/win32-ia32@0.27.4': optional: true '@esbuild/win32-x64@0.27.4': @@ -8099,73 +7739,10 @@ snapshots: '@ethereumjs/rlp@5.0.2': {} - '@ethersproject/abstract-provider@5.8.0': - dependencies: - '@ethersproject/bignumber': 5.8.0 - '@ethersproject/bytes': 5.8.0 - '@ethersproject/logger': 5.8.0 - '@ethersproject/networks': 5.8.0 - '@ethersproject/properties': 5.8.0 - '@ethersproject/transactions': 5.8.0 - '@ethersproject/web': 5.8.0 - optional: true - - '@ethersproject/abstract-signer@5.8.0': - dependencies: - '@ethersproject/abstract-provider': 5.8.0 - '@ethersproject/bignumber': 5.8.0 - '@ethersproject/bytes': 5.8.0 - '@ethersproject/logger': 5.8.0 - '@ethersproject/properties': 5.8.0 - optional: true - - '@ethersproject/address@5.8.0': - dependencies: - '@ethersproject/bignumber': 5.8.0 - '@ethersproject/bytes': 5.8.0 - '@ethersproject/keccak256': 5.8.0 - '@ethersproject/logger': 5.8.0 - '@ethersproject/rlp': 5.8.0 - - '@ethersproject/base64@5.8.0': - dependencies: - '@ethersproject/bytes': 5.8.0 - optional: true - - '@ethersproject/basex@5.8.0': - dependencies: - '@ethersproject/bytes': 5.8.0 - '@ethersproject/properties': 5.8.0 - optional: true - - '@ethersproject/bignumber@5.8.0': - dependencies: - '@ethersproject/bytes': 5.8.0 - '@ethersproject/logger': 5.8.0 - bn.js: 5.2.3 - '@ethersproject/bytes@5.8.0': dependencies: '@ethersproject/logger': 5.8.0 - '@ethersproject/constants@5.8.0': - dependencies: - '@ethersproject/bignumber': 5.8.0 - optional: true - - '@ethersproject/hash@5.8.0': - dependencies: - '@ethersproject/abstract-signer': 5.8.0 - '@ethersproject/address': 5.8.0 - '@ethersproject/base64': 5.8.0 - '@ethersproject/bignumber': 5.8.0 - '@ethersproject/bytes': 5.8.0 - '@ethersproject/keccak256': 5.8.0 - '@ethersproject/logger': 5.8.0 - '@ethersproject/properties': 5.8.0 - '@ethersproject/strings': 5.8.0 - optional: true - '@ethersproject/keccak256@5.8.0': dependencies: '@ethersproject/bytes': 5.8.0 @@ -8173,100 +7750,6 @@ snapshots: '@ethersproject/logger@5.8.0': {} - '@ethersproject/networks@5.8.0': - dependencies: - '@ethersproject/logger': 5.8.0 - optional: true - - '@ethersproject/properties@5.8.0': - dependencies: - '@ethersproject/logger': 5.8.0 - optional: true - - '@ethersproject/providers@5.8.0': - dependencies: - '@ethersproject/abstract-provider': 5.8.0 - '@ethersproject/abstract-signer': 5.8.0 - '@ethersproject/address': 5.8.0 - '@ethersproject/base64': 5.8.0 - '@ethersproject/basex': 5.8.0 - '@ethersproject/bignumber': 5.8.0 - '@ethersproject/bytes': 5.8.0 - '@ethersproject/constants': 5.8.0 - '@ethersproject/hash': 5.8.0 - '@ethersproject/logger': 5.8.0 - '@ethersproject/networks': 5.8.0 - '@ethersproject/properties': 5.8.0 - '@ethersproject/random': 5.8.0 - '@ethersproject/rlp': 5.8.0 - '@ethersproject/sha2': 5.8.0 - '@ethersproject/strings': 5.8.0 - '@ethersproject/transactions': 5.8.0 - '@ethersproject/web': 5.8.0 - bech32: 1.1.4 - ws: 8.18.0 - transitivePeerDependencies: - - bufferutil - - utf-8-validate - optional: true - - '@ethersproject/random@5.8.0': - dependencies: - '@ethersproject/bytes': 5.8.0 - '@ethersproject/logger': 5.8.0 - optional: true - - '@ethersproject/rlp@5.8.0': - dependencies: - '@ethersproject/bytes': 5.8.0 - '@ethersproject/logger': 5.8.0 - - '@ethersproject/sha2@5.8.0': - dependencies: - '@ethersproject/bytes': 5.8.0 - '@ethersproject/logger': 5.8.0 - hash.js: 1.1.7 - optional: true - - '@ethersproject/signing-key@5.8.0': - dependencies: - '@ethersproject/bytes': 5.8.0 - '@ethersproject/logger': 5.8.0 - '@ethersproject/properties': 5.8.0 - bn.js: 5.2.3 - elliptic: 6.6.1 - hash.js: 1.1.7 - optional: true - - '@ethersproject/strings@5.8.0': - dependencies: - '@ethersproject/bytes': 5.8.0 - '@ethersproject/constants': 5.8.0 - '@ethersproject/logger': 5.8.0 - optional: true - - '@ethersproject/transactions@5.8.0': - dependencies: - '@ethersproject/address': 5.8.0 - '@ethersproject/bignumber': 5.8.0 - '@ethersproject/bytes': 5.8.0 - '@ethersproject/constants': 5.8.0 - '@ethersproject/keccak256': 5.8.0 - '@ethersproject/logger': 5.8.0 - '@ethersproject/properties': 5.8.0 - '@ethersproject/rlp': 5.8.0 - '@ethersproject/signing-key': 5.8.0 - optional: true - - '@ethersproject/web@5.8.0': - dependencies: - '@ethersproject/base64': 5.8.0 - '@ethersproject/bytes': 5.8.0 - '@ethersproject/logger': 5.8.0 - '@ethersproject/properties': 5.8.0 - '@ethersproject/strings': 5.8.0 - optional: true - '@faker-js/faker@10.4.0': {} '@floating-ui/core@1.7.5': @@ -8447,7 +7930,7 @@ snapshots: '@napi-rs/wasm-runtime@0.2.12': dependencies: '@emnapi/core': 1.9.1 - '@emnapi/runtime': 1.9.1 + '@emnapi/runtime': 1.10.0 '@tybys/wasm-util': 0.10.1 optional: true @@ -8458,36 +7941,36 @@ snapshots: '@tybys/wasm-util': 0.10.1 optional: true - '@next/env@16.2.1': {} + '@next/env@15.5.15': {} - '@next/env@16.2.4': {} + '@next/env@16.2.1': {} '@next/eslint-plugin-next@15.5.3': dependencies: fast-glob: 3.3.1 - '@next/swc-darwin-arm64@16.2.4': + '@next/swc-darwin-arm64@15.5.15': optional: true - '@next/swc-darwin-x64@16.2.4': + '@next/swc-darwin-x64@15.5.15': optional: true - '@next/swc-linux-arm64-gnu@16.2.4': + '@next/swc-linux-arm64-gnu@15.5.15': optional: true - '@next/swc-linux-arm64-musl@16.2.4': + '@next/swc-linux-arm64-musl@15.5.15': optional: true - '@next/swc-linux-x64-gnu@16.2.4': + '@next/swc-linux-x64-gnu@15.5.15': optional: true - '@next/swc-linux-x64-musl@16.2.4': + '@next/swc-linux-x64-musl@15.5.15': optional: true - '@next/swc-win32-arm64-msvc@16.2.4': + '@next/swc-win32-arm64-msvc@15.5.15': optional: true - '@next/swc-win32-x64-msvc@16.2.4': + '@next/swc-win32-x64-msvc@15.5.15': optional: true '@noble/curves@1.2.0': @@ -10394,7 +9877,7 @@ snapshots: '@unrs/resolver-binding-win32-x64-msvc@1.11.1': optional: true - '@vitest/coverage-v8@4.1.2(vitest@4.1.1(@types/node@20.19.37)(vite@8.0.2(@emnapi/core@1.9.1)(@emnapi/runtime@1.10.0)(@types/node@20.19.37)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)))': + '@vitest/coverage-v8@4.1.2(vitest@4.1.1(@types/node@20.19.37)(vite@8.0.2(@emnapi/core@1.9.1)(@emnapi/runtime@1.10.0)(@types/node@20.19.37)(esbuild@0.15.18)(jiti@2.6.1)(tsx@4.21.0)))': dependencies: '@bcoe/v8-coverage': 1.0.2 '@vitest/utils': 4.1.2 @@ -10406,7 +9889,7 @@ snapshots: obug: 2.1.1 std-env: 4.0.0 tinyrainbow: 3.1.0 - vitest: 4.1.1(@types/node@20.19.37)(vite@8.0.2(@emnapi/core@1.9.1)(@emnapi/runtime@1.10.0)(@types/node@20.19.37)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)) + vitest: 4.1.1(@types/node@20.19.37)(vite@8.0.2(@emnapi/core@1.9.1)(@emnapi/runtime@1.10.0)(@types/node@20.19.37)(esbuild@0.15.18)(jiti@2.6.1)(tsx@4.21.0)) '@vitest/expect@4.1.1': dependencies: @@ -10417,13 +9900,13 @@ snapshots: chai: 6.2.2 tinyrainbow: 3.1.0 - '@vitest/mocker@4.1.1(vite@8.0.2(@emnapi/core@1.9.1)(@emnapi/runtime@1.10.0)(@types/node@20.19.37)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0))': + '@vitest/mocker@4.1.1(vite@8.0.2(@emnapi/core@1.9.1)(@emnapi/runtime@1.10.0)(@types/node@20.19.37)(esbuild@0.15.18)(jiti@2.6.1)(tsx@4.21.0))': dependencies: '@vitest/spy': 4.1.1 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 8.0.2(@emnapi/core@1.9.1)(@emnapi/runtime@1.10.0)(@types/node@20.19.37)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0) + vite: 8.0.2(@emnapi/core@1.9.1)(@emnapi/runtime@1.10.0)(@types/node@20.19.37)(esbuild@0.15.18)(jiti@2.6.1)(tsx@4.21.0) '@vitest/pretty-format@4.1.1': dependencies: @@ -10463,19 +9946,14 @@ snapshots: dependencies: '@web3-react/types': 6.0.7 - '@web3-react/core@8.2.3(@types/react@19.2.14)(immer@11.1.4)(react@19.1.0)': + '@web3-react/core@6.1.9(react@19.1.0)': dependencies: - '@web3-react/store': 8.2.3(@types/react@19.2.14)(immer@11.1.4)(react@19.1.0) - '@web3-react/types': 8.2.3(@types/react@19.2.14)(immer@11.1.4)(react@19.1.0) + '@ethersproject/keccak256': 5.8.0 + '@web3-react/abstract-connector': 6.0.7 + '@web3-react/types': 6.0.7 react: 19.1.0 - zustand: 4.4.0(@types/react@19.2.14)(immer@11.1.4)(react@19.1.0) - optionalDependencies: - '@ethersproject/providers': 5.8.0 - transitivePeerDependencies: - - '@types/react' - - bufferutil - - immer - - utf-8-validate + tiny-invariant: 1.3.3 + tiny-warning: 1.0.3 '@web3-react/injected-connector@6.0.7': dependencies: @@ -10483,26 +9961,8 @@ snapshots: '@web3-react/types': 6.0.7 tiny-warning: 1.0.3 - '@web3-react/store@8.2.3(@types/react@19.2.14)(immer@11.1.4)(react@19.1.0)': - dependencies: - '@ethersproject/address': 5.8.0 - '@web3-react/types': 8.2.3(@types/react@19.2.14)(immer@11.1.4)(react@19.1.0) - zustand: 4.4.0(@types/react@19.2.14)(immer@11.1.4)(react@19.1.0) - transitivePeerDependencies: - - '@types/react' - - immer - - react - '@web3-react/types@6.0.7': {} - '@web3-react/types@8.2.3(@types/react@19.2.14)(immer@11.1.4)(react@19.1.0)': - dependencies: - zustand: 4.4.0(@types/react@19.2.14)(immer@11.1.4)(react@19.1.0) - transitivePeerDependencies: - - '@types/react' - - immer - - react - abitype@0.7.1(typescript@5.9.3)(zod@4.3.6): dependencies: typescript: 5.9.3 @@ -10683,20 +10143,10 @@ snapshots: baseline-browser-mapping@2.10.10: {} - baseline-browser-mapping@2.10.21: {} - bcryptjs@3.0.3: {} - bech32@1.1.4: - optional: true - bignumber.js@9.3.1: {} - bn.js@4.12.3: - optional: true - - bn.js@5.2.3: {} - boolbase@1.0.0: {} bowser@2.14.1: {} @@ -10706,6 +10156,10 @@ snapshots: balanced-match: 1.0.2 concat-map: 0.0.1 + brace-expansion@2.1.0: + dependencies: + balanced-match: 1.0.2 + brace-expansion@5.0.5: dependencies: balanced-match: 4.0.4 @@ -10714,21 +10168,16 @@ snapshots: dependencies: fill-range: 7.1.1 - brorand@1.1.0: - optional: true - browserslist@4.28.1: dependencies: baseline-browser-mapping: 2.10.10 - caniuse-lite: 1.0.30001781 + caniuse-lite: 1.0.30001790 electron-to-chromium: 1.5.322 node-releases: 2.0.36 update-browserslist-db: 1.2.3(browserslist@4.28.1) buffer-equal-constant-time@1.0.1: {} - buffer-from@1.1.2: {} - buffer@6.0.3: dependencies: base64-js: 1.5.1 @@ -10759,7 +10208,7 @@ snapshots: camelcase@6.3.0: {} - caniuse-lite@1.0.30001781: {} + camelcase@7.0.1: {} caniuse-lite@1.0.30001790: {} @@ -10782,6 +10231,8 @@ snapshots: ansi-styles: 4.3.0 supports-color: 7.2.0 + chalk@5.6.2: {} + character-entities-legacy@3.0.0: {} character-entities@2.0.2: {} @@ -10794,6 +10245,14 @@ snapshots: classnames@2.5.1: {} + cli-color@2.0.4: + dependencies: + d: 1.0.2 + es5-ext: 0.10.64 + es6-iterator: 2.0.3 + memoizee: 0.4.17 + timers-ext: 0.1.8 + client-only@0.0.1: {} cliui@6.0.0: @@ -10824,8 +10283,7 @@ snapshots: commander@7.2.0: {} - commander@9.5.0: - optional: true + commander@9.5.0: {} concat-map@0.0.1: {} @@ -10939,6 +10397,11 @@ snapshots: d3-timer@3.0.1: {} + d@1.0.2: + dependencies: + es5-ext: 0.10.64 + type: 2.7.3 + damerau-levenshtein@1.0.8: {} data-uri-to-buffer@4.0.1: {} @@ -11009,6 +10472,10 @@ snapshots: diacritics@1.3.0: {} + difflib@0.2.4: + dependencies: + heap: 0.2.7 + dijkstrajs@1.0.3: {} doctrine@2.1.0: @@ -11050,12 +10517,24 @@ snapshots: drange@1.1.1: {} - drizzle-kit@0.31.10: + dreamopt@0.8.0: dependencies: - '@drizzle-team/brocli': 0.10.2 - '@esbuild-kit/esm-loader': 2.6.5 - esbuild: 0.25.12 - tsx: 4.21.0 + wordwrap: 1.0.0 + + drizzle-kit@0.18.1: + dependencies: + camelcase: 7.0.1 + chalk: 5.6.2 + commander: 9.5.0 + esbuild: 0.15.18 + esbuild-register: 3.6.0(esbuild@0.15.18) + glob: 8.1.0 + hanji: 0.0.5 + json-diff: 0.9.0 + minimatch: 7.4.9 + zod: 3.25.76 + transitivePeerDependencies: + - supports-color drizzle-orm@0.45.1(@types/pg@8.20.0)(pg@8.20.0): optionalDependencies: @@ -11074,17 +10553,6 @@ snapshots: electron-to-chromium@1.5.322: {} - elliptic@6.6.1: - dependencies: - bn.js: 4.12.3 - brorand: 1.1.0 - hash.js: 1.1.7 - hmac-drbg: 1.0.1 - inherits: 2.0.4 - minimalistic-assert: 1.0.1 - minimalistic-crypto-utils: 1.0.1 - optional: true - emoji-regex@8.0.0: {} emoji-regex@9.2.2: {} @@ -11206,59 +10674,122 @@ snapshots: es-toolkit@1.45.1: {} - esbuild@0.18.20: - optionalDependencies: - '@esbuild/android-arm': 0.18.20 - '@esbuild/android-arm64': 0.18.20 - '@esbuild/android-x64': 0.18.20 - '@esbuild/darwin-arm64': 0.18.20 - '@esbuild/darwin-x64': 0.18.20 - '@esbuild/freebsd-arm64': 0.18.20 - '@esbuild/freebsd-x64': 0.18.20 - '@esbuild/linux-arm': 0.18.20 - '@esbuild/linux-arm64': 0.18.20 - '@esbuild/linux-ia32': 0.18.20 - '@esbuild/linux-loong64': 0.18.20 - '@esbuild/linux-mips64el': 0.18.20 - '@esbuild/linux-ppc64': 0.18.20 - '@esbuild/linux-riscv64': 0.18.20 - '@esbuild/linux-s390x': 0.18.20 - '@esbuild/linux-x64': 0.18.20 - '@esbuild/netbsd-x64': 0.18.20 - '@esbuild/openbsd-x64': 0.18.20 - '@esbuild/sunos-x64': 0.18.20 - '@esbuild/win32-arm64': 0.18.20 - '@esbuild/win32-ia32': 0.18.20 - '@esbuild/win32-x64': 0.18.20 - - esbuild@0.25.12: + es5-ext@0.10.64: + dependencies: + es6-iterator: 2.0.3 + es6-symbol: 3.1.4 + esniff: 2.0.1 + next-tick: 1.1.0 + + es6-iterator@2.0.3: + dependencies: + d: 1.0.2 + es5-ext: 0.10.64 + es6-symbol: 3.1.4 + + es6-symbol@3.1.4: + dependencies: + d: 1.0.2 + ext: 1.7.0 + + es6-weak-map@2.0.3: + dependencies: + d: 1.0.2 + es5-ext: 0.10.64 + es6-iterator: 2.0.3 + es6-symbol: 3.1.4 + + esbuild-android-64@0.15.18: + optional: true + + esbuild-android-arm64@0.15.18: + optional: true + + esbuild-darwin-64@0.15.18: + optional: true + + esbuild-darwin-arm64@0.15.18: + optional: true + + esbuild-freebsd-64@0.15.18: + optional: true + + esbuild-freebsd-arm64@0.15.18: + optional: true + + esbuild-linux-32@0.15.18: + optional: true + + esbuild-linux-64@0.15.18: + optional: true + + esbuild-linux-arm64@0.15.18: + optional: true + + esbuild-linux-arm@0.15.18: + optional: true + + esbuild-linux-mips64le@0.15.18: + optional: true + + esbuild-linux-ppc64le@0.15.18: + optional: true + + esbuild-linux-riscv64@0.15.18: + optional: true + + esbuild-linux-s390x@0.15.18: + optional: true + + esbuild-netbsd-64@0.15.18: + optional: true + + esbuild-openbsd-64@0.15.18: + optional: true + + esbuild-register@3.6.0(esbuild@0.15.18): + dependencies: + debug: 4.4.3 + esbuild: 0.15.18 + transitivePeerDependencies: + - supports-color + + esbuild-sunos-64@0.15.18: + optional: true + + esbuild-windows-32@0.15.18: + optional: true + + esbuild-windows-64@0.15.18: + optional: true + + esbuild-windows-arm64@0.15.18: + optional: true + + esbuild@0.15.18: optionalDependencies: - '@esbuild/aix-ppc64': 0.25.12 - '@esbuild/android-arm': 0.25.12 - '@esbuild/android-arm64': 0.25.12 - '@esbuild/android-x64': 0.25.12 - '@esbuild/darwin-arm64': 0.25.12 - '@esbuild/darwin-x64': 0.25.12 - '@esbuild/freebsd-arm64': 0.25.12 - '@esbuild/freebsd-x64': 0.25.12 - '@esbuild/linux-arm': 0.25.12 - '@esbuild/linux-arm64': 0.25.12 - '@esbuild/linux-ia32': 0.25.12 - '@esbuild/linux-loong64': 0.25.12 - '@esbuild/linux-mips64el': 0.25.12 - '@esbuild/linux-ppc64': 0.25.12 - '@esbuild/linux-riscv64': 0.25.12 - '@esbuild/linux-s390x': 0.25.12 - '@esbuild/linux-x64': 0.25.12 - '@esbuild/netbsd-arm64': 0.25.12 - '@esbuild/netbsd-x64': 0.25.12 - '@esbuild/openbsd-arm64': 0.25.12 - '@esbuild/openbsd-x64': 0.25.12 - '@esbuild/openharmony-arm64': 0.25.12 - '@esbuild/sunos-x64': 0.25.12 - '@esbuild/win32-arm64': 0.25.12 - '@esbuild/win32-ia32': 0.25.12 - '@esbuild/win32-x64': 0.25.12 + '@esbuild/android-arm': 0.15.18 + '@esbuild/linux-loong64': 0.15.18 + esbuild-android-64: 0.15.18 + esbuild-android-arm64: 0.15.18 + esbuild-darwin-64: 0.15.18 + esbuild-darwin-arm64: 0.15.18 + esbuild-freebsd-64: 0.15.18 + esbuild-freebsd-arm64: 0.15.18 + esbuild-linux-32: 0.15.18 + esbuild-linux-64: 0.15.18 + esbuild-linux-arm: 0.15.18 + esbuild-linux-arm64: 0.15.18 + esbuild-linux-mips64le: 0.15.18 + esbuild-linux-ppc64le: 0.15.18 + esbuild-linux-riscv64: 0.15.18 + esbuild-linux-s390x: 0.15.18 + esbuild-netbsd-64: 0.15.18 + esbuild-openbsd-64: 0.15.18 + esbuild-sunos-64: 0.15.18 + esbuild-windows-32: 0.15.18 + esbuild-windows-64: 0.15.18 + esbuild-windows-arm64: 0.15.18 esbuild@0.27.4: optionalDependencies: @@ -11473,6 +11004,13 @@ snapshots: transitivePeerDependencies: - supports-color + esniff@2.0.1: + dependencies: + d: 1.0.2 + es5-ext: 0.10.64 + event-emitter: 0.3.5 + type: 2.7.3 + espree@10.4.0: dependencies: acorn: 8.16.0 @@ -11515,12 +11053,21 @@ snapshots: - bufferutil - utf-8-validate + event-emitter@0.3.5: + dependencies: + d: 1.0.2 + es5-ext: 0.10.64 + eventemitter3@5.0.4: {} eventsource@2.0.2: {} expect-type@1.3.0: {} + ext@1.7.0: + dependencies: + type: 2.7.3 + extend@3.0.2: {} fast-deep-equal@3.1.3: {} @@ -11719,6 +11266,14 @@ snapshots: once: 1.4.0 path-is-absolute: 1.0.1 + glob@8.1.0: + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 5.1.9 + once: 1.4.0 + globals@14.0.0: {} globalthis@1.0.4: @@ -11743,6 +11298,11 @@ snapshots: graceful-fs@4.2.11: {} + hanji@0.0.5: + dependencies: + lodash.throttle: 4.1.1 + sisteransi: 1.0.5 + has-bigints@1.1.0: {} has-flag@4.0.0: {} @@ -11761,12 +11321,6 @@ snapshots: dependencies: has-symbols: 1.1.0 - hash.js@1.1.7: - dependencies: - inherits: 2.0.4 - minimalistic-assert: 1.0.1 - optional: true - hasown@2.0.2: dependencies: function-bind: 1.1.2 @@ -11783,17 +11337,12 @@ snapshots: property-information: 7.1.0 space-separated-tokens: 2.0.2 + heap@0.2.7: {} + highlight.js@10.7.3: {} highlightjs-vue@1.0.0: {} - hmac-drbg@1.0.1: - dependencies: - hash.js: 1.1.7 - minimalistic-assert: 1.0.1 - minimalistic-crypto-utils: 1.0.1 - optional: true - html-escaper@2.0.2: {} html2canvas@1.4.1: @@ -11959,6 +11508,8 @@ snapshots: is-number@7.0.0: {} + is-promise@2.2.2: {} + is-regex@1.2.1: dependencies: call-bound: 1.0.4 @@ -12054,6 +11605,12 @@ snapshots: json-buffer@3.0.1: {} + json-diff@0.9.0: + dependencies: + cli-color: 2.0.4 + difflib: 0.2.4 + dreamopt: 0.8.0 + json-parse-even-better-errors@2.3.1: {} json-schema-traverse@0.4.1: {} @@ -12079,7 +11636,7 @@ snapshots: ms: 2.1.3 semver: 7.7.4 - jspdf@3.0.4: + jspdf@4.2.1: dependencies: '@babel/runtime': 7.29.2 fast-png: 6.4.0 @@ -12210,6 +11767,8 @@ snapshots: lodash.once@4.1.1: {} + lodash.throttle@4.1.1: {} + lodash@4.17.23: {} loose-envify@1.4.0: @@ -12229,6 +11788,10 @@ snapshots: dependencies: yallist: 3.1.1 + lru-queue@0.1.0: + dependencies: + es5-ext: 0.10.64 + lucide-react@0.544.0(react@19.1.0): dependencies: react: 19.1.0 @@ -12253,6 +11816,17 @@ snapshots: mdn-data@2.0.30: {} + memoizee@0.4.17: + dependencies: + d: 1.0.2 + es5-ext: 0.10.64 + es6-weak-map: 2.0.3 + event-emitter: 0.3.5 + is-promise: 2.2.2 + lru-queue: 0.1.0 + next-tick: 1.1.0 + timers-ext: 0.1.8 + merge2@1.4.1: {} micromatch@4.0.8: @@ -12270,12 +11844,6 @@ snapshots: dependencies: lodash: 4.17.23 - minimalistic-assert@1.0.1: - optional: true - - minimalistic-crypto-utils@1.0.1: - optional: true - minimatch@10.2.4: dependencies: brace-expansion: 5.0.5 @@ -12284,6 +11852,14 @@ snapshots: dependencies: brace-expansion: 1.1.12 + minimatch@5.1.9: + dependencies: + brace-expansion: 2.1.0 + + minimatch@7.4.9: + dependencies: + brace-expansion: 2.1.0 + minimist@1.2.8: {} motion-dom@12.38.0: @@ -12307,25 +11883,26 @@ snapshots: react: 19.1.0 react-dom: 19.1.0(react@19.1.0) - next@16.2.4(@babel/core@7.29.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0): + next-tick@1.1.0: {} + + next@15.5.15(@babel/core@7.29.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0): dependencies: - '@next/env': 16.2.4 + '@next/env': 15.5.15 '@swc/helpers': 0.5.15 - baseline-browser-mapping: 2.10.21 caniuse-lite: 1.0.30001790 postcss: 8.4.31 react: 19.1.0 react-dom: 19.1.0(react@19.1.0) styled-jsx: 5.1.6(@babel/core@7.29.0)(react@19.1.0) optionalDependencies: - '@next/swc-darwin-arm64': 16.2.4 - '@next/swc-darwin-x64': 16.2.4 - '@next/swc-linux-arm64-gnu': 16.2.4 - '@next/swc-linux-arm64-musl': 16.2.4 - '@next/swc-linux-x64-gnu': 16.2.4 - '@next/swc-linux-x64-musl': 16.2.4 - '@next/swc-win32-arm64-msvc': 16.2.4 - '@next/swc-win32-x64-msvc': 16.2.4 + '@next/swc-darwin-arm64': 15.5.15 + '@next/swc-darwin-x64': 15.5.15 + '@next/swc-linux-arm64-gnu': 15.5.15 + '@next/swc-linux-arm64-musl': 15.5.15 + '@next/swc-linux-x64-gnu': 15.5.15 + '@next/swc-linux-x64-musl': 15.5.15 + '@next/swc-win32-arm64-msvc': 15.5.15 + '@next/swc-win32-x64-msvc': 15.5.15 sharp: 0.34.5 transitivePeerDependencies: - '@babel/core' @@ -13054,6 +12631,8 @@ snapshots: siginfo@2.0.0: {} + sisteransi@1.0.5: {} + snake-case@3.0.4: dependencies: dot-case: 3.0.4 @@ -13061,13 +12640,6 @@ snapshots: source-map-js@1.2.1: {} - source-map-support@0.5.21: - dependencies: - buffer-from: 1.1.2 - source-map: 0.6.1 - - source-map@0.6.1: {} - space-separated-tokens@2.0.2: {} split2@4.2.0: {} @@ -13279,6 +12851,11 @@ snapshots: dependencies: utrie: 1.0.2 + timers-ext@0.1.8: + dependencies: + es5-ext: 0.10.64 + next-tick: 1.1.0 + tiny-invariant@1.3.3: {} tiny-warning@1.0.3: {} @@ -13369,6 +12946,8 @@ snapshots: type-fest@0.20.2: {} + type@2.7.3: {} + typed-array-buffer@1.0.3: dependencies: call-bound: 1.0.4 @@ -13488,10 +13067,6 @@ snapshots: optionalDependencies: '@types/react': 19.2.14 - use-sync-external-store@1.2.0(react@19.1.0): - dependencies: - react: 19.1.0 - use-sync-external-store@1.6.0(react@19.1.0): dependencies: react: 19.1.0 @@ -13527,7 +13102,7 @@ snapshots: d3-time: 3.1.0 d3-timer: 3.0.1 - vite@8.0.2(@emnapi/core@1.9.1)(@emnapi/runtime@1.10.0)(@types/node@20.19.37)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0): + vite@8.0.2(@emnapi/core@1.9.1)(@emnapi/runtime@1.10.0)(@types/node@20.19.37)(esbuild@0.15.18)(jiti@2.6.1)(tsx@4.21.0): dependencies: lightningcss: 1.32.0 picomatch: 4.0.4 @@ -13536,7 +13111,7 @@ snapshots: tinyglobby: 0.2.16 optionalDependencies: '@types/node': 20.19.37 - esbuild: 0.27.4 + esbuild: 0.15.18 fsevents: 2.3.3 jiti: 2.6.1 tsx: 4.21.0 @@ -13544,10 +13119,10 @@ snapshots: - '@emnapi/core' - '@emnapi/runtime' - vitest@4.1.1(@types/node@20.19.37)(vite@8.0.2(@emnapi/core@1.9.1)(@emnapi/runtime@1.10.0)(@types/node@20.19.37)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)): + vitest@4.1.1(@types/node@20.19.37)(vite@8.0.2(@emnapi/core@1.9.1)(@emnapi/runtime@1.10.0)(@types/node@20.19.37)(esbuild@0.15.18)(jiti@2.6.1)(tsx@4.21.0)): dependencies: '@vitest/expect': 4.1.1 - '@vitest/mocker': 4.1.1(vite@8.0.2(@emnapi/core@1.9.1)(@emnapi/runtime@1.10.0)(@types/node@20.19.37)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)) + '@vitest/mocker': 4.1.1(vite@8.0.2(@emnapi/core@1.9.1)(@emnapi/runtime@1.10.0)(@types/node@20.19.37)(esbuild@0.15.18)(jiti@2.6.1)(tsx@4.21.0)) '@vitest/pretty-format': 4.1.1 '@vitest/runner': 4.1.1 '@vitest/snapshot': 4.1.1 @@ -13564,7 +13139,7 @@ snapshots: tinyexec: 1.0.4 tinyglobby: 0.2.15 tinyrainbow: 3.1.0 - vite: 8.0.2(@emnapi/core@1.9.1)(@emnapi/runtime@1.10.0)(@types/node@20.19.37)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0) + vite: 8.0.2(@emnapi/core@1.9.1)(@emnapi/runtime@1.10.0)(@types/node@20.19.37)(esbuild@0.15.18)(jiti@2.6.1)(tsx@4.21.0) why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 20.19.37 @@ -13862,6 +13437,8 @@ snapshots: word-wrap@1.2.5: {} + wordwrap@1.0.0: {} + wrap-ansi@6.2.0: dependencies: ansi-styles: 4.3.0 @@ -13872,9 +13449,6 @@ snapshots: ws@8.17.1: {} - ws@8.18.0: - optional: true - ws@8.20.0: {} xml-but-prettier@1.0.1: @@ -13926,14 +13500,6 @@ snapshots: zod@4.3.6: {} - zustand@4.4.0(@types/react@19.2.14)(immer@11.1.4)(react@19.1.0): - dependencies: - use-sync-external-store: 1.2.0(react@19.1.0) - optionalDependencies: - '@types/react': 19.2.14 - immer: 11.1.4 - react: 19.1.0 - zustand@5.0.12(@types/react@19.2.14)(immer@11.1.4)(react@19.1.0)(use-sync-external-store@1.6.0(react@19.1.0)): optionalDependencies: '@types/react': 19.2.14 diff --git a/src/app/(dashboard)/dashboard/page.tsx b/src/app/(dashboard)/dashboard/page.tsx index 7f6d7cbf..331414a1 100644 --- a/src/app/(dashboard)/dashboard/page.tsx +++ b/src/app/(dashboard)/dashboard/page.tsx @@ -1,10 +1,10 @@ "use client"; -import { useEffect, useState } from "react"; import OnboardingCheckList from "@/components/features/dashboard/home/OnboardingCheckList"; import RequiringAttention from "@/components/features/dashboard/home/RequiringAttention"; import QuickAction from "@/components/features/dashboard/home/QuickAction"; import { motion, Variants } from "framer-motion"; import { useEffect, useRef, useState } from "react"; +import avatar from "@/../public/avatar/avatar.png"; import { KybService } from "@/lib/api/kyb"; import type { KybVerificationStatus } from "@/types/kyb"; import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; @@ -35,6 +35,7 @@ export default function DashboardPage() { const user = { name: "Peter", + firstName: "Peter", email: "peter@vestroll.com", userType: "Administrator", avatar: avatar, diff --git a/src/app/(dashboard)/layout.tsx b/src/app/(dashboard)/layout.tsx index a1490cbc..21e090a7 100644 --- a/src/app/(dashboard)/layout.tsx +++ b/src/app/(dashboard)/layout.tsx @@ -3,6 +3,8 @@ import { ThemeProvider } from "@/components/providers/theme-provider"; import PageTransition from "@/components/shared/animations/PageTransition"; import { FeedbackWidget } from "@/components/shared/feedback-widget"; import { formatNairaFromKobo } from "@/lib/format-naira"; +import { AuthUtils } from "@/server/utils/auth"; +import { FinanceWalletService } from "@/server/services/finance-wallet.service"; export default async function AppScopedLayout({ children, @@ -12,17 +14,13 @@ export default async function AppScopedLayout({ let formattedBalance: string = "₦0.00"; try { - const res = await fetch("/api/v1/finance/balance", { - next: { revalidate: 0 }, - }); - if (res.ok) { - const json = await res.json(); - const kobo = json.data?.balance ?? json.balance ?? 0; - formattedBalance = formatNairaFromKobo(kobo); + const user = await AuthUtils.getCurrentUser(); + if (user?.organizationId) { + const balance = await FinanceWalletService.getOrganizationFiatBalance(user.organizationId); + formattedBalance = formatNairaFromKobo(Number(balance)); } } catch (error) { console.error("Failed to fetch organization balance:", error); - // default remains ₦0.00 } return ( diff --git a/src/app/api/v1/auth/apple/route.ts b/src/app/api/v1/auth/apple/route.ts index 995ad414..ae4d9b11 100644 --- a/src/app/api/v1/auth/apple/route.ts +++ b/src/app/api/v1/auth/apple/route.ts @@ -102,6 +102,14 @@ export async function POST(req: NextRequest) { 200, ); + response.cookies.set("access_token", accessToken, { + httpOnly: true, + secure: process.env.NODE_ENV === "production", + sameSite: "lax", + maxAge: 60 * 60 * 24 * 7, + path: "/", + }); + response.cookies.set("refreshToken", refreshToken, { httpOnly: true, secure: process.env.NODE_ENV === "production", diff --git a/src/app/api/v1/auth/google/route.ts b/src/app/api/v1/auth/google/route.ts index 824a3d45..2bba375e 100644 --- a/src/app/api/v1/auth/google/route.ts +++ b/src/app/api/v1/auth/google/route.ts @@ -77,6 +77,14 @@ export async function POST(req: NextRequest) { 200, ); + response.cookies.set("access_token", accessToken, { + httpOnly: true, + secure: process.env.NODE_ENV === "production", + sameSite: "lax", + maxAge: 60 * 60 * 24 * 7, + path: "/", + }); + response.cookies.set("refreshToken", refreshToken, { httpOnly: true, secure: process.env.NODE_ENV === "production", diff --git a/src/app/api/v1/auth/login/route.ts b/src/app/api/v1/auth/login/route.ts index 490f3183..cbce69fd 100644 --- a/src/app/api/v1/auth/login/route.ts +++ b/src/app/api/v1/auth/login/route.ts @@ -47,16 +47,21 @@ export const POST = withHandler( const response = ApiResponse.success(result, result.message); // Set refresh token cookie if provided (e.g., during OTP-based login or social auth) - const resultWithOptionalRefreshToken = result as typeof result & { refreshToken?: string }; - if (resultWithOptionalRefreshToken.refreshToken) { + const resultWithTokens = result as typeof result & { refreshToken?: string; accessToken?: string }; + if (resultWithTokens.refreshToken) { const isProd = process.env.NODE_ENV === "production"; - response.cookies.set("refreshToken", resultWithOptionalRefreshToken.refreshToken, { + const cookieOptions = { httpOnly: true, secure: isProd, - sameSite: "lax", + sameSite: "lax" as const, path: "/", maxAge: 60 * 60 * 24 * 7, // 7 days - }); + }; + + response.cookies.set("refreshToken", resultWithTokens.refreshToken, cookieOptions); + if (resultWithTokens.accessToken) { + response.cookies.set("access_token", resultWithTokens.accessToken, cookieOptions); + } } return response; diff --git a/src/app/api/v1/auth/logout/route.ts b/src/app/api/v1/auth/logout/route.ts index 34951b6e..62855bd1 100644 --- a/src/app/api/v1/auth/logout/route.ts +++ b/src/app/api/v1/auth/logout/route.ts @@ -60,6 +60,7 @@ export async function POST(request: NextRequest) { message: "Logged out successfully", }); response.cookies.delete("refreshToken"); + response.cookies.delete("access_token"); return response; } catch (error) { diff --git a/src/app/api/v1/auth/refresh/route.ts b/src/app/api/v1/auth/refresh/route.ts index 8304a477..76a17545 100644 --- a/src/app/api/v1/auth/refresh/route.ts +++ b/src/app/api/v1/auth/refresh/route.ts @@ -91,6 +91,7 @@ export async function POST(request: NextRequest) { }; + response.cookies.set("access_token", result.accessToken, cookieOptions); response.cookies.set("refreshToken", result.refreshToken, cookieOptions); return response; diff --git a/src/app/api/v1/auth/verify-login-otp/route.ts b/src/app/api/v1/auth/verify-login-otp/route.ts index b508784e..52a40707 100644 --- a/src/app/api/v1/auth/verify-login-otp/route.ts +++ b/src/app/api/v1/auth/verify-login-otp/route.ts @@ -61,6 +61,7 @@ export const POST = withHandler( maxAge: body.rememberMe ? 30 * 24 * 60 * 60 : 7 * 24 * 60 * 60, }; + response.cookies.set("access_token", result.accessToken, cookieOptions); response.cookies.set("refreshToken", result.refreshToken, cookieOptions); return response; diff --git a/src/server/services/attention.service.ts b/src/server/services/attention.service.ts index a4305bba..4a42c68a 100644 --- a/src/server/services/attention.service.ts +++ b/src/server/services/attention.service.ts @@ -81,13 +81,11 @@ export class AttentionService { ]); return { - contractsPendingSignature: - contractsResult[0]?.contractsPendingSignature ?? 0, - milestonesCompleted: milestonesResult[0]?.milestonesCompleted ?? 0, - invoicesRequiringPayment: - invoicesResult[0]?.invoicesRequiringPayment ?? 0, - pendingTimesheets: timesheetsResult[0]?.pendingTimesheets ?? 0, - pendingTimeOffRequests: timeOffResult[0]?.pendingTimeOffRequests ?? 0, + contractsPendingSignature: Number(contractsResult[0]?.contractsPendingSignature ?? 0), + milestonesCompleted: Number(milestonesResult[0]?.milestonesCompleted ?? 0), + invoicesRequiringPayment: Number(invoicesResult[0]?.invoicesRequiringPayment ?? 0), + pendingTimesheets: Number(timesheetsResult[0]?.pendingTimesheets ?? 0), + pendingTimeOffRequests: Number(timeOffResult[0]?.pendingTimeOffRequests ?? 0), }; } } diff --git a/src/server/services/jwt.service.ts b/src/server/services/jwt.service.ts index 76ebe21c..f67aee7f 100644 --- a/src/server/services/jwt.service.ts +++ b/src/server/services/jwt.service.ts @@ -29,10 +29,12 @@ export class JWTService { } private static get ACCESS_SECRET() { - return new TextEncoder().encode(process.env.JWT_ACCESS_SECRET || ""); + const secret = process.env.JWT_ACCESS_SECRET || process.env.JWT_SECRET || "vestroll-fallback-secret"; + return new TextEncoder().encode(secret); } private static get REFRESH_SECRET() { - return new TextEncoder().encode(process.env.JWT_REFRESH_SECRET || ""); + const secret = process.env.JWT_REFRESH_SECRET || process.env.JWT_SECRET || "vestroll-fallback-secret"; + return new TextEncoder().encode(secret); } private static get ACCESS_EXPIRATION() { return process.env.JWT_ACCESS_EXPIRATION || "15m"; @@ -49,9 +51,6 @@ export class JWTService { * @throws {Error} If JWT_ACCESS_SECRET is not configured. */ static async generateAccessToken(payload: JWTPayload): Promise { - if (!process.env.JWT_ACCESS_SECRET) { - throw new Error("JWT_ACCESS_SECRET is not configured"); - } return await new jose.SignJWT(payload) .setProtectedHeader({ alg: "HS256" }) @@ -68,9 +67,6 @@ export class JWTService { * @throws {Error} If JWT_REFRESH_SECRET is not configured. */ static async generateRefreshToken(payload: JWTPayload): Promise { - if (!process.env.JWT_REFRESH_SECRET) { - throw new Error("JWT_REFRESH_SECRET is not configured"); - } return await new jose.SignJWT(payload) .setProtectedHeader({ alg: "HS256" }) @@ -88,9 +84,6 @@ export class JWTService { * @throws {InvalidTokenError} If the token is invalid. */ static async verifyAccessToken(token: string): Promise { - if (!process.env.JWT_ACCESS_SECRET) { - throw new Error("JWT_ACCESS_SECRET is not configured"); - } try { const { payload } = await jose.jwtVerify(token, this.ACCESS_SECRET); @@ -112,9 +105,6 @@ export class JWTService { * @throws {InvalidTokenError} If the token is invalid. */ static async verifyRefreshToken(token: string): Promise { - if (!process.env.JWT_REFRESH_SECRET) { - throw new Error("JWT_REFRESH_SECRET is not configured"); - } try { const { payload } = await jose.jwtVerify(token, this.REFRESH_SECRET); diff --git a/src/server/services/login-otp.service.ts b/src/server/services/login-otp.service.ts index ffc26fc1..6f9ae479 100644 --- a/src/server/services/login-otp.service.ts +++ b/src/server/services/login-otp.service.ts @@ -3,7 +3,7 @@ import { eq, and, desc } from "drizzle-orm"; import { OTPService } from "./otp.service"; import { EmailService } from "./email.service"; import { UserService } from "./user.service"; -import { JWTTokenService } from "./jwt-token.service"; +import { JWTService } from "./jwt.service"; import { SessionManagementService } from "./session-management.service"; import { NotFoundError, BadRequestError, UnauthorizedError, ForbiddenError } from "../utils/errors"; import { Logger } from "./logger.service"; @@ -120,15 +120,16 @@ export class LoginOTPService { // Issue tokens const sessionId = crypto.randomUUID(); - const accessToken = await JWTTokenService.generateAccessToken({ + const accessToken = await JWTService.generateAccessToken({ userId: user.id, email: user.email, }); - const refreshToken = await JWTTokenService.generateRefreshToken( - { userId: user.id, email: user.email, sessionId }, - rememberMe, - ); + const refreshToken = await JWTService.generateRefreshToken({ + userId: user.id, + email: user.email, + sessionId, + }); const expiresAt = new Date(Date.now() + (rememberMe ? 30 : 7) * 24 * 60 * 60 * 1000); diff --git a/src/server/services/onboarding.service.ts b/src/server/services/onboarding.service.ts index a07c86d2..93f48845 100644 --- a/src/server/services/onboarding.service.ts +++ b/src/server/services/onboarding.service.ts @@ -59,7 +59,7 @@ export class OnboardingService { const companyInfoProvided = companyProfileResult.length > 0; const kybVerified = kybResult[0]?.status === "verified"; - const walletFunded = !!(walletResult[0] as { funded?: boolean })?.funded; + const walletFunded = !!(walletResult[0] as any)?.funded; const steps: OnboardingStep[] = [ { key: "emailVerified", label: "Email Verification", completed: emailVerified }, diff --git a/src/server/services/token-refresh.service.ts b/src/server/services/token-refresh.service.ts index f19a3660..85ffddf2 100644 --- a/src/server/services/token-refresh.service.ts +++ b/src/server/services/token-refresh.service.ts @@ -1,8 +1,7 @@ import { db } from "../db"; import { sessions, users } from "../db/schema"; import { eq } from "drizzle-orm"; -import { JWTVerificationService } from "./jwt-verification.service"; -import { JWTTokenService } from "./jwt-token.service"; +import { JWTService } from "./jwt.service"; import { PasswordVerificationService } from "./password-verification.service"; import { ExpiredTokenError, @@ -13,11 +12,11 @@ import { import { Logger } from "./logger.service"; export class TokenRefreshService { - static async refresh(refreshToken: string, userAgent?: string, ipAddress?: string) { + static async refresh(refreshToken: string, _userAgent?: string, ipAddress?: string) { let payload; try { - payload = await JWTVerificationService.verify(refreshToken); + payload = await JWTService.verifyRefreshToken(refreshToken); } catch (error) { Logger.error("Token refresh verification failed", { ipAddress, error: String(error) }); throw error; @@ -56,19 +55,16 @@ export class TokenRefreshService { throw new InternalAuthError("User not found"); } - const accessToken = await JWTTokenService.generateAccessToken({ + const accessToken = await JWTService.generateAccessToken({ userId: user.id, email: user.email, }); - - const tokenExp = payload.exp as number; - - const newRefreshToken = await JWTTokenService.generateRotatedRefreshToken({ + const newRefreshToken = await JWTService.generateRefreshToken({ userId: user.id, email: user.email, sessionId - }, tokenExp); + }); const newRefreshTokenHash = await PasswordVerificationService.hash(newRefreshToken); diff --git a/src/server/utils/auth.ts b/src/server/utils/auth.ts index 48845ca7..105b6437 100644 --- a/src/server/utils/auth.ts +++ b/src/server/utils/auth.ts @@ -7,6 +7,8 @@ import crypto from "crypto"; import { JWTTokenService } from "../services/jwt-token.service"; import { SessionService } from "../services/session.service"; +import { JWTService } from "../services/jwt.service"; + interface TokenPayload { userId: string; email: string; @@ -15,52 +17,19 @@ interface TokenPayload { } export class AuthUtils { - private static readonly SECRET = process.env.JWT_SECRET || "vestroll-secret-key"; - private static readonly TOKEN_EXPIRY = 24 * 60 * 60 * 1000; - - static generateToken(userId: string, email: string): string { - const payload: TokenPayload = { - userId, - email, - iat: Date.now(), - exp: Date.now() + this.TOKEN_EXPIRY, - }; - - const payloadString = Buffer.from(JSON.stringify(payload)).toString("base64url"); - const signature = crypto - .createHmac("sha256", this.SECRET) - .update(payloadString) - .digest("base64url"); - - return `${payloadString}.${signature}`; + static async generateToken(userId: string, email: string): Promise { + return await JWTService.generateAccessToken({ userId, email }); } - static verifyToken(token: string): TokenPayload | null { + static async verifyToken(token: string): Promise { try { - const [payloadString, signature] = token.split("."); - - if (!payloadString || !signature) { - return null; - } - - const expectedSignature = crypto - .createHmac("sha256", this.SECRET) - .update(payloadString) - .digest("base64url"); - - if (signature !== expectedSignature) { - return null; - } - - const payload: TokenPayload = JSON.parse( - Buffer.from(payloadString, "base64url").toString() - ); - - if (payload.exp < Date.now()) { - return null; - } - - return payload; + const payload = await JWTService.verifyAccessToken(token); + return { + userId: payload.userId, + email: payload.email, + iat: (payload.iat ?? 0) * 1000, + exp: (payload.exp ?? 0) * 1000, + }; } catch { return null; } @@ -85,33 +54,51 @@ export class AuthUtils { email: string; user: typeof users.$inferSelect; }> { - const token = this.extractToken(request); + const authHeaderToken = this.extractToken(request); + const cookieToken = request.cookies.get("access_token")?.value; + + // Defensive check: sometimes request.cookies is not fully populated in some environments + let fallbackCookieToken = null; + if (!cookieToken) { + const cookieHeader = request.headers.get("cookie"); + fallbackCookieToken = cookieHeader?.match(/access_token=([^;]+)/)?.[1]; + } + + const token = authHeaderToken ?? cookieToken ?? fallbackCookieToken; if (!token) { throw new UnauthorizedError("Authentication required"); } - const payload = this.verifyToken(token); - - if (!payload) { - throw new UnauthorizedError("Invalid or expired token"); - } + try { + const payload = await this.verifyToken(token); + if (!payload) { + throw new UnauthorizedError("Invalid or expired token"); + } - const [user] = await db.select().from(users).where(eq(users.id, payload.userId)); + const [user] = await db.select().from(users).where(eq(users.id, payload.userId)); + if (!user) { + throw new UnauthorizedError("User not found"); + } - if (!user) { - throw new UnauthorizedError("User not found"); - } + if (user.status === "suspended") { + throw new UnauthorizedError("Account is suspended"); + } - if (user.status === "suspended") { - throw new UnauthorizedError("Account is suspended"); + return { + userId: payload.userId, + email: payload.email, + user, + }; + } catch (error) { + if (error instanceof UnauthorizedError) throw error; + console.error("[AuthUtils.authenticateRequest Error]", { + message: error instanceof Error ? error.message : String(error), + stack: error instanceof Error ? error.stack : undefined, + token: token ? `${token.substring(0, 10)}...` : "none" + }); + throw error; } - - return { - userId: payload.userId, - email: payload.email, - user, - }; } static async authenticateRequestOrRefreshCookie(request: NextRequest): Promise<{ @@ -130,7 +117,7 @@ export class AuthUtils { throw new UnauthorizedError("Authentication required"); } - const payload = await JWTTokenService.verifyToken(refreshToken); + const payload = await JWTService.verifyRefreshToken(refreshToken); const userId = payload && typeof payload.userId === "string" ? payload.userId : null; const email = payload && typeof payload.email === "string" ? payload.email : null; @@ -179,4 +166,22 @@ export class AuthUtils { static getUserAgent(request: NextRequest): string | undefined { return request.headers.get("user-agent") || undefined; } + + static async getCurrentUser() { + const { cookies } = await import("next/headers"); + const cookieStore = await cookies(); + const token = cookieStore.get("access_token")?.value; + + if (!token) { + return null; + } + + const payload = await this.verifyToken(token); + if (!payload) { + return null; + } + + const [user] = await db.select().from(users).where(eq(users.id, payload.userId)); + return user || null; + } } From 5feb3a683bb6fba8d937af556c4ea268266317ac Mon Sep 17 00:00:00 2001 From: Lewechi Date: Thu, 30 Apr 2026 19:04:14 +0100 Subject: [PATCH 02/21] fix: fixes --- .../profile-settings/2fa-setup/page.tsx | 4 +- src/app/(dashboard)/profile-settings/page.tsx | 6 +-- src/app/(dashboard)/team-management/page.tsx | 4 +- .../contracts/ContractCompletionModal.tsx | 2 +- .../contracts/ui/FirstContractBanner.tsx | 2 +- .../features/expenses/ExpenseHeader.tsx | 2 +- src/components/layout/sidebar.tsx | 45 ++++++++++++------ src/lib/api/auth.ts | 4 ++ src/lib/api/finance.ts | 47 +++++++------------ 9 files changed, 63 insertions(+), 53 deletions(-) diff --git a/src/app/(dashboard)/profile-settings/2fa-setup/page.tsx b/src/app/(dashboard)/profile-settings/2fa-setup/page.tsx index 7aa275b8..db2dec10 100644 --- a/src/app/(dashboard)/profile-settings/2fa-setup/page.tsx +++ b/src/app/(dashboard)/profile-settings/2fa-setup/page.tsx @@ -23,9 +23,9 @@ export default function FaSetupPage() { const handleContinue = () => { if (selectedMethod === "authenticator") { - router.push("/app/profile-settings/2fa-setup/qr-code"); + router.push("/profile-settings/2fa-setup/qr-code"); } else if (selectedMethod === "email") { - router.push("/app/profile-settings/2fa-setup/email-verify"); + router.push("/profile-settings/2fa-setup/email-verify"); } }; diff --git a/src/app/(dashboard)/profile-settings/page.tsx b/src/app/(dashboard)/profile-settings/page.tsx index 3d93ddb1..80afd0ab 100644 --- a/src/app/(dashboard)/profile-settings/page.tsx +++ b/src/app/(dashboard)/profile-settings/page.tsx @@ -83,7 +83,7 @@ export default function Page() {
- + diff --git a/src/components/features/contracts/ui/FirstContractBanner.tsx b/src/components/features/contracts/ui/FirstContractBanner.tsx index 119bf0fb..ca9d0676 100644 --- a/src/components/features/contracts/ui/FirstContractBanner.tsx +++ b/src/components/features/contracts/ui/FirstContractBanner.tsx @@ -40,7 +40,7 @@ export default function FirstContractBanner() { transition={{ delay: 0.4, duration: 0.4 }} > New contract diff --git a/src/components/features/expenses/ExpenseHeader.tsx b/src/components/features/expenses/ExpenseHeader.tsx index fe85f43a..a66e2f7a 100644 --- a/src/components/features/expenses/ExpenseHeader.tsx +++ b/src/components/features/expenses/ExpenseHeader.tsx @@ -21,7 +21,7 @@ export default function ExpenseHeader({
diff --git a/src/components/layout/sidebar.tsx b/src/components/layout/sidebar.tsx index bd25492e..96dee341 100644 --- a/src/components/layout/sidebar.tsx +++ b/src/components/layout/sidebar.tsx @@ -8,6 +8,8 @@ import { Settings, LogOut, X } from "lucide-react"; import { motion } from "framer-motion"; import { ThemeToggle } from "../shared/theme-toggle"; import logoSet from "@/../public/LogoSet.png"; +import { AuthService } from "@/lib/api/auth"; +import { useRouter } from "next/navigation"; type NavItem = { name: string; @@ -44,8 +46,21 @@ export default function Sidebar({ onCloseMobile, }: SidebarProps) { const pathname = usePathname(); + const router = useRouter(); const [isClient, setIsClient] = useState(false); + const handleLogout = async (e: React.MouseEvent) => { + e.preventDefault(); + try { + await AuthService.logout(); + router.push("/login"); + } catch (error) { + console.error("Logout failed:", error); + // Fallback redirect even if API fails + router.push("/login"); + } + }; + useEffect(() => setIsClient(true), []); const isActive = useMemo( @@ -136,19 +151,19 @@ export default function Sidebar({ })} -
+
-
@@ -230,16 +245,16 @@ export default function Sidebar({ })} -
+
-
@@ -333,19 +348,19 @@ export default function Sidebar({ })} -
+
-
diff --git a/src/lib/api/auth.ts b/src/lib/api/auth.ts index 8a8d5fe3..74895377 100644 --- a/src/lib/api/auth.ts +++ b/src/lib/api/auth.ts @@ -61,6 +61,10 @@ export class AuthService { static async completeRegistration(data: CompleteRegistrationData) { return apiClient.post("/api/v1/auth/complete-registration", data); } + + static async logout() { + return apiClient.post("/api/v1/auth/logout", {}); + } } diff --git a/src/lib/api/finance.ts b/src/lib/api/finance.ts index 1325374f..06c302ea 100644 --- a/src/lib/api/finance.ts +++ b/src/lib/api/finance.ts @@ -1,4 +1,6 @@ import { apiClient } from "../api-client"; +import { Contract } from "@/lib/data/contracts"; +import { Invoice } from "@/lib/data/invoices"; export interface PayrollEmployee { id: string; @@ -38,35 +40,6 @@ export interface RunPayrollInput { providerId?: "monnify" | "flutterwave"; } -export class FinanceService { - static async getPendingPayroll(): Promise { - return apiClient.get("/api/v1/finance/payroll"); - } - - static async submitPayroll(data: RunPayrollInput): Promise { - return apiClient.post("/api/v1/finance/payroll", data); -import { Contract } from "@/lib/data/contracts"; -import { Invoice } from "@/lib/data/invoices"; - -export class FinanceService { - static async getContracts(): Promise { - const res = await fetch("/api/v1/finance/contracts", { - credentials: "include", - }); - if (!res.ok) throw new Error("Failed to fetch contracts"); - const json = await res.json(); - return json.data ?? []; - } - - static async getInvoices(): Promise { - const res = await fetch("/api/v1/finance/invoices", { - credentials: "include", - }); - if (!res.ok) throw new Error("Failed to fetch invoices"); - const json = await res.json(); - return json.data ?? []; -import { apiClient } from "../api-client"; - interface DepositRequest { amount: number; provider?: "monnify" | "flutterwave"; @@ -85,6 +58,22 @@ interface DepositResponse { } export class FinanceService { + static async getPendingPayroll(): Promise { + return apiClient.get("/api/v1/finance/payroll"); + } + + static async submitPayroll(data: RunPayrollInput): Promise { + return apiClient.post("/api/v1/finance/payroll", data); + } + + static async getContracts(): Promise { + return apiClient.get("/api/v1/finance/contracts"); + } + + static async getInvoices(): Promise { + return apiClient.get("/api/v1/finance/invoices"); + } + static async initializeDeposit(request: DepositRequest): Promise { return apiClient.post("/api/v1/finance/fiat/deposit", request); } From b794f9753d066cf80e6b5593f93f9859e86a0de8 Mon Sep 17 00:00:00 2001 From: Lewechi Date: Thu, 30 Apr 2026 19:07:48 +0100 Subject: [PATCH 03/21] fix: fixes --- .../(dashboard)/dashboard/checklist/page.tsx | 2 +- src/app/(dashboard)/finance/page.tsx | 37 ++++++++++++++----- .../addresses/billing-address/page.tsx | 2 +- .../addresses/registered-address/page.tsx | 2 +- .../(components)/HiringTemplatePage.tsx | 2 +- .../hiring-templates/edit/[id]/page.tsx | 8 ++-- src/app/(dashboard)/settings/page.tsx | 2 +- .../settings/registered-address/page.tsx | 2 +- src/server/services/payroll.service.ts | 4 +- 9 files changed, 40 insertions(+), 21 deletions(-) diff --git a/src/app/(dashboard)/dashboard/checklist/page.tsx b/src/app/(dashboard)/dashboard/checklist/page.tsx index c5d32d62..6c61f4f7 100644 --- a/src/app/(dashboard)/dashboard/checklist/page.tsx +++ b/src/app/(dashboard)/dashboard/checklist/page.tsx @@ -5,7 +5,7 @@ import { useRouter } from "next/navigation"; export default function ChecklistPage() { const router = useRouter(); useEffect(() => { - router.replace("/app/dashboard"); + router.replace("/dashboard"); }, [router]); return null; diff --git a/src/app/(dashboard)/finance/page.tsx b/src/app/(dashboard)/finance/page.tsx index f9a251a2..74fdd894 100644 --- a/src/app/(dashboard)/finance/page.tsx +++ b/src/app/(dashboard)/finance/page.tsx @@ -2,7 +2,7 @@ import { motion } from "framer-motion"; import { useEffect, useMemo, useState } from "react"; -import { MOCK_ASSETS, generateMockTransactions } from "@/lib/mock-data"; +import { MOCK_ASSETS } from "@/lib/mock-data"; import { BalanceSection } from "@/components/features/finance/balance-section"; import { AssetsGrid } from "@/components/features/finance/assets-grid"; import { DepositModal } from "@/components/features/finance/DepositModal"; @@ -100,9 +100,22 @@ const normalizeTransaction = ( }); export default function FinancePage() { + // State for balance + const [ngnBalance, setNgnBalance] = useState("₦0.00"); + + // State for transactions + const [transactions, setTransactions] = useState([]); + const [isLoadingTransactions, setIsLoadingTransactions] = useState(true); + const [transactionsError, setTransactionsError] = useState(null); + const [currentPage, setCurrentPage] = useState(1); + const [itemsPerPage, setItemsPerPage] = useState(10); + const [totalItems, setTotalItems] = useState(0); + const [totalPages, setTotalPages] = useState(1); + + // State for UI const [search, setSearch] = useState(""); const [selectedItems, setSelectedItems] = useState([]); - const [ngnBalance, setNgnBalance] = useState("₦0.00"); + const [isDepositModalOpen, setIsDepositModalOpen] = useState(false); // Fetch organization fiat balance useEffect(() => { @@ -116,23 +129,31 @@ export default function FinancePage() { } } catch (error) { console.error("Failed to fetch NGN balance:", error); - // Keep default 0 } }; fetchBalance(); }, []); + // Fetch transactions + useEffect(() => { + const controller = new AbortController(); + + const fetchTransactions = async () => { + setIsLoadingTransactions(true); + setTransactionsError(null); + try { const params = new URLSearchParams({ page: String(currentPage), limit: String(itemsPerPage), }); + const response = await fetch( `/api/v1/finance/transactions?${params.toString()}`, - { signal: controller.signal }, + { signal: controller.signal } ); + const payload = (await response.json()) as TransactionsResponse; - const [isDepositModalOpen, setIsDepositModalOpen] = useState(false); if (!response.ok || !payload.success || !payload.data) { throw new Error(payload.message || "Unable to load transactions"); @@ -150,9 +171,7 @@ export default function FinancePage() { setTotalItems(0); setTotalPages(1); setTransactionsError( - error instanceof Error - ? error.message - : "Unable to load transactions", + error instanceof Error ? error.message : "Unable to load transactions" ); } finally { if (!controller.signal.aborted) { @@ -351,7 +370,7 @@ export default function FinancePage() { selectedTab="Transactions" searchPlaceholder="Search transactions..." showSearch={false} - seeAllHref="/app/finance/transactions" + seeAllHref="/finance/transactions" selectedItems={selectedItems} onSelectItem={handleSelectItem} onSelectAll={handleSelectAll} diff --git a/src/app/(dashboard)/settings/company/addresses/billing-address/page.tsx b/src/app/(dashboard)/settings/company/addresses/billing-address/page.tsx index 0d7d578b..eb492924 100644 --- a/src/app/(dashboard)/settings/company/addresses/billing-address/page.tsx +++ b/src/app/(dashboard)/settings/company/addresses/billing-address/page.tsx @@ -90,7 +90,7 @@ export default function BillingAddressPage() {
diff --git a/src/app/(dashboard)/settings/company/addresses/registered-address/page.tsx b/src/app/(dashboard)/settings/company/addresses/registered-address/page.tsx index a43560bb..ed5fb73f 100644 --- a/src/app/(dashboard)/settings/company/addresses/registered-address/page.tsx +++ b/src/app/(dashboard)/settings/company/addresses/registered-address/page.tsx @@ -90,7 +90,7 @@ export default function RegisteredAddressPage() {
diff --git a/src/app/(dashboard)/settings/hiring-templates/(components)/HiringTemplatePage.tsx b/src/app/(dashboard)/settings/hiring-templates/(components)/HiringTemplatePage.tsx index 72818f68..75055fd7 100644 --- a/src/app/(dashboard)/settings/hiring-templates/(components)/HiringTemplatePage.tsx +++ b/src/app/(dashboard)/settings/hiring-templates/(components)/HiringTemplatePage.tsx @@ -99,7 +99,7 @@ const HiringTemplatePage: React.FC = () => { alert("Template created successfully!"); - router.push("/app/settings"); + router.push("/settings"); } catch (error) { console.error("Error submitting form:", error); alert("Error creating template. Please try again."); diff --git a/src/app/(dashboard)/settings/hiring-templates/edit/[id]/page.tsx b/src/app/(dashboard)/settings/hiring-templates/edit/[id]/page.tsx index ee07f4de..b54508ba 100644 --- a/src/app/(dashboard)/settings/hiring-templates/edit/[id]/page.tsx +++ b/src/app/(dashboard)/settings/hiring-templates/edit/[id]/page.tsx @@ -110,7 +110,7 @@ export default function EditTemplatePage() { } }, 100); } else { - router.push("/app/settings/hiring-templates"); + router.push("/settings/hiring-templates"); } setIsLoading(false); }, [templateId, router]); @@ -178,7 +178,7 @@ export default function EditTemplatePage() { alert("Template updated successfully!"); // Redirect back to templates list - router.push("/app/settings/"); + router.push("/settings/"); } catch (error) { console.error("Error updating template:", error); alert("Error updating template. Please try again."); @@ -188,7 +188,7 @@ export default function EditTemplatePage() { }; const handleBack = (): void => { - router.push("/app/settings"); + router.push("/settings"); }; const handleInputChange = (e: React.ChangeEvent) => { @@ -225,7 +225,7 @@ export default function EditTemplatePage() { alert("Template deleted successfully!"); // Redirect back to templates list - router.push("/app/settings/hiring-templates"); + router.push("/settings/hiring-templates"); } catch (error) { console.error("Error deleting template:", error); alert("Error deleting template. Please try again."); diff --git a/src/app/(dashboard)/settings/page.tsx b/src/app/(dashboard)/settings/page.tsx index 06491fbe..1899ecaf 100644 --- a/src/app/(dashboard)/settings/page.tsx +++ b/src/app/(dashboard)/settings/page.tsx @@ -94,7 +94,7 @@ function HeaderTab({
diff --git a/src/app/(dashboard)/settings/registered-address/page.tsx b/src/app/(dashboard)/settings/registered-address/page.tsx index 51de5f40..11c5e639 100644 --- a/src/app/(dashboard)/settings/registered-address/page.tsx +++ b/src/app/(dashboard)/settings/registered-address/page.tsx @@ -40,7 +40,7 @@ export default function BillingAddressPage() {
diff --git a/src/server/services/payroll.service.ts b/src/server/services/payroll.service.ts index a6af296c..d1d05562 100644 --- a/src/server/services/payroll.service.ts +++ b/src/server/services/payroll.service.ts @@ -1,5 +1,5 @@ import { db, invoices } from "@/server/db"; -import { eq, and, sql } from "drizzle-orm"; +import { eq, and, inArray } from "drizzle-orm"; import { FiatDisbursementService } from "./fiat-disbursement.service"; import { createFiatProvider, type FiatProviderPreference } from "./fiat"; import { BadRequestError } from "@/server/utils/errors"; @@ -35,7 +35,7 @@ export class PayrollService { .where( and( eq(invoices.organizationId, organizationId), - sql`${invoices.id} IN (${sql.join(invoiceIds, sql`, `)})`, + inArray(invoices.id, invoiceIds), eq(invoices.status, "pending") ) ); From f35a0027286eaccfa0c8499b9e022d681b9be0db Mon Sep 17 00:00:00 2001 From: Lewechi Date: Thu, 30 Apr 2026 19:13:24 +0100 Subject: [PATCH 04/21] fix: fixes --- .../payroll/components/PayoutHistory.tsx | 33 +++---------------- 1 file changed, 4 insertions(+), 29 deletions(-) diff --git a/src/app/(dashboard)/payroll/components/PayoutHistory.tsx b/src/app/(dashboard)/payroll/components/PayoutHistory.tsx index 76c3b3dd..2b710f84 100644 --- a/src/app/(dashboard)/payroll/components/PayoutHistory.tsx +++ b/src/app/(dashboard)/payroll/components/PayoutHistory.tsx @@ -1,19 +1,13 @@ - 'use client'; + +import { useState, useEffect } from 'react'; +import { useRouter } from 'next/navigation'; +import { Filter, Search } from "lucide-react"; import Table from '@/components/shared/table/Table'; import { TableColumn } from '@/components/shared/table/TableHeader'; -import { useState } from 'react'; -import { useRouter } from 'next/navigation'; import { UsdtIcon } from '@/../public/svg'; -import { RoutePaths } from '@/routes/routesPath'; import { formatCurrency } from '@/utils/formatters'; -import Table from "@/components/shared/table/Table"; -import { TableColumn } from "@/components/shared/table/TableHeader"; -import { useEffect, useState } from "react"; -import { UsdtIcon } from "@/../public/svg"; -import { Filter, Search } from "lucide-react"; - type ContractType = "Fixed rate" | "Pay as you go" | "Milestone"; interface PayrollRecord { @@ -123,21 +117,6 @@ const columns: TableColumn[] = [ { key: "paidIn", header: "Paid in", align: "center" }, { key: "timestamp", header: "Timestamp", align: "right" }, ]; -function formatAmount(amount: number, currency: string): string { - return formatCurrency(amount, { currency, isKobo: false }); -} - -function formatRate(title: string, currency: string): string { - if (title.includes('[CUR]')) { - return title.replace('[CUR]', getCurrencyPrefix(currency)); - } - - if (!title.startsWith('$')) { - return title; - } - - return `${getCurrencyPrefix(currency)}${title.slice(1)}`; -} const SkeletonRow = () => (
@@ -227,7 +206,6 @@ const PayoutHistory = () => { const renderMobileCell = (item: PayrollRecord) => (
- {/* Row 1: name + badge */}

{item.employeeName} @@ -238,7 +216,6 @@ const PayoutHistory = () => { {item.contractType}

- {/* Row 2: amount | token | timestamp */}
${item.amount.toLocaleString()}.00 @@ -266,7 +243,6 @@ const PayoutHistory = () => { return (
- {/* Section header */}

History

@@ -286,7 +262,6 @@ const PayoutHistory = () => {
- {/* Loading skeleton */} {loading ? (
{Array.from({ length: 8 }).map((_, i) => ( From 034324c3731c609177f21503ce6cb1ff4869303d Mon Sep 17 00:00:00 2001 From: Lewechi Date: Thu, 30 Apr 2026 19:26:53 +0100 Subject: [PATCH 05/21] fix: fixes --- src/server/services/kyb-upload.service.ts | 5 +++-- src/server/services/onboarding.service.ts | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/server/services/kyb-upload.service.ts b/src/server/services/kyb-upload.service.ts index 24a99f4c..f600b3cf 100644 --- a/src/server/services/kyb-upload.service.ts +++ b/src/server/services/kyb-upload.service.ts @@ -2,6 +2,7 @@ import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3"; import { getSignedUrl } from "@aws-sdk/s3-request-presigner"; import crypto from "crypto"; import { AppError } from "../utils/errors"; +import { Logger } from "./logger.service"; import { KYB_FILE_CONSTRAINTS } from "../validations/kyb.schema"; const S3_CONFIG = { @@ -43,7 +44,7 @@ export class KybUploadService { contentType: string, ): Promise { // Validate content type - if (!KYB_FILE_CONSTRAINTS.allowedMimeTypes.includes(contentType as any)) { + if (!(KYB_FILE_CONSTRAINTS.allowedMimeTypes as readonly string[]).includes(contentType)) { throw new AppError( `Invalid content type. Allowed: ${KYB_FILE_CONSTRAINTS.allowedMimeTypes.join(", ")}`, 400, @@ -95,7 +96,7 @@ export class KybUploadService { }); await this.getS3Client().send(command); } catch (error) { - console.error(`Failed to delete S3 object: ${key}`, error); + Logger.error(`Failed to delete S3 object: ${key}`, { error: error instanceof Error ? error.message : String(error) }); } } } diff --git a/src/server/services/onboarding.service.ts b/src/server/services/onboarding.service.ts index 93f48845..f3b3cf0d 100644 --- a/src/server/services/onboarding.service.ts +++ b/src/server/services/onboarding.service.ts @@ -59,7 +59,7 @@ export class OnboardingService { const companyInfoProvided = companyProfileResult.length > 0; const kybVerified = kybResult[0]?.status === "verified"; - const walletFunded = !!(walletResult[0] as any)?.funded; + const walletFunded = walletResult.length > 0 && !!walletResult[0].funded; const steps: OnboardingStep[] = [ { key: "emailVerified", label: "Email Verification", completed: emailVerified }, From e6004d2d55513c56349da77538bfcbe001d0ed32 Mon Sep 17 00:00:00 2001 From: Lewechi Date: Thu, 30 Apr 2026 19:39:12 +0100 Subject: [PATCH 06/21] fix: fixes --- drizzle.config.ts | 17 +++++------------ scratch/check-kit.js | 2 ++ 2 files changed, 7 insertions(+), 12 deletions(-) create mode 100644 scratch/check-kit.js diff --git a/drizzle.config.ts b/drizzle.config.ts index aff7b178..c4e217aa 100644 --- a/drizzle.config.ts +++ b/drizzle.config.ts @@ -1,8 +1,7 @@ -import { defineConfig } from "drizzle-kit"; import fs from "node:fs"; import path from "node:path"; -function getDatabaseUrl() { +function getDatabaseUrl(): string { if (process.env.DATABASE_URL) { return process.env.DATABASE_URL; } @@ -22,22 +21,16 @@ function getDatabaseUrl() { if (match?.[1]) return match[1].trim(); } - return undefined; -} - -const databaseUrl = getDatabaseUrl(); - -if (!databaseUrl) { throw new Error( "DATABASE_URL is not set. Add it to .env.local or your shell environment.", ); } -export default defineConfig({ +export default { schema: "./src/server/db/schema.ts", out: "./drizzle/migrations", - dialect: "postgresql", + driver: "pg", dbCredentials: { - url: databaseUrl, + connectionString: getDatabaseUrl(), }, -}); +}; diff --git a/scratch/check-kit.js b/scratch/check-kit.js new file mode 100644 index 00000000..f240f3a5 --- /dev/null +++ b/scratch/check-kit.js @@ -0,0 +1,2 @@ +const kit = require('drizzle-kit'); +console.log(Object.keys(kit)); From bde17095102f9bf631cf90a220025a89e9796cc3 Mon Sep 17 00:00:00 2001 From: Lewechi Date: Thu, 30 Apr 2026 19:43:10 +0100 Subject: [PATCH 07/21] fix: fixes --- .../features/auth/business-details.tsx | 19 ++++++++++--------- src/components/ui/file-upload.tsx | 8 ++++++++ 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/src/components/features/auth/business-details.tsx b/src/components/features/auth/business-details.tsx index 9f858f22..1aff7cda 100644 --- a/src/components/features/auth/business-details.tsx +++ b/src/components/features/auth/business-details.tsx @@ -27,8 +27,9 @@ interface FormErrors { businessDescription?: string; registrationType?: string; registrationNo?: string; - incorporationCertificate?: string; - memorandumArticle?: string; + incorporationCertificatePath?: string; + memorandumArticlePath?: string; + formC02C07Path?: string; } interface DropdownProps { @@ -219,11 +220,11 @@ const BusinessRegistrationForm: React.FC = () => { } if (!formData.incorporationCertificatePath) { - newErrors.incorporationCertificate = "Incorporation certificate is required"; + newErrors.incorporationCertificatePath = "Incorporation certificate is required"; } if (!formData.memorandumArticlePath) { - newErrors.memorandumArticle = "Memorandum & Article is required"; + newErrors.memorandumArticlePath = "Memorandum & Article is required"; } setErrors(newErrors); @@ -258,12 +259,12 @@ const BusinessRegistrationForm: React.FC = () => { // 3. Update state setFormData((prev) => ({ ...prev, [field]: key })); - if (errors[field as keyof FormErrors]) { - setErrors((prev) => ({ ...prev, [field as keyof FormErrors]: undefined })); + if (errors[field]) { + setErrors((prev) => ({ ...prev, [field]: undefined })); } } catch (error) { console.error("Upload failed:", error); - setErrors((prev) => ({ ...prev, [field as keyof FormErrors]: "Upload failed. Please try again." })); + setErrors((prev) => ({ ...prev, [field]: "Upload failed. Please try again." })); } }; @@ -496,7 +497,7 @@ const BusinessRegistrationForm: React.FC = () => { file={formData.incorporationCertificatePath ? new File([], "Uploaded Certificate") : null} accept=".png,.jpg,.jpeg,.pdf" maxSize={5} - error={errors.incorporationCertificate} + error={errors.incorporationCertificatePath} /> { file={formData.memorandumArticlePath ? new File([], "Uploaded Memorandum") : null} accept=".png,.jpg,.jpeg,.pdf" maxSize={5} - error={errors.memorandumArticle} + error={errors.memorandumArticlePath} /> = ({ @@ -22,6 +23,7 @@ const FileUpload: React.FC = ({ className = "", isUploading = false, uploadProgress = 0, + error, }) => { const [isDragOver, setIsDragOver] = useState(false); const componentId = label.replace(/\s+/g, "-").toLowerCase(); @@ -281,6 +283,12 @@ const FileUpload: React.FC = ({ id={`file-${componentId}`} /> )} + + {error && ( +

+ {error} +

+ )}
); }; From 2a927210f2ebd76baa746b8aa9787f8ffcdd2c21 Mon Sep 17 00:00:00 2001 From: Lewechi Date: Thu, 30 Apr 2026 19:47:01 +0100 Subject: [PATCH 08/21] fix: fixes --- src/components/features/finance/NairaTransactionHistory.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/components/features/finance/NairaTransactionHistory.tsx b/src/components/features/finance/NairaTransactionHistory.tsx index bd36b2c6..b40dbef1 100644 --- a/src/components/features/finance/NairaTransactionHistory.tsx +++ b/src/components/features/finance/NairaTransactionHistory.tsx @@ -71,9 +71,10 @@ const MOCK_FIAT_TXS: FiatTransaction[] = [ }, ]; +import { formatNairaFromKobo } from "@/lib/format-naira"; + function formatNgn(kobo: number): string { - return - (kobo, { currency: "NGN" }); + return formatNairaFromKobo(kobo); } function statusBadge(status: FiatTransaction["status"]) { From 085f2b4cdc2cba550f001917a0778cafd505a14b Mon Sep 17 00:00:00 2001 From: Lewechi Date: Thu, 30 Apr 2026 19:56:26 +0100 Subject: [PATCH 09/21] fix: fixes --- src/components/features/profile-settings/ImageUploadModal.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/features/profile-settings/ImageUploadModal.tsx b/src/components/features/profile-settings/ImageUploadModal.tsx index dac3498a..f0dd121c 100644 --- a/src/components/features/profile-settings/ImageUploadModal.tsx +++ b/src/components/features/profile-settings/ImageUploadModal.tsx @@ -160,8 +160,8 @@ const ImageUploadModal: React.FC = ({ const croppedFile = await getCroppedImage(); await onSave(croppedFile); handleCancel(); - } catch (error) { - console.error("Failed to save image:", error); + } catch (err) { + console.error("Failed to save image:", err); error("Failed to save image. Please try again."); } finally { setIsLoading(false); From 079d79c0b12f58fbc99b605fe79591465521359f Mon Sep 17 00:00:00 2001 From: Lewechi Date: Thu, 30 Apr 2026 20:03:52 +0100 Subject: [PATCH 10/21] fix:fixes --- .../features/profile-settings/ImageUploadModal.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/features/profile-settings/ImageUploadModal.tsx b/src/components/features/profile-settings/ImageUploadModal.tsx index f0dd121c..965d2bd7 100644 --- a/src/components/features/profile-settings/ImageUploadModal.tsx +++ b/src/components/features/profile-settings/ImageUploadModal.tsx @@ -14,7 +14,7 @@ const ImageUploadModal: React.FC = ({ currentImage, shape = "circle", }) => { - const { error } = useToast(); + const { error: showError } = useToast(); const [selectedImage, setSelectedImage] = useState(null); const [selectedFile, setSelectedFile] = useState(null); const [isDragOver, setIsDragOver] = useState(false); @@ -31,13 +31,13 @@ const ImageUploadModal: React.FC = ({ const handleFileSelect = useCallback((file: File) => { if (!file.type.startsWith("image/")) { - error("Please select a valid image file"); + showError("Please select a valid image file"); return; } if (file.size > 5 * 1024 * 1024) { // 5MB limit - error("File size must be less than 5MB"); + showError("File size must be less than 5MB"); return; } @@ -162,7 +162,7 @@ const ImageUploadModal: React.FC = ({ handleCancel(); } catch (err) { console.error("Failed to save image:", err); - error("Failed to save image. Please try again."); + showError("Failed to save image. Please try again."); } finally { setIsLoading(false); } From 7149cc40b869de7d88e29d0cfaf3a668119b10d1 Mon Sep 17 00:00:00 2001 From: Lewechi Date: Thu, 30 Apr 2026 20:09:57 +0100 Subject: [PATCH 11/21] fix: fixes --- src/server/db/schema.ts | 1 + src/server/services/invitation.service.ts | 10 ++++++---- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/server/db/schema.ts b/src/server/db/schema.ts index b8f7c6f7..b762ef08 100644 --- a/src/server/db/schema.ts +++ b/src/server/db/schema.ts @@ -684,6 +684,7 @@ export const fiatTransactions = pgTable( providerReference: varchar("provider_reference", { length: 255 }) .notNull() .unique(), + reference: varchar("reference", { length: 255 }).unique(), metadata: jsonb("metadata"), createdAt: timestamp("created_at").defaultNow().notNull(), }, diff --git a/src/server/services/invitation.service.ts b/src/server/services/invitation.service.ts index be52ce40..4246c5b1 100644 --- a/src/server/services/invitation.service.ts +++ b/src/server/services/invitation.service.ts @@ -1,6 +1,6 @@ import { db } from "../db"; import { organizationInvitations, users, organizations } from "../db/schema"; -import { eq, and, desc, lt } from "drizzle-orm"; +import { eq, and, desc, lt, count } from "drizzle-orm"; import crypto from "crypto"; import { addDays, isPast } from "date-fns"; import type { SQL } from "drizzle-orm"; @@ -264,12 +264,14 @@ class InvitationService { .offset(offset); // Get total count - const [{ count }] = await db - .select({ count: organizationInvitations.id }) + const [countResult] = await db + .select({ value: count() }) .from(organizationInvitations) .where(and(...whereConditions)); - return toPaginatedResponse(invitations, page, limit, Number(count)); + const total = countResult?.value ?? 0; + + return toPaginatedResponse(invitations, page, limit, Number(total)); } async acceptInvitation(token: string, userId: string): Promise { From c0dd5947040a07fe3b4d40b475a5301760d31230 Mon Sep 17 00:00:00 2001 From: Lewechi Date: Thu, 30 Apr 2026 20:17:17 +0100 Subject: [PATCH 12/21] fix: fixes --- .../invitations/InvitationManagement.tsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/components/features/team-management/invitations/InvitationManagement.tsx b/src/components/features/team-management/invitations/InvitationManagement.tsx index caf01ec6..9b0dbd4c 100644 --- a/src/components/features/team-management/invitations/InvitationManagement.tsx +++ b/src/components/features/team-management/invitations/InvitationManagement.tsx @@ -41,7 +41,7 @@ interface InvitationManagementProps { } const roleOptions = [ - { value: "", label: "All Roles" }, + { value: "all", label: "All Roles" }, { value: "admin", label: "Administrator" }, { value: "hr_manager", label: "HR Manager" }, { value: "payroll_manager", label: "Payroll Manager" }, @@ -49,7 +49,7 @@ const roleOptions = [ ]; const statusOptions = [ - { value: "", label: "All Status" }, + { value: "all", label: "All Status" }, { value: "pending", label: "Pending" }, { value: "accepted", label: "Accepted" }, { value: "declined", label: "Declined" }, @@ -66,15 +66,15 @@ export function InvitationManagement({ error, }: InvitationManagementProps) { const [searchTerm, setSearchTerm] = useState(""); - const [roleFilter, setRoleFilter] = useState(""); - const [statusFilter, setStatusFilter] = useState(""); + const [roleFilter, setRoleFilter] = useState("all"); + const [statusFilter, setStatusFilter] = useState("all"); const [showCreateForm, setShowCreateForm] = useState(false); const filteredInvitations = invitations.filter((invitation) => { const matchesSearch = invitation.email.toLowerCase().includes(searchTerm.toLowerCase()) || `${invitation.invitedBy.firstName} ${invitation.invitedBy.lastName}`.toLowerCase().includes(searchTerm.toLowerCase()); - const matchesRole = !roleFilter || invitation.role === roleFilter; - const matchesStatus = !statusFilter || invitation.status === statusFilter; + const matchesRole = roleFilter === "all" || invitation.role === roleFilter; + const matchesStatus = statusFilter === "all" || invitation.status === statusFilter; return matchesSearch && matchesRole && matchesStatus; }); From 2f90d50d34d92bcef79926043b32b157fd3dc182 Mon Sep 17 00:00:00 2001 From: Lewechi Date: Thu, 30 Apr 2026 20:22:57 +0100 Subject: [PATCH 13/21] fix: fixes --- package.json | 2 +- scripts/migrate.ts | 37 ++++++++++++++++++++++++++++++++++ src/app/(dashboard)/layout.tsx | 2 ++ 3 files changed, 40 insertions(+), 1 deletion(-) create mode 100644 scripts/migrate.ts diff --git a/package.json b/package.json index 095985f8..8c321b52 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "start": "next start", "lint": "eslint", "db:generate": "drizzle-kit generate", - "db:migrate": "drizzle-kit migrate", + "db:migrate": "tsx scripts/migrate.ts", "db:push": "drizzle-kit push", "db:studio": "drizzle-kit studio", "db:reset:local": "dropdb --if-exists vestroll_dev && createdb vestroll_dev && npm run db:migrate && npm run db:seed:local", diff --git a/scripts/migrate.ts b/scripts/migrate.ts new file mode 100644 index 00000000..dc5280c4 --- /dev/null +++ b/scripts/migrate.ts @@ -0,0 +1,37 @@ +import { migrate } from "drizzle-orm/node-postgres/migrator"; +import { drizzle } from "drizzle-orm/node-postgres"; +import { Pool } from "pg"; +import * as path from "path"; +import * as fs from "fs"; + +async function main() { + const databaseUrl = process.env.DATABASE_URL; + + if (!databaseUrl) { + console.error("DATABASE_URL is not set"); + process.exit(1); + } + + const pool = new Pool({ + connectionString: databaseUrl, + ssl: process.env.NODE_ENV === "production" ? { rejectUnauthorized: false } : false, + }); + + const db = drizzle(pool); + + console.log("Running migrations..."); + + try { + await migrate(db, { + migrationsFolder: path.resolve(process.cwd(), "drizzle/migrations"), + }); + console.log("Migrations completed successfully"); + } catch (error) { + console.error("Migration failed:", error); + process.exit(1); + } finally { + await pool.end(); + } +} + +main(); diff --git a/src/app/(dashboard)/layout.tsx b/src/app/(dashboard)/layout.tsx index 21e090a7..75e95b88 100644 --- a/src/app/(dashboard)/layout.tsx +++ b/src/app/(dashboard)/layout.tsx @@ -1,3 +1,5 @@ +export const dynamic = "force-dynamic"; + import AppShell from "@/components/layout/app-shell"; import { ThemeProvider } from "@/components/providers/theme-provider"; import PageTransition from "@/components/shared/animations/PageTransition"; From 89227c864642bbc9788096f6e30233542a08bd36 Mon Sep 17 00:00:00 2001 From: Lewechi Date: Thu, 30 Apr 2026 20:28:29 +0100 Subject: [PATCH 14/21] fix: fixes --- src/app/(dashboard)/finance/page.tsx | 1 + src/app/(dashboard)/invoices/page.tsx | 21 +++-- src/app/(dashboard)/loading.tsx | 4 +- src/app/(dashboard)/payroll/page.tsx | 6 +- .../features/contracts/BasePage.tsx | 2 +- .../features/contracts/ContractHistory.tsx | 10 ++- .../features/contracts/ui/ContractMetrics.tsx | 61 +++++++------ .../invitations/InvitationManagement.tsx | 9 +- src/components/shared/table/Table.tsx | 36 +++++--- src/components/ui/skeleton.tsx | 85 +++++++++++++++++++ 10 files changed, 181 insertions(+), 54 deletions(-) create mode 100644 src/components/ui/skeleton.tsx diff --git a/src/app/(dashboard)/finance/page.tsx b/src/app/(dashboard)/finance/page.tsx index 74fdd894..5424559a 100644 --- a/src/app/(dashboard)/finance/page.tsx +++ b/src/app/(dashboard)/finance/page.tsx @@ -376,6 +376,7 @@ export default function FinancePage() { onSelectAll={handleSelectAll} renderCell={renderTransactionCell} renderMobileCell={renderMobileCell} + isLoading={isLoadingTransactions} showPagination={true} itemsPerPage={itemsPerPage} currentPage={currentPage} diff --git a/src/app/(dashboard)/invoices/page.tsx b/src/app/(dashboard)/invoices/page.tsx index d978f0f7..a549d32e 100644 --- a/src/app/(dashboard)/invoices/page.tsx +++ b/src/app/(dashboard)/invoices/page.tsx @@ -4,6 +4,7 @@ import React, { useState, useEffect } from "react"; import { motion, AnimatePresence } from "framer-motion"; import Table from "@/components/shared/table/Table"; import { TableColumn } from "@/components/shared/table/TableHeader"; +import { CardSkeleton } from "@/components/ui/skeleton"; import { invoiceMetricsData } from "@/constants"; import { useRouter } from "next/navigation"; import { RoutePaths } from "@/routes/routesPath"; @@ -187,9 +188,16 @@ const Invoices: React.FC = () => { animate="visible" className="flex flex-col flex-1 w-full h-full px-4 py-4 " > - {!loading && invoices.length > 0 && ( -
- {invoiceMetricsData.map((metric) => ( +
+ {loading ? ( + Array.from({ length: 4 }).map((_, i) => ( +
+ +
+ )) + ) : ( + invoices.length > 0 && + invoiceMetricsData.map((metric) => ( {
- ))} -
- )} + )) + )} +
{ onSelectAll={handleSelectAll} onRowClick={handleRowClick} renderCell={renderInvoiceCell} + isLoading={loading} emptyTitle={search ? "No invoices found" : "No invoices yet"} emptyDescription={ search diff --git a/src/app/(dashboard)/loading.tsx b/src/app/(dashboard)/loading.tsx index b7a30ab4..0ac0149d 100644 --- a/src/app/(dashboard)/loading.tsx +++ b/src/app/(dashboard)/loading.tsx @@ -1,5 +1,5 @@ -import LoadingSpinner from "@/components/ui/LoadingSpinner"; +import { DashboardSkeleton } from "@/components/ui/skeleton"; export default function Loading() { - return ; + return ; } diff --git a/src/app/(dashboard)/payroll/page.tsx b/src/app/(dashboard)/payroll/page.tsx index 21bf1290..a91f91c1 100644 --- a/src/app/(dashboard)/payroll/page.tsx +++ b/src/app/(dashboard)/payroll/page.tsx @@ -6,6 +6,7 @@ import { Filter, ChevronDown, ShareIcon, CheckCircle, XCircle, Loader2 } from "l import Image from "next/image"; import searchIcon from "@/../public/images/search-payroll.png"; import { FinanceService, PayrollItem } from "@/lib/api/finance"; +import { TableSkeleton } from "@/components/ui/skeleton"; import { RequestError } from "@/lib/api-client"; import useModal from "@/hooks/useModal"; @@ -230,9 +231,8 @@ export default function PayrollPage() { {/* Payroll Table */}
{isLoading ? ( -
- -

Loading payroll data...

+
+
) : fetchError ? (
diff --git a/src/components/features/contracts/BasePage.tsx b/src/components/features/contracts/BasePage.tsx index 7b036a72..1452c3f5 100644 --- a/src/components/features/contracts/BasePage.tsx +++ b/src/components/features/contracts/BasePage.tsx @@ -21,7 +21,7 @@ export default function BasePage() { return (
- {!loading && (contracts.length > 0 ? : )} +
diff --git a/src/components/features/contracts/ContractHistory.tsx b/src/components/features/contracts/ContractHistory.tsx index 615c0e60..acbb2276 100644 --- a/src/components/features/contracts/ContractHistory.tsx +++ b/src/components/features/contracts/ContractHistory.tsx @@ -9,6 +9,7 @@ import EmptyState from "@/components/ui/EmptyState"; import { cn } from "@/utils/classNames"; import FilterModal, { FilterSelection } from "./ui/FilterModal"; import Link from "next/link"; +import { CardSkeleton } from "@/components/ui/skeleton"; import { formatDateRange } from "@/utils/date"; interface ContractHistoryProps { @@ -213,7 +214,14 @@ function ContractHistory({ contracts, loading = false }: ContractHistoryProps) { )} {loading ? ( -
Loading contracts…
+
+ + + + + + +
) : filteredContracts.length > 0 ? ( - {contractMetricsData.map((metric) => ( - -
- -

- {metric.title} -

-

This year

-
-
-
- -

- {metric.value} -

-

- {metric.subValue} + {loading ? ( + Array.from({ length: 4 }).map((_, i) => ( +

+ +
+ )) + ) : ( + contractMetricsData.map((metric) => ( + +
+ +

+ {metric.title}

+

This year

- {metric.icon} +
+
+ +

+ {metric.value} +

+

+ {metric.subValue} +

+
+ {metric.icon} +
-
- - ))} + + )) + )} ); } diff --git a/src/components/features/team-management/invitations/InvitationManagement.tsx b/src/components/features/team-management/invitations/InvitationManagement.tsx index 9b0dbd4c..7d2530ee 100644 --- a/src/components/features/team-management/invitations/InvitationManagement.tsx +++ b/src/components/features/team-management/invitations/InvitationManagement.tsx @@ -7,6 +7,7 @@ import { Badge } from "@/components/ui/badge"; import { Search, Filter, UserPlus, RefreshCw } from "lucide-react"; import { InvitationCard } from "./InvitationCard"; import { CreateInvitationForm } from "./CreateInvitationForm"; +import { CardSkeleton } from "@/components/ui/skeleton"; interface Invitation { id: string; @@ -208,7 +209,13 @@ export function InvitationManagement({ {/* Invitations List */}
- {filteredInvitations.length === 0 ? ( + {isLoading && invitations.length === 0 ? ( + <> + + + + + ) : filteredInvitations.length === 0 ? ( diff --git a/src/components/shared/table/Table.tsx b/src/components/shared/table/Table.tsx index fc97f95a..5ecd5473 100644 --- a/src/components/shared/table/Table.tsx +++ b/src/components/shared/table/Table.tsx @@ -5,6 +5,7 @@ import TableContent from "./TableContent"; import TableFilterHeader from "./TableFilterHeader"; import TableHeader, { TableColumn } from "./TableHeader"; import Pagination from "@/components/ui/Pagination"; +import { TableSkeleton } from "@/components/ui/skeleton"; interface TableProps { data: T[]; @@ -50,6 +51,8 @@ interface TableProps { // Filter header props SearchIcon?: React.ComponentType; FilterIcon?: React.ComponentType; + + isLoading?: boolean; } const Table = >({ @@ -84,6 +87,7 @@ const Table = >({ getItemId, SearchIcon, FilterIcon, + isLoading = false, }: TableProps) => { const [currentPage, setCurrentPage] = useState(1); const [itemsPerPage, setItemsPerPage] = useState(initialItemsPerPage); @@ -181,20 +185,24 @@ const Table = >({ onSelectAll={handleSelectAll} allSelected={showPagination ? allSelected : isAllDataSelected} /> - + {isLoading ? ( + + ) : ( + + )}
{showPagination && activeTotalPages > 1 && ( diff --git a/src/components/ui/skeleton.tsx b/src/components/ui/skeleton.tsx new file mode 100644 index 00000000..fb37aa97 --- /dev/null +++ b/src/components/ui/skeleton.tsx @@ -0,0 +1,85 @@ +import { cn } from "@/lib/utils"; + +function Skeleton({ + className, + ...props +}: React.HTMLAttributes) { + return ( +
+ ); +} + +export { Skeleton }; + +export function TableSkeleton({ rows = 5, columns = 4 }: { rows?: number; columns?: number }) { + return ( +
+
+ + +
+
+
+ {Array.from({ length: columns }).map((_, i) => ( + + ))} +
+ {Array.from({ length: rows }).map((_, i) => ( +
+ {Array.from({ length: columns }).map((_, j) => ( + + ))} +
+ ))} +
+
+ ); +} + +export function CardSkeleton() { + return ( +
+
+ +
+ + +
+
+
+ + + +
+
+ ); +} + +export function DashboardSkeleton() { + return ( +
+
+
+ + +
+ +
+ +
+ + + +
+ +
+ + +
+
+ ); +} From 6652277dd82922e776b0dc56f264471d511546d1 Mon Sep 17 00:00:00 2001 From: Aditya Kadam <2025.akadam@isu.ac.in> Date: Fri, 1 May 2026 19:25:28 +0530 Subject: [PATCH 15/21] feat: extract api services and refactor components --- .../employees/[id]/accounts/page.tsx | 25 +--- src/app/(dashboard)/finance/page.tsx | 31 ++--- src/app/(dashboard)/settings/page.tsx | 50 ++------ src/app/(dashboard)/team/invitations/page.tsx | 64 +--------- src/app/invite/accept/page.tsx | 47 ++----- .../features/auth/RegistrationWizard.tsx | 18 +-- .../features/auth/business-details.tsx | 42 +++---- .../auth/register-steps/Step3VerifyEmail.tsx | 14 +-- .../dashboard/home/checklist/CompleteKYB.tsx | 49 +++----- .../employee-management/AccountForm.tsx | 17 +-- .../employee-management/AccountManagement.tsx | 117 ++++------------- .../features/finance/AddFundsModal.tsx | 45 +------ .../steps/Step3PaymentDetails.tsx | 15 +-- .../timeOff/CreateTimeOffForm.tsx | 35 +----- src/lib/api-client.ts | 13 ++ src/lib/api/auth.ts | 4 + src/lib/api/employees.ts | 118 +++++++++++++++++- src/lib/api/finance.ts | 65 ++++++++++ src/lib/api/kyb.ts | 29 +++++ src/lib/api/organization.ts | 28 +++++ src/lib/api/team.ts | 34 +++++ 21 files changed, 404 insertions(+), 456 deletions(-) diff --git a/src/app/(dashboard)/employees/[id]/accounts/page.tsx b/src/app/(dashboard)/employees/[id]/accounts/page.tsx index 53bfd537..9c80a306 100644 --- a/src/app/(dashboard)/employees/[id]/accounts/page.tsx +++ b/src/app/(dashboard)/employees/[id]/accounts/page.tsx @@ -6,39 +6,24 @@ import { AccountManagement } from "@/components/features/employee-management/Acc import { Alert, AlertDescription } from "@/components/ui/alert"; import { Loader2, ArrowLeft } from "lucide-react"; import { Button } from "@/components/ui/button"; - -interface Employee { - id: string; - firstName: string; - lastName: string; - email: string; - role: string; - department?: string; -} +import { EmployeesService, type EmployeeDetail } from "@/lib/api/employees"; export default function EmployeeAccountsPage() { const params = useParams(); const router = useRouter(); const employeeId = params.id as string; - const [employee, setEmployee] = useState(null); + const [employee, setEmployee] = useState(null); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); const fetchEmployee = async () => { setIsLoading(true); setError(null); - + try { - const response = await fetch(`/api/employees/${employeeId}`); - - if (!response.ok) { - const errorData = await response.json(); - throw new Error(errorData.message || "Failed to fetch employee"); - } - - const data = await response.json(); - setEmployee(data.data); + const data = await EmployeesService.getEmployee(employeeId); + setEmployee(data); } catch (err) { setError(err instanceof Error ? err.message : "Failed to load employee"); } finally { diff --git a/src/app/(dashboard)/finance/page.tsx b/src/app/(dashboard)/finance/page.tsx index 5424559a..495ece98 100644 --- a/src/app/(dashboard)/finance/page.tsx +++ b/src/app/(dashboard)/finance/page.tsx @@ -11,6 +11,7 @@ import { TableColumn } from "@/components/shared/table/TableHeader"; import type { Transaction } from "@/types/finance.types"; import { UsdtIcon } from "@/../public/svg"; import { formatNairaFromKobo } from "@/lib/format-naira"; +import { FinanceService } from "@/lib/api/finance"; const transactionColumns: TableColumn[] = [ { key: "id", header: "Transaction ID" }, @@ -121,12 +122,8 @@ export default function FinancePage() { useEffect(() => { const fetchBalance = async () => { try { - const res = await fetch("/api/v1/finance/balance"); - if (res.ok) { - const json = await res.json(); - const kobo = json.data?.balance ?? json.balance ?? 0; - setNgnBalance(formatNairaFromKobo(kobo)); - } + const { balance } = await FinanceService.getBalance(); + setNgnBalance(formatNairaFromKobo(balance ?? 0)); } catch (error) { console.error("Failed to fetch NGN balance:", error); } @@ -143,25 +140,11 @@ export default function FinancePage() { setTransactionsError(null); try { - const params = new URLSearchParams({ - page: String(currentPage), - limit: String(itemsPerPage), - }); - - const response = await fetch( - `/api/v1/finance/transactions?${params.toString()}`, - { signal: controller.signal } - ); - - const payload = (await response.json()) as TransactionsResponse; - - if (!response.ok || !payload.success || !payload.data) { - throw new Error(payload.message || "Unable to load transactions"); - } + const payload = await FinanceService.getTransactions(currentPage, itemsPerPage); - setTransactions(payload.data.data.map(normalizeTransaction)); - setTotalItems(payload.data.meta.total); - setTotalPages(Math.max(payload.data.meta.totalPages, 1)); + setTransactions(payload.data.map(normalizeTransaction as any)); + setTotalItems(payload.meta.total); + setTotalPages(Math.max(payload.meta.totalPages, 1)); } catch (error) { if (error instanceof DOMException && error.name === "AbortError") { return; diff --git a/src/app/(dashboard)/settings/page.tsx b/src/app/(dashboard)/settings/page.tsx index 1899ecaf..e4302eee 100644 --- a/src/app/(dashboard)/settings/page.tsx +++ b/src/app/(dashboard)/settings/page.tsx @@ -171,27 +171,15 @@ export default function Page() { setLogoSrc(localUrl); setIsLogoModalOpen(false); - // Read Bearer token from localStorage (set by the auth layer on login) - const accessToken = - typeof window !== "undefined" - ? window.localStorage.getItem("access_token") - : null; - const authHeaders: HeadersInit = accessToken - ? { Authorization: `Bearer ${accessToken}` } - : {}; - setIsUploadingLogo(true); try { - // Step 1 — get presigned S3 upload URL - const urlRes = await fetch( - `/api/v1/organizations/logo-upload-url?filename=${encodeURIComponent(file.name)}&contentType=${encodeURIComponent(file.type)}`, - { headers: authHeaders }, + // Step 1 — get presigned S3 upload URL via the service + const { signedUrl, key } = await OrganizationApi.getLogoUploadUrl( + file.name, + file.type ); - if (!urlRes.ok) throw new Error("Failed to get upload URL"); - const { data: urlData } = await urlRes.json(); - const { signedUrl, key } = urlData; - // Step 2 — upload blob directly to S3 + // Step 2 — upload blob directly to S3 (external URL, raw fetch is intentional) const s3Res = await fetch(signedUrl, { method: "PUT", headers: { "Content-Type": file.type }, @@ -199,32 +187,12 @@ export default function Page() { }); if (!s3Res.ok) throw new Error("Failed to upload logo to storage"); - // Step 3 — save the S3 key to the database - const patchRes = await fetch("/api/v1/organizations/logo", { - method: "PATCH", - headers: { "Content-Type": "application/json", ...authHeaders }, - body: JSON.stringify({ key }), - }); + // Step 3 — save the S3 key to the database via the service + const { logoUrl } = await OrganizationApi.updateLogo(key); - if (!patchRes.ok) { - // Parse server error message and surface it to the user - let errorMessage = "Failed to save logo"; - try { - const errorBody = await patchRes.json(); - if (typeof errorBody?.message === "string") errorMessage = errorBody.message; - else if (typeof errorBody?.error === "string") errorMessage = errorBody.error; - } catch { - // ignore JSON parse error, fall back to generic message - } - throw new Error(errorMessage); - } - - const { data: patchData } = await patchRes.json(); // Replace optimistic blob URL with the permanent CDN URL - if (patchData?.logoUrl) { - revokeBlobUrl(); // revoke the blob now that we have the real URL - setLogoSrc(patchData.logoUrl); - } + revokeBlobUrl(); + setLogoSrc(logoUrl); } catch (err) { console.error("[Logo upload error]", err); setUploadError( diff --git a/src/app/(dashboard)/team/invitations/page.tsx b/src/app/(dashboard)/team/invitations/page.tsx index 663dd234..2fd62eab 100644 --- a/src/app/(dashboard)/team/invitations/page.tsx +++ b/src/app/(dashboard)/team/invitations/page.tsx @@ -2,7 +2,7 @@ import React, { useState, useEffect } from "react"; import { InvitationManagement } from "@/components/features/team-management/invitations/InvitationManagement"; -import { createInvitationSchema } from "@/server/validations/invitation.schema"; +import { TeamService } from "@/lib/api/team"; interface Invitation { id: string; @@ -26,11 +26,6 @@ interface Invitation { }; } -interface InvitationsResponse { - invitations: Invitation[]; - total: number; -} - export default function InvitationsPage() { const [invitations, setInvitations] = useState([]); const [isLoading, setIsLoading] = useState(true); @@ -41,15 +36,8 @@ export default function InvitationsPage() { setError(null); try { - const response = await fetch("/api/v1/invitations"); - - if (!response.ok) { - const errorData = await response.json(); - throw new Error(errorData.message || "Failed to fetch invitations"); - } - - const data: InvitationsResponse = await response.json(); - setInvitations(data.invitations); + const data = await TeamService.listInvitations() as any; + setInvitations(data?.invitations ?? data ?? []); } catch (err) { setError(err instanceof Error ? err.message : "Failed to load invitations"); } finally { @@ -61,20 +49,7 @@ export default function InvitationsPage() { setError(null); try { - const response = await fetch("/api/v1/invitations", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify(data), - }); - - if (!response.ok) { - const errorData = await response.json(); - throw new Error(errorData.message || "Failed to create invitation"); - } - - // Refresh invitations list + await TeamService.createInvitation(data); await fetchInvitations(); } catch (err) { setError(err instanceof Error ? err.message : "Failed to create invitation"); @@ -86,20 +61,7 @@ export default function InvitationsPage() { setError(null); try { - const response = await fetch("/api/v1/invitations/resend", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ invitationId }), - }); - - if (!response.ok) { - const errorData = await response.json(); - throw new Error(errorData.message || "Failed to resend invitation"); - } - - // Refresh invitations list + await TeamService.resendInvitation({ invitationId } as any); await fetchInvitations(); } catch (err) { setError(err instanceof Error ? err.message : "Failed to resend invitation"); @@ -111,20 +73,7 @@ export default function InvitationsPage() { setError(null); try { - const response = await fetch("/api/v1/invitations/delete", { - method: "DELETE", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ invitationId }), - }); - - if (!response.ok) { - const errorData = await response.json(); - throw new Error(errorData.message || "Failed to delete invitation"); - } - - // Refresh invitations list + await TeamService.deleteInvitation(invitationId); await fetchInvitations(); } catch (err) { setError(err instanceof Error ? err.message : "Failed to delete invitation"); @@ -150,4 +99,3 @@ export default function InvitationsPage() {
); } - diff --git a/src/app/invite/accept/page.tsx b/src/app/invite/accept/page.tsx index d5af1e56..00a2c84e 100644 --- a/src/app/invite/accept/page.tsx +++ b/src/app/invite/accept/page.tsx @@ -11,6 +11,7 @@ import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Alert, AlertDescription } from "@/components/ui/alert"; import { Loader2, CheckCircle, XCircle, Mail, Building, User, Lock } from "lucide-react"; +import { TeamService } from "@/lib/api/team"; const acceptInvitationFormSchema = z.object({ token: z.string().min(1, "Token is required"), @@ -93,15 +94,8 @@ function AcceptInvitationPageContent() { const fetchInvitationDetails = async (invitationToken: string) => { try { - const response = await fetch(`/api/v1/invitations/validate?token=${invitationToken}`); - - if (!response.ok) { - const errorData = await response.json(); - throw new Error(errorData.message || "Invalid invitation"); - } - - const data = await response.json(); - setInvitationDetails(data.invitation); + const data = await TeamService.validateInvitation(invitationToken) as any; + setInvitationDetails(data?.invitation ?? data); } catch (err) { setError(err instanceof Error ? err.message : "Failed to load invitation details"); } finally { @@ -114,24 +108,13 @@ function AcceptInvitationPageContent() { setError(null); try { - const response = await fetch("/api/v1/invitations/accept", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - token: data.token, - firstName: data.firstName, - lastName: data.lastName, - password: data.password, - }), + await TeamService.acceptInvitation({ + token: data.token, + firstName: data.firstName, + lastName: data.lastName, + password: data.password, }); - if (!response.ok) { - const errorData = await response.json(); - throw new Error(errorData.message || "Failed to accept invitation"); - } - setSuccess(true); setTimeout(() => { router.push("/login?message=invitation-accepted"); @@ -148,19 +131,7 @@ function AcceptInvitationPageContent() { setIsLoading(true); try { - const response = await fetch("/api/v1/invitations/decline", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ token }), - }); - - if (!response.ok) { - const errorData = await response.json(); - throw new Error(errorData.message || "Failed to decline invitation"); - } - + await TeamService.declineInvitation(token); router.push("/login?message=invitation-declined"); } catch (err) { setError(err instanceof Error ? err.message : "Failed to decline invitation"); diff --git a/src/components/features/auth/RegistrationWizard.tsx b/src/components/features/auth/RegistrationWizard.tsx index 8b86df8e..9ab22456 100644 --- a/src/components/features/auth/RegistrationWizard.tsx +++ b/src/components/features/auth/RegistrationWizard.tsx @@ -12,6 +12,7 @@ import { useToast } from "@/hooks/useToast"; import { ToastContainer } from "@/components/ui/toast"; import { useRouter } from "next/navigation"; import ModalWelcomeOnboard from "@/components/shared/modal-welcome-onboard"; +import { AuthService } from "@/lib/api/auth"; export type RegistrationFormData = { firstName: string; @@ -60,23 +61,16 @@ export default function RegistrationWizard() { const handleStep4Submit = async () => { setIsLoading(true); try { - const response = await fetch("/api/v1/auth/register", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(formData), + await AuthService.register({ + firstName: formData.firstName, + lastName: formData.lastName, + businessEmail: formData.businessEmail, }); - const result = await response.json(); - - if (!response.ok) { - showError(result.message || "Registration failed"); - return; - } - // Success - Registration initiated, OTP sent nextStep(); } catch (err) { - showError("An unexpected error occurred"); + showError(err instanceof Error ? err.message : "Registration failed"); } finally { setIsLoading(false); } diff --git a/src/components/features/auth/business-details.tsx b/src/components/features/auth/business-details.tsx index 1aff7cda..e618ddc8 100644 --- a/src/components/features/auth/business-details.tsx +++ b/src/components/features/auth/business-details.tsx @@ -4,6 +4,7 @@ import { ChevronDown } from "lucide-react"; import ModalWelcomeOnboard from "@/components/shared/modal-welcome-onboard"; import Stepper from "@/components/features/auth/Stepper"; import { AuthService } from "@/lib/api/auth"; +import { KybService } from "@/lib/api/kyb"; import FileUpload from "@/components/ui/file-upload"; interface FormData { @@ -233,22 +234,13 @@ const BusinessRegistrationForm: React.FC = () => { const uploadFile = async (file: File, field: keyof FormData) => { try { - // 1. Get signed URL - const response = await fetch("/api/v1/kyb/upload", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - filename: file.name, - contentType: file.type, - }), - }); - - const { data, success, message } = await response.json(); - if (!success) throw new Error(message); - - const { signedUrl, key } = data; + // Step 1 — get presigned S3 upload URL via the service + const { signedUrl, key } = await KybService.getUploadUrl( + file.name, + file.type + ); - // 2. Upload to S3 + // Step 2 — upload the raw file directly to S3 (external URL, raw fetch is correct here) const uploadRes = await fetch(signedUrl, { method: "PUT", body: file, @@ -257,7 +249,7 @@ const BusinessRegistrationForm: React.FC = () => { if (!uploadRes.ok) throw new Error("Failed to upload to S3"); - // 3. Update state + // Step 3 — update state setFormData((prev) => ({ ...prev, [field]: key })); if (errors[field]) { setErrors((prev) => ({ ...prev, [field]: undefined })); @@ -297,17 +289,13 @@ const BusinessRegistrationForm: React.FC = () => { // Submit complete registration data using AuthService await AuthService.completeRegistration(completeData); - // Submit KYB data - await fetch("/api/v1/kyb/submit", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - registrationType: formData.registrationType, - registrationNo: formData.registrationNo, - incorporationCertificatePath: formData.incorporationCertificatePath, - memorandumArticlePath: formData.memorandumArticlePath, - formC02C07Path: formData.formC02C07Path || undefined, - }), + // Submit KYB data via KybService + await KybService.submit({ + registrationType: formData.registrationType, + registrationNo: formData.registrationNo, + incorporationCertificatePath: formData.incorporationCertificatePath, + memorandumArticlePath: formData.memorandumArticlePath, + formC02C07Path: formData.formC02C07Path || undefined, }); console.log("Registration completed successfully:", completeData); diff --git a/src/components/features/auth/register-steps/Step3VerifyEmail.tsx b/src/components/features/auth/register-steps/Step3VerifyEmail.tsx index b5b6f6dd..123080f9 100644 --- a/src/components/features/auth/register-steps/Step3VerifyEmail.tsx +++ b/src/components/features/auth/register-steps/Step3VerifyEmail.tsx @@ -2,6 +2,7 @@ import React from "react"; import EmailVerification from "@/components/shared/emailVerificationModal"; +import { AuthService } from "@/lib/api/auth"; interface Step3Props { email: string; @@ -16,18 +17,7 @@ export default function Step3VerifyEmail({ }: Step3Props) { const handleVerify = async (otp: string) => { try { - const response = await fetch("/api/v1/auth/verify-email", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ email, otp }), - }); - - const result = await response.json(); - - if (!response.ok) { - return false; - } - + await AuthService.verifyEmail({ email, otp }); onNext(); return true; } catch (error) { diff --git a/src/components/features/dashboard/home/checklist/CompleteKYB.tsx b/src/components/features/dashboard/home/checklist/CompleteKYB.tsx index 2b36eaca..dbac269e 100644 --- a/src/components/features/dashboard/home/checklist/CompleteKYB.tsx +++ b/src/components/features/dashboard/home/checklist/CompleteKYB.tsx @@ -6,6 +6,7 @@ import { Button } from "@/components/ui/button"; import InputField from "@/components/ui/input-field"; import Dropdown from "@/components/ui/dropdown"; import FileUpload from "@/components/ui/file-upload"; +import { KybService } from "@/lib/api/kyb"; interface KybFormData { businessRegistrationType: string; @@ -48,20 +49,13 @@ export default function CompleteKYBPage() { const uploadFile = async (file: File, field: keyof KybFormData) => { try { - const response = await fetch("/api/v1/kyb/upload", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - filename: file.name, - contentType: file.type, - }), - }); - - const { data, success, message } = await response.json(); - if (!success) throw new Error(message); - - const { signedUrl, key } = data; + // Step 1 — get presigned S3 upload URL via the service + const { signedUrl, key } = await KybService.getUploadUrl( + file.name, + file.type + ); + // Step 2 — upload the raw file directly to S3 (external URL, raw fetch is correct here) const uploadRes = await fetch(signedUrl, { method: "PUT", body: file, @@ -83,28 +77,19 @@ export default function CompleteKYBPage() { setError(null); try { - const response = await fetch("/api/v1/kyb/submit", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - registrationType: formData.businessRegistrationType, - registrationNo: formData.businessRegistrationNo, - incorporationCertificatePath: formData.incorporationCertificatePath, - memorandumArticlePath: formData.memorandumArticlePath, - formC02C07Path: formData.formC02C07Path || undefined, - }), + await KybService.submit({ + registrationType: formData.businessRegistrationType, + registrationNo: formData.businessRegistrationNo, + incorporationCertificatePath: formData.incorporationCertificatePath, + memorandumArticlePath: formData.memorandumArticlePath, + formC02C07Path: formData.formC02C07Path || undefined, }); - const result = await response.json(); - - if (!result.success) { - setError(result.message); - return; - } - // TODO: Handle success (show confirmation, update checklist state) - } catch { - setError("An unexpected error occurred. Please try again."); + } catch (err) { + setError( + err instanceof Error ? err.message : "An unexpected error occurred. Please try again." + ); } finally { setIsSubmitting(false); } diff --git a/src/components/features/employee-management/AccountForm.tsx b/src/components/features/employee-management/AccountForm.tsx index d1bd8f4b..ada20ea7 100644 --- a/src/components/features/employee-management/AccountForm.tsx +++ b/src/components/features/employee-management/AccountForm.tsx @@ -12,6 +12,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@ import { Alert, AlertDescription } from "@/components/ui/alert"; import { Badge } from "@/components/ui/badge"; import { Loader2, CheckCircle, XCircle, AlertCircle, Building, CreditCard, Globe } from "lucide-react"; +import { EmployeesService } from "@/lib/api/employees"; const accountFormSchema = z.object({ bankName: z.string().min(2, "Bank name is required").max(255), @@ -142,20 +143,8 @@ export function AccountForm({ employeeId, initialData, onSubmit, onCancel }: Acc try { const formData = watch(); - const response = await fetch("/api/v1/accounts/validate", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify(formData), - }); - - const data = await response.json(); - if (!response.ok) { - throw new Error(data.message || "Validation failed"); - } - - setValidationResult(data.data); + const result = await EmployeesService.validateAccount(formData as any); + setValidationResult(result); } catch (err) { setError(err instanceof Error ? err.message : "Account validation failed"); setValidationResult(null); diff --git a/src/components/features/employee-management/AccountManagement.tsx b/src/components/features/employee-management/AccountManagement.tsx index 2c6f470b..8ba16abc 100644 --- a/src/components/features/employee-management/AccountManagement.tsx +++ b/src/components/features/employee-management/AccountManagement.tsx @@ -17,26 +17,7 @@ import { AlertTriangle, CheckCircle } from "lucide-react"; - -interface AccountDetails { - id: string; - bankName: string; - accountNumber: string; - routingNumber?: string; - sortCode?: string; - iban?: string; - swiftCode?: string; - accountType: string; - accountHolderName: string; - isAccountVerified: boolean; - accountVerifiedAt?: string; - bankAddress?: string; - bankCity?: string; - bankCountry?: string; - employeeId: string; - employeeName: string; -} - +import { EmployeesService, type AccountDetails } from "@/lib/api/employees"; interface AccountManagementProps { employeeId: string; employeeName: string; @@ -53,28 +34,16 @@ export function AccountManagement({ employeeId, employeeName }: AccountManagemen const fetchAccounts = async () => { setIsLoading(true); setError(null); - + try { - const response = await fetch(`/api/v1/accounts?employeeId=${employeeId}`); - - if (!response.ok) { - const errorData = await response.json(); - throw new Error(errorData.message || "Failed to fetch accounts"); - } - - const data = await response.json(); - - if (data.data) { - // Add employee info to each account - const accountsWithEmployee = data.data.map((account: any) => ({ - ...account, - employeeId, - employeeName, - })); - setAccounts(accountsWithEmployee); - } else { - setAccounts([]); - } + const data = await EmployeesService.getAccounts(employeeId); + // Attach employee info that isn't returned by the API + const accountsWithEmployee = data.map((account) => ({ + ...account, + employeeId, + employeeName, + })); + setAccounts(accountsWithEmployee); } catch (err) { setError(err instanceof Error ? err.message : "Failed to load accounts"); } finally { @@ -84,21 +53,9 @@ export function AccountManagement({ employeeId, employeeName }: AccountManagemen const handleCreateAccount = async (data: any) => { setError(null); - + try { - const response = await fetch("/api/v1/accounts", { - method: "PUT", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify(data), - }); - - if (!response.ok) { - const errorData = await response.json(); - throw new Error(errorData.message || "Failed to create account"); - } - + await EmployeesService.upsertAccount(data); await fetchAccounts(); setShowForm(false); } catch (err) { @@ -109,21 +66,9 @@ export function AccountManagement({ employeeId, employeeName }: AccountManagemen const handleUpdateAccount = async (data: any) => { setError(null); - + try { - const response = await fetch("/api/v1/accounts", { - method: "PUT", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify(data), - }); - - if (!response.ok) { - const errorData = await response.json(); - throw new Error(errorData.message || "Failed to update account"); - } - + await EmployeesService.upsertAccount(data); await fetchAccounts(); setEditingAccount(null); setShowForm(false); @@ -135,28 +80,16 @@ export function AccountManagement({ employeeId, employeeName }: AccountManagemen const handleVerifyAccount = async (accountId: string) => { setError(null); - + try { const account = accounts.find(a => a.id === accountId); if (!account) throw new Error("Account not found"); - const response = await fetch("/api/v1/accounts/verify", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - employeeId, - accountNumber: account.accountNumber, - bankName: account.bankName, - }), + await EmployeesService.verifyAccount({ + employeeId, + accountNumber: account.accountNumber, + bankName: account.bankName, }); - - if (!response.ok) { - const errorData = await response.json(); - throw new Error(errorData.message || "Failed to verify account"); - } - await fetchAccounts(); } catch (err) { setError(err instanceof Error ? err.message : "Failed to verify account"); @@ -178,17 +111,9 @@ export function AccountManagement({ employeeId, employeeName }: AccountManagemen } setError(null); - + try { - const response = await fetch(`/api/v1/accounts/${accountId}`, { - method: "DELETE", - }); - - if (!response.ok) { - const errorData = await response.json(); - throw new Error(errorData.message || "Failed to delete account"); - } - + await EmployeesService.deleteAccount(accountId); await fetchAccounts(); } catch (err) { setError(err instanceof Error ? err.message : "Failed to delete account"); diff --git a/src/components/features/finance/AddFundsModal.tsx b/src/components/features/finance/AddFundsModal.tsx index e09af074..f67d540f 100644 --- a/src/components/features/finance/AddFundsModal.tsx +++ b/src/components/features/finance/AddFundsModal.tsx @@ -10,14 +10,7 @@ import { DialogTitle, } from "@/components/ui/dialog"; import { Button } from "@/components/ui/button"; - -type WalletFundingDetails = { - walletId: string | null; - organizationId: string | null; - virtualAccountNumber: string | null; - virtualBankName: string | null; - hasVirtualAccount: boolean; -}; +import { FinanceService, type WalletFundingDetails } from "@/lib/api/finance"; type AddFundsModalProps = { open: boolean; @@ -54,22 +47,8 @@ export function AddFundsModal({ open, onOpenChange }: AddFundsModalProps) { setError(null); try { - const response = await fetch("/api/v1/finance/wallet", { - method: "GET", - headers: { - "Content-Type": "application/json", - }, - }); - - const payload = await response.json(); - - if (!response.ok || !payload.success) { - throw new Error( - payload.message || "Unable to load your funding account details.", - ); - } - - setWallet(payload.data); + const data = await FinanceService.getWallet(); + setWallet(data); } catch (fetchError) { setError( fetchError instanceof Error @@ -86,22 +65,8 @@ export function AddFundsModal({ open, onOpenChange }: AddFundsModalProps) { setError(null); try { - const response = await fetch("/api/v1/finance/wallet", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - }); - - const payload = await response.json(); - - if (!response.ok || !payload.success) { - throw new Error( - payload.message || "Unable to refresh your virtual account.", - ); - } - - setWallet(payload.data); + const data = await FinanceService.refreshWallet(); + setWallet(data); } catch (refreshError) { setError( refreshError instanceof Error diff --git a/src/components/features/team-management/add-employee-wizard/steps/Step3PaymentDetails.tsx b/src/components/features/team-management/add-employee-wizard/steps/Step3PaymentDetails.tsx index 0ca41ef3..20e0f5ff 100644 --- a/src/components/features/team-management/add-employee-wizard/steps/Step3PaymentDetails.tsx +++ b/src/components/features/team-management/add-employee-wizard/steps/Step3PaymentDetails.tsx @@ -22,6 +22,7 @@ import { ContactRound, } from "lucide-react"; import { BankDetailsData } from "../types"; +import { EmployeesService } from "@/lib/api/employees"; // ─── Schema ────────────────────────────────────────────────────────────────── @@ -111,18 +112,14 @@ export function Step3PaymentDetails({ defaultValues, onNext, onBack }: Props) { setVerification({ status: "loading" }); try { - const res = await fetch("/api/v1/accounts/validate", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ bankName: selectedBank, accountNumber }), + const result = await EmployeesService.validateAccount({ + bankName: selectedBank, + accountNumber, }); - const json = await res.json(); - - if (!res.ok) throw new Error(json.message || "Verification failed"); const resolvedName: string = - json.data?.accountHolderName || - json.data?.accountName || + result.accountHolderName || + result.accountName || "Account Verified"; setValue("accountName", resolvedName); diff --git a/src/components/features/team-management/timeOff/CreateTimeOffForm.tsx b/src/components/features/team-management/timeOff/CreateTimeOffForm.tsx index 00c1965d..9c999a29 100644 --- a/src/components/features/team-management/timeOff/CreateTimeOffForm.tsx +++ b/src/components/features/team-management/timeOff/CreateTimeOffForm.tsx @@ -16,6 +16,7 @@ import { import { TimeOffFormData, Employee } from "@/types/teamManagement.types"; import { SelectEmployeeModal } from "./SelectEmployeeModal"; import { useToast } from "@/hooks/useToast"; +import { TeamService } from "@/lib/api/team"; const EmployeeSelector = ({ selectedEmployee, @@ -372,47 +373,17 @@ export const CreateTimeOffForm = ({ employees }: { employees: Employee[] }) => { setSubmitMessage(""); try { - const token = - typeof window !== "undefined" - ? localStorage.getItem("access_token") - : null; - // Map the UI's timeOffType to the API's leaveType enum - // Paid → vacation, Unpaid → other const leaveType = formData.timeOffType === "paid" ? "vacation" : "other"; - const payload: Record = { + await TeamService.submitTimeOff({ startDate: formData.startDate, endDate: formData.endDate, leaveType, reason: formData.reason, - }; - - // If an employee was explicitly selected (admin submitting on behalf) - if (formData.employee?.id) { - payload.employeeId = formData.employee.id; - } - - const res = await fetch("/api/v1/team/time-off", { - method: "POST", - headers: { - "Content-Type": "application/json", - ...(token ? { Authorization: `Bearer ${token}` } : {}), - }, - body: JSON.stringify(payload), + ...(formData.employee?.id ? { employeeId: String(formData.employee.id) } : {}), }); - const data = await res.json(); - - if (!res.ok) { - const msg = - data?.message || - (Array.isArray(data?.errors) - ? data.errors.map((e: { message: string }) => e.message).join(", ") - : "Failed to submit request. Please try again."); - throw new Error(msg); - } - setSubmitStatus("success"); setSubmitMessage("Your time-off request has been submitted and is pending approval."); success("Time off request created successfully!"); diff --git a/src/lib/api-client.ts b/src/lib/api-client.ts index ced7b4cd..d123ca70 100644 --- a/src/lib/api-client.ts +++ b/src/lib/api-client.ts @@ -118,4 +118,17 @@ export const apiClient = { }); return handleResponse(response); }, + + async patch(url: string, body?: any, options?: RequestInit): Promise { + const response = await fetch(url, { + ...options, + method: "PATCH", + headers: { + "Content-Type": "application/json", + ...options?.headers, + }, + body: body ? JSON.stringify(body) : undefined, + }); + return handleResponse(response); + }, }; diff --git a/src/lib/api/auth.ts b/src/lib/api/auth.ts index 74895377..794e7c3c 100644 --- a/src/lib/api/auth.ts +++ b/src/lib/api/auth.ts @@ -65,6 +65,10 @@ export class AuthService { static async logout() { return apiClient.post("/api/v1/auth/logout", {}); } + + static async verifyEmail(data: { email: string; otp: string }) { + return apiClient.post("/api/v1/auth/verify-email", data); + } } diff --git a/src/lib/api/employees.ts b/src/lib/api/employees.ts index 3f2ad0c8..14d204b9 100644 --- a/src/lib/api/employees.ts +++ b/src/lib/api/employees.ts @@ -1,6 +1,8 @@ -import { apiClient, RequestError } from "../api-client"; +import { apiClient } from "../api-client"; import { PaginatedResponse } from "@/types/pagination"; +// ─── Employee list ────────────────────────────────────────────────────────── + export interface EmployeeItem { id: string; name: string; @@ -11,6 +13,15 @@ export interface EmployeeItem { avatarUrl: string | null; } +export interface EmployeeDetail { + id: string; + firstName: string; + lastName: string; + email: string; + role: string; + department?: string; +} + export interface GetEmployeesParams { page?: number; limit?: number; @@ -30,6 +41,67 @@ export interface AddEmployeeResponse { invitedAt: string; } +// ─── Bank account ──────────────────────────────────────────────────────────── + +export interface AccountDetails { + id: string; + bankName: string; + accountNumber: string; + routingNumber?: string; + sortCode?: string; + iban?: string; + swiftCode?: string; + accountType: string; + accountHolderName: string; + isAccountVerified: boolean; + accountVerifiedAt?: string; + bankAddress?: string; + bankCity?: string; + bankCountry?: string; + employeeId: string; + employeeName?: string; +} + +export interface CreateAccountPayload { + employeeId: string; + bankName: string; + accountNumber: string; + routingNumber?: string; + sortCode?: string; + iban?: string; + swiftCode?: string; + accountType: string; + accountHolderName: string; + bankAddress?: string; + bankCity?: string; + bankCountry: string; +} + +export interface ValidateAccountPayload { + bankName: string; + accountNumber: string; + routingNumber?: string; + sortCode?: string; + iban?: string; + bankCountry?: string; +} + +export interface ValidateAccountResult { + isValid: boolean; + bankName?: string; + accountName?: string; + accountHolderName?: string; + error?: string; +} + +export interface VerifyAccountPayload { + employeeId: string; + accountNumber: string; + bankName: string; +} + +// ─── Exported functions (list / add) ──────────────────────────────────────── + export async function getEmployees( params: GetEmployeesParams = {} ): Promise> { @@ -51,3 +123,47 @@ export async function addEmployee( ): Promise { return apiClient.post("/api/v1/team/employees", payload); } + +// ─── EmployeesService (detail + accounts) ─────────────────────────────────── + +export class EmployeesService { + /** Fetch a single employee by ID. */ + static async getEmployee(employeeId: string): Promise { + return apiClient.get(`/api/v1/team/employees/${employeeId}`); + } + + /** Fetch all bank accounts belonging to an employee. */ + static async getAccounts(employeeId: string): Promise { + return apiClient.get( + `/api/v1/accounts?employeeId=${employeeId}` + ); + } + + /** Create or update a bank account (upsert via PUT). */ + static async upsertAccount(data: CreateAccountPayload): Promise { + return apiClient.put("/api/v1/accounts", data); + } + + /** Verify a bank account (mark as verified by the backend). */ + static async verifyAccount(payload: VerifyAccountPayload): Promise { + return apiClient.post("/api/v1/accounts/verify", payload); + } + + /** Delete a bank account by ID. */ + static async deleteAccount(accountId: string): Promise { + return apiClient.delete(`/api/v1/accounts/${accountId}`); + } + + /** + * Validate bank account details before saving. + * Used both in AccountForm and Step3PaymentDetails. + */ + static async validateAccount( + payload: ValidateAccountPayload + ): Promise { + return apiClient.post( + "/api/v1/accounts/validate", + payload + ); + } +} diff --git a/src/lib/api/finance.ts b/src/lib/api/finance.ts index 06c302ea..af35ed6d 100644 --- a/src/lib/api/finance.ts +++ b/src/lib/api/finance.ts @@ -2,6 +2,46 @@ import { apiClient } from "../api-client"; import { Contract } from "@/lib/data/contracts"; import { Invoice } from "@/lib/data/invoices"; +// ─── Wallet ────────────────────────────────────────────────────────────────── + +export interface WalletFundingDetails { + walletId: string | null; + organizationId: string | null; + virtualAccountNumber: string | null; + virtualBankName: string | null; + hasVirtualAccount: boolean; +} + +// ─── Balance ───────────────────────────────────────────────────────────────── + +export interface FiatBalanceResponse { + balance: number; // in kobo +} + +// ─── Transactions ───────────────────────────────────────────────────────────── + +export interface TransactionMeta { + page: number; + limit: number; + total: number; + totalPages: number; +} + +export interface TransactionRecord { + id: string; + type?: string; + description?: string; + amount: string; + asset?: string; + status: string; + timestamp: string; +} + +export interface TransactionsPage { + data: TransactionRecord[]; + meta: TransactionMeta; +} + export interface PayrollEmployee { id: string; firstName: string; @@ -77,4 +117,29 @@ export class FinanceService { static async initializeDeposit(request: DepositRequest): Promise { return apiClient.post("/api/v1/finance/fiat/deposit", request); } + + /** Fetch the organization's NGN virtual-account funding details. */ + static async getWallet(): Promise { + return apiClient.get("/api/v1/finance/wallet"); + } + + /** + * Request / refresh the organization's dedicated virtual account. + * Used by AddFundsModal when no virtual account exists yet. + */ + static async refreshWallet(): Promise { + return apiClient.post("/api/v1/finance/wallet"); + } + + /** Fetch the raw NGN fiat balance (returns amount in kobo). */ + static async getBalance(): Promise { + return apiClient.get("/api/v1/finance/balance"); + } + + /** Fetch a paginated list of transactions. */ + static async getTransactions(page = 1, limit = 10): Promise { + return apiClient.get( + `/api/v1/finance/transactions?page=${page}&limit=${limit}` + ); + } } diff --git a/src/lib/api/kyb.ts b/src/lib/api/kyb.ts index 2b83b98c..9ce7ca55 100644 --- a/src/lib/api/kyb.ts +++ b/src/lib/api/kyb.ts @@ -1,8 +1,37 @@ import { apiClient } from "../api-client"; import type { KybVerificationStatus } from "@/types/kyb"; +export interface KybUploadUrlResponse { + signedUrl: string; + key: string; +} + +export interface KybSubmitData { + registrationType: string; + registrationNo: string; + incorporationCertificatePath: string; + memorandumArticlePath: string; + formC02C07Path?: string; +} + export class KybService { static async getStatus(): Promise { return apiClient.get("/api/v1/kyb/status"); } + + /** + * Step 1 of file upload: request a presigned S3 URL from the server. + * Returns { signedUrl, key } so the caller can PUT the raw file to S3. + */ + static async getUploadUrl(filename: string, contentType: string): Promise { + return apiClient.post("/api/v1/kyb/upload", { + filename, + contentType, + }); + } + + /** Submit KYB verification data after all files have been uploaded to S3. */ + static async submit(data: KybSubmitData): Promise { + return apiClient.post("/api/v1/kyb/submit", data); + } } \ No newline at end of file diff --git a/src/lib/api/organization.ts b/src/lib/api/organization.ts index 5ec46ada..71d0f814 100644 --- a/src/lib/api/organization.ts +++ b/src/lib/api/organization.ts @@ -42,6 +42,15 @@ export interface UpdateCompanyProfileInput { }; } +export interface LogoUploadUrlResponse { + signedUrl: string; + key: string; +} + +export interface UpdateLogoResponse { + logoUrl: string; +} + export const OrganizationApi = { getProfile(): Promise { return apiClient.get("/api/v1/company/profile"); @@ -50,4 +59,23 @@ export const OrganizationApi = { updateProfile(data: UpdateCompanyProfileInput): Promise { return apiClient.put("/api/v1/company/profile", data); }, + + /** + * Step 1 of logo upload: request a presigned S3 URL. + * Pass `filename` and `contentType` to get back `{ signedUrl, key }`. + */ + getLogoUploadUrl(filename: string, contentType: string): Promise { + const params = new URLSearchParams({ filename, contentType }).toString(); + return apiClient.get( + `/api/v1/organizations/logo-upload-url?${params}` + ); + }, + + /** + * Step 3 of logo upload: save the S3 key to the database. + * Returns `{ logoUrl }` with the permanent CDN URL. + */ + updateLogo(key: string): Promise { + return apiClient.patch("/api/v1/organizations/logo", { key }); + }, }; diff --git a/src/lib/api/team.ts b/src/lib/api/team.ts index 50870e99..f915c1c0 100644 --- a/src/lib/api/team.ts +++ b/src/lib/api/team.ts @@ -1,6 +1,20 @@ import { apiClient } from "../api-client"; import { CreateInvitationInput, ResendInvitationInput, ListInvitationsInput } from "@/server/validations/invitation.schema"; +export interface AcceptInvitationPayload { + token: string; + firstName: string; + lastName: string; + password: string; +} + +export interface TimeOffPayload { + startDate: string; + endDate: string; + leaveType: string; + reason: string; + employeeId?: string; +} export class TeamService { static async listInvitations(params?: ListInvitationsInput) { let url = "/api/v1/invitations"; @@ -26,4 +40,24 @@ export class TeamService { static async getTeamMembers() { return apiClient.get("/api/v1/team/members"); } + + /** Validate an invitation token and return its details. */ + static async validateInvitation(token: string) { + return apiClient.get(`/api/v1/invitations/validate?token=${encodeURIComponent(token)}`); + } + + /** Accept an invitation and create a new user account. */ + static async acceptInvitation(payload: AcceptInvitationPayload) { + return apiClient.post("/api/v1/invitations/accept", payload); + } + + /** Decline an invitation by token. */ + static async declineInvitation(token: string) { + return apiClient.post("/api/v1/invitations/decline", { token }); + } + + /** Submit a time-off request on behalf of an employee (or the current user). */ + static async submitTimeOff(payload: TimeOffPayload) { + return apiClient.post("/api/v1/team/time-off", payload); + } } From 21d5526a8e3b31012348f0528ffd5973b388142f Mon Sep 17 00:00:00 2001 From: Aditya Kadam <2025.akadam@isu.ac.in> Date: Fri, 1 May 2026 22:29:39 +0530 Subject: [PATCH 16/21] feat: build Payroll Overview page UI (Closes #443) --- .../payroll/components/PayrollOverview.tsx | 581 ++++++++++++++++++ src/app/(dashboard)/payroll/page.tsx | 219 +------ 2 files changed, 583 insertions(+), 217 deletions(-) create mode 100644 src/app/(dashboard)/payroll/components/PayrollOverview.tsx diff --git a/src/app/(dashboard)/payroll/components/PayrollOverview.tsx b/src/app/(dashboard)/payroll/components/PayrollOverview.tsx new file mode 100644 index 00000000..62de500e --- /dev/null +++ b/src/app/(dashboard)/payroll/components/PayrollOverview.tsx @@ -0,0 +1,581 @@ +"use client"; + +import React, { useState } from "react"; +import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from "recharts"; +import { AlertCircle, Calendar, Users, DollarSign, ChevronLeft, ChevronRight, ExternalLink } from "lucide-react"; + +// Mock data for stat cards +const statsData = { + totalMonthlyPayout: "₦2,450,000", + totalEmployees: 24, + nextPayoutDate: "May 15, 2026", +}; + +// Mock data for payout overview chart +const payoutChartData = [ + { month: "Jan", payout: 1800000 }, + { month: "Feb", payout: 2100000 }, + { month: "Mar", payout: 1950000 }, + { month: "Apr", payout: 2300000 }, + { month: "May", payout: 2450000 }, + { month: "Jun", payout: 2200000 }, +]; + +// Mock data for payout schedule table +const payoutScheduleData = [ + { + id: "1", + name: "Aditya Kadam", + role: "Software Engineer", + contractType: "Full-time", + frequency: "Monthly", + amount: "₦450,000", + paidIn: "USDT", + nextPayout: "May 15, 2026", + avatar: null, + }, + { + id: "2", + name: "Sarah Chen", + role: "Product Designer", + contractType: "Full-time", + frequency: "Monthly", + amount: "₦380,000", + paidIn: "NGN", + nextPayout: "May 15, 2026", + avatar: null, + }, + { + id: "3", + name: "Michael Okon", + role: "DevOps Engineer", + contractType: "Contract", + frequency: "Bi-weekly", + amount: "₦280,000", + paidIn: "USDT", + nextPayout: "May 8, 2026", + avatar: null, + }, + { + id: "4", + name: "Fatima Ibrahim", + role: "Frontend Developer", + contractType: "Full-time", + frequency: "Monthly", + amount: "₦350,000", + paidIn: "NGN", + nextPayout: "May 15, 2026", + avatar: null, + }, + { + id: "5", + name: "David Paul", + role: "Backend Developer", + contractType: "Full-time", + frequency: "Monthly", + amount: "₦400,000", + paidIn: "USDT", + nextPayout: "May 15, 2026", + avatar: null, + }, + { + id: "6", + name: "Grace Emen", + role: "QA Engineer", + contractType: "Part-time", + frequency: "Monthly", + amount: "₦180,000", + paidIn: "NGN", + nextPayout: "May 15, 2026", + avatar: null, + }, + { + id: "7", + name: "James Wilson", + role: "Tech Lead", + contractType: "Full-time", + frequency: "Monthly", + amount: "₦550,000", + paidIn: "USDT", + nextPayout: "May 15, 2026", + avatar: null, + }, + { + id: "8", + name: "Amina Yusuf", + role: "UI Designer", + contractType: "Contract", + frequency: "Monthly", + amount: "₦320,000", + paidIn: "NGN", + nextPayout: "May 15, 2026", + avatar: null, + }, +]; + +const ITEMS_PER_PAGE_OPTIONS = [5, 10, 20]; + +function getInitials(name: string): string { + const parts = name.split(" "); + return parts.map((p) => p[0]).join("").toUpperCase().slice(0, 2); +} + +function getTokenColor(token: string): string { + if (token === "USDT") return "bg-green-100 text-green-700 border-green-200"; + if (token === "NGN") return "bg-blue-100 text-blue-700 border-blue-200"; + return "bg-gray-100 text-gray-700 border-gray-200"; +} + +function getContractTypeBadge(type: string): string { + switch (type) { + case "Full-time": + return "bg-purple-100 text-purple-700 border-purple-200"; + case "Part-time": + return "bg-blue-100 text-blue-700 border-blue-200"; + case "Contract": + return "bg-orange-100 text-orange-700 border-orange-200"; + default: + return "bg-gray-100 text-gray-700 border-gray-200"; + } +} + +// Stat Card Component +interface StatCardProps { + icon: React.ReactNode; + label: string; + value: string | number; + iconBg: string; +} + +function StatCard({ icon, label, value, iconBg }: StatCardProps) { + return ( +
+
+
+ {icon} +
+
+

{label}

+

{value}

+
+
+
+ + This year + +
+
+ ); +} + +// Urgent Action Banner Component +interface UrgentActionBannerProps { + visible: boolean; +} + +function UrgentActionBanner({ visible }: UrgentActionBannerProps) { + if (!visible) return null; + + return ( +
+
+
+ +
+
+

Urgent: Pending Payroll Action Required

+

+ You have 3 payroll items that require immediate attention. Please review and process them to avoid delays. +

+ +
+
+
+ ); +} + +// Payout Schedule Table Component +interface PayoutScheduleTableProps { + data: typeof payoutScheduleData; +} + +function PayoutScheduleTable({ data }: PayoutScheduleTableProps) { + const [currentPage, setCurrentPage] = useState(1); + const [itemsPerPage, setItemsPerPage] = useState(5); + const [selectedIds, setSelectedIds] = useState>(new Set()); + + const totalPages = Math.ceil(data.length / itemsPerPage); + const startIndex = (currentPage - 1) * itemsPerPage; + const endIndex = startIndex + itemsPerPage; + const currentPageData = data.slice(startIndex, endIndex); + + const allSelected = currentPageData.length > 0 && currentPageData.every((item) => selectedIds.has(item.id)); + + const toggleAll = () => { + if (allSelected) { + setSelectedIds((prev) => { + const next = new Set(prev); + currentPageData.forEach((item) => next.delete(item.id)); + return next; + }); + } else { + setSelectedIds((prev) => { + const next = new Set(prev); + currentPageData.forEach((item) => next.add(item.id)); + return next; + }); + } + }; + + const toggleItem = (id: string) => { + setSelectedIds((prev) => { + const next = new Set(prev); + if (next.has(id)) { + next.delete(id); + } else { + next.add(id); + } + return next; + }); + }; + + return ( +
+ {/* Table Header */} +
+
+ +
+ Employee + Contract Type + Frequency + Amount + Paid In + Next Payout Date +
+ + {/* Table Body */} +
+ {currentPageData.map((item) => ( +
+ {/* Checkbox - Desktop */} +
e.stopPropagation()}> + toggleItem(item.id)} + className="w-4 h-4 rounded border-gray-300 text-[#5E2A8C] focus:ring-[#5E2A8C] cursor-pointer" + /> +
+ + {/* Employee */} +
+
e.stopPropagation()}> + toggleItem(item.id)} + className="w-4 h-4 rounded border-gray-300 text-[#5E2A8C] focus:ring-[#5E2A8C] cursor-pointer" + /> +
+ {item.avatar ? ( + {item.name} + ) : ( +
+ {getInitials(item.name)} +
+ )} +
+

{item.name}

+

{item.role}

+
+
+ + {/* Contract Type */} +
+ + {item.contractType} + +
+ + {/* Frequency */} +
+ {item.frequency} +
+ + {/* Amount */} +
+ {item.amount} +
+ + {/* Paid In */} +
+ + {item.paidIn === "USDT" ? "USDT" : "₦"} {item.paidIn} + +
+ + {/* Next Payout Date */} +
+ {item.nextPayout} +
+
+ ))} +
+ + {/* Pagination */} +
+
+ Results per page: + +
+ +
+ + +
+ {Array.from({ length: totalPages }, (_, i) => i + 1).map((page) => ( + + ))} +
+ + +
+
+
+ ); +} + +// Mobile Card View Component +interface MobilePayoutCardsProps { + data: typeof payoutScheduleData; +} + +function MobilePayoutCards({ data }: MobilePayoutCardsProps) { + const [currentPage, setCurrentPage] = useState(1); + const [itemsPerPage, setItemsPerPage] = useState(5); + + const totalPages = Math.ceil(data.length / itemsPerPage); + const startIndex = (currentPage - 1) * itemsPerPage; + const endIndex = startIndex + itemsPerPage; + const currentPageData = data.slice(startIndex, endIndex); + + return ( +
+ {currentPageData.map((item) => ( +
+
+
+ {item.avatar ? ( + {item.name} + ) : ( +
+ {getInitials(item.name)} +
+ )} +
+

{item.name}

+ + {item.contractType} + +
+
+
+

{item.amount}

+ + {item.paidIn} + +
+
+
+
+

Frequency

+

{item.frequency}

+
+
+

Next Payout

+

{item.nextPayout}

+
+
+
+ ))} + + {/* Mobile Pagination */} +
+ + + Page {currentPage} of {totalPages} + + +
+
+ ); +} + +// Main Payroll Overview Component +export default function PayrollOverview() { + const [showPendingAction] = useState(true); // Set to false to hide banner + + return ( +
+ {/* Stat Cards */} +
+ } + label="Total Monthly Payout" + value={statsData.totalMonthlyPayout} + iconBg="bg-purple-100" + /> + } + label="Total Employees" + value={statsData.totalEmployees} + iconBg="bg-blue-100" + /> + } + label="Next Payout Date" + value={statsData.nextPayoutDate} + iconBg="bg-green-100" + /> +
+ + {/* Urgent Action Banner */} + + + {/* Payout Overview Chart */} +
+

Payout Overview

+
+ + + + + `₦${(value / 1000000).toFixed(1)}M`} + /> + { + if (typeof value === "number") { + return [`₦${value.toLocaleString()}`, "Payout"]; + } + return [String(value), "Payout"]; + }} + /> + + + +
+
+ + {/* Payout Schedule */} +
+

Payout Schedule

+ {/* Desktop Table */} +
+ +
+ {/* Mobile Cards */} +
+ +
+
+
+ ); +} diff --git a/src/app/(dashboard)/payroll/page.tsx b/src/app/(dashboard)/payroll/page.tsx index a91f91c1..8d1cd071 100644 --- a/src/app/(dashboard)/payroll/page.tsx +++ b/src/app/(dashboard)/payroll/page.tsx @@ -11,6 +11,7 @@ import { RequestError } from "@/lib/api-client"; import useModal from "@/hooks/useModal"; import PayoutHistory from "@/app/(dashboard)/payroll/components/PayoutHistory"; +import PayrollOverview from "@/app/(dashboard)/payroll/components/PayrollOverview"; type ModalState = | { type: "none" } @@ -181,223 +182,7 @@ export default function PayrollPage() { exit={{ opacity: 0, y: -15 }} transition={{ duration: 0.3 }} > - {/* Banner */} -
-

- Set up payroll for your employees -

-

- Let's make things easier! Automate payroll disbursement for your employees. -

- -
- - {/* Payout Schedule */} -
-
-

- Payout Schedule -

-

- Payroll -

- -
-
- setSearch(e.target.value)} - className="w-full sm:w-80 pl-4 pr-10 py-2.5 bg-white border border-[#DCE0E5] rounded-lg text-sm text-[#111827] placeholder-[#9CA3AF] focus:outline-none focus:ring-2 focus:ring-[#5E2A8C] focus:border-[#5E2A8C] transition-colors duration-200 dark:bg-gray-800 dark:border-gray-700 dark:text-gray-200 dark:placeholder-gray-500" - /> -
- -
-
-
- - {/* Payroll Table */} -
- {isLoading ? ( -
- -
- ) : fetchError ? ( -
- -

{fetchError}

- -
- ) : filtered.length === 0 ? ( -
-
- -
-

- {search ? "No employees match your search." : "You haven't set up any payrolls."} -

-

- {search ? "Try a different search term." : "Employees you put on payroll will be displayed here"} -

-
- ) : ( - <> - {/* Run Payroll bar */} - {selectedIds.size > 0 && ( -
- - {selectedIds.size} employee{selectedIds.size > 1 ? "s" : ""} selected -  ·  - Total: {formatAmount(totalAmount, "NGN")} - - -
- )} - - {/* Table header */} -
-
- -
- Employee - Role - Invoice - Amount - Currency -
- - {/* Rows */} -
- {filtered.map((item) => ( -
toggleItem(item.id)} - className={`grid md:grid-cols-[40px_1fr_1fr_140px_120px_100px] gap-4 px-6 py-4 cursor-pointer transition-colors ${ - selectedIds.has(item.id) - ? "bg-[#F5F0FF] dark:bg-purple-950/30" - : "hover:bg-[#F9FAFB] dark:hover:bg-gray-800/50" - }`} - > - {/* Checkbox */} -
e.stopPropagation()}> - toggleItem(item.id)} - className="w-4 h-4 rounded border-gray-300 text-[#5E2A8C] focus:ring-[#5E2A8C]" - /> -
- - {/* Employee */} -
-
e.stopPropagation()}> - toggleItem(item.id)} - className="w-4 h-4 rounded border-gray-300 text-[#5E2A8C] focus:ring-[#5E2A8C]" - /> -
- {item.employee.avatarUrl ? ( - - ) : ( -
- {getInitials(item.employee.firstName, item.employee.lastName)} -
- )} -
-

- {item.employee.firstName} {item.employee.lastName} -

-

- {item.employee.email} -

-
-
- - {/* Role */} -
- - {item.employee.role} - -
- - {/* Invoice */} -
- - #{item.invoiceNo} - -
- - {/* Amount */} -
- - {formatAmount(item.amount, item.paidIn)} - -
- - {/* Currency */} -
- - {item.paidIn} - -
-
- ))} -
- - {/* Footer: select all + run payroll */} -
- - -
- - )} -
+ )} From 7b18ee2cbbb37d27c8ae6ff2e7f9200f7eff0472 Mon Sep 17 00:00:00 2001 From: Lewechi Date: Sun, 3 May 2026 12:36:52 +0100 Subject: [PATCH 17/21] feat: update readme --- CODE_OF_CONDUCT.md | 14 +++ CONTRIBUTING.md | 14 +++ LICENSE | 21 ++++ README.md | 191 +++++++++++++++++++------------ SECURITY.md | 7 ++ docs/README.md | 17 +++ docs/architecture/overview.md | 15 +++ docs/context/project-overview.md | 9 ++ docs/context/user-personas.md | 15 +++ 9 files changed, 227 insertions(+), 76 deletions(-) create mode 100644 CODE_OF_CONDUCT.md create mode 100644 CONTRIBUTING.md create mode 100644 LICENSE create mode 100644 SECURITY.md create mode 100644 docs/README.md create mode 100644 docs/architecture/overview.md create mode 100644 docs/context/project-overview.md create mode 100644 docs/context/user-personas.md diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 00000000..103bec8c --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,14 @@ +# Code of Conduct + +## Our Pledge +We pledge to make participation in our project a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. + +## Our Standards +- Use welcoming and inclusive language. +- Be respectful of differing viewpoints and experiences. +- Gracefully accept constructive criticism. +- Focus on what is best for the community. +- Show empathy towards other community members. + +## Enforcement +Responsibilities for clarifying standards of acceptable behavior and taking appropriate and fair corrective action in response to any instances of unacceptable behavior lie with the project maintainers. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..6f5b51ef --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,14 @@ +# Contributing to VestRoll + +We love your input! We want to make contributing to VestRoll as easy and transparent as possible. + +## How to Contribute +1. Fork the repo and create your branch from `main`. +2. If you've added code that should be tested, add tests. +3. If you've changed APIs, update the documentation. +4. Ensure the test suite passes. +5. Make sure your code lints. +6. Issue that pull request! + +## License +By contributing, you agree that your contributions will be licensed under its MIT License. diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..06ad6786 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 VestRoll + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index c9a9758d..c67f85a8 100644 --- a/README.md +++ b/README.md @@ -1,117 +1,156 @@ -# VestRoll: Payroll +# VestRoll -Stablecoin and fiat Payroll and invoicing platform on Stellar +

+ License + Testnet + Mainnet +

---- +

+ Node.js 20 + TS 5 + Next.js 15 + Drizzle + Stellar +

-## 🏗️ The 2026 Technology Stack +**VestRoll** is a professional payroll and invoicing system built on the Stellar network. It enables businesses, contractors, and individuals to manage global payments with automated tax handling using both fiat and stablecoins. -### Core Architecture +## 🚀 Features -- **Framework**: [Next.js 15.5](https://nextjs.org/) (App Router & Turbopack) -- **Library**: [React 19](https://react.dev/) -- **State**: Redux Toolkit (UI) & Zustand (Store) +- 💰 **Hybrid Payments**: Full support for fiat and stablecoins (USDC) for global settlement. +- ⚡ **Payroll Management**: Automated disbursement of payments to large teams in seconds. +- 🤝 **Invoice as a Service**: Specialized infrastructure for generating and tracking invoices on Stellar. +- 💸 **Tax Handling**: Integrated tax calculations and reporting for every transaction. +- 🔐 **Multi-Account Support**: Tailored experiences for Business, Contractor, and Individual accounts. -### Identity & Privacy (ZK) +## 🛠️ Tech Stack -- **Auth**: [@stellar/passkey-kit](https://github.com/stellar/passkey-kit) (FaceID/TouchID Biometric Login) -- **Privacy**: [Stellar Protocol 25 (X-Ray)](https://stellar.org/blog/developers/protocol-25-x-ray) & `circomlib` for ZK-shielded payroll. -- **Smart Accounts**: [@stellar/smart-account-kit](https://github.com/stellar/smart-account-kit) for automated contract-wallet deployment. +- **Framework**: [Next.js 15](https://nextjs.org/) (Frontend & API) +- **Runtime**: Node.js 20 LTS +- **Database**: PostgreSQL (via Drizzle ORM) +- **Blockchain**: Stellar Network (SEP-24, Passkey Kit) +- **State Management**: Redux Toolkit & Zustand -### Finance & Fiat Bridge +## 🏁 Quick Start -- **Fiat Providers**: Native integration for **Monnify** and **Flutterwave** (NGN On-ramps). -- **Stellar Bridge**: [SEP-24](https://stellar.org/developers/stellar-wallet-sdk) via Stellar Wallet SDK for automated NGN-to-USDC settlement. -- **Gasless UX**: **Launchtube** / Fee-Bumping infrastructure (Zero XLM required for users). +1. **Clone and Prepare**: ---- + ```bash + git clone https://github.com/SafeVault/vestroll.git + cd vestroll + cp .env.example .env + ``` + +2. **Install Dependencies**: + + ```bash + pnpm install + ``` + +3. **Database Setup**: -## 📂 Project Structure + ```bash + pnpm drizzle-kit push + ``` + +4. **Run in Development**: + ```bash + pnpm dev + ``` + +## 🏗️ Project Structure -```text +``` vestroll/ ├── src/ -│ ├── api/ # ZK-Circuit logic & Service Orchestration -│ ├── app/ # Next.js App Router (Invisible Crypto UX) -│ ├── components/ # Biometric Auth & Shielded UI Components +│ ├── app/ # Next.js App Router (Pages & API) +│ ├── components/ # UI Components (Shadcn UI) │ ├── server/ -│ │ ├── services/ # Monnify, Flutterwave & Blockchain Services -│ │ └── db/ # Drizzle Schema (Auth, Org, Fiat, ZK) -│ └── lib/ # Passkey & Smart Account SDK wrappers -└── brain/ # Master Roadmaps & Technical Documentation +│ │ ├── services/ # Business Logic (Payroll, Tax, Stellar) +│ │ └── db/ # Drizzle Schema & Migrations +│ └── lib/ # Shared utilities & SDK wrappers +├── docs/ # Comprehensive documentation +├── public/ # Static assets +└── scripts/ # Utility scripts for DB and Swagger ``` ---- +## 📚 Documentation -## ✨ Key Features +Comprehensive documentation is available in the [`/docs`](./docs/) folder: -- **Invisible Onboarding**: Users sign up with Email and Biometrics (Passkeys). No seed phrases, no private keys, no 12-word recovery. -- **Hybrid Recovery**: A "Best of Both Worlds" security model—Biometric speed for daily use, Email recovery for account resets. -- **ZK-Shielded Payments**: Payroll amounts are hidden from the public ledger using Zero-Knowledge proofs, providing enterprise-grade confidentiality. -- **Automated Fiat-Stable Bridge**: Deposits in **Naira (NGN)** are automatically reflected as **USDC** in the Smart Wallet, enabling instant global payouts. -- **Atomic Batching**: Disburse 100+ payroll entries in a single biometric signature using Soroban atomic transactions. -- **Invisible Gas**: Transaction fees are sponsored (Launchtube) or paid in USDC, ensuring users never need to hold or know about XLM. +### Quick Start ---- +- 📖 **[Main Documentation](./docs/README.md)** - Complete documentation index -## 🎯 Target Audience & Ecosystem Impact +### Core Documentation -### Who is this for? +- 📐 [Architecture Overview](./docs/architecture/overview.md) - System architecture +- 📋 [Project Overview](./docs/context/project-overview.md) - Vision and goals +- 👥 [User Personas](./docs/context/user-personas.md) - Account types and use cases -- **Global Enterprises**: Companies with distributed teams needing seamless cross-border payroll. -- **DAO & Web3 Organizations**: Native crypto organizations requiring fiat and stablecoin payroll solutions. -- **Freelancers & Contractors**: Individuals seeking transparent, instant, and low-fee payments. +## 🎯 Use Cases -### Contribution to the Stellar Ecosystem +### Business Payroll -VestRoll plays a pivotal role in the **Stellar ecosystem** by: +Manage organizational payroll with ease: -1. **Driving Real-World Utility**: Moving beyond speculation to practical, high-volume stablecoin use cases (Payroll). -2. **Highlighting Efficiency**: Showcasing Stellar's speed and low fees for frequent, small-to-large value transactions. +1. Deposit funds via fiat or stablecoin. +2. Automate tax deductions based on jurisdiction. +3. Disburse payments to contractors and employees instantly. ---- +### Contractor Invoicing -## 🚀 Getting Started +Generate professional invoices and get paid: -### Prerequisites +1. Create invoices as a service on Stellar. +2. Receive payments in stablecoins for low-fee global settlement. +3. Track payment status and tax obligations. -- Node.js 20.x or higher -- **pnpm** (preferred) -- **Stellar CLI** (for local Soroban development) +### Individual Payments -### Installation +Simple and secure personal payment management: -1. Clone the repository and install dependencies: - ```bash - pnpm install - ``` -2. Configure Environment: - Add `STELLAR_RPC_URL` and `LAUNCHTUBE_API_KEY` to your `.env.local`. -3. **Database Setup & Seeding**: - To sync the schema and populate the database with realistic test data: - ```bash - pnpm drizzle-kit push - pnpm db:seed - ``` -4. Start development server: - ```bash - pnpm dev - ``` +1. Manage personal balances in fiat or stablecoins. +2. Secure onboarding with biometric Passkeys. ---- +## 🤝 Contributing + +We welcome contributions! Please see [CONTRIBUTING.md](./CONTRIBUTING.md) for detailed guidelines. + +## 📝 License + +This project is licensed under the MIT License - see the [LICENSE](./LICENSE) file for details. -## 🛡️ Roadmap & Strategy +## 🆘 Support -VestRoll development is structured across 4 Strategic Tranches: +- **Documentation**: [/docs](./docs/) +- **Issues**: [GitHub Issues](https://github.com/SafeVault/vestroll/issues) -1. **Tranche 1**: Foundation & Biometric Onboarding. -2. **Tranche 2**: Fiat-Stable Bridge (NGN MVP). -3. **Tranche 3**: Privacy Shield (Shielded Testnet). -4. **Tranche 4**: Mainnet Launch & UX Audit. +## 👥 Maintainers +
+ + + +
+ codeZe-us +

+ codeZe-us +

+ GitHub +
--- -## 📄 License +## **Thanks to all the contributors who have made this project possible!** -Commercial - All rights reserved to SafeVault/VestRoll. +[![Contributors](https://contrib.rocks/image?repo=SafeVault/vestroll)](https://github.com/SafeVault/vestroll/graphs/contributors) + +--- + +

+ 🚀 Empowering global payroll with Stellar 🚀 +

+ +--- diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 00000000..b5871273 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,7 @@ +# Security Policy + +## Supported Versions +Only the latest version of VestRoll is supported. + +## Reporting a Vulnerability +Please report security vulnerabilities by opening an issue or contacting the maintainers directly. We aim to respond within 48 hours. diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 00000000..290e188e --- /dev/null +++ b/docs/README.md @@ -0,0 +1,17 @@ +# Documentation Index + +Welcome to the VestRoll documentation. + +## Sections + +### 🏁 [Quick Start](./README.md) +Basic setup and installation. + +### 📐 [Architecture](./architecture/overview.md) +System design and technical implementation details. + +### 📋 [Project Overview](./context/project-overview.md) +Vision, goals, and roadmap. + +### 👥 [User Personas](./context/user-personas.md) +Detailed breakdown of account types (Business, Contractor, Individual). diff --git a/docs/architecture/overview.md b/docs/architecture/overview.md new file mode 100644 index 00000000..c95a6c30 --- /dev/null +++ b/docs/architecture/overview.md @@ -0,0 +1,15 @@ +# Architecture Overview + +VestRoll is built with a modern, scalable stack focusing on transparency and security. + +## System Components +- **Web App**: Next.js 15 providing a seamless UI and API routes. +- **Service Layer**: Decoupled business logic for payroll, invoicing, and tax. +- **Blockchain Layer**: Integration with Stellar for instant global settlement. +- **Database**: PostgreSQL with Drizzle ORM for type-safe data management. + +## Data Flow +1. User interacts with the Next.js frontend. +2. API routes call the appropriate service in `src/server/services`. +3. Services interact with the DB and/or the Stellar network. +4. Real-time updates are pushed back to the UI. diff --git a/docs/context/project-overview.md b/docs/context/project-overview.md new file mode 100644 index 00000000..3f4da3ac --- /dev/null +++ b/docs/context/project-overview.md @@ -0,0 +1,9 @@ +# Project Overview + +## Vision +To provide a seamless, borderless payroll and invoicing experience for the modern global workforce using Stellar. + +## Goals +- **Simplify Global Payments**: Use stablecoins to avoid high fees and slow traditional banking. +- **Automate Compliance**: Built-in tax handling for various jurisdictions. +- **Invoicing as a Service**: Provide developers and businesses with an API for Stellar-based invoicing. diff --git a/docs/context/user-personas.md b/docs/context/user-personas.md new file mode 100644 index 00000000..d7e85588 --- /dev/null +++ b/docs/context/user-personas.md @@ -0,0 +1,15 @@ +# User Personas + +VestRoll serves three primary user types: + +### 1. Business +- **Needs**: Manage large-scale payroll, track company expenses, handle tax compliance. +- **Features**: Batch payments, organizational dashboards, detailed reporting. + +### 2. Contractor +- **Needs**: Professional invoicing, fast global payments, easy tax tracking. +- **Features**: Invoice creation, stablecoin payouts, payment history. + +### 3. Individual +- **Needs**: Receive and send money globally, manage personal balances. +- **Features**: Simple wallet interface, biometric security, fiat on/off ramps. From ba30f8612334edc9c291f61ebed4d2cd5245de7e Mon Sep 17 00:00:00 2001 From: Lewechi Date: Sun, 3 May 2026 12:39:23 +0100 Subject: [PATCH 18/21] feat: update readme --- README.md | 38 +++++++++++++++++++------------------- docs/README.md | 4 ++++ 2 files changed, 23 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index c67f85a8..e5d0ab5e 100644 --- a/README.md +++ b/README.md @@ -16,15 +16,15 @@ **VestRoll** is a professional payroll and invoicing system built on the Stellar network. It enables businesses, contractors, and individuals to manage global payments with automated tax handling using both fiat and stablecoins. -## 🚀 Features +## Features -- 💰 **Hybrid Payments**: Full support for fiat and stablecoins (USDC) for global settlement. -- ⚡ **Payroll Management**: Automated disbursement of payments to large teams in seconds. -- 🤝 **Invoice as a Service**: Specialized infrastructure for generating and tracking invoices on Stellar. -- 💸 **Tax Handling**: Integrated tax calculations and reporting for every transaction. -- 🔐 **Multi-Account Support**: Tailored experiences for Business, Contractor, and Individual accounts. +- **Hybrid Payments**: Full support for fiat and stablecoins (USDC) for global settlement. +- **Payroll Management**: Automated disbursement of payments to large teams in seconds. +- **Invoice as a Service**: Specialized infrastructure for generating and tracking invoices on Stellar. +- **Tax Handling**: Integrated tax calculations and reporting for every transaction. +- **Multi-Account Support**: Tailored experiences for Business, Contractor, and Individual accounts. -## 🛠️ Tech Stack +## Tech Stack - **Framework**: [Next.js 15](https://nextjs.org/) (Frontend & API) - **Runtime**: Node.js 20 LTS @@ -32,7 +32,7 @@ - **Blockchain**: Stellar Network (SEP-24, Passkey Kit) - **State Management**: Redux Toolkit & Zustand -## 🏁 Quick Start +## Quick Start 1. **Clone and Prepare**: @@ -59,7 +59,7 @@ pnpm dev ``` -## 🏗️ Project Structure +## Project Structure ``` vestroll/ @@ -75,21 +75,21 @@ vestroll/ └── scripts/ # Utility scripts for DB and Swagger ``` -## 📚 Documentation +## Documentation Comprehensive documentation is available in the [`/docs`](./docs/) folder: ### Quick Start -- 📖 **[Main Documentation](./docs/README.md)** - Complete documentation index +- **[Main Documentation](./docs/README.md)** - Complete documentation index ### Core Documentation -- 📐 [Architecture Overview](./docs/architecture/overview.md) - System architecture -- 📋 [Project Overview](./docs/context/project-overview.md) - Vision and goals -- 👥 [User Personas](./docs/context/user-personas.md) - Account types and use cases +- [Architecture Overview](./docs/architecture/overview.md) - System architecture +- [Project Overview](./docs/context/project-overview.md) - Vision and goals +- [User Personas](./docs/context/user-personas.md) - Account types and use cases -## 🎯 Use Cases +## Use Cases ### Business Payroll @@ -114,15 +114,15 @@ Simple and secure personal payment management: 1. Manage personal balances in fiat or stablecoins. 2. Secure onboarding with biometric Passkeys. -## 🤝 Contributing +## Contributing We welcome contributions! Please see [CONTRIBUTING.md](./CONTRIBUTING.md) for detailed guidelines. -## 📝 License +## License This project is licensed under the MIT License - see the [LICENSE](./LICENSE) file for details. -## 🆘 Support +## Support - **Documentation**: [/docs](./docs/) - **Issues**: [GitHub Issues](https://github.com/SafeVault/vestroll/issues) @@ -150,7 +150,7 @@ This project is licensed under the MIT License - see the [LICENSE](./LICENSE) fi ---

- 🚀 Empowering global payroll with Stellar 🚀 + Empowering global payroll with Stellar

--- diff --git a/docs/README.md b/docs/README.md index 290e188e..212e660f 100644 --- a/docs/README.md +++ b/docs/README.md @@ -5,13 +5,17 @@ Welcome to the VestRoll documentation. ## Sections ### 🏁 [Quick Start](./README.md) + Basic setup and installation. ### 📐 [Architecture](./architecture/overview.md) + System design and technical implementation details. ### 📋 [Project Overview](./context/project-overview.md) + Vision, goals, and roadmap. ### 👥 [User Personas](./context/user-personas.md) + Detailed breakdown of account types (Business, Contractor, Individual). From 1a44ae1801dcad91df40039038086ab3bae0dee9 Mon Sep 17 00:00:00 2001 From: Lewechi Date: Tue, 26 May 2026 12:53:58 +0100 Subject: [PATCH 19/21] fix: fixes --- next.config.ts | 3 + src/api/db/schema.ts | 6 +- src/app/(auth)/verify-otp/page.tsx | 2 +- src/app/(dashboard)/contracts/[cid]/page.tsx | 2 +- src/app/(dashboard)/dashboard/page.tsx | 16 +- src/app/(dashboard)/employees/page.tsx | 2 +- src/app/(dashboard)/finance/StatusBadge.tsx | 8 +- .../finance/components/EmptyState.tsx | 2 +- src/app/(dashboard)/finance/page.tsx | 10 +- .../payroll/components/PayrollOverview.tsx | 18 +- .../2fa-setup/email-verify/page.tsx | 4 +- .../profile-settings/notifications/page.tsx | 4 +- src/app/(dashboard)/profile-settings/page.tsx | 6 +- .../profile-settings/preferences/page.tsx | 4 +- .../settings/address-book/page.tsx | 18 +- .../addresses/billing-address/page.tsx | 2 +- .../addresses/registered-address/page.tsx | 2 +- .../settings/company/company-info/page.tsx | 2 +- .../(components)/HiringTemplatePage.tsx | 2 +- .../hiring-templates/edit/[id]/page.tsx | 20 +- .../settings/hiring-templates/page.tsx | 10 +- src/app/(dashboard)/settings/page.tsx | 31 +- src/app/(dashboard)/team-management/page.tsx | 2 +- src/app/api-docs/page.tsx | 2 +- src/app/api/v1/auth/2fa/verify/route.test.ts | 2 +- src/app/api/v1/auth/login/route.ts | 2 +- src/app/api/v1/finance/balance/route.ts | 2 +- .../finance/ngn/webhook/[provider]/route.ts | 14 +- src/app/api/v1/invitations/validate/route.ts | 2 +- src/app/api/v1/kyb/submit/route.test.ts | 136 ++-- .../v1/organizations/logo-upload-url/route.ts | 10 +- src/app/api/v1/organizations/logo/route.ts | 26 +- src/app/api/v1/team/time-off/route.test.ts | 6 +- src/app/api/v1/team/time-off/route.ts | 6 +- src/components/contracts.tsx | 13 +- .../features/2fa/TwoFactorCodeModal.tsx | 12 +- src/components/features/auth/OtpForm.tsx | 4 +- .../features/auth/RegistrationWizard.tsx | 2 +- .../features/auth/billing-address.tsx | 14 +- .../features/auth/business-details.tsx | 14 +- .../features/auth/forgot-password-form.tsx | 2 +- src/components/features/auth/login-page.tsx | 2 +- .../features/contracts/AgreementSelector.tsx | 4 +- .../features/contracts/ContractDetails.tsx | 2 +- .../features/contracts/HireForm.tsx | 10 +- .../features/contracts/ui/FilterModal.tsx | 2 +- .../dashboard/home/checklist/CompleteKYB.tsx | 4 +- src/components/features/dashboard/home/lib.ts | 2 +- .../invoices/payment/InvoiceOtpInput.tsx | 10 +- .../features/email-verification/OTPInput.tsx | 6 +- .../employee-management/AccountForm.tsx | 2 +- .../employee-management/AccountManagement.tsx | 2 +- .../features/finance/DepositModal.tsx | 2 +- .../finance/NairaTransactionHistory.tsx | 4 +- .../invoices/InvoiceDetailsSection.tsx | 2 +- .../permissions/permission-form-view.tsx | 2 +- .../features/permissions/permissions-tab.tsx | 4 +- .../features/preferences/language-modal.tsx | 4 +- .../profile-settings/ImageUploadModal.tsx | 20 +- .../features/profile-settings/ProfileForm.tsx | 2 +- .../features/profile-settings/types.ts | 2 +- .../add-employee-wizard/AddEmployeeWizard.tsx | 8 +- .../steps/Step3PaymentDetails.tsx | 14 +- .../add-employee-wizard/types.ts | 2 +- .../features/team-management/employees.tsx | 4 +- .../features/team-management/expense.tsx | 12 +- .../features/team-management/milestone.tsx | 12 +- .../timeOff/CreateTimeOffForm.tsx | 14 +- .../timeOff/SelectEmployeeModal.tsx | 2 +- .../features/team-management/timeoff.tsx | 8 +- src/components/layout/sidebar.tsx | 4 +- .../shared/emailVerificationModal.tsx | 2 +- src/components/shared/feedback-widget.tsx | 2 +- .../shared/modal-didn't-get-email.tsx | 2 +- .../ContractCreationSuccessModal/page.tsx | 2 +- .../address-book/AddressBookEmptyModal.tsx | 2 +- .../modal/withdrawal-details-modal/page.tsx | 2 +- src/components/shared/table/Table.tsx | 20 +- src/components/ui/LoadingSpinner.tsx | 2 +- src/components/ui/Pagination.tsx | 10 +- src/components/ui/button.tsx | 5 +- src/components/ui/dropdown.tsx | 314 +++++----- src/components/ui/file-upload.tsx | 592 +++++++++--------- src/constants/index.tsx | 4 +- src/hooks/redux.types.ts | 2 +- src/hooks/use-sort.ts | 20 +- src/hooks/useAuth.ts | 6 +- src/hooks/useModal.ts | 57 +- src/hooks/useToast.ts | 17 +- src/lib/api-client.ts | 7 +- src/lib/api/employees.ts | 23 +- src/lib/api/finance.ts | 19 +- src/lib/api/kyb.ts | 7 +- src/lib/api/organization.ts | 10 +- src/lib/api/team.ts | 8 +- src/lib/format-naira.ts | 7 +- src/lib/slice/modalSlice.ts | 2 +- src/lib/store.ts | 6 +- src/lib/utils.ts | 7 +- src/middleware.ts | 26 +- src/routes/routesPath.ts | 8 +- src/server/db/schema.ts | 10 +- src/server/db/seed.ts | 35 +- src/server/jobs/prune-logs.job.spec.ts | 12 +- src/server/jobs/prune-logs.job.ts | 14 +- .../update-last-active.middleware.ts | 2 +- src/server/services/auth.service.spec.ts | 122 ++-- src/server/services/auth.service.ts | 86 +-- src/server/services/bank-account.service.ts | 38 +- .../services/blockchain.service.spec.ts | 8 +- src/server/services/blockchain.service.ts | 168 +---- src/server/services/email.service.ts | 5 +- src/server/services/employee.service.ts | 2 +- src/server/services/fiat-deposit.service.ts | 6 +- .../fiat/flutterwave.provider.test.ts | 4 +- src/server/services/fiat/index.ts | 18 +- .../services/fiat/monnify.provider.test.ts | 14 +- src/server/services/fiat/monnify.provider.ts | 2 +- .../fiat/payment-provider.interface.ts | 34 +- .../services/finance-wallet.service.spec.ts | 16 +- src/server/services/finance-wallet.service.ts | 24 +- .../services/invitation.service.spec.ts | 32 +- src/server/services/invitation.service.ts | 24 +- src/server/services/jwt-token.service.ts | 46 +- src/server/services/jwt.service.spec.ts | 2 +- src/server/services/jwt.service.ts | 47 +- src/server/services/kyb-admin-example.ts | 4 +- src/server/services/kyb-upload.service.ts | 6 +- src/server/services/kyb.service.ts | 36 +- src/server/services/login-otp.service.ts | 27 +- src/server/services/logo-upload.service.ts | 50 +- src/server/services/organization.service.ts | 39 +- src/server/services/otp.service.spec.ts | 2 +- src/server/services/payroll.service.ts | 34 +- src/server/services/rate-limit.service.ts | 2 +- src/server/services/time-off.service.ts | 22 +- src/server/services/transaction.service.ts | 2 +- src/server/services/two-factor.service.ts | 2 +- src/server/services/user.service.ts | 29 +- src/server/services/webhook.service.ts | 4 +- src/server/test/db-utils.ts | 35 +- .../test/fixtures/organization.fixture.ts | 2 +- src/server/test/setup.ts | 4 +- src/server/utils/api-response.ts | 33 +- src/server/utils/auth.ts | 2 +- src/server/utils/compress.test.ts | 2 +- src/server/utils/compress.ts | 13 +- src/server/utils/errors.ts | 73 +-- src/server/utils/errors/blockchain-error.ts | 44 +- src/server/utils/errors/index.ts | 2 +- src/server/utils/ping-db.ts | 5 +- src/server/utils/problem-details.ts | 40 +- src/server/utils/role.spec.ts | 6 +- src/server/utils/service-discovery.ts | 19 +- src/server/utils/slug.ts | 14 +- .../utils/transaction-idempotency.spec.ts | 4 +- src/server/utils/transaction-idempotency.ts | 35 +- src/server/utils/with-error-handler.ts | 15 +- src/server/validations/account.schema.ts | 6 +- .../validations/company-profile.schema.ts | 2 +- src/types/kyb.ts | 13 +- src/types/pagination.ts | 55 +- src/utils/date.ts | 30 +- 163 files changed, 1238 insertions(+), 2050 deletions(-) diff --git a/next.config.ts b/next.config.ts index 20421862..1fe7aa1f 100644 --- a/next.config.ts +++ b/next.config.ts @@ -15,6 +15,9 @@ const nextConfig: NextConfig = { }, }, allowedDevOrigins: ["10.199.228.216"], + experimental: { + optimizePackageImports: ['lucide-react', '@heroicons/react', 'lodash', 'date-fns', '@headlessui/react', '@radix-ui/react-icons'], + }, images: { remotePatterns: [ { diff --git a/src/api/db/schema.ts b/src/api/db/schema.ts index f2eaaff3..3a13235b 100644 --- a/src/api/db/schema.ts +++ b/src/api/db/schema.ts @@ -57,18 +57,18 @@ export const users = pgTable("users", { organizationId: uuid("organization_id").references(() => organizations.id, { onDelete: "cascade", }), - // Two-factor authentication fields + twoFactorEnabled: boolean("two_factor_enabled").default(false).notNull(), twoFactorSecret: text("two_factor_secret"), twoFactorEnabledAt: timestamp("two_factor_enabled_at"), - // Account lockout fields + failedTwoFactorAttempts: integer("failed_two_factor_attempts") .default(0) .notNull(), twoFactorLockoutUntil: timestamp("two_factor_lockout_until"), failedLoginAttempts: integer("failed_login_attempts").default(0).notNull(), lockedUntil: timestamp("locked_until"), - // OAuth fields + oauthProvider: oauthProviderEnum("oauth_provider"), oauthId: varchar("oauth_id", { length: 255 }), lastLoginAt: timestamp("last_login_at"), diff --git a/src/app/(auth)/verify-otp/page.tsx b/src/app/(auth)/verify-otp/page.tsx index 54449b3b..9d401750 100644 --- a/src/app/(auth)/verify-otp/page.tsx +++ b/src/app/(auth)/verify-otp/page.tsx @@ -19,7 +19,7 @@ function VerifyOTPContent() { try { const result = await AuthService.verifyLoginOTP({ email, otp, rememberMe }) as any; - // Store the access token in localStorage/cookie for subsequent requests + if (result?.accessToken) { localStorage.setItem("access_token", result.accessToken); } diff --git a/src/app/(dashboard)/contracts/[cid]/page.tsx b/src/app/(dashboard)/contracts/[cid]/page.tsx index 24d9b35d..3ebac140 100644 --- a/src/app/(dashboard)/contracts/[cid]/page.tsx +++ b/src/app/(dashboard)/contracts/[cid]/page.tsx @@ -21,7 +21,7 @@ export default function CidPage({ params }: Props) { const title = new URL(window.location.href).searchParams.get('title'); - // Tab configuration with conditional logic + const tabs = [ { id: 1, label: 'Details' }, { diff --git a/src/app/(dashboard)/dashboard/page.tsx b/src/app/(dashboard)/dashboard/page.tsx index 331414a1..35079759 100644 --- a/src/app/(dashboard)/dashboard/page.tsx +++ b/src/app/(dashboard)/dashboard/page.tsx @@ -20,7 +20,7 @@ const itemVariants: Variants = { visible: { opacity: 1, y: 0, transition: { duration: 0.5, ease: "easeOut" } }, }; -// Terminal statuses that stop polling + const TERMINAL_STATUSES: KybVerificationStatus["status"][] = [ "verified", "approved", @@ -53,25 +53,25 @@ export default function DashboardPage() { }; useEffect(() => { - // Initial fetch + fetchKybStatus(); }, []); useEffect(() => { - // Start polling when status is pending + if (kybStatus?.status === "pending" && !pollingRef.current) { pollingRef.current = true; - // Start first poll immediately, then schedule subsequent polls + const poll = async () => { if (!pollingRef.current || !isMountedRef.current) return; const status = await fetchKybStatus(); - // Continue polling only if status is still pending and component is mounted + if (status?.status === "pending" && pollingRef.current && isMountedRef.current) { timeoutRef.current = setTimeout(poll, 5000); } else if (status) { - // Terminal state reached - stop polling + pollingRef.current = false; if (timeoutRef.current) { clearTimeout(timeoutRef.current); @@ -82,7 +82,7 @@ export default function DashboardPage() { poll(); } - // Cleanup on unmount or status change + return () => { pollingRef.current = false; isMountedRef.current = false; @@ -92,7 +92,7 @@ export default function DashboardPage() { } }; // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); // Empty dependency array - polling is controlled via refs + }, []); const renderKybBanner = () => { if (!kybStatus || kybStatus.status === "not_started") return null; diff --git a/src/app/(dashboard)/employees/page.tsx b/src/app/(dashboard)/employees/page.tsx index 747250c3..0b3978e7 100644 --- a/src/app/(dashboard)/employees/page.tsx +++ b/src/app/(dashboard)/employees/page.tsx @@ -102,7 +102,7 @@ export default function EmployeesPage() { fetchEmployees(); }, [fetchEmployees]); - // Reset to page 1 when search/filters change + useEffect(() => { setCurrentPage(1); }, [searchQuery, filters]); diff --git a/src/app/(dashboard)/finance/StatusBadge.tsx b/src/app/(dashboard)/finance/StatusBadge.tsx index 7f592be6..2c88a51d 100644 --- a/src/app/(dashboard)/finance/StatusBadge.tsx +++ b/src/app/(dashboard)/finance/StatusBadge.tsx @@ -1,4 +1,4 @@ -// components/StatusBadge.tsx + import React from "react"; import { TransactionStatus } from "./types"; @@ -14,19 +14,19 @@ const StatusBadge: React.FC = ({ status }) => { switch (status) { case "Successful": - // Green color for success + bgColorClass = "bg-[#EDFEEC]"; textColorClass = "text-[#26902B]"; borderColorClass = "border-[#26902B]"; break; case "Pending": - // Yellow/Amber color for pending + bgColorClass = "bg-[#FEF7EB]"; textColorClass = "text-[#E79A23]"; borderColorClass = "border-[#E79A23]"; break; case "Failed": - // Red color for failed + bgColorClass = "bg-[#FEECEC]"; textColorClass = "text-[#C64242]"; borderColorClass = "border-[#C64242]"; diff --git a/src/app/(dashboard)/finance/components/EmptyState.tsx b/src/app/(dashboard)/finance/components/EmptyState.tsx index 3f2c0a75..7bfae688 100644 --- a/src/app/(dashboard)/finance/components/EmptyState.tsx +++ b/src/app/(dashboard)/finance/components/EmptyState.tsx @@ -1,4 +1,4 @@ -// components/EmptyState.tsx + import React from "react"; import Image from "next/image"; diff --git a/src/app/(dashboard)/finance/page.tsx b/src/app/(dashboard)/finance/page.tsx index 495ece98..21e61163 100644 --- a/src/app/(dashboard)/finance/page.tsx +++ b/src/app/(dashboard)/finance/page.tsx @@ -101,10 +101,10 @@ const normalizeTransaction = ( }); export default function FinancePage() { - // State for balance + const [ngnBalance, setNgnBalance] = useState("₦0.00"); - // State for transactions + const [transactions, setTransactions] = useState([]); const [isLoadingTransactions, setIsLoadingTransactions] = useState(true); const [transactionsError, setTransactionsError] = useState(null); @@ -113,12 +113,12 @@ export default function FinancePage() { const [totalItems, setTotalItems] = useState(0); const [totalPages, setTotalPages] = useState(1); - // State for UI + const [search, setSearch] = useState(""); const [selectedItems, setSelectedItems] = useState([]); const [isDepositModalOpen, setIsDepositModalOpen] = useState(false); - // Fetch organization fiat balance + useEffect(() => { const fetchBalance = async () => { try { @@ -131,7 +131,7 @@ export default function FinancePage() { fetchBalance(); }, []); - // Fetch transactions + useEffect(() => { const controller = new AbortController(); diff --git a/src/app/(dashboard)/payroll/components/PayrollOverview.tsx b/src/app/(dashboard)/payroll/components/PayrollOverview.tsx index 62de500e..e200e09c 100644 --- a/src/app/(dashboard)/payroll/components/PayrollOverview.tsx +++ b/src/app/(dashboard)/payroll/components/PayrollOverview.tsx @@ -4,14 +4,14 @@ import React, { useState } from "react"; import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from "recharts"; import { AlertCircle, Calendar, Users, DollarSign, ChevronLeft, ChevronRight, ExternalLink } from "lucide-react"; -// Mock data for stat cards + const statsData = { totalMonthlyPayout: "₦2,450,000", totalEmployees: 24, nextPayoutDate: "May 15, 2026", }; -// Mock data for payout overview chart + const payoutChartData = [ { month: "Jan", payout: 1800000 }, { month: "Feb", payout: 2100000 }, @@ -21,7 +21,7 @@ const payoutChartData = [ { month: "Jun", payout: 2200000 }, ]; -// Mock data for payout schedule table + const payoutScheduleData = [ { id: "1", @@ -139,7 +139,7 @@ function getContractTypeBadge(type: string): string { } } -// Stat Card Component + interface StatCardProps { icon: React.ReactNode; label: string; @@ -168,7 +168,7 @@ function StatCard({ icon, label, value, iconBg }: StatCardProps) { ); } -// Urgent Action Banner Component + interface UrgentActionBannerProps { visible: boolean; } @@ -197,7 +197,7 @@ function UrgentActionBanner({ visible }: UrgentActionBannerProps) { ); } -// Payout Schedule Table Component + interface PayoutScheduleTableProps { data: typeof payoutScheduleData; } @@ -400,7 +400,7 @@ function PayoutScheduleTable({ data }: PayoutScheduleTableProps) { ); } -// Mobile Card View Component + interface MobilePayoutCardsProps { data: typeof payoutScheduleData; } @@ -485,9 +485,9 @@ function MobilePayoutCards({ data }: MobilePayoutCardsProps) { ); } -// Main Payroll Overview Component + export default function PayrollOverview() { - const [showPendingAction] = useState(true); // Set to false to hide banner + const [showPendingAction] = useState(true); return (
diff --git a/src/app/(dashboard)/profile-settings/2fa-setup/email-verify/page.tsx b/src/app/(dashboard)/profile-settings/2fa-setup/email-verify/page.tsx index c603d76c..ac1b90e2 100644 --- a/src/app/(dashboard)/profile-settings/2fa-setup/email-verify/page.tsx +++ b/src/app/(dashboard)/profile-settings/2fa-setup/email-verify/page.tsx @@ -48,7 +48,7 @@ export default function EmailVerificationPage() { } } - // Handle delete + if (e.key === "Delete" && verificationCode[index] && index < 5) { const newCode = [...verificationCode]; newCode[index] = ""; @@ -70,7 +70,7 @@ export default function EmailVerificationPage() { } setVerificationCode(newCode); - // Focus the next empty input or the last one + const nextIndex = Math.min(pastedData.length, 5); const nextInput = document.getElementById(`otp-${nextIndex}`); if (nextInput) { diff --git a/src/app/(dashboard)/profile-settings/notifications/page.tsx b/src/app/(dashboard)/profile-settings/notifications/page.tsx index cba93207..3b6bd10a 100644 --- a/src/app/(dashboard)/profile-settings/notifications/page.tsx +++ b/src/app/(dashboard)/profile-settings/notifications/page.tsx @@ -15,7 +15,7 @@ type SectionType = "employment" | "teamManagement"; export default function NotificationsPage() { const router = useRouter(); - // Employment section notifications + const [employmentNotifications, setEmploymentNotifications] = useState< NotificationSetting[] >([ @@ -39,7 +39,7 @@ export default function NotificationsPage() { }, ]); - // Team Management section notifications + const [teamManagementNotifications, setTeamManagementNotifications] = useState([ { diff --git a/src/app/(dashboard)/profile-settings/page.tsx b/src/app/(dashboard)/profile-settings/page.tsx index 80afd0ab..ce821c04 100644 --- a/src/app/(dashboard)/profile-settings/page.tsx +++ b/src/app/(dashboard)/profile-settings/page.tsx @@ -1,4 +1,4 @@ -// app/settings/page.tsx (or wherever your main file is) + "use client"; import { motion, AnimatePresence } from "framer-motion"; @@ -129,7 +129,7 @@ export default function Page() { ); } -// Settings Content Component + function Settings() { const router = useRouter(); const [profImage, setProfImage] = useState(null); @@ -141,7 +141,7 @@ function Settings() { const imageUrl = URL.createObjectURL(file); setProfImage(imageUrl); - // Simulate API call + console.log("Uploading image:", file.name); }; diff --git a/src/app/(dashboard)/profile-settings/preferences/page.tsx b/src/app/(dashboard)/profile-settings/preferences/page.tsx index 79fc4f13..817a7429 100644 --- a/src/app/(dashboard)/profile-settings/preferences/page.tsx +++ b/src/app/(dashboard)/profile-settings/preferences/page.tsx @@ -98,7 +98,7 @@ export default function PreferencesPage() { { code: "it", name: "Italian", nativeName: "Italiano", flag: "🇮🇹" }, ]; - // Function to generate random IP addresses + const generateIP = (seed: number) => { const baseIPs = [ `192.168.${seed}.${Math.floor(Math.random() * 255)}`, @@ -108,7 +108,7 @@ export default function PreferencesPage() { return baseIPs[seed % 3]; }; - // Function to generate exact date/times + const generateDateTime = (daysAgo: number) => { const date = new Date(); date.setDate(date.getDate() - daysAgo); diff --git a/src/app/(dashboard)/settings/address-book/page.tsx b/src/app/(dashboard)/settings/address-book/page.tsx index acad31fe..66c44a7f 100644 --- a/src/app/(dashboard)/settings/address-book/page.tsx +++ b/src/app/(dashboard)/settings/address-book/page.tsx @@ -18,7 +18,7 @@ export default function AddressBook() { const [isDeleting, setIsDeleting] = useState(false); const [addressToDelete, setAddressToDelete] = useState(null); - // Generate random mock data + const generateMockAddresses = (): CryptoAddress[] => { const cryptoTypes = [ { type: "BTC", name: "Bitcoin" }, @@ -46,7 +46,7 @@ export default function AddressBook() { }); }; - // Generate realistic wallet addresses based on crypto type + const generateWalletAddress = (cryptoType: string): string => { const prefixes: { [key: string]: string } = { BTC: "1", @@ -63,7 +63,7 @@ export default function AddressBook() { const chars = "0123456789abcdefABCDEF"; let address = prefix; - // Generate random characters after prefix + for (let i = 0; i < (cryptoType === "BTC" ? 33 : 39); i++) { address += chars.charAt(Math.floor(Math.random() * chars.length)); } @@ -71,7 +71,7 @@ export default function AddressBook() { return address; }; - // Load mock addresses on component mount + useEffect(() => { const mockAddresses = generateMockAddresses(); setAddresses(mockAddresses); @@ -94,10 +94,10 @@ export default function AddressBook() { setIsDeleting(true); try { - // Simulate API call + await new Promise((resolve) => setTimeout(resolve, 1000)); - // Delete address from localStorage + const updatedAddresses = addresses.filter( (address) => address.id !== addressToDelete ); @@ -106,7 +106,7 @@ export default function AddressBook() { console.log("Address deleted:", addressToDelete); - // Close modal + setShowDeleteModal(false); setAddressToDelete(null); } catch (error) { @@ -143,7 +143,7 @@ export default function AddressBook() { {/* Conditional rendering based on whether addresses exist */} {addresses.length === 0 ? ( - // Empty state +
) : ( - // Addresses list +
{addresses.map((address) => ( diff --git a/src/app/(dashboard)/settings/company/addresses/billing-address/page.tsx b/src/app/(dashboard)/settings/company/addresses/billing-address/page.tsx index eb492924..08d6bbd3 100644 --- a/src/app/(dashboard)/settings/company/addresses/billing-address/page.tsx +++ b/src/app/(dashboard)/settings/company/addresses/billing-address/page.tsx @@ -33,7 +33,7 @@ export default function BillingAddressPage() { const [error, setError] = useState(null); const [successMsg, setSuccessMsg] = useState(null); - // Pre-fill from API + useEffect(() => { OrganizationApi.getProfile() .then((profile) => { diff --git a/src/app/(dashboard)/settings/company/addresses/registered-address/page.tsx b/src/app/(dashboard)/settings/company/addresses/registered-address/page.tsx index ed5fb73f..e10e2bb2 100644 --- a/src/app/(dashboard)/settings/company/addresses/registered-address/page.tsx +++ b/src/app/(dashboard)/settings/company/addresses/registered-address/page.tsx @@ -33,7 +33,7 @@ export default function RegisteredAddressPage() { const [error, setError] = useState(null); const [successMsg, setSuccessMsg] = useState(null); - // Pre-fill from API + useEffect(() => { OrganizationApi.getProfile() .then((profile) => { diff --git a/src/app/(dashboard)/settings/company/company-info/page.tsx b/src/app/(dashboard)/settings/company/company-info/page.tsx index bb5f025d..a44ca753 100644 --- a/src/app/(dashboard)/settings/company/company-info/page.tsx +++ b/src/app/(dashboard)/settings/company/company-info/page.tsx @@ -46,7 +46,7 @@ export default function CompanyInfoPage() { const [error, setError] = useState(null); const [successMsg, setSuccessMsg] = useState(null); - // Pre-fill form from API + useEffect(() => { OrganizationApi.getProfile() .then((profile) => { diff --git a/src/app/(dashboard)/settings/hiring-templates/(components)/HiringTemplatePage.tsx b/src/app/(dashboard)/settings/hiring-templates/(components)/HiringTemplatePage.tsx index 75055fd7..8bc04abc 100644 --- a/src/app/(dashboard)/settings/hiring-templates/(components)/HiringTemplatePage.tsx +++ b/src/app/(dashboard)/settings/hiring-templates/(components)/HiringTemplatePage.tsx @@ -72,7 +72,7 @@ const HiringTemplatePage: React.FC = () => { setIsSubmitting(true); try { - // Simulate API call + const formData: FormData = { jobTitle, description, diff --git a/src/app/(dashboard)/settings/hiring-templates/edit/[id]/page.tsx b/src/app/(dashboard)/settings/hiring-templates/edit/[id]/page.tsx index b54508ba..9b88211c 100644 --- a/src/app/(dashboard)/settings/hiring-templates/edit/[id]/page.tsx +++ b/src/app/(dashboard)/settings/hiring-templates/edit/[id]/page.tsx @@ -55,7 +55,7 @@ export default function EditTemplatePage() { const [isDeleting, setIsDeleting] = useState(false); const editorRef = useRef(null); - // Load template data + useEffect(() => { const savedTemplates = localStorage.getItem("hiringTemplates"); console.log("localStorage contents:", savedTemplates); @@ -68,7 +68,7 @@ export default function EditTemplatePage() { if (savedTemplates) { templates = JSON.parse(savedTemplates); } else { - // Fallback to initial mock data + templates = [ { id: 1, @@ -115,7 +115,7 @@ export default function EditTemplatePage() { setIsLoading(false); }, [templateId, router]); - // Rich text editor functions + const execCommand = (command: string, value?: string): void => { document.execCommand(command, false, value || ""); editorRef.current?.focus(); @@ -158,10 +158,10 @@ export default function EditTemplatePage() { setIsSubmitting(true); try { - // Simulate API call + await new Promise((resolve) => setTimeout(resolve, 1000)); - // Update template in localStorage + const savedTemplates = localStorage.getItem("hiringTemplates"); if (savedTemplates) { const templates: Template[] = JSON.parse(savedTemplates); @@ -177,7 +177,7 @@ export default function EditTemplatePage() { console.log("Template updated:", formData); alert("Template updated successfully!"); - // Redirect back to templates list + router.push("/settings/"); } catch (error) { console.error("Error updating template:", error); @@ -207,10 +207,10 @@ export default function EditTemplatePage() { setIsDeleting(true); try { - // Simulate API call + await new Promise((resolve) => setTimeout(resolve, 1000)); - // Delete template from localStorage + const savedTemplates = localStorage.getItem("hiringTemplates"); if (savedTemplates) { const templates: Template[] = JSON.parse(savedTemplates); @@ -224,7 +224,7 @@ export default function EditTemplatePage() { console.log("Template deleted:", templateId); alert("Template deleted successfully!"); - // Redirect back to templates list + router.push("/settings/hiring-templates"); } catch (error) { console.error("Error deleting template:", error); @@ -297,7 +297,7 @@ export default function EditTemplatePage() {
) : ( - // The dropzone UI to show when no file is selected +
{ }; const handleSubmit = async () => { - // Basic client-side validation + if (!formData.startDate || !formData.endDate) { setSubmitStatus("error"); setSubmitMessage("Please select both a start date and an end date."); @@ -373,7 +373,7 @@ export const CreateTimeOffForm = ({ employees }: { employees: Employee[] }) => { setSubmitMessage(""); try { - // Map the UI's timeOffType to the API's leaveType enum + const leaveType = formData.timeOffType === "paid" ? "vacation" : "other"; await TeamService.submitTimeOff({ @@ -388,7 +388,7 @@ export const CreateTimeOffForm = ({ employees }: { employees: Employee[] }) => { setSubmitMessage("Your time-off request has been submitted and is pending approval."); success("Time off request created successfully!"); - // Reset form + setFormData({ employee: null, timeOffType: "paid", diff --git a/src/components/features/team-management/timeOff/SelectEmployeeModal.tsx b/src/components/features/team-management/timeOff/SelectEmployeeModal.tsx index bf4a8a32..00d41231 100644 --- a/src/components/features/team-management/timeOff/SelectEmployeeModal.tsx +++ b/src/components/features/team-management/timeOff/SelectEmployeeModal.tsx @@ -9,7 +9,7 @@ type SelectEmployeeModalProps = { onSelect: (employee: Employee) => void; }; -// A sub-component for rendering a single employee row + const EmployeeRow = ({ employee, onSelect, diff --git a/src/components/features/team-management/timeoff.tsx b/src/components/features/team-management/timeoff.tsx index 6eb7bd9a..c4b4bf13 100644 --- a/src/components/features/team-management/timeoff.tsx +++ b/src/components/features/team-management/timeoff.tsx @@ -52,11 +52,11 @@ function TeamMgtTimeoff() { const handleApprove = () => { if (!selectedTimeoff) return; - // Update the selected item + const updatedTimeoff = { ...selectedTimeoff, status: "Approved" as const }; setSelectedTimeoff(updatedTimeoff); - // Update in the list + setTimeoffs((prev) => prev.map((item) => item.id === selectedTimeoff.id ? updatedTimeoff : item @@ -67,11 +67,11 @@ function TeamMgtTimeoff() { const handleReject = (status: string, reason?: string) => { if (!selectedTimeoff) return; - // Update the selected item + const updatedTimeoff = { ...selectedTimeoff, status: "Rejected" as const }; setSelectedTimeoff(updatedTimeoff); - // Update in the list + setTimeoffs((prev) => prev.map((item) => item.id === selectedTimeoff.id ? updatedTimeoff : item diff --git a/src/components/layout/sidebar.tsx b/src/components/layout/sidebar.tsx index 96dee341..7d5a7aec 100644 --- a/src/components/layout/sidebar.tsx +++ b/src/components/layout/sidebar.tsx @@ -56,7 +56,7 @@ export default function Sidebar({ router.push("/login"); } catch (error) { console.error("Logout failed:", error); - // Fallback redirect even if API fails + router.push("/login"); } }; @@ -170,7 +170,7 @@ export default function Sidebar({
); - // Desktop sidebar + return ( <>