From 320a24954d8897d3d1ec750d7111ffc5e86ffa1b Mon Sep 17 00:00:00 2001 From: meirrrr Date: Thu, 19 Jun 2025 13:23:49 +0500 Subject: [PATCH 1/2] init --- api/package-lock.json | 559 ++++++++------ api/package.json | 11 +- api/src/app.controller.spec.ts | 22 - api/src/app.controller.ts | 12 - api/src/app.module.ts | 35 +- api/src/app.service.ts | 8 - api/src/auth/auth.guard.ts | 30 + api/src/auth/auth.module.ts | 10 + api/src/common/utils/token.util.ts | 8 + api/src/likes/likes.controller.ts | 37 + api/src/likes/likes.entity.ts | 23 + api/src/likes/likes.module.ts | 13 + api/src/likes/likes.service.ts | 30 + api/src/main.ts | 4 + api/src/users/users.controller.ts | 30 + api/src/users/users.entity.ts | 18 + api/src/users/users.module.ts | 13 + api/src/users/users.service.ts | 22 + front/package-lock.json | 721 +++++++++++++++++- front/package.json | 6 +- front/src/App.css | 42 - front/src/App.tsx | 60 +- front/src/api/axios.ts | 17 + front/src/api/cats.ts | 41 + front/src/api/likes.ts | 16 + front/src/api/registerUser.ts | 10 + front/src/assets/full_heart.svg | 10 + front/src/assets/outlined_heart.svg | 10 + .../shared/catCard/CatCard.module.css | 34 + .../src/components/shared/catCard/CatCard.tsx | 52 ++ .../shared/catsGrid/CatsGrid.module.css | 7 + .../components/shared/catsGrid/CatsGrid.tsx | 21 + .../shared/notFound/NotFound.module.css | 38 + .../components/shared/notFound/NotFound.tsx | 17 + .../src/components/ui/error/Error.module.css | 25 + front/src/components/ui/error/Error.tsx | 14 + .../ui/skeleton/Skeleton.module.css | 65 ++ front/src/components/ui/skeleton/Skeleton.tsx | 30 + .../features/favorites/Favorite.module.css | 0 front/src/features/favorites/Favorite.tsx | 17 + front/src/features/home/Home.module.css | 0 front/src/features/home/index.tsx | 26 + front/src/index.css | 82 +- front/src/layout/Layout.tsx | 13 + front/src/layout/header/Header.module.css | 76 ++ front/src/layout/header/Header.tsx | 18 + front/src/main.tsx | 25 +- front/src/redux/catSlice.ts | 50 ++ front/src/redux/likesSlice.ts | 101 +++ front/src/routes/index.tsx | 17 + front/src/store/store.ts | 13 + front/src/types/types.ts | 4 + front/vite.config.ts | 6 +- openapi.yaml | 29 +- 54 files changed, 2168 insertions(+), 430 deletions(-) delete mode 100644 api/src/app.controller.spec.ts delete mode 100644 api/src/app.controller.ts delete mode 100644 api/src/app.service.ts create mode 100644 api/src/auth/auth.guard.ts create mode 100644 api/src/auth/auth.module.ts create mode 100644 api/src/common/utils/token.util.ts create mode 100644 api/src/likes/likes.controller.ts create mode 100644 api/src/likes/likes.entity.ts create mode 100644 api/src/likes/likes.module.ts create mode 100644 api/src/likes/likes.service.ts create mode 100644 api/src/users/users.controller.ts create mode 100644 api/src/users/users.entity.ts create mode 100644 api/src/users/users.module.ts create mode 100644 api/src/users/users.service.ts delete mode 100644 front/src/App.css create mode 100644 front/src/api/axios.ts create mode 100644 front/src/api/cats.ts create mode 100644 front/src/api/likes.ts create mode 100644 front/src/api/registerUser.ts create mode 100644 front/src/assets/full_heart.svg create mode 100644 front/src/assets/outlined_heart.svg create mode 100644 front/src/components/shared/catCard/CatCard.module.css create mode 100644 front/src/components/shared/catCard/CatCard.tsx create mode 100644 front/src/components/shared/catsGrid/CatsGrid.module.css create mode 100644 front/src/components/shared/catsGrid/CatsGrid.tsx create mode 100644 front/src/components/shared/notFound/NotFound.module.css create mode 100644 front/src/components/shared/notFound/NotFound.tsx create mode 100644 front/src/components/ui/error/Error.module.css create mode 100644 front/src/components/ui/error/Error.tsx create mode 100644 front/src/components/ui/skeleton/Skeleton.module.css create mode 100644 front/src/components/ui/skeleton/Skeleton.tsx create mode 100644 front/src/features/favorites/Favorite.module.css create mode 100644 front/src/features/favorites/Favorite.tsx create mode 100644 front/src/features/home/Home.module.css create mode 100644 front/src/features/home/index.tsx create mode 100644 front/src/layout/Layout.tsx create mode 100644 front/src/layout/header/Header.module.css create mode 100644 front/src/layout/header/Header.tsx create mode 100644 front/src/redux/catSlice.ts create mode 100644 front/src/redux/likesSlice.ts create mode 100644 front/src/routes/index.tsx create mode 100644 front/src/store/store.ts create mode 100644 front/src/types/types.ts diff --git a/api/package-lock.json b/api/package-lock.json index 42ef14d..e1f18a0 100644 --- a/api/package-lock.json +++ b/api/package-lock.json @@ -9,19 +9,22 @@ "version": "0.0.1", "license": "UNLICENSED", "dependencies": { - "@nestjs/common": "^10.0.0", - "@nestjs/core": "^10.0.0", + "@nestjs/common": "^10.4.19", + "@nestjs/config": "^4.0.2", + "@nestjs/core": "^10.4.19", "@nestjs/platform-express": "^10.0.0", "@nestjs/typeorm": "^10.0.2", - "pg": "^8.12.0", + "bcrypt": "^6.0.0", + "pg": "^8.16.0", "reflect-metadata": "^0.2.0", "rxjs": "^7.8.1", - "typeorm": "^0.3.20" + "typeorm": "^0.3.24" }, "devDependencies": { "@nestjs/cli": "^10.0.0", "@nestjs/schematics": "^10.0.0", "@nestjs/testing": "^10.0.0", + "@types/bcrypt": "^5.0.2", "@types/express": "^4.17.17", "@types/jest": "^29.5.2", "@types/node": "^20.3.1", @@ -1683,12 +1686,14 @@ } }, "node_modules/@nestjs/common": { - "version": "10.3.10", - "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-10.3.10.tgz", - "integrity": "sha512-H8k0jZtxk1IdtErGDmxFRy0PfcOAUg41Prrqpx76DQusGGJjsaovs1zjXVD1rZWaVYchfT1uczJ6L4Kio10VNg==", + "version": "10.4.19", + "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-10.4.19.tgz", + "integrity": "sha512-0TZJ8H+7qtaqZt6YfZJkDRp0e+v6jjo5/pevPAjUy0WYxaTy16bNNQxFPRKLMe/v1hUr2oGV9imvL2477zNt5g==", + "license": "MIT", "dependencies": { + "file-type": "20.4.1", "iterare": "1.2.1", - "tslib": "2.6.3", + "tslib": "2.8.1", "uid": "2.0.2" }, "funding": { @@ -1710,17 +1715,51 @@ } } }, + "node_modules/@nestjs/common/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "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/config/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/@nestjs/core": { - "version": "10.3.10", - "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-10.3.10.tgz", - "integrity": "sha512-ZbQ4jovQyzHtCGCrzK5NdtW1SYO2fHSsgSY1+/9WdruYCUra+JDkWEXgZ4M3Hv480Dl3OXehAmY1wCOojeMyMQ==", + "version": "10.4.19", + "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-10.4.19.tgz", + "integrity": "sha512-gahghu0y4Rn4gn/xPjTgNHFMpUM8TxfhdeMowVWTGVnYMZtGeEGbIXMFhJS0Dce3E4VKyqAglzgO9ecAZd4Ong==", "hasInstallScript": true, + "license": "MIT", "dependencies": { "@nuxtjs/opencollective": "0.3.2", "fast-safe-stringify": "2.1.1", "iterare": "1.2.1", - "path-to-regexp": "3.2.0", - "tslib": "2.6.3", + "path-to-regexp": "3.3.0", + "tslib": "2.8.1", "uid": "2.0.2" }, "funding": { @@ -1747,6 +1786,12 @@ } } }, + "node_modules/@nestjs/core/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, "node_modules/@nestjs/platform-express": { "version": "10.3.10", "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-10.3.10.tgz", @@ -1820,6 +1865,7 @@ "version": "10.0.2", "resolved": "https://registry.npmjs.org/@nestjs/typeorm/-/typeorm-10.0.2.tgz", "integrity": "sha512-H738bJyydK4SQkRCTeh1aFBxoO1E9xdL/HaLGThwrqN95os5mEyAtK7BLADOS+vldP4jDZ2VQPLj4epWwRqCeQ==", + "license": "MIT", "dependencies": { "uuid": "9.0.1" }, @@ -1933,6 +1979,30 @@ "resolved": "https://registry.npmjs.org/@sqltools/formatter/-/formatter-1.2.5.tgz", "integrity": "sha512-Uy0+khmZqUrUGm5dmMqVlnvufZRSK0FbYzVgp0UMstm+F5+W2/jnEEQyc9vo1ZR/E5ZI/B1WjjoTqBqwJL6Krw==" }, + "node_modules/@tokenizer/inflate": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/@tokenizer/inflate/-/inflate-0.2.7.tgz", + "integrity": "sha512-MADQgmZT1eKjp06jpI2yozxaU9uVs4GzzgSL+uEq7bVcJ9V1ZXQkeGNql1fsSI0gMy1vhvNTNbUqrx+pZfJVmg==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "fflate": "^0.8.2", + "token-types": "^6.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/@tokenizer/token": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz", + "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==", + "license": "MIT" + }, "node_modules/@tsconfig/node10": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", @@ -1998,6 +2068,16 @@ "@babel/types": "^7.20.7" } }, + "node_modules/@types/bcrypt": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@types/bcrypt/-/bcrypt-5.0.2.tgz", + "integrity": "sha512-6atioO8Y75fNcbmj0G7UjI9lXN2pQ/IGJ2FWT4a/btd0Lk9lQalHLKhkgKVZ3r+spnmWUKfbMi1GEe9wyHQfNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/body-parser": { "version": "1.19.5", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", @@ -2718,10 +2798,14 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/any-promise": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", - "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==" + "node_modules/ansis": { + "version": "3.17.0", + "resolved": "https://registry.npmjs.org/ansis/-/ansis-3.17.0.tgz", + "integrity": "sha512-0qWUglt9JEqLFr3w1I1pbrChn1grhaiAR2ocX1PP/flRmxgtwTzPFFFnfIlD6aMOLQZgSuCRlidD70lvx8yhzg==", + "license": "ISC", + "engines": { + "node": ">=14" + } }, "node_modules/anymatch": { "version": "3.1.3", @@ -2951,6 +3035,20 @@ } ] }, + "node_modules/bcrypt": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-6.0.0.tgz", + "integrity": "sha512-cU8v/EGSrnH+HnxV2z0J7/blxH8gq7Xh2JFT6Aroax7UohdmiJJlxApMxtKfuI7z68NvvVcmR78k2LbT6efhRg==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "node-addon-api": "^8.3.0", + "node-gyp-build": "^4.8.4" + }, + "engines": { + "node": ">= 18" + } + }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", @@ -3297,77 +3395,6 @@ "node": ">=8" } }, - "node_modules/cli-highlight": { - "version": "2.1.11", - "resolved": "https://registry.npmjs.org/cli-highlight/-/cli-highlight-2.1.11.tgz", - "integrity": "sha512-9KDcoEVwyUXrjcJNvHD0NFc/hiwe/WPVYIleQh2O1N2Zro5gWJZ/K+3DGn8w8P/F6FxOgzyC5bxDyHIgCSPhGg==", - "dependencies": { - "chalk": "^4.0.0", - "highlight.js": "^10.7.1", - "mz": "^2.4.0", - "parse5": "^5.1.1", - "parse5-htmlparser2-tree-adapter": "^6.0.0", - "yargs": "^16.0.0" - }, - "bin": { - "highlight": "bin/highlight" - }, - "engines": { - "node": ">=8.0.0", - "npm": ">=5.0.0" - } - }, - "node_modules/cli-highlight/node_modules/cliui": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", - "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^7.0.0" - } - }, - "node_modules/cli-highlight/node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/cli-highlight/node_modules/yargs": { - "version": "16.2.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", - "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", - "dependencies": { - "cliui": "^7.0.2", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.0", - "y18n": "^5.0.5", - "yargs-parser": "^20.2.2" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/cli-highlight/node_modules/yargs-parser": { - "version": "20.2.9", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", - "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", - "engines": { - "node": ">=10" - } - }, "node_modules/cli-spinners": { "version": "2.9.2", "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", @@ -3673,16 +3700,18 @@ } }, "node_modules/dayjs": { - "version": "1.11.12", - "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.12.tgz", - "integrity": "sha512-Rt2g+nTbLlDWZTwwrIXjy9MeiZmSDI375FvZs72ngxx8PDC6YXOeR3q5LAuPzjZQxhiWdRKac7RKV+YyQYfYIg==" + "version": "1.11.13", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz", + "integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==", + "license": "MIT" }, "node_modules/debug": { - "version": "4.3.5", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", - "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "license": "MIT", "dependencies": { - "ms": "2.1.2" + "ms": "^2.1.3" }, "engines": { "node": ">=6.0" @@ -3694,10 +3723,10 @@ } }, "node_modules/dedent": { - "version": "1.5.3", - "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.5.3.tgz", - "integrity": "sha512-NHQtfOOW68WD8lgypbLA5oT+Bt0xXJhiYvoR6SmmNXZfpzOGXwdKWmcwG8N7PwVVWV3eF/68nmD9BaJSsTBhyQ==", - "dev": true, + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.6.0.tgz", + "integrity": "sha512-F1Z+5UCFpmQUzJa11agbyPVMbpgT/qA3/SKyJ1jyBgm7dUcUEa8v9JwDkerSQXfakBwFljIxhOJqGkjUwZ9FSA==", + "license": "MIT", "peerDependencies": { "babel-plugin-macros": "^3.1.0" }, @@ -3838,9 +3867,25 @@ } }, "node_modules/dotenv": { - "version": "16.4.5", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", - "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", + "version": "16.5.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.5.0.tgz", + "integrity": "sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==", + "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" }, @@ -4436,6 +4481,12 @@ "bser": "2.1.1" } }, + "node_modules/fflate": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", + "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==", + "license": "MIT" + }, "node_modules/figures": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", @@ -4472,6 +4523,24 @@ "node": "^10.12.0 || >=12.0.0" } }, + "node_modules/file-type": { + "version": "20.4.1", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-20.4.1.tgz", + "integrity": "sha512-hw9gNZXUfZ02Jo0uafWLaFVPter5/k2rfcrjFJJHX/77xtSDOfJuEFb6oKlFV86FLP1SuyHMW1PSk0U9M5tKkQ==", + "license": "MIT", + "dependencies": { + "@tokenizer/inflate": "^0.2.6", + "strtok3": "^10.2.0", + "token-types": "^6.0.0", + "uint8array-extras": "^1.4.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sindresorhus/file-type?sponsor=1" + } + }, "node_modules/filelist": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", @@ -4788,6 +4857,7 @@ "version": "10.4.2", "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.2.tgz", "integrity": "sha512-GwMlUF6PkPo3Gk21UxkCohOv0PLcIXVtKyLlpEI28R/cO/4eNOdmLk3CMW1wROV/WR/EsZOWAfBbBOqYvs88/w==", + "dev": true, "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", @@ -4952,14 +5022,6 @@ "node": ">=8" } }, - "node_modules/highlight.js": { - "version": "10.7.3", - "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz", - "integrity": "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==", - "engines": { - "node": "*" - } - }, "node_modules/html-escaper": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", @@ -6221,8 +6283,7 @@ "node_modules/lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, "node_modules/lodash.memoize": { "version": "4.1.2", @@ -6457,9 +6518,10 @@ } }, "node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" }, "node_modules/multer": { "version": "1.4.4-lts.1", @@ -6484,16 +6546,6 @@ "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", "dev": true }, - "node_modules/mz": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", - "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", - "dependencies": { - "any-promise": "^1.0.0", - "object-assign": "^4.0.1", - "thenify-all": "^1.0.0" - } - }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -6520,6 +6572,15 @@ "integrity": "sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==", "dev": true }, + "node_modules/node-addon-api": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.4.0.tgz", + "integrity": "sha512-D9DI/gXHvVmjHS08SVch0Em8G5S1P+QWtU31appcKT/8wFSPRcdHadIFSAntdMMVM5zz+/DL+bL/gz3UDppqtg==", + "license": "MIT", + "engines": { + "node": "^18 || ^20 || >= 21" + } + }, "node_modules/node-emoji": { "version": "1.11.0", "resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-1.11.0.tgz", @@ -6548,6 +6609,17 @@ } } }, + "node_modules/node-gyp-build": { + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", + "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", + "license": "MIT", + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, "node_modules/node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", @@ -6758,24 +6830,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/parse5": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-5.1.1.tgz", - "integrity": "sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug==" - }, - "node_modules/parse5-htmlparser2-tree-adapter": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-6.0.1.tgz", - "integrity": "sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA==", - "dependencies": { - "parse5": "^6.0.1" - } - }, - "node_modules/parse5-htmlparser2-tree-adapter/node_modules/parse5": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", - "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==" - }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -6837,9 +6891,10 @@ "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==" }, "node_modules/path-to-regexp": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-3.2.0.tgz", - "integrity": "sha512-jczvQbCUS7XmS7o+y1aEO9OBVFeZBQ1MDSEqmO7xSoPgOPoowY/SxLpZ6Vh97/8qHZOteiCKb7gkG9gA2ZUxJA==" + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-3.3.0.tgz", + "integrity": "sha512-qyCH421YQPS2WFDxDjftfc1ZR5WKQzVzqsp4n9M2kQhVOo/ByahFoUNJfl58kOcEGfQ//7weFTDhm+ss8Ecxgw==", + "license": "MIT" }, "node_modules/path-type": { "version": "4.0.0", @@ -6851,21 +6906,22 @@ } }, "node_modules/pg": { - "version": "8.12.0", - "resolved": "https://registry.npmjs.org/pg/-/pg-8.12.0.tgz", - "integrity": "sha512-A+LHUSnwnxrnL/tZ+OLfqR1SxLN3c/pgDztZ47Rpbsd4jUytsTtwQo/TLPRzPJMp/1pbhYVhH9cuSZLAajNfjQ==", + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.16.0.tgz", + "integrity": "sha512-7SKfdvP8CTNXjMUzfcVTaI+TDzBEeaUnVwiVGZQD1Hh33Kpev7liQba9uLd4CfN8r9mCVsD0JIpq03+Unpz+kg==", + "license": "MIT", "dependencies": { - "pg-connection-string": "^2.6.4", - "pg-pool": "^3.6.2", - "pg-protocol": "^1.6.1", - "pg-types": "^2.1.0", - "pgpass": "1.x" + "pg-connection-string": "^2.9.0", + "pg-pool": "^3.10.0", + "pg-protocol": "^1.10.0", + "pg-types": "2.2.0", + "pgpass": "1.0.5" }, "engines": { "node": ">= 8.0.0" }, "optionalDependencies": { - "pg-cloudflare": "^1.1.1" + "pg-cloudflare": "^1.2.5" }, "peerDependencies": { "pg-native": ">=3.0.1" @@ -6877,15 +6933,17 @@ } }, "node_modules/pg-cloudflare": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.1.1.tgz", - "integrity": "sha512-xWPagP/4B6BgFO+EKz3JONXv3YDgvkbVrGw2mTo3D6tVDQRh1e7cqVGvyR3BE+eQgAvx1XhW/iEASj4/jCWl3Q==", + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.2.5.tgz", + "integrity": "sha512-OOX22Vt0vOSRrdoUPKJ8Wi2OpE/o/h9T8X1s4qSkCedbNah9ei2W2765be8iMVxQUsvgT7zIAT2eIa9fs5+vtg==", + "license": "MIT", "optional": true }, "node_modules/pg-connection-string": { - "version": "2.6.4", - "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.6.4.tgz", - "integrity": "sha512-v+Z7W/0EO707aNMaAEfiGnGL9sxxumwLl2fJvCQtMn9Fxsg+lPpPkdcyBSv/KFgpGdYkMfn+EI1Or2EHjpgLCA==" + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.9.0.tgz", + "integrity": "sha512-P2DEBKuvh5RClafLngkAuGe9OUlFV7ebu8w1kmaaOgPcpJd1RIFh7otETfI6hAR8YupOLFTY7nuvvIn7PLciUQ==", + "license": "MIT" }, "node_modules/pg-int8": { "version": "1.0.1", @@ -6896,17 +6954,19 @@ } }, "node_modules/pg-pool": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.6.2.tgz", - "integrity": "sha512-Htjbg8BlwXqSBQ9V8Vjtc+vzf/6fVUuak/3/XXKA9oxZprwW3IMDQTGHP+KDmVL7rtd+R1QjbnCFPuTHm3G4hg==", + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.10.0.tgz", + "integrity": "sha512-DzZ26On4sQ0KmqnO34muPcmKbhrjmyiO4lCCR0VwEd7MjmiKf5NTg/6+apUEu0NF7ESa37CGzFxH513CoUmWnA==", + "license": "MIT", "peerDependencies": { "pg": ">=8.0" } }, "node_modules/pg-protocol": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.6.1.tgz", - "integrity": "sha512-jPIlvgoD63hrEuihvIg+tJhoGjUsLPn6poJY9N5CnlPd91c2T18T/9zBtLxZSb1EhYxBRoZJtzScCaWlYLtktg==" + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.10.0.tgz", + "integrity": "sha512-IpdytjudNuLv8nhlHs/UrVBhU0e78J0oIS/0AVdTbWxSOkFUVdsHC/NrorO6nXsQNDTT1kzDSOMJubBQviX18Q==", + "license": "MIT" }, "node_modules/pg-types": { "version": "2.2.0", @@ -7633,11 +7693,6 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, - "node_modules/send/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" - }, "node_modules/serialize-javascript": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", @@ -7798,6 +7853,22 @@ "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", "dev": true }, + "node_modules/sql-highlight": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/sql-highlight/-/sql-highlight-6.1.0.tgz", + "integrity": "sha512-ed7OK4e9ywpE7pgRMkMQmZDPKSVdm0oX5IEtZiKnFucSF0zu6c80GZBe38UqHuVhTWJ9xsKgSMjCG2bml86KvA==", + "funding": [ + "https://github.com/scriptcoded/sql-highlight?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/scriptcoded" + } + ], + "license": "MIT", + "engines": { + "node": ">=14" + } + }, "node_modules/stack-utils": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", @@ -7941,6 +8012,22 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/strtok3": { + "version": "10.3.1", + "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-10.3.1.tgz", + "integrity": "sha512-3JWEZM6mfix/GCJBBUrkA8p2Id2pBkyTkVCJKto55w080QBKZ+8R171fGrbiSp+yMO/u6F8/yUh7K4V9K+YCnw==", + "license": "MIT", + "dependencies": { + "@tokenizer/token": "^0.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, "node_modules/superagent": { "version": "9.0.2", "resolved": "https://registry.npmjs.org/superagent/-/superagent-9.0.2.tgz", @@ -8193,25 +8280,6 @@ "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", "dev": true }, - "node_modules/thenify": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", - "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", - "dependencies": { - "any-promise": "^1.0.0" - } - }, - "node_modules/thenify-all": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", - "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", - "dependencies": { - "thenify": ">= 3.1.0 < 4" - }, - "engines": { - "node": ">=0.8" - } - }, "node_modules/through": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", @@ -8265,6 +8333,23 @@ "node": ">=0.6" } }, + "node_modules/token-types": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/token-types/-/token-types-6.0.0.tgz", + "integrity": "sha512-lbDrTLVsHhOMljPscd0yitpozq7Ga2M5Cvez5AjGg8GASBjtt6iERCAJ93yommPmz62fb45oFIXHEZ3u9bfJEA==", + "license": "MIT", + "dependencies": { + "@tokenizer/token": "^0.3.0", + "ieee754": "^1.2.1" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, "node_modules/tr46": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", @@ -8495,25 +8580,25 @@ "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==" }, "node_modules/typeorm": { - "version": "0.3.20", - "resolved": "https://registry.npmjs.org/typeorm/-/typeorm-0.3.20.tgz", - "integrity": "sha512-sJ0T08dV5eoZroaq9uPKBoNcGslHBR4E4y+EBHs//SiGbblGe7IeduP/IH4ddCcj0qp3PHwDwGnuvqEAnKlq/Q==", + "version": "0.3.24", + "resolved": "https://registry.npmjs.org/typeorm/-/typeorm-0.3.24.tgz", + "integrity": "sha512-4IrHG7A0tY8l5gEGXfW56VOMfUVWEkWlH/h5wmcyZ+V8oCiLj7iTPp0lEjMEZVrxEkGSdP9ErgTKHKXQApl/oA==", + "license": "MIT", "dependencies": { "@sqltools/formatter": "^1.2.5", + "ansis": "^3.17.0", "app-root-path": "^3.1.0", "buffer": "^6.0.3", - "chalk": "^4.1.2", - "cli-highlight": "^2.1.11", - "dayjs": "^1.11.9", - "debug": "^4.3.4", - "dotenv": "^16.0.3", - "glob": "^10.3.10", - "mkdirp": "^2.1.3", - "reflect-metadata": "^0.2.1", + "dayjs": "^1.11.13", + "debug": "^4.4.0", + "dedent": "^1.6.0", + "dotenv": "^16.4.7", + "glob": "^10.4.5", "sha.js": "^2.4.11", - "tslib": "^2.5.0", - "uuid": "^9.0.0", - "yargs": "^17.6.2" + "sql-highlight": "^6.0.0", + "tslib": "^2.8.1", + "uuid": "^11.1.0", + "yargs": "^17.7.2" }, "bin": { "typeorm": "cli.js", @@ -8527,23 +8612,24 @@ "url": "https://opencollective.com/typeorm" }, "peerDependencies": { - "@google-cloud/spanner": "^5.18.0", + "@google-cloud/spanner": "^5.18.0 || ^6.0.0 || ^7.0.0", "@sap/hana-client": "^2.12.25", - "better-sqlite3": "^7.1.2 || ^8.0.0 || ^9.0.0", + "better-sqlite3": "^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0", "hdb-pool": "^0.1.6", "ioredis": "^5.0.4", - "mongodb": "^5.8.0", - "mssql": "^9.1.1 || ^10.0.1", + "mongodb": "^5.8.0 || ^6.0.0", + "mssql": "^9.1.1 || ^10.0.1 || ^11.0.1", "mysql2": "^2.2.5 || ^3.0.1", "oracledb": "^6.3.0", "pg": "^8.5.1", "pg-native": "^3.0.0", "pg-query-stream": "^4.0.0", "redis": "^3.1.1 || ^4.0.0", + "reflect-metadata": "^0.1.14 || ^0.2.0", "sql.js": "^1.4.0", "sqlite3": "^5.0.3", "ts-node": "^10.7.0", - "typeorm-aurora-data-api-driver": "^2.0.0" + "typeorm-aurora-data-api-driver": "^2.0.0 || ^3.0.0" }, "peerDependenciesMeta": { "@google-cloud/spanner": { @@ -8622,20 +8708,45 @@ "ieee754": "^1.2.1" } }, - "node_modules/typeorm/node_modules/mkdirp": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-2.1.6.tgz", - "integrity": "sha512-+hEnITedc8LAtIP9u3HJDFIdcLV2vXP33sqLLIzkv1Db1zO/1OxbvYf0Y1OC/S/Qo5dxHXepofhmxL02PsKe+A==", - "bin": { - "mkdirp": "dist/cjs/src/bin.js" + "node_modules/typeorm/node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" }, - "engines": { - "node": ">=10" + "bin": { + "glob": "dist/esm/bin.mjs" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/typeorm/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/typeorm/node_modules/uuid": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", + "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/esm/bin/uuid" + } + }, "node_modules/typescript": { "version": "5.5.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.3.tgz", @@ -8660,6 +8771,18 @@ "node": ">=8" } }, + "node_modules/uint8array-extras": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/uint8array-extras/-/uint8array-extras-1.4.0.tgz", + "integrity": "sha512-ZPtzy0hu4cZjv3z5NW9gfKnNLjoz4y6uv4HlelAjDK7sY/xOkKZv9xK/WQpcsBB3jEybChz9DPC2U/+cusjJVQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/undici-types": { "version": "5.26.5", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", diff --git a/api/package.json b/api/package.json index 491568b..bdd5d9f 100644 --- a/api/package.json +++ b/api/package.json @@ -20,19 +20,22 @@ "test:e2e": "jest --config ./test/jest-e2e.json" }, "dependencies": { - "@nestjs/common": "^10.0.0", - "@nestjs/core": "^10.0.0", + "@nestjs/common": "^10.4.19", + "@nestjs/config": "^4.0.2", + "@nestjs/core": "^10.4.19", "@nestjs/platform-express": "^10.0.0", "@nestjs/typeorm": "^10.0.2", - "pg": "^8.12.0", + "bcrypt": "^6.0.0", + "pg": "^8.16.0", "reflect-metadata": "^0.2.0", "rxjs": "^7.8.1", - "typeorm": "^0.3.20" + "typeorm": "^0.3.24" }, "devDependencies": { "@nestjs/cli": "^10.0.0", "@nestjs/schematics": "^10.0.0", "@nestjs/testing": "^10.0.0", + "@types/bcrypt": "^5.0.2", "@types/express": "^4.17.17", "@types/jest": "^29.5.2", "@types/node": "^20.3.1", diff --git a/api/src/app.controller.spec.ts b/api/src/app.controller.spec.ts deleted file mode 100644 index d22f389..0000000 --- a/api/src/app.controller.spec.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { AppController } from './app.controller'; -import { AppService } from './app.service'; - -describe('AppController', () => { - let appController: AppController; - - beforeEach(async () => { - const app: TestingModule = await Test.createTestingModule({ - controllers: [AppController], - providers: [AppService], - }).compile(); - - appController = app.get(AppController); - }); - - describe('root', () => { - it('should return "Hello World!"', () => { - expect(appController.getHello()).toBe('Hello World!'); - }); - }); -}); diff --git a/api/src/app.controller.ts b/api/src/app.controller.ts deleted file mode 100644 index cce879e..0000000 --- a/api/src/app.controller.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { Controller, Get } from '@nestjs/common'; -import { AppService } from './app.service'; - -@Controller() -export class AppController { - constructor(private readonly appService: AppService) {} - - @Get() - getHello(): string { - return this.appService.getHello(); - } -} diff --git a/api/src/app.module.ts b/api/src/app.module.ts index 8662803..70acaac 100644 --- a/api/src/app.module.ts +++ b/api/src/app.module.ts @@ -1,10 +1,35 @@ import { Module } from '@nestjs/common'; -import { AppController } from './app.controller'; -import { AppService } from './app.service'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { ConfigModule } from '@nestjs/config'; +import { UsersModule } from './users/users.module'; +import { AuthModule } from './auth/auth.module'; +import { LikesModule } from './likes/likes.module'; + +// type: 'postgres', +// host: 'cat-pinterest-api-pg', +// port: 5432, +// username: 'postgres', +// password: '1', +// database: 'support_lk_db', +// synchronize: true, +// autoLoadEntities: true, @Module({ - imports: [], - controllers: [AppController], - providers: [AppService], + imports: [ + ConfigModule.forRoot({ isGlobal: true }), + TypeOrmModule.forRoot({ + type: 'postgres', + host: 'localhost', + port: 5432, + username: 'postgres', + password: '2004', + database: 'cat_service', + synchronize: true, + autoLoadEntities: true, + }), + UsersModule, + AuthModule, + LikesModule, + ], }) export class AppModule {} diff --git a/api/src/app.service.ts b/api/src/app.service.ts deleted file mode 100644 index 927d7cc..0000000 --- a/api/src/app.service.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { Injectable } from '@nestjs/common'; - -@Injectable() -export class AppService { - getHello(): string { - return 'Hello World!'; - } -} diff --git a/api/src/auth/auth.guard.ts b/api/src/auth/auth.guard.ts new file mode 100644 index 0000000..e43472e --- /dev/null +++ b/api/src/auth/auth.guard.ts @@ -0,0 +1,30 @@ +import { + Injectable, + CanActivate, + ExecutionContext, + UnauthorizedException, +} from '@nestjs/common'; + +import { Request } from 'express'; +import { UsersService } from '../users/users.service'; +import { generateToken } from '../common/utils/token.util'; + +const SECRET_SALT = 'supersecretsalt'; + +@Injectable() +export class AuthGuard implements CanActivate { + constructor(private readonly usersService: UsersService) {} + + async canActivate(context: ExecutionContext): Promise { + const req = context.switchToHttp().getRequest(); + const token = req.headers.authorization?.replace('Bearer ', ''); + if (!token) throw new UnauthorizedException(); + + const users = await this.usersService.findAll(); + const user = users.find((u) => generateToken(u.id, SECRET_SALT) === token); + if (!user) throw new UnauthorizedException(); + + (req as any).user = user; + return true; + } +} diff --git a/api/src/auth/auth.module.ts b/api/src/auth/auth.module.ts new file mode 100644 index 0000000..43a10e6 --- /dev/null +++ b/api/src/auth/auth.module.ts @@ -0,0 +1,10 @@ +import { Module } from '@nestjs/common'; +import { AuthGuard } from './auth.guard'; +import { UsersModule } from '../users/users.module'; + +@Module({ + imports: [UsersModule], + providers: [AuthGuard], + exports: [AuthGuard], +}) +export class AuthModule {} diff --git a/api/src/common/utils/token.util.ts b/api/src/common/utils/token.util.ts new file mode 100644 index 0000000..0787afc --- /dev/null +++ b/api/src/common/utils/token.util.ts @@ -0,0 +1,8 @@ +import * as crypto from 'crypto'; + +export function generateToken(userId: string, salt: string): string { + return crypto + .createHash('sha256') + .update(userId + salt) + .digest('hex'); +} diff --git a/api/src/likes/likes.controller.ts b/api/src/likes/likes.controller.ts new file mode 100644 index 0000000..3eae35c --- /dev/null +++ b/api/src/likes/likes.controller.ts @@ -0,0 +1,37 @@ +import { + Controller, + Get, + Post, + Delete, + Param, + Body, + Req, + UseGuards, + NotFoundException, +} from '@nestjs/common'; + +import { LikesService } from './likes.service'; +import { AuthGuard } from '../auth/auth.guard'; + +@Controller('likes') +@UseGuards(AuthGuard) +export class LikesController { + constructor(private readonly likesService: LikesService) {} + + @Get() + async list(@Req() req) { + return { data: await this.likesService.findByUser(req.user) }; + } + + @Post() + async create(@Body() body: { cat_id: string }, @Req() req) { + return await this.likesService.createLike(body.cat_id, req.user); + } + + @Delete(':cat_id') + async remove(@Param('cat_id') cat_id: string, @Req() req) { + const like = await this.likesService.findByCatId(req.user, cat_id); + if (!like) throw new NotFoundException(); + return await this.likesService.deleteLike(like); + } +} diff --git a/api/src/likes/likes.entity.ts b/api/src/likes/likes.entity.ts new file mode 100644 index 0000000..9c0613b --- /dev/null +++ b/api/src/likes/likes.entity.ts @@ -0,0 +1,23 @@ +import { + Entity, + PrimaryGeneratedColumn, + Column, + ManyToOne, + CreateDateColumn, +} from 'typeorm'; +import { User } from '../users/users.entity'; + +@Entity() +export class Like { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column() + cat_id: string; + + @CreateDateColumn() + created_at: Date; + + @ManyToOne(() => User, (user) => user.likes, { onDelete: 'CASCADE' }) + user: User; +} diff --git a/api/src/likes/likes.module.ts b/api/src/likes/likes.module.ts new file mode 100644 index 0000000..8643251 --- /dev/null +++ b/api/src/likes/likes.module.ts @@ -0,0 +1,13 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { Like } from './likes.entity'; +import { LikesService } from './likes.service'; +import { LikesController } from './likes.controller'; +import { UsersModule } from '../users/users.module'; + +@Module({ + imports: [TypeOrmModule.forFeature([Like]), UsersModule], + providers: [LikesService], + controllers: [LikesController], +}) +export class LikesModule {} diff --git a/api/src/likes/likes.service.ts b/api/src/likes/likes.service.ts new file mode 100644 index 0000000..de1aaa1 --- /dev/null +++ b/api/src/likes/likes.service.ts @@ -0,0 +1,30 @@ +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Like } from './likes.entity'; +import { Repository } from 'typeorm'; +import { User } from '../users/users.entity'; + +@Injectable() +export class LikesService { + constructor(@InjectRepository(Like) private repo: Repository) {} + + findByUser(user: User) { + return this.repo.find({ where: { user } }); + } + + async createLike(cat_id: string, user: User) { + const existing = await this.repo.findOne({ where: { cat_id, user } }); + if (existing) throw new Error('Already liked'); + + const like = this.repo.create({ cat_id, user }); + return this.repo.save(like); + } + + findByCatId(user: User, cat_id: string) { + return this.repo.findOne({ where: { cat_id, user } }); + } + + deleteLike(like: Like) { + return this.repo.remove(like); + } +} diff --git a/api/src/main.ts b/api/src/main.ts index 13cad38..106e4dd 100644 --- a/api/src/main.ts +++ b/api/src/main.ts @@ -3,6 +3,10 @@ import { AppModule } from './app.module'; async function bootstrap() { const app = await NestFactory.create(AppModule); + app.enableCors({ + origin: '*', + exposedHeaders: ['X-Auth-Token'], + }); await app.listen(3000); } bootstrap(); diff --git a/api/src/users/users.controller.ts b/api/src/users/users.controller.ts new file mode 100644 index 0000000..9b990f1 --- /dev/null +++ b/api/src/users/users.controller.ts @@ -0,0 +1,30 @@ +import { Controller, Post, Body, Res } from '@nestjs/common'; +import { UsersService } from './users.service'; +import { Response } from 'express'; +import * as bcrypt from 'bcrypt'; +import { generateToken } from '../common/utils/token.util'; + +const SECRET_SALT = 'supersecretsalt'; + +@Controller('user') +export class UsersController { + constructor(private readonly usersService: UsersService) {} + + @Post() + async register( + @Body() body: { login: string; password: string }, + @Res() res: Response, + ) { + const { login, password } = body; + if (!login || !password) return res.status(405).send('Invalid input'); + + const existing = await this.usersService.findByLogin(login); + if (existing) return res.status(405).send('User already exists'); + + const hash = await bcrypt.hash(password, 10); + const user = await this.usersService.createUser(login, hash); + const token = generateToken(user.id, SECRET_SALT!); + + return res.status(201).set('X-Auth-Token', token).json(user); + } +} diff --git a/api/src/users/users.entity.ts b/api/src/users/users.entity.ts new file mode 100644 index 0000000..e94173d --- /dev/null +++ b/api/src/users/users.entity.ts @@ -0,0 +1,18 @@ +import { Entity, PrimaryGeneratedColumn, Column, OneToMany } from 'typeorm'; + +import { Like } from '../likes/likes.entity'; + +@Entity() +export class User { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ unique: true }) + login: string; + + @Column() + password: string; + + @OneToMany(() => Like, (like) => like.user) + likes: Like[]; +} diff --git a/api/src/users/users.module.ts b/api/src/users/users.module.ts new file mode 100644 index 0000000..a06cb4d --- /dev/null +++ b/api/src/users/users.module.ts @@ -0,0 +1,13 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { User } from './users.entity'; +import { UsersService } from './users.service'; +import { UsersController } from './users.controller'; + +@Module({ + imports: [TypeOrmModule.forFeature([User])], + providers: [UsersService], + controllers: [UsersController], + exports: [UsersService], +}) +export class UsersModule {} diff --git a/api/src/users/users.service.ts b/api/src/users/users.service.ts new file mode 100644 index 0000000..43e2ffb --- /dev/null +++ b/api/src/users/users.service.ts @@ -0,0 +1,22 @@ +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { User } from './users.entity'; +import { Repository } from 'typeorm'; + +@Injectable() +export class UsersService { + constructor(@InjectRepository(User) private repo: Repository) {} + + findAll() { + return this.repo.find(); + } + + findByLogin(login: string) { + return this.repo.findOne({ where: { login } }); + } + + createUser(login: string, password: string) { + const user = this.repo.create({ login, password }); + return this.repo.save(user); + } +} diff --git a/front/package-lock.json b/front/package-lock.json index 26790f0..6219c5d 100644 --- a/front/package-lock.json +++ b/front/package-lock.json @@ -8,8 +8,12 @@ "name": "client", "version": "0.0.0", "dependencies": { + "@reduxjs/toolkit": "^2.8.2", + "axios": "^1.10.0", "react": "^18.2.0", - "react-dom": "^18.2.0" + "react-dom": "^18.2.0", + "react-redux": "^9.2.0", + "react-router-dom": "^7.6.2" }, "devDependencies": { "@types/react": "^18.2.15", @@ -972,6 +976,44 @@ "node": ">= 8" } }, + "node_modules/@reduxjs/toolkit": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.8.2.tgz", + "integrity": "sha512-MYlOhQ0sLdw4ud48FoC5w0dH9VfWQjtCjreKwYTT3l+r427qYC5Y8PihNutepr8XrNaBUDQo9khWUwQxZaqt5A==", + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@standard-schema/utils": "^0.3.0", + "immer": "^10.0.3", + "redux": "^5.0.1", + "redux-thunk": "^3.1.0", + "reselect": "^5.1.0" + }, + "peerDependencies": { + "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", + "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-redux": { + "optional": true + } + } + }, + "node_modules/@standard-schema/spec": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz", + "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==", + "license": "MIT" + }, + "node_modules/@standard-schema/utils": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz", + "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", + "license": "MIT" + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -1023,13 +1065,13 @@ "version": "15.7.12", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.12.tgz", "integrity": "sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==", - "dev": true + "devOptional": true }, "node_modules/@types/react": { "version": "18.3.3", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.3.tgz", "integrity": "sha512-hti/R0pS0q1/xx+TsI73XIqk26eBsISZ2R0wUijXIngRK9R/e7Xw/cXVxQK7R5JjW+SV4zGcn5hXjudkN/pLIw==", - "dev": true, + "devOptional": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.0.2" @@ -1050,6 +1092,12 @@ "integrity": "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==", "dev": true }, + "node_modules/@types/use-sync-external-store": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", + "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", + "license": "MIT" + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "6.21.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.21.0.tgz", @@ -1338,6 +1386,23 @@ "node": ">=8" } }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.10.0.tgz", + "integrity": "sha512-/1xYAC4MP/HEG+3duIhFr4ZQXR4sQXOIe+o6sdqzeykGLx6Upp/1p8MHqhINOvGeP7xyNHe7tsiJByc4SSVUxw==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -1397,6 +1462,19 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "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", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -1455,6 +1533,18 @@ "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", "dev": true }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -1467,6 +1557,15 @@ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", "dev": true }, + "node_modules/cookie": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz", + "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -1485,7 +1584,7 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "dev": true + "devOptional": true }, "node_modules/debug": { "version": "4.3.5", @@ -1510,6 +1609,27 @@ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "dev": true }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/detect-libc": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", + "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "peer": true, + "engines": { + "node": ">=8" + } + }, "node_modules/dir-glob": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", @@ -1534,12 +1654,71 @@ "node": ">=6.0.0" } }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/electron-to-chromium": { "version": "1.4.830", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.830.tgz", "integrity": "sha512-TrPKKH20HeN0J1LHzsYLs2qwXrp8TF4nHdu4sq61ozGbzMpWhI7iIOPYPPkxeq1azMT9PZ8enPFcftbs/Npcjg==", "dev": true }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/esbuild": { "version": "0.18.20", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz", @@ -1992,6 +2171,42 @@ "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", "dev": true }, + "node_modules/follow-redirects": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.3.tgz", + "integrity": "sha512-qsITQPfmvMOSAdeyZ+12I1c+CKSstAFAwu+97zrnWAbIr5u8wfsExUzCesVLC8NgHuRUqNN4Zy6UPWUTRGslcA==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -2012,6 +2227,15 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -2021,6 +2245,43 @@ "node": ">=6.9.0" } }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -2105,6 +2366,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/graphemer": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", @@ -2120,6 +2393,45 @@ "node": ">=4" } }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/ignore": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", @@ -2129,6 +2441,16 @@ "node": ">= 4" } }, + "node_modules/immer": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/immer/-/immer-10.1.1.tgz", + "integrity": "sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, "node_modules/import-fresh": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", @@ -2297,6 +2619,257 @@ "node": ">= 0.8.0" } }, + "node_modules/lightningcss": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.1.tgz", + "integrity": "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==", + "dev": true, + "license": "MPL-2.0", + "optional": true, + "peer": true, + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-darwin-arm64": "1.30.1", + "lightningcss-darwin-x64": "1.30.1", + "lightningcss-freebsd-x64": "1.30.1", + "lightningcss-linux-arm-gnueabihf": "1.30.1", + "lightningcss-linux-arm64-gnu": "1.30.1", + "lightningcss-linux-arm64-musl": "1.30.1", + "lightningcss-linux-x64-gnu": "1.30.1", + "lightningcss-linux-x64-musl": "1.30.1", + "lightningcss-win32-arm64-msvc": "1.30.1", + "lightningcss-win32-x64-msvc": "1.30.1" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.1.tgz", + "integrity": "sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.1.tgz", + "integrity": "sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.1.tgz", + "integrity": "sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.1.tgz", + "integrity": "sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.1.tgz", + "integrity": "sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.1.tgz", + "integrity": "sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.1.tgz", + "integrity": "sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.1.tgz", + "integrity": "sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.1.tgz", + "integrity": "sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.1.tgz", + "integrity": "sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -2338,6 +2911,15 @@ "yallist": "^3.0.2" } }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -2360,6 +2942,27 @@ "node": ">=8.6" } }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/minimatch": { "version": "9.0.3", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", @@ -2570,6 +3173,12 @@ "node": ">= 0.8.0" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -2622,6 +3231,29 @@ "react": "^18.3.1" } }, + "node_modules/react-redux": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", + "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", + "license": "MIT", + "dependencies": { + "@types/use-sync-external-store": "^0.0.6", + "use-sync-external-store": "^1.4.0" + }, + "peerDependencies": { + "@types/react": "^18.2.25 || ^19", + "react": "^18.0 || ^19", + "redux": "^5.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "redux": { + "optional": true + } + } + }, "node_modules/react-refresh": { "version": "0.14.2", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz", @@ -2631,6 +3263,65 @@ "node": ">=0.10.0" } }, + "node_modules/react-router": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.6.2.tgz", + "integrity": "sha512-U7Nv3y+bMimgWjhlT5CRdzHPu2/KVmqPwKUCChW8en5P3znxUqwlYFlbmyj8Rgp1SF6zs5X4+77kBVknkg6a0w==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router-dom": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.6.2.tgz", + "integrity": "sha512-Q8zb6VlTbdYKK5JJBLQEN06oTUa/RAbG/oQS1auK1I0TbJOXktqm+QENEVJU6QvWynlXPRBXI3fiOQcSEA78rA==", + "license": "MIT", + "dependencies": { + "react-router": "7.6.2" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, + "node_modules/redux": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", + "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", + "license": "MIT" + }, + "node_modules/redux-thunk": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz", + "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==", + "license": "MIT", + "peerDependencies": { + "redux": "^5.0.0" + } + }, + "node_modules/reselect": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz", + "integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==", + "license": "MIT" + }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", @@ -2725,6 +3416,12 @@ "node": ">=10" } }, + "node_modules/set-cookie-parser": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz", + "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==", + "license": "MIT" + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -2756,10 +3453,11 @@ } }, "node_modules/source-map-js": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", - "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", "dev": true, + "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" } @@ -2915,6 +3613,15 @@ "punycode": "^2.1.0" } }, + "node_modules/use-sync-external-store": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz", + "integrity": "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/vite": { "version": "4.5.3", "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.3.tgz", diff --git a/front/package.json b/front/package.json index 2cd3b5e..d559e65 100644 --- a/front/package.json +++ b/front/package.json @@ -10,8 +10,12 @@ "preview": "vite preview" }, "dependencies": { + "@reduxjs/toolkit": "^2.8.2", + "axios": "^1.10.0", "react": "^18.2.0", - "react-dom": "^18.2.0" + "react-dom": "^18.2.0", + "react-redux": "^9.2.0", + "react-router-dom": "^7.6.2" }, "devDependencies": { "@types/react": "^18.2.15", diff --git a/front/src/App.css b/front/src/App.css deleted file mode 100644 index b9d355d..0000000 --- a/front/src/App.css +++ /dev/null @@ -1,42 +0,0 @@ -#root { - max-width: 1280px; - margin: 0 auto; - padding: 2rem; - text-align: center; -} - -.logo { - height: 6em; - padding: 1.5em; - will-change: filter; - transition: filter 300ms; -} -.logo:hover { - filter: drop-shadow(0 0 2em #646cffaa); -} -.logo.react:hover { - filter: drop-shadow(0 0 2em #61dafbaa); -} - -@keyframes logo-spin { - from { - transform: rotate(0deg); - } - to { - transform: rotate(360deg); - } -} - -@media (prefers-reduced-motion: no-preference) { - a:nth-of-type(2) .logo { - animation: logo-spin infinite 20s linear; - } -} - -.card { - padding: 2em; -} - -.read-the-docs { - color: #888; -} diff --git a/front/src/App.tsx b/front/src/App.tsx index afe48ac..a77226a 100644 --- a/front/src/App.tsx +++ b/front/src/App.tsx @@ -1,35 +1,33 @@ -import { useState } from 'react' -import reactLogo from './assets/react.svg' -import viteLogo from '/vite.svg' -import './App.css' +import { useEffect } from "react"; +import { useDispatch } from "react-redux"; -function App() { - const [count, setCount] = useState(0) +import { AppDispatch } from "./store/store"; +import AppRoutes from "./routes"; + +import { fetchCats } from "./redux/catSlice"; +import { fetchLikes } from "./redux/likesSlice"; + +import { registerUser } from "./api/registerUser"; + +export default function App() { + const dispatch = useDispatch(); + + useEffect(() => { + const token = localStorage.getItem("auth_token"); + if (!token) { + const randomLogin = "user_" + Date.now(); + registerUser(randomLogin, "123456") + .then(() => console.log("User registered")) + .catch((err) => console.error("Failed to register user:", err)); + } + + dispatch(fetchCats(15)); + dispatch(fetchLikes()); + }, [dispatch]); return ( - <> - -

