Update login stuff
This commit is contained in:
@@ -45,8 +45,9 @@ cp ./env.example ./.env
|
|||||||
cp ./host/convex/docker/env.example ./host/convex/docker/.env
|
cp ./host/convex/docker/env.example ./host/convex/docker/.env
|
||||||
```
|
```
|
||||||
|
|
||||||
### Start self hosted convex by running
|
### Start self hosted convex
|
||||||
|
|
||||||
|
The basic gist is to run the commands below after you have filled out the environment variables you plan to use, but you should ultimately follow the [guide they provide](https://github.com/get-convex/convex-backend/tree/main/self-hosted)
|
||||||
```bash
|
```bash
|
||||||
cd ./host/convex/docker
|
cd ./host/convex/docker
|
||||||
sudo docker compose up -d
|
sudo docker compose up -d
|
||||||
|
65
bun.lock
65
bun.lock
@@ -5,7 +5,11 @@
|
|||||||
"name": "example",
|
"name": "example",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@convex-dev/auth": "^0.0.81",
|
"@convex-dev/auth": "^0.0.81",
|
||||||
|
"@hookform/resolvers": "^5.2.1",
|
||||||
|
"@radix-ui/react-label": "^2.1.7",
|
||||||
|
"@radix-ui/react-separator": "^1.1.7",
|
||||||
"@radix-ui/react-slot": "^1.2.3",
|
"@radix-ui/react-slot": "^1.2.3",
|
||||||
|
"@radix-ui/react-tabs": "^1.1.13",
|
||||||
"@sentry/nextjs": "^10.7.0",
|
"@sentry/nextjs": "^10.7.0",
|
||||||
"@t3-oss/env-nextjs": "^0.13.8",
|
"@t3-oss/env-nextjs": "^0.13.8",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
@@ -18,6 +22,7 @@
|
|||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
|
"react-hook-form": "^7.62.0",
|
||||||
"require-in-the-middle": "^7.5.2",
|
"require-in-the-middle": "^7.5.2",
|
||||||
"tailwind-merge": "^3.3.1",
|
"tailwind-merge": "^3.3.1",
|
||||||
"typescript-eslint": "^8.41.0",
|
"typescript-eslint": "^8.41.0",
|
||||||
@@ -291,6 +296,8 @@
|
|||||||
|
|
||||||
"@eslint/plugin-kit": ["@eslint/plugin-kit@0.3.5", "", { "dependencies": { "@eslint/core": "^0.15.2", "levn": "^0.4.1" } }, "sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w=="],
|
"@eslint/plugin-kit": ["@eslint/plugin-kit@0.3.5", "", { "dependencies": { "@eslint/core": "^0.15.2", "levn": "^0.4.1" } }, "sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w=="],
|
||||||
|
|
||||||
|
"@hookform/resolvers": ["@hookform/resolvers@5.2.1", "", { "dependencies": { "@standard-schema/utils": "^0.3.0" }, "peerDependencies": { "react-hook-form": "^7.55.0" } }, "sha512-u0+6X58gkjMcxur1wRWokA7XsiiBJ6aK17aPZxhkoYiK5J+HcTx0Vhu9ovXe6H+dVpO6cjrn2FkJTryXEMlryQ=="],
|
||||||
|
|
||||||
"@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="],
|
"@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="],
|
||||||
|
|
||||||
"@humanfs/node": ["@humanfs/node@0.16.6", "", { "dependencies": { "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.3.0" } }, "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw=="],
|
"@humanfs/node": ["@humanfs/node@0.16.6", "", { "dependencies": { "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.3.0" } }, "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw=="],
|
||||||
@@ -519,10 +526,40 @@
|
|||||||
|
|
||||||
"@prisma/instrumentation": ["@prisma/instrumentation@6.14.0", "", { "dependencies": { "@opentelemetry/instrumentation": "^0.52.0 || ^0.53.0 || ^0.54.0 || ^0.55.0 || ^0.56.0 || ^0.57.0" }, "peerDependencies": { "@opentelemetry/api": "^1.8" } }, "sha512-Po/Hry5bAeunRDq0yAQueKookW3glpP+qjjvvyOfm6dI2KG5/Y6Bgg3ahyWd7B0u2E+Wf9xRk2rtdda7ySgK1A=="],
|
"@prisma/instrumentation": ["@prisma/instrumentation@6.14.0", "", { "dependencies": { "@opentelemetry/instrumentation": "^0.52.0 || ^0.53.0 || ^0.54.0 || ^0.55.0 || ^0.56.0 || ^0.57.0" }, "peerDependencies": { "@opentelemetry/api": "^1.8" } }, "sha512-Po/Hry5bAeunRDq0yAQueKookW3glpP+qjjvvyOfm6dI2KG5/Y6Bgg3ahyWd7B0u2E+Wf9xRk2rtdda7ySgK1A=="],
|
||||||
|
|
||||||
|
"@radix-ui/primitive": ["@radix-ui/primitive@1.1.3", "", {}, "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-collection": ["@radix-ui/react-collection@1.1.7", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3" }, "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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw=="],
|
||||||
|
|
||||||
"@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg=="],
|
"@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-direction": ["@radix-ui/react-direction@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-id": ["@radix-ui/react-id@1.1.1", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-label": ["@radix-ui/react-label@2.1.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-YT1GqPSL8kJn20djelMX7/cTRp/Y9w5IZHvfxQTVHrOqa2yMl7i/UfMqKRU5V7mEyKTrUVgJXhNQPVCG8PBLoQ=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-presence": ["@radix-ui/react-presence@1.1.5", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-roving-focus": ["@radix-ui/react-roving-focus@1.1.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2" }, "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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-separator": ["@radix-ui/react-separator@1.1.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-0HEb8R9E8A+jZjvmFCy/J4xhbXy3TV+9XSnGJ3KvTtjlIUy/YQ/p6UYZvi7YbeoeXdyU9+Y3scizK6hkY37baA=="],
|
||||||
|
|
||||||
"@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
|
"@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-tabs": ["@radix-ui/react-tabs@1.1.13", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-use-controllable-state": "1.2.2" }, "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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-use-controllable-state": ["@radix-ui/react-use-controllable-state@1.2.2", "", { "dependencies": { "@radix-ui/react-use-effect-event": "0.0.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-use-effect-event": ["@radix-ui/react-use-effect-event@0.0.2", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ=="],
|
||||||
|
|
||||||
"@rollup/plugin-commonjs": ["@rollup/plugin-commonjs@28.0.1", "", { "dependencies": { "@rollup/pluginutils": "^5.0.1", "commondir": "^1.0.1", "estree-walker": "^2.0.2", "fdir": "^6.2.0", "is-reference": "1.2.1", "magic-string": "^0.30.3", "picomatch": "^4.0.2" }, "peerDependencies": { "rollup": "^2.68.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-+tNWdlWKbpB3WgBN7ijjYkq9X5uhjmcvyjEght4NmH5fAU++zfQzAJ6wumLS+dNcvwEZhKx2Z+skY8m7v0wGSA=="],
|
"@rollup/plugin-commonjs": ["@rollup/plugin-commonjs@28.0.1", "", { "dependencies": { "@rollup/pluginutils": "^5.0.1", "commondir": "^1.0.1", "estree-walker": "^2.0.2", "fdir": "^6.2.0", "is-reference": "1.2.1", "magic-string": "^0.30.3", "picomatch": "^4.0.2" }, "peerDependencies": { "rollup": "^2.68.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-+tNWdlWKbpB3WgBN7ijjYkq9X5uhjmcvyjEght4NmH5fAU++zfQzAJ6wumLS+dNcvwEZhKx2Z+skY8m7v0wGSA=="],
|
||||||
|
|
||||||
"@rollup/pluginutils": ["@rollup/pluginutils@5.2.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-walker": "^2.0.2", "picomatch": "^4.0.2" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-qWJ2ZTbmumwiLFomfzTyt5Kng4hwPi9rwCYN4SHb6eaRU1KNO4ccxINHr/VhH4GgPlt1XfSTLX2LBTme8ne4Zw=="],
|
"@rollup/pluginutils": ["@rollup/pluginutils@5.2.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-walker": "^2.0.2", "picomatch": "^4.0.2" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-qWJ2ZTbmumwiLFomfzTyt5Kng4hwPi9rwCYN4SHb6eaRU1KNO4ccxINHr/VhH4GgPlt1XfSTLX2LBTme8ne4Zw=="],
|
||||||
@@ -571,17 +608,17 @@
|
|||||||
|
|
||||||
"@rushstack/eslint-patch": ["@rushstack/eslint-patch@1.12.0", "", {}, "sha512-5EwMtOqvJMMa3HbmxLlF74e+3/HhwBTMcvt3nqVJgGCozO6hzIPOBlwm8mGVNR9SN2IJpxSnlxczyDjcn7qIyw=="],
|
"@rushstack/eslint-patch": ["@rushstack/eslint-patch@1.12.0", "", {}, "sha512-5EwMtOqvJMMa3HbmxLlF74e+3/HhwBTMcvt3nqVJgGCozO6hzIPOBlwm8mGVNR9SN2IJpxSnlxczyDjcn7qIyw=="],
|
||||||
|
|
||||||
"@sentry-internal/browser-utils": ["@sentry-internal/browser-utils@10.7.0", "", { "dependencies": { "@sentry/core": "10.7.0" } }, "sha512-M5L1XKVkhRhIV2nfUwNxBoqir4SVDcHdqRBq+k1EK6Z6DCVF9GMTiLg46+egBwgUDlAAGIQImdKgtJTH/47Z9g=="],
|
"@sentry-internal/browser-utils": ["@sentry-internal/browser-utils@10.8.0", "", { "dependencies": { "@sentry/core": "10.8.0" } }, "sha512-FaQX9eefc8sh3h3ZQy16U73KiH0xgDldXnrFiWK6OeWg8X4bJpnYbLqEi96LgHiQhjnnz+UQP1GDzH5oFuu5fA=="],
|
||||||
|
|
||||||
"@sentry-internal/feedback": ["@sentry-internal/feedback@10.7.0", "", { "dependencies": { "@sentry/core": "10.7.0" } }, "sha512-wTyoLjEKz6dwl9uyy5wfwmrlM59WmUM1DXMSyGr6j6ncDA9iPmoftU4O4+Kirkk8mRYoc0fHxjp21Z5BYtAehw=="],
|
"@sentry-internal/feedback": ["@sentry-internal/feedback@10.8.0", "", { "dependencies": { "@sentry/core": "10.8.0" } }, "sha512-n7SqgFQItq4QSPG7bCjcZcIwK6AatKnnmSDJ/i6e8jXNIyLwkEuY2NyvTXACxVdO/kafGD5VmrwnTo3Ekc1AMg=="],
|
||||||
|
|
||||||
"@sentry-internal/replay": ["@sentry-internal/replay@10.7.0", "", { "dependencies": { "@sentry-internal/browser-utils": "10.7.0", "@sentry/core": "10.7.0" } }, "sha512-lKK7NGSy20c0ArBQtGWP+ITMantNGOAeeILG2c2VaOxJUTe5EH3OcelHMNPEFTTXnYZt7RzMT03Tzmzto2LBdg=="],
|
"@sentry-internal/replay": ["@sentry-internal/replay@10.8.0", "", { "dependencies": { "@sentry-internal/browser-utils": "10.8.0", "@sentry/core": "10.8.0" } }, "sha512-9+qDEoEjv4VopLuOzK1zM4LcvcUsvB5N0iJ+FRCM3XzzOCbebJOniXTQbt5HflJc3XLnQNKFdKfTfgj8M/0RKQ=="],
|
||||||
|
|
||||||
"@sentry-internal/replay-canvas": ["@sentry-internal/replay-canvas@10.7.0", "", { "dependencies": { "@sentry-internal/replay": "10.7.0", "@sentry/core": "10.7.0" } }, "sha512-68jJfqa8r9UPGO4+S2IthkhhohTItgHjVj7S7dH5g1YUHUS1N03dZMrDMc2jOAhWURi3EOqmCkSjd2xxogRUVQ=="],
|
"@sentry-internal/replay-canvas": ["@sentry-internal/replay-canvas@10.8.0", "", { "dependencies": { "@sentry-internal/replay": "10.8.0", "@sentry/core": "10.8.0" } }, "sha512-jC4OOwiNgrlIPeXIPMLkaW53BSS1do+toYHoWzzO5AXGpN6jRhanoSj36FpVuH2N3kFnxKVfVxrwh8L+/3vFWg=="],
|
||||||
|
|
||||||
"@sentry/babel-plugin-component-annotate": ["@sentry/babel-plugin-component-annotate@4.2.0", "", {}, "sha512-GFpS3REqaHuyX4LCNqlneAQZIKyHb5ePiI1802n0fhtYjk68I1DTQ3PnbzYi50od/vAsTQVCknaS5F6tidNqTQ=="],
|
"@sentry/babel-plugin-component-annotate": ["@sentry/babel-plugin-component-annotate@4.2.0", "", {}, "sha512-GFpS3REqaHuyX4LCNqlneAQZIKyHb5ePiI1802n0fhtYjk68I1DTQ3PnbzYi50od/vAsTQVCknaS5F6tidNqTQ=="],
|
||||||
|
|
||||||
"@sentry/browser": ["@sentry/browser@10.7.0", "", { "dependencies": { "@sentry-internal/browser-utils": "10.7.0", "@sentry-internal/feedback": "10.7.0", "@sentry-internal/replay": "10.7.0", "@sentry-internal/replay-canvas": "10.7.0", "@sentry/core": "10.7.0" } }, "sha512-KPnKOIKFqxCpRMydyH8dn+MrPai2BEc+easWf8p3IWgUCx70+tZs5AXc0yWRgcXT534284waMnky2isn6Jbkvg=="],
|
"@sentry/browser": ["@sentry/browser@10.8.0", "", { "dependencies": { "@sentry-internal/browser-utils": "10.8.0", "@sentry-internal/feedback": "10.8.0", "@sentry-internal/replay": "10.8.0", "@sentry-internal/replay-canvas": "10.8.0", "@sentry/core": "10.8.0" } }, "sha512-2J7HST8/ixCaboq17yFn/j/OEokXSXoCBMXRrFx4FKJggKWZ90e2Iau5mP/IPPhrW+W9zCptCgNMY0167wS4qA=="],
|
||||||
|
|
||||||
"@sentry/bundler-plugin-core": ["@sentry/bundler-plugin-core@4.2.0", "", { "dependencies": { "@babel/core": "^7.18.5", "@sentry/babel-plugin-component-annotate": "4.2.0", "@sentry/cli": "^2.51.0", "dotenv": "^16.3.1", "find-up": "^5.0.0", "glob": "^9.3.2", "magic-string": "0.30.8", "unplugin": "1.0.1" } }, "sha512-EDG6ELSEN/Dzm4KUQOynoI2suEAdPdgwaBXVN4Ww705zdrYT79OGh51rkz74KGhovt7GukaPf0Z9LJwORXUbhg=="],
|
"@sentry/bundler-plugin-core": ["@sentry/bundler-plugin-core@4.2.0", "", { "dependencies": { "@babel/core": "^7.18.5", "@sentry/babel-plugin-component-annotate": "4.2.0", "@sentry/cli": "^2.51.0", "dotenv": "^16.3.1", "find-up": "^5.0.0", "glob": "^9.3.2", "magic-string": "0.30.8", "unplugin": "1.0.1" } }, "sha512-EDG6ELSEN/Dzm4KUQOynoI2suEAdPdgwaBXVN4Ww705zdrYT79OGh51rkz74KGhovt7GukaPf0Z9LJwORXUbhg=="],
|
||||||
|
|
||||||
@@ -603,22 +640,24 @@
|
|||||||
|
|
||||||
"@sentry/cli-win32-x64": ["@sentry/cli-win32-x64@2.52.0", "", { "os": "win32", "cpu": "x64" }, "sha512-hJT0C3FwHk1Mt9oFqcci88wbO1D+yAWUL8J29HEGM5ZAqlhdh7sAtPDIC3P2LceUJOjnXihow47Bkj62juatIQ=="],
|
"@sentry/cli-win32-x64": ["@sentry/cli-win32-x64@2.52.0", "", { "os": "win32", "cpu": "x64" }, "sha512-hJT0C3FwHk1Mt9oFqcci88wbO1D+yAWUL8J29HEGM5ZAqlhdh7sAtPDIC3P2LceUJOjnXihow47Bkj62juatIQ=="],
|
||||||
|
|
||||||
"@sentry/core": ["@sentry/core@10.7.0", "", {}, "sha512-y1Ni71O6TqeSi2Ug78StkVLHnybHZVYhnbYtj2w4g89XnQcqo4GUeR8dQRQBJpCX98UrHw22OAE8BXtKb03yXw=="],
|
"@sentry/core": ["@sentry/core@10.8.0", "", {}, "sha512-scYzM/UOItu4PjEq6CpHLdArpXjIS0laHYxE4YjkIbYIH6VMcXGQbD/FSBClsnCr1wXRnlXfXBzj0hrQAFyw+Q=="],
|
||||||
|
|
||||||
"@sentry/nextjs": ["@sentry/nextjs@10.7.0", "", { "dependencies": { "@opentelemetry/api": "^1.9.0", "@opentelemetry/semantic-conventions": "^1.34.0", "@rollup/plugin-commonjs": "28.0.1", "@sentry-internal/browser-utils": "10.7.0", "@sentry/core": "10.7.0", "@sentry/node": "10.7.0", "@sentry/opentelemetry": "10.7.0", "@sentry/react": "10.7.0", "@sentry/vercel-edge": "10.7.0", "@sentry/webpack-plugin": "^4.1.1", "chalk": "3.0.0", "resolve": "1.22.8", "rollup": "^4.35.0", "stacktrace-parser": "^0.1.10" }, "peerDependencies": { "next": "^13.2.0 || ^14.0 || ^15.0.0-rc.0" } }, "sha512-zGATwmUYd5rgy0G6Zi29GglxFYnn38FQQPjS+gnRPXaVIbck6Dtrjh+cJ3QDsttgUvfuZKpEE03JSrAA7D5QOA=="],
|
"@sentry/nextjs": ["@sentry/nextjs@10.8.0", "", { "dependencies": { "@opentelemetry/api": "^1.9.0", "@opentelemetry/semantic-conventions": "^1.34.0", "@rollup/plugin-commonjs": "28.0.1", "@sentry-internal/browser-utils": "10.8.0", "@sentry/core": "10.8.0", "@sentry/node": "10.8.0", "@sentry/opentelemetry": "10.8.0", "@sentry/react": "10.8.0", "@sentry/vercel-edge": "10.8.0", "@sentry/webpack-plugin": "^4.1.1", "chalk": "3.0.0", "resolve": "1.22.8", "rollup": "^4.35.0", "stacktrace-parser": "^0.1.10" }, "peerDependencies": { "next": "^13.2.0 || ^14.0 || ^15.0.0-rc.0" } }, "sha512-lMTALU8Iye7HUAIIKWsW3sOsuH+38jTpyZKxthGuo7kMcrnLCzK7sVuzw0gb9fDv6h2//XRdBl7npgke8wxlog=="],
|
||||||
|
|
||||||
"@sentry/node": ["@sentry/node@10.7.0", "", { "dependencies": { "@opentelemetry/api": "^1.9.0", "@opentelemetry/context-async-hooks": "^2.0.0", "@opentelemetry/core": "^2.0.0", "@opentelemetry/instrumentation": "^0.203.0", "@opentelemetry/instrumentation-amqplib": "0.50.0", "@opentelemetry/instrumentation-connect": "0.47.0", "@opentelemetry/instrumentation-dataloader": "0.21.1", "@opentelemetry/instrumentation-express": "0.52.0", "@opentelemetry/instrumentation-fs": "0.23.0", "@opentelemetry/instrumentation-generic-pool": "0.47.0", "@opentelemetry/instrumentation-graphql": "0.51.0", "@opentelemetry/instrumentation-hapi": "0.50.0", "@opentelemetry/instrumentation-http": "0.203.0", "@opentelemetry/instrumentation-ioredis": "0.51.0", "@opentelemetry/instrumentation-kafkajs": "0.13.0", "@opentelemetry/instrumentation-knex": "0.48.0", "@opentelemetry/instrumentation-koa": "0.51.0", "@opentelemetry/instrumentation-lru-memoizer": "0.48.0", "@opentelemetry/instrumentation-mongodb": "0.56.0", "@opentelemetry/instrumentation-mongoose": "0.50.0", "@opentelemetry/instrumentation-mysql": "0.49.0", "@opentelemetry/instrumentation-mysql2": "0.50.0", "@opentelemetry/instrumentation-pg": "0.55.0", "@opentelemetry/instrumentation-redis": "0.51.0", "@opentelemetry/instrumentation-tedious": "0.22.0", "@opentelemetry/instrumentation-undici": "0.14.0", "@opentelemetry/resources": "^2.0.0", "@opentelemetry/sdk-trace-base": "^2.0.0", "@opentelemetry/semantic-conventions": "^1.34.0", "@prisma/instrumentation": "6.14.0", "@sentry/core": "10.7.0", "@sentry/node-core": "10.7.0", "@sentry/opentelemetry": "10.7.0", "import-in-the-middle": "^1.14.2", "minimatch": "^9.0.0" } }, "sha512-VtUFyf8avWUqN5RRTTmcU8aGdyNUGHzz/f+3n86BR5gBL3lziKOajyc0VClfc80VLsih+PWQ/5FrIHl+S1S1YQ=="],
|
"@sentry/node": ["@sentry/node@10.8.0", "", { "dependencies": { "@opentelemetry/api": "^1.9.0", "@opentelemetry/context-async-hooks": "^2.0.0", "@opentelemetry/core": "^2.0.0", "@opentelemetry/instrumentation": "^0.203.0", "@opentelemetry/instrumentation-amqplib": "0.50.0", "@opentelemetry/instrumentation-connect": "0.47.0", "@opentelemetry/instrumentation-dataloader": "0.21.1", "@opentelemetry/instrumentation-express": "0.52.0", "@opentelemetry/instrumentation-fs": "0.23.0", "@opentelemetry/instrumentation-generic-pool": "0.47.0", "@opentelemetry/instrumentation-graphql": "0.51.0", "@opentelemetry/instrumentation-hapi": "0.50.0", "@opentelemetry/instrumentation-http": "0.203.0", "@opentelemetry/instrumentation-ioredis": "0.51.0", "@opentelemetry/instrumentation-kafkajs": "0.13.0", "@opentelemetry/instrumentation-knex": "0.48.0", "@opentelemetry/instrumentation-koa": "0.51.0", "@opentelemetry/instrumentation-lru-memoizer": "0.48.0", "@opentelemetry/instrumentation-mongodb": "0.56.0", "@opentelemetry/instrumentation-mongoose": "0.50.0", "@opentelemetry/instrumentation-mysql": "0.49.0", "@opentelemetry/instrumentation-mysql2": "0.50.0", "@opentelemetry/instrumentation-pg": "0.55.0", "@opentelemetry/instrumentation-redis": "0.51.0", "@opentelemetry/instrumentation-tedious": "0.22.0", "@opentelemetry/instrumentation-undici": "0.14.0", "@opentelemetry/resources": "^2.0.0", "@opentelemetry/sdk-trace-base": "^2.0.0", "@opentelemetry/semantic-conventions": "^1.34.0", "@prisma/instrumentation": "6.14.0", "@sentry/core": "10.8.0", "@sentry/node-core": "10.8.0", "@sentry/opentelemetry": "10.8.0", "import-in-the-middle": "^1.14.2", "minimatch": "^9.0.0" } }, "sha512-1TtCjxzn4SxoGw+ulLK+jF/v9NaZfP0yCclQIqfvWNDjMf2F+SbZL1UnXx4L184FGlNpRQnJBDrBe88gxnMX0A=="],
|
||||||
|
|
||||||
"@sentry/node-core": ["@sentry/node-core@10.7.0", "", { "dependencies": { "@sentry/core": "10.7.0", "@sentry/opentelemetry": "10.7.0", "import-in-the-middle": "^1.14.2" }, "peerDependencies": { "@opentelemetry/api": "^1.9.0", "@opentelemetry/context-async-hooks": "^1.30.1 || ^2.0.0", "@opentelemetry/core": "^1.30.1 || ^2.0.0", "@opentelemetry/instrumentation": ">=0.57.1 <1", "@opentelemetry/resources": "^1.30.1 || ^2.0.0", "@opentelemetry/sdk-trace-base": "^1.30.1 || ^2.0.0", "@opentelemetry/semantic-conventions": "^1.34.0" } }, "sha512-iafuG3Fp0pleuk1WaL4UW7wpT6C86pMEQBZ7ARZ7UHc9ujRi/dewKFi0Stu0SxJm6PZ706VZ8Igz9xpvQ0aEEg=="],
|
"@sentry/node-core": ["@sentry/node-core@10.8.0", "", { "dependencies": { "@sentry/core": "10.8.0", "@sentry/opentelemetry": "10.8.0", "import-in-the-middle": "^1.14.2" }, "peerDependencies": { "@opentelemetry/api": "^1.9.0", "@opentelemetry/context-async-hooks": "^1.30.1 || ^2.0.0", "@opentelemetry/core": "^1.30.1 || ^2.0.0", "@opentelemetry/instrumentation": ">=0.57.1 <1", "@opentelemetry/resources": "^1.30.1 || ^2.0.0", "@opentelemetry/sdk-trace-base": "^1.30.1 || ^2.0.0", "@opentelemetry/semantic-conventions": "^1.34.0" } }, "sha512-KCFy5Otq6KTXge8hBKMgU13EDRFkO4gNwSyZGXub8a7KHYFtoUgpRkborR59SWxeJmC6aEYTyh0PyOoWZJbHUQ=="],
|
||||||
|
|
||||||
"@sentry/opentelemetry": ["@sentry/opentelemetry@10.7.0", "", { "dependencies": { "@sentry/core": "10.7.0" }, "peerDependencies": { "@opentelemetry/api": "^1.9.0", "@opentelemetry/context-async-hooks": "^1.30.1 || ^2.0.0", "@opentelemetry/core": "^1.30.1 || ^2.0.0", "@opentelemetry/sdk-trace-base": "^1.30.1 || ^2.0.0", "@opentelemetry/semantic-conventions": "^1.34.0" } }, "sha512-8SrRZyERDfCYYett6dklGe+qWMDZSytKPIZpS0nDb0IqZGC02ZVIhRISbBTy4Gctowu/gMK9XaOXfBNN0pI1sg=="],
|
"@sentry/opentelemetry": ["@sentry/opentelemetry@10.8.0", "", { "dependencies": { "@sentry/core": "10.8.0" }, "peerDependencies": { "@opentelemetry/api": "^1.9.0", "@opentelemetry/context-async-hooks": "^1.30.1 || ^2.0.0", "@opentelemetry/core": "^1.30.1 || ^2.0.0", "@opentelemetry/sdk-trace-base": "^1.30.1 || ^2.0.0", "@opentelemetry/semantic-conventions": "^1.34.0" } }, "sha512-62R/RPwTYVaiZ5lVcxcjHCAGwgCyfn8Q3kaQld8/LPm8FRizZeUJmmtrI80KaYCvPJhSB/Pvfma4X3w+aN5Q3A=="],
|
||||||
|
|
||||||
"@sentry/react": ["@sentry/react@10.7.0", "", { "dependencies": { "@sentry/browser": "10.7.0", "@sentry/core": "10.7.0", "hoist-non-react-statics": "^3.3.2" }, "peerDependencies": { "react": "^16.14.0 || 17.x || 18.x || 19.x" } }, "sha512-A7y6tmREluH6OxWdwhIytQhThsUyl23cIz6isxQXqml7AMgt381JygRnm47nNl+NQLm+Uo1EGwxyvjdKPzJ9rw=="],
|
"@sentry/react": ["@sentry/react@10.8.0", "", { "dependencies": { "@sentry/browser": "10.8.0", "@sentry/core": "10.8.0", "hoist-non-react-statics": "^3.3.2" }, "peerDependencies": { "react": "^16.14.0 || 17.x || 18.x || 19.x" } }, "sha512-w/dGLMCLJG2lp8gKVKX1jjeg2inXewKfPb73+PS1CDi9/ihvqZU2DAXxnaNsBA7YYtGwlWVJe1bLAqguwTEpqw=="],
|
||||||
|
|
||||||
"@sentry/vercel-edge": ["@sentry/vercel-edge@10.7.0", "", { "dependencies": { "@opentelemetry/api": "^1.9.0", "@opentelemetry/resources": "^2.0.0", "@sentry/core": "10.7.0" } }, "sha512-HSmpxuar6hXhGdLHa0C3ZOx1tDzUuvVs31RAoRfShychIQp2904OOw5edOa9jM4AnhG1Qk/ZeF7sYJZAzdFnGQ=="],
|
"@sentry/vercel-edge": ["@sentry/vercel-edge@10.8.0", "", { "dependencies": { "@opentelemetry/api": "^1.9.0", "@opentelemetry/resources": "^2.0.0", "@sentry/core": "10.8.0" } }, "sha512-H08L/2CnnVNI2t+uDZQueXXXvmDaohM5MJVKY7QHS5TLHHhjnwsPo1DWD3PgA7UDaPQU1DioDiomEV/b5qarHg=="],
|
||||||
|
|
||||||
"@sentry/webpack-plugin": ["@sentry/webpack-plugin@4.2.0", "", { "dependencies": { "@sentry/bundler-plugin-core": "4.2.0", "unplugin": "1.0.1", "uuid": "^9.0.0" }, "peerDependencies": { "webpack": ">=4.40.0" } }, "sha512-2lPuvJhbiEOd/NAQv5EL8at9QVKchkEmWFDioDsOG6csFqbZ8hdWtTcbsXnhzH9j+CM1LmdeDNVjIF+SMoxCNg=="],
|
"@sentry/webpack-plugin": ["@sentry/webpack-plugin@4.2.0", "", { "dependencies": { "@sentry/bundler-plugin-core": "4.2.0", "unplugin": "1.0.1", "uuid": "^9.0.0" }, "peerDependencies": { "webpack": ">=4.40.0" } }, "sha512-2lPuvJhbiEOd/NAQv5EL8at9QVKchkEmWFDioDsOG6csFqbZ8hdWtTcbsXnhzH9j+CM1LmdeDNVjIF+SMoxCNg=="],
|
||||||
|
|
||||||
|
"@standard-schema/utils": ["@standard-schema/utils@0.3.0", "", {}, "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g=="],
|
||||||
|
|
||||||
"@swc/counter": ["@swc/counter@0.1.3", "", {}, "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ=="],
|
"@swc/counter": ["@swc/counter@0.1.3", "", {}, "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ=="],
|
||||||
|
|
||||||
"@swc/helpers": ["@swc/helpers@0.5.15", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g=="],
|
"@swc/helpers": ["@swc/helpers@0.5.15", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g=="],
|
||||||
@@ -1427,6 +1466,8 @@
|
|||||||
|
|
||||||
"react-dom": ["react-dom@19.1.1", "", { "dependencies": { "scheduler": "^0.26.0" }, "peerDependencies": { "react": "^19.1.1" } }, "sha512-Dlq/5LAZgF0Gaz6yiqZCf6VCcZs1ghAJyrsu84Q/GT0gV+mCxbfmKNoGRKBYMJ8IEdGPqu49YWXD02GCknEDkw=="],
|
"react-dom": ["react-dom@19.1.1", "", { "dependencies": { "scheduler": "^0.26.0" }, "peerDependencies": { "react": "^19.1.1" } }, "sha512-Dlq/5LAZgF0Gaz6yiqZCf6VCcZs1ghAJyrsu84Q/GT0gV+mCxbfmKNoGRKBYMJ8IEdGPqu49YWXD02GCknEDkw=="],
|
||||||
|
|
||||||
|
"react-hook-form": ["react-hook-form@7.62.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17 || ^18 || ^19" } }, "sha512-7KWFejc98xqG/F4bAxpL41NB3o1nnvQO1RWZT3TqRZYL8RryQETGfEdVnJN2fy1crCiBLLjkRBVK05j24FxJGA=="],
|
||||||
|
|
||||||
"react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="],
|
"react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="],
|
||||||
|
|
||||||
"read-pkg": ["read-pkg@3.0.0", "", { "dependencies": { "load-json-file": "^4.0.0", "normalize-package-data": "^2.3.2", "path-type": "^3.0.0" } }, "sha512-BLq/cCO9two+lBgiTYNqD6GdtK8s4NpaWrl6/rCO9w0TUS8oJl7cmToOZfRYllKTISY6nt1U7jQ53brmKqY6BA=="],
|
"read-pkg": ["read-pkg@3.0.0", "", { "dependencies": { "load-json-file": "^4.0.0", "normalize-package-data": "^2.3.2", "path-type": "^3.0.0" } }, "sha512-BLq/cCO9two+lBgiTYNqD6GdtK8s4NpaWrl6/rCO9w0TUS8oJl7cmToOZfRYllKTISY6nt1U7jQ53brmKqY6BA=="],
|
||||||
|
228
convex/CustomPassword.ts
Normal file
228
convex/CustomPassword.ts
Normal file
@@ -0,0 +1,228 @@
|
|||||||
|
import {
|
||||||
|
ConvexCredentials,
|
||||||
|
type ConvexCredentialsUserConfig,
|
||||||
|
} from "@convex-dev/auth/providers/ConvexCredentials";
|
||||||
|
import {
|
||||||
|
type EmailConfig,
|
||||||
|
type GenericActionCtxWithAuthConfig,
|
||||||
|
type GenericDoc,
|
||||||
|
createAccount,
|
||||||
|
invalidateSessions,
|
||||||
|
modifyAccountCredentials,
|
||||||
|
retrieveAccount,
|
||||||
|
signInViaProvider,
|
||||||
|
} from "@convex-dev/auth/server";
|
||||||
|
import type {
|
||||||
|
DocumentByName,
|
||||||
|
GenericDataModel,
|
||||||
|
WithoutSystemFields,
|
||||||
|
} from "convex/server";
|
||||||
|
import type { Value } from "convex/values";
|
||||||
|
import { Scrypt } from "lucia";
|
||||||
|
|
||||||
|
export type PasswordConfig<DataModel extends GenericDataModel> = {
|
||||||
|
id?: string;
|
||||||
|
/**
|
||||||
|
* Perform checks on provided params and customize the user
|
||||||
|
* information stored after sign up, including email normalization.
|
||||||
|
*
|
||||||
|
* Called for every flow ("signUp", "signIn", "reset",
|
||||||
|
* "reset-verification" and "email-verification").
|
||||||
|
*/
|
||||||
|
profile?: (
|
||||||
|
/**
|
||||||
|
* The values passed to the `signIn` function.
|
||||||
|
*/
|
||||||
|
params: Record<string, Value | undefined>,
|
||||||
|
/**
|
||||||
|
* Convex ActionCtx in case you want to read from or write to
|
||||||
|
* the database.
|
||||||
|
*/
|
||||||
|
ctx: GenericActionCtxWithAuthConfig<DataModel>,
|
||||||
|
) => WithoutSystemFields<DocumentByName<DataModel, "users">> & {
|
||||||
|
email: string;
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* Performs custom validation on password provided during sign up or reset.
|
||||||
|
*
|
||||||
|
* Otherwise the default validation is used (password is not empty and
|
||||||
|
* at least 8 characters in length).
|
||||||
|
*
|
||||||
|
* If the provided password is invalid, implementations must throw an Error.
|
||||||
|
*
|
||||||
|
* @param password the password supplied during "signUp" or
|
||||||
|
* "reset-verification" flows.
|
||||||
|
*/
|
||||||
|
validatePasswordRequirements?: (password: string) => void;
|
||||||
|
/**
|
||||||
|
* Provide hashing and verification functions if you want to control
|
||||||
|
* how passwords are hashed.
|
||||||
|
*/
|
||||||
|
crypto?: ConvexCredentialsUserConfig["crypto"];
|
||||||
|
/**
|
||||||
|
* An Auth.js email provider used to require verification
|
||||||
|
* before password reset.
|
||||||
|
*/
|
||||||
|
reset?: EmailConfig | ((...args: any) => EmailConfig);
|
||||||
|
/**
|
||||||
|
* An Auth.js email provider used to require verification
|
||||||
|
* before sign up / sign in.
|
||||||
|
*/
|
||||||
|
verify?: EmailConfig | ((...args: any) => EmailConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Email and password authentication provider.
|
||||||
|
*
|
||||||
|
* Passwords are by default hashed using Scrypt from Lucia.
|
||||||
|
* You can customize the hashing via the `crypto` option.
|
||||||
|
*
|
||||||
|
* Email verification is not required unless you pass
|
||||||
|
* an email provider to the `verify` option.
|
||||||
|
*/
|
||||||
|
export function Password<DataModel extends GenericDataModel>(
|
||||||
|
config: PasswordConfig<DataModel> = {},
|
||||||
|
) {
|
||||||
|
const provider = config.id ?? "password";
|
||||||
|
return ConvexCredentials<DataModel>({
|
||||||
|
id: "password",
|
||||||
|
authorize: async (params, ctx) => {
|
||||||
|
const flow = params.flow as string;
|
||||||
|
const passwordToValidate =
|
||||||
|
flow === "signUp"
|
||||||
|
? (params.password as string)
|
||||||
|
: flow === "reset-verification"
|
||||||
|
? (params.newPassword as string)
|
||||||
|
: null;
|
||||||
|
if (passwordToValidate !== null) {
|
||||||
|
if (config.validatePasswordRequirements !== undefined) {
|
||||||
|
config.validatePasswordRequirements(passwordToValidate);
|
||||||
|
} else {
|
||||||
|
validateDefaultPasswordRequirements(passwordToValidate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const profile = config.profile?.(params, ctx) ?? defaultProfile(params);
|
||||||
|
const { email } = profile;
|
||||||
|
const secret = params.password as string;
|
||||||
|
let account: GenericDoc<DataModel, "authAccounts">;
|
||||||
|
let user: GenericDoc<DataModel, "users">;
|
||||||
|
if (flow === "signUp") {
|
||||||
|
if (secret === undefined) {
|
||||||
|
throw new Error("Missing `password` param for `signUp` flow");
|
||||||
|
}
|
||||||
|
const created = await createAccount(ctx, {
|
||||||
|
provider,
|
||||||
|
account: { id: email, secret },
|
||||||
|
profile: profile as any,
|
||||||
|
shouldLinkViaEmail: config.verify !== undefined,
|
||||||
|
shouldLinkViaPhone: false,
|
||||||
|
});
|
||||||
|
({ account, user } = created);
|
||||||
|
} else if (flow === "signIn") {
|
||||||
|
if (secret === undefined) {
|
||||||
|
throw new Error("Missing `password` param for `signIn` flow");
|
||||||
|
}
|
||||||
|
const retrieved = await retrieveAccount(ctx, {
|
||||||
|
provider,
|
||||||
|
account: { id: email, secret },
|
||||||
|
});
|
||||||
|
if (retrieved === null) {
|
||||||
|
throw new Error("Invalid credentials");
|
||||||
|
}
|
||||||
|
({ account, user } = retrieved);
|
||||||
|
// START: Optional, support password reset
|
||||||
|
} else if (flow === "reset") {
|
||||||
|
if (!config.reset) {
|
||||||
|
throw new Error(`Password reset is not enabled for ${provider}`);
|
||||||
|
}
|
||||||
|
const { account } = await retrieveAccount(ctx, {
|
||||||
|
provider,
|
||||||
|
account: { id: email },
|
||||||
|
});
|
||||||
|
return await signInViaProvider(ctx, config.reset, {
|
||||||
|
accountId: account._id,
|
||||||
|
params,
|
||||||
|
});
|
||||||
|
} else if (flow === "reset-verification") {
|
||||||
|
if (!config.reset) {
|
||||||
|
throw new Error(`Password reset is not enabled for ${provider}`);
|
||||||
|
}
|
||||||
|
if (params.newPassword === undefined) {
|
||||||
|
throw new Error(
|
||||||
|
"Missing `newPassword` param for `reset-verification` flow",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const result = await signInViaProvider(ctx, config.reset, { params });
|
||||||
|
if (result === null) {
|
||||||
|
throw new Error("Invalid code");
|
||||||
|
}
|
||||||
|
const { userId, sessionId } = result;
|
||||||
|
const secret = params.newPassword as string;
|
||||||
|
await modifyAccountCredentials(ctx, {
|
||||||
|
provider,
|
||||||
|
account: { id: email, secret },
|
||||||
|
});
|
||||||
|
await invalidateSessions(ctx, { userId, except: [sessionId] });
|
||||||
|
return { userId, sessionId };
|
||||||
|
// END
|
||||||
|
// START: Optional, email verification during sign in
|
||||||
|
} else if (flow === "email-verification") {
|
||||||
|
if (!config.verify) {
|
||||||
|
throw new Error(`Email verification is not enabled for ${provider}`);
|
||||||
|
}
|
||||||
|
const { account } = await retrieveAccount(ctx, {
|
||||||
|
provider,
|
||||||
|
account: { id: email },
|
||||||
|
});
|
||||||
|
return await signInViaProvider(ctx, config.verify, {
|
||||||
|
accountId: account._id,
|
||||||
|
params,
|
||||||
|
});
|
||||||
|
// END
|
||||||
|
} else {
|
||||||
|
throw new Error(
|
||||||
|
"Missing `flow` param, it must be one of " +
|
||||||
|
'"signUp", "signIn", "reset", "reset-verification" or ' +
|
||||||
|
'"email-verification"!',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// START: Optional, email verification during sign in
|
||||||
|
if (config.verify && !account.emailVerified) {
|
||||||
|
return await signInViaProvider(ctx, config.verify, {
|
||||||
|
accountId: account._id,
|
||||||
|
params,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// END
|
||||||
|
return { userId: user._id };
|
||||||
|
},
|
||||||
|
crypto: {
|
||||||
|
async hashSecret(password: string) {
|
||||||
|
return await new Scrypt().hash(password);
|
||||||
|
},
|
||||||
|
async verifySecret(password: string, hash: string) {
|
||||||
|
return await new Scrypt().verify(hash, password);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
extraProviders: [config.reset, config.verify],
|
||||||
|
...config,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateDefaultPasswordRequirements(password: string) {
|
||||||
|
if (
|
||||||
|
password.length < 8 ||
|
||||||
|
!/\d/.test(password) ||
|
||||||
|
!/[a-z]/.test(password) ||
|
||||||
|
!/[A-Z]/.test(password)
|
||||||
|
) {
|
||||||
|
throw new Error("Invalid password.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function defaultProfile(params: Record<string, unknown>) {
|
||||||
|
return {
|
||||||
|
email: params.email as string,
|
||||||
|
name: params.name as string,
|
||||||
|
};
|
||||||
|
}
|
2
convex/_generated/api.d.ts
vendored
2
convex/_generated/api.d.ts
vendored
@@ -13,6 +13,7 @@ import type {
|
|||||||
FilterApi,
|
FilterApi,
|
||||||
FunctionReference,
|
FunctionReference,
|
||||||
} from "convex/server";
|
} from "convex/server";
|
||||||
|
import type * as CustomPassword from "../CustomPassword.js";
|
||||||
import type * as auth from "../auth.js";
|
import type * as auth from "../auth.js";
|
||||||
import type * as http from "../http.js";
|
import type * as http from "../http.js";
|
||||||
import type * as myFunctions from "../myFunctions.js";
|
import type * as myFunctions from "../myFunctions.js";
|
||||||
@@ -26,6 +27,7 @@ import type * as myFunctions from "../myFunctions.js";
|
|||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
declare const fullApi: ApiFromModules<{
|
declare const fullApi: ApiFromModules<{
|
||||||
|
CustomPassword: typeof CustomPassword;
|
||||||
auth: typeof auth;
|
auth: typeof auth;
|
||||||
http: typeof http;
|
http: typeof http;
|
||||||
myFunctions: typeof myFunctions;
|
myFunctions: typeof myFunctions;
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import { Password } from '@convex-dev/auth/providers/Password';
|
|
||||||
import { convexAuth } from '@convex-dev/auth/server';
|
import { convexAuth } from '@convex-dev/auth/server';
|
||||||
|
import { Password } from './CustomPassword';
|
||||||
|
|
||||||
export const { auth, signIn, signOut, store, isAuthenticated } = convexAuth({
|
export const { auth, signIn, signOut, store, isAuthenticated } = convexAuth({
|
||||||
providers: [Password],
|
providers: [Password],
|
||||||
|
35
package.json
35
package.json
@@ -5,7 +5,9 @@
|
|||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "npm-run-all --parallel dev:frontend dev:backend",
|
"dev": "npm-run-all --parallel dev:frontend dev:backend",
|
||||||
|
"dev:slow": "npm-run-all --parallel dev:frontend:slow dev:backend",
|
||||||
"dev:frontend": "next dev --turbo",
|
"dev:frontend": "next dev --turbo",
|
||||||
|
"dev:frontend:slow": "next dev",
|
||||||
"dev:backend": "convex dev",
|
"dev:backend": "convex dev",
|
||||||
"predev": "convex dev --until-success && convex dev --once --run-sh \"node setup.mjs --once\" && convex dashboard",
|
"predev": "convex dev --until-success && convex dev --once --run-sh \"node setup.mjs --once\" && convex dashboard",
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
@@ -17,37 +19,42 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@convex-dev/auth": "^0.0.81",
|
"@convex-dev/auth": "^0.0.81",
|
||||||
|
"@hookform/resolvers": "^5.2.1",
|
||||||
|
"@radix-ui/react-label": "^2.1.7",
|
||||||
|
"@radix-ui/react-separator": "^1.1.7",
|
||||||
"@radix-ui/react-slot": "^1.2.3",
|
"@radix-ui/react-slot": "^1.2.3",
|
||||||
"@sentry/nextjs": "^10.7.0",
|
"@radix-ui/react-tabs": "^1.1.13",
|
||||||
|
"@sentry/nextjs": "^10.8.0",
|
||||||
"@t3-oss/env-nextjs": "^0.13.8",
|
"@t3-oss/env-nextjs": "^0.13.8",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"convex": "^1.26.0",
|
"convex": "^1.26.2",
|
||||||
"eslint-plugin-prettier": "^5.5.4",
|
"eslint-plugin-prettier": "^5.5.4",
|
||||||
"lucide-react": "^0.542.0",
|
"lucide-react": "^0.542.0",
|
||||||
"next": "15.2.3",
|
"next": "15.2.3",
|
||||||
"next-plausible": "^3.12.4",
|
"next-plausible": "^3.12.4",
|
||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
"react": "^19.0.0",
|
"react": "^19.1.1",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.1.1",
|
||||||
|
"react-hook-form": "^7.62.0",
|
||||||
"require-in-the-middle": "^7.5.2",
|
"require-in-the-middle": "^7.5.2",
|
||||||
"tailwind-merge": "^3.3.1",
|
"tailwind-merge": "^3.3.1",
|
||||||
"typescript-eslint": "^8.41.0",
|
"typescript-eslint": "^8.41.0",
|
||||||
"zod": "^4.1.5"
|
"zod": "^4.1.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/eslintrc": "^3",
|
"@eslint/eslintrc": "^3.3.1",
|
||||||
"@tailwindcss/postcss": "^4",
|
"@tailwindcss/postcss": "^4.1.12",
|
||||||
"@types/node": "^20",
|
"@types/node": "^20.19.11",
|
||||||
"@types/react": "^19",
|
"@types/react": "^19.1.12",
|
||||||
"@types/react-dom": "^19",
|
"@types/react-dom": "^19.1.9",
|
||||||
"dotenv": "^16.4.7",
|
"dotenv": "^16.6.1",
|
||||||
"eslint": "^9",
|
"eslint": "^9.34.0",
|
||||||
"eslint-config-next": "15.2.3",
|
"eslint-config-next": "15.2.3",
|
||||||
"npm-run-all": "^4.1.5",
|
"npm-run-all": "^4.1.5",
|
||||||
"prettier": "^3.5.3",
|
"prettier": "^3.6.2",
|
||||||
"tailwindcss": "^4",
|
"tailwindcss": "^4.1.12",
|
||||||
"tw-animate-css": "^1.3.7",
|
"tw-animate-css": "^1.3.7",
|
||||||
"typescript": "^5"
|
"typescript": "^5.9.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,71 +1,328 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
import { z } from 'zod';
|
||||||
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
|
import { useForm } from 'react-hook-form';
|
||||||
|
import Link from 'next/link';
|
||||||
import { useAuthActions } from '@convex-dev/auth/react';
|
import { useAuthActions } from '@convex-dev/auth/react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
|
import { ConvexError } from 'convex/values';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
Input,
|
||||||
|
Separator,
|
||||||
|
StatusMessage,
|
||||||
|
SubmitButton,
|
||||||
|
Tabs,
|
||||||
|
TabsContent,
|
||||||
|
TabsList,
|
||||||
|
TabsTrigger,
|
||||||
|
} from '@/components/ui';
|
||||||
|
|
||||||
|
const signInFormSchema = z.object({
|
||||||
|
email: z.email({
|
||||||
|
message: 'Please enter a valid email address.',
|
||||||
|
}),
|
||||||
|
password: z.string()
|
||||||
|
.min(8, {
|
||||||
|
message: 'Password must be at least 8 characters.',
|
||||||
|
})
|
||||||
|
.regex(/^(?=.*[0-9])(?=.*[a-z])(?=.*[A-Z])(?=.*[@#$%^&+=])(?=\S+$).{8,}$/, {
|
||||||
|
message: 'Password must contain at least one digit, ' +
|
||||||
|
'one uppercase letter, one lowercase letter, ' +
|
||||||
|
'and one special character.'
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
const signUpFormSchema = z.object({
|
||||||
|
name: z.string()
|
||||||
|
.min(2, {
|
||||||
|
message: 'Name must be at least 2 characters.',
|
||||||
|
}),
|
||||||
|
email: z.email({
|
||||||
|
message: 'Please enter a valid email address.',
|
||||||
|
}),
|
||||||
|
password: z.string()
|
||||||
|
.min(8, {
|
||||||
|
message: 'Password must be at least 8 characters.',
|
||||||
|
})
|
||||||
|
.regex(/^(?=.*[0-9])(?=.*[a-z])(?=.*[A-Z])(?=.*[@#$%^&+=])(?=\S+$).{8,}$/, {
|
||||||
|
message: 'Password must contain at least one digit, ' +
|
||||||
|
'one uppercase letter, one lowercase letter, ' +
|
||||||
|
'and one special character.'
|
||||||
|
}),
|
||||||
|
confirmPassword: z.string().min(8, {
|
||||||
|
message: 'Password must be at least 8 characters.',
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
.refine((data) => data.password === data.confirmPassword, {
|
||||||
|
message: 'Passwords do not match!',
|
||||||
|
path: ['confirmPassword'],
|
||||||
|
});
|
||||||
|
|
||||||
export default function SignIn() {
|
export default function SignIn() {
|
||||||
const { signIn } = useAuthActions();
|
const { signIn } = useAuthActions();
|
||||||
const [flow, setFlow] = useState<'signIn' | 'signUp'>('signIn');
|
const [flow, setFlow] = useState<'signIn' | 'signUp'>('signIn');
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [statusMessage, setStatusMessage] = useState('');
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
|
const signInForm = useForm<z.infer<typeof signInFormSchema>>({
|
||||||
|
resolver: zodResolver(signInFormSchema),
|
||||||
|
defaultValues: { email: '', password: '' },
|
||||||
|
});
|
||||||
|
|
||||||
|
const signUpForm = useForm<z.infer<typeof signUpFormSchema>>({
|
||||||
|
resolver: zodResolver(signUpFormSchema),
|
||||||
|
defaultValues: {
|
||||||
|
name: '',
|
||||||
|
email: '',
|
||||||
|
password: '',
|
||||||
|
confirmPassword: '',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleSignIn = async (values: z.infer<typeof signInFormSchema>) => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
setStatusMessage('');
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('email', values.email);
|
||||||
|
formData.append('password', values.password);
|
||||||
|
formData.append('flow', flow);
|
||||||
|
if (flow === 'signUp') {
|
||||||
|
formData.append('name', values.name);
|
||||||
|
if (values.confirmPassword !== values.password)
|
||||||
|
throw new ConvexError({message: 'Passwords do not match!'});
|
||||||
|
}
|
||||||
|
await signIn('password', formData);
|
||||||
|
signInForm.reset();
|
||||||
|
router.push('/');
|
||||||
|
} catch (error) {
|
||||||
|
setStatusMessage(`Error signing in: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='flex flex-col gap-8 w-96 mx-auto h-screen justify-center items-center'>
|
<div className='flex flex-col items-center'>
|
||||||
<p>Log in to see the numbers</p>
|
<Card className='p-4 bg-card/25 min-h-[720px] w-md'>
|
||||||
<form
|
<Tabs
|
||||||
className='flex flex-col gap-2'
|
defaultValue={flow}
|
||||||
onSubmit={(e) => {
|
onValueChange={(value) => setFlow(value as 'signIn' | 'signUp')}
|
||||||
e.preventDefault();
|
className='items-center'
|
||||||
const formData = new FormData(e.target as HTMLFormElement);
|
|
||||||
formData.set('flow', flow);
|
|
||||||
void signIn('password', formData)
|
|
||||||
.catch((error) => {
|
|
||||||
setError(error.message);
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
router.push('/');
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
className='bg-background text-foreground rounded-md p-2 border-2 border-slate-200 dark:border-slate-800'
|
|
||||||
type='email'
|
|
||||||
name='email'
|
|
||||||
placeholder='Email'
|
|
||||||
/>
|
|
||||||
<input
|
|
||||||
className='bg-background text-foreground rounded-md p-2 border-2 border-slate-200 dark:border-slate-800'
|
|
||||||
type='password'
|
|
||||||
name='password'
|
|
||||||
placeholder='Password'
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
className='bg-foreground text-background rounded-md'
|
|
||||||
type='submit'
|
|
||||||
>
|
>
|
||||||
{flow === 'signIn' ? 'Sign in' : 'Sign up'}
|
<TabsList className='py-6'>
|
||||||
</button>
|
<TabsTrigger
|
||||||
<div className='flex flex-row gap-2'>
|
value='signIn'
|
||||||
<span>
|
className='p-6 text-2xl font-bold cursor-pointer'
|
||||||
{flow === 'signIn'
|
>
|
||||||
? "Don't have an account?"
|
Sign In
|
||||||
: 'Already have an account?'}
|
</TabsTrigger>
|
||||||
</span>
|
<TabsTrigger
|
||||||
<span
|
value='signUp'
|
||||||
className='text-foreground underline hover:no-underline cursor-pointer'
|
className='p-6 text-2xl font-bold cursor-pointer'
|
||||||
onClick={() => setFlow(flow === 'signIn' ? 'signUp' : 'signIn')}
|
>
|
||||||
>
|
Sign Up
|
||||||
{flow === 'signIn' ? 'Sign up instead' : 'Sign in instead'}
|
</TabsTrigger>
|
||||||
</span>
|
</TabsList>
|
||||||
</div>
|
<TabsContent value='signIn'>
|
||||||
{error && (
|
<Card className='min-w-xs sm:min-w-sm bg-card/50'>
|
||||||
<div className='bg-red-500/20 border-2 border-red-500/50 rounded-md p-2'>
|
<CardContent>
|
||||||
<p className='text-foreground font-mono text-xs'>
|
<Form {...signInForm}>
|
||||||
Error signing in: {error}
|
<form
|
||||||
</p>
|
onSubmit={signInForm.handleSubmit(handleSignIn)}
|
||||||
</div>
|
className='flex flex-col space-y-8'
|
||||||
)}
|
>
|
||||||
</form>
|
<FormField
|
||||||
|
control={signInForm.control}
|
||||||
|
name='email'
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel className='text-xl'>
|
||||||
|
Email
|
||||||
|
</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
type='email'
|
||||||
|
placeholder='you@example.com'
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<div className='flex flex-col w-full items-center'>
|
||||||
|
<FormMessage className='w-5/6 text-center' />
|
||||||
|
</div>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={signInForm.control}
|
||||||
|
name='password'
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<div className='flex justify-between'>
|
||||||
|
<FormLabel className='text-xl'>
|
||||||
|
Password
|
||||||
|
</FormLabel>
|
||||||
|
<Link href='/forgot-password'>Forgot Password?</Link>
|
||||||
|
</div>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
type='password'
|
||||||
|
placeholder='Your password'
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<div className='flex flex-col w-full items-center'>
|
||||||
|
<FormMessage className='w-5/6 text-center' />
|
||||||
|
</div>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{statusMessage && (
|
||||||
|
<StatusMessage
|
||||||
|
message={
|
||||||
|
statusMessage.toLowerCase().includes('error')
|
||||||
|
? { error: statusMessage }
|
||||||
|
: { success: statusMessage }
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<SubmitButton
|
||||||
|
disabled={loading}
|
||||||
|
pendingText='Signing in...'
|
||||||
|
className='text-lg font-semibold w-2/3 mx-auto'
|
||||||
|
>
|
||||||
|
Sign In
|
||||||
|
</SubmitButton>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</TabsContent>
|
||||||
|
<TabsContent value='signUp'>
|
||||||
|
<Card className='min-w-xs sm:min-w-sm bg-card/50'>
|
||||||
|
<CardContent>
|
||||||
|
<Form {...signUpForm}>
|
||||||
|
<form
|
||||||
|
onSubmit={signUpForm.handleSubmit(handleSignIn)}
|
||||||
|
className='flex flex-col space-y-8'
|
||||||
|
>
|
||||||
|
<FormField
|
||||||
|
control={signUpForm.control}
|
||||||
|
name='name'
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel className='text-xl'>
|
||||||
|
Name
|
||||||
|
</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
type='text'
|
||||||
|
placeholder='Full Name'
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<div className='flex flex-col w-full items-center'>
|
||||||
|
<FormMessage className='w-5/6 text-center' />
|
||||||
|
</div>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={signUpForm.control}
|
||||||
|
name='email'
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel className='text-xl'>
|
||||||
|
Email
|
||||||
|
</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
type='email'
|
||||||
|
placeholder='you@example.com'
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<div className='flex flex-col w-full items-center'>
|
||||||
|
<FormMessage className='w-5/6 text-center' />
|
||||||
|
</div>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={signUpForm.control}
|
||||||
|
name='password'
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel className='text-xl'>
|
||||||
|
Password
|
||||||
|
</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
type='password'
|
||||||
|
placeholder='Your password'
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<div className='flex flex-col w-full items-center'>
|
||||||
|
<FormMessage className='w-5/6 text-center' />
|
||||||
|
</div>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={signUpForm.control}
|
||||||
|
name='confirmPassword'
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel className='text-xl'>
|
||||||
|
Confirm Passsword
|
||||||
|
</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
type='password'
|
||||||
|
placeholder='Confirm your password'
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<div className='flex flex-col w-full items-center'>
|
||||||
|
<FormMessage className='w-5/6 text-center' />
|
||||||
|
</div>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{statusMessage && (
|
||||||
|
<StatusMessage
|
||||||
|
message={
|
||||||
|
statusMessage.toLowerCase().includes('error')
|
||||||
|
? { error: statusMessage }
|
||||||
|
: { success: statusMessage }
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<SubmitButton
|
||||||
|
disabled={loading}
|
||||||
|
pendingText='Signing Up...'
|
||||||
|
className='text-lg font-semibold w-2/3 mx-auto'
|
||||||
|
>
|
||||||
|
Sign Up
|
||||||
|
</SubmitButton>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</TabsContent>
|
||||||
|
</Tabs>
|
||||||
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
92
src/components/ui/card.tsx
Normal file
92
src/components/ui/card.tsx
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function Card({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="card"
|
||||||
|
className={cn(
|
||||||
|
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="card-header"
|
||||||
|
className={cn(
|
||||||
|
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="card-title"
|
||||||
|
className={cn("leading-none font-semibold", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="card-description"
|
||||||
|
className={cn("text-muted-foreground text-sm", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="card-action"
|
||||||
|
className={cn(
|
||||||
|
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="card-content"
|
||||||
|
className={cn("px-6", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="card-footer"
|
||||||
|
className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
Card,
|
||||||
|
CardHeader,
|
||||||
|
CardFooter,
|
||||||
|
CardTitle,
|
||||||
|
CardAction,
|
||||||
|
CardDescription,
|
||||||
|
CardContent,
|
||||||
|
}
|
167
src/components/ui/form.tsx
Normal file
167
src/components/ui/form.tsx
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import * as LabelPrimitive from "@radix-ui/react-label"
|
||||||
|
import { Slot } from "@radix-ui/react-slot"
|
||||||
|
import {
|
||||||
|
Controller,
|
||||||
|
FormProvider,
|
||||||
|
useFormContext,
|
||||||
|
useFormState,
|
||||||
|
type ControllerProps,
|
||||||
|
type FieldPath,
|
||||||
|
type FieldValues,
|
||||||
|
} 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<TFieldValues> = FieldPath<TFieldValues>,
|
||||||
|
> = {
|
||||||
|
name: TName
|
||||||
|
}
|
||||||
|
|
||||||
|
const FormFieldContext = React.createContext<FormFieldContextValue>(
|
||||||
|
{} as FormFieldContextValue
|
||||||
|
)
|
||||||
|
|
||||||
|
const FormField = <
|
||||||
|
TFieldValues extends FieldValues = FieldValues,
|
||||||
|
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
||||||
|
>({
|
||||||
|
...props
|
||||||
|
}: ControllerProps<TFieldValues, TName>) => {
|
||||||
|
return (
|
||||||
|
<FormFieldContext.Provider value={{ name: props.name }}>
|
||||||
|
<Controller {...props} />
|
||||||
|
</FormFieldContext.Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const useFormField = () => {
|
||||||
|
const fieldContext = React.useContext(FormFieldContext)
|
||||||
|
const itemContext = React.useContext(FormItemContext)
|
||||||
|
const { getFieldState } = useFormContext()
|
||||||
|
const formState = useFormState({ name: fieldContext.name })
|
||||||
|
const fieldState = getFieldState(fieldContext.name, formState)
|
||||||
|
|
||||||
|
if (!fieldContext) {
|
||||||
|
throw new Error("useFormField should be used within <FormField>")
|
||||||
|
}
|
||||||
|
|
||||||
|
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<FormItemContextValue>(
|
||||||
|
{} as FormItemContextValue
|
||||||
|
)
|
||||||
|
|
||||||
|
function FormItem({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
const id = React.useId()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormItemContext.Provider value={{ id }}>
|
||||||
|
<div
|
||||||
|
data-slot="form-item"
|
||||||
|
className={cn("grid gap-2", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</FormItemContext.Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function FormLabel({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
|
||||||
|
const { error, formItemId } = useFormField()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Label
|
||||||
|
data-slot="form-label"
|
||||||
|
data-error={!!error}
|
||||||
|
className={cn("data-[error=true]:text-destructive", className)}
|
||||||
|
htmlFor={formItemId}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function FormControl({ ...props }: React.ComponentProps<typeof Slot>) {
|
||||||
|
const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Slot
|
||||||
|
data-slot="form-control"
|
||||||
|
id={formItemId}
|
||||||
|
aria-describedby={
|
||||||
|
!error
|
||||||
|
? `${formDescriptionId}`
|
||||||
|
: `${formDescriptionId} ${formMessageId}`
|
||||||
|
}
|
||||||
|
aria-invalid={!!error}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function FormDescription({ className, ...props }: React.ComponentProps<"p">) {
|
||||||
|
const { formDescriptionId } = useFormField()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<p
|
||||||
|
data-slot="form-description"
|
||||||
|
id={formDescriptionId}
|
||||||
|
className={cn("text-muted-foreground text-sm", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function FormMessage({ className, ...props }: React.ComponentProps<"p">) {
|
||||||
|
const { error, formMessageId } = useFormField()
|
||||||
|
const body = error ? String(error?.message ?? "") : props.children
|
||||||
|
|
||||||
|
if (!body) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<p
|
||||||
|
data-slot="form-message"
|
||||||
|
id={formMessageId}
|
||||||
|
className={cn("text-destructive text-sm", className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{body}
|
||||||
|
</p>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
useFormField,
|
||||||
|
Form,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormControl,
|
||||||
|
FormDescription,
|
||||||
|
FormMessage,
|
||||||
|
FormField,
|
||||||
|
}
|
@@ -1 +1,31 @@
|
|||||||
export { Button, buttonVariants } from './button';
|
export { Button, buttonVariants } from './button';
|
||||||
|
export {
|
||||||
|
Card,
|
||||||
|
CardHeader,
|
||||||
|
CardFooter,
|
||||||
|
CardTitle,
|
||||||
|
CardAction,
|
||||||
|
CardDescription,
|
||||||
|
CardContent,
|
||||||
|
} from './card';
|
||||||
|
export {
|
||||||
|
useFormField,
|
||||||
|
Form,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormControl,
|
||||||
|
FormDescription,
|
||||||
|
FormMessage,
|
||||||
|
FormField,
|
||||||
|
} from './form';
|
||||||
|
export { Input } from './input';
|
||||||
|
export { Label } from './label';
|
||||||
|
export { Separator } from './separator';
|
||||||
|
export { StatusMessage } from './status-message';
|
||||||
|
export { SubmitButton } from './submit-button';
|
||||||
|
export {
|
||||||
|
Tabs,
|
||||||
|
TabsList,
|
||||||
|
TabsTrigger,
|
||||||
|
TabsContent
|
||||||
|
} from './tabs';
|
||||||
|
21
src/components/ui/input.tsx
Normal file
21
src/components/ui/input.tsx
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
|
||||||
|
return (
|
||||||
|
<input
|
||||||
|
type={type}
|
||||||
|
data-slot="input"
|
||||||
|
className={cn(
|
||||||
|
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||||
|
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
|
||||||
|
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Input }
|
24
src/components/ui/label.tsx
Normal file
24
src/components/ui/label.tsx
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import * as LabelPrimitive from "@radix-ui/react-label"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function Label({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
|
||||||
|
return (
|
||||||
|
<LabelPrimitive.Root
|
||||||
|
data-slot="label"
|
||||||
|
className={cn(
|
||||||
|
"flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Label }
|
28
src/components/ui/separator.tsx
Normal file
28
src/components/ui/separator.tsx
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import * as SeparatorPrimitive from "@radix-ui/react-separator"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function Separator({
|
||||||
|
className,
|
||||||
|
orientation = "horizontal",
|
||||||
|
decorative = true,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SeparatorPrimitive.Root>) {
|
||||||
|
return (
|
||||||
|
<SeparatorPrimitive.Root
|
||||||
|
data-slot="separator"
|
||||||
|
decorative={decorative}
|
||||||
|
orientation={orientation}
|
||||||
|
className={cn(
|
||||||
|
"bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Separator }
|
55
src/components/ui/status-message.tsx
Normal file
55
src/components/ui/status-message.tsx
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
import { type ComponentProps } from 'react';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
|
type Message = { success: string } | { error: string } | { message: string };
|
||||||
|
|
||||||
|
type StatusMessageProps = {
|
||||||
|
message: Message;
|
||||||
|
containerProps?: ComponentProps<'div'>;
|
||||||
|
textProps?: ComponentProps<'div'>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const StatusMessage = ({
|
||||||
|
message,
|
||||||
|
containerProps,
|
||||||
|
textProps,
|
||||||
|
}: StatusMessageProps) => {
|
||||||
|
return (
|
||||||
|
<div className='flex flex-col items-center w-full'>
|
||||||
|
{'success' in message && (
|
||||||
|
<div {...containerProps}
|
||||||
|
className={cn(
|
||||||
|
'flex flex-col items-center w-11/12 rounded-md p-2',
|
||||||
|
'dark:bg-green-500/20 bg-green-700/20 border-2',
|
||||||
|
'dark:border-green-500/50 border-green-700/50',
|
||||||
|
containerProps?.className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<p {...textProps}>{message.success}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{'error' in message && (
|
||||||
|
<div {...containerProps}
|
||||||
|
className={cn(
|
||||||
|
'flex flex-col items-center w-11/12 rounded-md p-2',
|
||||||
|
'bg-destructive/20 border-2 border-destructive/80',
|
||||||
|
containerProps?.className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<p {...textProps}>{message.error}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{'message' in message && (
|
||||||
|
<div {...containerProps}
|
||||||
|
className={cn(
|
||||||
|
'flex flex-col items-center w-11/12 rounded-md p-2',
|
||||||
|
'bg-accent/20 border-2 border-primary/80',
|
||||||
|
containerProps?.className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<p {...textProps}>{message.message}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
50
src/components/ui/submit-button.tsx
Normal file
50
src/components/ui/submit-button.tsx
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
'use client';
|
||||||
|
import { Button } from '@/components/ui';
|
||||||
|
import { type ComponentProps } from 'react';
|
||||||
|
import { useFormStatus } from 'react-dom';
|
||||||
|
import { Loader2 } from 'lucide-react';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
|
export type SubmitButtonProps = Omit<
|
||||||
|
ComponentProps<typeof Button>,
|
||||||
|
'type' | 'aria-disabled'
|
||||||
|
> & {
|
||||||
|
pendingText?: string;
|
||||||
|
pendingTextProps?: ComponentProps<'p'>;
|
||||||
|
loaderProps?: ComponentProps<typeof Loader2>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SubmitButton = ({
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
pendingText = 'Submitting...',
|
||||||
|
pendingTextProps,
|
||||||
|
loaderProps,
|
||||||
|
...props
|
||||||
|
}: SubmitButtonProps) => {
|
||||||
|
const { pending } = useFormStatus();
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
type='submit'
|
||||||
|
aria-disabled={pending}
|
||||||
|
{...props}
|
||||||
|
className={cn('cursor-pointer', className)}
|
||||||
|
>
|
||||||
|
{pending || props.disabled ? (
|
||||||
|
<>
|
||||||
|
<Loader2
|
||||||
|
{...loaderProps}
|
||||||
|
className={cn('mr-2 h-4 w-4 animate-spin', loaderProps?.className)}
|
||||||
|
/>
|
||||||
|
<p {...pendingTextProps}
|
||||||
|
className={cn('text-sm font-medium', pendingTextProps?.className)}
|
||||||
|
>
|
||||||
|
{pendingText}
|
||||||
|
</p>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
children
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
};
|
66
src/components/ui/tabs.tsx
Normal file
66
src/components/ui/tabs.tsx
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import * as TabsPrimitive from "@radix-ui/react-tabs"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function Tabs({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof TabsPrimitive.Root>) {
|
||||||
|
return (
|
||||||
|
<TabsPrimitive.Root
|
||||||
|
data-slot="tabs"
|
||||||
|
className={cn("flex flex-col gap-2", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function TabsList({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof TabsPrimitive.List>) {
|
||||||
|
return (
|
||||||
|
<TabsPrimitive.List
|
||||||
|
data-slot="tabs-list"
|
||||||
|
className={cn(
|
||||||
|
"bg-muted text-muted-foreground inline-flex h-9 w-fit items-center justify-center rounded-lg p-[3px]",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function TabsTrigger({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof TabsPrimitive.Trigger>) {
|
||||||
|
return (
|
||||||
|
<TabsPrimitive.Trigger
|
||||||
|
data-slot="tabs-trigger"
|
||||||
|
className={cn(
|
||||||
|
"data-[state=active]:bg-background dark:data-[state=active]:text-foreground focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 text-foreground dark:text-muted-foreground inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:shadow-sm [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function TabsContent({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof TabsPrimitive.Content>) {
|
||||||
|
return (
|
||||||
|
<TabsPrimitive.Content
|
||||||
|
data-slot="tabs-content"
|
||||||
|
className={cn("flex-1 outline-none", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Tabs, TabsList, TabsTrigger, TabsContent }
|
Reference in New Issue
Block a user