diff --git a/bun.lock b/bun.lock
index 7110137..4c01583 100644
--- a/bun.lock
+++ b/bun.lock
@@ -6,6 +6,7 @@
"dependencies": {
"@clerk/nextjs": "^6.20.2",
"@neondatabase/serverless": "neondatabase/serverless",
+ "@radix-ui/react-alert-dialog": "^1.1.14",
"@radix-ui/react-avatar": "^1.1.10",
"@radix-ui/react-dialog": "^1.1.14",
"@radix-ui/react-dropdown-menu": "^2.1.15",
@@ -16,6 +17,7 @@
"@trpc/client": "^11.0.0",
"@trpc/react-query": "^11.0.0",
"@trpc/server": "^11.0.0",
+ "better-auth": "^1.2.8",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"drizzle-orm": "^0.44.1",
@@ -55,6 +57,10 @@
"@ampproject/remapping": ["@ampproject/remapping@2.3.0", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw=="],
+ "@better-auth/utils": ["@better-auth/utils@0.2.5", "", { "dependencies": { "typescript": "^5.8.2", "uncrypto": "^0.1.3" } }, "sha512-uI2+/8h/zVsH8RrYdG8eUErbuGBk16rZKQfz8CjxQOyCE6v7BqFYEbFwvOkvl1KbUdxhqOnXp78+uE5h8qVEgQ=="],
+
+ "@better-fetch/fetch": ["@better-fetch/fetch@1.1.18", "", {}, "sha512-rEFOE1MYIsBmoMJtQbl32PGHHXuG2hDxvEd7rUHE0vCBoFQVSDqaVs9hkZEtHCxRoY+CljXKFCOuJ8uxqw1LcA=="],
+
"@clerk/backend": ["@clerk/backend@1.34.0", "", { "dependencies": { "@clerk/shared": "^3.9.5", "@clerk/types": "^4.59.3", "cookie": "1.0.2", "snakecase-keys": "8.0.1", "tslib": "2.8.1" }, "peerDependencies": { "svix": "^1.62.0" }, "optionalPeers": ["svix"] }, "sha512-9rZ8hQJVpX5KX2bEpiuVXfpjhojQCiqCWADJDdCI0PCeKxn58Ep0JPYiIcczg4VKUc3a7jve9vXylykG2XajLQ=="],
"@clerk/clerk-react": ["@clerk/clerk-react@5.31.8", "", { "dependencies": { "@clerk/shared": "^3.9.5", "@clerk/types": "^4.59.3", "tslib": "2.8.1" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0 || ^19.0.0-0", "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-0" } }, "sha512-GPhOdI7drAaamiKIhzfWiOVe4zw4wUi1sKp6khgUzcjr9hRopdZvzMts0fU+XLHFnYUSX8IPw4c0CDXY1wBKuw=="],
@@ -153,6 +159,8 @@
"@floating-ui/utils": ["@floating-ui/utils@0.2.9", "", {}, "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg=="],
+ "@hexagon/base64": ["@hexagon/base64@1.1.28", "", {}, "sha512-lhqDEAvWixy3bZ+UOYbPwUbBkwBq5C1LAJ/xPC8Oi+lL54oyakv/npbA0aU2hgCsx/1NUd4IBvV03+aUBWxerw=="],
+
"@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=="],
@@ -215,6 +223,8 @@
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.25", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ=="],
+ "@levischuck/tiny-cbor": ["@levischuck/tiny-cbor@0.2.11", "", {}, "sha512-llBRm4dT4Z89aRsm6u2oEZ8tfwL/2l6BwpZ7JcyieouniDECM5AqNgr/y08zalEIvW3RSK4upYyybDcmjXqAow=="],
+
"@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@0.2.10", "", { "dependencies": { "@emnapi/core": "^1.4.3", "@emnapi/runtime": "^1.4.3", "@tybys/wasm-util": "^0.9.0" } }, "sha512-bCsCyeZEwVErsGmyPNSzwfwFn4OdxBj0mmv6hOFucB/k81Ojdu68RbZdxYsRQUPc9l6SU5F/cG+bXgWs3oUgsQ=="],
"@neondatabase/serverless": ["@neondatabase/serverless@github:neondatabase/serverless#915f90f", { "dependencies": { "@types/node": "^22.10.2", "@types/pg": "^8.8.0" } }, "neondatabase-serverless-915f90f"],
@@ -239,6 +249,10 @@
"@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@15.3.3", "", { "os": "win32", "cpu": "x64" }, "sha512-4QZG6F8enl9/S2+yIiOiju0iCTFd93d8VC1q9LZS4p/Xuk81W2QDjCFeoogmrWWkAD59z8ZxepBQap2dKS5ruw=="],
+ "@noble/ciphers": ["@noble/ciphers@0.6.0", "", {}, "sha512-mIbq/R9QXk5/cTfESb1OKtyFnk7oc1Om/8onA1158K9/OZUQFDEVy55jVTato+xmp3XX6F6Qh0zz0Nc1AxAlRQ=="],
+
+ "@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="],
+
"@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="],
"@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="],
@@ -247,8 +261,20 @@
"@nolyfill/is-core-module": ["@nolyfill/is-core-module@1.0.39", "", {}, "sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA=="],
+ "@peculiar/asn1-android": ["@peculiar/asn1-android@2.3.16", "", { "dependencies": { "@peculiar/asn1-schema": "^2.3.15", "asn1js": "^3.0.5", "tslib": "^2.8.1" } }, "sha512-a1viIv3bIahXNssrOIkXZIlI2ePpZaNmR30d4aBL99mu2rO+mT9D6zBsp7H6eROWGtmwv0Ionp5olJurIo09dw=="],
+
+ "@peculiar/asn1-ecc": ["@peculiar/asn1-ecc@2.3.15", "", { "dependencies": { "@peculiar/asn1-schema": "^2.3.15", "@peculiar/asn1-x509": "^2.3.15", "asn1js": "^3.0.5", "tslib": "^2.8.1" } }, "sha512-/HtR91dvgog7z/WhCVdxZJ/jitJuIu8iTqiyWVgRE9Ac5imt2sT/E4obqIVGKQw7PIy+X6i8lVBoT6wC73XUgA=="],
+
+ "@peculiar/asn1-rsa": ["@peculiar/asn1-rsa@2.3.15", "", { "dependencies": { "@peculiar/asn1-schema": "^2.3.15", "@peculiar/asn1-x509": "^2.3.15", "asn1js": "^3.0.5", "tslib": "^2.8.1" } }, "sha512-p6hsanvPhexRtYSOHihLvUUgrJ8y0FtOM97N5UEpC+VifFYyZa0iZ5cXjTkZoDwxJ/TTJ1IJo3HVTB2JJTpXvg=="],
+
+ "@peculiar/asn1-schema": ["@peculiar/asn1-schema@2.3.15", "", { "dependencies": { "asn1js": "^3.0.5", "pvtsutils": "^1.3.6", "tslib": "^2.8.1" } }, "sha512-QPeD8UA8axQREpgR5UTAfu2mqQmm97oUqahDtNdBcfj3qAnoXzFdQW+aNf/tD2WVXF8Fhmftxoj0eMIT++gX2w=="],
+
+ "@peculiar/asn1-x509": ["@peculiar/asn1-x509@2.3.15", "", { "dependencies": { "@peculiar/asn1-schema": "^2.3.15", "asn1js": "^3.0.5", "pvtsutils": "^1.3.6", "tslib": "^2.8.1" } }, "sha512-0dK5xqTqSLaxv1FHXIcd4Q/BZNuopg+u1l23hT9rOmQ1g4dNtw0g/RnEi+TboB0gOwGtrWn269v27cMgchFIIg=="],
+
"@radix-ui/primitive": ["@radix-ui/primitive@1.1.2", "", {}, "sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA=="],
+ "@radix-ui/react-alert-dialog": ["@radix-ui/react-alert-dialog@1.1.14", "", { "dependencies": { "@radix-ui/primitive": "1.1.2", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dialog": "1.1.14", "@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-IOZfZ3nPvN6lXpJTBCunFQPRSvK8MDgSc1FB85xnIpUKOw9en0dJj8JmCAxV7BiZdtYlUpmrQjoTFkVYtdoWzQ=="],
+
"@radix-ui/react-arrow": ["@radix-ui/react-arrow@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-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w=="],
"@radix-ui/react-avatar": ["@radix-ui/react-avatar@1.1.10", "", { "dependencies": { "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-is-hydrated": "0.1.0", "@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-V8piFfWapM5OmNCXTzVQY+E1rDa53zY+MQ4Y7356v4fFz6vqCyUtIz2rUD44ZEdwg78/jKmMJHj07+C/Z/rcog=="],
@@ -311,6 +337,10 @@
"@rushstack/eslint-patch": ["@rushstack/eslint-patch@1.11.0", "", {}, "sha512-zxnHvoMQVqewTJr/W4pKjF0bMGiKJv1WX7bSrkl46Hg0QjESbzBROWK0Wg4RphzSOS5Jiy7eFimmM3UgMrMZbQ=="],
+ "@simplewebauthn/browser": ["@simplewebauthn/browser@13.1.0", "", {}, "sha512-WuHZ/PYvyPJ9nxSzgHtOEjogBhwJfC8xzYkPC+rR/+8chl/ft4ngjiK8kSU5HtRJfczupyOh33b25TjYbvwAcg=="],
+
+ "@simplewebauthn/server": ["@simplewebauthn/server@13.1.1", "", { "dependencies": { "@hexagon/base64": "^1.1.27", "@levischuck/tiny-cbor": "^0.2.2", "@peculiar/asn1-android": "^2.3.10", "@peculiar/asn1-ecc": "^2.3.8", "@peculiar/asn1-rsa": "^2.3.8", "@peculiar/asn1-schema": "^2.3.8", "@peculiar/asn1-x509": "^2.3.8" } }, "sha512-1hsLpRHfSuMB9ee2aAdh0Htza/X3f4djhYISrggqGe3xopNjOcePiSDkDDoPzDYaaMCrbqGP1H2TYU7bgL9PmA=="],
+
"@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=="],
@@ -459,6 +489,8 @@
"arraybuffer.prototype.slice": ["arraybuffer.prototype.slice@1.0.4", "", { "dependencies": { "array-buffer-byte-length": "^1.0.1", "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "is-array-buffer": "^3.0.4" } }, "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ=="],
+ "asn1js": ["asn1js@3.0.6", "", { "dependencies": { "pvtsutils": "^1.3.6", "pvutils": "^1.1.3", "tslib": "^2.8.1" } }, "sha512-UOCGPYbl0tv8+006qks/dTgV9ajs97X2p0FAbyS2iyCRrmLSRolDaHdp+v/CLgnzHc3fVB+CwYiUmei7ndFcgA=="],
+
"ast-types-flow": ["ast-types-flow@0.0.8", "", {}, "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ=="],
"async-function": ["async-function@1.0.0", "", {}, "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA=="],
@@ -471,6 +503,10 @@
"balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
+ "better-auth": ["better-auth@1.2.8", "", { "dependencies": { "@better-auth/utils": "0.2.5", "@better-fetch/fetch": "^1.1.18", "@noble/ciphers": "^0.6.0", "@noble/hashes": "^1.6.1", "@simplewebauthn/browser": "^13.0.0", "@simplewebauthn/server": "^13.0.0", "better-call": "^1.0.8", "defu": "^6.1.4", "jose": "^5.9.6", "kysely": "^0.28.1", "nanostores": "^0.11.3", "zod": "^3.24.1" } }, "sha512-y8ry7ZW3/3ZIr82Eo1zUDtMzdoQlFnwNuZ0+b0RxoNZgqmvgTIc/0tCDC7NDJerqSu4UCzer0dvYxBsv3WMIGg=="],
+
+ "better-call": ["better-call@1.0.9", "", { "dependencies": { "@better-fetch/fetch": "^1.1.4", "rou3": "^0.5.1", "set-cookie-parser": "^2.7.1", "uncrypto": "^0.1.3" } }, "sha512-Qfm0gjk0XQz0oI7qvTK1hbqTsBY4xV2hsHAxF8LZfUYl3RaECCIifXuVqtPpZJWvlCCMlQSvkvhhyuApGUba6g=="],
+
"brace-expansion": ["brace-expansion@1.1.11", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA=="],
"braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="],
@@ -533,6 +569,8 @@
"define-properties": ["define-properties@1.2.1", "", { "dependencies": { "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", "object-keys": "^1.1.1" } }, "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg=="],
+ "defu": ["defu@6.1.4", "", {}, "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg=="],
+
"dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="],
"detect-libc": ["detect-libc@2.0.4", "", {}, "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA=="],
@@ -749,6 +787,8 @@
"jiti": ["jiti@2.4.2", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A=="],
+ "jose": ["jose@5.10.0", "", {}, "sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg=="],
+
"js-cookie": ["js-cookie@3.0.5", "", {}, "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw=="],
"js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
@@ -767,6 +807,8 @@
"keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="],
+ "kysely": ["kysely@0.28.2", "", {}, "sha512-4YAVLoF0Sf0UTqlhgQMFU9iQECdah7n+13ANkiuVfRvlK+uI0Etbgd7bVP36dKlG+NXWbhGua8vnGt+sdhvT7A=="],
+
"language-subtag-registry": ["language-subtag-registry@0.3.23", "", {}, "sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ=="],
"language-tags": ["language-tags@1.0.9", "", { "dependencies": { "language-subtag-registry": "^0.3.20" } }, "sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA=="],
@@ -829,6 +871,8 @@
"nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
+ "nanostores": ["nanostores@0.11.4", "", {}, "sha512-k1oiVNN4hDK8NcNERSZLQiMfRzEGtfnvZvdBvey3SQbgn8Dcrk0h1I6vpxApjb10PFUflZrgJ2WEZyJQ+5v7YQ=="],
+
"napi-postinstall": ["napi-postinstall@0.2.4", "", { "bin": { "napi-postinstall": "lib/cli.js" } }, "sha512-ZEzHJwBhZ8qQSbknHqYcdtQVr8zUgGyM/q6h6qAyhtyVMNrSgDhrC4disf03dYW0e+czXyLnZINnCTEkWy0eJg=="],
"natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="],
@@ -911,6 +955,10 @@
"punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
+ "pvtsutils": ["pvtsutils@1.3.6", "", { "dependencies": { "tslib": "^2.8.1" } }, "sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg=="],
+
+ "pvutils": ["pvutils@1.1.3", "", {}, "sha512-pMpnA0qRdFp32b1sJl1wOJNxZLQ2cbQx+k6tjNtZ8CpvVhNqEPRgivZ2WOUev2YMajecdH7ctUPDvEe87nariQ=="],
+
"queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="],
"react": ["react@19.1.0", "", {}, "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg=="],
@@ -937,6 +985,8 @@
"reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="],
+ "rou3": ["rou3@0.5.1", "", {}, "sha512-OXMmJ3zRk2xeXFGfA3K+EOPHC5u7RDFG7lIOx0X1pdnhUkI8MdVrbV+sNsD80ElpUZ+MRHdyxPnFthq9VHs8uQ=="],
+
"run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="],
"safe-array-concat": ["safe-array-concat@1.1.3", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", "get-intrinsic": "^1.2.6", "has-symbols": "^1.1.0", "isarray": "^2.0.5" } }, "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q=="],
@@ -951,6 +1001,8 @@
"server-only": ["server-only@0.0.1", "", {}, "sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA=="],
+ "set-cookie-parser": ["set-cookie-parser@2.7.1", "", {}, "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ=="],
+
"set-function-length": ["set-function-length@1.2.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "function-bind": "^1.1.2", "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", "has-property-descriptors": "^1.0.2" } }, "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg=="],
"set-function-name": ["set-function-name@2.0.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "functions-have-names": "^1.2.3", "has-property-descriptors": "^1.0.2" } }, "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ=="],
@@ -1055,6 +1107,8 @@
"unbox-primitive": ["unbox-primitive@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "has-bigints": "^1.0.2", "has-symbols": "^1.1.0", "which-boxed-primitive": "^1.1.1" } }, "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw=="],
+ "uncrypto": ["uncrypto@0.1.3", "", {}, "sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q=="],
+
"undici-types": ["undici-types@6.19.8", "", {}, "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw=="],
"unrs-resolver": ["unrs-resolver@1.7.8", "", { "dependencies": { "napi-postinstall": "^0.2.2" }, "optionalDependencies": { "@unrs/resolver-binding-darwin-arm64": "1.7.8", "@unrs/resolver-binding-darwin-x64": "1.7.8", "@unrs/resolver-binding-freebsd-x64": "1.7.8", "@unrs/resolver-binding-linux-arm-gnueabihf": "1.7.8", "@unrs/resolver-binding-linux-arm-musleabihf": "1.7.8", "@unrs/resolver-binding-linux-arm64-gnu": "1.7.8", "@unrs/resolver-binding-linux-arm64-musl": "1.7.8", "@unrs/resolver-binding-linux-ppc64-gnu": "1.7.8", "@unrs/resolver-binding-linux-riscv64-gnu": "1.7.8", "@unrs/resolver-binding-linux-riscv64-musl": "1.7.8", "@unrs/resolver-binding-linux-s390x-gnu": "1.7.8", "@unrs/resolver-binding-linux-x64-gnu": "1.7.8", "@unrs/resolver-binding-linux-x64-musl": "1.7.8", "@unrs/resolver-binding-wasm32-wasi": "1.7.8", "@unrs/resolver-binding-win32-arm64-msvc": "1.7.8", "@unrs/resolver-binding-win32-ia32-msvc": "1.7.8", "@unrs/resolver-binding-win32-x64-msvc": "1.7.8" } }, "sha512-2zsXwyOXmCX9nGz4vhtZRYhe30V78heAv+KDc21A/KMdovGHbZcixeD5JHEF0DrFXzdytwuzYclcPbvp8A3Jlw=="],
diff --git a/drizzle.config.ts b/drizzle.config.ts
index 52fc6ff..f23b33e 100644
--- a/drizzle.config.ts
+++ b/drizzle.config.ts
@@ -3,7 +3,10 @@ import { type Config } from "drizzle-kit";
import { env } from "@/env";
export default {
- schema: "./src/server/db/schema.ts",
+ schema: [
+ "./src/server/db/schema.ts",
+ // "./src/server/db/auth-schema.ts"
+ ],
dialect: "postgresql",
dbCredentials: {
url: env.DATABASE_URL,
diff --git a/drizzle/0000_classy_lilandra.sql b/drizzle/0000_classy_lilandra.sql
new file mode 100644
index 0000000..8c7b4a4
--- /dev/null
+++ b/drizzle/0000_classy_lilandra.sql
@@ -0,0 +1,13 @@
+CREATE TABLE "flipside_article" (
+ "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
+ "user_id" text NOT NULL,
+ "url" text NOT NULL,
+ "image_url" text,
+ "title" text NOT NULL,
+ "description" text,
+ "tags" text,
+ "created_at" timestamp DEFAULT now() NOT NULL,
+ "updated_at" timestamp DEFAULT now() NOT NULL
+);
+--> statement-breakpoint
+CREATE INDEX "title_idx" ON "flipside_article" USING btree ("title");
\ No newline at end of file
diff --git a/drizzle/0000_harsh_iron_fist.sql b/drizzle/0000_harsh_iron_fist.sql
deleted file mode 100644
index 2175db4..0000000
--- a/drizzle/0000_harsh_iron_fist.sql
+++ /dev/null
@@ -1,9 +0,0 @@
-CREATE TABLE "flipside_articles" (
- "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
- "user_id" uuid NOT NULL,
- "url" text NOT NULL,
- "title" text,
- "description" text,
- "tags" text,
- "created_at" timestamp DEFAULT now()
-);
diff --git a/drizzle/0001_glamorous_ben_parker.sql b/drizzle/0001_glamorous_ben_parker.sql
deleted file mode 100644
index cd4c48c..0000000
--- a/drizzle/0001_glamorous_ben_parker.sql
+++ /dev/null
@@ -1,2 +0,0 @@
-ALTER TABLE "flipside_articles" RENAME TO "flipside_article";--> statement-breakpoint
-CREATE INDEX "title_idx" ON "flipside_article" USING btree ("title");
\ No newline at end of file
diff --git a/drizzle/0002_magical_nico_minoru.sql b/drizzle/0002_magical_nico_minoru.sql
deleted file mode 100644
index 86d91de..0000000
--- a/drizzle/0002_magical_nico_minoru.sql
+++ /dev/null
@@ -1,5 +0,0 @@
-ALTER TABLE "flipside_article" ALTER COLUMN "user_id" SET DATA TYPE text;--> statement-breakpoint
-ALTER TABLE "flipside_article" ALTER COLUMN "title" SET NOT NULL;--> statement-breakpoint
-ALTER TABLE "flipside_article" ALTER COLUMN "created_at" SET NOT NULL;--> statement-breakpoint
-ALTER TABLE "flipside_article" ADD COLUMN "image_url" text;--> statement-breakpoint
-ALTER TABLE "flipside_article" ADD COLUMN "updated_at" timestamp DEFAULT now() NOT NULL;
\ No newline at end of file
diff --git a/drizzle/0003_perpetual_spectrum.sql b/drizzle/0003_perpetual_spectrum.sql
deleted file mode 100644
index 4e4ddba..0000000
--- a/drizzle/0003_perpetual_spectrum.sql
+++ /dev/null
@@ -1 +0,0 @@
-ALTER TABLE "flipside_article" ALTER COLUMN "title" DROP NOT NULL;
\ No newline at end of file
diff --git a/drizzle/0004_opposite_dark_phoenix.sql b/drizzle/0004_opposite_dark_phoenix.sql
deleted file mode 100644
index 503ec6b..0000000
--- a/drizzle/0004_opposite_dark_phoenix.sql
+++ /dev/null
@@ -1 +0,0 @@
-ALTER TABLE "flipside_article" ALTER COLUMN "title" SET NOT NULL;
\ No newline at end of file
diff --git a/drizzle/meta/0000_snapshot.json b/drizzle/meta/0000_snapshot.json
index 3a0df57..70b9ece 100644
--- a/drizzle/meta/0000_snapshot.json
+++ b/drizzle/meta/0000_snapshot.json
@@ -1,11 +1,11 @@
{
- "id": "d564ae8e-acdb-4f1d-8da9-325cc8bb8a5a",
+ "id": "a4eb6f81-6111-40ae-8ab1-f3b0c4148a2d",
"prevId": "00000000-0000-0000-0000-000000000000",
"version": "7",
"dialect": "postgresql",
"tables": {
- "public.flipside_articles": {
- "name": "flipside_articles",
+ "public.flipside_article": {
+ "name": "flipside_article",
"schema": "",
"columns": {
"id": {
@@ -17,7 +17,7 @@
},
"user_id": {
"name": "user_id",
- "type": "uuid",
+ "type": "text",
"primaryKey": false,
"notNull": true
},
@@ -27,11 +27,17 @@
"primaryKey": false,
"notNull": true
},
+ "image_url": {
+ "name": "image_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
"title": {
"name": "title",
"type": "text",
"primaryKey": false,
- "notNull": false
+ "notNull": true
},
"description": {
"name": "description",
@@ -49,11 +55,34 @@
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
- "notNull": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
"default": "now()"
}
},
- "indexes": {},
+ "indexes": {
+ "title_idx": {
+ "name": "title_idx",
+ "columns": [
+ {
+ "expression": "title",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
diff --git a/drizzle/meta/0001_snapshot.json b/drizzle/meta/0001_snapshot.json
deleted file mode 100644
index af84528..0000000
--- a/drizzle/meta/0001_snapshot.json
+++ /dev/null
@@ -1,92 +0,0 @@
-{
- "id": "9c8a25c5-9684-438a-a61d-1df97a052646",
- "prevId": "d564ae8e-acdb-4f1d-8da9-325cc8bb8a5a",
- "version": "7",
- "dialect": "postgresql",
- "tables": {
- "public.flipside_article": {
- "name": "flipside_article",
- "schema": "",
- "columns": {
- "id": {
- "name": "id",
- "type": "uuid",
- "primaryKey": true,
- "notNull": true,
- "default": "gen_random_uuid()"
- },
- "user_id": {
- "name": "user_id",
- "type": "uuid",
- "primaryKey": false,
- "notNull": true
- },
- "url": {
- "name": "url",
- "type": "text",
- "primaryKey": false,
- "notNull": true
- },
- "title": {
- "name": "title",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "description": {
- "name": "description",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "tags": {
- "name": "tags",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "created_at": {
- "name": "created_at",
- "type": "timestamp",
- "primaryKey": false,
- "notNull": false,
- "default": "now()"
- }
- },
- "indexes": {
- "title_idx": {
- "name": "title_idx",
- "columns": [
- {
- "expression": "title",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": false,
- "concurrently": false,
- "method": "btree",
- "with": {}
- }
- },
- "foreignKeys": {},
- "compositePrimaryKeys": {},
- "uniqueConstraints": {},
- "policies": {},
- "checkConstraints": {},
- "isRLSEnabled": false
- }
- },
- "enums": {},
- "schemas": {},
- "sequences": {},
- "roles": {},
- "policies": {},
- "views": {},
- "_meta": {
- "columns": {},
- "schemas": {},
- "tables": {}
- }
-}
\ No newline at end of file
diff --git a/drizzle/meta/0002_snapshot.json b/drizzle/meta/0002_snapshot.json
deleted file mode 100644
index 41cfc20..0000000
--- a/drizzle/meta/0002_snapshot.json
+++ /dev/null
@@ -1,105 +0,0 @@
-{
- "id": "b097010d-2b82-4f2e-af77-b0728110f64e",
- "prevId": "9c8a25c5-9684-438a-a61d-1df97a052646",
- "version": "7",
- "dialect": "postgresql",
- "tables": {
- "public.flipside_article": {
- "name": "flipside_article",
- "schema": "",
- "columns": {
- "id": {
- "name": "id",
- "type": "uuid",
- "primaryKey": true,
- "notNull": true,
- "default": "gen_random_uuid()"
- },
- "user_id": {
- "name": "user_id",
- "type": "text",
- "primaryKey": false,
- "notNull": true
- },
- "url": {
- "name": "url",
- "type": "text",
- "primaryKey": false,
- "notNull": true
- },
- "image_url": {
- "name": "image_url",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "title": {
- "name": "title",
- "type": "text",
- "primaryKey": false,
- "notNull": true
- },
- "description": {
- "name": "description",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "tags": {
- "name": "tags",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "created_at": {
- "name": "created_at",
- "type": "timestamp",
- "primaryKey": false,
- "notNull": true,
- "default": "now()"
- },
- "updated_at": {
- "name": "updated_at",
- "type": "timestamp",
- "primaryKey": false,
- "notNull": true,
- "default": "now()"
- }
- },
- "indexes": {
- "title_idx": {
- "name": "title_idx",
- "columns": [
- {
- "expression": "title",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": false,
- "concurrently": false,
- "method": "btree",
- "with": {}
- }
- },
- "foreignKeys": {},
- "compositePrimaryKeys": {},
- "uniqueConstraints": {},
- "policies": {},
- "checkConstraints": {},
- "isRLSEnabled": false
- }
- },
- "enums": {},
- "schemas": {},
- "sequences": {},
- "roles": {},
- "policies": {},
- "views": {},
- "_meta": {
- "columns": {},
- "schemas": {},
- "tables": {}
- }
-}
\ No newline at end of file
diff --git a/drizzle/meta/0003_snapshot.json b/drizzle/meta/0003_snapshot.json
deleted file mode 100644
index 45e0073..0000000
--- a/drizzle/meta/0003_snapshot.json
+++ /dev/null
@@ -1,105 +0,0 @@
-{
- "id": "53528728-7967-459d-b52e-07421fc0a36a",
- "prevId": "b097010d-2b82-4f2e-af77-b0728110f64e",
- "version": "7",
- "dialect": "postgresql",
- "tables": {
- "public.flipside_article": {
- "name": "flipside_article",
- "schema": "",
- "columns": {
- "id": {
- "name": "id",
- "type": "uuid",
- "primaryKey": true,
- "notNull": true,
- "default": "gen_random_uuid()"
- },
- "user_id": {
- "name": "user_id",
- "type": "text",
- "primaryKey": false,
- "notNull": true
- },
- "url": {
- "name": "url",
- "type": "text",
- "primaryKey": false,
- "notNull": true
- },
- "image_url": {
- "name": "image_url",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "title": {
- "name": "title",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "description": {
- "name": "description",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "tags": {
- "name": "tags",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "created_at": {
- "name": "created_at",
- "type": "timestamp",
- "primaryKey": false,
- "notNull": true,
- "default": "now()"
- },
- "updated_at": {
- "name": "updated_at",
- "type": "timestamp",
- "primaryKey": false,
- "notNull": true,
- "default": "now()"
- }
- },
- "indexes": {
- "title_idx": {
- "name": "title_idx",
- "columns": [
- {
- "expression": "title",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": false,
- "concurrently": false,
- "method": "btree",
- "with": {}
- }
- },
- "foreignKeys": {},
- "compositePrimaryKeys": {},
- "uniqueConstraints": {},
- "policies": {},
- "checkConstraints": {},
- "isRLSEnabled": false
- }
- },
- "enums": {},
- "schemas": {},
- "sequences": {},
- "roles": {},
- "policies": {},
- "views": {},
- "_meta": {
- "columns": {},
- "schemas": {},
- "tables": {}
- }
-}
\ No newline at end of file
diff --git a/drizzle/meta/0004_snapshot.json b/drizzle/meta/0004_snapshot.json
deleted file mode 100644
index a9d9b3f..0000000
--- a/drizzle/meta/0004_snapshot.json
+++ /dev/null
@@ -1,105 +0,0 @@
-{
- "id": "2aee8837-ca7a-451c-9fa3-02172faedbd0",
- "prevId": "53528728-7967-459d-b52e-07421fc0a36a",
- "version": "7",
- "dialect": "postgresql",
- "tables": {
- "public.flipside_article": {
- "name": "flipside_article",
- "schema": "",
- "columns": {
- "id": {
- "name": "id",
- "type": "uuid",
- "primaryKey": true,
- "notNull": true,
- "default": "gen_random_uuid()"
- },
- "user_id": {
- "name": "user_id",
- "type": "text",
- "primaryKey": false,
- "notNull": true
- },
- "url": {
- "name": "url",
- "type": "text",
- "primaryKey": false,
- "notNull": true
- },
- "image_url": {
- "name": "image_url",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "title": {
- "name": "title",
- "type": "text",
- "primaryKey": false,
- "notNull": true
- },
- "description": {
- "name": "description",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "tags": {
- "name": "tags",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "created_at": {
- "name": "created_at",
- "type": "timestamp",
- "primaryKey": false,
- "notNull": true,
- "default": "now()"
- },
- "updated_at": {
- "name": "updated_at",
- "type": "timestamp",
- "primaryKey": false,
- "notNull": true,
- "default": "now()"
- }
- },
- "indexes": {
- "title_idx": {
- "name": "title_idx",
- "columns": [
- {
- "expression": "title",
- "isExpression": false,
- "asc": true,
- "nulls": "last"
- }
- ],
- "isUnique": false,
- "concurrently": false,
- "method": "btree",
- "with": {}
- }
- },
- "foreignKeys": {},
- "compositePrimaryKeys": {},
- "uniqueConstraints": {},
- "policies": {},
- "checkConstraints": {},
- "isRLSEnabled": false
- }
- },
- "enums": {},
- "schemas": {},
- "sequences": {},
- "roles": {},
- "policies": {},
- "views": {},
- "_meta": {
- "columns": {},
- "schemas": {},
- "tables": {}
- }
-}
\ No newline at end of file
diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json
index 5c011c3..5a39d70 100644
--- a/drizzle/meta/_journal.json
+++ b/drizzle/meta/_journal.json
@@ -5,36 +5,8 @@
{
"idx": 0,
"version": "7",
- "when": 1748671340868,
- "tag": "0000_harsh_iron_fist",
- "breakpoints": true
- },
- {
- "idx": 1,
- "version": "7",
- "when": 1748671600706,
- "tag": "0001_glamorous_ben_parker",
- "breakpoints": true
- },
- {
- "idx": 2,
- "version": "7",
- "when": 1749018745913,
- "tag": "0002_magical_nico_minoru",
- "breakpoints": true
- },
- {
- "idx": 3,
- "version": "7",
- "when": 1749021195475,
- "tag": "0003_perpetual_spectrum",
- "breakpoints": true
- },
- {
- "idx": 4,
- "version": "7",
- "when": 1749021231634,
- "tag": "0004_opposite_dark_phoenix",
+ "when": 1749113694834,
+ "tag": "0000_classy_lilandra",
"breakpoints": true
}
]
diff --git a/package.json b/package.json
index 467c8cd..3e6fbbd 100644
--- a/package.json
+++ b/package.json
@@ -34,6 +34,7 @@
"@trpc/client": "^11.0.0",
"@trpc/react-query": "^11.0.0",
"@trpc/server": "^11.0.0",
+ "better-auth": "^1.2.8",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"drizzle-orm": "^0.44.1",
diff --git a/src/app/(auth)/sign-in/page.tsx b/src/app/(auth)/sign-in/page.tsx
index 26f4587..fbd19ab 100644
--- a/src/app/(auth)/sign-in/page.tsx
+++ b/src/app/(auth)/sign-in/page.tsx
@@ -1,10 +1,5 @@
-import { SignIn as ClerkSignInUI } from "@clerk/nextjs";
import React from "react";
export default function SignInPage() {
- return (
-
-
-
- );
+ return ;
}
diff --git a/src/app/(auth)/sign-up/page.tsx b/src/app/(auth)/sign-up/page.tsx
index 568a737..fbd19ab 100644
--- a/src/app/(auth)/sign-up/page.tsx
+++ b/src/app/(auth)/sign-up/page.tsx
@@ -1,10 +1,5 @@
-import { SignUp as ClerkSignUpUI } from "@clerk/nextjs";
import React from "react";
export default function SignInPage() {
- return (
-
-
-
- );
+ return ;
}
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/articles/articles-page.tsx b/src/app/articles/articles-page.tsx
new file mode 100644
index 0000000..7e92cb3
--- /dev/null
+++ b/src/app/articles/articles-page.tsx
@@ -0,0 +1,169 @@
+"use client";
+
+import { useState, useMemo } from "react";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import { Plus, Search } from "lucide-react";
+import { ArticleList } from "@/components/article-list";
+import { NewArticleModal } from "@/components/modals/new-article-modal";
+import { EditArticleModal } from "@/components/modals/edit-article-modal";
+import type { Article } from "@/lib/types";
+import { api } from "@/trpc/react";
+
+export default function ArticlesPage() {
+ const {
+ data: articles,
+ isLoading,
+ isError,
+ error,
+ } = api.articles.getAll.useQuery();
+ const [searchQuery, setSearchQuery] = useState("");
+ const [isNewArticleModalOpen, setIsNewArticleModalOpen] = useState(false);
+ const [isEditArticleModalOpen, setIsEditArticleModalOpen] = useState(false);
+ const [editingArticle, setEditingArticle] = useState(null);
+ const utils = api.useUtils();
+
+ const deleteArticle = api.articles.delete.useMutation({
+ onSuccess: () => {
+ // Invalidate and refetch articles query
+ utils.articles.getAll.invalidate().catch((error) => {
+ console.error("Failed to invalidate cache:", error);
+ });
+ console.log("Article deleted successfully!");
+ },
+ onError: (error) => {
+ console.log(`Failed to delete article: ${error.message}`);
+ },
+ });
+
+ const filteredArticles = useMemo((): Article[] => {
+ if (!articles) return [];
+ return articles.filter((article) => {
+ const matchesSearch = article.title
+ .toLowerCase()
+ .includes(searchQuery.toLowerCase());
+
+ return matchesSearch;
+ });
+ }, [articles, searchQuery]);
+
+ const handleNewArticle = () => {
+ // Refresh articles list - in real app, this would refetch from API
+ console.log("Refreshing articles list...");
+ };
+
+ const handleEditArticle = (article: Article) => {
+ setEditingArticle(article);
+ setIsEditArticleModalOpen(true);
+ };
+
+ const handleUpdateArticle = () => {
+ // Refresh articles list - in real app, this would refetch from API
+ console.log("Refreshing articles list after update...");
+ };
+
+ const handleDeleteArticle = async (id: string) => {
+ try {
+ deleteArticle.mutateAsync({ id: id });
+ } catch (error) {
+ console.log("Failed to delete article", error);
+ }
+ };
+
+ if (isLoading) {
+ return (
+
+ );
+ }
+
+ if (isError) {
+ return (
+
+
Error loading articles: {error?.message}
+
+
+ );
+ }
+
+ if (!articles) {
+ return Something went wrong
;
+ }
+
+ return (
+
+ {/* Header */}
+
+
+
My Articles
+
+ {articles.length} article{articles.length !== 1 ? "s" : ""} saved
+
+
+
+
+
+ {/* Search Bar */}
+
+
+ setSearchQuery(e.target.value)}
+ className="pl-10"
+ />
+
+
+ {/* Articles List or Empty State */}
+ {articles.length === 0 ? (
+
+
📚
+
+ No articles yet
+
+
+ Add your first article to get started!
+
+
+
+ ) : (
+
+ )}
+
+ {/* Modals */}
+
setIsNewArticleModalOpen(false)}
+ onSave={handleNewArticle}
+ />
+
+ {
+ setIsEditArticleModalOpen(false);
+ setEditingArticle(null);
+ }}
+ onSave={handleUpdateArticle}
+ />
+
+ );
+}
diff --git a/src/app/articles/page.tsx b/src/app/articles/page.tsx
index 7e92cb3..02fae81 100644
--- a/src/app/articles/page.tsx
+++ b/src/app/articles/page.tsx
@@ -1,169 +1,42 @@
"use client";
-import { useState, useMemo } from "react";
+import { LoadingSpinner } from "@/components/loading-spinner";
import { Button } from "@/components/ui/button";
-import { Input } from "@/components/ui/input";
-import { Plus, Search } from "lucide-react";
-import { ArticleList } from "@/components/article-list";
-import { NewArticleModal } from "@/components/modals/new-article-modal";
-import { EditArticleModal } from "@/components/modals/edit-article-modal";
-import type { Article } from "@/lib/types";
-import { api } from "@/trpc/react";
+import { authClient } from "@/lib/auth-client";
+import React from "react";
+import ArticlesPage from "./articles-page";
-export default function ArticlesPage() {
+export default function page() {
const {
- data: articles,
- isLoading,
- isError,
- error,
- } = api.articles.getAll.useQuery();
- const [searchQuery, setSearchQuery] = useState("");
- const [isNewArticleModalOpen, setIsNewArticleModalOpen] = useState(false);
- const [isEditArticleModalOpen, setIsEditArticleModalOpen] = useState(false);
- const [editingArticle, setEditingArticle] = useState(null);
- const utils = api.useUtils();
-
- const deleteArticle = api.articles.delete.useMutation({
- onSuccess: () => {
- // Invalidate and refetch articles query
- utils.articles.getAll.invalidate().catch((error) => {
- console.error("Failed to invalidate cache:", error);
- });
- console.log("Article deleted successfully!");
- },
- onError: (error) => {
- console.log(`Failed to delete article: ${error.message}`);
- },
- });
-
- const filteredArticles = useMemo((): Article[] => {
- if (!articles) return [];
- return articles.filter((article) => {
- const matchesSearch = article.title
- .toLowerCase()
- .includes(searchQuery.toLowerCase());
-
- return matchesSearch;
- });
- }, [articles, searchQuery]);
-
- const handleNewArticle = () => {
- // Refresh articles list - in real app, this would refetch from API
- console.log("Refreshing articles list...");
- };
-
- const handleEditArticle = (article: Article) => {
- setEditingArticle(article);
- setIsEditArticleModalOpen(true);
- };
-
- const handleUpdateArticle = () => {
- // Refresh articles list - in real app, this would refetch from API
- console.log("Refreshing articles list after update...");
- };
-
- const handleDeleteArticle = async (id: string) => {
- try {
- deleteArticle.mutateAsync({ id: id });
- } catch (error) {
- console.log("Failed to delete article", error);
- }
- };
-
- if (isLoading) {
- return (
-
- );
+ data: session,
+ isPending, //loading state
+ error, //error object
+ refetch, //refetch the session
+ } = authClient.useSession();
+
+ if (isPending) {
+ return ;
}
- if (isError) {
+ if (error) {
return (
Error loading articles: {error?.message}
-
);
}
- if (!articles) {
- return Something went wrong
;
- }
-
- return (
-
- {/* Header */}
-
-
-
My Articles
-
- {articles.length} article{articles.length !== 1 ? "s" : ""} saved
-
-
-
setIsNewArticleModalOpen(true)}>
-
- Add New Article
-
-
-
- {/* Search Bar */}
-
-
-
setSearchQuery(e.target.value)}
- className="pl-10"
- />
+ if (!session) {
+ return (
+
+ Something went wrong
+ {/* Should be buttons to sign in or go home */}
+ );
+ }
- {/* Articles List or Empty State */}
- {articles.length === 0 ? (
-
-
📚
-
- No articles yet
-
-
- Add your first article to get started!
-
-
setIsNewArticleModalOpen(true)} size="lg">
-
- Add New Article
-
-
- ) : (
-
- )}
-
- {/* Modals */}
-
setIsNewArticleModalOpen(false)}
- onSave={handleNewArticle}
- />
-
- {
- setIsEditArticleModalOpen(false);
- setEditingArticle(null);
- }}
- onSave={handleUpdateArticle}
- />
-
- );
+ return
;
}
diff --git a/src/app/layout.tsx b/src/app/layout.tsx
index d813161..0ea4d99 100644
--- a/src/app/layout.tsx
+++ b/src/app/layout.tsx
@@ -4,7 +4,6 @@ import { Inter } from "next/font/google";
import "@/styles/globals.css";
import { Navbar } from "@/components/navbar";
import { Footer } from "@/components/footer";
-import { ClerkProvider } from "@clerk/nextjs";
import { TRPCReactProvider } from "@/trpc/react";
const inter = Inter({ subsets: ["latin"] });
@@ -21,18 +20,16 @@ export default function RootLayout({
children: React.ReactNode;
}) {
return (
-
-
-
-
-
-
- {children}
-
-
-
-
-
-
+
+
+
+
+
+ {children}
+
+
+
+
+
);
}
diff --git a/src/components/modals/edit-article-modal.tsx b/src/components/modals/edit-article-modal.tsx
index 2d92930..94a20c3 100644
--- a/src/components/modals/edit-article-modal.tsx
+++ b/src/components/modals/edit-article-modal.tsx
@@ -65,7 +65,7 @@ export function EditArticleModal({
await updateArticle.mutateAsync({
id: article.id,
title: title,
- tags: tags || article.tags,
+ tags: tags || article.tags || "",
});
onSave();
diff --git a/src/components/navbar.tsx b/src/components/navbar.tsx
index 116328e..6fe1ece 100644
--- a/src/components/navbar.tsx
+++ b/src/components/navbar.tsx
@@ -11,29 +11,21 @@ import {
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
-
-// Mock user state - in real app, this would come from Clerk
-const mockUser = {
- id: "1",
- name: "John Doe",
- email: "john@example.com",
- imageUrl: "/placeholder.svg?height=32&width=32",
-};
+import { authClient } from "@/lib/auth-client";
export function Navbar() {
const [isMenuOpen, setIsMenuOpen] = useState(false);
- const [isSignedIn, setIsSignedIn] = useState(true); // Mock auth state
+ const user = authClient.useSession().data?.user;
- const handleSignOut = () => {
- console.log("Signing out...");
- setIsSignedIn(false);
- // In real app: clerk.signOut()
+ const handleSignOut = async () => {
+ await authClient.signOut();
};
- const handleSignIn = () => {
- console.log("Signing in...");
- setIsSignedIn(true);
- // In real app: clerk.openSignIn()
+ const handleSignIn = async () => {
+ await authClient.signIn.social({
+ provider: "google",
+ callbackURL: "/articles",
+ });
};
return (
@@ -42,7 +34,7 @@ export function Navbar() {
{/* Logo */}
@@ -51,7 +43,7 @@ export function Navbar() {
{/* Desktop Navigation */}
- {isSignedIn && (
+ {user && (
<>
- {isSignedIn ? (
+ {user ? (
@@ -91,9 +83,9 @@ export function Navbar() {
- {mockUser.name}
+ {user.name}
- {mockUser.email}
+ {user.email}
@@ -127,7 +119,7 @@ export function Navbar() {
{isMenuOpen && (
- {isSignedIn ? (
+ {user ? (
<>
-
{mockUser.name}
+
{user.name}
- {mockUser.email}
+ {user.email}
void;
-}
-
-export default function ArticleList({
- articles,
- handleDeleteArticle,
-}: ArticleListProps) {
- if (!articles) {
- return You do not have any saved articles
;
- }
- return (
-
- {articles.map((article: Article) => (
-
-
-
- {article.title}
- {article.description && ` : ${article.description}`}
- handleDeleteArticle(article.id)}>
- Delete
-
-
-
-
- ))}
-
- );
-}
diff --git a/src/components/old:HomePage.tsx b/src/components/old:HomePage.tsx
deleted file mode 100644
index 811be92..0000000
--- a/src/components/old:HomePage.tsx
+++ /dev/null
@@ -1,167 +0,0 @@
-"use client";
-import { api } from "@/trpc/react";
-import React, { useState } from "react";
-import ArticleList from "./ArticleList";
-import { Button } from "./ui/button";
-import {
- Dialog,
- DialogContent,
- DialogFooter,
- DialogHeader,
- DialogTitle,
- DialogTrigger,
-} from "@/components/ui/dialog";
-import { Input } from "@/components/ui/input";
-import { DialogClose } from "@radix-ui/react-dialog";
-import { z } from "zod";
-
-export default function HomePage() {
- const utils = api.useUtils();
- const [url, setUrl] = useState("");
- const [open, setOpen] = useState(false);
-
- const {
- data: articles,
- isLoading,
- isError,
- error,
- } = api.articles.getAll.useQuery();
-
- const createArticle = api.articles.create.useMutation({
- onSuccess: () => {
- // Invalidate and refetch articles query
- utils.articles.getAll.invalidate().catch((error) => {
- console.error("Failed to invalidate cache:", error);
- });
- console.log("Article created successfully!");
- },
- onError: (error) => {
- console.log(`Failed to create article: ${error.message}`);
- },
- });
-
- const deleteArticle = api.articles.delete.useMutation({
- onSuccess: () => {
- // Invalidate and refetch articles query
- utils.articles.getAll.invalidate().catch((error) => {
- console.error("Failed to invalidate cache:", error);
- });
- console.log("Article deleted successfully!");
- },
- onError: (error) => {
- console.log(`Failed to delete article: ${error.message}`);
- },
- });
-
- const handleSubmit = async () => {
- const parseResult = z.string().url().safeParse(url);
-
- if (!parseResult.success) {
- alert("Please enter a valid url");
- return;
- }
-
- try {
- await createArticle.mutateAsync({ url: url });
- setUrl("");
- setOpen(false);
- } catch (error) {
- // Error is already handled in onError callback
- console.error("Failed to create article:", error);
- }
- };
-
- const handleDeleteArticle = async (articleId: string) => {
- try {
- await deleteArticle.mutateAsync({
- id: articleId,
- });
- } catch (error) {
- // Error is already handled in onError callback
- console.error("Failed to delete article:", error);
- }
- };
-
- if (isLoading) {
- return (
-
- );
- }
-
- if (isError) {
- return (
-
-
Error loading articles: {error?.message}
-
utils.articles.getAll.invalidate()}
- variant="outline"
- className="mt-2"
- >
- Retry
-
-
- );
- }
-
- return (
-
-
-
Articles
-
Manage your articles
-
-
- {deleteArticle.isPending && (
-
- )}
-
-
-
- {articles && (
-
- )}
-
- );
-}
diff --git a/src/components/old:LandingPage.tsx b/src/components/old:LandingPage.tsx
deleted file mode 100644
index ea886fd..0000000
--- a/src/components/old:LandingPage.tsx
+++ /dev/null
@@ -1,14 +0,0 @@
-import React from "react";
-import { Button } from "./ui/button";
-import Link from "next/link";
-
-export default function LandingPage() {
- return (
-
-
Sign in to start using Flipside
-
- Sign In
-
-
- );
-}
diff --git a/src/lib/auth-client.ts b/src/lib/auth-client.ts
new file mode 100644
index 0000000..d1775da
--- /dev/null
+++ b/src/lib/auth-client.ts
@@ -0,0 +1,5 @@
+import { createAuthClient } from "better-auth/react";
+export const authClient = createAuthClient({
+ /** The base URL of the server (optional if you're using the same domain) */
+ // baseURL: "http://localhost:3000"
+});
diff --git a/src/lib/auth.ts b/src/lib/auth.ts
new file mode 100644
index 0000000..c3432b8
--- /dev/null
+++ b/src/lib/auth.ts
@@ -0,0 +1,22 @@
+import { betterAuth } from "better-auth";
+import { drizzleAdapter } from "better-auth/adapters/drizzle";
+import { db } from "@/server/db"; // your drizzle instance
+import { account, session, user, verification } from "@/server/db/auth-schema";
+
+export const auth = betterAuth({
+ database: drizzleAdapter(db, {
+ provider: "pg", // or "mysql", "sqlite"
+ schema: {
+ verification: verification,
+ account: account,
+ session: session,
+ user: user,
+ },
+ }),
+ socialProviders: {
+ google: {
+ clientId: process.env.GOOGLE_CLIENT_ID as string,
+ clientSecret: process.env.GOOGLE_CLIENT_SECRET as string,
+ },
+ },
+});
diff --git a/src/middleware.ts b/src/middleware.ts
deleted file mode 100644
index 21b699f..0000000
--- a/src/middleware.ts
+++ /dev/null
@@ -1,12 +0,0 @@
-import { clerkMiddleware } from "@clerk/nextjs/server";
-
-export default clerkMiddleware();
-
-export const config = {
- matcher: [
- // Skip Next.js internals and all static files, unless found in search params
- "/((?!_next|[^?]*\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)",
- // Always run for API routes
- "/(api|trpc)(.*)",
- ],
-};
diff --git a/src/server/api/routers/articles.ts b/src/server/api/routers/articles.ts
index 592ddba..8422c78 100644
--- a/src/server/api/routers/articles.ts
+++ b/src/server/api/routers/articles.ts
@@ -2,9 +2,10 @@ import { z } from "zod";
import { createTRPCRouter, publicProcedure } from "@/server/api/trpc";
import { articles } from "@/server/db/schema";
import { eq, and, desc } from "drizzle-orm";
-import { auth } from "@clerk/nextjs/server";
import { TRPCError } from "@trpc/server";
import { getMetadata } from "@/lib/utils";
+import { auth } from "@/lib/auth";
+import { headers } from "next/headers";
export const articlesRouter = createTRPCRouter({
// POST equivalent - save URL
@@ -19,8 +20,11 @@ export const articlesRouter = createTRPCRouter({
)
.mutation(async ({ ctx, input }) => {
// Manual auth check
- const { userId } = await auth();
- if (!userId) throw new TRPCError({ code: "UNAUTHORIZED" });
+ const session = await auth.api.getSession({
+ headers: await headers(), // you need to pass the headers object.
+ });
+ if (!session?.session.userId)
+ throw new TRPCError({ code: "UNAUTHORIZED" });
const metadata = await getMetadata(input.url);
@@ -28,7 +32,7 @@ export const articlesRouter = createTRPCRouter({
const article = await ctx.db
.insert(articles)
.values({
- userId: userId,
+ userId: session?.session.userId,
url: input.url,
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
title: input.title || metadata.title || input.url,
@@ -43,13 +47,16 @@ export const articlesRouter = createTRPCRouter({
// GET equivalent - fetch user articles
getAll: publicProcedure.query(async ({ ctx }) => {
// Manual auth check
- const { userId } = await auth();
- if (!userId) throw new TRPCError({ code: "UNAUTHORIZED" });
+ const session = await auth.api.getSession({
+ headers: await headers(), // you need to pass the headers object.
+ });
+
+ if (!session?.session.userId) throw new TRPCError({ code: "UNAUTHORIZED" });
return ctx.db
.select()
.from(articles)
- .where(eq(articles.userId, userId))
+ .where(eq(articles.userId, session?.session.userId))
.orderBy(desc(articles.createdAt));
}),
@@ -58,12 +65,20 @@ export const articlesRouter = createTRPCRouter({
.input(z.object({ id: z.string().uuid() }))
.mutation(async ({ ctx, input }) => {
// Manual auth check
- const { userId } = await auth();
- if (!userId) throw new TRPCError({ code: "UNAUTHORIZED" });
+ const session = await auth.api.getSession({
+ headers: await headers(), // you need to pass the headers object.
+ });
+ if (!session?.session.userId)
+ throw new TRPCError({ code: "UNAUTHORIZED" });
return ctx.db
.delete(articles)
- .where(and(eq(articles.id, input.id), eq(articles.userId, userId)));
+ .where(
+ and(
+ eq(articles.id, input.id),
+ eq(articles.userId, session?.session.userId),
+ ),
+ );
}),
// PUT equivalent - update tags
@@ -77,8 +92,11 @@ export const articlesRouter = createTRPCRouter({
)
.mutation(async ({ ctx, input }) => {
// Manual auth check
- const { userId } = await auth();
- if (!userId) throw new TRPCError({ code: "UNAUTHORIZED" });
+ const session = await auth.api.getSession({
+ headers: await headers(), // you need to pass the headers object.
+ });
+ if (!session?.session.userId)
+ throw new TRPCError({ code: "UNAUTHORIZED" });
return ctx.db
.update(articles)
@@ -86,6 +104,11 @@ export const articlesRouter = createTRPCRouter({
title: input.title,
tags: input.tags,
})
- .where(and(eq(articles.id, input.id), eq(articles.userId, userId)));
+ .where(
+ and(
+ eq(articles.id, input.id),
+ eq(articles.userId, session?.session.userId),
+ ),
+ );
}),
});
diff --git a/src/server/db/auth-schema.ts b/src/server/db/auth-schema.ts
new file mode 100644
index 0000000..3acdc54
--- /dev/null
+++ b/src/server/db/auth-schema.ts
@@ -0,0 +1,47 @@
+import { pgTable, text, timestamp, boolean, integer } from "drizzle-orm/pg-core";
+
+export const user = pgTable("user", {
+ id: text('id').primaryKey(),
+ name: text('name').notNull(),
+ email: text('email').notNull().unique(),
+ emailVerified: boolean('email_verified').$defaultFn(() => !1).notNull(),
+ image: text('image'),
+ createdAt: timestamp('created_at').$defaultFn(() => new Date).notNull(),
+ updatedAt: timestamp('updated_at').$defaultFn(() => new Date).notNull()
+ });
+
+export const session = pgTable("session", {
+ id: text('id').primaryKey(),
+ expiresAt: timestamp('expires_at').notNull(),
+ token: text('token').notNull().unique(),
+ createdAt: timestamp('created_at').notNull(),
+ updatedAt: timestamp('updated_at').notNull(),
+ ipAddress: text('ip_address'),
+ userAgent: text('user_agent'),
+ userId: text('user_id').notNull().references(()=> user.id, { onDelete: 'cascade' })
+ });
+
+export const account = pgTable("account", {
+ id: text('id').primaryKey(),
+ accountId: text('account_id').notNull(),
+ providerId: text('provider_id').notNull(),
+ userId: text('user_id').notNull().references(()=> user.id, { onDelete: 'cascade' }),
+ accessToken: text('access_token'),
+ refreshToken: text('refresh_token'),
+ idToken: text('id_token'),
+ accessTokenExpiresAt: timestamp('access_token_expires_at'),
+ refreshTokenExpiresAt: timestamp('refresh_token_expires_at'),
+ scope: text('scope'),
+ password: text('password'),
+ createdAt: timestamp('created_at').notNull(),
+ updatedAt: timestamp('updated_at').notNull()
+ });
+
+export const verification = pgTable("verification", {
+ id: text('id').primaryKey(),
+ identifier: text('identifier').notNull(),
+ value: text('value').notNull(),
+ expiresAt: timestamp('expires_at').notNull(),
+ createdAt: timestamp('created_at').$defaultFn(() => new Date),
+ updatedAt: timestamp('updated_at').$defaultFn(() => new Date)
+ });
diff --git a/src/server/db/schema.ts b/src/server/db/schema.ts
index 5cb14e8..623499f 100644
--- a/src/server/db/schema.ts
+++ b/src/server/db/schema.ts
@@ -21,7 +21,7 @@ export const articles = createTable(
"article",
() => ({
id: uuid("id").defaultRandom().primaryKey(),
- userId: text("user_id").notNull(), // Clerk user ID
+ userId: text("user_id").notNull(),
url: text("url").notNull(),
imageUrl: text("image_url"),
title: text("title").notNull(),