Vite + React

-
- -

- Edit src/App.tsx and save to test HMR -

-
-

- Click on the Vite and React logos to learn more -

- - ) +
+ +
+ ); } - -export default App diff --git a/front/src/api/axios.ts b/front/src/api/axios.ts new file mode 100644 index 0000000..0ad331e --- /dev/null +++ b/front/src/api/axios.ts @@ -0,0 +1,17 @@ +import axios from "axios"; + +export const catsApiInstance = axios.create({ + baseURL: "https://api.thecatapi.com/v1", +}); + +export const axiosInstance = axios.create({ + baseURL: "http://localhost:3000/", +}); + +axiosInstance.interceptors.request.use((config) => { + const token = localStorage.getItem("auth_token"); + if (token) { + config.headers.Authorization = `Bearer ${token}`; + } + return config; +}); diff --git a/front/src/api/cats.ts b/front/src/api/cats.ts new file mode 100644 index 0000000..481ade7 --- /dev/null +++ b/front/src/api/cats.ts @@ -0,0 +1,41 @@ +import { catsApiInstance } from "./axios"; + +const API_KEY = + "live_LPSKueMvMxePXWmuh2AYbpz2VOtOzQB3yc2BJnt5MJ0LYurRlSApO8z7uHbrnsSw"; + +export const fetchCats = async (limit: number) => { + if (limit <= 0) { + throw new Error("Limit must be a positive number"); + } + + try { + const response = await catsApiInstance.get("/images/search", { + params: { + limit, + api_key: API_KEY, + }, + }); + return response.data; + } catch (error) { + console.error("Failed to fetch cats:", error); + throw error; + } +}; + +export async function fetchCatById(catId: string) { + if (!catId) { + throw new Error("Cat ID is required"); + } + + try { + const response = await catsApiInstance.get(`/images/${catId}`, { + params: { + api_key: API_KEY, + }, + }); + return response.data; + } catch (error) { + console.error(`Failed to fetch cat with ID ${catId}:`, error); + throw error; + } +} diff --git a/front/src/api/likes.ts b/front/src/api/likes.ts new file mode 100644 index 0000000..cb6e96e --- /dev/null +++ b/front/src/api/likes.ts @@ -0,0 +1,16 @@ +import { axiosInstance } from "./axios"; + +export const getLikes = async () => { + const res = await axiosInstance.get("/likes"); + return res.data.data; +}; + +export const likeCat = async (cat_id: string) => { + const res = await axiosInstance.post("/likes", { cat_id }); + return res.data; +}; + +export const unlikeCat = async (cat_id: string) => { + const res = await axiosInstance.delete(`/likes/${cat_id}`); + return res.data; +}; diff --git a/front/src/api/registerUser.ts b/front/src/api/registerUser.ts new file mode 100644 index 0000000..86b2dac --- /dev/null +++ b/front/src/api/registerUser.ts @@ -0,0 +1,10 @@ +import { axiosInstance } from "./axios"; + +export async function registerUser(login: string, password: string) { + const response = await axiosInstance.post("/user", { login, password }); + const token = response.headers["x-auth-token"]; + console.log("Response headers:", response.headers); + console.log("Token received:", token); + localStorage.setItem("auth_token", token); + return response.data; +} diff --git a/front/src/assets/full_heart.svg b/front/src/assets/full_heart.svg new file mode 100644 index 0000000..5d6a291 --- /dev/null +++ b/front/src/assets/full_heart.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/front/src/assets/outlined_heart.svg b/front/src/assets/outlined_heart.svg new file mode 100644 index 0000000..7193b25 --- /dev/null +++ b/front/src/assets/outlined_heart.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/front/src/components/shared/catCard/CatCard.module.css b/front/src/components/shared/catCard/CatCard.module.css new file mode 100644 index 0000000..cd20d02 --- /dev/null +++ b/front/src/components/shared/catCard/CatCard.module.css @@ -0,0 +1,34 @@ +.catCard { + position: relative; + width: 100%; + aspect-ratio: 1 / 1; + background-color: #f2f2f2; + overflow: hidden; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); + transition: transform 0.3s ease, box-shadow 0.3s ease; +} + +.catCard:hover { + transform: scale(1.14); + box-shadow: 0px 6px 5px 0px #0000003d; + z-index: 10; +} + +.catImage { + width: 100%; + height: 100%; + object-fit: cover; + display: block; +} + +.heartIcon { + position: absolute; + top: 170px; + right: 15px; + width: 40px; + height: 36.7px; + cursor: pointer; + z-index: 2; + transition: transform 0.2s ease, opacity 0.2s ease; + opacity: 1; +} diff --git a/front/src/components/shared/catCard/CatCard.tsx b/front/src/components/shared/catCard/CatCard.tsx new file mode 100644 index 0000000..5b50c5b --- /dev/null +++ b/front/src/components/shared/catCard/CatCard.tsx @@ -0,0 +1,52 @@ +import { useDispatch, useSelector } from "react-redux"; +import { useState } from "react"; + +import { CatItem } from "../../../types/types"; +import { RootState, AppDispatch } from "../../../store/store"; +import { addLike, removeLike } from "../../../redux/likesSlice"; + +import styles from "./CatCard.module.css"; +import full_heart from "../../../assets/full_heart.svg"; +import outlined_heart from "../../../assets/outlined_heart.svg"; + +type CatCardProps = { + cat: CatItem; +}; + +export default function CatCard({ cat }: CatCardProps) { + const dispatch = useDispatch(); + const likedCats = useSelector((state: RootState) => state.likes.likedCats); + + const [isHovered, setIsHovered] = useState(false); + const [iconHovered, setIconHovered] = useState(false); + + const liked = likedCats.includes(cat.id); + + const handleLikeToggle = () => { + liked ? dispatch(removeLike(cat.id)) : dispatch(addLike(cat)); + }; + + const handleMouseEnter = () => setIsHovered(true); + const handleMouseLeave = () => setIsHovered(false); + + return ( +
+ {cat.url} + + {isHovered && ( + {`Cat setIconHovered(true)} + onMouseLeave={() => setIconHovered(false)} + /> + )} +
+ ); +} diff --git a/front/src/components/shared/catsGrid/CatsGrid.module.css b/front/src/components/shared/catsGrid/CatsGrid.module.css new file mode 100644 index 0000000..f487490 --- /dev/null +++ b/front/src/components/shared/catsGrid/CatsGrid.module.css @@ -0,0 +1,7 @@ +.catsGrid { + display: grid; + grid-template-columns: repeat(5, 1fr); + grid-template-rows: repeat(3, auto); + gap: 50px; + padding: 50px 60px; +} diff --git a/front/src/components/shared/catsGrid/CatsGrid.tsx b/front/src/components/shared/catsGrid/CatsGrid.tsx new file mode 100644 index 0000000..3094889 --- /dev/null +++ b/front/src/components/shared/catsGrid/CatsGrid.tsx @@ -0,0 +1,21 @@ +import CatCard from "../catCard/CatCard"; +import styles from "./CatsGrid.module.css"; + +type CatItem = { + id: string; + url: string; +}; + +type CatGridProps = { + cats: CatItem[]; +}; + +export default function CatGrid({ cats }: CatGridProps) { + return ( +
+ {cats.map((cat) => ( + + ))} +
+ ); +} diff --git a/front/src/components/shared/notFound/NotFound.module.css b/front/src/components/shared/notFound/NotFound.module.css new file mode 100644 index 0000000..ec99558 --- /dev/null +++ b/front/src/components/shared/notFound/NotFound.module.css @@ -0,0 +1,38 @@ +.container { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + text-align: center; + height: calc(100vh - 200px); + padding: 0 16px; +} + +.icon { + margin-bottom: 30px; +} + +.title { + font-size: 2.25rem; /* 36px */ + font-weight: bold; + margin: 0; +} + +.text { + font-size: 1.125rem; /* 18px */ + margin-top: 8px; +} + +.link { + margin-top: 20px; + padding: 8px 16px; + background-color: #2196f3; + color: #fff; + border-radius: 4px; + text-decoration: none; + transition: background-color 0.2s ease; +} + +.link:hover { + background-color: #333; +} diff --git a/front/src/components/shared/notFound/NotFound.tsx b/front/src/components/shared/notFound/NotFound.tsx new file mode 100644 index 0000000..9069cf3 --- /dev/null +++ b/front/src/components/shared/notFound/NotFound.tsx @@ -0,0 +1,17 @@ +import { Link } from "react-router-dom"; +import styles from "./NotFound.module.css"; + +export default function NotFound() { + return ( +
+ {/* */} +

404 - Страница не найдена

+

+ Проверьте адрес или вернитесь на главную страницу. +

+ + На главную странице + +
+ ); +} diff --git a/front/src/components/ui/error/Error.module.css b/front/src/components/ui/error/Error.module.css new file mode 100644 index 0000000..4f7b74a --- /dev/null +++ b/front/src/components/ui/error/Error.module.css @@ -0,0 +1,25 @@ +.errorContainer { + background-color: #ffe5e5; + color: #b00020; + border: 1px solid #ffb3b3; + padding: 20px; + margin: 40px auto; + display: flex; + justify-content: center; + align-items: center; + max-width: 600px; + border-radius: 8px; + text-align: center; + box-shadow: 0 4px 8px rgba(255, 0, 0, 0.1); +} + +.title { + margin: 0 0 10px; + font-size: 1.5rem; + font-weight: bold; +} + +.message { + margin: 0; + font-size: 1rem; +} diff --git a/front/src/components/ui/error/Error.tsx b/front/src/components/ui/error/Error.tsx new file mode 100644 index 0000000..63b4d06 --- /dev/null +++ b/front/src/components/ui/error/Error.tsx @@ -0,0 +1,14 @@ +import styles from "./Error.module.css"; + +interface ErrorMessageProps { + message: string | null; +} + +export default function ErrorMessage({ message }: ErrorMessageProps) { + return ( +
+

Что то пошло не так

+

{message}

+
+ ); +} diff --git a/front/src/components/ui/skeleton/Skeleton.module.css b/front/src/components/ui/skeleton/Skeleton.module.css new file mode 100644 index 0000000..e8adea1 --- /dev/null +++ b/front/src/components/ui/skeleton/Skeleton.module.css @@ -0,0 +1,65 @@ +.skeleton { + position: relative; + overflow: hidden; + background-color: #e0e0e0; + width: 100%; + aspect-ratio: 1 / 1; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); + border-radius: 4px; +} + +.skeleton::after { + content: ""; + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + transform: translateX(-100%); + background: linear-gradient( + 90deg, + rgba(255, 255, 255, 0) 0%, + rgba(255, 255, 255, 0.2) 50%, + rgba(255, 255, 255, 0) 100% + ); + animation: shimmer 1.5s infinite; +} + +@keyframes shimmer { + 100% { + transform: translateX(100%); + } +} + +.skeletonGrid { + display: grid; + grid-template-columns: repeat(5, 1fr); + gap: 50px; + padding: 50px 60px; +} + +@media (max-width: 1200px) { + .skeletonGrid { + grid-template-columns: repeat(4, 1fr); + } +} + +@media (max-width: 992px) { + .skeletonGrid { + grid-template-columns: repeat(3, 1fr); + gap: 30px; + padding: 30px; + } +} + +@media (max-width: 768px) { + .skeletonGrid { + grid-template-columns: repeat(2, 1fr); + } +} + +@media (max-width: 480px) { + .skeletonGrid { + grid-template-columns: 1fr; + } +} diff --git a/front/src/components/ui/skeleton/Skeleton.tsx b/front/src/components/ui/skeleton/Skeleton.tsx new file mode 100644 index 0000000..3148b1a --- /dev/null +++ b/front/src/components/ui/skeleton/Skeleton.tsx @@ -0,0 +1,30 @@ +import React from "react"; +import styles from "./Skeleton.module.css"; + +export const SkeletonCard: React.FC = () => { + return
; +}; + +interface SkeletonGridProps { + count?: number; +} + +export default function SkeletonGrid({ count = 15 }: SkeletonGridProps) { + return ( +
+ {Array(count) + .fill(0) + .map((_, index) => ( + + ))} +
+ ); +} diff --git a/front/src/features/favorites/Favorite.module.css b/front/src/features/favorites/Favorite.module.css new file mode 100644 index 0000000..e69de29 diff --git a/front/src/features/favorites/Favorite.tsx b/front/src/features/favorites/Favorite.tsx new file mode 100644 index 0000000..15a2554 --- /dev/null +++ b/front/src/features/favorites/Favorite.tsx @@ -0,0 +1,17 @@ +import { useSelector } from "react-redux"; +import CatGrid from "../../components/shared/catsGrid/CatsGrid"; +import { RootState } from "../../store/store"; + +export default function Favorite() { + const likedCats = useSelector((state: RootState) => state.likes.likedCatData); + + if (!likedCats.length) { + return

You haven't liked any cats yet 🐱

; + } + + return ( +
+ +
+ ); +} diff --git a/front/src/features/home/Home.module.css b/front/src/features/home/Home.module.css new file mode 100644 index 0000000..e69de29 diff --git a/front/src/features/home/index.tsx b/front/src/features/home/index.tsx new file mode 100644 index 0000000..1dcb3d2 --- /dev/null +++ b/front/src/features/home/index.tsx @@ -0,0 +1,26 @@ +import { useSelector } from "react-redux"; +import { RootState } from "../../store/store"; + +import CatGrid from "../../components/shared/catsGrid/CatsGrid"; +import SkeletonGrid from "../../components/ui/skeleton/Skeleton"; +import ErrorMessage from "../../components/ui/error/Error"; + +export default function Home() { + const { + items: cats, + status, + error, + } = useSelector((state: RootState) => state.cats); + + if (status === "loading") { + return ; + } + + if (status === "failed") return ; + + return ( +
+ +
+ ); +} diff --git a/front/src/index.css b/front/src/index.css index 2c3fac6..aa759ae 100644 --- a/front/src/index.css +++ b/front/src/index.css @@ -1,69 +1,45 @@ -:root { - font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; - line-height: 1.5; - font-weight: 400; +@import url("https://fonts.googleapis.com/css2?family=Roboto:ital,wght@0,100..900;1,100..900&display=swap"); - color-scheme: light dark; - color: rgba(255, 255, 255, 0.87); - background-color: #242424; - - font-synthesis: none; - text-rendering: optimizeLegibility; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; - -webkit-text-size-adjust: 100%; -} - -a { - font-weight: 500; - color: #646cff; - text-decoration: inherit; -} -a:hover { - color: #535bf2; +*, +*::before, +*::after { + box-sizing: border-box; + margin: 0; + padding: 0; } body { - margin: 0; - display: flex; - place-items: center; - min-width: 320px; + font-family: "Roboto", sans-serif; + line-height: 1.6; + background-color: #f9f9f9; + color: #222; min-height: 100vh; } -h1 { - font-size: 3.2em; - line-height: 1.1; +a { + color: inherit; + text-decoration: none; } button { - border-radius: 8px; - border: 1px solid transparent; - padding: 0.6em 1.2em; - font-size: 1em; - font-weight: 500; - font-family: inherit; - background-color: #1a1a1a; + font: inherit; cursor: pointer; - transition: border-color 0.25s; + border: none; + background: none; } -button:hover { - border-color: #646cff; + +img { + max-width: 100%; + display: block; } -button:focus, -button:focus-visible { - outline: 4px auto -webkit-focus-ring-color; + +ul, +ol { + list-style: none; } -@media (prefers-color-scheme: light) { - :root { - color: #213547; - background-color: #ffffff; - } - a:hover { - color: #747bff; - } - button { - background-color: #f9f9f9; - } +#root { + display: flex; + flex-direction: column; + min-height: 100vh; } diff --git a/front/src/layout/Layout.tsx b/front/src/layout/Layout.tsx new file mode 100644 index 0000000..425401c --- /dev/null +++ b/front/src/layout/Layout.tsx @@ -0,0 +1,13 @@ +import { Outlet } from "react-router-dom"; +import Header from "./header/Header"; + +export default function Layout() { + return ( +
+
+
+ +
+
+ ); +} diff --git a/front/src/layout/header/Header.module.css b/front/src/layout/header/Header.module.css new file mode 100644 index 0000000..742a147 --- /dev/null +++ b/front/src/layout/header/Header.module.css @@ -0,0 +1,76 @@ +.header { + width: 100%; + height: 64px; + background-color: #2196f3; + display: flex; + align-items: center; + justify-content: flex-start; + padding-left: 62px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + position: relative; + z-index: 10; +} + +.nav { + display: flex; +} + +.navItem { + min-width: 120px; + height: 64px; + display: flex; + align-items: center; + justify-content: center; + padding: 0 16px; + color: #fff; + font-size: 14px; + line-height: 21px; + text-decoration: none; + transition: background-color 0.2s, color 0.2s; + letter-spacing: 0.25px; + font-weight: 400; + opacity: 70%; + cursor: pointer; + padding: 0 27px; +} + +.navItem:hover { + background-color: #1e88e5; + color: #ffffff; + opacity: 100%; +} + +@media (max-width: 768px) { + .header { + padding-left: 16px; + height: 56px; + } + + .navItem { + min-width: auto; + height: 48px; + font-size: 13px; + padding: 0 16px; + } +} + +@media (max-width: 480px) { + .header { + flex-direction: column; + align-items: flex-start; + height: auto; + padding: 8px 16px; + } + + .nav { + flex-direction: column; + width: 100%; + } + + .navItem { + width: 100%; + justify-content: flex-start; + padding: 12px 16px; + font-size: 14px; + } +} diff --git a/front/src/layout/header/Header.tsx b/front/src/layout/header/Header.tsx new file mode 100644 index 0000000..6c79b1b --- /dev/null +++ b/front/src/layout/header/Header.tsx @@ -0,0 +1,18 @@ +import { Link } from "react-router-dom"; + +import styles from "./Header.module.css"; + +export default function Header() { + return ( +
+ +
+ ); +} diff --git a/front/src/main.tsx b/front/src/main.tsx index 3d7150d..e4a3165 100644 --- a/front/src/main.tsx +++ b/front/src/main.tsx @@ -1,10 +1,17 @@ -import React from 'react' -import ReactDOM from 'react-dom/client' -import App from './App.tsx' -import './index.css' +// import React from "react"; +import ReactDOM from "react-dom/client"; +import { BrowserRouter } from "react-router-dom"; +import { Provider } from "react-redux"; +import { store } from "./store/store.ts"; +import App from "./App.tsx"; +import "./index.css"; -ReactDOM.createRoot(document.getElementById('root')!).render( - - - , -) +ReactDOM.createRoot(document.getElementById("root")!).render( + // + + + + + + // +); diff --git a/front/src/redux/catSlice.ts b/front/src/redux/catSlice.ts new file mode 100644 index 0000000..b235911 --- /dev/null +++ b/front/src/redux/catSlice.ts @@ -0,0 +1,50 @@ +import { createSlice, createAsyncThunk } from "@reduxjs/toolkit"; +import { fetchCats as fetchCatsAPI } from "../api/cats"; +import { CatItem } from "../types/types"; + +interface CatsState { + items: CatItem[]; + status: "idle" | "loading" | "succeeded" | "failed"; + error: string | null; +} + +const initialState: CatsState = { + items: [], + status: "idle", + error: null, +}; + +export const fetchCats = createAsyncThunk( + "cats/fetchCats", + async (limit: number, thunkAPI) => { + try { + const data = await fetchCatsAPI(limit); + return data; + } catch (error: any) { + return thunkAPI.rejectWithValue(error.message || "Failed to fetch cats"); + } + } +); + +const catsSlice = createSlice({ + name: "cats", + initialState, + reducers: {}, + extraReducers: (builder) => { + builder + .addCase(fetchCats.pending, (state) => { + state.status = "loading"; + state.error = null; + }) + .addCase(fetchCats.fulfilled, (state, action) => { + state.status = "succeeded"; + state.items = action.payload; + }) + .addCase(fetchCats.rejected, (state, action) => { + state.status = "failed"; + state.error = action.payload as string; + }); + }, +}); + +export default catsSlice.reducer; diff --git a/front/src/redux/likesSlice.ts b/front/src/redux/likesSlice.ts new file mode 100644 index 0000000..5e066c1 --- /dev/null +++ b/front/src/redux/likesSlice.ts @@ -0,0 +1,101 @@ +import { createSlice, createAsyncThunk } from "@reduxjs/toolkit"; +import { getLikes, likeCat, unlikeCat } from "../api/likes"; +import { CatItem } from "../types/types"; + +import { fetchCatById } from "../api/cats"; + +interface LikesState { + likedCats: string[]; + likedCatData: CatItem[]; + status: string; +} + +const initialState: LikesState = { + likedCats: [], + likedCatData: [], + status: "idle", +}; + +export const fetchLikes = createAsyncThunk( + "likes/fetch", + async (_arg, thunkAPI) => { + try { + const data = await getLikes(); + const catIds = data.map((like: any) => like.cat_id); + + const cats = await Promise.all( + catIds.map((id: string) => fetchCatById(id)) + ); + + return { catIds, cats }; + } catch (error: any) { + return thunkAPI.rejectWithValue(error.message || "Failed to fetch likes"); + } + } +); + +export const addLike = createAsyncThunk( + "likes/add", + async (cat: CatItem, thunkAPI) => { + try { + await likeCat(cat.id); + return cat; + } catch (error: any) { + return thunkAPI.rejectWithValue( + error.message || "Failed to like the cat" + ); + } + } +); + +export const removeLike = createAsyncThunk( + "likes/remove", + async (cat_id: string, thunkAPI) => { + try { + await unlikeCat(cat_id); + return cat_id; + } catch (error: any) { + throw thunkAPI.rejectWithValue( + error.message || "Failed to unlike the cat" + ); + } + } +); + +const likesSlice = createSlice({ + name: "likes", + initialState: initialState, + reducers: {}, + extraReducers: (builder) => { + builder + .addCase(fetchLikes.fulfilled, (state, action) => { + state.likedCats = action.payload.catIds; + state.likedCatData = action.payload.cats; + }) + .addCase(addLike.pending, (state, action) => { + const cat = action.meta.arg; + if (!state.likedCats.includes(cat.id)) { + state.likedCats.push(cat.id); + state.likedCatData.push(cat); + } + }) + .addCase(addLike.rejected, (state, action) => { + const cat = action.meta.arg; + state.likedCats = state.likedCats.filter((id) => id !== cat.id); + state.likedCatData = state.likedCatData.filter((c) => c.id !== cat.id); + }) + .addCase(removeLike.pending, (state, action) => { + const id = action.meta.arg; + state.likedCats = state.likedCats.filter((catId) => catId !== id); + state.likedCatData = state.likedCatData.filter((c) => c.id !== id); + }) + .addCase(removeLike.rejected, (state, action) => { + const id = action.meta.arg; + if (!state.likedCats.includes(id)) { + state.likedCats.push(id); + } + }); + }, +}); + +export default likesSlice.reducer; diff --git a/front/src/routes/index.tsx b/front/src/routes/index.tsx new file mode 100644 index 0000000..974be58 --- /dev/null +++ b/front/src/routes/index.tsx @@ -0,0 +1,17 @@ +import { Routes, Route } from "react-router-dom"; +import Layout from "../layout/Layout"; +import NotFound from "../components/shared/notFound/NotFound"; +import Home from "../features/home"; +import Favorite from "../features/favorites/Favorite"; + +export default function AppRoutes() { + return ( + + }> + } /> + } /> + } /> + + + ); +} diff --git a/front/src/store/store.ts b/front/src/store/store.ts new file mode 100644 index 0000000..d5caa88 --- /dev/null +++ b/front/src/store/store.ts @@ -0,0 +1,13 @@ +import { configureStore } from "@reduxjs/toolkit"; +import catsReducer from "../redux/catSlice"; +import likesReducer from "../redux/likesSlice"; + +export const store = configureStore({ + reducer: { + cats: catsReducer, + likes: likesReducer, + }, +}); + +export type RootState = ReturnType; +export type AppDispatch = typeof store.dispatch; diff --git a/front/src/types/types.ts b/front/src/types/types.ts new file mode 100644 index 0000000..4de89d4 --- /dev/null +++ b/front/src/types/types.ts @@ -0,0 +1,4 @@ +export interface CatItem { + id: string; + url: string; +} diff --git a/front/vite.config.ts b/front/vite.config.ts index 5a33944..9cc50ea 100644 --- a/front/vite.config.ts +++ b/front/vite.config.ts @@ -1,7 +1,7 @@ -import { defineConfig } from 'vite' -import react from '@vitejs/plugin-react' +import { defineConfig } from "vite"; +import react from "@vitejs/plugin-react"; // https://vitejs.dev/config/ export default defineConfig({ plugins: [react()], -}) +}); diff --git a/openapi.yaml b/openapi.yaml index 081106e..1a264c0 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -11,13 +11,13 @@ paths: /likes: get: security: - - bearerAuth: [ ] + - bearerAuth: [] tags: - cats summary: Список лайков operationId: listLikes responses: - '200': + "200": description: Successful operation content: application/json: @@ -27,7 +27,7 @@ paths: data: type: array items: - $ref: '#/components/schemas/Like' + $ref: "#/components/schemas/Like" post: security: - bearerAuth: [] @@ -40,21 +40,21 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/Like' + $ref: "#/components/schemas/Like" required: true responses: - '201': + "201": description: Successful operation content: application/json: schema: - $ref: '#/components/schemas/Like' - '405': + $ref: "#/components/schemas/Like" + "405": description: Invalid input /likes/{cat_id}: delete: security: - - bearerAuth: [ ] + - bearerAuth: [] tags: - cats summary: Удаление лайка @@ -67,9 +67,9 @@ paths: schema: type: string responses: - '200': + "200": description: Successful operation - '404': + "404": description: Like not found /user: post: @@ -82,10 +82,10 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/User' + $ref: "#/components/schemas/User" required: true responses: - '201': + "201": description: Successful operation headers: X-Auth-Token: @@ -96,8 +96,8 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/User' - '405': + $ref: "#/components/schemas/User" + "405": description: Invalid input components: @@ -134,4 +134,3 @@ components: password: type: string description: Пароль пользователя - From 3004391e83fb8c9bdca725193981742ac6fcfeec Mon Sep 17 00:00:00 2001 From: meirrrr Date: Thu, 19 Jun 2025 16:28:59 +0500 Subject: [PATCH 2/2] refactor: create url field in likes entity, change redux logic --- api/src/likes/likes.controller.ts | 4 ++-- api/src/likes/likes.entity.ts | 3 +++ api/src/likes/likes.service.ts | 4 ++-- front/src/App.tsx | 4 +--- front/src/api/likes.ts | 4 ++-- front/src/features/favorites/Favorite.module.css | 5 +++++ front/src/features/favorites/Favorite.tsx | 3 ++- front/src/features/home/Home.module.css | 0 front/src/features/home/index.tsx | 4 ++-- front/src/redux/likesSlice.ts | 14 ++++++-------- 10 files changed, 25 insertions(+), 20 deletions(-) delete mode 100644 front/src/features/home/Home.module.css diff --git a/api/src/likes/likes.controller.ts b/api/src/likes/likes.controller.ts index 3eae35c..d5e734a 100644 --- a/api/src/likes/likes.controller.ts +++ b/api/src/likes/likes.controller.ts @@ -24,8 +24,8 @@ export class LikesController { } @Post() - async create(@Body() body: { cat_id: string }, @Req() req) { - return await this.likesService.createLike(body.cat_id, req.user); + async create(@Body() body: { cat_id: string; url: string }, @Req() req) { + return await this.likesService.createLike(body.cat_id, body.url, req.user); } @Delete(':cat_id') diff --git a/api/src/likes/likes.entity.ts b/api/src/likes/likes.entity.ts index 9c0613b..9861777 100644 --- a/api/src/likes/likes.entity.ts +++ b/api/src/likes/likes.entity.ts @@ -15,6 +15,9 @@ export class Like { @Column() cat_id: string; + @Column() + url: string; + @CreateDateColumn() created_at: Date; diff --git a/api/src/likes/likes.service.ts b/api/src/likes/likes.service.ts index de1aaa1..835b0ad 100644 --- a/api/src/likes/likes.service.ts +++ b/api/src/likes/likes.service.ts @@ -12,11 +12,11 @@ export class LikesService { return this.repo.find({ where: { user } }); } - async createLike(cat_id: string, user: User) { + async createLike(cat_id: string, url: string, user: User) { const existing = await this.repo.findOne({ where: { cat_id, user } }); if (existing) throw new Error('Already liked'); - const like = this.repo.create({ cat_id, user }); + const like = this.repo.create({ cat_id, url, user }); return this.repo.save(like); } diff --git a/front/src/App.tsx b/front/src/App.tsx index a77226a..6e4c667 100644 --- a/front/src/App.tsx +++ b/front/src/App.tsx @@ -2,12 +2,10 @@ import { useEffect } from "react"; import { useDispatch } from "react-redux"; import { AppDispatch } from "./store/store"; -import AppRoutes from "./routes"; - import { fetchCats } from "./redux/catSlice"; import { fetchLikes } from "./redux/likesSlice"; - import { registerUser } from "./api/registerUser"; +import AppRoutes from "./routes"; export default function App() { const dispatch = useDispatch(); diff --git a/front/src/api/likes.ts b/front/src/api/likes.ts index cb6e96e..319b099 100644 --- a/front/src/api/likes.ts +++ b/front/src/api/likes.ts @@ -5,8 +5,8 @@ export const getLikes = async () => { return res.data.data; }; -export const likeCat = async (cat_id: string) => { - const res = await axiosInstance.post("/likes", { cat_id }); +export const likeCat = async (cat_id: string, url: string) => { + const res = await axiosInstance.post("/likes", { cat_id, url }); return res.data; }; diff --git a/front/src/features/favorites/Favorite.module.css b/front/src/features/favorites/Favorite.module.css index e69de29..a7266d7 100644 --- a/front/src/features/favorites/Favorite.module.css +++ b/front/src/features/favorites/Favorite.module.css @@ -0,0 +1,5 @@ +.gridContainer { + display: "flex"; + flex-wrap: "wrap"; + gap: "10px"; +} diff --git a/front/src/features/favorites/Favorite.tsx b/front/src/features/favorites/Favorite.tsx index 15a2554..ee53cf8 100644 --- a/front/src/features/favorites/Favorite.tsx +++ b/front/src/features/favorites/Favorite.tsx @@ -1,6 +1,7 @@ import { useSelector } from "react-redux"; import CatGrid from "../../components/shared/catsGrid/CatsGrid"; import { RootState } from "../../store/store"; +import styles from "./Favorite.module.css"; export default function Favorite() { const likedCats = useSelector((state: RootState) => state.likes.likedCatData); @@ -10,7 +11,7 @@ export default function Favorite() { } return ( -
+
); diff --git a/front/src/features/home/Home.module.css b/front/src/features/home/Home.module.css deleted file mode 100644 index e69de29..0000000 diff --git a/front/src/features/home/index.tsx b/front/src/features/home/index.tsx index 1dcb3d2..2d91116 100644 --- a/front/src/features/home/index.tsx +++ b/front/src/features/home/index.tsx @@ -19,8 +19,8 @@ export default function Home() { if (status === "failed") return ; return ( -
+ <> -
+ ); } diff --git a/front/src/redux/likesSlice.ts b/front/src/redux/likesSlice.ts index 5e066c1..ccdefea 100644 --- a/front/src/redux/likesSlice.ts +++ b/front/src/redux/likesSlice.ts @@ -2,8 +2,6 @@ import { createSlice, createAsyncThunk } from "@reduxjs/toolkit"; import { getLikes, likeCat, unlikeCat } from "../api/likes"; import { CatItem } from "../types/types"; -import { fetchCatById } from "../api/cats"; - interface LikesState { likedCats: string[]; likedCatData: CatItem[]; @@ -21,11 +19,11 @@ export const fetchLikes = createAsyncThunk( async (_arg, thunkAPI) => { try { const data = await getLikes(); - const catIds = data.map((like: any) => like.cat_id); - - const cats = await Promise.all( - catIds.map((id: string) => fetchCatById(id)) - ); + const cats = data.map((like: any) => ({ + id: like.cat_id, + url: like.url, + })); + const catIds = cats.map((cat: CatItem) => cat.id); return { catIds, cats }; } catch (error: any) { @@ -38,7 +36,7 @@ export const addLike = createAsyncThunk( "likes/add", async (cat: CatItem, thunkAPI) => { try { - await likeCat(cat.id); + await likeCat(cat.id, cat.url); return cat; } catch (error: any) { return thunkAPI.rejectWithValue(