diff --git a/apps/web/next.config.mjs b/apps/web/next.config.mjs index 3b5073de..a5b9193f 100644 --- a/apps/web/next.config.mjs +++ b/apps/web/next.config.mjs @@ -4,7 +4,7 @@ import { fileURLToPath } from 'node:url' const jiti = createJiti(fileURLToPath(import.meta.url)) // Validate env during build. -jiti('./src/env') +jiti('./src/config/env') /** @type {import('next').NextConfig} */ const nextConfig = { diff --git a/apps/web/package.json b/apps/web/package.json index 626fbc9e..a876700d 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -19,6 +19,7 @@ "@radix-ui/react-icons": "^1.3.0", "@radix-ui/react-label": "^2.0.2", "@radix-ui/react-popover": "^1.0.7", + "@radix-ui/react-select": "^2.1.1", "@radix-ui/react-slot": "^1.0.2", "@radix-ui/react-switch": "^1.0.3", "@t3-oss/env-nextjs": "^0.10.1", @@ -29,7 +30,7 @@ "geist": "^1.3.0", "jiti": "^1.21.0", "lucide-react": "^0.378.0", - "next": "14.2.3", + "next": "14.2.5", "next-auth": "5.0.0-beta.17", "next-themes": "^0.3.0", "nextjs-toploader": "^1.6.12", @@ -38,7 +39,8 @@ "sonner": "^1.4.41", "tailwind-merge": "^2.3.0", "tailwindcss-animate": "^1.0.7", - "zod": "^3.23.8" + "zod": "^3.23.8", + "zsa": "^0.5.0" }, "devDependencies": { "@trivago/prettier-plugin-sort-imports": "^4.3.0", diff --git a/apps/web/pnpm-lock.yaml b/apps/web/pnpm-lock.yaml index 29bd768e..2b5c237e 100644 --- a/apps/web/pnpm-lock.yaml +++ b/apps/web/pnpm-lock.yaml @@ -13,7 +13,7 @@ importers: version: 2.0.0(@prisma/client@5.13.0(prisma@5.13.0)) '@next/third-parties': specifier: ^14.2.3 - version: 14.2.3(next@14.2.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1) + version: 14.2.3(next@14.2.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1) '@prisma/client': specifier: ^5.13.0 version: 5.13.0(prisma@5.13.0) @@ -38,6 +38,9 @@ importers: '@radix-ui/react-popover': specifier: ^1.0.7 version: 1.0.7(@types/react-dom@18.3.0)(@types/react@18.3.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-select': + specifier: ^2.1.1 + version: 2.1.1(@types/react-dom@18.3.0)(@types/react@18.3.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-slot': specifier: ^1.0.2 version: 1.0.2(@types/react@18.3.2)(react@18.3.1) @@ -61,7 +64,7 @@ importers: version: 1.0.0(@types/react-dom@18.3.0)(@types/react@18.3.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) geist: specifier: ^1.3.0 - version: 1.3.0(next@14.2.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1)) + version: 1.3.0(next@14.2.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)) jiti: specifier: ^1.21.0 version: 1.21.0 @@ -69,17 +72,17 @@ importers: specifier: ^0.378.0 version: 0.378.0(react@18.3.1) next: - specifier: 14.2.3 - version: 14.2.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + specifier: 14.2.5 + version: 14.2.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1) next-auth: specifier: 5.0.0-beta.17 - version: 5.0.0-beta.17(next@14.2.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1) + version: 5.0.0-beta.17(next@14.2.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1) next-themes: specifier: ^0.3.0 version: 0.3.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) nextjs-toploader: specifier: ^1.6.12 - version: 1.6.12(next@14.2.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 1.6.12(next@14.2.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: specifier: ^18.3.1 version: 18.3.1 @@ -98,6 +101,9 @@ importers: zod: specifier: ^3.23.8 version: 3.23.8 + zsa: + specifier: ^0.5.0 + version: 0.5.0(zod@3.23.8) devDependencies: '@trivago/prettier-plugin-sort-imports': specifier: ^4.3.0 @@ -301,62 +307,62 @@ packages: '@jridgewell/trace-mapping@0.3.25': resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} - '@next/env@14.2.3': - resolution: {integrity: sha512-W7fd7IbkfmeeY2gXrzJYDx8D2lWKbVoTIj1o1ScPHNzvp30s1AuoEFSdr39bC5sjxJaxTtq3OTCZboNp0lNWHA==} + '@next/env@14.2.5': + resolution: {integrity: sha512-/zZGkrTOsraVfYjGP8uM0p6r0BDT6xWpkjdVbcz66PJVSpwXX3yNiRycxAuDfBKGWBrZBXRuK/YVlkNgxHGwmA==} '@next/eslint-plugin-next@14.2.3': resolution: {integrity: sha512-L3oDricIIjgj1AVnRdRor21gI7mShlSwU/1ZGHmqM3LzHhXXhdkrfeNY5zif25Bi5Dd7fiJHsbhoZCHfXYvlAw==} - '@next/swc-darwin-arm64@14.2.3': - resolution: {integrity: sha512-3pEYo/RaGqPP0YzwnlmPN2puaF2WMLM3apt5jLW2fFdXD9+pqcoTzRk+iZsf8ta7+quAe4Q6Ms0nR0SFGFdS1A==} + '@next/swc-darwin-arm64@14.2.5': + resolution: {integrity: sha512-/9zVxJ+K9lrzSGli1///ujyRfon/ZneeZ+v4ptpiPoOU+GKZnm8Wj8ELWU1Pm7GHltYRBklmXMTUqM/DqQ99FQ==} engines: {node: '>= 10'} cpu: [arm64] os: [darwin] - '@next/swc-darwin-x64@14.2.3': - resolution: {integrity: sha512-6adp7waE6P1TYFSXpY366xwsOnEXM+y1kgRpjSRVI2CBDOcbRjsJ67Z6EgKIqWIue52d2q/Mx8g9MszARj8IEA==} + '@next/swc-darwin-x64@14.2.5': + resolution: {integrity: sha512-vXHOPCwfDe9qLDuq7U1OYM2wUY+KQ4Ex6ozwsKxp26BlJ6XXbHleOUldenM67JRyBfVjv371oneEvYd3H2gNSA==} engines: {node: '>= 10'} cpu: [x64] os: [darwin] - '@next/swc-linux-arm64-gnu@14.2.3': - resolution: {integrity: sha512-cuzCE/1G0ZSnTAHJPUT1rPgQx1w5tzSX7POXSLaS7w2nIUJUD+e25QoXD/hMfxbsT9rslEXugWypJMILBj/QsA==} + '@next/swc-linux-arm64-gnu@14.2.5': + resolution: {integrity: sha512-vlhB8wI+lj8q1ExFW8lbWutA4M2ZazQNvMWuEDqZcuJJc78iUnLdPPunBPX8rC4IgT6lIx/adB+Cwrl99MzNaA==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - '@next/swc-linux-arm64-musl@14.2.3': - resolution: {integrity: sha512-0D4/oMM2Y9Ta3nGuCcQN8jjJjmDPYpHX9OJzqk42NZGJocU2MqhBq5tWkJrUQOQY9N+In9xOdymzapM09GeiZw==} + '@next/swc-linux-arm64-musl@14.2.5': + resolution: {integrity: sha512-NpDB9NUR2t0hXzJJwQSGu1IAOYybsfeB+LxpGsXrRIb7QOrYmidJz3shzY8cM6+rO4Aojuef0N/PEaX18pi9OA==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - '@next/swc-linux-x64-gnu@14.2.3': - resolution: {integrity: sha512-ENPiNnBNDInBLyUU5ii8PMQh+4XLr4pG51tOp6aJ9xqFQ2iRI6IH0Ds2yJkAzNV1CfyagcyzPfROMViS2wOZ9w==} + '@next/swc-linux-x64-gnu@14.2.5': + resolution: {integrity: sha512-8XFikMSxWleYNryWIjiCX+gU201YS+erTUidKdyOVYi5qUQo/gRxv/3N1oZFCgqpesN6FPeqGM72Zve+nReVXQ==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - '@next/swc-linux-x64-musl@14.2.3': - resolution: {integrity: sha512-BTAbq0LnCbF5MtoM7I/9UeUu/8ZBY0i8SFjUMCbPDOLv+un67e2JgyN4pmgfXBwy/I+RHu8q+k+MCkDN6P9ViQ==} + '@next/swc-linux-x64-musl@14.2.5': + resolution: {integrity: sha512-6QLwi7RaYiQDcRDSU/os40r5o06b5ue7Jsk5JgdRBGGp8l37RZEh9JsLSM8QF0YDsgcosSeHjglgqi25+m04IQ==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - '@next/swc-win32-arm64-msvc@14.2.3': - resolution: {integrity: sha512-AEHIw/dhAMLNFJFJIJIyOFDzrzI5bAjI9J26gbO5xhAKHYTZ9Or04BesFPXiAYXDNdrwTP2dQceYA4dL1geu8A==} + '@next/swc-win32-arm64-msvc@14.2.5': + resolution: {integrity: sha512-1GpG2VhbspO+aYoMOQPQiqc/tG3LzmsdBH0LhnDS3JrtDx2QmzXe0B6mSZZiN3Bq7IOMXxv1nlsjzoS1+9mzZw==} engines: {node: '>= 10'} cpu: [arm64] os: [win32] - '@next/swc-win32-ia32-msvc@14.2.3': - resolution: {integrity: sha512-vga40n1q6aYb0CLrM+eEmisfKCR45ixQYXuBXxOOmmoV8sYST9k7E3US32FsY+CkkF7NtzdcebiFT4CHuMSyZw==} + '@next/swc-win32-ia32-msvc@14.2.5': + resolution: {integrity: sha512-Igh9ZlxwvCDsu6438FXlQTHlRno4gFpJzqPjSIBZooD22tKeI4fE/YMRoHVJHmrQ2P5YL1DoZ0qaOKkbeFWeMg==} engines: {node: '>= 10'} cpu: [ia32] os: [win32] - '@next/swc-win32-x64-msvc@14.2.3': - resolution: {integrity: sha512-Q1/zm43RWynxrO7lW4ehciQVj+5ePBhOK+/K2P7pLFX3JaJ/IZVC69SHidrmZSOkqz7ECIOhhy7XhAFG4JYyHA==} + '@next/swc-win32-x64-msvc@14.2.5': + resolution: {integrity: sha512-tEQ7oinq1/CjSG9uSTerca3v4AZ+dFa+4Yu6ihaG8Ud8ddqLQgFGcnwYls13H5X5CPDPZJdYxyeMui6muOLd4g==} engines: {node: '>= 10'} cpu: [x64] os: [win32] @@ -410,9 +416,15 @@ packages: '@prisma/get-platform@5.13.0': resolution: {integrity: sha512-B/WrQwYTzwr7qCLifQzYOmQhZcFmIFhR81xC45gweInSUn2hTEbfKUPd2keAog+y5WI5xLAFNJ3wkXplvSVkSw==} + '@radix-ui/number@1.1.0': + resolution: {integrity: sha512-V3gRzhVNU1ldS5XhAPTom1fOIo4ccrjjJgmE+LI2h/WaFpHmx0MQApT+KZHnx8abG6Avtfcz4WoEciMnpFT3HQ==} + '@radix-ui/primitive@1.0.1': resolution: {integrity: sha512-yQ8oGX2GVsEYMWGxcovu1uGWPCxV5BFfeeYxqPmuAzUyLT9qmaMXSAhXpb0WrspIeqYzdJpkh2vHModJPgRIaw==} + '@radix-ui/primitive@1.1.0': + resolution: {integrity: sha512-4Z8dn6Upk0qk4P74xBhZ6Hd/w0mPEzOOLxy4xiPXOXqjF7jZS0VAKk7/x/H6FyY2zCkYJqePf1G5KmkmNJ4RBA==} + '@radix-ui/react-arrow@1.0.3': resolution: {integrity: sha512-wSP+pHsB/jQRaL6voubsQ/ZlrGBHHrOjmBnr19hxYgtS0WvAFwZhK2WP/YY5yF9uKECCEEDGxuLxq1NBK51wFA==} peerDependencies: @@ -426,6 +438,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-arrow@1.1.0': + resolution: {integrity: sha512-FmlW1rCg7hBpEBwFbjHwCW6AmWLQM6g/v0Sn8XbP9NvmSZ2San1FpQeyPtufzOMSIx7Y4dzjlHoifhp+7NkZhw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-avatar@1.0.4': resolution: {integrity: sha512-kVK2K7ZD3wwj3qhle0ElXhOjbezIgyl2hVvgwfIdexL3rN6zJmy5AqqIf+D31lxVppdzV8CjAfZ6PklkmInZLw==} peerDependencies: @@ -465,6 +490,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-collection@1.1.0': + resolution: {integrity: sha512-GZsZslMJEyo1VKm5L1ZJY8tGDxZNPAoUeQUIbKeJfoi7Q4kmig5AsgLMYYuyYbfjd8fBmFORAIwYAkXMnXZgZw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-compose-refs@1.0.1': resolution: {integrity: sha512-fDSBgd44FKHa1FRMU59qBMPFcl2PZE+2nmqunj+BWFyYYjnhIDWL2ItDs3rrbJDQOtzt5nIebLCQc4QRfz6LJw==} peerDependencies: @@ -474,6 +512,15 @@ packages: '@types/react': optional: true + '@radix-ui/react-compose-refs@1.1.0': + resolution: {integrity: sha512-b4inOtiaOnYf9KWyO3jAeeCG6FeyfY6ldiEPanbUjWd+xIk5wZeHa8yVwmrJ2vderhu/BQvzCrJI0lHd+wIiqw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@radix-ui/react-context@1.0.1': resolution: {integrity: sha512-ebbrdFoYTcuZ0v4wG5tedGnp9tzcV8awzsxYph7gXUyvnNLuTIcCk1q17JEbnVhXAKG9oX3KtchwiMIAYp9NLg==} peerDependencies: @@ -483,6 +530,15 @@ packages: '@types/react': optional: true + '@radix-ui/react-context@1.1.0': + resolution: {integrity: sha512-OKrckBy+sMEgYM/sMmqmErVn0kZqrHPJze+Ql3DzYsDDp0hl0L62nx/2122/Bvps1qz645jlcu2tD9lrRSdf8A==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@radix-ui/react-dialog@1.0.5': resolution: {integrity: sha512-GjWJX/AUpB703eEBanuBnIWdIXg6NvJFCXcNlSZk4xdszCdhrJgBoUd1cGk67vFO+WdA2pfI/plOpqz/5GUP6Q==} peerDependencies: @@ -505,6 +561,15 @@ packages: '@types/react': optional: true + '@radix-ui/react-direction@1.1.0': + resolution: {integrity: sha512-BUuBvgThEiAXh2DWu93XsT+a3aWrGqolGlqqw5VU1kG7p/ZH2cuDlM1sRLNnY3QcBS69UIz2mcKhMxDsdewhjg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@radix-ui/react-dismissable-layer@1.0.5': resolution: {integrity: sha512-aJeDjQhywg9LBu2t/At58hCvr7pEm0o2Ke1x33B+MhjNmmZ17sy4KImo0KPLgsnc/zN7GPdce8Cnn0SWvwZO7g==} peerDependencies: @@ -518,6 +583,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-dismissable-layer@1.1.0': + resolution: {integrity: sha512-/UovfmmXGptwGcBQawLzvn2jOfM0t4z3/uKffoBlj724+n3FvBbZ7M0aaBOmkp6pqFYpO4yx8tSVJjx3Fl2jig==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-dropdown-menu@2.0.6': resolution: {integrity: sha512-i6TuFOoWmLWq+M/eCLGd/bQ2HfAX1RJgvrBQ6AQLmzfvsLdefxbWu8G9zczcPFfcSPehz9GcpF6K9QYreFV8hA==} peerDependencies: @@ -540,6 +618,15 @@ packages: '@types/react': optional: true + '@radix-ui/react-focus-guards@1.1.0': + resolution: {integrity: sha512-w6XZNUPVv6xCpZUqb/yN9DL6auvpGX3C/ee6Hdi16v2UUy25HV2Q5bcflsiDyT/g5RwbPQ/GIT1vLkeRb+ITBw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@radix-ui/react-focus-scope@1.0.4': resolution: {integrity: sha512-sL04Mgvf+FmyvZeYfNu1EPAaaxD+aw7cYeIB9L9Fvq8+urhltTRaEo5ysKOpHuKPclsZcSUMKlN05x4u+CINpA==} peerDependencies: @@ -553,6 +640,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-focus-scope@1.1.0': + resolution: {integrity: sha512-200UD8zylvEyL8Bx+z76RJnASR2gRMuxlgFCPAe/Q/679a/r0eK3MBVYMb7vZODZcffZBdob1EGnky78xmVvcA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-icons@1.3.0': resolution: {integrity: sha512-jQxj/0LKgp+j9BiTXz3O3sgs26RNet2iLWmsPyRz2SIcR4q/4SbazXfnYwbAr+vLYKSfc7qxzyGQA1HLlYiuNw==} peerDependencies: @@ -567,6 +667,15 @@ packages: '@types/react': optional: true + '@radix-ui/react-id@1.1.0': + resolution: {integrity: sha512-EJUrI8yYh7WOjNOqpoJaf1jlFIH2LvtgAl+YcFqNCa+4hj64ZXmPkAKOFs/ukjz3byN6bdb/AVUqHkI8/uWWMA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@radix-ui/react-label@2.0.2': resolution: {integrity: sha512-N5ehvlM7qoTLx7nWPodsPYPgMzA5WM8zZChQg8nyFJKnDO5WHdba1vv5/H6IO5LtJMfD2Q3wh1qHFGNtK0w3bQ==} peerDependencies: @@ -619,6 +728,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-popper@1.2.0': + resolution: {integrity: sha512-ZnRMshKF43aBxVWPWvbj21+7TQCvhuULWJ4gNIKYpRlQt5xGRhLx66tMp8pya2UkGHTSlhpXwmjqltDYHhw7Vg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-portal@1.0.4': resolution: {integrity: sha512-Qki+C/EuGUVCQTOTD5vzJzJuMUlewbzuKyUy+/iHM2uwGiru9gZeBJtHAPKAEkB5KWGi9mP/CHKcY0wt1aW45Q==} peerDependencies: @@ -632,6 +754,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-portal@1.1.1': + resolution: {integrity: sha512-A3UtLk85UtqhzFqtoC8Q0KvR2GbXF3mtPgACSazajqq6A41mEQgo53iPzY4i6BwDxlIFqWIhiQ2G729n+2aw/g==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-presence@1.0.1': resolution: {integrity: sha512-UXLW4UAbIY5ZjcvzjfRFo5gxva8QirC9hF7wRE4U5gz+TP0DbRk+//qyuAQ1McDxBt1xNMBTaciFGvEmJvAZCg==} peerDependencies: @@ -658,6 +793,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-primitive@2.0.0': + resolution: {integrity: sha512-ZSpFm0/uHa8zTvKBDjLFWLo8dkr4MBsiDLz0g3gMUwqgLHz9rTaRRGYDgvZPtBJgYCBKXkS9fzmoySgr8CO6Cw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-roving-focus@1.0.4': resolution: {integrity: sha512-2mUg5Mgcu001VkGy+FfzZyzbmuUWzgWkj3rvv4yu+mLw03+mTzbxZHvfcGyFp2b8EkQeMkpRQ5FiA2Vr2O6TeQ==} peerDependencies: @@ -671,6 +819,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-select@2.1.1': + resolution: {integrity: sha512-8iRDfyLtzxlprOo9IicnzvpsO1wNCkuwzzCM+Z5Rb5tNOpCdMvcc2AkzX0Fz+Tz9v6NJ5B/7EEgyZveo4FBRfQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-slot@1.0.2': resolution: {integrity: sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==} peerDependencies: @@ -680,6 +841,15 @@ packages: '@types/react': optional: true + '@radix-ui/react-slot@1.1.0': + resolution: {integrity: sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@radix-ui/react-switch@1.0.3': resolution: {integrity: sha512-mxm87F88HyHztsI7N+ZUmEoARGkC22YVW5CaC+Byc+HRpuvCrOBPTAnXgf+tZ/7i0Sg/eOePGdMhUKhPaQEqow==} peerDependencies: @@ -702,6 +872,15 @@ packages: '@types/react': optional: true + '@radix-ui/react-use-callback-ref@1.1.0': + resolution: {integrity: sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@radix-ui/react-use-controllable-state@1.0.1': resolution: {integrity: sha512-Svl5GY5FQeN758fWKrjM6Qb7asvXeiZltlT4U2gVfl8Gx5UAv2sMR0LWo8yhsIZh2oQ0eFdZ59aoOOMV7b47VA==} peerDependencies: @@ -711,6 +890,15 @@ packages: '@types/react': optional: true + '@radix-ui/react-use-controllable-state@1.1.0': + resolution: {integrity: sha512-MtfMVJiSr2NjzS0Aa90NPTnvTSg6C/JLCV7ma0W6+OMV78vd8OyRpID+Ng9LxzsPbLeuBnWBA1Nq30AtBIDChw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@radix-ui/react-use-escape-keydown@1.0.3': resolution: {integrity: sha512-vyL82j40hcFicA+M4Ex7hVkB9vHgSse1ZWomAqV2Je3RleKGO5iM8KMOEtfoSB0PnIelMd2lATjTGMYqN5ylTg==} peerDependencies: @@ -720,6 +908,15 @@ packages: '@types/react': optional: true + '@radix-ui/react-use-escape-keydown@1.1.0': + resolution: {integrity: sha512-L7vwWlR1kTTQ3oh7g1O0CBF3YCyyTj8NmhLR+phShpyA50HCfBFKVJTpshm9PzLiKmehsrQzTYTpX9HvmC9rhw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@radix-ui/react-use-layout-effect@1.0.1': resolution: {integrity: sha512-v/5RegiJWYdoCvMnITBkNNx6bCj20fiaJnWtRkU18yITptraXjffz5Qbn05uOiQnOvi+dbkznkoaMltz1GnszQ==} peerDependencies: @@ -729,6 +926,15 @@ packages: '@types/react': optional: true + '@radix-ui/react-use-layout-effect@1.1.0': + resolution: {integrity: sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@radix-ui/react-use-previous@1.0.1': resolution: {integrity: sha512-cV5La9DPwiQ7S0gf/0qiD6YgNqM5Fk97Kdrlc5yBcrF3jyEZQwm7vYFqMo4IfeHgJXsRaMvLABFtd0OVEmZhDw==} peerDependencies: @@ -738,6 +944,15 @@ packages: '@types/react': optional: true + '@radix-ui/react-use-previous@1.1.0': + resolution: {integrity: sha512-Z/e78qg2YFnnXcW88A4JmTtm4ADckLno6F7OXotmkQfeuCVaKuYzqAATPhVzl3delXE7CxIV8shofPn3jPc5Og==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@radix-ui/react-use-rect@1.0.1': resolution: {integrity: sha512-Cq5DLuSiuYVKNU8orzJMbl15TXilTnJKUCltMVQg53BQOF1/C5toAaGrowkgksdBQ9H+SRL23g0HDmg9tvmxXw==} peerDependencies: @@ -747,6 +962,15 @@ packages: '@types/react': optional: true + '@radix-ui/react-use-rect@1.1.0': + resolution: {integrity: sha512-0Fmkebhr6PiseyZlYAOtLS+nb7jLmpqTrJyv61Pe68MKYW6OWdRE2kI70TaYY27u7H0lajqM3hSMMLFq18Z7nQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@radix-ui/react-use-size@1.0.1': resolution: {integrity: sha512-ibay+VqrgcaI6veAojjofPATwledXiSmX+C0KrBk/xgpX9rBzPV3OsfwlhQdUOFbh+LKQorLYT+xTXW9V8yd0g==} peerDependencies: @@ -756,9 +980,34 @@ packages: '@types/react': optional: true + '@radix-ui/react-use-size@1.1.0': + resolution: {integrity: sha512-XW3/vWuIXHa+2Uwcc2ABSfcCledmXhhQPlGbfcRXbiUQI5Icjcg19BGCZVKKInYbvUCut/ufbbLLPFC5cbb1hw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-visually-hidden@1.1.0': + resolution: {integrity: sha512-N8MDZqtgCgG5S3aV60INAB475osJousYpZ4cTJ2cFbMpdHS5Y6loLTH8LPtkj2QN0x93J30HT/M3qJXM0+lyeQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/rect@1.0.1': resolution: {integrity: sha512-fyrgCaedtvMg9NK3en0pnOYJdtfwxUcNolezkNPUsoX57X8oQk+NkqcvzHXD2uKNij6GXmWU9NDru2IWjrO4BQ==} + '@radix-ui/rect@1.1.0': + resolution: {integrity: sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg==} + '@rushstack/eslint-patch@1.10.2': resolution: {integrity: sha512-hw437iINopmQuxWPSUEvqE56NCPsiU8N4AYtfHmJFckclktzK9YQJieD3XkDCDH4OjL+C7zgPUh73R/nrcHrqw==} @@ -1785,8 +2034,8 @@ packages: react: ^16.8 || ^17 || ^18 react-dom: ^16.8 || ^17 || ^18 - next@14.2.3: - resolution: {integrity: sha512-dowFkFTR8v79NPJO4QsBUtxv0g9BrS/phluVpMAt2ku7H+cbcBJlopXjkWlwxrk/xGqMemr7JkGPGemPrLLX7A==} + next@14.2.5: + resolution: {integrity: sha512-0f8aRfBVL+mpzfBjYfQuLWh2WyAwtJXCRfkPF4UJ5qd2YwrHczsrSzXU4tRMV0OAxR8ZJZWPFn6uhSC56UTsLA==} engines: {node: '>=18.17.0'} hasBin: true peerDependencies: @@ -2084,6 +2333,16 @@ packages: '@types/react': optional: true + react-remove-scroll@2.5.7: + resolution: {integrity: sha512-FnrTWO4L7/Bhhf3CYBNArEG/yROV0tKmTv7/3h9QCFvH6sndeFf1wPqOcbFVu5VAulS5dV1wGT3GZZ/1GawqiA==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + react-style-singleton@2.2.1: resolution: {integrity: sha512-ZWj0fHEMyWkHzKYUr2Bs/4zU6XLmq9HsgBURm7g5pAVfyn49DgUiNgY2d4lXRlYSiCif9YBGpQleewkcqddc7g==} engines: {node: '>=10'} @@ -2438,6 +2697,11 @@ packages: zod@3.23.8: resolution: {integrity: sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==} + zsa@0.5.0: + resolution: {integrity: sha512-SHh5cHjvi9uRnGhsHXUDYvYeSg8N7esgwrZVhIyGc5VR2UlI7S1mXsCGqO7nLIMOcDk4ULfB5SYLcmpai5auQw==} + peerDependencies: + zod: ^3.23.5 + snapshots: '@alloc/quick-lru@5.2.0': {} @@ -2623,42 +2887,42 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.4.15 - '@next/env@14.2.3': {} + '@next/env@14.2.5': {} '@next/eslint-plugin-next@14.2.3': dependencies: glob: 10.3.10 - '@next/swc-darwin-arm64@14.2.3': + '@next/swc-darwin-arm64@14.2.5': optional: true - '@next/swc-darwin-x64@14.2.3': + '@next/swc-darwin-x64@14.2.5': optional: true - '@next/swc-linux-arm64-gnu@14.2.3': + '@next/swc-linux-arm64-gnu@14.2.5': optional: true - '@next/swc-linux-arm64-musl@14.2.3': + '@next/swc-linux-arm64-musl@14.2.5': optional: true - '@next/swc-linux-x64-gnu@14.2.3': + '@next/swc-linux-x64-gnu@14.2.5': optional: true - '@next/swc-linux-x64-musl@14.2.3': + '@next/swc-linux-x64-musl@14.2.5': optional: true - '@next/swc-win32-arm64-msvc@14.2.3': + '@next/swc-win32-arm64-msvc@14.2.5': optional: true - '@next/swc-win32-ia32-msvc@14.2.3': + '@next/swc-win32-ia32-msvc@14.2.5': optional: true - '@next/swc-win32-x64-msvc@14.2.3': + '@next/swc-win32-x64-msvc@14.2.5': optional: true - '@next/third-parties@14.2.3(next@14.2.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)': + '@next/third-parties@14.2.3(next@14.2.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)': dependencies: - next: 14.2.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + next: 14.2.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: 18.3.1 third-party-capital: 1.0.20 @@ -2704,10 +2968,14 @@ snapshots: dependencies: '@prisma/debug': 5.13.0 + '@radix-ui/number@1.1.0': {} + '@radix-ui/primitive@1.0.1': dependencies: '@babel/runtime': 7.24.5 + '@radix-ui/primitive@1.1.0': {} + '@radix-ui/react-arrow@1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@babel/runtime': 7.24.5 @@ -2718,6 +2986,15 @@ snapshots: '@types/react': 18.3.2 '@types/react-dom': 18.3.0 + '@radix-ui/react-arrow@1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.2 + '@types/react-dom': 18.3.0 + '@radix-ui/react-avatar@1.0.4(@types/react-dom@18.3.0)(@types/react@18.3.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@babel/runtime': 7.24.5 @@ -2761,6 +3038,18 @@ snapshots: '@types/react': 18.3.2 '@types/react-dom': 18.3.0 + '@radix-ui/react-collection@1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.2)(react@18.3.1) + '@radix-ui/react-context': 1.1.0(@types/react@18.3.2)(react@18.3.1) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': 1.1.0(@types/react@18.3.2)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.2 + '@types/react-dom': 18.3.0 + '@radix-ui/react-compose-refs@1.0.1(@types/react@18.3.2)(react@18.3.1)': dependencies: '@babel/runtime': 7.24.5 @@ -2768,6 +3057,12 @@ snapshots: optionalDependencies: '@types/react': 18.3.2 + '@radix-ui/react-compose-refs@1.1.0(@types/react@18.3.2)(react@18.3.1)': + dependencies: + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.2 + '@radix-ui/react-context@1.0.1(@types/react@18.3.2)(react@18.3.1)': dependencies: '@babel/runtime': 7.24.5 @@ -2775,6 +3070,12 @@ snapshots: optionalDependencies: '@types/react': 18.3.2 + '@radix-ui/react-context@1.1.0(@types/react@18.3.2)(react@18.3.1)': + dependencies: + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.2 + '@radix-ui/react-dialog@1.0.5(@types/react-dom@18.3.0)(@types/react@18.3.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@babel/runtime': 7.24.5 @@ -2805,6 +3106,12 @@ snapshots: optionalDependencies: '@types/react': 18.3.2 + '@radix-ui/react-direction@1.1.0(@types/react@18.3.2)(react@18.3.1)': + dependencies: + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.2 + '@radix-ui/react-dismissable-layer@1.0.5(@types/react-dom@18.3.0)(@types/react@18.3.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@babel/runtime': 7.24.5 @@ -2819,6 +3126,19 @@ snapshots: '@types/react': 18.3.2 '@types/react-dom': 18.3.0 + '@radix-ui/react-dismissable-layer@1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.0 + '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.2)(react@18.3.1) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.2)(react@18.3.1) + '@radix-ui/react-use-escape-keydown': 1.1.0(@types/react@18.3.2)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.2 + '@types/react-dom': 18.3.0 + '@radix-ui/react-dropdown-menu@2.0.6(@types/react-dom@18.3.0)(@types/react@18.3.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@babel/runtime': 7.24.5 @@ -2842,6 +3162,12 @@ snapshots: optionalDependencies: '@types/react': 18.3.2 + '@radix-ui/react-focus-guards@1.1.0(@types/react@18.3.2)(react@18.3.1)': + dependencies: + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.2 + '@radix-ui/react-focus-scope@1.0.4(@types/react-dom@18.3.0)(@types/react@18.3.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@babel/runtime': 7.24.5 @@ -2854,6 +3180,17 @@ snapshots: '@types/react': 18.3.2 '@types/react-dom': 18.3.0 + '@radix-ui/react-focus-scope@1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.2)(react@18.3.1) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.2)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.2 + '@types/react-dom': 18.3.0 + '@radix-ui/react-icons@1.3.0(react@18.3.1)': dependencies: react: 18.3.1 @@ -2866,6 +3203,13 @@ snapshots: optionalDependencies: '@types/react': 18.3.2 + '@radix-ui/react-id@1.1.0(@types/react@18.3.2)(react@18.3.1)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.2)(react@18.3.1) + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.2 + '@radix-ui/react-label@2.0.2(@types/react-dom@18.3.0)(@types/react@18.3.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@babel/runtime': 7.24.5 @@ -2946,6 +3290,24 @@ snapshots: '@types/react': 18.3.2 '@types/react-dom': 18.3.0 + '@radix-ui/react-popper@1.2.0(@types/react-dom@18.3.0)(@types/react@18.3.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@floating-ui/react-dom': 2.0.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-arrow': 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.2)(react@18.3.1) + '@radix-ui/react-context': 1.1.0(@types/react@18.3.2)(react@18.3.1) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.2)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.2)(react@18.3.1) + '@radix-ui/react-use-rect': 1.1.0(@types/react@18.3.2)(react@18.3.1) + '@radix-ui/react-use-size': 1.1.0(@types/react@18.3.2)(react@18.3.1) + '@radix-ui/rect': 1.1.0 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.2 + '@types/react-dom': 18.3.0 + '@radix-ui/react-portal@1.0.4(@types/react-dom@18.3.0)(@types/react@18.3.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@babel/runtime': 7.24.5 @@ -2956,6 +3318,16 @@ snapshots: '@types/react': 18.3.2 '@types/react-dom': 18.3.0 + '@radix-ui/react-portal@1.1.1(@types/react-dom@18.3.0)(@types/react@18.3.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.2)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.2 + '@types/react-dom': 18.3.0 + '@radix-ui/react-presence@1.0.1(@types/react-dom@18.3.0)(@types/react@18.3.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@babel/runtime': 7.24.5 @@ -2977,6 +3349,15 @@ snapshots: '@types/react': 18.3.2 '@types/react-dom': 18.3.0 + '@radix-ui/react-primitive@2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-slot': 1.1.0(@types/react@18.3.2)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.2 + '@types/react-dom': 18.3.0 + '@radix-ui/react-roving-focus@1.0.4(@types/react-dom@18.3.0)(@types/react@18.3.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@babel/runtime': 7.24.5 @@ -2995,6 +3376,35 @@ snapshots: '@types/react': 18.3.2 '@types/react-dom': 18.3.0 + '@radix-ui/react-select@2.1.1(@types/react-dom@18.3.0)(@types/react@18.3.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/number': 1.1.0 + '@radix-ui/primitive': 1.1.0 + '@radix-ui/react-collection': 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.2)(react@18.3.1) + '@radix-ui/react-context': 1.1.0(@types/react@18.3.2)(react@18.3.1) + '@radix-ui/react-direction': 1.1.0(@types/react@18.3.2)(react@18.3.1) + '@radix-ui/react-dismissable-layer': 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-focus-guards': 1.1.0(@types/react@18.3.2)(react@18.3.1) + '@radix-ui/react-focus-scope': 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-id': 1.1.0(@types/react@18.3.2)(react@18.3.1) + '@radix-ui/react-popper': 1.2.0(@types/react-dom@18.3.0)(@types/react@18.3.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-portal': 1.1.1(@types/react-dom@18.3.0)(@types/react@18.3.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': 1.1.0(@types/react@18.3.2)(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.2)(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.2)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.2)(react@18.3.1) + '@radix-ui/react-use-previous': 1.1.0(@types/react@18.3.2)(react@18.3.1) + '@radix-ui/react-visually-hidden': 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + aria-hidden: 1.2.4 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-remove-scroll: 2.5.7(@types/react@18.3.2)(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.2 + '@types/react-dom': 18.3.0 + '@radix-ui/react-slot@1.0.2(@types/react@18.3.2)(react@18.3.1)': dependencies: '@babel/runtime': 7.24.5 @@ -3003,6 +3413,13 @@ snapshots: optionalDependencies: '@types/react': 18.3.2 + '@radix-ui/react-slot@1.1.0(@types/react@18.3.2)(react@18.3.1)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.2)(react@18.3.1) + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.2 + '@radix-ui/react-switch@1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@babel/runtime': 7.24.5 @@ -3026,6 +3443,12 @@ snapshots: optionalDependencies: '@types/react': 18.3.2 + '@radix-ui/react-use-callback-ref@1.1.0(@types/react@18.3.2)(react@18.3.1)': + dependencies: + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.2 + '@radix-ui/react-use-controllable-state@1.0.1(@types/react@18.3.2)(react@18.3.1)': dependencies: '@babel/runtime': 7.24.5 @@ -3034,6 +3457,13 @@ snapshots: optionalDependencies: '@types/react': 18.3.2 + '@radix-ui/react-use-controllable-state@1.1.0(@types/react@18.3.2)(react@18.3.1)': + dependencies: + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.2)(react@18.3.1) + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.2 + '@radix-ui/react-use-escape-keydown@1.0.3(@types/react@18.3.2)(react@18.3.1)': dependencies: '@babel/runtime': 7.24.5 @@ -3042,6 +3472,13 @@ snapshots: optionalDependencies: '@types/react': 18.3.2 + '@radix-ui/react-use-escape-keydown@1.1.0(@types/react@18.3.2)(react@18.3.1)': + dependencies: + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.2)(react@18.3.1) + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.2 + '@radix-ui/react-use-layout-effect@1.0.1(@types/react@18.3.2)(react@18.3.1)': dependencies: '@babel/runtime': 7.24.5 @@ -3049,6 +3486,12 @@ snapshots: optionalDependencies: '@types/react': 18.3.2 + '@radix-ui/react-use-layout-effect@1.1.0(@types/react@18.3.2)(react@18.3.1)': + dependencies: + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.2 + '@radix-ui/react-use-previous@1.0.1(@types/react@18.3.2)(react@18.3.1)': dependencies: '@babel/runtime': 7.24.5 @@ -3056,6 +3499,12 @@ snapshots: optionalDependencies: '@types/react': 18.3.2 + '@radix-ui/react-use-previous@1.1.0(@types/react@18.3.2)(react@18.3.1)': + dependencies: + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.2 + '@radix-ui/react-use-rect@1.0.1(@types/react@18.3.2)(react@18.3.1)': dependencies: '@babel/runtime': 7.24.5 @@ -3064,6 +3513,13 @@ snapshots: optionalDependencies: '@types/react': 18.3.2 + '@radix-ui/react-use-rect@1.1.0(@types/react@18.3.2)(react@18.3.1)': + dependencies: + '@radix-ui/rect': 1.1.0 + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.2 + '@radix-ui/react-use-size@1.0.1(@types/react@18.3.2)(react@18.3.1)': dependencies: '@babel/runtime': 7.24.5 @@ -3072,10 +3528,28 @@ snapshots: optionalDependencies: '@types/react': 18.3.2 + '@radix-ui/react-use-size@1.1.0(@types/react@18.3.2)(react@18.3.1)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.2)(react@18.3.1) + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.2 + + '@radix-ui/react-visually-hidden@1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.2 + '@types/react-dom': 18.3.0 + '@radix-ui/rect@1.0.1': dependencies: '@babel/runtime': 7.24.5 + '@radix-ui/rect@1.1.0': {} + '@rushstack/eslint-patch@1.10.2': {} '@swc/counter@0.1.3': {} @@ -3633,8 +4107,8 @@ snapshots: '@typescript-eslint/parser': 7.2.0(eslint@8.57.0)(typescript@5.4.5) eslint: 8.57.0 eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.57.0) - eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.9.0(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0) + eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.9.0(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0))(eslint@8.57.0) + eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0) eslint-plugin-jsx-a11y: 6.8.0(eslint@8.57.0) eslint-plugin-react: 7.34.1(eslint@8.57.0) eslint-plugin-react-hooks: 4.6.2(eslint@8.57.0) @@ -3656,13 +4130,13 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.57.0): + eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.9.0(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0))(eslint@8.57.0): dependencies: debug: 4.3.4 enhanced-resolve: 5.16.1 eslint: 8.57.0 - eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.57.0))(eslint@8.57.0) - eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.9.0(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0) + eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.9.0(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0) + eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0) fast-glob: 3.3.2 get-tsconfig: 4.7.5 is-core-module: 2.13.1 @@ -3673,28 +4147,18 @@ snapshots: - eslint-import-resolver-webpack - supports-color - eslint-module-utils@2.8.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.57.0))(eslint@8.57.0): + eslint-module-utils@2.8.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.9.0(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0): dependencies: debug: 3.2.7 optionalDependencies: '@typescript-eslint/parser': 7.2.0(eslint@8.57.0)(typescript@5.4.5) eslint: 8.57.0 eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.57.0) + eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.9.0(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0))(eslint@8.57.0) transitivePeerDependencies: - supports-color - eslint-module-utils@2.8.1(@typescript-eslint/parser@7.9.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint@8.57.0): - dependencies: - debug: 3.2.7 - optionalDependencies: - '@typescript-eslint/parser': 7.9.0(eslint@8.57.0)(typescript@5.4.5) - eslint: 8.57.0 - eslint-import-resolver-node: 0.3.9 - transitivePeerDependencies: - - supports-color - - eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.9.0(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0): + eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0): dependencies: array-includes: 3.1.8 array.prototype.findlastindex: 1.2.5 @@ -3704,7 +4168,7 @@ snapshots: doctrine: 2.1.0 eslint: 8.57.0 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.9.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint@8.57.0) + eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.9.0(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0) hasown: 2.0.2 is-core-module: 2.13.1 is-glob: 4.0.3 @@ -3715,7 +4179,7 @@ snapshots: semver: 6.3.1 tsconfig-paths: 3.15.0 optionalDependencies: - '@typescript-eslint/parser': 7.9.0(eslint@8.57.0)(typescript@5.4.5) + '@typescript-eslint/parser': 7.2.0(eslint@8.57.0)(typescript@5.4.5) transitivePeerDependencies: - eslint-import-resolver-typescript - eslint-import-resolver-webpack @@ -3905,9 +4369,9 @@ snapshots: functions-have-names@1.2.3: {} - geist@1.3.0(next@14.2.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1)): + geist@1.3.0(next@14.2.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)): dependencies: - next: 14.2.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + next: 14.2.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1) get-intrinsic@1.2.4: dependencies: @@ -4265,10 +4729,10 @@ snapshots: natural-compare@1.4.0: {} - next-auth@5.0.0-beta.17(next@14.2.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1): + next-auth@5.0.0-beta.17(next@14.2.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1): dependencies: '@auth/core': 0.30.0 - next: 14.2.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + next: 14.2.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: 18.3.1 next-themes@0.3.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1): @@ -4276,9 +4740,9 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - next@14.2.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + next@14.2.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: - '@next/env': 14.2.3 + '@next/env': 14.2.5 '@swc/helpers': 0.5.5 busboy: 1.6.0 caniuse-lite: 1.0.30001618 @@ -4288,22 +4752,22 @@ snapshots: react-dom: 18.3.1(react@18.3.1) styled-jsx: 5.1.1(react@18.3.1) optionalDependencies: - '@next/swc-darwin-arm64': 14.2.3 - '@next/swc-darwin-x64': 14.2.3 - '@next/swc-linux-arm64-gnu': 14.2.3 - '@next/swc-linux-arm64-musl': 14.2.3 - '@next/swc-linux-x64-gnu': 14.2.3 - '@next/swc-linux-x64-musl': 14.2.3 - '@next/swc-win32-arm64-msvc': 14.2.3 - '@next/swc-win32-ia32-msvc': 14.2.3 - '@next/swc-win32-x64-msvc': 14.2.3 + '@next/swc-darwin-arm64': 14.2.5 + '@next/swc-darwin-x64': 14.2.5 + '@next/swc-linux-arm64-gnu': 14.2.5 + '@next/swc-linux-arm64-musl': 14.2.5 + '@next/swc-linux-x64-gnu': 14.2.5 + '@next/swc-linux-x64-musl': 14.2.5 + '@next/swc-win32-arm64-msvc': 14.2.5 + '@next/swc-win32-ia32-msvc': 14.2.5 + '@next/swc-win32-x64-msvc': 14.2.5 transitivePeerDependencies: - '@babel/core' - babel-plugin-macros - nextjs-toploader@1.6.12(next@14.2.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + nextjs-toploader@1.6.12(next@14.2.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: - next: 14.2.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + next: 14.2.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1) nprogress: 0.2.0 prop-types: 15.8.1 react: 18.3.1 @@ -4519,6 +4983,17 @@ snapshots: optionalDependencies: '@types/react': 18.3.2 + react-remove-scroll@2.5.7(@types/react@18.3.2)(react@18.3.1): + dependencies: + react: 18.3.1 + react-remove-scroll-bar: 2.3.6(@types/react@18.3.2)(react@18.3.1) + react-style-singleton: 2.2.1(@types/react@18.3.2)(react@18.3.1) + tslib: 2.6.2 + use-callback-ref: 1.3.2(@types/react@18.3.2)(react@18.3.1) + use-sidecar: 1.1.2(@types/react@18.3.2)(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.2 + react-style-singleton@2.2.1(@types/react@18.3.2)(react@18.3.1): dependencies: get-nonce: 1.0.1 @@ -4936,3 +5411,7 @@ snapshots: yocto-queue@0.1.0: {} zod@3.23.8: {} + + zsa@0.5.0(zod@3.23.8): + dependencies: + zod: 3.23.8 diff --git a/apps/web/src/app/(auth)/signin/page.tsx b/apps/web/src/app/(auth)/signin/page.tsx index a28f3b2e..f4b3dd69 100644 --- a/apps/web/src/app/(auth)/signin/page.tsx +++ b/apps/web/src/app/(auth)/signin/page.tsx @@ -2,32 +2,45 @@ import type { Metadata } from 'next' import Link from 'next/link' import { redirect } from 'next/navigation' -import { ArrowLeft } from 'lucide-react' +import { ArrowLeft, TriangleAlertIcon } from 'lucide-react' -import { SignInError } from '@/components/pages/signin/signin-error' -import { SignInForm } from '@/components/pages/signin/signin-form' +import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert' import { Button } from '@/components/ui/button' -import { auth } from '@/lib/auth' +import { useSession } from '@/hooks/use-session' + +import { SignInForm } from './signin-form' export const metadata: Metadata = { title: 'Sign in', } +type Error = 'OAuthAccountNotLinked' +function getErrorMessage(error: Error) { + switch (error) { + case 'OAuthAccountNotLinked': + return 'The account is already associated with another user.' + default: + return 'Something went wrong!' + } +} + interface Props { searchParams: { - error: string + error: Error } } export default async function Page({ searchParams }: Props) { - const session = await auth() + const session = await useSession() // show error if user received an error while linking an account if (session && !searchParams.error) { - redirect('/dashboard') + throw redirect('/dashboard') } + const errorMessage = getErrorMessage(searchParams.error) + return (
+
{session ? (

@@ -52,7 +66,14 @@ export default async function Page({ searchParams }: Props) { )} - + + {searchParams.error && ( + + + Error + {errorMessage} + + )}

) diff --git a/apps/web/src/app/(auth)/signin/signin-button.tsx b/apps/web/src/app/(auth)/signin/signin-button.tsx new file mode 100644 index 00000000..fb80e736 --- /dev/null +++ b/apps/web/src/app/(auth)/signin/signin-button.tsx @@ -0,0 +1,40 @@ +'use client' + +import { useTransition } from 'react' + +import { Button } from '@/components/ui/button' +import { DiscordIcon, LoaderIcon, TwitchIcon } from '@/components/ui/icons' + +import { signInWithProvider } from '@/services/actions/auth' + +interface Props { + provider: 'twitch' | 'discord' + isDisabled?: boolean +} + +export function SignInButton({ provider, isDisabled = false }: Props) { + const [pending, startTrantision] = useTransition() + + return ( + + ) +} diff --git a/apps/web/src/app/(auth)/signin/signin-form.tsx b/apps/web/src/app/(auth)/signin/signin-form.tsx new file mode 100644 index 00000000..a3e8d5cb --- /dev/null +++ b/apps/web/src/app/(auth)/signin/signin-form.tsx @@ -0,0 +1,69 @@ +'use client' + +import { useState } from 'react' + +import { Checkbox } from '@/components/ui/checkbox' +import { Label } from '@/components/ui/label' +import { Link } from '@/components/ui/link' + +import { SignInButton } from './signin-button' + +const legalLinks = [ + { + label: 'Privacy Policy', + href: '/privacy-policy', + }, + { + label: 'Terms of Service', + href: '/terms-of-service', + }, + { + label: 'Cookie Policy', + href: '/cookie-policy', + }, + { + label: 'EULA', + href: '/eula', + }, +] + +export function SignInForm() { + const [checked, setChecked] = useState(false) + + return ( +
+
+ + +
+
+ setChecked((prev) => !prev)} + /> +
+ +

+ You agree to our{' '} + {legalLinks.map((item) => ( + + + {item.label} + + + ))} +

+
+
+
+ ) +} diff --git a/apps/web/src/app/(landing)/page.tsx b/apps/web/src/app/(landing)/page.tsx index 578cbd7f..8158a42a 100644 --- a/apps/web/src/app/(landing)/page.tsx +++ b/apps/web/src/app/(landing)/page.tsx @@ -1,15 +1,275 @@ -import { Features } from '@/components/pages/landing/features' -import { Footer } from '@/components/pages/landing/footer' -import { Header } from '@/components/pages/landing/header' -import { Hero } from '@/components/pages/landing/hero' +import Image from 'next/image' +import NextLink from 'next/link' + +import { CheckCircleIcon, XIcon } from 'lucide-react' + +import { ThemeToggle } from '@/components/theme-toggle' +import { Button } from '@/components/ui/button' +import { GitHubIcon } from '@/components/ui/icons' +import { Link } from '@/components/ui/link' + +import { ThemedImage } from './themed-image' + +const navLinks = [ + { + label: 'Documentation', + path: '/docs', + options: { + target: '_blank', + rel: 'noreferrer', + }, + }, +] + +const productLinks = [ + { + label: 'Dashboard', + href: '/dashboard', + }, + { + label: 'Documentation', + href: '/docs', + options: { + target: '_blank', + rel: 'noreferrer', + }, + }, +] + +const legalLinks = [ + { + label: 'Terms of Service', + href: '/terms-of-service', + }, + { + label: 'Privacy Policy', + href: '/privacy-policy', + }, + { + label: 'Cookie Policy', + href: '/cookie-policy', + }, + { + label: 'EULA', + href: '/eula', + }, +] + +const socialLinks = [ + { + label: 'Twitter', + href: 'https://x.com/senchabot', + }, + { + label: 'Discord', + href: '/discord', + }, + { + label: 'GitHub', + href: '/github', + }, + { + label: 'LinkedIn', + href: 'https://www.linkedin.com/company/senchabot/', + }, +] export default function Page() { return (
-
- - -
+ {/* header */} +
+ +
+ + {/* hero */} +
+
+
+

+ Manage Your Community from One Place +

+

+ Multi-platform bot designed for seamless integration with Twitch + and Discord. +

+
+ +

+ We're open source on + + + GitHub + +

+
+
+
+ +
+
+
+ + {/* features */} +
+
+
    +
  • + +

    + Download required +

    +
  • +
  • + +

    Free

    +
  • +
  • + +

    Open-source

    +
  • +
+
+
+ + {/* footer */} +
+
+
+
+
+ Senchabot +
+ Senchabot +
+

+ Manage your Discord and Twitch community with an open-source + multi-platform bot. +

+
+
+
+ + Product + +
    + {productLinks.map((item) => ( +
  • + + {item.label} + +
  • + ))} +
+
+
+ + Legal + +
    + {legalLinks.map((item) => ( +
  • + + {item.label} + +
  • + ))} +
+
+
+ + Social + +
    + {socialLinks.map((item) => ( +
  • + + {item.label} + +
  • + ))} +
+
+
+
+
+
+

© {new Date().getFullYear()} Senchabot. All Rights Reserved.

+

+ Made with from the + community. +

+
+
+
) } diff --git a/apps/web/src/components/pages/landing/themed-image.tsx b/apps/web/src/app/(landing)/themed-image.tsx similarity index 100% rename from apps/web/src/components/pages/landing/themed-image.tsx rename to apps/web/src/app/(landing)/themed-image.tsx diff --git a/apps/web/src/app/(legal)/layout.tsx b/apps/web/src/app/(legal)/layout.tsx index 806115c2..72b9c13d 100644 --- a/apps/web/src/app/(legal)/layout.tsx +++ b/apps/web/src/app/(legal)/layout.tsx @@ -1,6 +1,3 @@ -import { Footer } from '@/components/pages/landing/footer' -import { Header } from '@/components/pages/landing/header' - interface Props { children: React.ReactNode } @@ -8,11 +5,9 @@ interface Props { export default function LegalLayout({ children }: Props) { return (
-
-
+
{children}
-
) } diff --git a/apps/web/src/app/(protected)/dashboard/[platform]/[id]/commands/@tabs/command-status-switch.tsx b/apps/web/src/app/(protected)/dashboard/[platform]/[id]/commands/@tabs/command-status-switch.tsx new file mode 100644 index 00000000..d890b166 --- /dev/null +++ b/apps/web/src/app/(protected)/dashboard/[platform]/[id]/commands/@tabs/command-status-switch.tsx @@ -0,0 +1,46 @@ +'use client' + +import { useTransition } from 'react' + +import { toast } from 'sonner' + +import { Switch } from '@/components/ui/switch' + +import { setCommandStatus } from '@/services/actions/commands' + +import type { EntityCommand } from '@/types/command' + +interface Props { + command: EntityCommand +} + +export function CommandStatusSwitch({ command }: Props) { + const [pending, startTransition] = useTransition() + + return ( +
+ { + startTransition(async () => { + const [, error] = await setCommandStatus({ + id: command.id, + platform: command.platform, + platformEntityId: command.platform_entity_id, + status: checked, + }) + + if (error) { + toast.error(error.message) + return + } + + toast.success('Successfully updated!') + }) + }} + /> +
+ ) +} diff --git a/apps/web/src/components/pages/commands/commands.tsx b/apps/web/src/app/(protected)/dashboard/[platform]/[id]/commands/@tabs/commands-list.tsx similarity index 74% rename from apps/web/src/components/pages/commands/commands.tsx rename to apps/web/src/app/(protected)/dashboard/[platform]/[id]/commands/@tabs/commands-list.tsx index ff180d27..529f19df 100644 --- a/apps/web/src/components/pages/commands/commands.tsx +++ b/apps/web/src/app/(protected)/dashboard/[platform]/[id]/commands/@tabs/commands-list.tsx @@ -7,20 +7,22 @@ import { TableRow, } from '@/components/ui/table' -import { getEntityCommands } from '@/data-layer/queries/entity' +import { getCommands } from '@/services/queries/commands' -import { DeleteCommand } from './delete-command' -import { UpdateCommand } from './update-command' -import { UpdateCommandStatus } from './update-command-status' +import type { Platform } from '@/types/platform' + +import { CommandStatusSwitch } from './command-status-switch' +import { DeleteCommand } from './delete-command-button' +import { UpdateCommand } from './update-command-dialog' interface Props { platform: Platform id: string - type: CommandType + type: 'custom' | 'global' } -export async function Commands({ platform, id, type }: Props) { - const commands = await getEntityCommands(platform, id, type) +export async function CommandsList({ platform, id, type }: Props) { + const commands = await getCommands(platform, id, type) if (!commands.length) { return

No command found.

@@ -42,9 +44,9 @@ export async function Commands({ platform, id, type }: Props) { {commands.map((item) => ( - + - !{item.name} + {item.name}

{item.content} @@ -69,7 +71,7 @@ export async function Commands({ platform, id, type }: Props) { } else if (type === 'global') { return (

- +
Name @@ -79,9 +81,9 @@ export async function Commands({ platform, id, type }: Props) { {commands.map((item) => ( - !{item.name} + {item.name} -

+

{item.content}

diff --git a/apps/web/src/app/(protected)/dashboard/[platform]/[id]/commands/@tabs/custom/loading.tsx b/apps/web/src/app/(protected)/dashboard/[platform]/[id]/commands/@tabs/custom/loading.tsx new file mode 100644 index 00000000..d1a8959d --- /dev/null +++ b/apps/web/src/app/(protected)/dashboard/[platform]/[id]/commands/@tabs/custom/loading.tsx @@ -0,0 +1,9 @@ +import { LoaderIcon } from '@/components/ui/icons' + +export default function Loading() { + return ( +
+ +
+ ) +} diff --git a/apps/web/src/app/(protected)/dashboard/[platform]/[id]/commands/@tabs/custom/page.tsx b/apps/web/src/app/(protected)/dashboard/[platform]/[id]/commands/@tabs/custom/page.tsx new file mode 100644 index 00000000..9b40a289 --- /dev/null +++ b/apps/web/src/app/(protected)/dashboard/[platform]/[id]/commands/@tabs/custom/page.tsx @@ -0,0 +1,31 @@ +import type { Metadata } from 'next' +import { redirect } from 'next/navigation' + +import { useSession } from '@/hooks/use-session' + +import type { Platform } from '@/types/platform' + +import { CommandsList } from '../commands-list' + +export const metadata: Metadata = { + title: 'Custom Commands', +} + +interface Props { + params: { + platform: Platform + id: string + } +} + +export default async function Page({ params }: Props) { + const session = await useSession() + + if (!session) { + throw redirect('/signin') + } + + return ( + + ) +} diff --git a/apps/web/src/components/pages/commands/delete-command.tsx b/apps/web/src/app/(protected)/dashboard/[platform]/[id]/commands/@tabs/delete-command-button.tsx similarity index 57% rename from apps/web/src/components/pages/commands/delete-command.tsx rename to apps/web/src/app/(protected)/dashboard/[platform]/[id]/commands/@tabs/delete-command-button.tsx index d16af695..02b84f57 100644 --- a/apps/web/src/components/pages/commands/delete-command.tsx +++ b/apps/web/src/app/(protected)/dashboard/[platform]/[id]/commands/@tabs/delete-command-button.tsx @@ -7,7 +7,9 @@ import { toast } from 'sonner' import { Button } from '@/components/ui/button' import { LoaderIcon } from '@/components/ui/icons' -import { deleteEntityCommand } from '@/data-layer/actions/command' +import { deleteCommand } from '@/services/actions/commands' + +import type { Platform } from '@/types/platform' interface Props { id: number @@ -26,21 +28,19 @@ export function DeleteCommand({ id, platform, platformEntityId }: Props) { onClick={() => { if (!confirm('Are you sure you want to perform this action?')) return - startTransition(() => { - const dispatch = deleteEntityCommand(id, platform, platformEntityId) - - toast.promise(dispatch, { - loading: 'Loading...', - success: ({ success, message }) => { - if (!success) { - throw new Error(message) - } - return message - }, - error: ({ message }) => { - return message - }, + startTransition(async () => { + const [, error] = await deleteCommand({ + id, + platform, + platformEntityId, }) + + if (error) { + toast.error(error.message) + return + } + + toast.success('Successfully deleted!') }) }} > diff --git a/apps/web/src/app/(protected)/dashboard/[platform]/[id]/commands/@tabs/global/loading.tsx b/apps/web/src/app/(protected)/dashboard/[platform]/[id]/commands/@tabs/global/loading.tsx new file mode 100644 index 00000000..d1a8959d --- /dev/null +++ b/apps/web/src/app/(protected)/dashboard/[platform]/[id]/commands/@tabs/global/loading.tsx @@ -0,0 +1,9 @@ +import { LoaderIcon } from '@/components/ui/icons' + +export default function Loading() { + return ( +
+ +
+ ) +} diff --git a/apps/web/src/app/(protected)/dashboard/[platform]/[id]/commands/@tabs/global/page.tsx b/apps/web/src/app/(protected)/dashboard/[platform]/[id]/commands/@tabs/global/page.tsx new file mode 100644 index 00000000..b9e39787 --- /dev/null +++ b/apps/web/src/app/(protected)/dashboard/[platform]/[id]/commands/@tabs/global/page.tsx @@ -0,0 +1,31 @@ +import type { Metadata } from 'next' +import { redirect } from 'next/navigation' + +import { useSession } from '@/hooks/use-session' + +import type { Platform } from '@/types/platform' + +import { CommandsList } from '../commands-list' + +export const metadata: Metadata = { + title: 'Global Commands', +} + +interface Props { + params: { + platform: Platform + id: string + } +} + +export default async function Page({ params }: Props) { + const session = await useSession() + + if (!session) { + throw redirect('/signin') + } + + return ( + + ) +} diff --git a/apps/web/src/components/pages/commands/commands-tabs-layout.tsx b/apps/web/src/app/(protected)/dashboard/[platform]/[id]/commands/@tabs/layout.tsx similarity index 56% rename from apps/web/src/components/pages/commands/commands-tabs-layout.tsx rename to apps/web/src/app/(protected)/dashboard/[platform]/[id]/commands/@tabs/layout.tsx index 55bfb002..8538e959 100644 --- a/apps/web/src/components/pages/commands/commands-tabs-layout.tsx +++ b/apps/web/src/app/(protected)/dashboard/[platform]/[id]/commands/@tabs/layout.tsx @@ -1,24 +1,34 @@ import { TabGroup, TabGroupItem } from '@/components/ui/tab-group' -interface Props { - platform: Platform - id: string - children: React.ReactNode -} +import type { Platform } from '@/types/platform' const tabs = [ - { label: 'Custom Commands', slug: 'custom' }, - { label: 'Global Commands', slug: 'global' }, + { + label: 'Custom Commands', + slug: 'custom', + }, + { + label: 'Global Commands', + slug: 'global', + }, ] -export function CommandsTabsLayout({ platform, id, children }: Props) { +interface Props { + params: { + platform: Platform + id: string + } + children: React.ReactNode +} + +export default function TabsLayout({ params, children }: Props) { return (
{tabs.map((item) => { return ( diff --git a/apps/web/src/app/(protected)/dashboard/[platform]/[id]/commands/@tabs/update-command-dialog.tsx b/apps/web/src/app/(protected)/dashboard/[platform]/[id]/commands/@tabs/update-command-dialog.tsx new file mode 100644 index 00000000..71b277e8 --- /dev/null +++ b/apps/web/src/app/(protected)/dashboard/[platform]/[id]/commands/@tabs/update-command-dialog.tsx @@ -0,0 +1,121 @@ +'use client' + +import { useState } from 'react' +import { useFormStatus } from 'react-dom' + +import { toast } from 'sonner' + +import { Button } from '@/components/ui/button' +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogTrigger, +} from '@/components/ui/dialog' +import { LoaderIcon } from '@/components/ui/icons' +import { Input } from '@/components/ui/input' +import { Label } from '@/components/ui/label' +import { Link } from '@/components/ui/link' +import { Switch } from '@/components/ui/switch' + +import { updateCommand } from '@/services/actions/commands' + +import type { EntityCommand } from '@/types/command' + +interface Props { + command: EntityCommand +} + +export function UpdateCommand({ command }: Props) { + const [open, setOpen] = useState(false) + + return ( + + + + + + + Update Command + +
{ + // set platform fields + formData.append('id', String(command.id)) + formData.append('platform', command.platform) + formData.append('platformEntityId', command.platform_entity_id) + + const [, error] = await updateCommand(formData) + + if (error) { + if (error.code === 'INPUT_PARSE_ERROR') { + toast.error('Invalid submission!') + return + } else { + toast.error(error.message) + return + } + } + + toast.success('Successfully updated!') + }} + > +
+
+ + +
+
+ + +

+ See our{' '} + + docs + {' '} + for more variables. +

+
+
+ + +
+
+ + +
+
+ ) +} + +function SaveButton() { + const { pending } = useFormStatus() + return ( +
+ +
+ ) +} diff --git a/apps/web/src/app/(protected)/dashboard/[platform]/[id]/commands/create-command-dialog.tsx b/apps/web/src/app/(protected)/dashboard/[platform]/[id]/commands/create-command-dialog.tsx new file mode 100644 index 00000000..fe1c33b4 --- /dev/null +++ b/apps/web/src/app/(protected)/dashboard/[platform]/[id]/commands/create-command-dialog.tsx @@ -0,0 +1,121 @@ +'use client' + +import { useRef, useState } from 'react' +import { useFormStatus } from 'react-dom' + +import { PlusIcon } from 'lucide-react' +import { toast } from 'sonner' + +import { Button } from '@/components/ui/button' +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogTrigger, +} from '@/components/ui/dialog' +import { LoaderIcon } from '@/components/ui/icons' +import { Input } from '@/components/ui/input' +import { Label } from '@/components/ui/label' +import { Link } from '@/components/ui/link' +import { Switch } from '@/components/ui/switch' + +import { createCommand } from '@/services/actions/commands' + +import type { Platform } from '@/types/platform' + +interface Props { + platform: Platform + entityId: string +} + +export function CreateCommand({ platform, entityId }: Props) { + const ref = useRef(null) + const [open, setOpen] = useState(false) + + return ( + + + + + + + Create Command + +
{ + // set platform field + formData.append('platform', platform) + formData.append('platformEntityId', entityId) + + const [, error] = await createCommand(formData) + + if (error) { + if (error.code === 'INPUT_PARSE_ERROR') { + toast.error('Invalid submission!') + return + } else { + toast.error(error.message) + return + } + } + + ref.current?.reset() + setOpen(false) + toast.success('Successfully added!') + }} + > +
+
+ + +
+
+ + +

+ See our{' '} + + docs + {' '} + for more variables. +

+
+
+ + +
+
+ + +
+
+ ) +} + +function SubmitButton() { + const { pending } = useFormStatus() + return ( +
+ +
+ ) +} diff --git a/apps/web/src/app/(protected)/dashboard/[platform]/[id]/commands/layout.tsx b/apps/web/src/app/(protected)/dashboard/[platform]/[id]/commands/layout.tsx new file mode 100644 index 00000000..d88019e8 --- /dev/null +++ b/apps/web/src/app/(protected)/dashboard/[platform]/[id]/commands/layout.tsx @@ -0,0 +1,30 @@ +import type { Platform } from '@/types/platform' + +import { CreateCommand } from './create-command-dialog' +import { ShareCommands } from './share-commands-button' + +interface Props { + params: { + platform: Platform + id: string + } + tabs: React.ReactNode +} + +export default function Layout({ params, tabs }: Props) { + return ( +
+
+
+

Commands

+
+ + +
+
+

Manage your commands.

+
+ {tabs} +
+ ) +} diff --git a/apps/web/src/components/pages/commands/share-commands.tsx b/apps/web/src/app/(protected)/dashboard/[platform]/[id]/commands/share-commands-button.tsx similarity index 74% rename from apps/web/src/components/pages/commands/share-commands.tsx rename to apps/web/src/app/(protected)/dashboard/[platform]/[id]/commands/share-commands-button.tsx index 27022298..b9707dd8 100644 --- a/apps/web/src/components/pages/commands/share-commands.tsx +++ b/apps/web/src/app/(protected)/dashboard/[platform]/[id]/commands/share-commands-button.tsx @@ -6,7 +6,7 @@ import { Button } from '@/components/ui/button' export function ShareCommands() { return ( - diff --git a/apps/web/src/app/(protected)/dashboard/[platform]/[id]/entity-logs-card.tsx b/apps/web/src/app/(protected)/dashboard/[platform]/[id]/entity-logs-card.tsx new file mode 100644 index 00000000..cdd77fb0 --- /dev/null +++ b/apps/web/src/app/(protected)/dashboard/[platform]/[id]/entity-logs-card.tsx @@ -0,0 +1,49 @@ +import { Badge } from '@/components/ui/badge' +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' + +import { formatDate } from '@/lib/utils' + +import { getEntityLogs } from '@/services/queries/entities' + +import type { Platform } from '@/types/platform' + +interface Props { + platform: Platform + id: string +} + +export async function EntityLogsCard({ platform, id }: Props) { + const logs = await getEntityLogs(platform, id) + + return ( + + + Audit Logs + + + {logs.length > 0 ? ( +
    + {logs + .map((item) => ( +
  • +
    + @{item.author} + {item.activity} +
    + + {formatDate(item.activity_date)} + +
  • + )) + .slice(0, 10)} +
+ ) : ( +

No logs found.

+ )} +
+
+ ) +} diff --git a/apps/web/src/app/(protected)/dashboard/[platform]/[id]/page.tsx b/apps/web/src/app/(protected)/dashboard/[platform]/[id]/page.tsx new file mode 100644 index 00000000..f623e383 --- /dev/null +++ b/apps/web/src/app/(protected)/dashboard/[platform]/[id]/page.tsx @@ -0,0 +1,53 @@ +import { Suspense } from 'react' + +import type { Metadata } from 'next' +import { redirect } from 'next/navigation' + +import { LoaderIcon } from '@/components/ui/icons' + +import { useSession } from '@/hooks/use-session' + +import type { Platform } from '@/types/platform' + +import { EntityLogsCard } from './entity-logs-card' + +export const metadata: Metadata = { + title: 'Overview', +} + +interface Props { + params: { + platform: Platform + id: string + } +} + +export default async function Page({ params }: Props) { + const session = await useSession() + + if (!session) { + throw redirect('/signin') + } + + return ( +
+
+

Overview

+

+ Overview of your server or channel. +

+
+
+ + +
+ } + > + + +
+
+ ) +} diff --git a/apps/web/src/components/pages/dashboard/bottom-nav.tsx b/apps/web/src/app/(protected)/dashboard/_sidebar/bottom-nav.tsx similarity index 100% rename from apps/web/src/components/pages/dashboard/bottom-nav.tsx rename to apps/web/src/app/(protected)/dashboard/_sidebar/bottom-nav.tsx diff --git a/apps/web/src/components/pages/dashboard/entities-dropdown-wrapper.tsx b/apps/web/src/app/(protected)/dashboard/_sidebar/entities-dropdown-wrapper.tsx similarity index 91% rename from apps/web/src/components/pages/dashboard/entities-dropdown-wrapper.tsx rename to apps/web/src/app/(protected)/dashboard/_sidebar/entities-dropdown-wrapper.tsx index abced6ac..d61ec970 100644 --- a/apps/web/src/components/pages/dashboard/entities-dropdown-wrapper.tsx +++ b/apps/web/src/app/(protected)/dashboard/_sidebar/entities-dropdown-wrapper.tsx @@ -4,7 +4,7 @@ import { PlusIcon } from 'lucide-react' import { Button } from '@/components/ui/button' -import { getUserEntities } from '@/data-layer/queries/user' +import { getUserEntities } from '@/services/queries/users' import { EntitiesDropdown } from './entities-dropdown' diff --git a/apps/web/src/components/pages/dashboard/entities-dropdown.tsx b/apps/web/src/app/(protected)/dashboard/_sidebar/entities-dropdown.tsx similarity index 89% rename from apps/web/src/components/pages/dashboard/entities-dropdown.tsx rename to apps/web/src/app/(protected)/dashboard/_sidebar/entities-dropdown.tsx index dffcdcc6..efd97ff4 100644 --- a/apps/web/src/components/pages/dashboard/entities-dropdown.tsx +++ b/apps/web/src/app/(protected)/dashboard/_sidebar/entities-dropdown.tsx @@ -27,6 +27,8 @@ import { Skeleton } from '@/components/ui/skeleton' import { cn } from '@/lib/utils' +import type { UserEntity } from '@/types/user' + interface Props { entities: UserEntity[] } @@ -58,6 +60,9 @@ export function EntitiesDropdown({ entities }: Props) { + + {selectedEntity.platform} + {selectedEntity.entity_name} @@ -67,7 +72,10 @@ export function EntitiesDropdown({ entities }: Props) { - + No server found. @@ -90,7 +98,10 @@ export function EntitiesDropdown({ entities }: Props) { {item.entity_name.charAt(0)} - + + + {item.platform} + {item.entity_name} diff --git a/apps/web/src/components/pages/dashboard/main-nav.tsx b/apps/web/src/app/(protected)/dashboard/_sidebar/main-nav.tsx similarity index 87% rename from apps/web/src/components/pages/dashboard/main-nav.tsx rename to apps/web/src/app/(protected)/dashboard/_sidebar/main-nav.tsx index dbbc27bf..ac7ac281 100644 --- a/apps/web/src/components/pages/dashboard/main-nav.tsx +++ b/apps/web/src/app/(protected)/dashboard/_sidebar/main-nav.tsx @@ -4,7 +4,7 @@ import { useMemo } from 'react' import { useParams, useSelectedLayoutSegment } from 'next/navigation' -import { HomeIcon, ListIcon } from 'lucide-react' +import { CalendarIcon, HomeIcon, ListIcon, MegaphoneIcon } from 'lucide-react' import { NavLinkItem } from './nav-link-item' @@ -39,6 +39,11 @@ export function MainNav() { href: `${BASE_URL}/commands`, icon: ListIcon, }, + { + label: 'Event Channels', + href: `${BASE_URL}/event-channels`, + icon: CalendarIcon, + }, ] } else { return [ diff --git a/apps/web/src/components/pages/dashboard/nav-link-item.tsx b/apps/web/src/app/(protected)/dashboard/_sidebar/nav-link-item.tsx similarity index 100% rename from apps/web/src/components/pages/dashboard/nav-link-item.tsx rename to apps/web/src/app/(protected)/dashboard/_sidebar/nav-link-item.tsx diff --git a/apps/web/src/components/pages/dashboard/sidebar.tsx b/apps/web/src/app/(protected)/dashboard/_sidebar/sidebar.tsx similarity index 100% rename from apps/web/src/components/pages/dashboard/sidebar.tsx rename to apps/web/src/app/(protected)/dashboard/_sidebar/sidebar.tsx diff --git a/apps/web/src/components/pages/dashboard/user-dropdown.tsx b/apps/web/src/app/(protected)/dashboard/_sidebar/user-dropdown.tsx similarity index 95% rename from apps/web/src/components/pages/dashboard/user-dropdown.tsx rename to apps/web/src/app/(protected)/dashboard/_sidebar/user-dropdown.tsx index 11836f56..64cb4987 100644 --- a/apps/web/src/components/pages/dashboard/user-dropdown.tsx +++ b/apps/web/src/app/(protected)/dashboard/_sidebar/user-dropdown.tsx @@ -12,10 +12,12 @@ import { DropdownMenuTrigger, } from '@/components/ui/dropdown-menu' -import { auth, signOut } from '@/lib/auth' +import { signOut } from '@/lib/auth' + +import { useSession } from '@/hooks/use-session' export async function UserDropdown() { - const session = await auth() + const session = await useSession() if (!session?.user) { return null diff --git a/apps/web/src/app/(protected)/dashboard/discord/[id]/commands/@tabs/custom/page.tsx b/apps/web/src/app/(protected)/dashboard/discord/[id]/commands/@tabs/custom/page.tsx deleted file mode 100644 index 1c2c2f47..00000000 --- a/apps/web/src/app/(protected)/dashboard/discord/[id]/commands/@tabs/custom/page.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import type { Metadata } from 'next' - -import CommandsWrapper from '@/components/pages/commands/commands-wrapper' - -export const metadata: Metadata = { - title: 'Custom Commands', -} - -interface Props { - params: { - id: string - } -} - -export default async function Page({ params }: Props) { - return -} diff --git a/apps/web/src/app/(protected)/dashboard/discord/[id]/commands/@tabs/global/page.tsx b/apps/web/src/app/(protected)/dashboard/discord/[id]/commands/@tabs/global/page.tsx deleted file mode 100644 index 9b216f99..00000000 --- a/apps/web/src/app/(protected)/dashboard/discord/[id]/commands/@tabs/global/page.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import type { Metadata } from 'next' - -import CommandsWrapper from '@/components/pages/commands/commands-wrapper' - -export const metadata: Metadata = { - title: 'Global Commands', -} - -interface Props { - params: { - id: string - } -} - -export default async function Page({ params }: Props) { - return -} diff --git a/apps/web/src/app/(protected)/dashboard/discord/[id]/commands/@tabs/layout.tsx b/apps/web/src/app/(protected)/dashboard/discord/[id]/commands/@tabs/layout.tsx deleted file mode 100644 index 3a048e65..00000000 --- a/apps/web/src/app/(protected)/dashboard/discord/[id]/commands/@tabs/layout.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import { CommandsTabsLayout } from '@/components/pages/commands/commands-tabs-layout' - -interface Props { - params: { - id: string - } - children: React.ReactNode -} - -export default function TabsLayout({ params, children }: Props) { - return ( - - {children} - - ) -} diff --git a/apps/web/src/app/(protected)/dashboard/discord/[id]/commands/layout.tsx b/apps/web/src/app/(protected)/dashboard/discord/[id]/commands/layout.tsx deleted file mode 100644 index 6187488d..00000000 --- a/apps/web/src/app/(protected)/dashboard/discord/[id]/commands/layout.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import { CommandsLayout } from '@/components/pages/commands/commands-layout' - -interface Props { - tabs: React.ReactNode -} - -export default function Layout({ tabs }: Props) { - return -} diff --git a/apps/web/src/app/(protected)/dashboard/discord/[id]/event-channels/create-event-channel-form.tsx b/apps/web/src/app/(protected)/dashboard/discord/[id]/event-channels/create-event-channel-form.tsx new file mode 100644 index 00000000..18cbfe8b --- /dev/null +++ b/apps/web/src/app/(protected)/dashboard/discord/[id]/event-channels/create-event-channel-form.tsx @@ -0,0 +1,89 @@ +'use client' + +import { useState } from 'react' +import { useFormStatus } from 'react-dom' + +import { PlusIcon } from 'lucide-react' +import { toast } from 'sonner' + +import { Button } from '@/components/ui/button' +import { LoaderIcon } from '@/components/ui/icons' +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select' + +import { createEventChannel } from '@/services/actions/livestreams' + +import type { GuildChannel } from '@/types/discord' + +interface Props { + platformEntityId: string + channels: GuildChannel[] +} + +export function CreateEventChannelForm({ platformEntityId, channels }: Props) { + const [key, setKey] = useState(+new Date()) + + return ( +
{ + formData.append('platformEntityId', platformEntityId) + + const [_, error] = await createEventChannel(formData) + + if (error) { + if (error.code === 'INPUT_PARSE_ERROR') { + return toast.error('Invalid submission!') + } else { + return toast.error(error.message) + } + } + + toast.success('Successfully added.') + setKey(+new Date()) + }} + > +
+ + +
+ + ) +} + +function SubmitButton({ isDisabled }: { isDisabled?: boolean }) { + const { pending } = useFormStatus() + + return ( + + ) +} diff --git a/apps/web/src/app/(protected)/dashboard/discord/[id]/event-channels/delete-channel-button.tsx b/apps/web/src/app/(protected)/dashboard/discord/[id]/event-channels/delete-channel-button.tsx new file mode 100644 index 00000000..38ce7066 --- /dev/null +++ b/apps/web/src/app/(protected)/dashboard/discord/[id]/event-channels/delete-channel-button.tsx @@ -0,0 +1,43 @@ +'use client' + +import { useTransition } from 'react' + +import { toast } from 'sonner' + +import { Button } from '@/components/ui/button' +import { LoaderIcon } from '@/components/ui/icons' + +import { deleteEventChannel } from '@/services/actions/livestreams' + +interface Props { + id: string + platformEntityId: string +} + +export function DeleteChannel({ id, platformEntityId }: Props) { + const [pending, startTransition] = useTransition() + + return ( + + ) +} diff --git a/apps/web/src/app/(protected)/dashboard/discord/[id]/event-channels/page.tsx b/apps/web/src/app/(protected)/dashboard/discord/[id]/event-channels/page.tsx new file mode 100644 index 00000000..72a24e67 --- /dev/null +++ b/apps/web/src/app/(protected)/dashboard/discord/[id]/event-channels/page.tsx @@ -0,0 +1,104 @@ +import type { Metadata } from 'next' +import { redirect } from 'next/navigation' + +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from '@/components/ui/table' + +import { formatDate } from '@/lib/utils' + +import { useSession } from '@/hooks/use-session' + +import { getDiscordGuildChannels } from '@/services/queries/discord' +import { getEventChannels } from '@/services/queries/livestreams' + +import { CreateEventChannelForm } from './create-event-channel-form' +import { DeleteChannel } from './delete-channel-button' + +export const metadata: Metadata = { + title: 'Event Channels', +} + +interface Props { + params: { + id: string + } +} + +export default async function Page({ params }: Props) { + const session = await useSession() + + if (!session) { + throw redirect('/signin') + } + + const [guildChannels, eventChannels] = await Promise.all([ + getDiscordGuildChannels(params.id), + getEventChannels(params.id), + ]) + + const filterChannels = guildChannels.filter( + (guildChannel) => + !eventChannels.some( + (eventChannel) => eventChannel.channel_id === guildChannel.id, + ), + ) + + // fixthis + function getEventChannelName(eventChannelId: String) { + return guildChannels.find((channel) => channel.id === eventChannelId)?.name + } + + return ( +
+
+

Event Channels

+

+ Manage channels to create automated Discord events for your livestream announcements. +

+
+
+
+ +
+
+
+ + + Channel + Added Date + + + + + {eventChannels.map((channel) => ( + + + {getEventChannelName(channel.channel_id)} + + {formatDate(channel.created_at)} + +
+ +
+
+
+ ))} +
+
+
+ + + ) +} diff --git a/apps/web/src/app/(protected)/dashboard/discord/[id]/page.tsx b/apps/web/src/app/(protected)/dashboard/discord/[id]/page.tsx deleted file mode 100644 index ce409412..00000000 --- a/apps/web/src/app/(protected)/dashboard/discord/[id]/page.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import type { Metadata } from 'next' - -import { OverviewView } from '@/components/pages/overview/overview-view' - -export const metadata: Metadata = { - title: 'Overview', -} - -interface Props { - params: { - id: string - } -} - -export default function Page({ params }: Props) { - return -} diff --git a/apps/web/src/components/pages/dashboard/joined-entities.tsx b/apps/web/src/app/(protected)/dashboard/joined-entities-list.tsx similarity index 68% rename from apps/web/src/components/pages/dashboard/joined-entities.tsx rename to apps/web/src/app/(protected)/dashboard/joined-entities-list.tsx index b3076084..30b4292c 100644 --- a/apps/web/src/components/pages/dashboard/joined-entities.tsx +++ b/apps/web/src/app/(protected)/dashboard/joined-entities-list.tsx @@ -3,9 +3,9 @@ import NextLink from 'next/link' import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar' import { Link } from '@/components/ui/link' -import { getUserEntities } from '@/data-layer/queries/user' +import { getUserEntities } from '@/services/queries/users' -export async function JoinedEntities() { +export async function JoinedEntitiesList() { const entities = await getUserEntities('joined') if (!entities.length) { @@ -21,7 +21,7 @@ export async function JoinedEntities() {
{entities.map((item) => ( @@ -29,7 +29,12 @@ export async function JoinedEntities() { {item.entity_name.charAt(0)} - {item.entity_name} + + + {item.platform} + + {item.entity_name} + ))}
diff --git a/apps/web/src/app/(protected)/dashboard/layout.tsx b/apps/web/src/app/(protected)/dashboard/layout.tsx index d4c2aa6c..818f4a4b 100644 --- a/apps/web/src/app/(protected)/dashboard/layout.tsx +++ b/apps/web/src/app/(protected)/dashboard/layout.tsx @@ -1,6 +1,7 @@ -import { Sidebar } from '@/components/pages/dashboard/sidebar' -import { TopLoader } from '@/components/pages/dashboard/top-loader' import { Toaster } from '@/components/ui/sonner' +import { TopLoader } from '@/components/ui/top-loader' + +import { Sidebar } from './_sidebar/sidebar' interface Props { children: React.ReactNode diff --git a/apps/web/src/app/(protected)/dashboard/page.tsx b/apps/web/src/app/(protected)/dashboard/page.tsx index 33b36d4d..645ce762 100644 --- a/apps/web/src/app/(protected)/dashboard/page.tsx +++ b/apps/web/src/app/(protected)/dashboard/page.tsx @@ -3,20 +3,21 @@ import { Suspense } from 'react' import type { Metadata } from 'next' import { redirect } from 'next/navigation' -import { JoinedEntities } from '@/components/pages/dashboard/joined-entities' import { LoaderIcon } from '@/components/ui/icons' -import { auth } from '@/lib/auth' +import { useSession } from '@/hooks/use-session' + +import { JoinedEntitiesList } from './joined-entities-list' export const metadata: Metadata = { title: 'Dashboard', } export default async function Page() { - const session = await auth() + const session = await useSession() if (!session) { - redirect('/signin') + throw redirect('/signin') } return ( @@ -30,7 +31,7 @@ export default async function Page() {

My Servers & Channels

}> - +
diff --git a/apps/web/src/app/(protected)/dashboard/settings/@tabs/layout.tsx b/apps/web/src/app/(protected)/dashboard/settings/@tabs/layout.tsx index a084e24c..12a0c003 100644 --- a/apps/web/src/app/(protected)/dashboard/settings/@tabs/layout.tsx +++ b/apps/web/src/app/(protected)/dashboard/settings/@tabs/layout.tsx @@ -4,7 +4,7 @@ interface Props { children: React.ReactNode } -export default function TabsLayout({ children }: Props) { +export default function Layout({ children }: Props) { return (
diff --git a/apps/web/src/app/(protected)/dashboard/settings/@tabs/privacy/page.tsx b/apps/web/src/app/(protected)/dashboard/settings/@tabs/privacy/page.tsx index 5af88696..c87d6e57 100644 --- a/apps/web/src/app/(protected)/dashboard/settings/@tabs/privacy/page.tsx +++ b/apps/web/src/app/(protected)/dashboard/settings/@tabs/privacy/page.tsx @@ -4,17 +4,17 @@ import { redirect } from 'next/navigation' import { Button } from '@/components/ui/button' import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' -import { auth } from '@/lib/auth' +import { useSession } from '@/hooks/use-session' export const metadata: Metadata = { title: 'Privacy', } export default async function Page() { - const session = await auth() + const session = await useSession() if (!session) { - redirect('/signin') + throw redirect('/signin') } return ( diff --git a/apps/web/src/app/(protected)/dashboard/settings/@tabs/profile/link-account-button.tsx b/apps/web/src/app/(protected)/dashboard/settings/@tabs/profile/link-account-button.tsx new file mode 100644 index 00000000..d3d2bb10 --- /dev/null +++ b/apps/web/src/app/(protected)/dashboard/settings/@tabs/profile/link-account-button.tsx @@ -0,0 +1,35 @@ +'use client' + +import { useTransition } from 'react' + +import { Button } from '@/components/ui/button' +import { LoaderIcon } from '@/components/ui/icons' + +import { signInWithProvider } from '@/services/actions/auth' + +interface Props { + provider: 'twitch' | 'discord' +} + +export function LinkAccount({ provider }: Props) { + const [pending, startTransition] = useTransition() + + return ( + + ) +} diff --git a/apps/web/src/components/pages/settings/linked-accounts.tsx b/apps/web/src/app/(protected)/dashboard/settings/@tabs/profile/linked-accounts-list.tsx similarity index 92% rename from apps/web/src/components/pages/settings/linked-accounts.tsx rename to apps/web/src/app/(protected)/dashboard/settings/@tabs/profile/linked-accounts-list.tsx index be7298e7..9d273160 100644 --- a/apps/web/src/components/pages/settings/linked-accounts.tsx +++ b/apps/web/src/app/(protected)/dashboard/settings/@tabs/profile/linked-accounts-list.tsx @@ -4,9 +4,9 @@ import { DiscordIcon, TwitchIcon } from '@/components/ui/icons' import { formatDate } from '@/lib/utils' -import { getUserAccounts } from '@/data-layer/queries/user' +import { getUserAccounts } from '@/services/queries/users' -import { LinkAccount } from './link-account' +import { LinkAccount } from './link-account-button' export async function LinkedAccounts() { const accounts = await getUserAccounts() @@ -27,7 +27,7 @@ export async function LinkedAccounts() { {formatDate(findTwitchAcc.created_at)} ) : ( - + )}
@@ -56,7 +56,7 @@ export async function LinkedAccounts() { {formatDate(findDiscordAcc.created_at)} ) : ( - + )}
diff --git a/apps/web/src/app/(protected)/dashboard/settings/@tabs/profile/page.tsx b/apps/web/src/app/(protected)/dashboard/settings/@tabs/profile/page.tsx index 98827e7f..c0fcd4d7 100644 --- a/apps/web/src/app/(protected)/dashboard/settings/@tabs/profile/page.tsx +++ b/apps/web/src/app/(protected)/dashboard/settings/@tabs/profile/page.tsx @@ -3,8 +3,6 @@ import { Suspense } from 'react' import type { Metadata } from 'next' import { redirect } from 'next/navigation' -import { LinkedAccounts } from '@/components/pages/settings/linked-accounts' -import { PersonalInformation } from '@/components/pages/settings/personal-information' import { Card, CardContent, @@ -14,17 +12,20 @@ import { } from '@/components/ui/card' import { LoaderIcon } from '@/components/ui/icons' -import { auth } from '@/lib/auth' +import { useSession } from '@/hooks/use-session' + +import { LinkedAccounts } from './linked-accounts-list' +import { PersonalInformation } from './personal-information' export const metadata: Metadata = { title: 'Profile', } export default async function Page() { - const session = await auth() + const session = await useSession() if (!session) { - redirect('/signin') + throw redirect('/signin') } return ( diff --git a/apps/web/src/components/pages/settings/personal-information.tsx b/apps/web/src/app/(protected)/dashboard/settings/@tabs/profile/personal-information.tsx similarity index 75% rename from apps/web/src/components/pages/settings/personal-information.tsx rename to apps/web/src/app/(protected)/dashboard/settings/@tabs/profile/personal-information.tsx index 9197df83..e7c06aba 100644 --- a/apps/web/src/components/pages/settings/personal-information.tsx +++ b/apps/web/src/app/(protected)/dashboard/settings/@tabs/profile/personal-information.tsx @@ -1,23 +1,24 @@ import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' -import { auth } from '@/lib/auth' import { maskEmail } from '@/lib/utils' +import { useSession } from '@/hooks/use-session' + export async function PersonalInformation() { - const session = await auth() + const session = await useSession() if (!session?.user) { return null } return ( -
-
+
+
-
+
{ if (!confirm('Are you sure you want to perform this action?')) return - startTransition(() => { - const dispatch = executeEntityAction( - 'depart', + startTransition(async () => { + const [, error] = await executeEntityAction({ platform, platformEntityId, - ) - - toast.promise(dispatch, { - loading: 'Loading...', - success: ({ success, message }) => { - if (!success) { - throw new Error(message) - } - return message - }, - error: ({ message }) => { - return message - }, + action: 'depart', }) + + if (error) { + toast.error(error.message) + return + } + + toast.success('Successfully departed!') }) }} > diff --git a/apps/web/src/components/pages/settings/entities.tsx b/apps/web/src/app/(protected)/dashboard/settings/@tabs/servers/entities-list.tsx similarity index 74% rename from apps/web/src/components/pages/settings/entities.tsx rename to apps/web/src/app/(protected)/dashboard/settings/@tabs/servers/entities-list.tsx index 66784f5a..e67d9ca5 100644 --- a/apps/web/src/components/pages/settings/entities.tsx +++ b/apps/web/src/app/(protected)/dashboard/settings/@tabs/servers/entities-list.tsx @@ -1,15 +1,16 @@ import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar' -import { env } from '@/env' +import { env } from '@/config/env' +import type { UserEntity } from '@/types/user' -import { DepartEntity } from './depart-entity' -import { JoinEntity } from './join-entity' +import { DepartEntity } from './depart-entity-button' +import { JoinEntity } from './join-entity-button' interface Props { entities: UserEntity[] } -export function Entities({ entities }: Props) { +export function EntitiesList({ entities }: Props) { const DISCORD_CLIENT_ID = env.AUTH_DISCORD_ID return ( @@ -25,7 +26,12 @@ export function Entities({ entities }: Props) { {item.entity_name.charAt(0)} - {item.entity_name} + + + {item.platform} + + {item.entity_name} +
{item.entity_bot_joined ? ( { - startTransition(() => { + startTransition(async () => { if (platform === 'twitch') { - const dispatch = executeEntityAction( - 'join', + const [, error] = await executeEntityAction({ platform, platformEntityId, - ) - - toast.promise(dispatch, { - loading: 'Loading...', - success: ({ success, message }) => { - if (!success) { - throw new Error(message) - } - return message - }, - error: ({ message }) => { - return message - }, + action: 'join', }) + + if (error) { + toast.error(error.message) + return + } + + toast.success('Successfully joined!') } else if (platform === 'discord') { const BASE_URL = 'https://discord.com/oauth2/authorize?' - const params = new URLSearchParams({ - client_id: discordClientId, - guild_id: platformEntityId, - disable_guild_select: 'true', - permission: '2199022698327', - scope: ['bot', 'applications.commands'].join(' '), - }) + const params = new URLSearchParams() + params.append('client_id', discordClientId) + params.append('guild_id', platformEntityId) + params.append('disable_guild_select', 'true') + params.append('permission', '2199022698327') + params.append('scope', ['bot', 'applications.commands'].join(' ')) window.open(new URL(BASE_URL + params), '_blank', 'noreferrer') } diff --git a/apps/web/src/app/(protected)/dashboard/settings/@tabs/servers/joinable-entities-list.tsx b/apps/web/src/app/(protected)/dashboard/settings/@tabs/servers/joinable-entities-list.tsx new file mode 100644 index 00000000..add30988 --- /dev/null +++ b/apps/web/src/app/(protected)/dashboard/settings/@tabs/servers/joinable-entities-list.tsx @@ -0,0 +1,8 @@ +import { getUserEntities } from '@/services/queries/users' + +import { EntitiesList } from './entities-list' + +export async function JoinableEntities() { + const entities = await getUserEntities('not_joined') + return +} diff --git a/apps/web/src/app/(protected)/dashboard/settings/@tabs/servers/joined-entities-list.tsx b/apps/web/src/app/(protected)/dashboard/settings/@tabs/servers/joined-entities-list.tsx new file mode 100644 index 00000000..ab272b55 --- /dev/null +++ b/apps/web/src/app/(protected)/dashboard/settings/@tabs/servers/joined-entities-list.tsx @@ -0,0 +1,8 @@ +import { getUserEntities } from '@/services/queries/users' + +import { EntitiesList } from './entities-list' + +export async function JoinedEntities() { + const entities = await getUserEntities('joined') + return +} diff --git a/apps/web/src/app/(protected)/dashboard/settings/@tabs/servers/page.tsx b/apps/web/src/app/(protected)/dashboard/settings/@tabs/servers/page.tsx index c89bd97f..b47d9d00 100644 --- a/apps/web/src/app/(protected)/dashboard/settings/@tabs/servers/page.tsx +++ b/apps/web/src/app/(protected)/dashboard/settings/@tabs/servers/page.tsx @@ -3,8 +3,6 @@ import { Suspense } from 'react' import type { Metadata } from 'next' import { redirect } from 'next/navigation' -import { JoinableEntities } from '@/components/pages/settings/joinable-entities' -import { JoinedEntities } from '@/components/pages/settings/joined-entities' import { Card, CardContent, @@ -14,17 +12,20 @@ import { } from '@/components/ui/card' import { LoaderIcon } from '@/components/ui/icons' -import { auth } from '@/lib/auth' +import { useSession } from '@/hooks/use-session' + +import { JoinableEntities } from './joinable-entities-list' +import { JoinedEntities } from './joined-entities-list' export const metadata: Metadata = { title: 'Servers & Channels', } export default async function Page() { - const session = await auth() + const session = await useSession() if (!session) { - redirect('/signin') + throw redirect('/signin') } return ( diff --git a/apps/web/src/app/(protected)/dashboard/settings/layout.tsx b/apps/web/src/app/(protected)/dashboard/settings/layout.tsx index a2f35ac0..4e8f300d 100644 --- a/apps/web/src/app/(protected)/dashboard/settings/layout.tsx +++ b/apps/web/src/app/(protected)/dashboard/settings/layout.tsx @@ -2,7 +2,7 @@ interface Props { tabs: React.ReactNode } -export default function SettingsLayout({ tabs }: Props) { +export default function Layout({ tabs }: Props) { return (
diff --git a/apps/web/src/app/(protected)/dashboard/twitch/[id]/commands/@tabs/custom/page.tsx b/apps/web/src/app/(protected)/dashboard/twitch/[id]/commands/@tabs/custom/page.tsx deleted file mode 100644 index 7f1ce141..00000000 --- a/apps/web/src/app/(protected)/dashboard/twitch/[id]/commands/@tabs/custom/page.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import type { Metadata } from 'next' - -import CommandsWrapper from '@/components/pages/commands/commands-wrapper' - -export const metadata: Metadata = { - title: 'Custom Commands', -} - -interface Props { - params: { - id: string - } -} - -export default async function Page({ params }: Props) { - return -} diff --git a/apps/web/src/app/(protected)/dashboard/twitch/[id]/commands/@tabs/global/page.tsx b/apps/web/src/app/(protected)/dashboard/twitch/[id]/commands/@tabs/global/page.tsx deleted file mode 100644 index d6ef50db..00000000 --- a/apps/web/src/app/(protected)/dashboard/twitch/[id]/commands/@tabs/global/page.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import type { Metadata } from 'next' - -import CommandsWrapper from '@/components/pages/commands/commands-wrapper' - -export const metadata: Metadata = { - title: 'Global Commands', -} - -interface Props { - params: { - id: string - } -} - -export default async function Page({ params }: Props) { - return -} diff --git a/apps/web/src/app/(protected)/dashboard/twitch/[id]/commands/@tabs/layout.tsx b/apps/web/src/app/(protected)/dashboard/twitch/[id]/commands/@tabs/layout.tsx deleted file mode 100644 index 11ae78c7..00000000 --- a/apps/web/src/app/(protected)/dashboard/twitch/[id]/commands/@tabs/layout.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import { CommandsTabsLayout } from '@/components/pages/commands/commands-tabs-layout' - -interface Props { - params: { - id: string - } - children: React.ReactNode -} - -export default function TabsLayout({ params, children }: Props) { - return ( - - {children} - - ) -} diff --git a/apps/web/src/app/(protected)/dashboard/twitch/[id]/commands/layout.tsx b/apps/web/src/app/(protected)/dashboard/twitch/[id]/commands/layout.tsx deleted file mode 100644 index 90947e7d..00000000 --- a/apps/web/src/app/(protected)/dashboard/twitch/[id]/commands/layout.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import { CommandsLayout } from '@/components/pages/commands/commands-layout' - -interface Props { - tabs: React.ReactNode -} - -export default function Layout({ tabs }: Props) { - return -} diff --git a/apps/web/src/app/(protected)/dashboard/twitch/[id]/page.tsx b/apps/web/src/app/(protected)/dashboard/twitch/[id]/page.tsx deleted file mode 100644 index 94e8dd41..00000000 --- a/apps/web/src/app/(protected)/dashboard/twitch/[id]/page.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import type { Metadata } from 'next' - -import { OverviewView } from '@/components/pages/overview/overview-view' - -export const metadata: Metadata = { - title: 'Overview', -} - -interface Props { - params: { - id: string - } -} - -export default function Page({ params }: Props) { - return -} diff --git a/apps/web/src/app/layout.tsx b/apps/web/src/app/layout.tsx index 66cc17ef..9f72b925 100644 --- a/apps/web/src/app/layout.tsx +++ b/apps/web/src/app/layout.tsx @@ -36,7 +36,7 @@ interface Props { children: React.ReactNode } -export default function RootLayout({ children }: Props) { +export default function Layout({ children }: Props) { return ( -
-
-

Commands

- - -
-

Manage your commands.

-
- {tabs} -
- ) -} diff --git a/apps/web/src/components/pages/commands/commands-wrapper.tsx b/apps/web/src/components/pages/commands/commands-wrapper.tsx deleted file mode 100644 index 93ae03ae..00000000 --- a/apps/web/src/components/pages/commands/commands-wrapper.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import { Suspense } from 'react' - -import { redirect } from 'next/navigation' - -import { Commands } from '@/components/pages/commands/commands' -import { LoaderIcon } from '@/components/ui/icons' - -import { auth } from '@/lib/auth' - -interface Props { - platform: Platform - id: string - type: CommandType -} - -export default async function CommandsWrapper({ platform, id, type }: Props) { - const session = await auth() - - if (!session) { - redirect('/signin') - } - - return ( - - -
- } - key={Math.random()} - > - - - ) -} diff --git a/apps/web/src/components/pages/commands/create-command-form.tsx b/apps/web/src/components/pages/commands/create-command-form.tsx deleted file mode 100644 index 0edb7f8a..00000000 --- a/apps/web/src/components/pages/commands/create-command-form.tsx +++ /dev/null @@ -1,98 +0,0 @@ -'use client' - -import { useFormStatus } from 'react-dom' - -import { toast } from 'sonner' - -import { Button } from '@/components/ui/button' -import { LoaderIcon } from '@/components/ui/icons' -import { Input } from '@/components/ui/input' -import { Label } from '@/components/ui/label' -import { Link } from '@/components/ui/link' -import { Switch } from '@/components/ui/switch' - -import { createEntityCommand } from '@/data-layer/actions/command' - -interface Props { - platform: Platform - platformEntityId: string - afterSubmission: () => void -} - -export function CreateCommandForm({ - platform, - platformEntityId, - afterSubmission, -}: Props) { - return ( -
{ - // set platform fields - formData.append('platform', platform) - formData.append('platformEntityId', platformEntityId) - - const dispatch = createEntityCommand(formData) - - toast.promise(dispatch, { - loading: 'Loading...', - success: ({ success, message }) => { - if (!success) { - throw new Error(message) - } - afterSubmission() - return message - }, - error: ({ message }) => { - return message - }, - }) - }} - > -
-
- - -
-
- - -

- See our{' '} - - docs - {' '} - for more variables. -

-
-
- - -
-
- - - ) -} - -function SubmitButton() { - const { pending } = useFormStatus() - return ( -
- -
- ) -} diff --git a/apps/web/src/components/pages/commands/create-command.tsx b/apps/web/src/components/pages/commands/create-command.tsx deleted file mode 100644 index 9ed61048..00000000 --- a/apps/web/src/components/pages/commands/create-command.tsx +++ /dev/null @@ -1,56 +0,0 @@ -'use client' - -import { useState } from 'react' - -import { useParams, useRouter, useSelectedLayoutSegment } from 'next/navigation' - -import { PlusIcon } from 'lucide-react' - -import { Button } from '@/components/ui/button' -import { - Dialog, - DialogContent, - DialogHeader, - DialogTitle, - DialogTrigger, -} from '@/components/ui/dialog' - -import { CreateCommandForm } from './create-command-form' - -interface Props { - platform: Platform -} - -export function CreateCommand({ platform }: Props) { - const [open, setOpen] = useState(false) - - const segment = useSelectedLayoutSegment() - const params = useParams<{ id: string }>() - const router = useRouter() - - return ( - - - - - - - Create Command - - { - setOpen(false) - if (segment !== 'custom') { - router.push(`/dashboard/${platform}/${params.id}/commands`) - } - }} - /> - - - ) -} diff --git a/apps/web/src/components/pages/commands/update-command-form.tsx b/apps/web/src/components/pages/commands/update-command-form.tsx deleted file mode 100644 index c5722e70..00000000 --- a/apps/web/src/components/pages/commands/update-command-form.tsx +++ /dev/null @@ -1,96 +0,0 @@ -'use client' - -import { useFormStatus } from 'react-dom' - -import { toast } from 'sonner' - -import { Button } from '@/components/ui/button' -import { LoaderIcon } from '@/components/ui/icons' -import { Input } from '@/components/ui/input' -import { Label } from '@/components/ui/label' -import { Link } from '@/components/ui/link' -import { Switch } from '@/components/ui/switch' - -import { updateEntityCommand } from '@/data-layer/actions/command' - -interface Props { - command: EntityCommand - afterSubmission: () => void -} - -export function UpdateCommandForm({ command, afterSubmission }: Props) { - return ( -
{ - // set platform fields - formData.append('id', String(command.id)) - formData.append('platform', command.platform) - formData.append('platformEntityId', command.platform_entity_id) - - const dispatch = updateEntityCommand(formData) - - toast.promise(dispatch, { - loading: 'Loading...', - success: ({ success, message }) => { - if (!success) { - throw new Error(message) - } - afterSubmission() - return message - }, - error: ({ message }) => { - return message - }, - }) - }} - > -
-
- - -
-
- - -

- See our{' '} - - docs - {' '} - for more variables. -

-
-
- - -
-
- - - ) -} - -function SaveButton() { - const { pending } = useFormStatus() - return ( -
- -
- ) -} diff --git a/apps/web/src/components/pages/commands/update-command-status.tsx b/apps/web/src/components/pages/commands/update-command-status.tsx deleted file mode 100644 index 4bcdc597..00000000 --- a/apps/web/src/components/pages/commands/update-command-status.tsx +++ /dev/null @@ -1,50 +0,0 @@ -'use client' - -import { useTransition } from 'react' - -import { toast } from 'sonner' - -import { Switch } from '@/components/ui/switch' - -import { updateEntityCommandStatus } from '@/data-layer/actions/command' - -interface Props { - command: EntityCommand -} - -export function UpdateCommandStatus({ command }: Props) { - const [pending, startTransition] = useTransition() - - return ( -
- { - startTransition(() => { - const dispatch = updateEntityCommandStatus( - command.id, - command.platform, - command.platform_entity_id, - checked, - ) - - toast.promise(dispatch, { - loading: 'Loading...', - success: ({ success, message }) => { - if (!success) { - throw new Error(message) - } - return message - }, - error: ({ message }) => { - return message - }, - }) - }) - }} - /> -
- ) -} diff --git a/apps/web/src/components/pages/commands/update-command.tsx b/apps/web/src/components/pages/commands/update-command.tsx deleted file mode 100644 index 1297f926..00000000 --- a/apps/web/src/components/pages/commands/update-command.tsx +++ /dev/null @@ -1,41 +0,0 @@ -'use client' - -import { useState } from 'react' - -import { Button } from '@/components/ui/button' -import { - Dialog, - DialogContent, - DialogHeader, - DialogTitle, - DialogTrigger, -} from '@/components/ui/dialog' - -import { UpdateCommandForm } from './update-command-form' - -interface Props { - command: EntityCommand -} - -export function UpdateCommand({ command }: Props) { - const [open, setOpen] = useState(false) - - return ( - - - - - - - Update Command - - setOpen(false)} - /> - - - ) -} diff --git a/apps/web/src/components/pages/landing/features.tsx b/apps/web/src/components/pages/landing/features.tsx deleted file mode 100644 index dec437f0..00000000 --- a/apps/web/src/components/pages/landing/features.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import { CheckCircleIcon, XIcon } from 'lucide-react' - -export function Features() { - return ( -
-
-
    -
  • - -

    - Download required -

    -
  • -
  • - -

    Free

    -
  • -
  • - -

    Open-source

    -
  • -
-
-
- ) -} diff --git a/apps/web/src/components/pages/landing/footer.tsx b/apps/web/src/components/pages/landing/footer.tsx deleted file mode 100644 index 9640e42d..00000000 --- a/apps/web/src/components/pages/landing/footer.tsx +++ /dev/null @@ -1,148 +0,0 @@ -import Image from 'next/image' - -import { Link } from '@/components/ui/link' - -const productLinks = [ - { - label: 'Dashboard', - href: '/dashboard', - }, - { - label: 'Documentation', - href: '/docs', - options: { - target: '_blank', - rel: 'noreferrer', - }, - }, -] - -const legalLinks = [ - { - label: 'Terms of Service', - href: '/terms-of-service', - }, - { - label: 'Privacy Policy', - href: '/privacy-policy', - }, - { - label: 'Cookie Policy', - href: '/cookie-policy', - }, - { - label: 'EULA', - href: '/eula', - }, -] - -const socialLinks = [ - { - label: 'Twitter', - href: 'https://x.com/senchabot', - }, - { - label: 'Discord', - href: '/discord', - }, - { - label: 'GitHub', - href: '/github', - }, - { - label: 'LinkedIn', - href: 'https://www.linkedin.com/company/senchabot/', - }, -] - -export function Footer() { - return ( -
-
-
-
-
- Senchabot -
- Senchabot -
-

- Manage your Discord and Twitch community with an open-source - multi-platform bot. -

-
-
-
- - Product - -
    - {productLinks.map((item) => ( -
  • - - {item.label} - -
  • - ))} -
-
-
- - Legal - -
    - {legalLinks.map((item) => ( -
  • - - {item.label} - -
  • - ))} -
-
-
- - Social - -
    - {socialLinks.map((item) => ( -
  • - - {item.label} - -
  • - ))} -
-
-
-
-
-
-

© {new Date().getFullYear()} Senchabot. All Rights Reserved.

-

- Made with from the - community. -

-
-
-
- ) -} diff --git a/apps/web/src/components/pages/landing/header.tsx b/apps/web/src/components/pages/landing/header.tsx deleted file mode 100644 index e9b8342b..00000000 --- a/apps/web/src/components/pages/landing/header.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import Image from 'next/image' -import NextLink from 'next/link' - -import { ThemeToggle } from '@/components/theme-toggle' -import { Button } from '@/components/ui/button' -import { Link } from '@/components/ui/link' - -const navLinks = [ - { - label: 'Documentation', - path: '/docs', - options: { - target: '_blank', - rel: 'noreferrer', - }, - }, -] - -export function Header() { - return ( -
- -
- ) -} diff --git a/apps/web/src/components/pages/landing/hero.tsx b/apps/web/src/components/pages/landing/hero.tsx deleted file mode 100644 index 4dedb0e1..00000000 --- a/apps/web/src/components/pages/landing/hero.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import NextLink from 'next/link' - -import { Button } from '@/components/ui/button' -import { GitHubIcon } from '@/components/ui/icons' -import { Link } from '@/components/ui/link' - -import { ThemedImage } from './themed-image' - -export function Hero() { - return ( -
-
-
-

- Manage Your Community from One Place -

-

- Multi-platform bot designed for seamless integration with Twitch and - Discord. -

-
- -

- We're open source on - - - GitHub - -

-
-
-
- -
-
-
- ) -} diff --git a/apps/web/src/components/pages/overview/entity-logs.tsx b/apps/web/src/components/pages/overview/entity-logs.tsx deleted file mode 100644 index 76534cda..00000000 --- a/apps/web/src/components/pages/overview/entity-logs.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import { Badge } from '@/components/ui/badge' - -import { formatDate } from '@/lib/utils' - -import { getEntityLogs } from '@/data-layer/queries/entity' - -interface Props { - platform: Platform - platformEntityId: string -} - -export async function EntityLogs({ platform, platformEntityId }: Props) { - const logs = await getEntityLogs(platform, platformEntityId) - - /** - * IMPORTANT - * add here "activity logs are disabled" texts when added [platform]/settings page - */ - if (!logs.length) { - return

No logs found.

- } - - return ( -
    - {logs - .map((item) => ( -
  • -
    - @{item.author} - {item.activity} -
    - - {formatDate(item.activity_date)} - -
  • - )) - .slice(0, 10)} -
- ) -} diff --git a/apps/web/src/components/pages/overview/overview-view.tsx b/apps/web/src/components/pages/overview/overview-view.tsx deleted file mode 100644 index 06da98e2..00000000 --- a/apps/web/src/components/pages/overview/overview-view.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import { Suspense } from 'react' - -import { redirect } from 'next/navigation' - -import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' -import { LoaderIcon } from '@/components/ui/icons' - -import { auth } from '@/lib/auth' - -import { EntityLogs } from './entity-logs' - -interface Props { - platform: Platform - id: string -} - -export async function OverviewView({ platform, id }: Props) { - const session = await auth() - - if (!session) { - redirect('/signin') - } - - return ( -
-
-

Overview

-

- Overview of your server or channel. -

-
-
- - - Audit Logs - - - }> - - - - -
-
- ) -} diff --git a/apps/web/src/components/pages/settings/joinable-entities.tsx b/apps/web/src/components/pages/settings/joinable-entities.tsx deleted file mode 100644 index 28960654..00000000 --- a/apps/web/src/components/pages/settings/joinable-entities.tsx +++ /dev/null @@ -1,8 +0,0 @@ -import { getUserEntities } from '@/data-layer/queries/user' - -import { Entities } from './entities' - -export async function JoinableEntities() { - const entities = await getUserEntities('not_joined') - return -} diff --git a/apps/web/src/components/pages/settings/joined-entities.tsx b/apps/web/src/components/pages/settings/joined-entities.tsx deleted file mode 100644 index d3001852..00000000 --- a/apps/web/src/components/pages/settings/joined-entities.tsx +++ /dev/null @@ -1,8 +0,0 @@ -import { getUserEntities } from '@/data-layer/queries/user' - -import { Entities } from './entities' - -export async function JoinedEntities() { - const entities = await getUserEntities('joined') - return -} diff --git a/apps/web/src/components/pages/settings/link-account.tsx b/apps/web/src/components/pages/settings/link-account.tsx deleted file mode 100644 index 460d0e63..00000000 --- a/apps/web/src/components/pages/settings/link-account.tsx +++ /dev/null @@ -1,38 +0,0 @@ -'use client' - -import { useState } from 'react' - -import { signIn } from 'next-auth/react' - -import { Button } from '@/components/ui/button' -import { LoaderIcon } from '@/components/ui/icons' - -interface Props { - platform: Platform -} - -export function LinkAccount({ platform }: Props) { - const [pending, setPending] = useState(false) - - return ( - - ) -} diff --git a/apps/web/src/components/pages/signin/signin-button.tsx b/apps/web/src/components/pages/signin/signin-button.tsx deleted file mode 100644 index 131ba0b0..00000000 --- a/apps/web/src/components/pages/signin/signin-button.tsx +++ /dev/null @@ -1,53 +0,0 @@ -'use client' - -import { useState } from 'react' - -import { signIn } from 'next-auth/react' - -import { Button } from '@/components/ui/button' -import { DiscordIcon, LoaderIcon, TwitchIcon } from '@/components/ui/icons' - -interface Props { - platform: Platform - label: string - callbackUrl?: string - isDisabled?: boolean -} - -export function SignInButton({ - platform, - label, - callbackUrl, - isDisabled = false, -}: Props) { - const [pending, setPending] = useState(false) - - return ( - - ) -} diff --git a/apps/web/src/components/pages/signin/signin-error.tsx b/apps/web/src/components/pages/signin/signin-error.tsx deleted file mode 100644 index 2b303916..00000000 --- a/apps/web/src/components/pages/signin/signin-error.tsx +++ /dev/null @@ -1,39 +0,0 @@ -'use client' - -import { useMemo } from 'react' - -import { useSearchParams } from 'next/navigation' - -import { TriangleAlertIcon } from 'lucide-react' - -import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert' - -type Error = 'OAuthAccountNotLinked' - -export function SignInError() { - const search = useSearchParams() - const error = search.get('error') as Error - - let errorMessage = useMemo(() => { - if (error) { - switch (error) { - case 'OAuthAccountNotLinked': - return 'The account is already associated with another user.' - default: - return 'Something went wrong!' - } - } - }, [error]) - - if (!errorMessage) { - return null - } - - return ( - - - Error - {errorMessage} - - ) -} diff --git a/apps/web/src/components/pages/signin/signin-form.tsx b/apps/web/src/components/pages/signin/signin-form.tsx deleted file mode 100644 index 99797afa..00000000 --- a/apps/web/src/components/pages/signin/signin-form.tsx +++ /dev/null @@ -1,95 +0,0 @@ -'use client' - -import { useState } from 'react' - -import { Checkbox } from '@/components/ui/checkbox' -import { Label } from '@/components/ui/label' -import { Link } from '@/components/ui/link' - -import { cn } from '@/lib/utils' - -import { SignInButton } from './signin-button' - -const platforms: { - platform: Platform - label: string -}[] = [ - { - platform: 'twitch', - label: 'Continue with Twitch', - }, - { - platform: 'discord', - label: 'Continue with Discord', - }, -] - -export function SignInForm() { - const [checked, setChecked] = useState(false) - - return ( -
-
    - {platforms.map((item) => ( -
  • - -
  • - ))} -
-
- setChecked((prev) => !prev)} - /> -
- -

- You agree to our{' '} - - Privacy Policy - - ,{' '} - - Terms of Service - - ,{' '} - - Cookie Policy - {' '} - and{' '} - - EULA - - . -

-
-
-
- ) -} diff --git a/apps/web/src/components/ui/select.tsx b/apps/web/src/components/ui/select.tsx new file mode 100644 index 00000000..03def281 --- /dev/null +++ b/apps/web/src/components/ui/select.tsx @@ -0,0 +1,165 @@ +'use client' + +import * as React from 'react' + +import { + CaretSortIcon, + CheckIcon, + ChevronDownIcon, + ChevronUpIcon, +} from '@radix-ui/react-icons' +import * as SelectPrimitive from '@radix-ui/react-select' + +import { cn } from '@/lib/utils' + +const Select = SelectPrimitive.Root + +const SelectGroup = SelectPrimitive.Group + +const SelectValue = SelectPrimitive.Value + +const SelectTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + span]:line-clamp-1', + className, + )} + {...props} + > + {children} + + + + +)) +SelectTrigger.displayName = SelectPrimitive.Trigger.displayName + +const SelectScrollUpButton = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)) +SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName + +const SelectScrollDownButton = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)) +SelectScrollDownButton.displayName = + SelectPrimitive.ScrollDownButton.displayName + +const SelectContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, position = 'popper', ...props }, ref) => ( + + + + + {children} + + + + +)) +SelectContent.displayName = SelectPrimitive.Content.displayName + +const SelectLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +SelectLabel.displayName = SelectPrimitive.Label.displayName + +const SelectItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + + + + {children} + +)) +SelectItem.displayName = SelectPrimitive.Item.displayName + +const SelectSeparator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +SelectSeparator.displayName = SelectPrimitive.Separator.displayName + +export { + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectLabel, + SelectScrollDownButton, + SelectScrollUpButton, + SelectSeparator, + SelectTrigger, + SelectValue, +} diff --git a/apps/web/src/components/pages/dashboard/top-loader.tsx b/apps/web/src/components/ui/top-loader.tsx similarity index 100% rename from apps/web/src/components/pages/dashboard/top-loader.tsx rename to apps/web/src/components/ui/top-loader.tsx diff --git a/apps/web/src/env.ts b/apps/web/src/config/env.ts similarity index 100% rename from apps/web/src/env.ts rename to apps/web/src/config/env.ts diff --git a/apps/web/src/data-layer/actions/command.ts b/apps/web/src/data-layer/actions/command.ts deleted file mode 100644 index 87a718cd..00000000 --- a/apps/web/src/data-layer/actions/command.ts +++ /dev/null @@ -1,172 +0,0 @@ -'use server' - -import { revalidateTag } from 'next/cache' - -import { ApiError, fetcher } from '../fetcher' -import { - createCommandSchema, - updateCommandSchema, -} from '../validations/command' - -/** - * - * @param formData - * @returns - */ -export async function createEntityCommand( - formData: FormData, -): Promise<{ success: boolean; message: string }> { - try { - const parsed = createCommandSchema.safeParse(Object.fromEntries(formData)) - - if (!parsed.success) { - return { - success: false, - message: 'Invalid submission!', - } - } - - const { platform, platformEntityId, ...input } = parsed.data - - const params = new URLSearchParams({ platform, platformEntityId }) - await fetcher('/me/commands?' + params, { - method: 'POST', - body: JSON.stringify(input), - }) - - revalidateTag(`getEntityCommands-${platformEntityId}-custom`) - - return { - success: true, - message: 'Successfully!', - } - } catch (error) { - console.log('createEntityCommand =>', error) - if (error instanceof ApiError) { - if (error.status === 409) { - return { - success: false, - message: 'This command already exists.', - } - } else { - return { - success: false, - message: 'Something went wrong!', - } - } - } else { - return { - success: false, - message: 'Something went wrong!', - } - } - } -} - -/** - * - * @param formData - * @returns - */ -export async function updateEntityCommand( - formData: FormData, -): Promise<{ success: boolean; message: string }> { - try { - const parsed = updateCommandSchema.safeParse(Object.fromEntries(formData)) - - if (!parsed.success) { - return { - success: false, - message: 'Invalid submission!', - } - } - - const { id, platform, platformEntityId, ...input } = parsed.data - - const params = new URLSearchParams({ platform, platformEntityId }) - await fetcher(`/me/commands/${id}?` + params, { - method: 'PATCH', - body: JSON.stringify(input), - }) - - revalidateTag(`getEntityCommands-${platformEntityId}-custom`) - - return { - success: true, - message: 'Successfully!', - } - } catch (error) { - console.log('updateEntityCommand =>', error) - return { - success: false, - message: 'Something went wrong!', - } - } -} - -/** - * - * @param id - * @param platform - * @param platformEntityId - * @param status - * @returns - */ -export async function updateEntityCommandStatus( - id: number, - platform: Platform, - platformEntityId: string, - status: boolean, -): Promise<{ success: boolean; message: string }> { - try { - const params = new URLSearchParams({ platform, platformEntityId }) - await fetcher(`/me/commands/${id}?` + params, { - method: 'PATCH', - body: JSON.stringify({ status }), - }) - - return { - success: true, - message: 'Successfully!', - } - } catch (error) { - console.log('updateEntityCommandStatus =>', error) - return { - success: false, - message: 'Something went wrong!', - } - } -} - -/** - * - * @param id - * @param platform - * @param platformEntityId - * @returns - */ -export async function deleteEntityCommand( - id: number, - platform: Platform, - platformEntityId: string, -): Promise<{ success: boolean; message: string }> { - try { - const params = new URLSearchParams({ platform, platformEntityId }) - await fetcher(`/me/commands/${id}?` + params, { - method: 'DELETE', - }) - - revalidateTag(`getEntityCommands-${platformEntityId}-custom`) - - return { - success: true, - message: 'Successfully!', - } - } catch (error) { - console.log('deleteEntityCommand =>', error) - return { - success: false, - message: 'Something went wrong!', - } - } -} diff --git a/apps/web/src/data-layer/actions/entity.ts b/apps/web/src/data-layer/actions/entity.ts deleted file mode 100644 index a3f1583b..00000000 --- a/apps/web/src/data-layer/actions/entity.ts +++ /dev/null @@ -1,60 +0,0 @@ -'use server' - -import { revalidateTag } from 'next/cache' - -import { fetcher } from '../fetcher' - -/** - * - * @param provider - * @param providerAccountId - * @param userId - * @returns - */ -export async function linkEntity( - provider: string, - providerAccountId: string, - userId: string, -) { - return fetcher('/platforms/link', { - method: 'POST', - body: JSON.stringify({ - provider: provider, - provider_account_id: providerAccountId, - user_id: userId, - }), - }) -} - -/** - * - * @param action - * @param platform - * @param platformEntityId - * @returns - */ -export async function executeEntityAction( - action: 'join' | 'depart', - platform: Platform, - platformEntityId: string, -): Promise<{ success: boolean; message: string }> { - try { - const params = new URLSearchParams({ platform, platformEntityId }) - await fetcher(`/me/platforms/actions/${action}?` + params, { - method: 'POST', - }) - - revalidateTag('getUserEntities') - - return { - success: true, - message: 'Successfully!', - } - } catch (error) { - console.log('executeEntityAction =>', error) - return { - success: false, - message: 'Something went wrong!', - } - } -} diff --git a/apps/web/src/data-layer/queries/entity.ts b/apps/web/src/data-layer/queries/entity.ts deleted file mode 100644 index 8dec5ef8..00000000 --- a/apps/web/src/data-layer/queries/entity.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { fetcher } from '../fetcher' - -/** - * - * @param platform - * @param platformEntityId - * @returns - */ -export async function getEntityLogs( - platform: Platform, - platformEntityId: string, -): Promise { - const params = new URLSearchParams({ - noCache: 'true', - platform, - platformEntityId, - }) - - return fetcher('/me/platforms/logs?' + params, { - next: { - tags: [`getEntityLogs-${platformEntityId}`], - }, - }) -} - -/** - * - * @param platform - * @param platformEntityId - * @param type - * @returns - */ -export async function getEntityCommands( - platform: Platform, - platformEntityId: string, - type: CommandType, -): Promise { - const params = new URLSearchParams({ - noCache: 'true', - platform, - platformEntityId, - type, - }) - return fetcher('/me/commands?' + params, { - next: { - tags: [`getEntityCommands-${platformEntityId}-${type}`], - }, - }) -} diff --git a/apps/web/src/hooks/use-session.tsx b/apps/web/src/hooks/use-session.tsx new file mode 100644 index 00000000..d182da91 --- /dev/null +++ b/apps/web/src/hooks/use-session.tsx @@ -0,0 +1,8 @@ +import { cache } from 'react' + +import { auth } from '@/lib/auth' + +export const useSession = cache(async () => { + const session = await auth() + return session +}) diff --git a/apps/web/src/lib/auth.ts b/apps/web/src/lib/auth.ts index a7eed266..716e0f18 100644 --- a/apps/web/src/lib/auth.ts +++ b/apps/web/src/lib/auth.ts @@ -4,7 +4,7 @@ import Twitch from 'next-auth/providers/twitch' import { PrismaAdapter } from '@auth/prisma-adapter' -import { linkEntity } from '@/data-layer/actions/entity' +import { linkEntity } from '@/services/actions/entities' import { prisma } from './db' @@ -13,7 +13,11 @@ export const { handlers, signIn, signOut, auth } = NextAuth({ providers: [Twitch, Discord], events: { linkAccount: ({ account, user }) => { - linkEntity(account.provider, account.providerAccountId, user.id!) + linkEntity({ + provider: account.provider as 'twitch' | 'discord', + providerAccountId: account.providerAccountId, + userId: user.id!, + }) }, }, pages: { diff --git a/apps/web/src/data-layer/fetcher.ts b/apps/web/src/lib/fetcher.ts similarity index 97% rename from apps/web/src/data-layer/fetcher.ts rename to apps/web/src/lib/fetcher.ts index 8d0f24a0..5ab9f295 100644 --- a/apps/web/src/data-layer/fetcher.ts +++ b/apps/web/src/lib/fetcher.ts @@ -1,7 +1,7 @@ import { cookies } from 'next/headers' import { notFound } from 'next/navigation' -import { env } from '@/env' +import { env } from '@/config/env' const BASE_URL = env.API_URL diff --git a/apps/web/src/services/actions/auth.ts b/apps/web/src/services/actions/auth.ts new file mode 100644 index 00000000..8cfabed9 --- /dev/null +++ b/apps/web/src/services/actions/auth.ts @@ -0,0 +1,31 @@ +'use server' + +import { createServerAction } from 'zsa' + +import { signIn as _signIn, signOut as _signOut } from '@/lib/auth' + +import { signInWithProviderSchema } from '../schemas/auth' + +/** + * + */ +export const signInWithProvider = createServerAction() + .input(signInWithProviderSchema) + .handler(async ({ input }) => { + if (input.provider === 'twitch') { + await _signIn(input.provider, { reditectTo: input.redirectTo }) + } else if (input.provider === 'discord') { + await _signIn( + input.provider, + { reditectTo: input.redirectTo }, + { scope: ['identify', 'email', 'guilds'].join(' ') }, + ) + } + }) + +/** + * + */ +export const signOut = createServerAction().handler(async () => { + await _signOut({ redirectTo: '/signin' }) +}) diff --git a/apps/web/src/services/actions/commands.ts b/apps/web/src/services/actions/commands.ts new file mode 100644 index 00000000..e6c0443f --- /dev/null +++ b/apps/web/src/services/actions/commands.ts @@ -0,0 +1,112 @@ +'use server' + +import { revalidateTag } from 'next/cache' + +import { ZSAError, createServerAction } from 'zsa' + +import { ApiError, fetcher } from '@/lib/fetcher' + +import { + createCommandSchema, + deleteCommandSchema, + setCommandStatusSchema, + updateCommandSchema, +} from '../schemas/commands' + +/** + * + */ +export const createCommand = createServerAction() + .input(createCommandSchema, { + type: 'formData', + }) + .handler(async ({ input }) => { + try { + const params = new URLSearchParams() + params.append('platform', input.platform) + params.append('platformEntityId', input.platformEntityId) + + await fetcher('/me/commands?' + params, { + method: 'POST', + body: JSON.stringify(input), + }) + + revalidateTag(`getEntityCommands-${input.platformEntityId}-custom`) + } catch (error) { + console.error('createEntityCommand =>', error) + if (error instanceof ApiError) { + if (error.status === 409) { + throw new ZSAError('CONFLICT', 'This command already exists!') + } + } + throw new ZSAError('ERROR', 'Something went wrong!') + } + }) + +/** + * + */ +export const updateCommand = createServerAction() + .input(updateCommandSchema, { + type: 'formData', + }) + .handler(async ({ input }) => { + try { + const params = new URLSearchParams() + params.append('platform', input.platform) + params.append('platformEntityId', input.platformEntityId) + + await fetcher(`/me/commands/${input.id}?` + params, { + method: 'PATCH', + body: JSON.stringify(input), + }) + + revalidateTag(`getEntityCommands-${input.platformEntityId}-custom`) + } catch (error) { + console.error('updateEntityCommand =>', error) + throw new ZSAError('ERROR', 'Something went wrong!') + } + }) + +/** + * + */ +export const setCommandStatus = createServerAction() + .input(setCommandStatusSchema) + .handler(async ({ input }) => { + try { + const params = new URLSearchParams() + params.append('platform', input.platform) + params.append('platformEntityId', input.platformEntityId) + + await fetcher(`/me/commands/${input.id}?` + params, { + method: 'PATCH', + body: JSON.stringify(input), + }) + } catch (error) { + console.error('setCommandStatus =>', error) + throw new ZSAError('ERROR', 'Something went wrong!') + } + }) + +/** + * + */ +export const deleteCommand = createServerAction() + .input(deleteCommandSchema) + .handler(async ({ input }) => { + try { + const params = new URLSearchParams() + params.append('platform', input.platform) + params.append('platformEntityId', input.platformEntityId) + + await fetcher(`/me/commands/${input.id}?` + params, { + method: 'DELETE', + }) + + revalidateTag(`getEntityCommands-${input.platformEntityId}-custom`) + } catch (error) { + console.error('deleteCommand =>', error) + throw new ZSAError('ERROR', 'Something went wrong!') + } + }) diff --git a/apps/web/src/services/actions/entities.ts b/apps/web/src/services/actions/entities.ts new file mode 100644 index 00000000..1dfaea47 --- /dev/null +++ b/apps/web/src/services/actions/entities.ts @@ -0,0 +1,47 @@ +'use server' + +import { revalidateTag } from 'next/cache' + +import { ZSAError, createServerAction } from 'zsa' + +import { fetcher } from '@/lib/fetcher' + +import { executeEntityActionSchema, linkEntitySchema } from '../schemas/entity' + +/** + * + */ +export const linkEntity = createServerAction() + .input(linkEntitySchema) + .handler(async ({ input }) => { + await fetcher('/platforms/link', { + method: 'POST', + body: JSON.stringify({ + provider: input.provider, + provider_account_id: input.providerAccountId, + user_id: input.userId, + }), + }) + }) + +/** + * + */ +export const executeEntityAction = createServerAction() + .input(executeEntityActionSchema) + .handler(async ({ input }) => { + try { + const params = new URLSearchParams() + params.append('platform', input.platform) + params.append('platformEntityId', input.platformEntityId) + + await fetcher(`/me/platforms/actions/${input.action}?` + params, { + method: 'POST', + }) + + revalidateTag('getUserEntities') + } catch (error) { + console.error('executeEntityAction => ', error) + throw new ZSAError('ERROR', 'Something went wrong!') + } + }) diff --git a/apps/web/src/services/actions/livestreams.ts b/apps/web/src/services/actions/livestreams.ts new file mode 100644 index 00000000..96efdd35 --- /dev/null +++ b/apps/web/src/services/actions/livestreams.ts @@ -0,0 +1,59 @@ +'use server' + +import { revalidateTag } from 'next/cache' + +import { ZSAError, createServerAction } from 'zsa' + +import { fetcher } from '@/lib/fetcher' + +import { + createEventChannelSchema, + deleteEventChannelSchema, +} from '../schemas/livestreams' + +/** + * + */ +export const createEventChannel = createServerAction() + .input(createEventChannelSchema, { + type: 'formData', + }) + .handler(async ({ input }) => { + try { + const params = new URLSearchParams() + params.append('platformEntityId', input.platformEntityId) + + await fetcher('/me/livestreams/event-channels?' + params, { + method: 'POST', + body: JSON.stringify({ + guild_channel_id: input.guild_channel_id, + }), + }) + + revalidateTag(`getEventChannels-${input.platformEntityId}`) + } catch (error) { + console.error('createEventChannel =>', error) + throw new ZSAError('ERROR', 'Something went wrong!') + } + }) + +/** + * + */ +export const deleteEventChannel = createServerAction() + .input(deleteEventChannelSchema) + .handler(async ({ input }) => { + try { + const params = new URLSearchParams() + params.append('platformEntityId', input.platformEntityId) + + await fetcher(`/me/livestreams/event-channels/${input.id}?` + params, { + method: 'DELETE', + }) + + revalidateTag(`getEventChannels-${input.platformEntityId}`) + } catch (error) { + console.error('deleteEventChannel =>', error) + throw new ZSAError('ERROR', 'Something went wrong!') + } + }) diff --git a/apps/web/src/services/queries/commands.ts b/apps/web/src/services/queries/commands.ts new file mode 100644 index 00000000..12bd2f16 --- /dev/null +++ b/apps/web/src/services/queries/commands.ts @@ -0,0 +1,29 @@ +import { fetcher } from '@/lib/fetcher' + +import type { EntityCommand } from '@/types/command' +import type { Platform } from '@/types/platform' + +/** + * + * @param platform + * @param platformEntityId + * @param type + * @returns + */ +export async function getCommands( + platform: Platform, + platformEntityId: string, + type: 'custom' | 'global', +): Promise { + const params = new URLSearchParams() + params.append('noCache', 'true') + params.append('platform', platform) + params.append('platformEntityId', platformEntityId) + params.append('type', type) + + return fetcher('/me/commands?' + params, { + next: { + tags: [`getEntityCommands-${platformEntityId}-${type}`], + }, + }) +} diff --git a/apps/web/src/services/queries/discord.ts b/apps/web/src/services/queries/discord.ts new file mode 100644 index 00000000..35a4829e --- /dev/null +++ b/apps/web/src/services/queries/discord.ts @@ -0,0 +1,23 @@ +import { fetcher } from '@/lib/fetcher' + +import type { GuildChannel } from '@/types/discord' + +/** + * + * @param platformEntityId + * @returns + */ +export async function getDiscordGuildChannels( + platformEntityId: string, +): Promise { + const params = new URLSearchParams() + params.append('noCache', 'true') + params.append('platformEntityId', platformEntityId) + + return fetcher('/me/discord/guild-channels?' + params, { + cache: 'no-store', + next: { + tags: [`getEventChannels-${platformEntityId}`], + }, + }) +} diff --git a/apps/web/src/services/queries/entities.ts b/apps/web/src/services/queries/entities.ts new file mode 100644 index 00000000..147a67bf --- /dev/null +++ b/apps/web/src/services/queries/entities.ts @@ -0,0 +1,26 @@ +import { fetcher } from '@/lib/fetcher' + +import type { EntityLog } from '@/types/entity' +import type { Platform } from '@/types/platform' + +/** + * + * @param platform + * @param platformEntityId + * @returns + */ +export async function getEntityLogs( + platform: Platform, + platformEntityId: string, +): Promise { + const params = new URLSearchParams() + params.append('noCache', 'true') + params.append('platform', platform) + params.append('platformEntityId', platformEntityId) + + return fetcher('/me/platforms/logs?' + params, { + next: { + tags: [`getEntityLogs-${platformEntityId}`], + }, + }) +} diff --git a/apps/web/src/services/queries/livestreams.ts b/apps/web/src/services/queries/livestreams.ts new file mode 100644 index 00000000..118400a4 --- /dev/null +++ b/apps/web/src/services/queries/livestreams.ts @@ -0,0 +1,22 @@ +import { fetcher } from '@/lib/fetcher' + +import type { EventChannel } from '@/types/livestreams' + +/** + * + * @param platformEntityId + * @returns + */ +export async function getEventChannels( + platformEntityId: string, +): Promise { + const params = new URLSearchParams() + params.append('noCache', 'true') + params.append('platformEntityId', platformEntityId) + + return fetcher('/me/livestreams/event-channels?' + params, { + next: { + tags: [`getEventChannels-${platformEntityId}`], + }, + }) +} diff --git a/apps/web/src/data-layer/queries/user.ts b/apps/web/src/services/queries/users.ts similarity index 61% rename from apps/web/src/data-layer/queries/user.ts rename to apps/web/src/services/queries/users.ts index 6793e0c9..31e4ce25 100644 --- a/apps/web/src/data-layer/queries/user.ts +++ b/apps/web/src/services/queries/users.ts @@ -1,11 +1,16 @@ -import { fetcher } from '../fetcher' +import { fetcher } from '@/lib/fetcher' + +import type { UserAccount, UserEntity } from '@/types/user' /** * * @returns */ export async function getUserAccounts(): Promise { - return fetcher('/me/accounts?noCache=true', { + const params = new URLSearchParams() + params.append('noCache', 'true') + + return fetcher('/me/accounts?' + params, { next: { tags: ['getUserAccounts'], }, @@ -20,7 +25,8 @@ export async function getUserAccounts(): Promise { export async function getUserEntities( type?: 'joined' | 'not_joined', ): Promise { - const params = new URLSearchParams({ noCache: 'true' }) + const params = new URLSearchParams() + params.append('noCache', 'true') if (type) { params.append('joined', type === 'joined' ? 'true' : 'false') diff --git a/apps/web/src/services/schemas/auth.ts b/apps/web/src/services/schemas/auth.ts new file mode 100644 index 00000000..c0183ae2 --- /dev/null +++ b/apps/web/src/services/schemas/auth.ts @@ -0,0 +1,8 @@ +import { z } from 'zod' + +import { platform } from './platform' + +export const signInWithProviderSchema = z.object({ + provider: platform, + redirectTo: z.string().min(1), +}) diff --git a/apps/web/src/data-layer/validations/command.ts b/apps/web/src/services/schemas/commands.ts similarity index 59% rename from apps/web/src/data-layer/validations/command.ts rename to apps/web/src/services/schemas/commands.ts index 8f935b9f..799456bc 100644 --- a/apps/web/src/data-layer/validations/command.ts +++ b/apps/web/src/services/schemas/commands.ts @@ -1,6 +1,6 @@ import { z } from 'zod' -import { platform } from '.' +import { platform } from './platform' export const createCommandSchema = z.object({ platform: platform, @@ -20,3 +20,16 @@ export const updateCommandSchema = z.object({ command_content: z.string().min(1), status: z.coerce.boolean(), }) + +export const setCommandStatusSchema = z.object({ + id: z.number(), + platform: platform, + platformEntityId: z.string().min(1), + status: z.coerce.boolean(), +}) + +export const deleteCommandSchema = z.object({ + id: z.number(), + platform: platform, + platformEntityId: z.string().min(1), +}) diff --git a/apps/web/src/services/schemas/entity.ts b/apps/web/src/services/schemas/entity.ts new file mode 100644 index 00000000..88ba30cb --- /dev/null +++ b/apps/web/src/services/schemas/entity.ts @@ -0,0 +1,15 @@ +import { z } from 'zod' + +import { platform } from './platform' + +export const linkEntitySchema = z.object({ + provider: platform, + providerAccountId: z.string().min(1), + userId: z.string().min(1), +}) + +export const executeEntityActionSchema = z.object({ + action: z.enum(['join', 'depart']), + platform: platform, + platformEntityId: z.string().min(1), +}) diff --git a/apps/web/src/services/schemas/livestreams.ts b/apps/web/src/services/schemas/livestreams.ts new file mode 100644 index 00000000..c4d2a77e --- /dev/null +++ b/apps/web/src/services/schemas/livestreams.ts @@ -0,0 +1,11 @@ +import { z } from 'zod' + +export const createEventChannelSchema = z.object({ + platformEntityId: z.string(), + guild_channel_id: z.string(), +}) + +export const deleteEventChannelSchema = z.object({ + id: z.string(), + platformEntityId: z.string(), +}) diff --git a/apps/web/src/data-layer/validations/index.ts b/apps/web/src/services/schemas/platform.ts similarity index 100% rename from apps/web/src/data-layer/validations/index.ts rename to apps/web/src/services/schemas/platform.ts diff --git a/apps/web/src/types/command.ts b/apps/web/src/types/command.ts new file mode 100644 index 00000000..263deadd --- /dev/null +++ b/apps/web/src/types/command.ts @@ -0,0 +1,14 @@ +import type { Platform } from './platform' + +export type EntityCommand = { + id: number + name: string + content: string + status: boolean + platform: Platform + platform_entity_id: string + type: number + created_by: string + updated_by: string + created_at: string +} diff --git a/apps/web/src/types/discord.ts b/apps/web/src/types/discord.ts new file mode 100644 index 00000000..54e042b8 --- /dev/null +++ b/apps/web/src/types/discord.ts @@ -0,0 +1,6 @@ +export type GuildChannel = { + id: string + name: string + guild_id: string + type: number +} diff --git a/apps/web/src/types/entity.ts b/apps/web/src/types/entity.ts new file mode 100644 index 00000000..c564a148 --- /dev/null +++ b/apps/web/src/types/entity.ts @@ -0,0 +1,11 @@ +import type { Platform } from '@/types/platform' + +export type EntityLog = { + id: string + author: string + author_id: string + activity: string + activity_date: Date + platform: Platform + platform_entity_id: string +} diff --git a/apps/web/src/types/index.ts b/apps/web/src/types/index.ts deleted file mode 100644 index feb4f290..00000000 --- a/apps/web/src/types/index.ts +++ /dev/null @@ -1,43 +0,0 @@ -type Platform = 'twitch' | 'discord' -type CommandType = 'custom' | 'global' - -type UserAccount = { - user_id: string - account_username: string - provider: Platform - provider_account_id: string - created_at: Date - updated_at: Date -} - -type UserEntity = { - entity_name: string - entity_icon: string - entity_owner_id: string - entity_bot_joined: boolean - platform: Platform - platform_entity_id: string -} - -type EntityLog = { - id: string - author: string - author_id: string - activity: string - activity_date: Date - platform: Platform - platform_entity_id: string -} - -type EntityCommand = { - id: number - name: string - content: string - status: boolean - platform: Platform - platform_entity_id: string - type: number - created_by: string - updated_by: string - created_at: string -} diff --git a/apps/web/src/types/livestreams.ts b/apps/web/src/types/livestreams.ts new file mode 100644 index 00000000..bf66b486 --- /dev/null +++ b/apps/web/src/types/livestreams.ts @@ -0,0 +1,7 @@ +export type EventChannel = { + id: number + server_id: string + channel_id: string + created_at: string + created_by: string +} diff --git a/apps/web/src/types/platform.ts b/apps/web/src/types/platform.ts new file mode 100644 index 00000000..d3064b88 --- /dev/null +++ b/apps/web/src/types/platform.ts @@ -0,0 +1,5 @@ +import { z } from 'zod' + +import { platform } from '@/services/schemas/platform' + +export type Platform = z.infer diff --git a/apps/web/src/types/user.ts b/apps/web/src/types/user.ts new file mode 100644 index 00000000..5220a5d9 --- /dev/null +++ b/apps/web/src/types/user.ts @@ -0,0 +1,19 @@ +import type { Platform } from './platform' + +export type UserAccount = { + user_id: string + account_username: string + provider: Platform + provider_account_id: string + created_at: Date + updated_at: Date +} + +export type UserEntity = { + entity_name: string + entity_icon: string + entity_owner_id: string + entity_bot_joined: boolean + platform: Platform + platform_entity_id: string +}