From 18b3c193283052152f881b28c2ee70a7d2ff8816 Mon Sep 17 00:00:00 2001 From: Jack-86 Date: Mon, 22 Dec 2025 07:39:07 +0800 Subject: [PATCH] Add premilinary auth and form logic --- package-lock.json | 362 ++++++++++++++++++++++++++++- package.json | 15 +- src/app/api/auth/[...all]/route.ts | 4 + src/app/dashboard/dummyPage.tsx | 7 + src/app/signup/page.tsx | 11 + src/components/signin-form.tsx | 0 src/components/signup-form.tsx | 218 +++++++++++++++++ src/components/ui/button.tsx | 57 +++++ src/components/ui/card.tsx | 83 +++++++ src/components/ui/field.tsx | 244 +++++++++++++++++++ src/components/ui/form.tsx | 184 +++++++++++++++ src/components/ui/input.tsx | 22 ++ src/components/ui/label.tsx | 24 ++ src/components/ui/separator.tsx | 28 +++ src/lib/auth-client.ts | 4 + src/lib/auth.ts | 18 ++ src/lib/schema.ts | 105 ++++++++- src/middleware.ts | 27 +++ src/server/users.ts | 20 ++ 19 files changed, 1422 insertions(+), 11 deletions(-) create mode 100644 src/app/api/auth/[...all]/route.ts create mode 100644 src/app/dashboard/dummyPage.tsx create mode 100644 src/app/signup/page.tsx create mode 100644 src/components/signin-form.tsx create mode 100644 src/components/signup-form.tsx create mode 100644 src/components/ui/button.tsx create mode 100644 src/components/ui/card.tsx create mode 100644 src/components/ui/field.tsx create mode 100644 src/components/ui/form.tsx create mode 100644 src/components/ui/input.tsx create mode 100644 src/components/ui/label.tsx create mode 100644 src/components/ui/separator.tsx create mode 100644 src/lib/auth-client.ts create mode 100644 src/lib/auth.ts create mode 100644 src/middleware.ts create mode 100644 src/server/users.ts diff --git a/package-lock.json b/package-lock.json index 625ff7d..afb12de 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,12 +9,17 @@ "version": "0.1.0", "dependencies": { "@floating-ui/dom": "^1.7.4", + "@hookform/resolvers": "^5.2.2", "@mantine/core": "^8.3.4", "@mantine/dates": "^8.3.5", "@mantine/hooks": "^8.3.4", "@neondatabase/serverless": "^1.0.2", "@next/eslint-plugin-next": "^15.5.4", + "@radix-ui/react-label": "^2.1.8", + "@radix-ui/react-separator": "^1.1.8", + "@radix-ui/react-slot": "^1.2.4", "@tanstack/react-table": "^8.21.3", + "better-auth": "^1.4.1", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "d3": "^7.9.0", @@ -26,11 +31,13 @@ "react": "19.1.0", "react-dom": "19.1.0", "react-excel-renderer": "^1.1.0", + "react-hook-form": "^7.67.0", "react-icons": "^5.5.0", "tailwind-merge": "^3.4.0", "tailwindcss-animate": "^1.0.7", "vercel": "^48.2.0", - "xlsx": "^0.18.5" + "xlsx": "^0.18.5", + "zod": "^4.1.13" }, "devDependencies": { "@eslint/js": "^9.37.0", @@ -38,7 +45,7 @@ "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", - "drizzle-kit": "^0.31.6", + "drizzle-kit": "^0.31.7", "eslint": "^9.37.0", "eslint-config-prettier": "^10.1.8", "eslint-plugin-react": "^7.37.5", @@ -73,6 +80,17 @@ "node": ">=6.9.0" } }, + "node_modules/@better-auth/utils": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@better-auth/utils/-/utils-0.3.0.tgz", + "integrity": "sha512-W+Adw6ZA6mgvnSnhOki270rwJ42t4XzSK6YWGF//BbVXL6SwCLWfyzBc1lN2m/4RM28KubdBKQ4X5VMoLRNPQw==", + "license": "MIT" + }, + "node_modules/@better-fetch/fetch": { + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/@better-fetch/fetch/-/fetch-1.1.18.tgz", + "integrity": "sha512-rEFOE1MYIsBmoMJtQbl32PGHHXuG2hDxvEd7rUHE0vCBoFQVSDqaVs9hkZEtHCxRoY+CljXKFCOuJ8uxqw1LcA==" + }, "node_modules/@cspotcode/source-map-support": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", @@ -1278,6 +1296,18 @@ "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==", "license": "MIT" }, + "node_modules/@hookform/resolvers": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-5.2.2.tgz", + "integrity": "sha512-A/IxlMLShx3KjV/HeTcTfaMxdwy690+L/ZADoeaTltLx+CVuzkeVIPuybK3jrRfw7YZnmdKsVVHAlEPIAEUNlA==", + "license": "MIT", + "dependencies": { + "@standard-schema/utils": "^0.3.0" + }, + "peerDependencies": { + "react-hook-form": "^7.55.0" + } + }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", @@ -2225,6 +2255,30 @@ "node": ">= 10" } }, + "node_modules/@noble/ciphers": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-2.0.1.tgz", + "integrity": "sha512-xHK3XHPUW8DTAobU+G0XT+/w+JLM7/8k1UFdB5xg/zTFPnFCobhftzw8wl4Lw2aq/Rvir5pxfZV5fEazmeCJ2g==", + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/hashes": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.0.1.tgz", + "integrity": "sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw==", + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -2288,6 +2342,108 @@ "node": ">=14" } }, + "node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", + "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-label": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.8.tgz", + "integrity": "sha512-FmXs37I6hSBVDlO4y764TNz1rLgKwjJMQ0EGte6F3Cb3f4bIuHB/iLa/8I9VKkmOy+gNHq8rql3j686ACVV21A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.4" + }, + "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-primitive": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.4.tgz", + "integrity": "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.4" + }, + "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-separator": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.8.tgz", + "integrity": "sha512-sDvqVY4itsKwwSMEe0jtKgfTh+72Sy3gPmQpjqcQneqQ4PFmr/1I0YA+2/puilhggCe2gJcx5EBAYFkWkdpa5g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.4" + }, + "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.2.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.4.tgz", + "integrity": "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA==", + "license": "MIT", + "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" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@rolldown/binding-android-arm64": { "version": "1.0.0-beta.35", "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-beta.35.tgz", @@ -2519,6 +2675,18 @@ "integrity": "sha512-XJfwUVUKDHF5ugKwIcxEgc9k8b7HbznCp6eUfWgu710hMPNIO4aw4/zB5RogDQz8nd6gyCDpU9O/m6qYEWY6yQ==", "license": "MIT" }, + "node_modules/@standard-schema/spec": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz", + "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==", + "license": "MIT" + }, + "node_modules/@standard-schema/utils": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz", + "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", + "license": "MIT" + }, "node_modules/@swc/helpers": { "version": "0.5.15", "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", @@ -2933,7 +3101,7 @@ "version": "19.2.3", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", - "dev": true, + "devOptional": true, "license": "MIT", "peerDependencies": { "@types/react": "^19.2.0" @@ -3336,6 +3504,15 @@ "zod": "3.22.4" } }, + "node_modules/@vercel/express/node_modules/zod": { + "version": "3.22.4", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.22.4.tgz", + "integrity": "sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, "node_modules/@vercel/fastify": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/@vercel/fastify/-/fastify-0.1.7.tgz", @@ -3519,6 +3696,15 @@ "zod": "3.22.4" } }, + "node_modules/@vercel/hono/node_modules/zod": { + "version": "3.22.4", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.22.4.tgz", + "integrity": "sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, "node_modules/@vercel/hydrogen": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/@vercel/hydrogen/-/hydrogen-1.3.2.tgz", @@ -3539,6 +3725,15 @@ "zod": "3.22.4" } }, + "node_modules/@vercel/introspection/node_modules/zod": { + "version": "3.22.4", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.22.4.tgz", + "integrity": "sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, "node_modules/@vercel/next": { "version": "4.15.3", "resolved": "https://registry.npmjs.org/@vercel/next/-/next-4.15.3.tgz", @@ -4749,6 +4944,103 @@ ], "license": "MIT" }, + "node_modules/better-auth": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/better-auth/-/better-auth-1.4.1.tgz", + "integrity": "sha512-HDVE69Nw6Y1FPTcmFEmPolfsjMfVB5U823Ij9yWBoM8MdHZ2lA3JVus4xQJ2oRE1riJTlcSLFcgJKWGD7V7hmw==", + "license": "MIT", + "dependencies": { + "@better-auth/core": "1.4.1", + "@better-auth/telemetry": "1.4.1", + "@better-auth/utils": "0.3.0", + "@better-fetch/fetch": "1.1.18", + "@noble/ciphers": "^2.0.0", + "@noble/hashes": "^2.0.0", + "@standard-schema/spec": "^1.0.0", + "better-call": "1.1.0", + "defu": "^6.1.4", + "jose": "^6.1.0", + "kysely": "^0.28.5", + "nanostores": "^1.0.1", + "zod": "^4.1.12" + }, + "peerDependenciesMeta": { + "@lynx-js/react": { + "optional": true + }, + "@sveltejs/kit": { + "optional": true + }, + "next": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + }, + "solid-js": { + "optional": true + }, + "svelte": { + "optional": true + }, + "vue": { + "optional": true + } + } + }, + "node_modules/better-auth/node_modules/@better-auth/core": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@better-auth/core/-/core-1.4.1.tgz", + "integrity": "sha512-N4kyRdA472WGLoCjsJpUeYdZZvpoBDgP65hUeQQxTQYwBTqD9O17Tokax9CdNbkb4g34sTfxaJCfcncE3Hy4SA==", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "zod": "^4.1.12" + }, + "peerDependencies": { + "@better-auth/utils": "0.3.0", + "@better-fetch/fetch": "1.1.18", + "better-call": "1.1.0", + "jose": "^6.1.0", + "kysely": "^0.28.5", + "nanostores": "^1.0.1" + } + }, + "node_modules/better-auth/node_modules/@better-auth/telemetry": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@better-auth/telemetry/-/telemetry-1.4.1.tgz", + "integrity": "sha512-yNeazXYvMbyuCe1AA6tYWsJEKgcS7gF9PmmACmrPVhVBe1ncDhVfWMZ++YCmA2h8hjkR9755ZyofiYRPbj+kXQ==", + "dependencies": { + "@better-auth/utils": "0.3.0", + "@better-fetch/fetch": "1.1.18" + }, + "peerDependencies": { + "@better-auth/core": "1.4.1" + } + }, + "node_modules/better-auth/node_modules/jose": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.1.2.tgz", + "integrity": "sha512-MpcPtHLE5EmztuFIqB0vzHAWJPpmN1E6L4oo+kze56LIs3MyXIj9ZHMDxqOvkP38gBR7K1v3jqd4WU2+nrfONQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/better-call": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/better-call/-/better-call-1.1.0.tgz", + "integrity": "sha512-7CecYG+yN8J1uBJni/Mpjryp8bW/YySYsrGEWgFe048ORASjq17keGjbKI2kHEOSc6u8pi11UxzkJ7jIovQw6w==", + "license": "MIT", + "dependencies": { + "@better-auth/utils": "^0.3.0", + "@better-fetch/fetch": "^1.1.4", + "rou3": "^0.5.1", + "set-cookie-parser": "^2.7.1" + } + }, "node_modules/big.js": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", @@ -6171,6 +6463,12 @@ "node": ">=0.10.0" } }, + "node_modules/defu": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz", + "integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==", + "license": "MIT" + }, "node_modules/delaunator": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.0.1.tgz", @@ -9302,6 +9600,15 @@ "node": ">=0.10.0" } }, + "node_modules/kysely": { + "version": "0.28.8", + "resolved": "https://registry.npmjs.org/kysely/-/kysely-0.28.8.tgz", + "integrity": "sha512-QUOgl5ZrS9IRuhq5FvOKFSsD/3+IA6MLE81/bOOTRA/YQpKDza2sFdN5g6JCB9BOpqMJDGefLCQ9F12hRS13TA==", + "license": "MIT", + "engines": { + "node": ">=20.0.0" + } + }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -10140,6 +10447,21 @@ "node": ">=0.10.0" } }, + "node_modules/nanostores": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/nanostores/-/nanostores-1.1.0.tgz", + "integrity": "sha512-yJBmDJr18xy47dbNVlHcgdPrulSn1nhSE6Ns9vTG+Nx9VPT6iV1MD6aQFp/t52zpf82FhLLTXAXr30NuCnxvwA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "engines": { + "node": "^20.0.0 || >=22.0.0" + } + }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -11491,6 +11813,22 @@ "node": ">=0.8" } }, + "node_modules/react-hook-form": { + "version": "7.67.0", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.67.0.tgz", + "integrity": "sha512-E55EOwKJHHIT/I6J9DmQbCWToAYSw9nN5R57MZw9rMtjh+YQreMDxRLfdjfxQbiJ3/qbg3Z02wGzBX4M+5fMtQ==", + "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-icons": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.5.0.tgz", @@ -11949,6 +12287,12 @@ "@rolldown/binding-win32-x64-msvc": "1.0.0-beta.35" } }, + "node_modules/rou3": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/rou3/-/rou3-0.5.1.tgz", + "integrity": "sha512-OXMmJ3zRk2xeXFGfA3K+EOPHC5u7RDFG7lIOx0X1pdnhUkI8MdVrbV+sNsD80ElpUZ+MRHdyxPnFthq9VHs8uQ==", + "license": "MIT" + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -12115,6 +12459,12 @@ "randombytes": "^2.1.0" } }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "license": "MIT" + }, "node_modules/set-function-length": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", @@ -15465,9 +15815,9 @@ } }, "node_modules/zod": { - "version": "3.22.4", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.22.4.tgz", - "integrity": "sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==", + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.1.13.tgz", + "integrity": "sha512-AvvthqfqrAhNH9dnfmrfKzX5upOdjUVJYFqNSlkmGf64gRaTzlPwz99IHYnVs28qYAybvAlBV+H7pn0saFY4Ig==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" diff --git a/package.json b/package.json index 19b4460..1fa6777 100644 --- a/package.json +++ b/package.json @@ -13,15 +13,20 @@ }, "dependencies": { "@floating-ui/dom": "^1.7.4", + "@hookform/resolvers": "^5.2.2", "@mantine/core": "^8.3.4", "@mantine/dates": "^8.3.5", "@mantine/hooks": "^8.3.4", "@neondatabase/serverless": "^1.0.2", "@next/eslint-plugin-next": "^15.5.4", - "d3": "^7.9.0", + "@radix-ui/react-label": "^2.1.8", + "@radix-ui/react-separator": "^1.1.8", + "@radix-ui/react-slot": "^1.2.4", "@tanstack/react-table": "^8.21.3", + "better-auth": "^1.4.1", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "d3": "^7.9.0", "dotenv": "^17.2.3", "drizzle-orm": "^0.44.7", "lucide": "^0.544.0", @@ -30,11 +35,13 @@ "react": "19.1.0", "react-dom": "19.1.0", "react-excel-renderer": "^1.1.0", + "react-hook-form": "^7.67.0", + "react-icons": "^5.5.0", "tailwind-merge": "^3.4.0", "tailwindcss-animate": "^1.0.7", - "react-icons": "^5.5.0", "vercel": "^48.2.0", - "xlsx": "^0.18.5" + "xlsx": "^0.18.5", + "zod": "^4.1.13" }, "devDependencies": { "@eslint/js": "^9.37.0", @@ -42,7 +49,7 @@ "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", - "drizzle-kit": "^0.31.6", + "drizzle-kit": "^0.31.7", "eslint": "^9.37.0", "eslint-config-prettier": "^10.1.8", "eslint-plugin-react": "^7.37.5", diff --git a/src/app/api/auth/[...all]/route.ts b/src/app/api/auth/[...all]/route.ts new file mode 100644 index 0000000..7cbe91b --- /dev/null +++ b/src/app/api/auth/[...all]/route.ts @@ -0,0 +1,4 @@ +import { auth } from "@/lib/auth"; +import { toNextJsHandler } from "better-auth/next-js"; + +export const { POST, GET } = toNextJsHandler(auth); diff --git a/src/app/dashboard/dummyPage.tsx b/src/app/dashboard/dummyPage.tsx new file mode 100644 index 0000000..acf1135 --- /dev/null +++ b/src/app/dashboard/dummyPage.tsx @@ -0,0 +1,7 @@ +export default function DummyPage() { + return ( +
+

