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 (
+
+ );
+}
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 (
+