diff --git a/components.json b/components.json index 42f059b..1569586 100644 --- a/components.json +++ b/components.json @@ -17,4 +17,4 @@ "lib": "@/lib", "hooks": "@/hooks" } -} \ No newline at end of file +} diff --git a/package-lock.json b/package-lock.json index 21c9e00..3d00fe1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,22 +8,28 @@ "name": "tedx", "version": "0.1.0", "dependencies": { + "@hookform/resolvers": "^3.9.0", "@libsql/client": "^0.8.1", "@next-auth/prisma-adapter": "^1.0.7", "@prisma/adapter-libsql": "^5.19.1", "@prisma/client": "^5.19.1", + "@radix-ui/react-accordion": "^1.2.0", + "@radix-ui/react-alert-dialog": "^1.1.1", "@radix-ui/react-avatar": "^1.1.0", "@radix-ui/react-checkbox": "^1.1.1", + "@radix-ui/react-collapsible": "^1.1.0", "@radix-ui/react-dialog": "^1.1.1", "@radix-ui/react-icons": "^1.3.0", "@radix-ui/react-label": "^2.1.0", "@radix-ui/react-popover": "^1.1.1", + "@radix-ui/react-select": "^2.1.1", "@radix-ui/react-slot": "^1.1.0", "@radix-ui/react-tabs": "^1.1.0", "@react-email/components": "^0.0.25", "@tanstack/react-query": "^5.56.2", "@types/ioredis": "^4.28.10", "@types/lodash.debounce": "^4.0.9", + "@uploadthing/react": "^7.0.2", "axios": "^1.7.7", "bullmq": "^5.13.0", "class-variance-authority": "^0.7.0", @@ -36,12 +42,15 @@ "next-auth": "^4.24.7", "nodemailer": "^6.9.15", "otp-generator": "^4.0.1", - "react": "^18", + "react": "^18.3.1", "react-dom": "^18", "react-email": "^3.0.1", + "react-hook-form": "^7.53.0", "resend": "^4.0.0", + "sonner": "^1.5.0", "tailwind-merge": "^2.5.2", "tailwindcss-animate": "^1.0.7", + "uploadthing": "^7.0.2", "zod": "^3.23.8" }, "devDependencies": { @@ -699,6 +708,32 @@ "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", "license": "MIT" }, + "node_modules/@effect/platform": { + "version": "0.63.2", + "resolved": "https://registry.npmjs.org/@effect/platform/-/platform-0.63.2.tgz", + "integrity": "sha512-b39pVFw0NGo/tXjGShW7Yg0M+kG7bRrFR6+dQ3aIu99ePTkTp6bGb/kDB7n+dXsFFdIqHsQGYESeYcOQngxdFQ==", + "license": "MIT", + "dependencies": { + "find-my-way-ts": "^0.1.5", + "multipasta": "^0.2.5" + }, + "peerDependencies": { + "@effect/schema": "^0.72.2", + "effect": "^3.7.2" + } + }, + "node_modules/@effect/schema": { + "version": "0.72.2", + "resolved": "https://registry.npmjs.org/@effect/schema/-/schema-0.72.2.tgz", + "integrity": "sha512-/x1BIA2pqcUidNrOMmwYe6Z58KtSgHSc5iJu7bNwIxi2LHMVuUao1BvpI5x6i7T/zkoi4dd1S6qasZzJIYDjdw==", + "license": "MIT", + "dependencies": { + "fast-check": "^3.21.0" + }, + "peerDependencies": { + "effect": "^3.7.2" + } + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.19.11", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.19.11.tgz", @@ -1157,6 +1192,15 @@ "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.7.tgz", "integrity": "sha512-X8R8Oj771YRl/w+c1HqAC1szL8zWQRwFvgDwT129k9ACdBoud/+/rX9V0qiMl6LWUdP9voC2nDVZYPMQQsb6eA==" }, + "node_modules/@hookform/resolvers": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-3.9.0.tgz", + "integrity": "sha512-bU0Gr4EepJ/EQsH/IwEzYLsT/PEj5C0ynLQ4m+GSHS+xKH4TfSelhluTgOaoc4kA5s7eCsQbM4wvZLzELmWzUg==", + "license": "MIT", + "peerDependencies": { + "react-hook-form": "^7.0.0" + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.11.14", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", @@ -2253,11 +2297,76 @@ "@prisma/debug": "5.19.1" } }, + "node_modules/@radix-ui/number": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.0.tgz", + "integrity": "sha512-V3gRzhVNU1ldS5XhAPTom1fOIo4ccrjjJgmE+LI2h/WaFpHmx0MQApT+KZHnx8abG6Avtfcz4WoEciMnpFT3HQ==", + "license": "MIT" + }, "node_modules/@radix-ui/primitive": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.0.tgz", "integrity": "sha512-4Z8dn6Upk0qk4P74xBhZ6Hd/w0mPEzOOLxy4xiPXOXqjF7jZS0VAKk7/x/H6FyY2zCkYJqePf1G5KmkmNJ4RBA==" }, + "node_modules/@radix-ui/react-accordion": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-accordion/-/react-accordion-1.2.0.tgz", + "integrity": "sha512-HJOzSX8dQqtsp/3jVxCU3CXEONF7/2jlGAB28oX8TTw1Dz8JYbEI1UcL8355PuLBE41/IRRMvCw7VkiK/jcUOQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.0", + "@radix-ui/react-collapsible": "1.1.0", + "@radix-ui/react-collection": "1.1.0", + "@radix-ui/react-compose-refs": "1.1.0", + "@radix-ui/react-context": "1.1.0", + "@radix-ui/react-direction": "1.1.0", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-primitive": "2.0.0", + "@radix-ui/react-use-controllable-state": "1.1.0" + }, + "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 + } + } + }, + "node_modules/@radix-ui/react-alert-dialog": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-alert-dialog/-/react-alert-dialog-1.1.1.tgz", + "integrity": "sha512-wmCoJwj7byuVuiLKqDLlX7ClSUU0vd9sdCeM+2Ls+uf13+cpSJoMgwysHq1SGVVkJj5Xn0XWi1NoRCdkMpr6Mw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.0", + "@radix-ui/react-compose-refs": "1.1.0", + "@radix-ui/react-context": "1.1.0", + "@radix-ui/react-dialog": "1.1.1", + "@radix-ui/react-primitive": "2.0.0", + "@radix-ui/react-slot": "1.1.0" + }, + "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 + } + } + }, "node_modules/@radix-ui/react-arrow": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.0.tgz", @@ -2334,6 +2443,36 @@ } } }, + "node_modules/@radix-ui/react-collapsible": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.1.0.tgz", + "integrity": "sha512-zQY7Epa8sTL0mq4ajSJpjgn2YmCgyrG7RsQgLp3C0LQVkG7+Tf6Pv1CeNWZLyqMjhdPkBa5Lx7wYBeSu7uCSTA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.0", + "@radix-ui/react-compose-refs": "1.1.0", + "@radix-ui/react-context": "1.1.0", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-presence": "1.1.0", + "@radix-ui/react-primitive": "2.0.0", + "@radix-ui/react-use-controllable-state": "1.1.0", + "@radix-ui/react-use-layout-effect": "1.1.0" + }, + "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 + } + } + }, "node_modules/@radix-ui/react-collection": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.0.tgz", @@ -2530,6 +2669,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.0.tgz", "integrity": "sha512-peLblDlFw/ngk3UWq0VnYaOLy6agTZZ+MUO/WhVfm14vJGML+xH4FAl2XQGLqdefjNb7ApRg6Yn7U42ZhmYXdw==", + "license": "MIT", "dependencies": { "@radix-ui/react-primitive": "2.0.0" }, @@ -2713,10 +2853,54 @@ } } }, + "node_modules/@radix-ui/react-select": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.1.1.tgz", + "integrity": "sha512-8iRDfyLtzxlprOo9IicnzvpsO1wNCkuwzzCM+Z5Rb5tNOpCdMvcc2AkzX0Fz+Tz9v6NJ5B/7EEgyZveo4FBRfQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.0", + "@radix-ui/primitive": "1.1.0", + "@radix-ui/react-collection": "1.1.0", + "@radix-ui/react-compose-refs": "1.1.0", + "@radix-ui/react-context": "1.1.0", + "@radix-ui/react-direction": "1.1.0", + "@radix-ui/react-dismissable-layer": "1.1.0", + "@radix-ui/react-focus-guards": "1.1.0", + "@radix-ui/react-focus-scope": "1.1.0", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-popper": "1.2.0", + "@radix-ui/react-portal": "1.1.1", + "@radix-ui/react-primitive": "2.0.0", + "@radix-ui/react-slot": "1.1.0", + "@radix-ui/react-use-callback-ref": "1.1.0", + "@radix-ui/react-use-controllable-state": "1.1.0", + "@radix-ui/react-use-layout-effect": "1.1.0", + "@radix-ui/react-use-previous": "1.1.0", + "@radix-ui/react-visually-hidden": "1.1.0", + "aria-hidden": "^1.1.1", + "react-remove-scroll": "2.5.7" + }, + "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 + } + } + }, "node_modules/@radix-ui/react-slot": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.0.tgz", "integrity": "sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw==", + "license": "MIT", "dependencies": { "@radix-ui/react-compose-refs": "1.1.0" }, @@ -2869,6 +3053,29 @@ } } }, + "node_modules/@radix-ui/react-visually-hidden": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.1.0.tgz", + "integrity": "sha512-N8MDZqtgCgG5S3aV60INAB475osJousYpZ4cTJ2cFbMpdHS5Y6loLTH8LPtkj2QN0x93J30HT/M3qJXM0+lyeQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.0.0" + }, + "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 + } + } + }, "node_modules/@radix-ui/rect": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.0.tgz", @@ -3589,6 +3796,72 @@ "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", "dev": true }, + "node_modules/@uploadthing/dropzone": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@uploadthing/dropzone/-/dropzone-0.4.1.tgz", + "integrity": "sha512-RHSpo/2kg/mrRSYQA4EKlyvkOCYWOeE2+QQYW9YiUvWCuawnTfD7DQvk8RN/nYXi1Sw7/v0NegmQpiVELVGtnA==", + "license": "MIT", + "dependencies": { + "file-selector": "^0.6.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "solid-js": "^1.7.11", + "svelte": "^4.2.12", + "vue": "^3.4.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "solid-js": { + "optional": true + }, + "svelte": { + "optional": true + }, + "vue": { + "optional": true + } + } + }, + "node_modules/@uploadthing/mime-types": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@uploadthing/mime-types/-/mime-types-0.3.0.tgz", + "integrity": "sha512-jN/XFvpKTzcd3MXT/9D9oxx05scnYiSYxAXF/e6hyg377zFducRxivU/kHyYTkpUZPTmOL5q9EQbOkUsXMlSMg==", + "license": "MIT" + }, + "node_modules/@uploadthing/react": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@uploadthing/react/-/react-7.0.2.tgz", + "integrity": "sha512-343zTofWBo1yR+31/oP75WCTCC1lUuyAhfoqgJ0MEY64i15KoQAaTe3ICSNnXyeRaHCmJSQeK3hbEl44QGq/iQ==", + "license": "MIT", + "dependencies": { + "@uploadthing/dropzone": "0.4.1", + "@uploadthing/shared": "7.0.2" + }, + "peerDependencies": { + "next": "*", + "react": "^17.0.2 || ^18.0.0", + "uploadthing": "7.0.2" + }, + "peerDependenciesMeta": { + "next": { + "optional": true + } + } + }, + "node_modules/@uploadthing/shared": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@uploadthing/shared/-/shared-7.0.2.tgz", + "integrity": "sha512-yhE7lA42m6g7Qw245Ey/8uK5J4d8FJhOg90VVt0PG1iJYvBZHbboSq1ndsGZ1X8jFaskBVORzWc66gEw43FEsQ==", + "license": "MIT", + "dependencies": { + "@uploadthing/mime-types": "0.3.0", + "effect": "3.7.2", + "sqids": "^0.3.0" + } + }, "node_modules/abbrev": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-2.0.0.tgz", @@ -5095,6 +5368,21 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/effect": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/effect/-/effect-3.7.2.tgz", + "integrity": "sha512-pV7l1+LSZFvVObj4zuy4nYiBaC7qZOfrKV6s/Ef4p3KueiQwZFgamazklwyZ+x7Nyj2etRDFvHE/xkThTfQD1w==", + "license": "MIT" + }, + "node_modules/effect-log": { + "version": "0.32.0", + "resolved": "https://registry.npmjs.org/effect-log/-/effect-log-0.32.0.tgz", + "integrity": "sha512-zlh4S+zBkHeDhiV5IAAXwecqxASVJk9tYNJUb12EuJqgtRGGyhrXYNn8zz5Gk/w7PmnNLTl9Vb6bQo2BFn+J/Q==", + "license": "MIT", + "peerDependencies": { + "effect": "^3.7.0" + } + }, "node_modules/electron-to-chromium": { "version": "1.5.19", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.19.tgz", @@ -5927,6 +6215,28 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/fast-check": { + "version": "3.22.0", + "resolved": "https://registry.npmjs.org/fast-check/-/fast-check-3.22.0.tgz", + "integrity": "sha512-8HKz3qXqnHYp/VCNn2qfjHdAdcI8zcSqOyX64GOMukp7SL2bfzfeDKjSd+UyECtejccaZv3LcvZTm9YDD22iCQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT", + "dependencies": { + "pure-rand": "^6.1.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -6022,6 +6332,18 @@ "node": "^10.12.0 || >=12.0.0" } }, + "node_modules/file-selector": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/file-selector/-/file-selector-0.6.0.tgz", + "integrity": "sha512-QlZ5yJC0VxHxQQsQhXvBaC7VRJ2uaxTf+Tfpu4Z/OcVQJVpZO+DGU0rkoVW5ce2SccxugvpBJoMvUs59iILYdw==", + "license": "MIT", + "dependencies": { + "tslib": "^2.4.0" + }, + "engines": { + "node": ">= 12" + } + }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -6033,6 +6355,12 @@ "node": ">=8" } }, + "node_modules/find-my-way-ts": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/find-my-way-ts/-/find-my-way-ts-0.1.5.tgz", + "integrity": "sha512-4GOTMrpGQVzsCH2ruUn2vmwzV/02zF4q+ybhCIrw/Rkt3L8KWcycdC6aJMctJzwN4fXD4SD5F/4B9Sksh5rE0A==", + "license": "MIT" + }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -8610,6 +8938,12 @@ "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.3" } }, + "node_modules/multipasta": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/multipasta/-/multipasta-0.2.5.tgz", + "integrity": "sha512-c8eMDb1WwZcE02WVjHoOmUVk7fnKU/RmUcosHACglrWAuPQsEJv+E8430sXj6jNc1jHw0zrS16aCjQh4BcEb4A==", + "license": "MIT" + }, "node_modules/mz": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", @@ -9677,6 +10011,7 @@ "version": "18.3.1", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", "dependencies": { "loose-envify": "^1.1.0" }, @@ -10070,6 +10405,22 @@ "semver": "bin/semver.js" } }, + "node_modules/react-hook-form": { + "version": "7.53.0", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.53.0.tgz", + "integrity": "sha512-M1n3HhqCww6S2hxLxciEXy2oISPnAzxY7gvwVPrtlczTM/1dDadXgUxDpHMrMTblDOcm/AXtXxHwZ3jpg1mqKQ==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-hook-form" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17 || ^18 || ^19" + } + }, "node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", @@ -10671,6 +11022,16 @@ "node": ">=10.0.0" } }, + "node_modules/sonner": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/sonner/-/sonner-1.5.0.tgz", + "integrity": "sha512-FBjhG/gnnbN6FY0jaNnqZOMmB73R+5IiyYAw8yBj7L54ER7HB3fOSE5OFiQiE2iXWxeXKvg6fIP4LtVppHEdJA==", + "license": "MIT", + "peerDependencies": { + "react": "^18.0.0", + "react-dom": "^18.0.0" + } + }, "node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -10704,6 +11065,12 @@ "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", "license": "BSD-3-Clause" }, + "node_modules/sqids": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/sqids/-/sqids-0.3.0.tgz", + "integrity": "sha512-lOQK1ucVg+W6n3FhRwwSeUijxe93b51Bfz5PMRMihVf1iVkl82ePQG7V5vwrhzB11v0NtsR25PSZRGiSomJaJw==", + "license": "MIT" + }, "node_modules/stack-utils": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", @@ -11396,6 +11763,47 @@ "browserslist": ">= 4.21.0" } }, + "node_modules/uploadthing": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/uploadthing/-/uploadthing-7.0.2.tgz", + "integrity": "sha512-B5/r0nmOfWjo+cGvZLyDQ8jypJYWoW/VsmoL+VqkiGMg5yIkKzMGJ5InrDOJS+3WQBbW8KdhVrRyA+mGSZEGUw==", + "license": "MIT", + "dependencies": { + "@effect/platform": "0.63.2", + "@effect/schema": "0.72.2", + "@uploadthing/mime-types": "0.3.0", + "@uploadthing/shared": "7.0.2", + "effect": "3.7.2", + "effect-log": "0.32.0" + }, + "engines": { + "node": ">=18.13.0" + }, + "peerDependencies": { + "express": "*", + "fastify": "*", + "h3": "*", + "next": "*", + "tailwindcss": "*" + }, + "peerDependenciesMeta": { + "express": { + "optional": true + }, + "fastify": { + "optional": true + }, + "h3": { + "optional": true + }, + "next": { + "optional": true + }, + "tailwindcss": { + "optional": true + } + } + }, "node_modules/uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", diff --git a/package.json b/package.json index fb5f991..7606a45 100644 --- a/package.json +++ b/package.json @@ -11,22 +11,28 @@ "migrate-latest": "export $(grep -v '^#' .env | xargs) && LATEST_MIGRATION=$(find prisma/migrations/*/ -type d -name '[0-9]*_*' | sort -r | head -n 1) && turso db shell $TURSO_DB_NAME < ${LATEST_MIGRATION}migration.sql" }, "dependencies": { + "@hookform/resolvers": "^3.9.0", "@libsql/client": "^0.8.1", "@next-auth/prisma-adapter": "^1.0.7", "@prisma/adapter-libsql": "^5.19.1", "@prisma/client": "^5.19.1", + "@radix-ui/react-accordion": "^1.2.0", + "@radix-ui/react-alert-dialog": "^1.1.1", "@radix-ui/react-avatar": "^1.1.0", "@radix-ui/react-checkbox": "^1.1.1", + "@radix-ui/react-collapsible": "^1.1.0", "@radix-ui/react-dialog": "^1.1.1", "@radix-ui/react-icons": "^1.3.0", "@radix-ui/react-label": "^2.1.0", "@radix-ui/react-popover": "^1.1.1", + "@radix-ui/react-select": "^2.1.1", "@radix-ui/react-slot": "^1.1.0", "@radix-ui/react-tabs": "^1.1.0", "@react-email/components": "^0.0.25", "@tanstack/react-query": "^5.56.2", "@types/ioredis": "^4.28.10", "@types/lodash.debounce": "^4.0.9", + "@uploadthing/react": "^7.0.2", "axios": "^1.7.7", "bullmq": "^5.13.0", "class-variance-authority": "^0.7.0", @@ -39,12 +45,15 @@ "next-auth": "^4.24.7", "nodemailer": "^6.9.15", "otp-generator": "^4.0.1", - "react": "^18", + "react": "^18.3.1", "react-dom": "^18", "react-email": "^3.0.1", + "react-hook-form": "^7.53.0", "resend": "^4.0.0", + "sonner": "^1.5.0", "tailwind-merge": "^2.5.2", "tailwindcss-animate": "^1.0.7", + "uploadthing": "^7.0.2", "zod": "^3.23.8" }, "devDependencies": { diff --git a/prisma/dev.db b/prisma/dev.db index aa2a551..32df833 100644 Binary files a/prisma/dev.db and b/prisma/dev.db differ diff --git a/prisma/dev.db-journal b/prisma/dev.db-journal index 8f0b674..08a0f40 100644 Binary files a/prisma/dev.db-journal and b/prisma/dev.db-journal differ diff --git a/prisma/migrations/20240926074940_form/migration.sql b/prisma/migrations/20240926074940_form/migration.sql new file mode 100644 index 0000000..8267238 --- /dev/null +++ b/prisma/migrations/20240926074940_form/migration.sql @@ -0,0 +1,42 @@ +/* + Warnings: + + - You are about to drop the `VerificationRequest` table. If the table is not empty, all the data it contains will be lost. + - You are about to drop the column `email_verified` on the `Form` table. All the data in the column will be lost. + - You are about to drop the column `referral_id` on the `Form` table. All the data in the column will be lost. + - Added the required column `paid_amount` to the `Form` table without a default value. This is not possible if the table is not empty. + +*/ +-- DropIndex +DROP INDEX "VerificationRequest_identifier_otp_key"; + +-- DropTable +PRAGMA foreign_keys=off; +DROP TABLE "VerificationRequest"; +PRAGMA foreign_keys=on; + +-- RedefineTables +PRAGMA defer_foreign_keys=ON; +PRAGMA foreign_keys=OFF; +CREATE TABLE "new_Form" ( + "id" TEXT NOT NULL PRIMARY KEY, + "name" TEXT NOT NULL, + "usn" TEXT NOT NULL, + "email" TEXT NOT NULL, + "contact" TEXT, + "designation" TEXT, + "photo_url" TEXT NOT NULL, + "college_id_card" TEXT, + "entity_name" TEXT NOT NULL, + "referral_used" TEXT, + "paid_amount" REAL NOT NULL, + "created_by_id" TEXT NOT NULL, + "created_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT "Form_created_by_id_fkey" FOREIGN KEY ("created_by_id") REFERENCES "User" ("id") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT "Form_referral_used_fkey" FOREIGN KEY ("referral_used") REFERENCES "Referral" ("code") ON DELETE SET NULL ON UPDATE CASCADE +); +INSERT INTO "new_Form" ("college_id_card", "contact", "created_at", "created_by_id", "designation", "email", "entity_name", "id", "name", "photo_url", "usn") SELECT "college_id_card", "contact", "created_at", "created_by_id", "designation", "email", "entity_name", "id", "name", "photo_url", "usn" FROM "Form"; +DROP TABLE "Form"; +ALTER TABLE "new_Form" RENAME TO "Form"; +PRAGMA foreign_keys=ON; +PRAGMA defer_foreign_keys=OFF; diff --git a/prisma/migrations/20240926091757_tip/migration.sql b/prisma/migrations/20240926091757_tip/migration.sql new file mode 100644 index 0000000..0b6548c --- /dev/null +++ b/prisma/migrations/20240926091757_tip/migration.sql @@ -0,0 +1,20 @@ +-- RedefineTables +PRAGMA defer_foreign_keys=ON; +PRAGMA foreign_keys=OFF; +CREATE TABLE "new_Referral" ( + "id" TEXT NOT NULL PRIMARY KEY, + "code" TEXT NOT NULL, + "discount_percentage" TEXT NOT NULL DEFAULT '20', + "createdby_id" TEXT NOT NULL, + "usedby_id" TEXT, + "isUsed" BOOLEAN NOT NULL DEFAULT false, + "created_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT "Referral_createdby_id_fkey" FOREIGN KEY ("createdby_id") REFERENCES "User" ("id") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT "Referral_usedby_id_fkey" FOREIGN KEY ("usedby_id") REFERENCES "User" ("email") ON DELETE SET NULL ON UPDATE CASCADE +); +INSERT INTO "new_Referral" ("code", "created_at", "createdby_id", "discount_percentage", "id", "isUsed", "usedby_id") SELECT "code", "created_at", "createdby_id", "discount_percentage", "id", "isUsed", "usedby_id" FROM "Referral"; +DROP TABLE "Referral"; +ALTER TABLE "new_Referral" RENAME TO "Referral"; +CREATE UNIQUE INDEX "Referral_code_key" ON "Referral"("code"); +PRAGMA foreign_keys=ON; +PRAGMA defer_foreign_keys=OFF; diff --git a/prisma/migrations/20240926092604_usn/migration.sql b/prisma/migrations/20240926092604_usn/migration.sql new file mode 100644 index 0000000..00ebd76 --- /dev/null +++ b/prisma/migrations/20240926092604_usn/migration.sql @@ -0,0 +1,26 @@ +-- RedefineTables +PRAGMA defer_foreign_keys=ON; +PRAGMA foreign_keys=OFF; +CREATE TABLE "new_Form" ( + "id" TEXT NOT NULL PRIMARY KEY, + "name" TEXT NOT NULL, + "usn" TEXT, + "email" TEXT NOT NULL, + "contact" TEXT, + "designation" TEXT, + "photo_url" TEXT NOT NULL, + "college_id_card" TEXT, + "entity_name" TEXT NOT NULL, + "referral_used" TEXT, + "paid_amount" REAL NOT NULL, + "created_by_id" TEXT NOT NULL, + "created_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT "Form_created_by_id_fkey" FOREIGN KEY ("created_by_id") REFERENCES "User" ("id") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT "Form_referral_used_fkey" FOREIGN KEY ("referral_used") REFERENCES "Referral" ("code") ON DELETE SET NULL ON UPDATE CASCADE +); +INSERT INTO "new_Form" ("college_id_card", "contact", "created_at", "created_by_id", "designation", "email", "entity_name", "id", "name", "paid_amount", "photo_url", "referral_used", "usn") SELECT "college_id_card", "contact", "created_at", "created_by_id", "designation", "email", "entity_name", "id", "name", "paid_amount", "photo_url", "referral_used", "usn" FROM "Form"; +DROP TABLE "Form"; +ALTER TABLE "new_Form" RENAME TO "Form"; +CREATE UNIQUE INDEX "Form_usn_key" ON "Form"("usn"); +PRAGMA foreign_keys=ON; +PRAGMA defer_foreign_keys=OFF; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 5be0632..8348fce 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -8,12 +8,6 @@ datasource db { url = "file:./dev.db" } -// enum Role { -// ADMIN -// USER -// MODERATOR -// } - model User { id String @id @default(cuid()) name String? @@ -68,42 +62,30 @@ model Referral { discountPercentage String @default("20") @map("discount_percentage") createdById String @map("createdby_id") usedById String? @map("usedby_id") - isUsed Boolean @default(false) // Whether the referral has been used or not, itll use only once + isUsed Boolean @default(false) created_at DateTime @default(now()) - // Relations createdBy User @relation("createdByUser", fields: [createdById], references: [id], onDelete: Cascade) // Admin who created the referral - usedBy User? @relation("usedByUser", fields: [usedById], references: [id]) // User who used the referral (nullable) + usedBy User? @relation("usedByUser", fields: [usedById], references: [email]) forms Form[] } model Form { id String @id @default(cuid()) name String - usn String + usn String? email String - emailVerified Boolean @default(false) @map("email_verified") contact String? designation String? photo String @map("photo_url") // URL of the photo uploaded by the participan collegeIdCard String? @map("college_id_card") entityName String @map("entity_name") - referralId String? @map("referral_id") + referralUsed String? @map("referral_used") + paidAmount Float @map("paid_amount") createdById String @map("created_by_id") created_at DateTime @default(now()) //registrationEmailSent Boolean? @default(false) @map("registration_email_sent") user User @relation(fields: [createdById], references: [id], onDelete: Cascade) - referral Referral? @relation(fields: [referralId], references: [id]) -} - -model VerificationRequest { - id String @id @default(cuid()) - identifier String // Like email or phone number - otp String - expires DateTime - created_at DateTime @default(now()) - updated_at DateTime @updatedAt - - @@unique([identifier, otp]) // Ensure OTP is unique for a given identifier + referral Referral? @relation(fields: [referralUsed], references: [code]) } diff --git a/src/app/actions/change-role.ts b/src/app/actions/change-role.ts index e3495df..b28fea2 100644 --- a/src/app/actions/change-role.ts +++ b/src/app/actions/change-role.ts @@ -4,22 +4,22 @@ import prisma from "@/server/db"; import { revalidatePath } from "next/cache"; async function updateUserRole(id: string, role: string) { - try { - const updatedUser = await prisma.user.update({ - where: { id }, - data: { role }, - }); - revalidatePath("/admin/users"); - return updatedUser; - } catch (error) { - console.error("Error updating user role:", error); - return null; - } + try { + const updatedUser = await prisma.user.update({ + where: { id }, + data: { role }, + }); + revalidatePath("/admin/users"); + return updatedUser; + } catch (error) { + console.error("Error updating user role:", error); + return null; + } } export const makeAdmin = async (userId: string) => { - return await updateUserRole(userId, "ADMIN"); + return await updateUserRole(userId, "ADMIN"); }; export const makeParticipant = async (userId: string) => { - return await updateUserRole(userId, "PARTICIPANT"); + return await updateUserRole(userId, "PARTICIPANT"); }; diff --git a/src/app/actions/create-coupon-code.ts b/src/app/actions/create-coupon-code.ts index 83c4c89..b659ffb 100644 --- a/src/app/actions/create-coupon-code.ts +++ b/src/app/actions/create-coupon-code.ts @@ -1,13 +1,17 @@ "use server"; import prisma from "@/server/db"; -export const saveCoupon = async (coupon: string, id: string, discount: string = "20") => { - const resp = await prisma.referral.create({ - data: { - code: coupon, - isUsed: false, - createdById: id, - discountPercentage: discount, - }, - }); +export const saveCoupon = async ( + coupon: string, + id: string, + discount: string = "20", +) => { + const resp = await prisma.referral.create({ + data: { + code: coupon, + isUsed: false, + createdById: id, + discountPercentage: discount, + }, + }); }; diff --git a/src/app/actions/get-price.ts b/src/app/actions/get-price.ts index a15c398..a8acbb3 100644 --- a/src/app/actions/get-price.ts +++ b/src/app/actions/get-price.ts @@ -1,36 +1,42 @@ "use server"; import prisma from "@/server/db"; +import { basePrice, initialdiscount } from "@/constants"; class CouponError extends Error { - constructor(message: string) { - super(message); - this.name = "CouponError"; - } + constructor(message: string) { + super(message); + this.name = "CouponError"; + } } -export const getPrice = async (couponCode?: string): Promise => { - const basePrice = 1000; - if (couponCode) { - const coupon = await prisma.referral.findUnique({ - where: { code: couponCode }, - }); - if (!coupon) { - throw new CouponError("Coupon code not found"); - } - - if (coupon.isUsed) { - throw new CouponError("Coupon code is already used"); - } - const discountPercentage = parseFloat(coupon.discountPercentage ?? "0"); +export const getPrice = async ( + couponCode?: string, +): Promise<{ + basePrice: number; + discountAmount: number; + finalPrice: number; +}> => { + let discountAmount = initialdiscount; + let finalPrice = basePrice; + if (couponCode) { + const coupon = await prisma.referral.findUnique({ + where: { code: couponCode }, + }); + if (!coupon) { + throw new CouponError("Coupon code not found"); + } - if (isNaN(discountPercentage)) { - throw new CouponError("Invalid discount percentage format"); - } - const discountAmount = basePrice * (discountPercentage / 100); - const finalPrice = Math.floor(basePrice - discountAmount); + if (coupon.isUsed) { + throw new CouponError("Coupon code is already used"); + } + const discountPercentage = parseFloat(coupon.discountPercentage ?? "0"); - return finalPrice; + if (isNaN(discountPercentage)) { + throw new CouponError("Invalid discount percentage format"); } - return basePrice; + discountAmount = basePrice * (discountPercentage / 100); + finalPrice = Math.floor(basePrice - discountAmount); + } + return { basePrice, discountAmount, finalPrice }; }; diff --git a/src/app/admin/page.tsx b/src/app/admin/page.tsx index 2cb1948..216dd1e 100644 --- a/src/app/admin/page.tsx +++ b/src/app/admin/page.tsx @@ -2,17 +2,17 @@ import { Coupon } from "@/components/Admin/code-generation-card"; import { useSession } from "next-auth/react"; export default function AdminPage() { - const { data: session } = useSession(); + const { data: session } = useSession(); - if (!session || session.user.role != "ADMIN") { - return
Unauthorized
; - } + if (!session || session.user.role != "ADMIN") { + return
Unauthorized
; + } - return ( - <> -
- -
- - ); + return ( + <> +
+ +
+ + ); } diff --git a/src/app/admin/users/page.tsx b/src/app/admin/users/page.tsx index eeb6bb9..075768b 100644 --- a/src/app/admin/users/page.tsx +++ b/src/app/admin/users/page.tsx @@ -2,32 +2,32 @@ import UsersList from "@/components/Admin/user-list"; import prisma from "@/server/db"; export default async function Users() { - let initialUserData = await prisma.user.findMany({ - select: { - id: true, - name: true, - email: true, - role: true, - image: true, - }, - take: 10, - }); - if (initialUserData === null) { - initialUserData = [ - { - id: "1", - name: "Test name", - email: "testEmail@gmail.com", - role: "PARTICIPANT", - image: "https://i.pravatar.cc/300?img=1", - }, - ]; - } - return ( - <> -
- -
- - ); + let initialUserData = await prisma.user.findMany({ + select: { + id: true, + name: true, + email: true, + role: true, + image: true, + }, + take: 10, + }); + if (initialUserData === null) { + initialUserData = [ + { + id: "1", + name: "Test name", + email: "testEmail@gmail.com", + role: "PARTICIPANT", + image: "https://i.pravatar.cc/300?img=1", + }, + ]; + } + return ( + <> +
+ +
+ + ); } diff --git a/src/app/api/(verification)/send-mail/route.ts b/src/app/api/(verification)/send-mail/route.ts index 7b64a72..e2a7fa5 100644 --- a/src/app/api/(verification)/send-mail/route.ts +++ b/src/app/api/(verification)/send-mail/route.ts @@ -11,28 +11,28 @@ export async function POST(req: NextRequest) { const body = await req.json(); const parsedBody = emailSchema.parse(body); - const otp = otpGenerator.generate(6, { - upperCaseAlphabets: false, - lowerCaseAlphabets: false, - specialChars: false, - }); + // const otp = otpGenerator.generate(6, { + // upperCaseAlphabets: false, + // lowerCaseAlphabets: false, + // specialChars: false, + // }); - const expiresIn = 10; // OTP valid for 10 minutes - const expiresAt = new Date(Date.now() + expiresIn * 60 * 1000); + // const expiresIn = 10; // OTP valid for 10 minutes + // const expiresAt = new Date(Date.now() + expiresIn * 60 * 1000); - await prisma.verificationRequest.create({ - data: { - identifier: parsedBody.email, - otp, - expires: expiresAt, - }, - }); + // await prisma.verificationRequest.create({ + // data: { + // identifier: parsedBody.email, + // otp, + // expires: expiresAt, + // }, + // }); - const mailResponse = await MailUsingResend({ - email: parsedBody.email, - name: parsedBody.name, - OTP: otp, - }); + // const mailResponse = await MailUsingResend({ + // email: parsedBody.email, + // name: parsedBody.name, + // OTP: otp, + // }); // const mailResponse1 = await addToQueue({ // email: parsedBody.email, @@ -42,23 +42,22 @@ export async function POST(req: NextRequest) { return NextResponse.json({ message: "Email sent successfully!", - mailResponse, + // mailResponse, }); - - } catch (error:unknown) { + } catch (error: unknown) { const errorMessage = getErrorMessage(error); if (error instanceof z.ZodError) { return NextResponse.json( { message: "Validation error", errors: errorMessage }, - { status: 400 } + { status: 400 }, ); } // Handle general server errors return NextResponse.json( { message: "Internal Server Error", errorMessage }, - { status: 500 } + { status: 500 }, ); } } diff --git a/src/app/api/(verification)/verify-mail/route.ts b/src/app/api/(verification)/verify-mail/route.ts index 84c02f6..676c353 100644 --- a/src/app/api/(verification)/verify-mail/route.ts +++ b/src/app/api/(verification)/verify-mail/route.ts @@ -14,37 +14,37 @@ export async function POST(req: NextRequest) { } try { await prisma.$transaction(async (tx) => { - const request = await tx.verificationRequest.findFirst({ - where: { - identifier, - otp, - expires: { - gte: new Date(), - }, - }, - orderBy: { - created_at: "desc", - }, - }); + // const request = await tx.verificationRequest.findFirst({ + // where: { + // identifier, + // otp, + // expires: { + // gte: new Date(), + // }, + // }, + // orderBy: { + // created_at: "desc", + // }, + // }); - if (!request) { - throw new Error("Verification failed: Invalid or expired OTP"); - } + // if (!request) { + // throw new Error("Verification failed: Invalid or expired OTP"); + // } - await tx.form.updateMany({ - where: { - email: identifier, - }, - data: { - emailVerified: true, - }, - }); + // await tx.form.updateMany({ + // where: { + // email: identifier, + // }, + // data: { + // emailVerified: true, + // }, + // }); - await tx.verificationRequest.deleteMany({ - where: { - identifier, - }, - }); + // await tx.verificationRequest.deleteMany({ + // where: { + // identifier, + // }, + // }); }); return NextResponse.json( diff --git a/src/app/api/auth/[...nextauth]/route.ts b/src/app/api/auth/[...nextauth]/route.ts index b32de7d..bc8c90e 100644 --- a/src/app/api/auth/[...nextauth]/route.ts +++ b/src/app/api/auth/[...nextauth]/route.ts @@ -1,3 +1,3 @@ import { handlers } from "@/lib/auth-options"; -export { handlers as GET, handlers as POST }; \ No newline at end of file +export { handlers as GET, handlers as POST }; diff --git a/src/app/api/submit-form/route.ts b/src/app/api/submit-form/route.ts index 35880ef..7abaa15 100644 --- a/src/app/api/submit-form/route.ts +++ b/src/app/api/submit-form/route.ts @@ -1,8 +1,8 @@ -import type { NextApiRequest, NextApiResponse } from "next"; import { NextRequest, NextResponse } from "next/server"; import { RegistrationFormSchema, TRegistrationForm } from "@/utils/zod-schemas"; import prisma from "@/server/db"; -import determinePrice from "@/utils/determinePrice"; +import { getPrice } from "@/app/actions/get-price"; +import getErrorMessage from "@/utils/getErrorMessage"; export async function GET() { return NextResponse.json({ message: "Hello from the API!" }); @@ -18,7 +18,7 @@ export async function POST(req: NextRequest, res: NextResponse) { if (!validationResult.success) { throw new Error( "Validation error: " + - validationResult.error.errors.map((e) => e.message).join(", ") + validationResult.error.errors.map((e) => e.message).join(", "), ); } @@ -31,36 +31,46 @@ export async function POST(req: NextRequest, res: NextResponse) { photo, collegeIdCard, entityName, - referralId, + referralUsed, createdById, } = body; - let price = await determinePrice(email, referralId); + let { finalPrice } = await getPrice(referralUsed); - const newFormEntry = await prisma.form.create({ - data: { - name, - usn, - email, - contact, - designation, - photo, - collegeIdCard, - entityName, - referralId, - createdById, - }, - }); + await prisma.$transaction(async (tx) => { + prisma.referral.update({ + where: { + code: referralUsed, + }, + data: { + usedById: email, + isUsed: true, + }, + }); - return NextResponse.json({ newFormEntry }, { status: 201 }); + const newFormEntry = await prisma.form.create({ + data: { + name, + usn: usn || "", + email, + contact, + designation, + photo, + collegeIdCard, + entityName, + referralUsed, + paidAmount: finalPrice, + createdById, + }, + }); + return NextResponse.json({ newFormEntry }, { status: 201 }); + }); } catch (error: any) { console.error(error); + const message = getErrorMessage(error); return NextResponse.json( - { error: error.message || "An error occurred" }, - { status: 400 } // Error response + { error: message || "An error occurred" }, + { status: 400 }, // Error response ); } } - - - diff --git a/src/app/api/uploadthing/core.ts b/src/app/api/uploadthing/core.ts new file mode 100644 index 0000000..b0c641e --- /dev/null +++ b/src/app/api/uploadthing/core.ts @@ -0,0 +1,34 @@ +import { getServerSideSession } from "@/lib/get-server-session"; +import { createUploadthing, type FileRouter } from "uploadthing/next"; +import { UploadThingError } from "uploadthing/server"; + +const f = createUploadthing(); + +const auth = async (req: Request) => { + const session = await getServerSideSession(); + if (session && session.user) { + return session.user; + } else return null; +}; + +export const ourFileRouter = { + imageUploader: f({ image: { maxFileSize: "4MB" } }) + .middleware(async ({ req }) => { + console.log("Middleware for imageUploader", req.url); + const user = await auth(req); + + if (!user) throw new UploadThingError("Unauthorized"); + + return { userId: user.id }; + }) + .onUploadComplete(async ({ metadata, file }) => { + // console.log("Upload complete for userId:", metadata.userId); + + // console.log("file url", file.url); + + // !!! Whatever is returned here is sent to the clientside `onClientUploadComplete` callback + return { uploadedBy: metadata.userId }; + }), +} satisfies FileRouter; + +export type OurFileRouter = typeof ourFileRouter; diff --git a/src/app/api/uploadthing/route.ts b/src/app/api/uploadthing/route.ts new file mode 100644 index 0000000..81af864 --- /dev/null +++ b/src/app/api/uploadthing/route.ts @@ -0,0 +1,11 @@ +import { createRouteHandler } from "uploadthing/next"; + +import { ourFileRouter } from "./core"; + +// Export routes for Next App Router +export const { GET, POST } = createRouteHandler({ + router: ourFileRouter, + + // Apply an (optional) custom config: + // config: { ... }, +}); diff --git a/src/app/api/users/route.ts b/src/app/api/users/route.ts index 804e89a..84b9f0a 100644 --- a/src/app/api/users/route.ts +++ b/src/app/api/users/route.ts @@ -2,46 +2,49 @@ import prisma from "@/server/db"; import { NextResponse } from "next/server"; export async function GET(req: Request) { - try { - const { searchParams } = new URL(req.url); + try { + const { searchParams } = new URL(req.url); - const page = Math.max(1, parseInt(searchParams.get("page") || "1", 10)); - const search = searchParams.get("search") || ""; - const limit = 10; + const page = Math.max(1, parseInt(searchParams.get("page") || "1", 10)); + const search = searchParams.get("search") || ""; + const limit = 10; - const [users, totalCount] = await Promise.all([ - prisma.user.findMany({ - skip: (page - 1) * limit, - take: limit, - where: { - name: { - contains: search, - }, - }, - }), - prisma.user.count({ // Get the total number of users for pagination - where: { - name: { - contains: search, - }, - }, - }) - ]); + const [users, totalCount] = await Promise.all([ + prisma.user.findMany({ + skip: (page - 1) * limit, + take: limit, + where: { + name: { + contains: search, + }, + }, + }), + prisma.user.count({ + // Get the total number of users for pagination + where: { + name: { + contains: search, + }, + }, + }), + ]); - const totalPages = Math.ceil(totalCount / limit); + const totalPages = Math.ceil(totalCount / limit); - return NextResponse.json({ - users, - pagination: { - currentPage: page, - totalPages, - totalCount, - limit - } - }); - - } catch (error) { - console.error("Failed to fetch users:", error); - return NextResponse.json({ message: "Failed to fetch users", status: 500 }, { status: 500 }); - } + return NextResponse.json({ + users, + pagination: { + currentPage: page, + totalPages, + totalCount, + limit, + }, + }); + } catch (error) { + console.error("Failed to fetch users:", error); + return NextResponse.json( + { message: "Failed to fetch users", status: 500 }, + { status: 500 }, + ); + } } diff --git a/src/app/layout.tsx b/src/app/layout.tsx index a0fd797..75412c8 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,25 +1,25 @@ +import Providers from "@/components/Layout/Provider"; import type { Metadata } from "next"; import { Inter } from "next/font/google"; import "./globals.css"; -import Providers from "@/components/Layout/Provider"; const inter = Inter({ subsets: ["latin"] }); export const metadata: Metadata = { - title: "Create Next App", - description: "Generated by create next app", + title: "Create Next App", + description: "Generated by create next app", }; export default function RootLayout({ - children, + children, }: Readonly<{ - children: React.ReactNode; + children: React.ReactNode; }>) { - return ( - - - {children} - - - ); + return ( + + + {children} + + + ); } diff --git a/src/app/page.tsx b/src/app/page.tsx index 2374118..23f2be8 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -47,9 +47,10 @@ export default function Home() { identifier: "joywinbennis0987@gmail.com", otp, }); - console.log("1",response.data); + console.log("1", response.data); if (response.data.status === 200) alert(response.data.message); - else if (response.data.status === 400) alert(response.data.message); + else if (response.data.status === 400) + alert(response.data.message); } catch (error) { console.error("Error verifying OTP:", error); } diff --git a/src/app/register/page.tsx b/src/app/register/page.tsx new file mode 100644 index 0000000..1c992d1 --- /dev/null +++ b/src/app/register/page.tsx @@ -0,0 +1,21 @@ +import { Payment } from "@/components/payment"; +import RegistrationForm from "@/components/registration-form"; +import React from "react"; + +export default function page() { + return ( +
+ + +
+ ); +} + +{ + /*
+
+

TEDx 2024

+

Registration Form

+
+
*/ +} diff --git a/src/components/Admin/change-role.tsx b/src/components/Admin/change-role.tsx index a5c8bf1..28e5fa9 100644 --- a/src/components/Admin/change-role.tsx +++ b/src/components/Admin/change-role.tsx @@ -2,33 +2,39 @@ import { makeAdmin, makeParticipant } from "@/app/actions/change-role"; import { Button } from "@/components/ui/button"; -export default function ChangeRole({ userId, userRole }: { userId: string; userRole: string }) { - async function handleMakeAdmin() { - await makeAdmin(userId); - } +export default function ChangeRole({ + userId, + userRole, +}: { + userId: string; + userRole: string; +}) { + async function handleMakeAdmin() { + await makeAdmin(userId); + } - async function handleMakeParticipant() { - await makeParticipant(userId); - } + async function handleMakeParticipant() { + await makeParticipant(userId); + } - return ( -
- - -
- ); + return ( +
+ + +
+ ); } diff --git a/src/components/Admin/code-generation-card.tsx b/src/components/Admin/code-generation-card.tsx index 59a4271..bedbbd2 100644 --- a/src/components/Admin/code-generation-card.tsx +++ b/src/components/Admin/code-generation-card.tsx @@ -2,7 +2,14 @@ import { saveCoupon } from "@/app/actions/create-coupon-code"; import { Button } from "@/components/ui/button"; -import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"; +import { + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from "@/components/ui/card"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; @@ -14,89 +21,89 @@ import { Checkbox } from "../ui/checkbox"; import { useState } from "react"; export function Coupon({ session }: { session: NextAuthSession }) { - const [discount, setDiscount] = useState("20"); - const [checked, setChecked] = useState(false); - const { data, isPending, isError, error, refetch } = useQuery({ - queryKey: ["coupon"], - queryFn: createCouponCode, - enabled: false, - }); + const [discount, setDiscount] = useState("20"); + const [checked, setChecked] = useState(false); + const { data, isPending, isError, error, refetch } = useQuery({ + queryKey: ["coupon"], + queryFn: createCouponCode, + enabled: false, + }); - - - return ( - - - Create - Store - - - - - Coupon code - - Here you can create the coupon code and add it to the database - - - -
- - -
-
- - { - setDiscount(e.target.value ); - }} - /> - { - setChecked(!checked); - }} - /> - -
-
- - - -
-
- - - - Coupon code - You can see the generated coupon code below - - -
- - -
-
- - - -
-
-
- ); + return ( + + + Create + Store + + + + + Coupon code + + Here you can create the coupon code and add it to the database + + + +
+ + +
+
+ + { + setDiscount(e.target.value); + }} + /> + { + setChecked(!checked); + }} + /> + +
+
+ + + +
+
+ + + + Coupon code + + You can see the generated coupon code below + + + +
+ + +
+
+ + + +
+
+
+ ); } diff --git a/src/components/Admin/user-list.tsx b/src/components/Admin/user-list.tsx index 9174dfc..66ba46b 100644 --- a/src/components/Admin/user-list.tsx +++ b/src/components/Admin/user-list.tsx @@ -3,7 +3,13 @@ import { useState, useEffect, useRef, useCallback } from "react"; import axios from "axios"; -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "../ui/card"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "../ui/card"; import { Avatar, AvatarFallback, AvatarImage } from "../ui/avatar"; import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover"; import { Button } from "../ui/button"; @@ -13,153 +19,161 @@ import debounce from "lodash.debounce"; import ChangeRole from "./change-role"; export interface User { - id: string; - name: string | null; - email: string | null; - role: string; - image: string | null; + id: string; + name: string | null; + email: string | null; + role: string; + image: string | null; } interface UsersListProps { - initialUsers: User[]; - initialPage: number; + initialUsers: User[]; + initialPage: number; } export const dynamic = "force-dynamic"; const UsersList: React.FC = ({ initialUsers, initialPage }) => { - const [userList, setUserList] = useState(initialUsers); - const [currentPage, setCurrentPage] = useState(initialPage); - const [loading, setLoading] = useState(false); - const [hasMore, setHasMore] = useState(true); - const [searchQuery, setSearchQuery] = useState(""); // Search query state - const loader = useRef(null); + const [userList, setUserList] = useState(initialUsers); + const [currentPage, setCurrentPage] = useState(initialPage); + const [loading, setLoading] = useState(false); + const [hasMore, setHasMore] = useState(true); + const [searchQuery, setSearchQuery] = useState(""); // Search query state + const loader = useRef(null); - const fetchUsers = async (page: number, query: string) => { - if (loading) return; - setLoading(true); - try { - const response = await axios.get(`/api/users?page=${page}&search=${encodeURIComponent(query)}`); - if (response.data.users.length > 0) { - setUserList((prevUsers) => [...prevUsers, ...response.data.users]); - setCurrentPage(page); - } else { - setHasMore(false); - } - } catch (error) { - console.error("Error fetching users:", error); - } - setLoading(false); - }; - const loadMoreUsers = useCallback(() => { - if (hasMore) { - fetchUsers(currentPage + 1, searchQuery); - } + const fetchUsers = async (page: number, query: string) => { + if (loading) return; + setLoading(true); + try { + const response = await axios.get( + `/api/users?page=${page}&search=${encodeURIComponent(query)}`, + ); + if (response.data.users.length > 0) { + setUserList((prevUsers) => [...prevUsers, ...response.data.users]); + setCurrentPage(page); + } else { + setHasMore(false); + } + } catch (error) { + console.error("Error fetching users:", error); + } + setLoading(false); + }; + const loadMoreUsers = useCallback(() => { + if (hasMore) { + fetchUsers(currentPage + 1, searchQuery); + } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [currentPage, hasMore, searchQuery]); + }, [currentPage, hasMore, searchQuery]); - const debouncedFetchUsers = useCallback( - debounce(async (query: string) => { - setCurrentPage(1); // Reset page number - setHasMore(true); // Reset hasMore - try { - const response = await axios.get(`/api/users?page=1&search=${encodeURIComponent(query)}`); - setUserList(response.data.users); - } catch (error) { - console.error("Error fetching users:", error); - } - }, 500), - [] - ); - const handleSearchChange = (e: React.ChangeEvent) => { - const query = e.target.value; - setSearchQuery(query); - debouncedFetchUsers(query); // Use debounced fetch function - }; + const debouncedFetchUsers = useCallback( + debounce(async (query: string) => { + setCurrentPage(1); // Reset page number + setHasMore(true); // Reset hasMore + try { + const response = await axios.get( + `/api/users?page=1&search=${encodeURIComponent(query)}`, + ); + setUserList(response.data.users); + } catch (error) { + console.error("Error fetching users:", error); + } + }, 500), + [], + ); + const handleSearchChange = (e: React.ChangeEvent) => { + const query = e.target.value; + setSearchQuery(query); + debouncedFetchUsers(query); // Use debounced fetch function + }; - // Observe scroll and load more users when scrolled to the bottom - useEffect(() => { - if (loader.current) { - const observer = new IntersectionObserver( - (entries) => { - if (entries[0].isIntersecting && hasMore) { - loadMoreUsers(); - } - }, - { threshold: 1.0 } - ); - observer.observe(loader.current); - return () => observer.disconnect(); - } - }, [loader.current, hasMore, loadMoreUsers]); + // Observe scroll and load more users when scrolled to the bottom + useEffect(() => { + if (loader.current) { + const observer = new IntersectionObserver( + (entries) => { + if (entries[0].isIntersecting && hasMore) { + loadMoreUsers(); + } + }, + { threshold: 1.0 }, + ); + observer.observe(loader.current); + return () => observer.disconnect(); + } + }, [loader.current, hasMore, loadMoreUsers]); - return ( - <> -
-
- -
-
- - -
-
- - Users - Manage user roles and permissions. - - -
- {userList.map((user) => ( -
-
- - - - {user.name ? user.name[0] : "N/A"} - - -
-

- {user.name || "Unknown"} -

-

- {user.email || "No email"} -

-
-
- - - - - - - - -
- ))} -
- {hasMore && ( -
- {loading ? "Loading..." : "Load more"} -
- )} -
-
-
+ return ( + <> +
+
+ +
+
+ + +
- - ); + + Users + + Manage user roles and permissions. + + + +
+ {userList.map((user) => ( +
+
+ + + + {user.name ? user.name[0] : "N/A"} + + +
+

+ {user.name || "Unknown"} +

+

+ {user.email || "No email"} +

+
+
+ + + + + + + + +
+ ))} +
+ {hasMore && ( +
+ {loading ? "Loading..." : "Load more"} +
+ )} +
+
+
+
+ + ); }; export default UsersList; diff --git a/src/components/Layout/Provider.tsx b/src/components/Layout/Provider.tsx index 15f07fd..73f8edb 100644 --- a/src/components/Layout/Provider.tsx +++ b/src/components/Layout/Provider.tsx @@ -1,14 +1,33 @@ -"use client"; +"use client";; +import { ourFileRouter } from "@/app/api/uploadthing/core"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { NextSSRPlugin } from "@uploadthing/react/next-ssr-plugin"; import { SessionProvider } from "next-auth/react"; import { ReactNode } from "react"; -import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { Toaster } from "sonner"; +import { extractRouterConfig } from "uploadthing/server"; + export default function Providers({ children }: { children: ReactNode }) { - const queryClient = new QueryClient(); - return ( - <> - - {children} - - - ); + const queryClient = new QueryClient(); + return ( + <> + + + + {children} + + + + + ); } diff --git a/src/components/confirmation-dialog.tsx b/src/components/confirmation-dialog.tsx new file mode 100644 index 0000000..38eaede --- /dev/null +++ b/src/components/confirmation-dialog.tsx @@ -0,0 +1,42 @@ +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogTrigger, +} from "@/components/ui/alert-dialog"; +import { Button } from "@/components/ui/button"; + +interface AlertDialogProps { + onConfirm: () => void; + title: string; + description: string; +} + +export function ConfirmationDialog({ + onConfirm, + title, + description, +}: AlertDialogProps) { + return ( + + + + + + + Are you absolutely sure? + {description} + + + Cancel + Continue + + + + ); +} diff --git a/src/components/coupon-generator-dialog.tsx b/src/components/coupon-generator-dialog.tsx index aa82b81..80d8154 100644 --- a/src/components/coupon-generator-dialog.tsx +++ b/src/components/coupon-generator-dialog.tsx @@ -1,5 +1,5 @@ -import { useState } from 'react' -import { Button } from "@/components/ui/button" +import { useState } from "react"; +import { Button } from "@/components/ui/button"; import { Dialog, DialogContent, @@ -8,32 +8,35 @@ import { DialogHeader, DialogTitle, DialogTrigger, -} from "@/components/ui/dialog" -import { Input } from "@/components/ui/input" -import { Label } from "@/components/ui/label" -import { AlertCircle, CheckCircle } from 'lucide-react' -import clsx from 'clsx' +} from "@/components/ui/dialog"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { AlertCircle, CheckCircle } from "lucide-react"; +import clsx from "clsx"; interface CouponGeneratorDialogProps { - className?: string + className?: string; onGenerateCoupon: () => void; } -export default function CouponGeneratorDialog({ className, onGenerateCoupon}: CouponGeneratorDialogProps) { - const [password, setPassword] = useState('') - const [error, setError] = useState('') - const [isOpen, setIsOpen] = useState(false) +export default function CouponGeneratorDialog({ + className, + onGenerateCoupon, +}: CouponGeneratorDialogProps) { + const [password, setPassword] = useState(""); + const [error, setError] = useState(""); + const [isOpen, setIsOpen] = useState(false); const handleGenerateCoupon = () => { if (password === process.env.NEXT_PUBLIC_ADMIN_PASSWORD) { - onGenerateCoupon() - setIsOpen(false) - setPassword('') - setError('') + onGenerateCoupon(); + setIsOpen(false); + setPassword(""); + setError(""); } else { - setError('Incorrect password. Please try again.') + setError("Incorrect password. Please try again."); } - } + }; return ( @@ -65,9 +68,11 @@ export default function CouponGeneratorDialog({ className, onGenerateCoupon}: Co )}
- + - ) -} \ No newline at end of file + ); +} diff --git a/src/components/payment.tsx b/src/components/payment.tsx new file mode 100644 index 0000000..e021638 --- /dev/null +++ b/src/components/payment.tsx @@ -0,0 +1,76 @@ +"use client"; +import { Card } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { getPrice } from "@/app/actions/get-price"; +import { useState } from "react"; +import { Input } from "./ui/input"; +import { toast } from "sonner"; +import getErrorMessage from "@/utils/getErrorMessage"; +import { basePrice, initialdiscount } from "@/constants"; + +export function Payment() { + const [coupon, setCoupon] = useState(""); + const [pricing, setPricing] = useState({ + basePrice: basePrice, + discountAmount: initialdiscount, + finalPrice: basePrice, + }); + const verifyCoupon = async () => { + try { + const { basePrice, discountAmount, finalPrice } = await getPrice(coupon); + setPricing({ basePrice, discountAmount, finalPrice }); + toast.success("Coupon applied successfully"); + } catch (e) { + console.error(e); + const message = getErrorMessage(e); + toast.error(`${message}`); + } + }; + return ( + +
+
+

Order Summary

+
+
+

₹{pricing.finalPrice}

+
+
+ setCoupon(e.target.value)} + /> + +
+
+
+ Subtotal + ₹{pricing.basePrice} +
+
+ Discount + ₹{pricing.discountAmount} +
+
+
+ Total Amount + ₹{pricing.finalPrice} +
+
+ +
+ + {/*
+ Your trial will end immediately and your card will be charged +
*/} +
+
+ ); +} diff --git a/src/components/registration-form.tsx b/src/components/registration-form.tsx new file mode 100644 index 0000000..5be5f3e --- /dev/null +++ b/src/components/registration-form.tsx @@ -0,0 +1,294 @@ +"use client"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { useUploadThing } from "@/utils/uploadthing"; +import "@uploadthing/react/styles.css"; +import { useState, useMemo } from "react"; +import { useForm } from "react-hook-form"; +import { toast } from "sonner"; +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; +import { RegistrationFormSchema, TRegistrationForm } from "@/utils/zod-schemas"; +import { zodResolver } from "@hookform/resolvers/zod"; + +export default function RegistrationForm() { + const form = useForm({ + resolver: zodResolver(RegistrationFormSchema), + defaultValues: { + name: "", + usn: "", + email: "", + contact: "", + designation: "", + photo: "", + collegeIdCard: "", + entityName: "", + referralUsed: "", + }, + }); + const [files, setFiles] = useState<{ + photo: File | null; + collegeId: File | null; + }>({ photo: null, collegeId: null }); + const [uploading, setUploading] = useState(false); + const [designation, setDesignation] = useState(undefined); + + //uploadthing code -- custom one + const { startUpload } = useUploadThing("imageUploader", { + onClientUploadComplete: (res) => { + console.log("Client upload complete, response:", res[0].url); + }, + onUploadError: () => { + toast.error("Upload failed"); + }, + onUploadBegin: (file: string) => { + console.log("upload has begun for", file); + }, + }); + + const handleRegister = async (values: TRegistrationForm) => { + setUploading(true); + const { photo, collegeId } = files; + + if (collegeId) { + await startUpload([collegeId]); + toast.success("College ID uploaded"); + } + if (photo) { + await startUpload([photo]); + toast.success("Photo uploaded"); + } + toast.success("Registered successfully"); + setUploading(false); + }; + + const designationOptions = useMemo( + () => [ + { value: "student", label: "Student" }, + { value: "employee", label: "Employee" }, + { value: "faculty", label: "Faculty" }, + ], + [], + ); + + return ( +
+ +
+
+

Registration Form

+
+
+
+ ( + + Name + + + + + This is your public display name. + + + + )} + /> + ( + + Email + + + + + For an additional discount, please use your SJEC email ID. + + + + )} + /> +
+
+ ( + + + Contact + + + + + + )} + /> + ( + + + Designation + + + + )} + /> +
+
+ ( + + + USN (University Seat Number) + + + + + + )} + /> + ( + + + Organization/College + + + + + + )} + /> +
+
+ ( + + + College ID Card + + + { + const file = e.target.files?.[0] || null; + setFiles((prev) => ({ ...prev, collegeId: file })); + }} + disabled={designation !== "student"} + /> + + + )} + /> + ( + + + Photo + + + { + const file = e.target.files?.[0] || null; + setFiles((prev) => ({ ...prev, photo: file })); + }} + /> + + + )} + /> +
+
+
+ +
+
+
+ + ); +} diff --git a/src/components/ui/accordion.tsx b/src/components/ui/accordion.tsx new file mode 100644 index 0000000..fd546f3 --- /dev/null +++ b/src/components/ui/accordion.tsx @@ -0,0 +1,57 @@ +"use client"; + +import * as React from "react"; +import * as AccordionPrimitive from "@radix-ui/react-accordion"; +import { ChevronDownIcon } from "@radix-ui/react-icons"; + +import { cn } from "@/lib/utils"; + +const Accordion = AccordionPrimitive.Root; + +const AccordionItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +AccordionItem.displayName = "AccordionItem"; + +const AccordionTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + svg]:rotate-180", + className, + )} + {...props} + > + {children} + + + +)); +AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName; + +const AccordionContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + +
{children}
+
+)); +AccordionContent.displayName = AccordionPrimitive.Content.displayName; + +export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }; diff --git a/src/components/ui/alert-dialog.tsx b/src/components/ui/alert-dialog.tsx new file mode 100644 index 0000000..f69596f --- /dev/null +++ b/src/components/ui/alert-dialog.tsx @@ -0,0 +1,141 @@ +"use client"; + +import * as React from "react"; +import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"; + +import { cn } from "@/lib/utils"; +import { buttonVariants } from "@/components/ui/button"; + +const AlertDialog = AlertDialogPrimitive.Root; + +const AlertDialogTrigger = AlertDialogPrimitive.Trigger; + +const AlertDialogPortal = AlertDialogPrimitive.Portal; + +const AlertDialogOverlay = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName; + +const AlertDialogContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + + +)); +AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName; + +const AlertDialogHeader = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+); +AlertDialogHeader.displayName = "AlertDialogHeader"; + +const AlertDialogFooter = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+); +AlertDialogFooter.displayName = "AlertDialogFooter"; + +const AlertDialogTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName; + +const AlertDialogDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +AlertDialogDescription.displayName = + AlertDialogPrimitive.Description.displayName; + +const AlertDialogAction = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName; + +const AlertDialogCancel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName; + +export { + AlertDialog, + AlertDialogPortal, + AlertDialogOverlay, + AlertDialogTrigger, + AlertDialogContent, + AlertDialogHeader, + AlertDialogFooter, + AlertDialogTitle, + AlertDialogDescription, + AlertDialogAction, + AlertDialogCancel, +}; diff --git a/src/components/ui/alert.tsx b/src/components/ui/alert.tsx new file mode 100644 index 0000000..1df0436 --- /dev/null +++ b/src/components/ui/alert.tsx @@ -0,0 +1,59 @@ +import * as React from "react"; +import { cva, type VariantProps } from "class-variance-authority"; + +import { cn } from "@/lib/utils"; + +const alertVariants = cva( + "relative w-full rounded-lg border px-4 py-3 text-sm [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground [&>svg~*]:pl-7", + { + variants: { + variant: { + default: "bg-background text-foreground", + destructive: + "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive", + }, + }, + defaultVariants: { + variant: "default", + }, + }, +); + +const Alert = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes & VariantProps +>(({ className, variant, ...props }, ref) => ( +
+)); +Alert.displayName = "Alert"; + +const AlertTitle = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)); +AlertTitle.displayName = "AlertTitle"; + +const AlertDescription = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)); +AlertDescription.displayName = "AlertDescription"; + +export { Alert, AlertTitle, AlertDescription }; diff --git a/src/components/ui/avatar.tsx b/src/components/ui/avatar.tsx index 51e507b..09cd14d 100644 --- a/src/components/ui/avatar.tsx +++ b/src/components/ui/avatar.tsx @@ -1,9 +1,9 @@ -"use client" +"use client"; -import * as React from "react" -import * as AvatarPrimitive from "@radix-ui/react-avatar" +import * as React from "react"; +import * as AvatarPrimitive from "@radix-ui/react-avatar"; -import { cn } from "@/lib/utils" +import { cn } from "@/lib/utils"; const Avatar = React.forwardRef< React.ElementRef, @@ -13,12 +13,12 @@ const Avatar = React.forwardRef< ref={ref} className={cn( "relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full", - className + className, )} {...props} /> -)) -Avatar.displayName = AvatarPrimitive.Root.displayName +)); +Avatar.displayName = AvatarPrimitive.Root.displayName; const AvatarImage = React.forwardRef< React.ElementRef, @@ -29,8 +29,8 @@ const AvatarImage = React.forwardRef< className={cn("aspect-square h-full w-full", className)} {...props} /> -)) -AvatarImage.displayName = AvatarPrimitive.Image.displayName +)); +AvatarImage.displayName = AvatarPrimitive.Image.displayName; const AvatarFallback = React.forwardRef< React.ElementRef, @@ -40,11 +40,11 @@ const AvatarFallback = React.forwardRef< ref={ref} className={cn( "flex h-full w-full items-center justify-center rounded-full bg-muted", - className + className, )} {...props} /> -)) -AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName +)); +AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName; -export { Avatar, AvatarImage, AvatarFallback } +export { Avatar, AvatarImage, AvatarFallback }; diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx index 0270f64..225e113 100644 --- a/src/components/ui/button.tsx +++ b/src/components/ui/button.tsx @@ -1,8 +1,8 @@ -import * as React from "react" -import { Slot } from "@radix-ui/react-slot" -import { cva, type VariantProps } from "class-variance-authority" +import * as React from "react"; +import { Slot } from "@radix-ui/react-slot"; +import { cva, type VariantProps } from "class-variance-authority"; -import { cn } from "@/lib/utils" +import { cn } from "@/lib/utils"; const buttonVariants = cva( "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50", @@ -31,27 +31,27 @@ const buttonVariants = cva( variant: "default", size: "default", }, - } -) + }, +); export interface ButtonProps extends React.ButtonHTMLAttributes, VariantProps { - asChild?: boolean + asChild?: boolean; } const Button = React.forwardRef( ({ className, variant, size, asChild = false, ...props }, ref) => { - const Comp = asChild ? Slot : "button" + const Comp = asChild ? Slot : "button"; return ( - ) - } -) -Button.displayName = "Button" + ); + }, +); +Button.displayName = "Button"; -export { Button, buttonVariants } +export { Button, buttonVariants }; diff --git a/src/components/ui/card.tsx b/src/components/ui/card.tsx index 77e9fb7..75cc369 100644 --- a/src/components/ui/card.tsx +++ b/src/components/ui/card.tsx @@ -1,6 +1,6 @@ -import * as React from "react" +import * as React from "react"; -import { cn } from "@/lib/utils" +import { cn } from "@/lib/utils"; const Card = React.forwardRef< HTMLDivElement, @@ -10,12 +10,12 @@ const Card = React.forwardRef< ref={ref} className={cn( "rounded-xl border bg-card text-card-foreground shadow", - className + className, )} {...props} /> -)) -Card.displayName = "Card" +)); +Card.displayName = "Card"; const CardHeader = React.forwardRef< HTMLDivElement, @@ -26,8 +26,8 @@ const CardHeader = React.forwardRef< className={cn("flex flex-col space-y-1.5 p-6", className)} {...props} /> -)) -CardHeader.displayName = "CardHeader" +)); +CardHeader.displayName = "CardHeader"; const CardTitle = React.forwardRef< HTMLParagraphElement, @@ -38,8 +38,8 @@ const CardTitle = React.forwardRef< className={cn("font-semibold leading-none tracking-tight", className)} {...props} /> -)) -CardTitle.displayName = "CardTitle" +)); +CardTitle.displayName = "CardTitle"; const CardDescription = React.forwardRef< HTMLParagraphElement, @@ -50,16 +50,16 @@ const CardDescription = React.forwardRef< className={cn("text-sm text-muted-foreground", className)} {...props} /> -)) -CardDescription.displayName = "CardDescription" +)); +CardDescription.displayName = "CardDescription"; const CardContent = React.forwardRef< HTMLDivElement, React.HTMLAttributes >(({ className, ...props }, ref) => (
-)) -CardContent.displayName = "CardContent" +)); +CardContent.displayName = "CardContent"; const CardFooter = React.forwardRef< HTMLDivElement, @@ -70,7 +70,14 @@ const CardFooter = React.forwardRef< className={cn("flex items-center p-6 pt-0", className)} {...props} /> -)) -CardFooter.displayName = "CardFooter" +)); +CardFooter.displayName = "CardFooter"; -export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } +export { + Card, + CardHeader, + CardFooter, + CardTitle, + CardDescription, + CardContent, +}; diff --git a/src/components/ui/checkbox.tsx b/src/components/ui/checkbox.tsx index 7d2b3c3..6d36331 100644 --- a/src/components/ui/checkbox.tsx +++ b/src/components/ui/checkbox.tsx @@ -1,10 +1,10 @@ -"use client" +"use client"; -import * as React from "react" -import * as CheckboxPrimitive from "@radix-ui/react-checkbox" -import { CheckIcon } from "@radix-ui/react-icons" +import * as React from "react"; +import * as CheckboxPrimitive from "@radix-ui/react-checkbox"; +import { CheckIcon } from "@radix-ui/react-icons"; -import { cn } from "@/lib/utils" +import { cn } from "@/lib/utils"; const Checkbox = React.forwardRef< React.ElementRef, @@ -14,7 +14,7 @@ const Checkbox = React.forwardRef< ref={ref} className={cn( "peer h-4 w-4 shrink-0 rounded-sm border border-primary shadow focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground", - className + className, )} {...props} > @@ -24,7 +24,7 @@ const Checkbox = React.forwardRef< -)) -Checkbox.displayName = CheckboxPrimitive.Root.displayName +)); +Checkbox.displayName = CheckboxPrimitive.Root.displayName; -export { Checkbox } +export { Checkbox }; diff --git a/src/components/ui/collapsible.tsx b/src/components/ui/collapsible.tsx new file mode 100644 index 0000000..cb003d1 --- /dev/null +++ b/src/components/ui/collapsible.tsx @@ -0,0 +1,11 @@ +"use client"; + +import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"; + +const Collapsible = CollapsiblePrimitive.Root; + +const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger; + +const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent; + +export { Collapsible, CollapsibleTrigger, CollapsibleContent }; diff --git a/src/components/ui/dialog.tsx b/src/components/ui/dialog.tsx index 95b0d38..3db2a7d 100644 --- a/src/components/ui/dialog.tsx +++ b/src/components/ui/dialog.tsx @@ -1,18 +1,18 @@ -"use client" +"use client"; -import * as React from "react" -import * as DialogPrimitive from "@radix-ui/react-dialog" -import { Cross2Icon } from "@radix-ui/react-icons" +import * as React from "react"; +import * as DialogPrimitive from "@radix-ui/react-dialog"; +import { Cross2Icon } from "@radix-ui/react-icons"; -import { cn } from "@/lib/utils" +import { cn } from "@/lib/utils"; -const Dialog = DialogPrimitive.Root +const Dialog = DialogPrimitive.Root; -const DialogTrigger = DialogPrimitive.Trigger +const DialogTrigger = DialogPrimitive.Trigger; -const DialogPortal = DialogPrimitive.Portal +const DialogPortal = DialogPrimitive.Portal; -const DialogClose = DialogPrimitive.Close +const DialogClose = DialogPrimitive.Close; const DialogOverlay = React.forwardRef< React.ElementRef, @@ -22,12 +22,12 @@ const DialogOverlay = React.forwardRef< ref={ref} className={cn( "fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0", - className + className, )} {...props} /> -)) -DialogOverlay.displayName = DialogPrimitive.Overlay.displayName +)); +DialogOverlay.displayName = DialogPrimitive.Overlay.displayName; const DialogContent = React.forwardRef< React.ElementRef, @@ -39,7 +39,7 @@ const DialogContent = React.forwardRef< ref={ref} className={cn( "fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg", - className + className, )} {...props} > @@ -50,8 +50,8 @@ const DialogContent = React.forwardRef< -)) -DialogContent.displayName = DialogPrimitive.Content.displayName +)); +DialogContent.displayName = DialogPrimitive.Content.displayName; const DialogHeader = ({ className, @@ -60,12 +60,12 @@ const DialogHeader = ({
-) -DialogHeader.displayName = "DialogHeader" +); +DialogHeader.displayName = "DialogHeader"; const DialogFooter = ({ className, @@ -74,12 +74,12 @@ const DialogFooter = ({
-) -DialogFooter.displayName = "DialogFooter" +); +DialogFooter.displayName = "DialogFooter"; const DialogTitle = React.forwardRef< React.ElementRef, @@ -89,12 +89,12 @@ const DialogTitle = React.forwardRef< ref={ref} className={cn( "text-lg font-semibold leading-none tracking-tight", - className + className, )} {...props} /> -)) -DialogTitle.displayName = DialogPrimitive.Title.displayName +)); +DialogTitle.displayName = DialogPrimitive.Title.displayName; const DialogDescription = React.forwardRef< React.ElementRef, @@ -105,8 +105,8 @@ const DialogDescription = React.forwardRef< className={cn("text-sm text-muted-foreground", className)} {...props} /> -)) -DialogDescription.displayName = DialogPrimitive.Description.displayName +)); +DialogDescription.displayName = DialogPrimitive.Description.displayName; export { Dialog, @@ -119,4 +119,4 @@ export { DialogFooter, DialogTitle, DialogDescription, -} +}; diff --git a/src/components/ui/form.tsx b/src/components/ui/form.tsx new file mode 100644 index 0000000..c3daa38 --- /dev/null +++ b/src/components/ui/form.tsx @@ -0,0 +1,179 @@ +"use client"; + +import * as React from "react"; +import * as LabelPrimitive from "@radix-ui/react-label"; +import { Slot } from "@radix-ui/react-slot"; +import { + Controller, + ControllerProps, + FieldPath, + FieldValues, + FormProvider, + useFormContext, +} from "react-hook-form"; + +import { cn } from "@/lib/utils"; +import { Label } from "@/components/ui/label"; + +const Form = FormProvider; + +type FormFieldContextValue< + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath = FieldPath, +> = { + name: TName; +}; + +const FormFieldContext = React.createContext( + {} as FormFieldContextValue, +); + +const FormField = < + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath = FieldPath, +>({ + ...props +}: ControllerProps) => { + return ( + + + + ); +}; + +const useFormField = () => { + const fieldContext = React.useContext(FormFieldContext); + const itemContext = React.useContext(FormItemContext); + const { getFieldState, formState } = useFormContext(); + + const fieldState = getFieldState(fieldContext.name, formState); + + if (!fieldContext) { + throw new Error("useFormField should be used within "); + } + + const { id } = itemContext; + + return { + id, + name: fieldContext.name, + formItemId: `${id}-form-item`, + formDescriptionId: `${id}-form-item-description`, + formMessageId: `${id}-form-item-message`, + ...fieldState, + }; +}; + +type FormItemContextValue = { + id: string; +}; + +const FormItemContext = React.createContext( + {} as FormItemContextValue, +); + +const FormItem = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => { + const id = React.useId(); + + return ( + +
+ + ); +}); +FormItem.displayName = "FormItem"; + +const FormLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => { + const { error, formItemId } = useFormField(); + + return ( +