Hello World

+
+ ); +} diff --git a/src/app/signup/page.tsx b/src/app/signup/page.tsx new file mode 100644 index 0000000..defba56 --- /dev/null +++ b/src/app/signup/page.tsx @@ -0,0 +1,11 @@ +import { SignupForm } from "@/components/signup-form"; + +export default function SignupPage() { + return ( +
+
+ +
+
+ ); +} diff --git a/src/components/signin-form.tsx b/src/components/signin-form.tsx new file mode 100644 index 0000000..e69de29 diff --git a/src/components/signup-form.tsx b/src/components/signup-form.tsx new file mode 100644 index 0000000..81a3bcf --- /dev/null +++ b/src/components/signup-form.tsx @@ -0,0 +1,218 @@ +"use client"; + +import { zodResolver } from "@hookform/resolvers/zod"; +import { useForm } from "react-hook-form"; + +import { cn } from "@/lib/utils"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent } from "@/components/ui/card"; +import { + Field, + FieldDescription, + FieldGroup, + FieldLabel, + FieldSeparator, +} from "@/components/ui/field"; + +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; + +import { Input } from "@/components/ui/input"; +import { signUp } from "@/server/users"; + +import { z } from "zod"; + +const formSchema = z.object({ + em: z.email(), + pw: z.string().min(8), +}); + +export function SignupForm({ + className, + ...props +}: React.ComponentProps<"div">) { + // 1. Define your form. + const form = useForm>({ + resolver: zodResolver(formSchema), + defaultValues: { + em: "", + pw: "", + }, + }); + + // 2. Define a submit handler. + function onSubmit(values: z.infer) { + signUp(values.em, values.pw); + } + return ( +
+ + +
+ + +
+

+ Create your account +

+

+ Enter your email below to create your + account +

+
+ + ( + + Email + + + + + + )} + /> + + We'll use this to contact you. We + will not share your email with anyone + else. + + + + + + ( + + + Password + + + + + + + )} + /> + + + ( + + + Confirm Password + + + + + + + )} + /> + + + + Must be at least 8 characters long. + + + + + + + Or continue with + + + + + + + + Already have an account?{" "} + Sign in + +
+
+ +
+ Image +
+
+
+ + By clicking continue, you agree to our{" "} + Terms of Service and{" "} + Privacy Policy. + +
+ ); +} diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx new file mode 100644 index 0000000..5301880 --- /dev/null +++ b/src/components/ui/button.tsx @@ -0,0 +1,57 @@ +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"; + +const buttonVariants = cva( + "inline-flex items-center justify-center gap-2 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 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", + { + variants: { + variant: { + default: + "bg-primary text-primary-foreground shadow hover:bg-primary/90", + destructive: + "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90", + outline: + "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground", + secondary: + "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80", + ghost: "hover:bg-accent hover:text-accent-foreground", + link: "text-primary underline-offset-4 hover:underline", + }, + size: { + default: "h-9 px-4 py-2", + sm: "h-8 rounded-md px-3 text-xs", + lg: "h-10 rounded-md px-8", + icon: "h-9 w-9", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + }, +); + +export interface ButtonProps + extends React.ButtonHTMLAttributes, + VariantProps { + asChild?: boolean; +} + +const Button = React.forwardRef( + ({ className, variant, size, asChild = false, ...props }, ref) => { + const Comp = asChild ? Slot : "button"; + return ( + + ); + }, +); +Button.displayName = "Button"; + +export { Button, buttonVariants }; diff --git a/src/components/ui/card.tsx b/src/components/ui/card.tsx new file mode 100644 index 0000000..f1ebb15 --- /dev/null +++ b/src/components/ui/card.tsx @@ -0,0 +1,83 @@ +import * as React from "react"; + +import { cn } from "@/lib/utils"; + +const Card = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)); +Card.displayName = "Card"; + +const CardHeader = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)); +CardHeader.displayName = "CardHeader"; + +const CardTitle = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)); +CardTitle.displayName = "CardTitle"; + +const CardDescription = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)); +CardDescription.displayName = "CardDescription"; + +const CardContent = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)); +CardContent.displayName = "CardContent"; + +const CardFooter = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)); +CardFooter.displayName = "CardFooter"; + +export { + Card, + CardHeader, + CardFooter, + CardTitle, + CardDescription, + CardContent, +}; diff --git a/src/components/ui/field.tsx b/src/components/ui/field.tsx new file mode 100644 index 0000000..4f889a6 --- /dev/null +++ b/src/components/ui/field.tsx @@ -0,0 +1,244 @@ +"use client"; + +import { useMemo } from "react"; +import { cva, type VariantProps } from "class-variance-authority"; + +import { cn } from "@/lib/utils"; +import { Label } from "@/components/ui/label"; +import { Separator } from "@/components/ui/separator"; + +function FieldSet({ className, ...props }: React.ComponentProps<"fieldset">) { + return ( +
[data-slot=checkbox-group]]:gap-3 has-[>[data-slot=radio-group]]:gap-3", + className, + )} + {...props} + /> + ); +} + +function FieldLegend({ + className, + variant = "legend", + ...props +}: React.ComponentProps<"legend"> & { variant?: "legend" | "label" }) { + return ( + + ); +} + +function FieldGroup({ className, ...props }: React.ComponentProps<"div">) { + return ( +
[data-slot=field-group]]:gap-4", + className, + )} + {...props} + /> + ); +} + +const fieldVariants = cva( + "group/field data-[invalid=true]:text-destructive flex w-full gap-3", + { + variants: { + orientation: { + vertical: ["flex-col [&>*]:w-full [&>.sr-only]:w-auto"], + horizontal: [ + "flex-row items-center", + "[&>[data-slot=field-label]]:flex-auto", + "has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px has-[>[data-slot=field-content]]:items-start", + ], + responsive: [ + "@md/field-group:flex-row @md/field-group:items-center @md/field-group:[&>*]:w-auto flex-col [&>*]:w-full [&>.sr-only]:w-auto", + "@md/field-group:[&>[data-slot=field-label]]:flex-auto", + "@md/field-group:has-[>[data-slot=field-content]]:items-start @md/field-group:has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px", + ], + }, + }, + defaultVariants: { + orientation: "vertical", + }, + }, +); + +function Field({ + className, + orientation = "vertical", + ...props +}: React.ComponentProps<"div"> & VariantProps) { + return ( +
+ ); +} + +function FieldContent({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ); +} + +function FieldLabel({ + className, + ...props +}: React.ComponentProps) { + return ( +