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 ( +
+

Loading articles...

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

Loading articles...

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

-
- -
- - {/* 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! -

- -
- ) : ( - - )} - - {/* 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 ? ( -
- -
- ))} -
- ); -} 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 ( -
-

Loading articles...

-
- ); - } - - if (isError) { - return ( -
-

Error loading articles: {error?.message}

- -
- ); - } - - return ( -
-
-

Articles

-

Manage your articles

-
- - {deleteArticle.isPending && ( - - - Deleting... - - - )} - - -
- - - - - - Add a URL - -
-
- setUrl(e.target.value)} - placeholder="e.g. https://flipside.akashamba.me" - /> -
-
- - - - - - -
-
-
- - {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

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