backup
This commit is contained in:
parent
5a01897a44
commit
62b6a26a4f
250
backend/package-lock.json
generated
250
backend/package-lock.json
generated
@ -9,15 +9,24 @@
|
||||
"version": "0.0.1",
|
||||
"license": "UNLICENSED",
|
||||
"dependencies": {
|
||||
"@nestjs/cache-manager": "^3.0.1",
|
||||
"@nestjs/common": "^11.0.1",
|
||||
"@nestjs/config": "^4.0.2",
|
||||
"@nestjs/core": "^11.0.1",
|
||||
"@nestjs/jwt": "^11.0.0",
|
||||
"@nestjs/platform-express": "^11.0.1",
|
||||
"@nestjs/schedule": "^5.0.1",
|
||||
"@prisma/client": "^6.6.0",
|
||||
"@types/bcrypt": "^5.0.2",
|
||||
"@types/passport-jwt": "^4.0.1",
|
||||
"bcrypt": "^5.1.1",
|
||||
"bullmq": "^5.47.2",
|
||||
"cache-manager": "^6.4.2",
|
||||
"cache-manager-redis-store": "^3.0.1",
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "^0.14.1",
|
||||
"express-rate-limit": "^7.5.0",
|
||||
"helmet": "^8.1.0",
|
||||
"passport-jwt": "^4.0.1",
|
||||
"prisma": "^6.6.0",
|
||||
"redis": "^4.7.0",
|
||||
@ -2331,6 +2340,39 @@
|
||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||
}
|
||||
},
|
||||
"node_modules/@keyv/serialize": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@keyv/serialize/-/serialize-1.0.3.tgz",
|
||||
"integrity": "sha512-qnEovoOp5Np2JDGonIDL6Ayihw0RhnRh6vxPuHo4RDn1UOzwEo4AeIfpL6UGIrsceWrCMiVPgwRjbHu4vYFc3g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"buffer": "^6.0.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@keyv/serialize/node_modules/buffer": {
|
||||
"version": "6.0.3",
|
||||
"resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
|
||||
"integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"base64-js": "^1.3.1",
|
||||
"ieee754": "^1.2.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@lukeed/csprng": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@lukeed/csprng/-/csprng-1.1.0.tgz",
|
||||
@ -2767,6 +2809,19 @@
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@nestjs/cache-manager": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@nestjs/cache-manager/-/cache-manager-3.0.1.tgz",
|
||||
"integrity": "sha512-4UxTnR0fsmKL5YDalU2eLFVnL+OBebWUpX+hEduKGncrVKH4PPNoiRn1kXyOCjmzb0UvWgqubpssNouc8e0MCw==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@nestjs/common": "^9.0.0 || ^10.0.0 || ^11.0.0",
|
||||
"@nestjs/core": "^9.0.0 || ^10.0.0 || ^11.0.0",
|
||||
"cache-manager": ">=6",
|
||||
"keyv": ">=5",
|
||||
"rxjs": "^7.8.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@nestjs/cli": {
|
||||
"version": "11.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@nestjs/cli/-/cli-11.0.6.tgz",
|
||||
@ -3025,6 +3080,21 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@nestjs/config": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@nestjs/config/-/config-4.0.2.tgz",
|
||||
"integrity": "sha512-McMW6EXtpc8+CwTUwFdg6h7dYcBUpH5iUILCclAsa+MbCEvC9ZKu4dCHRlJqALuhjLw97pbQu62l4+wRwGeZqA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"dotenv": "16.4.7",
|
||||
"dotenv-expand": "12.0.1",
|
||||
"lodash": "4.17.21"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@nestjs/common": "^10.0.0 || ^11.0.0",
|
||||
"rxjs": "^7.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@nestjs/core": {
|
||||
"version": "11.0.13",
|
||||
"resolved": "https://registry.npmjs.org/@nestjs/core/-/core-11.0.13.tgz",
|
||||
@ -3100,6 +3170,19 @@
|
||||
"@nestjs/core": "^11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@nestjs/schedule": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@nestjs/schedule/-/schedule-5.0.1.tgz",
|
||||
"integrity": "sha512-kFoel84I4RyS2LNPH6yHYTKxB16tb3auAEciFuc788C3ph6nABkUfzX5IE+unjVaRX+3GuruJwurNepMlHXpQg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cron": "3.5.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@nestjs/common": "^10.0.0 || ^11.0.0",
|
||||
"@nestjs/core": "^10.0.0 || ^11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@nestjs/schematics": {
|
||||
"version": "11.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@nestjs/schematics/-/schematics-11.0.4.tgz",
|
||||
@ -3955,6 +4038,12 @@
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/luxon": {
|
||||
"version": "3.4.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.4.2.tgz",
|
||||
"integrity": "sha512-TifLZlFudklWlMBfhubvgqTXRzLDI5pCbGa4P8a3wPyUQSW+1xQ5eDsreP9DWHX3tjq1ke96uYG/nwundroWcA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/methods": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz",
|
||||
@ -4070,6 +4159,12 @@
|
||||
"@types/superagent": "^8.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/validator": {
|
||||
"version": "13.12.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.12.3.tgz",
|
||||
"integrity": "sha512-2ipwZ2NydGQJImne+FhNdhgRM37e9lCev99KnqkbFHd94Xn/mErARWI1RSLem1QA19ch5kOhzIZd7e8CA2FI8g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/yargs": {
|
||||
"version": "17.0.33",
|
||||
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz",
|
||||
@ -5108,7 +5203,6 @@
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
|
||||
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
@ -5382,6 +5476,27 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/cache-manager": {
|
||||
"version": "6.4.2",
|
||||
"resolved": "https://registry.npmjs.org/cache-manager/-/cache-manager-6.4.2.tgz",
|
||||
"integrity": "sha512-oT0d1cGWZAlqEGDPjOfhmldTS767jT6kBT3KIdn7MX5OevlRVYqJT+LxRv5WY4xW9heJtYxeRRXaoKlEW+Biew==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"keyv": "^5.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/cache-manager-redis-store": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/cache-manager-redis-store/-/cache-manager-redis-store-3.0.1.tgz",
|
||||
"integrity": "sha512-o560kw+dFqusC9lQJhcm6L2F2fMKobJ5af+FoR2PdnMVdpQ3f3Bz6qzvObTGyvoazQJxjQNWgMQeChP4vRTuXQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"redis": "^4.3.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 16.18.0"
|
||||
}
|
||||
},
|
||||
"node_modules/cacheable-lookup": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-7.0.0.tgz",
|
||||
@ -5411,6 +5526,16 @@
|
||||
"node": ">=14.16"
|
||||
}
|
||||
},
|
||||
"node_modules/cacheable-request/node_modules/keyv": {
|
||||
"version": "4.5.4",
|
||||
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
|
||||
"integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"json-buffer": "3.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/call-bind-apply-helpers": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
|
||||
@ -5573,6 +5698,23 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/class-transformer": {
|
||||
"version": "0.5.1",
|
||||
"resolved": "https://registry.npmjs.org/class-transformer/-/class-transformer-0.5.1.tgz",
|
||||
"integrity": "sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/class-validator": {
|
||||
"version": "0.14.1",
|
||||
"resolved": "https://registry.npmjs.org/class-validator/-/class-validator-0.14.1.tgz",
|
||||
"integrity": "sha512-2VEG9JICxIqTpoK1eMzZqaV+u/EiwEJkMGzTrZf6sU/fwsnOITVgYJ8yojSy6CaXtO9V0Cc6ZQZ8h8m4UBuLwQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/validator": "^13.11.8",
|
||||
"libphonenumber-js": "^1.10.53",
|
||||
"validator": "^13.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/cli-cursor": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz",
|
||||
@ -5962,6 +6104,16 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/cron": {
|
||||
"version": "3.5.0",
|
||||
"resolved": "https://registry.npmjs.org/cron/-/cron-3.5.0.tgz",
|
||||
"integrity": "sha512-0eYZqCnapmxYcV06uktql93wNWdlTmmBFP2iYz+JPVcQqlyFYcn1lFuIk4R54pkOmE7mcldTAPZv6X5XA4Q46A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/luxon": "~3.4.0",
|
||||
"luxon": "~3.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/cron-parser": {
|
||||
"version": "4.9.0",
|
||||
"resolved": "https://registry.npmjs.org/cron-parser/-/cron-parser-4.9.0.tgz",
|
||||
@ -5974,6 +6126,15 @@
|
||||
"node": ">=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/cron/node_modules/luxon": {
|
||||
"version": "3.5.0",
|
||||
"resolved": "https://registry.npmjs.org/luxon/-/luxon-3.5.0.tgz",
|
||||
"integrity": "sha512-rh+Zjr6DNfUYR3bPwJEnuwDdqMbxZW7LOQfUN4B54+Cl+0o5zaU9RJ6bcidfDtC1cWCZXQ+nvX8bf6bAji37QQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/cross-spawn": {
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||
@ -6174,6 +6335,33 @@
|
||||
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/dotenv": {
|
||||
"version": "16.4.7",
|
||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz",
|
||||
"integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==",
|
||||
"license": "BSD-2-Clause",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://dotenvx.com"
|
||||
}
|
||||
},
|
||||
"node_modules/dotenv-expand": {
|
||||
"version": "12.0.1",
|
||||
"resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-12.0.1.tgz",
|
||||
"integrity": "sha512-LaKRbou8gt0RNID/9RoI+J2rvXsBRPMV7p+ElHlPhcSARbCPDYcYG2s1TIzAfWv4YSgyY5taidWzzs31lNV3yQ==",
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"dotenv": "^16.4.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://dotenvx.com"
|
||||
}
|
||||
},
|
||||
"node_modules/dunder-proto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||
@ -6759,6 +6947,21 @@
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/express-rate-limit": {
|
||||
"version": "7.5.0",
|
||||
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.0.tgz",
|
||||
"integrity": "sha512-eB5zbQh5h+VenMPM3fh+nw1YExi5nMr6HUCR62ELSP11huvxm/Uir1H1QEyTkk5QX6A58pX6NmaTMceKZ0Eodg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/express-rate-limit"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"express": "^4.11 || 5 || ^5.0.0-beta.1"
|
||||
}
|
||||
},
|
||||
"node_modules/express/node_modules/content-disposition": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz",
|
||||
@ -7135,6 +7338,16 @@
|
||||
"node": ">=16"
|
||||
}
|
||||
},
|
||||
"node_modules/flat-cache/node_modules/keyv": {
|
||||
"version": "4.5.4",
|
||||
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
|
||||
"integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"json-buffer": "3.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/flatted": {
|
||||
"version": "3.3.3",
|
||||
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz",
|
||||
@ -7688,6 +7901,15 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/helmet": {
|
||||
"version": "8.1.0",
|
||||
"resolved": "https://registry.npmjs.org/helmet/-/helmet-8.1.0.tgz",
|
||||
"integrity": "sha512-jOiHyAZsmnr8LqoPGmCjYAaiuWwjAPLgY8ZX2XrmHawt99/u1y6RgrZMTeoPfpUbV96HOalYgz1qzkRbw54Pmg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/hexoid": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/hexoid/-/hexoid-2.0.0.tgz",
|
||||
@ -7781,7 +8003,6 @@
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
|
||||
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
@ -8979,13 +9200,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/keyv": {
|
||||
"version": "4.5.4",
|
||||
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
|
||||
"integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
|
||||
"dev": true,
|
||||
"version": "5.3.2",
|
||||
"resolved": "https://registry.npmjs.org/keyv/-/keyv-5.3.2.tgz",
|
||||
"integrity": "sha512-Lji2XRxqqa5Wg+CHLVfFKBImfJZ4pCSccu9eVWK6w4c2SDFLd8JAn1zqTuSFnsxb7ope6rMsnIHfp+eBbRBRZQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"json-buffer": "3.0.1"
|
||||
"@keyv/serialize": "^1.0.3"
|
||||
}
|
||||
},
|
||||
"node_modules/kind-of": {
|
||||
@ -9032,6 +9252,12 @@
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/libphonenumber-js": {
|
||||
"version": "1.12.6",
|
||||
"resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.12.6.tgz",
|
||||
"integrity": "sha512-PJiS4ETaUfCOFLpmtKzAbqZQjCCKVu2OhTV4SVNNE7c2nu/dACvtCqj4L0i/KWNnIgRv7yrILvBj5Lonv5Ncxw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lines-and-columns": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
|
||||
@ -9069,7 +9295,6 @@
|
||||
"version": "4.17.21",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.defaults": {
|
||||
@ -12295,6 +12520,15 @@
|
||||
"node": ">=10.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/validator": {
|
||||
"version": "13.15.0",
|
||||
"resolved": "https://registry.npmjs.org/validator/-/validator-13.15.0.tgz",
|
||||
"integrity": "sha512-36B2ryl4+oL5QxZ3AzD0t5SsMNGvTtQHpjgFO5tbNxfXbMFkY822ktCDe1MnlqV3301QQI9SLHDNJokDI+Z9pA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/vary": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
|
||||
|
||||
@ -20,15 +20,24 @@
|
||||
"test:e2e": "jest --config ./test/jest-e2e.json"
|
||||
},
|
||||
"dependencies": {
|
||||
"@nestjs/cache-manager": "^3.0.1",
|
||||
"@nestjs/common": "^11.0.1",
|
||||
"@nestjs/config": "^4.0.2",
|
||||
"@nestjs/core": "^11.0.1",
|
||||
"@nestjs/jwt": "^11.0.0",
|
||||
"@nestjs/platform-express": "^11.0.1",
|
||||
"@nestjs/schedule": "^5.0.1",
|
||||
"@prisma/client": "^6.6.0",
|
||||
"@types/bcrypt": "^5.0.2",
|
||||
"@types/passport-jwt": "^4.0.1",
|
||||
"bcrypt": "^5.1.1",
|
||||
"bullmq": "^5.47.2",
|
||||
"cache-manager": "^6.4.2",
|
||||
"cache-manager-redis-store": "^3.0.1",
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "^0.14.1",
|
||||
"express-rate-limit": "^7.5.0",
|
||||
"helmet": "^8.1.0",
|
||||
"passport-jwt": "^4.0.1",
|
||||
"prisma": "^6.6.0",
|
||||
"redis": "^4.7.0",
|
||||
|
||||
@ -1,13 +1,22 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { ScheduleModule } from '@nestjs/schedule';
|
||||
import { AppController } from './app.controller';
|
||||
import { AppService } from './app.service';
|
||||
import { ArticlesModule } from './articles/articles.module';
|
||||
import { AuthModule } from './auth/auth.module';
|
||||
import { RssModule } from './rss/rss.module';
|
||||
import { CollectionsModule } from './collections/collections.module';
|
||||
import { RedisCacheModule } from './cache/cache.module';
|
||||
|
||||
@Module({
|
||||
imports: [ArticlesModule, AuthModule, RssModule, CollectionsModule],
|
||||
imports: [
|
||||
ScheduleModule.forRoot(),
|
||||
ArticlesModule,
|
||||
AuthModule,
|
||||
RssModule,
|
||||
CollectionsModule,
|
||||
RedisCacheModule,
|
||||
],
|
||||
controllers: [AppController],
|
||||
providers: [AppService],
|
||||
})
|
||||
|
||||
22
backend/src/cache/cache.module.ts
vendored
Normal file
22
backend/src/cache/cache.module.ts
vendored
Normal file
@ -0,0 +1,22 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { CacheModule } from '@nestjs/cache-manager';
|
||||
import * as redisStore from 'cache-manager-redis-store';
|
||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
CacheModule.registerAsync({
|
||||
imports: [ConfigModule],
|
||||
inject: [ConfigService],
|
||||
useFactory: (configService: ConfigService) => ({
|
||||
store: redisStore,
|
||||
host: configService.get('REDIS_HOST', 'localhost'),
|
||||
port: configService.get('REDIS_PORT', 6379),
|
||||
ttl: 60 * 60 * 24, // 24 hours
|
||||
}),
|
||||
}),
|
||||
],
|
||||
exports: [CacheModule],
|
||||
})
|
||||
export class RedisCacheModule {}
|
||||
|
||||
@ -6,10 +6,14 @@ import {
|
||||
Param,
|
||||
UseGuards,
|
||||
Request,
|
||||
Query,
|
||||
Inject,
|
||||
} from '@nestjs/common';
|
||||
import { CACHE_MANAGER } from '@nestjs/cache-manager';
|
||||
import { Cache } from 'cache-manager';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
import { JwtAuthGuard } from '../auth';
|
||||
import { Article, Collection, QuickNote } from '@prisma/client';
|
||||
import { CreateCollectionDto, AddArticleDto, CreateQuickNoteDto } from './dto';
|
||||
|
||||
interface RequestWithUser extends Request {
|
||||
user: {
|
||||
@ -18,59 +22,63 @@ interface RequestWithUser extends Request {
|
||||
};
|
||||
}
|
||||
|
||||
type CollectionWithRelations = Collection & {
|
||||
articles: Article[];
|
||||
quickNotes: QuickNote[];
|
||||
notes: any[];
|
||||
};
|
||||
|
||||
interface CreateCollectionDto {
|
||||
name: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
interface CreateArticleDto {
|
||||
title: string;
|
||||
content: string;
|
||||
source: string;
|
||||
url: string;
|
||||
publishedAt: Date;
|
||||
}
|
||||
|
||||
interface CreateQuickNoteDto {
|
||||
title: string;
|
||||
content: string;
|
||||
shared?: boolean;
|
||||
sharedVia?: string;
|
||||
}
|
||||
|
||||
@Controller('api/collections')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
export class CollectionsController {
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
constructor(
|
||||
private readonly prisma: PrismaService,
|
||||
@Inject(CACHE_MANAGER) private cacheManager: Cache,
|
||||
) {}
|
||||
|
||||
@Get()
|
||||
async getAllCollections(
|
||||
@Request() req: RequestWithUser,
|
||||
): Promise<CollectionWithRelations[]> {
|
||||
return await this.prisma.collection.findMany({
|
||||
@Query('limit') limit?: number,
|
||||
@Query('cursor') cursor?: string,
|
||||
) {
|
||||
const cacheKey = `collections:${req.user.id}:${limit}:${cursor}`;
|
||||
const cached = await this.cacheManager.get(cacheKey);
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
const take = limit ? Number(limit) : 20;
|
||||
const collections = await this.prisma.collection.findMany({
|
||||
where: {
|
||||
userId: req.user.id,
|
||||
},
|
||||
take: take + 1,
|
||||
skip: cursor ? 1 : 0,
|
||||
cursor: cursor ? { id: cursor } : undefined,
|
||||
orderBy: [{ updatedAt: 'desc' }, { id: 'desc' }],
|
||||
include: {
|
||||
articles: true,
|
||||
quickNotes: true,
|
||||
notes: true,
|
||||
},
|
||||
});
|
||||
|
||||
let nextCursor: string | null = null;
|
||||
if (collections.length > take) {
|
||||
const nextItem = collections.pop()!;
|
||||
nextCursor = nextItem.id;
|
||||
}
|
||||
|
||||
const result = {
|
||||
collections,
|
||||
nextCursor,
|
||||
};
|
||||
|
||||
await this.cacheManager.set(cacheKey, result, 300); // Cache for 5 minutes
|
||||
return result;
|
||||
}
|
||||
|
||||
@Post()
|
||||
async createCollection(
|
||||
@Body() data: CreateCollectionDto,
|
||||
@Request() req: RequestWithUser,
|
||||
): Promise<CollectionWithRelations> {
|
||||
return await this.prisma.collection.create({
|
||||
) {
|
||||
const collection = await this.prisma.collection.create({
|
||||
data: {
|
||||
...data,
|
||||
userId: req.user.id,
|
||||
@ -81,13 +89,20 @@ export class CollectionsController {
|
||||
notes: true,
|
||||
},
|
||||
});
|
||||
|
||||
// Invalidate user's collections cache
|
||||
const cachePattern = `collections:${req.user.id}:*`;
|
||||
await this.cacheManager.del(cachePattern);
|
||||
|
||||
return collection;
|
||||
}
|
||||
|
||||
@Post(':id/add-article')
|
||||
async addArticleToCollection(
|
||||
@Param('id') id: string,
|
||||
@Body() articleData: CreateArticleDto,
|
||||
): Promise<Article> {
|
||||
@Body() articleData: AddArticleDto,
|
||||
@Request() req: RequestWithUser,
|
||||
) {
|
||||
const existingArticle = await this.prisma.article.findUnique({
|
||||
where: { url: articleData.url },
|
||||
});
|
||||
@ -101,10 +116,15 @@ export class CollectionsController {
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Invalidate cache for this collection
|
||||
const cachePattern = `collections:${req.user.id}:*`;
|
||||
await this.cacheManager.del(cachePattern);
|
||||
|
||||
return existingArticle;
|
||||
}
|
||||
|
||||
return await this.prisma.article.create({
|
||||
const article = await this.prisma.article.create({
|
||||
data: {
|
||||
...articleData,
|
||||
collections: {
|
||||
@ -112,6 +132,12 @@ export class CollectionsController {
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Invalidate cache for this collection
|
||||
const cachePattern = `collections:${req.user.id}:*`;
|
||||
await this.cacheManager.del(cachePattern);
|
||||
|
||||
return article;
|
||||
}
|
||||
|
||||
@Post(':id/quick-notes')
|
||||
@ -119,8 +145,8 @@ export class CollectionsController {
|
||||
@Param('id') id: string,
|
||||
@Body() quickNoteData: CreateQuickNoteDto,
|
||||
@Request() req: RequestWithUser,
|
||||
): Promise<QuickNote> {
|
||||
return await this.prisma.quickNote.create({
|
||||
) {
|
||||
const quickNote = await this.prisma.quickNote.create({
|
||||
data: {
|
||||
...quickNoteData,
|
||||
shared: quickNoteData.shared ?? false,
|
||||
@ -132,5 +158,11 @@ export class CollectionsController {
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Invalidate cache for this collection
|
||||
const cachePattern = `collections:${req.user.id}:*`;
|
||||
await this.cacheManager.del(cachePattern);
|
||||
|
||||
return quickNote;
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,9 +2,10 @@ import { Module } from '@nestjs/common';
|
||||
import { PrismaModule } from '../prisma/prisma.module';
|
||||
import { CollectionsController } from './collections.controller';
|
||||
import { AuthModule } from '../auth/auth.module';
|
||||
import { RedisCacheModule } from '../cache/cache.module';
|
||||
|
||||
@Module({
|
||||
imports: [PrismaModule, AuthModule],
|
||||
imports: [PrismaModule, AuthModule, RedisCacheModule],
|
||||
controllers: [CollectionsController],
|
||||
})
|
||||
export class CollectionsModule {}
|
||||
|
||||
63
backend/src/collections/dto.ts
Normal file
63
backend/src/collections/dto.ts
Normal file
@ -0,0 +1,63 @@
|
||||
import {
|
||||
IsString,
|
||||
IsOptional,
|
||||
IsUUID,
|
||||
IsNotEmpty,
|
||||
Length,
|
||||
} from 'class-validator';
|
||||
|
||||
export class CreateCollectionDto {
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@Length(1, 100)
|
||||
name: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@Length(0, 500)
|
||||
description?: string;
|
||||
|
||||
@IsUUID()
|
||||
userId: string;
|
||||
}
|
||||
|
||||
export class AddArticleDto {
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
title: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
content: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
source: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
url: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
publishedAt: string;
|
||||
}
|
||||
|
||||
export class CreateQuickNoteDto {
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@Length(1, 100)
|
||||
title: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@Length(1, 5000)
|
||||
content: string;
|
||||
|
||||
@IsOptional()
|
||||
shared?: boolean;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
sharedVia?: string;
|
||||
}
|
||||
39
backend/src/filters/http-exception.filter.ts
Normal file
39
backend/src/filters/http-exception.filter.ts
Normal file
@ -0,0 +1,39 @@
|
||||
import {
|
||||
ExceptionFilter,
|
||||
Catch,
|
||||
ArgumentsHost,
|
||||
HttpException,
|
||||
HttpStatus,
|
||||
} from '@nestjs/common';
|
||||
import { Response } from 'express';
|
||||
|
||||
@Catch()
|
||||
export class HttpExceptionFilter implements ExceptionFilter {
|
||||
catch(exception: unknown, host: ArgumentsHost) {
|
||||
const ctx = host.switchToHttp();
|
||||
const response = ctx.getResponse<Response>();
|
||||
|
||||
const status =
|
||||
exception instanceof HttpException
|
||||
? exception.getStatus()
|
||||
: HttpStatus.INTERNAL_SERVER_ERROR;
|
||||
|
||||
const message =
|
||||
exception instanceof HttpException
|
||||
? exception.message
|
||||
: 'Internal server error';
|
||||
|
||||
const errorResponse = {
|
||||
statusCode: status,
|
||||
message,
|
||||
timestamp: new Date().toISOString(),
|
||||
path: ctx.getRequest().url,
|
||||
};
|
||||
|
||||
// Log error for debugging
|
||||
console.error('Exception:', exception);
|
||||
console.error('Stack trace:', (exception as Error).stack);
|
||||
|
||||
response.status(status).json(errorResponse);
|
||||
}
|
||||
}
|
||||
@ -1,8 +1,37 @@
|
||||
import { NestFactory } from '@nestjs/core';
|
||||
import { AppModule } from './app.module';
|
||||
import { HttpExceptionFilter } from './filters/http-exception.filter';
|
||||
import helmet from 'helmet';
|
||||
import { ValidationPipe } from '@nestjs/common';
|
||||
import rateLimit from 'express-rate-limit';
|
||||
|
||||
async function bootstrap() {
|
||||
const app = await NestFactory.create(AppModule);
|
||||
|
||||
// Global exception filter
|
||||
app.useGlobalFilters(new HttpExceptionFilter());
|
||||
|
||||
// Global validation pipe
|
||||
app.useGlobalPipes(
|
||||
new ValidationPipe({
|
||||
whitelist: true,
|
||||
transform: true,
|
||||
forbidNonWhitelisted: true,
|
||||
}),
|
||||
);
|
||||
|
||||
// Security headers
|
||||
app.use(helmet());
|
||||
|
||||
// Rate limiting
|
||||
app.use(
|
||||
rateLimit({
|
||||
windowMs: 15 * 60 * 1000, // 15 minutes
|
||||
max: 100, // limit each IP to 100 requests per windowMs
|
||||
message: 'Too many requests from this IP, please try again later',
|
||||
}),
|
||||
);
|
||||
|
||||
await app.listen(process.env.PORT ?? 3000);
|
||||
}
|
||||
|
||||
|
||||
@ -1,10 +1,11 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { ScheduleModule } from '@nestjs/schedule';
|
||||
import { PrismaModule } from '../prisma/prisma.module';
|
||||
import { RssService } from './rss.service';
|
||||
import { RssController } from './rss.controller';
|
||||
|
||||
@Module({
|
||||
imports: [PrismaModule],
|
||||
imports: [PrismaModule, ScheduleModule],
|
||||
providers: [RssService],
|
||||
controllers: [RssController],
|
||||
exports: [RssService],
|
||||
|
||||
@ -1,9 +1,19 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
import { Cron, CronExpression } from '@nestjs/schedule';
|
||||
import * as Parser from 'rss-parser';
|
||||
|
||||
@Injectable()
|
||||
export class RssService {
|
||||
private feedUrls: string[] = [
|
||||
'https://rt.com/rss',
|
||||
'http://feeds.abcnews.com',
|
||||
'http://rss.cnn.com/rss/cnn',
|
||||
'https://www.npr.org/rss/',
|
||||
'https://www.theguardian.com/rss',
|
||||
'https://www.bbc.co.uk/news',
|
||||
];
|
||||
|
||||
constructor(private prisma: PrismaService) {}
|
||||
|
||||
private parser: Parser;
|
||||
@ -20,6 +30,25 @@ export class RssService {
|
||||
});
|
||||
}
|
||||
|
||||
addFeedUrl(url: string) {
|
||||
if (!this.feedUrls.includes(url)) {
|
||||
this.feedUrls.push(url);
|
||||
}
|
||||
}
|
||||
|
||||
@Cron(CronExpression.EVERY_5_MINUTES)
|
||||
async handleCron() {
|
||||
console.log('Fetching RSS feeds...', Date.now().toLocaleString());
|
||||
for (const url of this.feedUrls) {
|
||||
try {
|
||||
await this.fetchAndStoreArticles(url);
|
||||
console.log(`Successfully fetched articles from ${url}`);
|
||||
} catch (error) {
|
||||
console.error(`Error fetching from ${url}:`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private cleanHtml(html: string): string {
|
||||
// Remove HTML tags and decode entities
|
||||
return html
|
||||
@ -36,7 +65,11 @@ export class RssService {
|
||||
|
||||
for (const item of feed.items) {
|
||||
// Extract content from either contentEncoded or description
|
||||
const rawContent = item.contentEncoded || item.content || item.description || '';
|
||||
const rawContent =
|
||||
(item as Parser.Item & { contentEncoded?: string }).contentEncoded ||
|
||||
item.content ||
|
||||
item.description ||
|
||||
'';
|
||||
const cleanContent = this.cleanHtml(rawContent);
|
||||
|
||||
// Get the first image URL from content if available
|
||||
@ -44,7 +77,9 @@ export class RssService {
|
||||
const imageUrl = imageMatch ? imageMatch[1] : null;
|
||||
|
||||
// Extract categories and authors
|
||||
const categories = Array.isArray(item.categories) ? item.categories : [];
|
||||
const categories = Array.isArray(item.categories)
|
||||
? item.categories
|
||||
: [];
|
||||
const authors = item.creator ? [item.creator] : [];
|
||||
|
||||
await this.prisma.article.upsert({
|
||||
|
||||
424
frontend/package-lock.json
generated
424
frontend/package-lock.json
generated
@ -9,10 +9,11 @@
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-accordion": "^1.2.3",
|
||||
"@radix-ui/react-alert-dialog": "^1.1.6",
|
||||
"@radix-ui/react-alert-dialog": "^1.1.7",
|
||||
"@radix-ui/react-dialog": "^1.1.6",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.6",
|
||||
"@radix-ui/react-hover-card": "^1.1.6",
|
||||
"@radix-ui/react-icons": "^1.3.2",
|
||||
"@radix-ui/react-label": "^2.1.2",
|
||||
"@radix-ui/react-menubar": "^1.1.6",
|
||||
"@radix-ui/react-navigation-menu": "^1.2.5",
|
||||
@ -29,6 +30,7 @@
|
||||
"lucide-react": "^0.487.0",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-resizable-panels": "^2.1.7",
|
||||
"tailwind-merge": "^3.2.0",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"tw-animate-css": "^1.2.5"
|
||||
@ -1193,17 +1195,17 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-alert-dialog": {
|
||||
"version": "1.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-alert-dialog/-/react-alert-dialog-1.1.6.tgz",
|
||||
"integrity": "sha512-p4XnPqgej8sZAAReCAKgz1REYZEBLR8hU9Pg27wFnCWIMc8g1ccCs0FjBcy05V15VTu8pAePw/VDYeOm/uZ6yQ==",
|
||||
"version": "1.1.7",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-alert-dialog/-/react-alert-dialog-1.1.7.tgz",
|
||||
"integrity": "sha512-7Gx1gcoltd0VxKoR8mc+TAVbzvChJyZryZsTam0UhoL92z0L+W8ovxvcgvd+nkz24y7Qc51JQKBAGe4+825tYw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/primitive": "1.1.1",
|
||||
"@radix-ui/react-compose-refs": "1.1.1",
|
||||
"@radix-ui/react-context": "1.1.1",
|
||||
"@radix-ui/react-dialog": "1.1.6",
|
||||
"@radix-ui/react-primitive": "2.0.2",
|
||||
"@radix-ui/react-slot": "1.1.2"
|
||||
"@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.7",
|
||||
"@radix-ui/react-primitive": "2.0.3",
|
||||
"@radix-ui/react-slot": "1.2.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
@ -1220,6 +1222,83 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-alert-dialog/node_modules/@radix-ui/primitive": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.2.tgz",
|
||||
"integrity": "sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@radix-ui/react-alert-dialog/node_modules/@radix-ui/react-compose-refs": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz",
|
||||
"integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-alert-dialog/node_modules/@radix-ui/react-context": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz",
|
||||
"integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-alert-dialog/node_modules/@radix-ui/react-primitive": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.3.tgz",
|
||||
"integrity": "sha512-Pf/t/GkndH7CQ8wE2hbkXA+WyZ83fhQQn5DDmwDiDo6AwN/fhaH8oqZ0jRjMrO2iaMhDi6P1HRx6AZwyMinY1g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-slot": "1.2.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-alert-dialog/node_modules/@radix-ui/react-slot": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.0.tgz",
|
||||
"integrity": "sha512-ujc+V6r0HNDviYqIK3rW4ffgYiZ8g5DEHrGJVk4x7kTlLXRDILnKX9vAUYeIsLOoDpDJ0ujpqMkjH4w2ofuo6w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-arrow": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.2.tgz",
|
||||
@ -1330,23 +1409,23 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-dialog": {
|
||||
"version": "1.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.6.tgz",
|
||||
"integrity": "sha512-/IVhJV5AceX620DUJ4uYVMymzsipdKBzo3edo+omeskCKGm9FRHM0ebIdbPnlQVJqyuHbuBltQUOG2mOTq2IYw==",
|
||||
"version": "1.1.7",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.7.tgz",
|
||||
"integrity": "sha512-EIdma8C0C/I6kL6sO02avaCRqi3fmWJpxH6mqbVScorW6nNktzKJT/le7VPho3o/7wCsyRg3z0+Q+Obr0Gy/VQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/primitive": "1.1.1",
|
||||
"@radix-ui/react-compose-refs": "1.1.1",
|
||||
"@radix-ui/react-context": "1.1.1",
|
||||
"@radix-ui/react-dismissable-layer": "1.1.5",
|
||||
"@radix-ui/react-focus-guards": "1.1.1",
|
||||
"@radix-ui/react-focus-scope": "1.1.2",
|
||||
"@radix-ui/react-id": "1.1.0",
|
||||
"@radix-ui/react-portal": "1.1.4",
|
||||
"@radix-ui/react-presence": "1.1.2",
|
||||
"@radix-ui/react-primitive": "2.0.2",
|
||||
"@radix-ui/react-slot": "1.1.2",
|
||||
"@radix-ui/react-use-controllable-state": "1.1.0",
|
||||
"@radix-ui/primitive": "1.1.2",
|
||||
"@radix-ui/react-compose-refs": "1.1.2",
|
||||
"@radix-ui/react-context": "1.1.2",
|
||||
"@radix-ui/react-dismissable-layer": "1.1.6",
|
||||
"@radix-ui/react-focus-guards": "1.1.2",
|
||||
"@radix-ui/react-focus-scope": "1.1.3",
|
||||
"@radix-ui/react-id": "1.1.1",
|
||||
"@radix-ui/react-portal": "1.1.5",
|
||||
"@radix-ui/react-presence": "1.1.3",
|
||||
"@radix-ui/react-primitive": "2.0.3",
|
||||
"@radix-ui/react-slot": "1.2.0",
|
||||
"@radix-ui/react-use-controllable-state": "1.1.1",
|
||||
"aria-hidden": "^1.2.4",
|
||||
"react-remove-scroll": "^2.6.3"
|
||||
},
|
||||
@ -1365,6 +1444,282 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/primitive": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.2.tgz",
|
||||
"integrity": "sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-compose-refs": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz",
|
||||
"integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-context": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz",
|
||||
"integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-dismissable-layer": {
|
||||
"version": "1.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.6.tgz",
|
||||
"integrity": "sha512-7gpgMT2gyKym9Jz2ZhlRXSg2y6cNQIK8d/cqBZ0RBCaps8pFryCWXiUKI+uHGFrhMrbGUP7U6PWgiXzIxoyF3Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/primitive": "1.1.2",
|
||||
"@radix-ui/react-compose-refs": "1.1.2",
|
||||
"@radix-ui/react-primitive": "2.0.3",
|
||||
"@radix-ui/react-use-callback-ref": "1.1.1",
|
||||
"@radix-ui/react-use-escape-keydown": "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"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-focus-guards": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.2.tgz",
|
||||
"integrity": "sha512-fyjAACV62oPV925xFCrH8DR5xWhg9KYtJT4s3u54jxp+L/hbpTY2kIeEFFbFe+a/HCE94zGQMZLIpVTPVZDhaA==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-focus-scope": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.3.tgz",
|
||||
"integrity": "sha512-4XaDlq0bPt7oJwR+0k0clCiCO/7lO7NKZTAaJBYxDNQT/vj4ig0/UvctrRscZaFREpRvUTkpKR96ov1e6jptQg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.2",
|
||||
"@radix-ui/react-primitive": "2.0.3",
|
||||
"@radix-ui/react-use-callback-ref": "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"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-id": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz",
|
||||
"integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-use-layout-effect": "1.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-portal": {
|
||||
"version": "1.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.5.tgz",
|
||||
"integrity": "sha512-ps/67ZqsFm+Mb6lSPJpfhRLrVL2i2fntgCmGMqqth4eaGUf+knAuuRtWVJrNjUhExgmdRqftSgzpf0DF0n6yXA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-primitive": "2.0.3",
|
||||
"@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"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-presence": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.3.tgz",
|
||||
"integrity": "sha512-IrVLIhskYhH3nLvtcBLQFZr61tBG7wx7O3kEmdzcYwRGAEBmBicGGL7ATzNgruYJ3xBTbuzEEq9OXJM3PAX3tA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.2",
|
||||
"@radix-ui/react-use-layout-effect": "1.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-primitive": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.3.tgz",
|
||||
"integrity": "sha512-Pf/t/GkndH7CQ8wE2hbkXA+WyZ83fhQQn5DDmwDiDo6AwN/fhaH8oqZ0jRjMrO2iaMhDi6P1HRx6AZwyMinY1g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-slot": "1.2.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-slot": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.0.tgz",
|
||||
"integrity": "sha512-ujc+V6r0HNDviYqIK3rW4ffgYiZ8g5DEHrGJVk4x7kTlLXRDILnKX9vAUYeIsLOoDpDJ0ujpqMkjH4w2ofuo6w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-use-callback-ref": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz",
|
||||
"integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-use-controllable-state": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.1.1.tgz",
|
||||
"integrity": "sha512-YnEXIy8/ga01Y1PN0VfaNH//MhA91JlEGVBDxDzROqwrAtG5Yr2QGEPz8A/rJA3C7ZAHryOYGaUv8fLSW2H/mg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-use-callback-ref": "1.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-use-escape-keydown": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz",
|
||||
"integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-use-callback-ref": "1.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-use-layout-effect": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz",
|
||||
"integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-direction": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.0.tgz",
|
||||
@ -1507,6 +1862,15 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-icons": {
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-icons/-/react-icons-1.3.2.tgz",
|
||||
"integrity": "sha512-fyQIhGDhzfc9pK2kH6Pl9c4BDJGfMkPqkyIgYDthyNYoNg3wVhoJMMh19WS4Up/1KMPFVpNsT2q3WmXn2N1m6g==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"react": "^16.x || ^17.x || ^18.x || ^19.0.0 || ^19.0.0-rc"
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-id": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.0.tgz",
|
||||
@ -5081,6 +5445,16 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/react-resizable-panels": {
|
||||
"version": "2.1.7",
|
||||
"resolved": "https://registry.npmjs.org/react-resizable-panels/-/react-resizable-panels-2.1.7.tgz",
|
||||
"integrity": "sha512-JtT6gI+nURzhMYQYsx8DKkx6bSoOGFp7A3CwMrOb8y5jFHFyqwo9m68UhmXRw57fRVJksFn1TSlm3ywEQ9vMgA==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"react": "^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
|
||||
}
|
||||
},
|
||||
"node_modules/react-style-singleton": {
|
||||
"version": "2.2.3",
|
||||
"resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz",
|
||||
|
||||
@ -11,10 +11,11 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@radix-ui/react-accordion": "^1.2.3",
|
||||
"@radix-ui/react-alert-dialog": "^1.1.6",
|
||||
"@radix-ui/react-alert-dialog": "^1.1.7",
|
||||
"@radix-ui/react-dialog": "^1.1.6",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.6",
|
||||
"@radix-ui/react-hover-card": "^1.1.6",
|
||||
"@radix-ui/react-icons": "^1.3.2",
|
||||
"@radix-ui/react-label": "^2.1.2",
|
||||
"@radix-ui/react-menubar": "^1.1.6",
|
||||
"@radix-ui/react-navigation-menu": "^1.2.5",
|
||||
@ -31,6 +32,7 @@
|
||||
"lucide-react": "^0.487.0",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-resizable-panels": "^2.1.7",
|
||||
"tailwind-merge": "^3.2.0",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"tw-animate-css": "^1.2.5"
|
||||
|
||||
@ -9,13 +9,24 @@ const getAuthHeaders = () => {
|
||||
};
|
||||
};
|
||||
|
||||
export async function fetchCollections(): Promise<Collection[]> {
|
||||
const response = await fetch('/api/collections', {
|
||||
export interface PaginatedCollections {
|
||||
collections: Collection[];
|
||||
nextCursor: string | null;
|
||||
}
|
||||
|
||||
export async function fetchCollections(cursor?: string, limit: number = 20): Promise<PaginatedCollections> {
|
||||
const params = new URLSearchParams();
|
||||
if (cursor) params.append('cursor', cursor);
|
||||
if (limit) params.append('limit', limit.toString());
|
||||
|
||||
const response = await fetch(`/api/collections?${params.toString()}`, {
|
||||
headers: getAuthHeaders(),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch collections');
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
@ -25,19 +36,35 @@ export async function createCollection(data: { name: string; description?: strin
|
||||
headers: getAuthHeaders(),
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to create collection');
|
||||
const error = await response.json();
|
||||
throw new Error(error.message || 'Failed to create collection');
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
export async function addArticleToCollection(collectionId: string, articleData: { title: string; content: string; source: string; url: string; publishedAt: Date }): Promise<void> {
|
||||
export async function addArticleToCollection(
|
||||
collectionId: string,
|
||||
articleData: {
|
||||
title: string;
|
||||
content: string;
|
||||
source: string;
|
||||
url: string;
|
||||
publishedAt: Date;
|
||||
categories?: string[];
|
||||
authors?: string[];
|
||||
}
|
||||
): Promise<void> {
|
||||
const response = await fetch(`/api/collections/${collectionId}/add-article`, {
|
||||
method: 'POST',
|
||||
headers: getAuthHeaders(),
|
||||
body: JSON.stringify(articleData),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to add article to collection');
|
||||
const error = await response.json();
|
||||
throw new Error(error.message || 'Failed to add article to collection');
|
||||
}
|
||||
}
|
||||
139
frontend/src/components/ui/alert-dialog.tsx
Normal file
139
frontend/src/components/ui/alert-dialog.tsx
Normal file
@ -0,0 +1,139 @@
|
||||
import * as React from "react"
|
||||
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const AlertDialog = AlertDialogPrimitive.Root
|
||||
|
||||
const AlertDialogTrigger = AlertDialogPrimitive.Trigger
|
||||
|
||||
const AlertDialogPortal = AlertDialogPrimitive.Portal
|
||||
|
||||
const AlertDialogOverlay = React.forwardRef<
|
||||
React.ElementRef<typeof AlertDialogPrimitive.Overlay>,
|
||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AlertDialogPrimitive.Overlay
|
||||
className={cn(
|
||||
"fixed inset-0 z-50 bg-black/50 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
ref={ref}
|
||||
/>
|
||||
))
|
||||
AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName
|
||||
|
||||
const AlertDialogContent = React.forwardRef<
|
||||
React.ElementRef<typeof AlertDialogPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AlertDialogPortal>
|
||||
<AlertDialogOverlay />
|
||||
<AlertDialogPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</AlertDialogPortal>
|
||||
))
|
||||
AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName
|
||||
|
||||
const AlertDialogHeader = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col space-y-2 text-center sm:text-left",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
AlertDialogHeader.displayName = "AlertDialogHeader"
|
||||
|
||||
const AlertDialogFooter = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
AlertDialogFooter.displayName = "AlertDialogFooter"
|
||||
|
||||
const AlertDialogTitle = React.forwardRef<
|
||||
React.ElementRef<typeof AlertDialogPrimitive.Title>,
|
||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AlertDialogPrimitive.Title
|
||||
ref={ref}
|
||||
className={cn("text-lg font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName
|
||||
|
||||
const AlertDialogDescription = React.forwardRef<
|
||||
React.ElementRef<typeof AlertDialogPrimitive.Description>,
|
||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AlertDialogPrimitive.Description
|
||||
ref={ref}
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
AlertDialogDescription.displayName = AlertDialogPrimitive.Description.displayName
|
||||
|
||||
const AlertDialogAction = React.forwardRef<
|
||||
React.ElementRef<typeof AlertDialogPrimitive.Action>,
|
||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AlertDialogPrimitive.Action
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 bg-primary text-primary-foreground hover:bg-primary/90 h-10 px-4 py-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName
|
||||
|
||||
const AlertDialogCancel = React.forwardRef<
|
||||
React.ElementRef<typeof AlertDialogPrimitive.Cancel>,
|
||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AlertDialogPrimitive.Cancel
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 border border-input bg-background hover:bg-accent hover:text-accent-foreground mt-2 h-10 px-4 py-2 sm:mt-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName
|
||||
|
||||
export {
|
||||
AlertDialog,
|
||||
AlertDialogPortal,
|
||||
AlertDialogOverlay,
|
||||
AlertDialogTrigger,
|
||||
AlertDialogContent,
|
||||
AlertDialogHeader,
|
||||
AlertDialogFooter,
|
||||
AlertDialogTitle,
|
||||
AlertDialogDescription,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
}
|
||||
43
frontend/src/components/ui/resizable.tsx
Normal file
43
frontend/src/components/ui/resizable.tsx
Normal file
@ -0,0 +1,43 @@
|
||||
import { DragHandleDots2Icon } from "@radix-ui/react-icons"
|
||||
import * as ResizablePrimitive from "react-resizable-panels"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const ResizablePanelGroup = ({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof ResizablePrimitive.PanelGroup>) => (
|
||||
<ResizablePrimitive.PanelGroup
|
||||
className={cn(
|
||||
"flex h-full w-full data-[panel-group-direction=vertical]:flex-col",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
|
||||
const ResizablePanel = ResizablePrimitive.Panel
|
||||
|
||||
const ResizableHandle = ({
|
||||
withHandle,
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof ResizablePrimitive.PanelResizeHandle> & {
|
||||
withHandle?: boolean
|
||||
}) => (
|
||||
<ResizablePrimitive.PanelResizeHandle
|
||||
className={cn(
|
||||
"relative flex w-px items-center justify-center bg-border after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring focus-visible:ring-offset-1 data-[panel-group-direction=vertical]:h-px data-[panel-group-direction=vertical]:w-full data-[panel-group-direction=vertical]:after:left-0 data-[panel-group-direction=vertical]:after:h-1 data-[panel-group-direction=vertical]:after:w-full data-[panel-group-direction=vertical]:after:-translate-y-1/2 data-[panel-group-direction=vertical]:after:translate-x-0 [&[data-panel-group-direction=vertical]>div]:rotate-90",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{withHandle && (
|
||||
<div className="z-10 flex h-4 w-3 items-center justify-center rounded-sm border bg-border">
|
||||
<DragHandleDots2Icon className="h-2.5 w-2.5" />
|
||||
</div>
|
||||
)}
|
||||
</ResizablePrimitive.PanelResizeHandle>
|
||||
)
|
||||
|
||||
export { ResizablePanelGroup, ResizablePanel, ResizableHandle }
|
||||
@ -1,24 +1,40 @@
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { useInfiniteQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { fetchCollections, createCollection } from '@/api/collections';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { useState } from 'react';
|
||||
import { useState, useRef, useCallback, useEffect } from 'react';
|
||||
import { createFileRoute } from '@tanstack/react-router';
|
||||
import { useUser, useIsAuthenticated } from '@/store/user';
|
||||
import { useUser } from '@/store/user';
|
||||
import { useNavigate } from '@tanstack/react-router';
|
||||
import { ProtectedRoute } from '@/components/ProtectedRoute';
|
||||
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger } from '@/components/ui/alert-dialog';
|
||||
|
||||
function CollectionsPage() {
|
||||
const queryClient = useQueryClient();
|
||||
const user = useUser();
|
||||
const navigate = useNavigate();
|
||||
const { data: collections, isLoading, isError } = useQuery({
|
||||
const loadMoreRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const {
|
||||
data,
|
||||
isLoading,
|
||||
isError,
|
||||
error,
|
||||
fetchNextPage,
|
||||
hasNextPage,
|
||||
isFetchingNextPage
|
||||
} = useInfiniteQuery({
|
||||
queryKey: ['collections'],
|
||||
queryFn: fetchCollections,
|
||||
queryFn: ({ pageParam }) => fetchCollections(pageParam as string | undefined),
|
||||
getNextPageParam: (lastPage) => lastPage.nextCursor,
|
||||
initialPageParam: undefined as string | undefined,
|
||||
});
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: (data: { name: string; description: string; userId: string }) => createCollection(data),
|
||||
onSuccess: () => queryClient.invalidateQueries(['collections']),
|
||||
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['collections'] }),
|
||||
onError: (error) => {
|
||||
alert(error instanceof Error ? error.message : 'Failed to create collection');
|
||||
},
|
||||
});
|
||||
|
||||
const [newCollection, setNewCollection] = useState({ name: '', description: '' });
|
||||
@ -30,47 +46,171 @@ function CollectionsPage() {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!newCollection.name.trim()) {
|
||||
alert('Please enter a collection name');
|
||||
return;
|
||||
}
|
||||
|
||||
mutation.mutate({ ...newCollection, userId: user.id });
|
||||
setNewCollection({ name: '', description: '' });
|
||||
};
|
||||
|
||||
if (isLoading) return <div>Loading collections...</div>;
|
||||
if (isError) return <div>Failed to load collections. Please try again later.</div>;
|
||||
// Intersection Observer for infinite scroll
|
||||
const handleObserver = useCallback((entries: IntersectionObserverEntry[]) => {
|
||||
const [target] = entries;
|
||||
if (target.isIntersecting && hasNextPage && !isFetchingNextPage) {
|
||||
void fetchNextPage();
|
||||
}
|
||||
}, [fetchNextPage, hasNextPage, isFetchingNextPage]);
|
||||
|
||||
useEffect(() => {
|
||||
const element = loadMoreRef.current;
|
||||
const observer = new IntersectionObserver(handleObserver, {
|
||||
root: null,
|
||||
rootMargin: '20px',
|
||||
threshold: 0.1,
|
||||
});
|
||||
|
||||
if (element) {
|
||||
observer.observe(element);
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (element) {
|
||||
observer.unobserve(element);
|
||||
}
|
||||
};
|
||||
}, [handleObserver]);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-64">
|
||||
<div className="animate-spin w-8 h-8 border-4 border-primary border-t-transparent rounded-full"></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (isError) {
|
||||
return (
|
||||
<div className="p-6 bg-destructive/10 border border-destructive/30 rounded-lg">
|
||||
<h3 className="text-lg font-semibold text-destructive">Error</h3>
|
||||
<p className="text-destructive/90">{error instanceof Error ? error.message : 'Failed to load collections'}</p>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="mt-4"
|
||||
onClick={() => window.location.reload()}
|
||||
>
|
||||
Try Again
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const allCollections = data?.pages.flatMap(page => page.collections) ?? [];
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<h1 className="text-3xl font-bold">Collections</h1>
|
||||
|
||||
<div className="space-y-4">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Collection Name"
|
||||
value={newCollection.name}
|
||||
onChange={(e) => setNewCollection({ ...newCollection, name: e.target.value })}
|
||||
className="border p-2 rounded w-full"
|
||||
/>
|
||||
<textarea
|
||||
placeholder="Description"
|
||||
value={newCollection.description}
|
||||
onChange={(e) => setNewCollection({ ...newCollection, description: e.target.value })}
|
||||
className="border p-2 rounded w-full"
|
||||
/>
|
||||
<Button onClick={handleCreateCollection}>Create Collection</Button>
|
||||
<div className="space-y-4 p-4 border rounded-lg bg-card">
|
||||
<div className="grid gap-4">
|
||||
<div className="grid gap-2">
|
||||
<label htmlFor="name" className="text-sm font-medium">Collection Name</label>
|
||||
<input
|
||||
id="name"
|
||||
type="text"
|
||||
placeholder="Enter collection name"
|
||||
value={newCollection.name}
|
||||
onChange={(e) => setNewCollection({ ...newCollection, name: e.target.value })}
|
||||
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<label htmlFor="description" className="text-sm font-medium">Description</label>
|
||||
<textarea
|
||||
id="description"
|
||||
placeholder="Enter collection description"
|
||||
value={newCollection.description}
|
||||
onChange={(e) => setNewCollection({ ...newCollection, description: e.target.value })}
|
||||
className="flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
onClick={handleCreateCollection}
|
||||
disabled={mutation.isPending}
|
||||
>
|
||||
{mutation.isPending ? (
|
||||
<>
|
||||
<div className="animate-spin w-4 h-4 border-2 border-current border-t-transparent rounded-full mr-2"></div>
|
||||
Creating...
|
||||
</>
|
||||
) : (
|
||||
'Create Collection'
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{collections?.map((collection) => (
|
||||
<div key={collection.id} className="border p-4 rounded-lg">
|
||||
{allCollections.map((collection) => (
|
||||
<div key={collection.id} className="border p-4 rounded-lg bg-card hover:shadow-md transition-shadow">
|
||||
<h2 className="text-xl font-semibold">{collection.name}</h2>
|
||||
<p className="text-sm text-muted-foreground">{collection.description}</p>
|
||||
<p className="text-sm text-muted-foreground mt-2">{collection.description}</p>
|
||||
<div className="mt-4 space-y-2">
|
||||
<p className="text-sm">Articles: {collection.articles.length}</p>
|
||||
<p className="text-sm">Quick Notes: {collection.quickNotes.length}</p>
|
||||
<p className="text-sm">Notes: {collection.notes.length}</p>
|
||||
</div>
|
||||
<div className="mt-4 flex gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => navigate({ to: `/collections/${collection.name}` })}
|
||||
>
|
||||
View Details
|
||||
</Button>
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button variant="destructive" size="sm">Delete</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Are you sure?</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
This will permanently delete the collection "{collection.name}" and all its contents.
|
||||
This action cannot be undone.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
||||
onClick={() => {
|
||||
// TODO: Implement delete collection
|
||||
}}
|
||||
>
|
||||
Delete
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Loading more indicator */}
|
||||
<div ref={loadMoreRef} className="flex justify-center py-8">
|
||||
{isFetchingNextPage ? (
|
||||
<div className="animate-spin w-6 h-6 border-3 border-primary border-t-transparent rounded-full"></div>
|
||||
) : hasNextPage ? (
|
||||
<span className="text-sm text-muted-foreground">Scroll to load more</span>
|
||||
) : allCollections.length > 0 ? (
|
||||
<span className="text-sm text-muted-foreground">No more collections to load</span>
|
||||
) : (
|
||||
<span className="text-sm text-muted-foreground">No collections found</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -78,7 +218,11 @@ function CollectionsPage() {
|
||||
export default CollectionsPage;
|
||||
|
||||
export const Route = createFileRoute('/collections')({
|
||||
component: CollectionsPage,
|
||||
})
|
||||
component: () => (
|
||||
<ProtectedRoute>
|
||||
<CollectionsPage />
|
||||
</ProtectedRoute>
|
||||
),
|
||||
});
|
||||
|
||||
|
||||
|
||||
@ -4,6 +4,7 @@ import { useNavigate } from '@tanstack/react-router'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { fetchArticle } from '@/api/articles'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { ResizablePanelGroup, ResizablePanel, ResizableHandle } from '@/components/ui/resizable'
|
||||
|
||||
export const Route = createFileRoute('/notebook')({
|
||||
component: NotebookPage,
|
||||
@ -60,49 +61,63 @@ function NotebookPage() {
|
||||
)
|
||||
}
|
||||
|
||||
const cells = [
|
||||
{
|
||||
cell_type: 'markdown',
|
||||
metadata: {
|
||||
language: 'markdown'
|
||||
},
|
||||
source: [
|
||||
`# ${article.title}`,
|
||||
'',
|
||||
`Source: ${article.source}`,
|
||||
`Published: ${new Date(article.publishedAt).toLocaleString()}`,
|
||||
article.authors.length > 0 ? `Authors: ${article.authors.join(', ')}` : '',
|
||||
article.categories.length > 0 ? `Categories: ${article.categories.join(', ')}` : '',
|
||||
'',
|
||||
'---',
|
||||
''
|
||||
]
|
||||
const articleContent = {
|
||||
cell_type: 'markdown',
|
||||
metadata: {
|
||||
language: 'markdown'
|
||||
},
|
||||
{
|
||||
cell_type: 'markdown',
|
||||
metadata: {
|
||||
language: 'markdown'
|
||||
},
|
||||
source: [article.content.split('\n').map(line => line.trim()).join('\n\n')]
|
||||
source: [
|
||||
`# ${article.title}`,
|
||||
'',
|
||||
`Source: ${article.source}`,
|
||||
`Published: ${new Date(article.publishedAt).toLocaleString()}`,
|
||||
article.authors.length > 0 ? `Authors: ${article.authors.join(', ')}` : '',
|
||||
article.categories.length > 0 ? `Categories: ${article.categories.join(', ')}` : '',
|
||||
'',
|
||||
'---',
|
||||
'',
|
||||
article.content.split('\n').map(line => line.trim()).join('\n\n'),
|
||||
'',
|
||||
'---',
|
||||
'',
|
||||
'## Additional Information',
|
||||
'',
|
||||
`Original Article: [${article.url}](${article.url})`
|
||||
]
|
||||
}
|
||||
|
||||
const notesTemplate = {
|
||||
cell_type: 'markdown',
|
||||
metadata: {
|
||||
language: 'markdown'
|
||||
},
|
||||
{
|
||||
cell_type: 'markdown',
|
||||
metadata: {
|
||||
language: 'markdown'
|
||||
},
|
||||
source: [
|
||||
'',
|
||||
'---',
|
||||
'',
|
||||
'## Additional Information',
|
||||
'',
|
||||
`Original Article: [${article.url}](${article.url})`
|
||||
]
|
||||
}
|
||||
]
|
||||
source: [
|
||||
'# Notes',
|
||||
'',
|
||||
'## Summary',
|
||||
'',
|
||||
'<!-- Write your summary here -->',
|
||||
'',
|
||||
'## Key Points',
|
||||
'',
|
||||
'- Point 1',
|
||||
'- Point 2',
|
||||
'- Point 3',
|
||||
'',
|
||||
'## Questions',
|
||||
'',
|
||||
'1. Question 1?',
|
||||
'2. Question 2?',
|
||||
'',
|
||||
'## Action Items',
|
||||
'',
|
||||
'- [ ] Action 1',
|
||||
'- [ ] Action 2',
|
||||
]
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container mx-auto py-8 max-w-4xl">
|
||||
<div className="container mx-auto py-8">
|
||||
<div className="mb-6 flex justify-between items-center">
|
||||
<h1 className="text-3xl font-bold">Article Notebook</h1>
|
||||
<Button
|
||||
@ -113,19 +128,32 @@ function NotebookPage() {
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-8">
|
||||
{cells.map((cell, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="prose prose-gray text-white dark:prose-invert max-w-none border rounded-lg p-8 bg-card"
|
||||
>
|
||||
{cell.source.map((line, i) => (
|
||||
<div key={i} className="whitespace-pre-wrap">
|
||||
{line}
|
||||
<div className="h-[800px] rounded-lg border">
|
||||
<ResizablePanelGroup direction="horizontal">
|
||||
<ResizablePanel defaultSize={50}>
|
||||
<div className="h-full overflow-auto p-6">
|
||||
<div className="prose prose-gray text-white dark:prose-invert max-w-none">
|
||||
{articleContent.source.map((line, i) => (
|
||||
<div key={i} className="whitespace-pre-wrap">
|
||||
{line}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</ResizablePanel>
|
||||
<ResizableHandle withHandle />
|
||||
<ResizablePanel defaultSize={50}>
|
||||
<div className="h-full overflow-auto p-6">
|
||||
<div className="prose prose-gray text-white dark:prose-invert max-w-none">
|
||||
{notesTemplate.source.map((line, i) => (
|
||||
<div key={i} className="whitespace-pre-wrap">
|
||||
{line}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</ResizablePanel>
|
||||
</ResizablePanelGroup>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user