From 4449062115aff8adfa51f98e87697bfec9f53756 Mon Sep 17 00:00:00 2001 From: mkour Date: Sat, 15 Nov 2025 14:51:19 +0200 Subject: [PATCH 01/17] initial project setup with Vite, dependencies, and assets - Added Vite for fast React development. - Installed main dependencies: - Installed dev dependencies: - Added favicon and image assets. - Fixed dev dependency files and configuration. --- .gitignore | 29 + .prettierignore | 0 .prettierrc | 10 + README.md | 78 +- eslint.config.js | 59 + index.html | 14 + package-lock.json | 5248 ++++++++++++++++++++++++++++++++++++++ package.json | 44 + public/cat.png | Bin 0 -> 155849 bytes public/favicon-dark.png | Bin 0 -> 4307 bytes public/favicon-light.png | Bin 0 -> 4228 bytes src/App.css | 42 + src/App.tsx | 12 + src/assets/react.svg | 1 + src/index.css | 68 + src/main.tsx | 10 + tsconfig.app.json | 28 + tsconfig.json | 7 + tsconfig.node.json | 26 + vite.config.ts | 7 + 20 files changed, 5670 insertions(+), 13 deletions(-) create mode 100644 .gitignore create mode 100644 .prettierignore create mode 100644 .prettierrc create mode 100644 eslint.config.js create mode 100644 index.html create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 public/cat.png create mode 100644 public/favicon-dark.png create mode 100644 public/favicon-light.png create mode 100644 src/App.css create mode 100644 src/App.tsx create mode 100644 src/assets/react.svg create mode 100644 src/index.css create mode 100644 src/main.tsx create mode 100644 tsconfig.app.json create mode 100644 tsconfig.json create mode 100644 tsconfig.node.json create mode 100644 vite.config.ts diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..e59e80a8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,29 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Environment variables +.env +.env.local +.env.production + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 00000000..e69de29b diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 00000000..3171de5a --- /dev/null +++ b/.prettierrc @@ -0,0 +1,10 @@ +{ + "semi": true, + "trailingComma": "es5", + "singleQuote": true, + "printWidth": 80, + "tabWidth": 2, + "useTabs": false, + "arrowParens": "always", + "endOfLine": "lf" +} diff --git a/README.md b/README.md index 91d4d5b9..d2e77611 100644 --- a/README.md +++ b/README.md @@ -1,21 +1,73 @@ -# GlobalWebIndex Engineering Challenge +# React + TypeScript + Vite -## Exercise: CatLover +This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. -Create a React application for cat lovers which is going to build upon thecatapi.com and will have 3 views. -The **first** view displays a list of 10 random cat images and a button to load more. Clicking on any of those images opens a modal view with the image and the information about the catโ€™s breed if available. This would be a link to the second view below - the breed detail. The modal should also contain a form to mark the image as your favourite (a part of the third view as well). Make sure you can copy-paste the URL of the modal and send it to your friends - they should see the same image as you can see. +Currently, two official plugins are available: -The **second** view displays a list of cat breeds. Each breed opens a modal again with a list of cat images of that breed. Each of those images must be a link to the image detail from the previous point. +- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh +- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh -The **third** view allows you do the following things: +## React Compiler -- Display your favourite cats -- Remove an image from your favourites (use any UX option you like) +The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation). -You can find the API documentation here: https://developers.thecatapi.com/ -We give you a lot of freedom in technologies and ways of doing things. We only insist on you using React.js. Get creative as much as you want, we WILL appreciate it. You will not be evaluated based on how well you follow these instructions, but based on how sensible your solution will be. In case you are not able to implement something you would normally implement for time reasons, make it clear with a comment. +## Expanding the ESLint configuration -## Submission +If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules: -Once you have built your app, share your code in the mean suits you best -Good luck, potential colleague! +```js +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + // Other configs... + + // Remove tseslint.configs.recommended and replace with this + tseslint.configs.recommendedTypeChecked, + // Alternatively, use this for stricter rules + tseslint.configs.strictTypeChecked, + // Optionally, add this for stylistic rules + tseslint.configs.stylisticTypeChecked, + + // Other configs... + ], + languageOptions: { + parserOptions: { + project: ['./tsconfig.node.json', './tsconfig.app.json'], + tsconfigRootDir: import.meta.dirname, + }, + // other options... + }, + }, +]) +``` + +You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules: + +```js +// eslint.config.js +import reactX from 'eslint-plugin-react-x' +import reactDom from 'eslint-plugin-react-dom' + +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + // Other configs... + // Enable lint rules for React + reactX.configs['recommended-typescript'], + // Enable lint rules for React DOM + reactDom.configs.recommended, + ], + languageOptions: { + parserOptions: { + project: ['./tsconfig.node.json', './tsconfig.app.json'], + tsconfigRootDir: import.meta.dirname, + }, + // other options... + }, + }, +]) +``` diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 00000000..b6fd5b07 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,59 @@ +import js from '@eslint/js'; +import globals from 'globals'; +import reactHooks from 'eslint-plugin-react-hooks'; +import reactRefresh from 'eslint-plugin-react-refresh'; +import tseslint from 'typescript-eslint'; +import { defineConfig, globalIgnores } from 'eslint/config'; +import simpleImportSort from 'eslint-plugin-simple-import-sort'; + +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + js.configs.recommended, + tseslint.configs.recommended, + reactHooks.configs.flat.recommended, + reactRefresh.configs.vite, + ], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + }, + plugins: { + 'simple-import-sort': simpleImportSort, + }, + rules: { + ...reactHooks.configs.recommended.rules, + 'react-refresh/only-export-components': [ + 'warn', + { allowConstantExport: true }, + ], + 'simple-import-sort/imports': [ + 'error', + { + groups: [ + // 1. React and React Router DOM + ['^react$', '^react-router-dom'], + // 2. Redux hooks and third-party libraries (excluding type imports) + ['^(?!.*\\btype\\b)@?\\w'], + // 3. Custom hooks (relative imports starting with use*.ts) + ['^\\.+\\/use[A-Z]\\w*$'], + // 4. Component imports (not utils, constants, hooks, or types) + [ + '^(?!.*\\btype\\b)(?!.*(?:\\/utils|\\/constants|\\/use[A-Z])).*\\/components\\/', + ], + ['^(?!.*\\btype\\b)\\.\\.?\\/(?!(?:utils|constants|use[A-Z]))'], + // 5. Utils and Constants + ['^\\.+\\/(?:utils|constants)$'], + // 6. CSS modules + ['\\.module\\.css$'], + // 7. Type imports + ['^.*\\btype\\b'], + ], + }, + ], + 'simple-import-sort/exports': 'error', + }, + }, +]); diff --git a/index.html b/index.html new file mode 100644 index 00000000..d667825e --- /dev/null +++ b/index.html @@ -0,0 +1,14 @@ + + + + + + + + Cat Lovers + + +
+ + + diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 00000000..48cea43c --- /dev/null +++ b/package-lock.json @@ -0,0 +1,5248 @@ +{ + "name": "cat-lover", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "cat-lover", + "version": "0.0.0", + "dependencies": { + "@reduxjs/toolkit": "^2.10.1", + "axios": "^1.13.2", + "react": "^19.2.0", + "react-dom": "^19.2.0", + "react-redux": "^9.2.0", + "react-router-dom": "^7.9.5", + "redux-persist": "^6.0.0" + }, + "devDependencies": { + "@eslint/js": "^9.39.1", + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/react": "^16.3.0", + "@testing-library/user-event": "^14.6.1", + "@types/node": "^24.10.0", + "@types/react": "^19.2.2", + "@types/react-dom": "^19.2.2", + "@vitejs/plugin-react": "^5.1.0", + "@vitest/ui": "^4.0.8", + "eslint": "^9.39.1", + "eslint-config-prettier": "^10.1.8", + "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-react-refresh": "^0.4.24", + "eslint-plugin-simple-import-sort": "^12.1.1", + "globals": "^16.5.0", + "jsdom": "^27.2.0", + "prettier": "^3.6.2", + "typescript": "~5.9.3", + "typescript-eslint": "^8.46.3", + "vite": "^7.2.2", + "vitest": "^4.0.8" + } + }, + "node_modules/@acemir/cssom": { + "version": "0.9.23", + "resolved": "https://registry.npmjs.org/@acemir/cssom/-/cssom-0.9.23.tgz", + "integrity": "sha512-2kJ1HxBKzPLbmhZpxBiTZggjtgCwKg1ma5RHShxvd6zgqhDEdEkzpiwe7jLkI2p2BrZvFCXIihdoMkl1H39VnA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@adobe/css-tools": { + "version": "4.4.4", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.4.tgz", + "integrity": "sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@asamuzakjp/css-color": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-4.0.5.tgz", + "integrity": "sha512-lMrXidNhPGsDjytDy11Vwlb6OIGrT3CmLg3VWNFyWkLWtijKl7xjvForlh8vuj0SHGjgl4qZEQzUmYTeQA2JFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@csstools/css-calc": "^2.1.4", + "@csstools/css-color-parser": "^3.1.0", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "lru-cache": "^11.2.1" + } + }, + "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": { + "version": "11.2.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.2.tgz", + "integrity": "sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@asamuzakjp/dom-selector": { + "version": "6.7.4", + "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-6.7.4.tgz", + "integrity": "sha512-buQDjkm+wDPXd6c13534URWZqbz0RP5PAhXZ+LIoa5LgwInT9HVJvGIJivg75vi8I13CxDGdTnz+aY5YUJlIAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/nwsapi": "^2.3.9", + "bidi-js": "^1.0.3", + "css-tree": "^3.1.0", + "is-potential-custom-element-name": "^1.0.1", + "lru-cache": "^11.2.2" + } + }, + "node_modules/@asamuzakjp/dom-selector/node_modules/lru-cache": { + "version": "11.2.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.2.tgz", + "integrity": "sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@asamuzakjp/nwsapi": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz", + "integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.5.tgz", + "integrity": "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz", + "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.5", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.28.3", + "@babel/helpers": "^7.28.4", + "@babel/parser": "^7.28.5", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.5", + "@babel/types": "^7.28.5", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz", + "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.5", + "@babel/types": "^7.28.5", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", + "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.28.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", + "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", + "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", + "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.5" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz", + "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.5.tgz", + "integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.5", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.5", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.5", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", + "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@csstools/color-helpers": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz", + "integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@csstools/css-calc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz", + "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz", + "integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^5.1.0", + "@csstools/css-calc": "^2.1.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz", + "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "peer": true, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-syntax-patches-for-csstree": { + "version": "1.0.16", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.0.16.tgz", + "integrity": "sha512-2SpS4/UaWQaGpBINyG5ZuCHnUDeVByOhvbkARwfmnfxDvTaj80yOI1cD8Tw93ICV5Fx4fnyDKWQZI1CDtcWyUg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz", + "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", + "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.1", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz", + "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", + "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.1.tgz", + "integrity": "sha512-S26Stp4zCy88tH94QbBv3XCuzRQiZ9yXofEILmglYTh/Ug/a9/umqvgFtYBAo3Lp0nsI/5/qH1CCrbdK3AP1Tw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@polka/url": { + "version": "1.0.0-next.29", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", + "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==", + "dev": true, + "license": "MIT" + }, + "node_modules/@reduxjs/toolkit": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.10.1.tgz", + "integrity": "sha512-/U17EXQ9Do9Yx4DlNGU6eVNfZvFJfYpUtRRdLf19PbPjdWBxNlxGZXywQZ1p1Nz8nMkWplTI7iD/23m07nolDA==", + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@standard-schema/utils": "^0.3.0", + "immer": "^10.2.0", + "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/@rolldown/pluginutils": { + "version": "1.0.0-beta.47", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.47.tgz", + "integrity": "sha512-8QagwMH3kNCuzD8EWL8R2YPW5e4OrHNSAHRFDdmFqEwEaD/KcNKjVoumo+gP2vW5eKB2UPbM6vTYiGZX0ixLnw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.2.tgz", + "integrity": "sha512-yDPzwsgiFO26RJA4nZo8I+xqzh7sJTZIWQOxn+/XOdPE31lAvLIYCKqjV+lNH/vxE2L2iH3plKxDCRK6i+CwhA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.53.2.tgz", + "integrity": "sha512-k8FontTxIE7b0/OGKeSN5B6j25EuppBcWM33Z19JoVT7UTXFSo3D9CdU39wGTeb29NO3XxpMNauh09B+Ibw+9g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.53.2.tgz", + "integrity": "sha512-A6s4gJpomNBtJ2yioj8bflM2oogDwzUiMl2yNJ2v9E7++sHrSrsQ29fOfn5DM/iCzpWcebNYEdXpaK4tr2RhfQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.53.2.tgz", + "integrity": "sha512-e6XqVmXlHrBlG56obu9gDRPW3O3hLxpwHpLsBJvuI8qqnsrtSZ9ERoWUXtPOkY8c78WghyPHZdmPhHLWNdAGEw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.53.2.tgz", + "integrity": "sha512-v0E9lJW8VsrwPux5Qe5CwmH/CF/2mQs6xU1MF3nmUxmZUCHazCjLgYvToOk+YuuUqLQBio1qkkREhxhc656ViA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.53.2.tgz", + "integrity": "sha512-ClAmAPx3ZCHtp6ysl4XEhWU69GUB1D+s7G9YjHGhIGCSrsg00nEGRRZHmINYxkdoJehde8VIsDC5t9C0gb6yqA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.53.2.tgz", + "integrity": "sha512-EPlb95nUsz6Dd9Qy13fI5kUPXNSljaG9FiJ4YUGU1O/Q77i5DYFW5KR8g1OzTcdZUqQQ1KdDqsTohdFVwCwjqg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.53.2.tgz", + "integrity": "sha512-BOmnVW+khAUX+YZvNfa0tGTEMVVEerOxN0pDk2E6N6DsEIa2Ctj48FOMfNDdrwinocKaC7YXUZ1pHlKpnkja/Q==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.53.2.tgz", + "integrity": "sha512-Xt2byDZ+6OVNuREgBXr4+CZDJtrVso5woFtpKdGPhpTPHcNG7D8YXeQzpNbFRxzTVqJf7kvPMCub/pcGUWgBjA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.53.2.tgz", + "integrity": "sha512-+LdZSldy/I9N8+klim/Y1HsKbJ3BbInHav5qE9Iy77dtHC/pibw1SR/fXlWyAk0ThnpRKoODwnAuSjqxFRDHUQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.53.2.tgz", + "integrity": "sha512-8ms8sjmyc1jWJS6WdNSA23rEfdjWB30LH8Wqj0Cqvv7qSHnvw6kgMMXRdop6hkmGPlyYBdRPkjJnj3KCUHV/uQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.53.2.tgz", + "integrity": "sha512-3HRQLUQbpBDMmzoxPJYd3W6vrVHOo2cVW8RUo87Xz0JPJcBLBr5kZ1pGcQAhdZgX9VV7NbGNipah1omKKe23/g==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.53.2.tgz", + "integrity": "sha512-fMjKi+ojnmIvhk34gZP94vjogXNNUKMEYs+EDaB/5TG/wUkoeua7p7VCHnE6T2Tx+iaghAqQX8teQzcvrYpaQA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.53.2.tgz", + "integrity": "sha512-XuGFGU+VwUUV5kLvoAdi0Wz5Xbh2SrjIxCtZj6Wq8MDp4bflb/+ThZsVxokM7n0pcbkEr2h5/pzqzDYI7cCgLQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.53.2.tgz", + "integrity": "sha512-w6yjZF0P+NGzWR3AXWX9zc0DNEGdtvykB03uhonSHMRa+oWA6novflo2WaJr6JZakG2ucsyb+rvhrKac6NIy+w==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.53.2.tgz", + "integrity": "sha512-yo8d6tdfdeBArzC7T/PnHd7OypfI9cbuZzPnzLJIyKYFhAQ8SvlkKtKBMbXDxe1h03Rcr7u++nFS7tqXz87Gtw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.53.2.tgz", + "integrity": "sha512-ah59c1YkCxKExPP8O9PwOvs+XRLKwh/mV+3YdKqQ5AMQ0r4M4ZDuOrpWkUaqO7fzAHdINzV9tEVu8vNw48z0lA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.53.2.tgz", + "integrity": "sha512-4VEd19Wmhr+Zy7hbUsFZ6YXEiP48hE//KPLCSVNY5RMGX2/7HZ+QkN55a3atM1C/BZCGIgqN+xrVgtdak2S9+A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.53.2.tgz", + "integrity": "sha512-IlbHFYc/pQCgew/d5fslcy1KEaYVCJ44G8pajugd8VoOEI8ODhtb/j8XMhLpwHCMB3yk2J07ctup10gpw2nyMA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.53.2.tgz", + "integrity": "sha512-lNlPEGgdUfSzdCWU176ku/dQRnA7W+Gp8d+cWv73jYrb8uT7HTVVxq62DUYxjbaByuf1Yk0RIIAbDzp+CnOTFg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.53.2.tgz", + "integrity": "sha512-S6YojNVrHybQis2lYov1sd+uj7K0Q05NxHcGktuMMdIQ2VixGwAfbJ23NnlvvVV1bdpR2m5MsNBViHJKcA4ADw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.53.2.tgz", + "integrity": "sha512-k+/Rkcyx//P6fetPoLMb8pBeqJBNGx81uuf7iljX9++yNBVRDQgD04L+SVXmXmh5ZP4/WOp4mWF0kmi06PW2tA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "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/@testing-library/dom": { + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", + "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "picocolors": "1.1.1", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@testing-library/jest-dom": { + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz", + "integrity": "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@adobe/css-tools": "^4.4.0", + "aria-query": "^5.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.6.3", + "picocolors": "^1.1.1", + "redent": "^3.0.0" + }, + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", + "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@testing-library/react": { + "version": "16.3.0", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.0.tgz", + "integrity": "sha512-kFSyxiEDwv1WLl2fgsq6pPBbw5aWKrsY2/noi1Id0TK0UParSF62oFQFGHXIyaG4pp2tEub/Zlel+fjjZILDsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@testing-library/dom": "^10.0.0", + "@types/react": "^18.0.0 || ^19.0.0", + "@types/react-dom": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@testing-library/user-event": { + "version": "14.6.1", + "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.1.tgz", + "integrity": "sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "peerDependencies": { + "@testing-library/dom": ">=7.21.4" + } + }, + "node_modules/@types/aria-query": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "dev": true, + "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", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.10.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.1.tgz", + "integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@types/react": { + "version": "19.2.5", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.5.tgz", + "integrity": "sha512-keKxkZMqnDicuvFoJbzrhbtdLSPhj/rZThDlKWCDbgXmUg0rEUFtRssDXKYmtXluZlIqiC5VqkCgRwzuyLHKHw==", + "devOptional": true, + "license": "MIT", + "peer": true, + "dependencies": { + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "dev": true, + "license": "MIT", + "peer": true, + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "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": "8.46.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.46.4.tgz", + "integrity": "sha512-R48VhmTJqplNyDxCyqqVkFSZIx1qX6PzwqgcXn1olLrzxcSBDlOsbtcnQuQhNtnNiJ4Xe5gREI1foajYaYU2Vg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "8.46.4", + "@typescript-eslint/type-utils": "8.46.4", + "@typescript-eslint/utils": "8.46.4", + "@typescript-eslint/visitor-keys": "8.46.4", + "graphemer": "^1.4.0", + "ignore": "^7.0.0", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.46.4", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.46.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.46.4.tgz", + "integrity": "sha512-tK3GPFWbirvNgsNKto+UmB/cRtn6TZfyw0D6IKrW55n6Vbs7KJoZtI//kpTKzE/DUmmnAFD8/Ca46s7Obs92/w==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@typescript-eslint/scope-manager": "8.46.4", + "@typescript-eslint/types": "8.46.4", + "@typescript-eslint/typescript-estree": "8.46.4", + "@typescript-eslint/visitor-keys": "8.46.4", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.46.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.46.4.tgz", + "integrity": "sha512-nPiRSKuvtTN+no/2N1kt2tUh/HoFzeEgOm9fQ6XQk4/ApGqjx0zFIIaLJ6wooR1HIoozvj2j6vTi/1fgAz7UYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.46.4", + "@typescript-eslint/types": "^8.46.4", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.46.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.46.4.tgz", + "integrity": "sha512-tMDbLGXb1wC+McN1M6QeDx7P7c0UWO5z9CXqp7J8E+xGcJuUuevWKxuG8j41FoweS3+L41SkyKKkia16jpX7CA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.46.4", + "@typescript-eslint/visitor-keys": "8.46.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.46.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.46.4.tgz", + "integrity": "sha512-+/XqaZPIAk6Cjg7NWgSGe27X4zMGqrFqZ8atJsX3CWxH/jACqWnrWI68h7nHQld0y+k9eTTjb9r+KU4twLoo9A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.46.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.46.4.tgz", + "integrity": "sha512-V4QC8h3fdT5Wro6vANk6eojqfbv5bpwHuMsBcJUJkqs2z5XnYhJzyz9Y02eUmF9u3PgXEUiOt4w4KHR3P+z0PQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.46.4", + "@typescript-eslint/typescript-estree": "8.46.4", + "@typescript-eslint/utils": "8.46.4", + "debug": "^4.3.4", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.46.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.46.4.tgz", + "integrity": "sha512-USjyxm3gQEePdUwJBFjjGNG18xY9A2grDVGuk7/9AkjIF1L+ZrVnwR5VAU5JXtUnBL/Nwt3H31KlRDaksnM7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.46.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.46.4.tgz", + "integrity": "sha512-7oV2qEOr1d4NWNmpXLR35LvCfOkTNymY9oyW+lUHkmCno7aOmIf/hMaydnJBUTBMRCOGZh8YjkFOc8dadEoNGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.46.4", + "@typescript-eslint/tsconfig-utils": "8.46.4", + "@typescript-eslint/types": "8.46.4", + "@typescript-eslint/visitor-keys": "8.46.4", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.46.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.46.4.tgz", + "integrity": "sha512-AbSv11fklGXV6T28dp2Me04Uw90R2iJ30g2bgLz529Koehrmkbs1r7paFqr1vPCZi7hHwYxYtxfyQMRC8QaVSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.7.0", + "@typescript-eslint/scope-manager": "8.46.4", + "@typescript-eslint/types": "8.46.4", + "@typescript-eslint/typescript-estree": "8.46.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.46.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.46.4.tgz", + "integrity": "sha512-/++5CYLQqsO9HFGLI7APrxBJYo+5OCMpViuhV8q5/Qa3o5mMrF//eQHks+PXcsAVaLdn817fMuS7zqoXNNZGaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.46.4", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.1.1.tgz", + "integrity": "sha512-WQfkSw0QbQ5aJ2CHYw23ZGkqnRwqKHD/KYsMeTkZzPT4Jcf0DcBxBtwMJxnu6E7oxw5+JC6ZAiePgh28uJ1HBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.5", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.47", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.18.0" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/@vitest/expect": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.9.tgz", + "integrity": "sha512-C2vyXf5/Jfj1vl4DQYxjib3jzyuswMi/KHHVN2z+H4v16hdJ7jMZ0OGe3uOVIt6LyJsAofDdaJNIFEpQcrSTFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.0.9", + "@vitest/utils": "4.0.9", + "chai": "^6.2.0", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.0.9.tgz", + "integrity": "sha512-PUyaowQFHW+9FKb4dsvvBM4o025rWMlEDXdWRxIOilGaHREYTi5Q2Rt9VCgXgPy/hHZu1LeuXtrA/GdzOatP2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.0.9", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.0.9.tgz", + "integrity": "sha512-Hor0IBTwEi/uZqB7pvGepyElaM8J75pYjrrqbC8ZYMB9/4n5QA63KC15xhT+sqHpdGWfdnPo96E8lQUxs2YzSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.0.9.tgz", + "integrity": "sha512-aF77tsXdEvIJRkj9uJZnHtovsVIx22Ambft9HudC+XuG/on1NY/bf5dlDti1N35eJT+QZLb4RF/5dTIG18s98w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.0.9", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.0.9.tgz", + "integrity": "sha512-r1qR4oYstPbnOjg0Vgd3E8ADJbi4ditCzqr+Z9foUrRhIy778BleNyZMeAJ2EjV+r4ASAaDsdciC9ryMy8xMMg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.0.9", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.0.9.tgz", + "integrity": "sha512-J9Ttsq0hDXmxmT8CUOWUr1cqqAj2FJRGTdyEjSR+NjoOGKEqkEWj+09yC0HhI8t1W6t4Ctqawl1onHgipJve1A==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/ui": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@vitest/ui/-/ui-4.0.9.tgz", + "integrity": "sha512-6HV2HHl9aRJ09TlYj/WAQxaa797Ezb5u0LpgabthlASAUAWKgw/W1DSPX7t848mMZmIUvzZgnUHGIylAoYHP0w==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@vitest/utils": "4.0.9", + "fflate": "^0.8.2", + "flatted": "^3.3.3", + "pathe": "^2.0.3", + "sirv": "^3.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "vitest": "4.0.9" + } + }, + "node_modules/@vitest/utils": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.0.9.tgz", + "integrity": "sha512-cEol6ygTzY4rUPvNZM19sDf7zGa35IYTm9wfzkHoT/f5jX10IOY7QleWSOh5T0e3I3WVozwK5Asom79qW8DiuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.0.9", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "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.13.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz", + "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.4", + "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", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.8.28", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.28.tgz", + "integrity": "sha512-gYjt7OIqdM0PcttNYP2aVrr2G0bMALkBaoehD4BuRGjAOtipg0b6wHg1yNL+s5zSnLZZrGHOw4IrND8CD+3oIQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, + "node_modules/bidi-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", + "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "require-from-string": "^2.0.2" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.0.tgz", + "integrity": "sha512-tbydkR/CxfMwelN0vwdP/pLkDwyAASZ+VfWm4EOwlB6SWhx1sYnWLqo8N5j0rAzPfzfRaxt0mM/4wPU/Su84RQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "baseline-browser-mapping": "^2.8.25", + "caniuse-lite": "^1.0.30001754", + "electron-to-chromium": "^1.5.249", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.1.4" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "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", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001754", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001754.tgz", + "integrity": "sha512-x6OeBXueoAceOmotzx3PO4Zpt4rzpeIFsSr6AAePTZxSkXiYDUmpypEl7e2+8NCd9bD7bXjqyef8CJYPC1jfxg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chai": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.1.tgz", + "integrity": "sha512-p4Z49OGG5W/WBCPSS/dH3jQ73kD6tiMmUM+bckNK6Jr5JHMG3k9bg/BvKR8lKmtVBKmOiuVaV2ws8s9oSbwysg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "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", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "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.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css-tree": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.1.0.tgz", + "integrity": "sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "mdn-data": "2.12.2", + "source-map-js": "^1.0.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/css.escape": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cssstyle": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-5.3.3.tgz", + "integrity": "sha512-OytmFH+13/QXONJcC75QNdMtKpceNk3u8ThBjyyYjkEcy/ekBwR1mMAuNvi3gdBPW3N5TlCzQ0WZw8H0lN/bDw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^4.0.3", + "@csstools/css-syntax-patches-for-csstree": "^1.0.14", + "css-tree": "^3.1.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/csstype": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.1.tgz", + "integrity": "sha512-98XGutrXoh75MlgLihlNxAGbUuFQc7l1cqcnEZlLNKc0UrVdPndgmaDmYTDDh929VS/eqTZV0rozmhu2qqT1/g==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/data-urls": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-6.0.0.tgz", + "integrity": "sha512-BnBS08aLUM+DKamupXs3w2tJJoqU+AkaE/+6vQxi/G/DPmIZFJJp9Dkb1kM03AZx8ADehDUZgsNxju3mPXZYIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^15.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true, + "license": "MIT" + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "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/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "dev": true, + "license": "MIT" + }, + "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.5.253", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.253.tgz", + "integrity": "sha512-O0tpQ/35rrgdiGQ0/OFWhy1itmd9A6TY9uQzlqj3hKSu/aYpe7UIn5d7CU2N9myH6biZiWF3VMZVuup8pw5U9w==", + "dev": true, + "license": "ISC" + }, + "node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "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-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "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.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.39.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.1.tgz", + "integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.1", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.39.1", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-config-prettier": { + "version": "10.1.8", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz", + "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", + "dev": true, + "license": "MIT", + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "funding": { + "url": "https://opencollective.com/eslint-config-prettier" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.0.1.tgz", + "integrity": "sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.24.4", + "@babel/parser": "^7.24.4", + "hermes-parser": "^0.25.1", + "zod": "^3.25.0 || ^4.0.0", + "zod-validation-error": "^3.5.0 || ^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" + } + }, + "node_modules/eslint-plugin-react-refresh": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.24.tgz", + "integrity": "sha512-nLHIW7TEq3aLrEYWpVaJ1dRgFR+wLDPN8e8FpYAql/bMV2oBEfC37K0gLEGgv9fy66juNShSMV8OkTqzltcG/w==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": ">=8.40" + } + }, + "node_modules/eslint-plugin-simple-import-sort": { + "version": "12.1.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-simple-import-sort/-/eslint-plugin-simple-import-sort-12.1.1.tgz", + "integrity": "sha512-6nuzu4xwQtE3332Uz0to+TxDQYRLTKRESSc2hefVT48Zc8JthmN23Gx9lnYhu0FtkRSL1oxny3kJ2aveVhmOVA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": ">=5.0.0" + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expect-type": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.2.2.tgz", + "integrity": "sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fflate": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", + "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==", + "dev": true, + "license": "MIT" + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" + }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "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.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", + "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/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "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", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "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-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "16.5.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.5.0.tgz", + "integrity": "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "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", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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/hermes-estree": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", + "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", + "dev": true, + "license": "MIT" + }, + "node_modules/hermes-parser": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", + "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hermes-estree": "0.25.1" + } + }, + "node_modules/html-encoding-sniffer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", + "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-encoding": "^3.1.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/immer": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/immer/-/immer-10.2.0.tgz", + "integrity": "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsdom": { + "version": "27.2.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-27.2.0.tgz", + "integrity": "sha512-454TI39PeRDW1LgpyLPyURtB4Zx1tklSr6+OFOipsxGUH1WMTvk6C65JQdrj455+DP2uJ1+veBEHTGFKWVLFoA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@acemir/cssom": "^0.9.23", + "@asamuzakjp/dom-selector": "^6.7.4", + "cssstyle": "^5.3.3", + "data-urls": "^6.0.0", + "decimal.js": "^10.6.0", + "html-encoding-sniffer": "^4.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.6", + "is-potential-custom-element-name": "^1.0.1", + "parse5": "^8.0.0", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^6.0.0", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^8.0.0", + "whatwg-encoding": "^3.1.1", + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^15.1.0", + "ws": "^8.18.3", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true, + "license": "MIT", + "bin": { + "lz-string": "bin/bin.js" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "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/mdn-data": { + "version": "2.12.2", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.12.2.tgz", + "integrity": "sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "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/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/mrmime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", + "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse5": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.0.tgz", + "integrity": "sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz", + "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "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", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/react": { + "version": "19.2.0", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz", + "integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz", + "integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.0" + } + }, + "node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true, + "license": "MIT" + }, + "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", + "peer": true, + "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.18.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", + "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-router": { + "version": "7.9.6", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.9.6.tgz", + "integrity": "sha512-Y1tUp8clYRXpfPITyuifmSoE2vncSME18uVLgaqyxh9H35JWpIfzHo+9y3Fzh5odk/jxPW29IgLgzcdwxGqyNA==", + "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.9.6", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.9.6.tgz", + "integrity": "sha512-2MkC2XSXq6HjGcihnx1s0DBWQETI4mlis4Ux7YTLvP67xnGxCvq+BcCQSO81qQHVUTM1V53tl4iVVaY5sReCOA==", + "license": "MIT", + "dependencies": { + "react-router": "7.9.6" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "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", + "peer": true + }, + "node_modules/redux-persist": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/redux-persist/-/redux-persist-6.0.0.tgz", + "integrity": "sha512-71LLMbUq2r02ng2We9S215LtPu3fY0KgaGE0k8WRgl6RkqxtGfl7HUozz1Dftwsb0D/5mZ8dwAaPbtnzfvbEwQ==", + "license": "MIT", + "peerDependencies": { + "redux": ">4.0.0" + } + }, + "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/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.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", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.2.tgz", + "integrity": "sha512-MHngMYwGJVi6Fmnk6ISmnk7JAHRNF0UkuucA0CUW3N3a4KnONPEZz+vUanQP/ZC/iY1Qkf3bwPWzyY84wEks1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.53.2", + "@rollup/rollup-android-arm64": "4.53.2", + "@rollup/rollup-darwin-arm64": "4.53.2", + "@rollup/rollup-darwin-x64": "4.53.2", + "@rollup/rollup-freebsd-arm64": "4.53.2", + "@rollup/rollup-freebsd-x64": "4.53.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.53.2", + "@rollup/rollup-linux-arm-musleabihf": "4.53.2", + "@rollup/rollup-linux-arm64-gnu": "4.53.2", + "@rollup/rollup-linux-arm64-musl": "4.53.2", + "@rollup/rollup-linux-loong64-gnu": "4.53.2", + "@rollup/rollup-linux-ppc64-gnu": "4.53.2", + "@rollup/rollup-linux-riscv64-gnu": "4.53.2", + "@rollup/rollup-linux-riscv64-musl": "4.53.2", + "@rollup/rollup-linux-s390x-gnu": "4.53.2", + "@rollup/rollup-linux-x64-gnu": "4.53.2", + "@rollup/rollup-linux-x64-musl": "4.53.2", + "@rollup/rollup-openharmony-arm64": "4.53.2", + "@rollup/rollup-win32-arm64-msvc": "4.53.2", + "@rollup/rollup-win32-ia32-msvc": "4.53.2", + "@rollup/rollup-win32-x64-gnu": "4.53.2", + "@rollup/rollup-win32-x64-msvc": "4.53.2", + "fsevents": "~2.3.2" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "license": "MIT" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/sirv": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.2.tgz", + "integrity": "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@polka/url": "^1.0.0-next.24", + "mrmime": "^2.0.0", + "totalist": "^3.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/source-map-js": { + "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" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/tinyrainbow": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.0.3.tgz", + "integrity": "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tldts": { + "version": "7.0.17", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.17.tgz", + "integrity": "sha512-Y1KQBgDd/NUc+LfOtKS6mNsC9CCaH+m2P1RoIZy7RAPo3C3/t8X45+zgut31cRZtZ3xKPjfn3TkGTrctC2TQIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^7.0.17" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "7.0.17", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.17.tgz", + "integrity": "sha512-DieYoGrP78PWKsrXr8MZwtQ7GLCUeLxihtjC1jZsW1DnvSMdKPitJSe8OSYDM2u5H6g3kWJZpePqkp43TfLh0g==", + "dev": true, + "license": "MIT" + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/totalist": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", + "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/tough-cookie": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.0.tgz", + "integrity": "sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^7.0.5" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz", + "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/ts-api-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", + "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.46.4", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.46.4.tgz", + "integrity": "sha512-KALyxkpYV5Ix7UhvjTwJXZv76VWsHG+NjNlt/z+a17SOQSiOcBdUXdbJdyXi7RPxrBFECtFOiPwUJQusJuCqrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.46.4", + "@typescript-eslint/parser": "8.46.4", + "@typescript-eslint/typescript-estree": "8.46.4", + "@typescript-eslint/utils": "8.46.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "dev": true, + "license": "MIT" + }, + "node_modules/update-browserslist-db": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.4.tgz", + "integrity": "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/vite": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.2.2.tgz", + "integrity": "sha512-BxAKBWmIbrDgrokdGZH1IgkIk/5mMHDreLDmCJ0qpyJaAteP8NvMhkwr/ZCQNqNH97bw/dANTE9PDzqwJghfMQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/vitest": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.0.9.tgz", + "integrity": "sha512-E0Ja2AX4th+CG33yAFRC+d1wFx2pzU5r6HtG6LiPSE04flaE0qB6YyjSw9ZcpJAtVPfsvZGtJlKWZpuW7EHRxg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@vitest/expect": "4.0.9", + "@vitest/mocker": "4.0.9", + "@vitest/pretty-format": "4.0.9", + "@vitest/runner": "4.0.9", + "@vitest/snapshot": "4.0.9", + "@vitest/spy": "4.0.9", + "@vitest/utils": "4.0.9", + "debug": "^4.4.3", + "es-module-lexer": "^1.7.0", + "expect-type": "^1.2.2", + "magic-string": "^0.30.21", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^3.10.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.0.3", + "vite": "^6.0.0 || ^7.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/debug": "^4.1.12", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.0.9", + "@vitest/browser-preview": "4.0.9", + "@vitest/browser-webdriverio": "4.0.9", + "@vitest/ui": "4.0.9", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/debug": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/vitest/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/webidl-conversions": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.0.tgz", + "integrity": "sha512-n4W4YFyz5JzOfQeA8oN7dUYpR+MBP3PIUsn2jLjWXwK5ASUzt0Jc/A5sAUZoCYFJRGF0FBKJ+1JjN43rNdsQzA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20" + } + }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-url": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-15.1.0.tgz", + "integrity": "sha512-2ytDk0kiEj/yu90JOAp44PVPUkO9+jVhyf+SybKlRHSDlvOOZhdPIrr7xTH64l4WixO2cP+wQIcgujkGBPPz6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "^6.0.0", + "webidl-conversions": "^8.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.1.12.tgz", + "integrity": "sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==", + "dev": true, + "license": "MIT", + "peer": true, + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-validation-error": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz", + "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 00000000..6611739e --- /dev/null +++ b/package.json @@ -0,0 +1,44 @@ +{ + "name": "cat-lover", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "lint": "eslint .", + "preview": "vite preview" + }, + "dependencies": { + "@reduxjs/toolkit": "^2.10.1", + "axios": "^1.13.2", + "react": "^19.2.0", + "react-dom": "^19.2.0", + "react-redux": "^9.2.0", + "react-router-dom": "^7.9.5", + "redux-persist": "^6.0.0" + }, + "devDependencies": { + "@eslint/js": "^9.39.1", + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/react": "^16.3.0", + "@testing-library/user-event": "^14.6.1", + "@types/node": "^24.10.0", + "@types/react": "^19.2.2", + "@types/react-dom": "^19.2.2", + "@vitejs/plugin-react": "^5.1.0", + "@vitest/ui": "^4.0.8", + "eslint": "^9.39.1", + "eslint-config-prettier": "^10.1.8", + "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-react-refresh": "^0.4.24", + "eslint-plugin-simple-import-sort": "^12.1.1", + "globals": "^16.5.0", + "jsdom": "^27.2.0", + "prettier": "^3.6.2", + "typescript": "~5.9.3", + "typescript-eslint": "^8.46.3", + "vite": "^7.2.2", + "vitest": "^4.0.8" + } +} diff --git a/public/cat.png b/public/cat.png new file mode 100644 index 0000000000000000000000000000000000000000..3616bb9ee7531179226963b3fd963c84a8fb43d3 GIT binary patch literal 155849 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

R8f5D&qmw)#U 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 E08@6?mjD0& literal 0 HcmV?d00001 diff --git a/public/favicon-light.png b/public/favicon-light.png new file mode 100644 index 0000000000000000000000000000000000000000..7ac923952bf87170a0954f1627b0b68e0225664f GIT binary patch literal 4228 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 literal 0 HcmV?d00001 diff --git a/src/App.css b/src/App.css new file mode 100644 index 00000000..b9d355df --- /dev/null +++ b/src/App.css @@ -0,0 +1,42 @@ +#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/src/App.tsx b/src/App.tsx new file mode 100644 index 00000000..959e1ce3 --- /dev/null +++ b/src/App.tsx @@ -0,0 +1,12 @@ +import './App.css' + +const App = () => { + + return ( + <> + + + ) +} + +export default App diff --git a/src/assets/react.svg b/src/assets/react.svg new file mode 100644 index 00000000..6c87de9b --- /dev/null +++ b/src/assets/react.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/index.css b/src/index.css new file mode 100644 index 00000000..08a3ac9e --- /dev/null +++ b/src/index.css @@ -0,0 +1,68 @@ +:root { + font-family: system-ui, Avenir, Helvetica, Arial, sans-serif; + line-height: 1.5; + font-weight: 400; + + 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; +} + +a { + font-weight: 500; + color: #646cff; + text-decoration: inherit; +} +a:hover { + color: #535bf2; +} + +body { + margin: 0; + display: flex; + place-items: center; + min-width: 320px; + min-height: 100vh; +} + +h1 { + font-size: 3.2em; + line-height: 1.1; +} + +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; + cursor: pointer; + transition: border-color 0.25s; +} +button:hover { + border-color: #646cff; +} +button:focus, +button:focus-visible { + outline: 4px auto -webkit-focus-ring-color; +} + +@media (prefers-color-scheme: light) { + :root { + color: #213547; + background-color: #ffffff; + } + a:hover { + color: #747bff; + } + button { + background-color: #f9f9f9; + } +} diff --git a/src/main.tsx b/src/main.tsx new file mode 100644 index 00000000..bef5202a --- /dev/null +++ b/src/main.tsx @@ -0,0 +1,10 @@ +import { StrictMode } from 'react' +import { createRoot } from 'react-dom/client' +import './index.css' +import App from './App.tsx' + +createRoot(document.getElementById('root')!).render( + + + , +) diff --git a/tsconfig.app.json b/tsconfig.app.json new file mode 100644 index 00000000..a9b5a59c --- /dev/null +++ b/tsconfig.app.json @@ -0,0 +1,28 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "target": "ES2022", + "useDefineForClassFields": true, + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "module": "ESNext", + "types": ["vite/client"], + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["src"] +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 00000000..1ffef600 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,7 @@ +{ + "files": [], + "references": [ + { "path": "./tsconfig.app.json" }, + { "path": "./tsconfig.node.json" } + ] +} diff --git a/tsconfig.node.json b/tsconfig.node.json new file mode 100644 index 00000000..8a67f62f --- /dev/null +++ b/tsconfig.node.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + "target": "ES2023", + "lib": ["ES2023"], + "module": "ESNext", + "types": ["node"], + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["vite.config.ts"] +} diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 00000000..8b0f57b9 --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' + +// https://vite.dev/config/ +export default defineConfig({ + plugins: [react()], +}) From ec19cad100881a583629a816d43ba02c651a5a3c Mon Sep 17 00:00:00 2001 From: mkour Date: Sat, 15 Nov 2025 15:34:25 +0200 Subject: [PATCH 02/17] integrate Cat API with all needed endpoints - Import and test all API calls (fetchRandomCats, fetchAllBreeds, fetchCatImageById, fetchImagesByBreed) - Configure env variables - Add axios interceptor --- src/App.tsx | 31 +++++++++++++++++++++-------- src/api/api.ts | 31 +++++++++++++++++++++++++++++ src/api/constants.ts | 13 ++++++++++++ src/api/endpoints.ts | 8 ++++++++ src/api/index.ts | 1 + src/api/services.ts | 47 ++++++++++++++++++++++++++++++++++++++++++++ src/main.tsx | 5 ++++- src/types.ts | 24 ++++++++++++++++++++++ vite.config.ts | 2 +- 9 files changed, 152 insertions(+), 10 deletions(-) create mode 100644 src/api/api.ts create mode 100644 src/api/constants.ts create mode 100644 src/api/endpoints.ts create mode 100644 src/api/index.ts create mode 100644 src/api/services.ts create mode 100644 src/types.ts diff --git a/src/App.tsx b/src/App.tsx index 959e1ce3..9e66c438 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,12 +1,27 @@ -import './App.css' +import { useEffect } from 'react'; + +import { + fetchAllBreeds, + fetchCatImageById, + fetchImagesByBreed, + fetchRandomCats, +} from './api/services'; + +import './App.css'; const App = () => { + useEffect(() => { + fetchAllBreeds().then(console.log); + fetchRandomCats().then(console.log); + fetchCatImageById('r_njVlaSz').then((data) => + console.log('cat image:', data) + ); + fetchImagesByBreed('abys').then((data) => + console.log('breed images:', data) + ); + }, []); - return ( - <> - - - ) -} + return <>; +}; -export default App +export default App; diff --git a/src/api/api.ts b/src/api/api.ts new file mode 100644 index 00000000..d677c78a --- /dev/null +++ b/src/api/api.ts @@ -0,0 +1,31 @@ +import axios from 'axios'; + +import C from './constants'; + +const API_KEY = import.meta.env.VITE_CAT_API_KEY; +const API_BASE_URL = + import.meta.env.VITE_CAT_API_BASE_URL || 'https://api.thecatapi.com/v1'; + +const api = axios.create({ + baseURL: API_BASE_URL, + headers: { + 'x-api-key': API_KEY || '', + }, + timeout: 10000, +}); + +api.interceptors.response.use( + (response) => response, + (error) => { + if (error.response) { + console.error(C.API_ERROR, error.response.status, error.response.data); + } else if (error.request) { + console.error(C.NETWORK_ERROR); + } else { + console.error(C.REQUEST_ERROR, error.message); + } + return Promise.reject(error); + } +); + +export default api; diff --git a/src/api/constants.ts b/src/api/constants.ts new file mode 100644 index 00000000..34d0c6d4 --- /dev/null +++ b/src/api/constants.ts @@ -0,0 +1,13 @@ +const API_ERROR = 'API Error:'; +const NETWORK_ERROR = 'Network Error: No response from server'; +const REQUEST_ERROR = 'Request Error:'; +const DEFAULT_CATS_LIMIT = 3; +const DEFAULT_BREED_IMAGES_LIMIT = 4; + +export default { + API_ERROR, + DEFAULT_CATS_LIMIT, + DEFAULT_BREED_IMAGES_LIMIT, + NETWORK_ERROR, + REQUEST_ERROR, +}; diff --git a/src/api/endpoints.ts b/src/api/endpoints.ts new file mode 100644 index 00000000..f67d1090 --- /dev/null +++ b/src/api/endpoints.ts @@ -0,0 +1,8 @@ +const endpoints = { + IMAGES: '/images/search' as const, + IMAGE_BY_ID: (imageId: string): string => `/images/${imageId}`, + BREEDS: '/breeds' as const, + BREED_BY_ID: (breedId: string): string => `/breeds/${breedId}`, +} as const; + +export default endpoints; diff --git a/src/api/index.ts b/src/api/index.ts new file mode 100644 index 00000000..e371345e --- /dev/null +++ b/src/api/index.ts @@ -0,0 +1 @@ +export * from './services'; diff --git a/src/api/services.ts b/src/api/services.ts new file mode 100644 index 00000000..fadf88f9 --- /dev/null +++ b/src/api/services.ts @@ -0,0 +1,47 @@ +import type { Breed, Cat } from '../types'; +import api from './api'; +import endpoints from './endpoints'; + +import C from './constants'; + +const fetchRandomCats = async ( + limit: number = C.DEFAULT_CATS_LIMIT +): Promise => { + const response = await api.get(endpoints.IMAGES, { + params: { + limit, + has_breeds: true, + }, + }); + return response.data; +}; + +const fetchCatImageById = async (imageId: string): Promise => { + const response = await api.get(endpoints.IMAGE_BY_ID(imageId)); + return response.data; +}; + +const fetchAllBreeds = async (): Promise => { + const response = await api.get(endpoints.BREEDS); + return response.data; +}; + +const fetchImagesByBreed = async ( + breedId: string, + limit: number = C.DEFAULT_CATS_LIMIT +): Promise => { + const response = await api.get(endpoints.IMAGES, { + params: { + breed_ids: breedId, + limit, + }, + }); + return response.data; +}; + +export { + fetchAllBreeds, + fetchCatImageById, + fetchImagesByBreed, + fetchRandomCats, +}; diff --git a/src/main.tsx b/src/main.tsx index bef5202a..2683f76b 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,8 +1,11 @@ import { StrictMode } from 'react' + import { createRoot } from 'react-dom/client' -import './index.css' + import App from './App.tsx' +import './index.css' + createRoot(document.getElementById('root')!).render( diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 00000000..70cd4fad --- /dev/null +++ b/src/types.ts @@ -0,0 +1,24 @@ +export type Cat = { + id: string; + url: string; + width: number; + height: number; + breeds?: Breed[]; +}; + +export type Breed = { + weight: { + imperial: string; + metric: string; + }; + id: string; + name: string; + temperament: string; + origin: string; + description: string; + life_span: string; + affection_level: number; + child_friendly: number; + dog_friendly: number; + image: Cat; +}; diff --git a/vite.config.ts b/vite.config.ts index 8b0f57b9..9982072c 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,5 +1,5 @@ -import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' +import { defineConfig } from 'vite' // https://vite.dev/config/ export default defineConfig({ From d8539bcceb728159a527f90a4ee2a2c956680b26 Mon Sep 17 00:00:00 2001 From: mkour Date: Sat, 15 Nov 2025 17:06:42 +0200 Subject: [PATCH 03/17] implement routing and a basic architecture - Set up routing with React Router for gallery, breeds, and favourites - Add modal and breed modal routes for nested navigation --- src/App.tsx | 26 ++++++------------------ src/components/breedModal/BreedModal.tsx | 5 +++++ src/components/breedModal/index.ts | 1 + src/components/index.ts | 2 ++ src/components/modal/Modal.tsx | 5 +++++ src/components/modal/index.ts | 1 + src/main.tsx | 14 ++++++------- src/router.tsx | 24 ++++++++++++++++++++++ src/views/breeds/Breeds.tsx | 5 +++++ src/views/breeds/index.ts | 1 + src/views/favourites/Favourites.tsx | 5 +++++ src/views/favourites/index.ts | 1 + src/views/gallery/Gallery.tsx | 5 +++++ src/views/gallery/index.ts | 1 + src/views/index.ts | 3 +++ 15 files changed, 72 insertions(+), 27 deletions(-) create mode 100644 src/components/breedModal/BreedModal.tsx create mode 100644 src/components/breedModal/index.ts create mode 100644 src/components/index.ts create mode 100644 src/components/modal/Modal.tsx create mode 100644 src/components/modal/index.ts create mode 100644 src/router.tsx create mode 100644 src/views/breeds/Breeds.tsx create mode 100644 src/views/breeds/index.ts create mode 100644 src/views/favourites/Favourites.tsx create mode 100644 src/views/favourites/index.ts create mode 100644 src/views/gallery/Gallery.tsx create mode 100644 src/views/gallery/index.ts create mode 100644 src/views/index.ts diff --git a/src/App.tsx b/src/App.tsx index 9e66c438..0dff3c41 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,27 +1,13 @@ -import { useEffect } from 'react'; - -import { - fetchAllBreeds, - fetchCatImageById, - fetchImagesByBreed, - fetchRandomCats, -} from './api/services'; +import AppRouter from './router'; import './App.css'; const App = () => { - useEffect(() => { - fetchAllBreeds().then(console.log); - fetchRandomCats().then(console.log); - fetchCatImageById('r_njVlaSz').then((data) => - console.log('cat image:', data) - ); - fetchImagesByBreed('abys').then((data) => - console.log('breed images:', data) - ); - }, []); - - return <>; + return ( +

+ +
+ ); }; export default App; diff --git a/src/components/breedModal/BreedModal.tsx b/src/components/breedModal/BreedModal.tsx new file mode 100644 index 00000000..8aa93bd3 --- /dev/null +++ b/src/components/breedModal/BreedModal.tsx @@ -0,0 +1,5 @@ +const BreedModal = () => { + return <>BreedModal; +}; + +export default BreedModal; diff --git a/src/components/breedModal/index.ts b/src/components/breedModal/index.ts new file mode 100644 index 00000000..eddc094e --- /dev/null +++ b/src/components/breedModal/index.ts @@ -0,0 +1 @@ +export { default } from './BreedModal'; diff --git a/src/components/index.ts b/src/components/index.ts new file mode 100644 index 00000000..c16a01b4 --- /dev/null +++ b/src/components/index.ts @@ -0,0 +1,2 @@ +export { default as BreedModal } from './breedModal'; +export { default as Modal } from './modal'; diff --git a/src/components/modal/Modal.tsx b/src/components/modal/Modal.tsx new file mode 100644 index 00000000..57143884 --- /dev/null +++ b/src/components/modal/Modal.tsx @@ -0,0 +1,5 @@ +const Modal = () => { + return <>Modal; +}; + +export default Modal; diff --git a/src/components/modal/index.ts b/src/components/modal/index.ts new file mode 100644 index 00000000..0690fecf --- /dev/null +++ b/src/components/modal/index.ts @@ -0,0 +1 @@ +export { default } from './Modal'; diff --git a/src/main.tsx b/src/main.tsx index 2683f76b..f8a2b612 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,13 +1,13 @@ -import { StrictMode } from 'react' +import { BrowserRouter } from 'react-router-dom'; -import { createRoot } from 'react-dom/client' +import { createRoot } from 'react-dom/client'; -import App from './App.tsx' +import App from './App'; -import './index.css' +import './index.css'; createRoot(document.getElementById('root')!).render( - + - , -) + +); diff --git a/src/router.tsx b/src/router.tsx new file mode 100644 index 00000000..6a459eb6 --- /dev/null +++ b/src/router.tsx @@ -0,0 +1,24 @@ +import { Navigate, Route, Routes } from 'react-router-dom'; + +import { BreedModal, Modal } from './components'; +import { Breeds, Favourites, Gallery } from './views'; + +const AppRouter = () => { + return ( + + }> + } /> + + }> + } /> + } /> + + }> + } /> + + } /> + + ); +}; + +export default AppRouter; diff --git a/src/views/breeds/Breeds.tsx b/src/views/breeds/Breeds.tsx new file mode 100644 index 00000000..a51fe6c0 --- /dev/null +++ b/src/views/breeds/Breeds.tsx @@ -0,0 +1,5 @@ +const Breeds = () => { + return <>Breeds; +}; + +export default Breeds; diff --git a/src/views/breeds/index.ts b/src/views/breeds/index.ts new file mode 100644 index 00000000..984072ce --- /dev/null +++ b/src/views/breeds/index.ts @@ -0,0 +1 @@ +export { default } from './Breeds'; diff --git a/src/views/favourites/Favourites.tsx b/src/views/favourites/Favourites.tsx new file mode 100644 index 00000000..27d09f1c --- /dev/null +++ b/src/views/favourites/Favourites.tsx @@ -0,0 +1,5 @@ +const Favourites = () => { + return <>Favourites; +}; + +export default Favourites; diff --git a/src/views/favourites/index.ts b/src/views/favourites/index.ts new file mode 100644 index 00000000..112e85a1 --- /dev/null +++ b/src/views/favourites/index.ts @@ -0,0 +1 @@ +export { default } from './Favourites'; diff --git a/src/views/gallery/Gallery.tsx b/src/views/gallery/Gallery.tsx new file mode 100644 index 00000000..f3193599 --- /dev/null +++ b/src/views/gallery/Gallery.tsx @@ -0,0 +1,5 @@ +const Gallery = () => { + return <>Gallery; +}; + +export default Gallery; diff --git a/src/views/gallery/index.ts b/src/views/gallery/index.ts new file mode 100644 index 00000000..cde3adaa --- /dev/null +++ b/src/views/gallery/index.ts @@ -0,0 +1 @@ +export { default } from './Gallery'; diff --git a/src/views/index.ts b/src/views/index.ts new file mode 100644 index 00000000..263670b2 --- /dev/null +++ b/src/views/index.ts @@ -0,0 +1,3 @@ +export { default as Breeds } from './breeds'; +export { default as Favourites } from './favourites'; +export { default as Gallery } from './gallery'; From 50030c314582129c2b6bef5a3109244202f87d38 Mon Sep 17 00:00:00 2001 From: mkour Date: Sat, 15 Nov 2025 18:12:58 +0200 Subject: [PATCH 04/17] implement responsive navigation bar --- src/App.css | 48 ++------- src/App.tsx | 7 +- src/components/index.ts | 1 + .../navigation/Navigation.module.css | 98 +++++++++++++++++++ src/components/navigation/Navigation.tsx | 50 ++++++++++ src/components/navigation/constants.ts | 23 +++++ src/components/navigation/index.ts | 1 + src/components/navigation/useNavigation.ts | 36 +++++++ src/index.css | 7 +- 9 files changed, 229 insertions(+), 42 deletions(-) create mode 100644 src/components/navigation/Navigation.module.css create mode 100644 src/components/navigation/Navigation.tsx create mode 100644 src/components/navigation/constants.ts create mode 100644 src/components/navigation/index.ts create mode 100644 src/components/navigation/useNavigation.ts diff --git a/src/App.css b/src/App.css index b9d355df..6b9dc2e8 100644 --- a/src/App.css +++ b/src/App.css @@ -1,42 +1,12 @@ -#root { - max-width: 1280px; - margin: 0 auto; - padding: 2rem; - text-align: center; +.app { + min-height: 100vh; + display: flex; + flex-direction: column; + background: #fef9f0; + width: 100%; } -.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; +.main { + flex: 1; + background: #fef9f0; } diff --git a/src/App.tsx b/src/App.tsx index 0dff3c41..0ef188e3 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,3 +1,5 @@ +import Navigation from './components/navigation'; + import AppRouter from './router'; import './App.css'; @@ -5,7 +7,10 @@ import './App.css'; const App = () => { return (
- + +
+ +
); }; diff --git a/src/components/index.ts b/src/components/index.ts index c16a01b4..df511070 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -1,2 +1,3 @@ export { default as BreedModal } from './breedModal'; export { default as Modal } from './modal'; +export { default as Navigation } from './navigation'; diff --git a/src/components/navigation/Navigation.module.css b/src/components/navigation/Navigation.module.css new file mode 100644 index 00000000..d1c59f02 --- /dev/null +++ b/src/components/navigation/Navigation.module.css @@ -0,0 +1,98 @@ +.nav { + background: #f4d4d0; + box-shadow: 0 2px 8px rgba(61, 40, 23, 0.1); + position: sticky; + top: 0; + z-index: 100; + padding: 1rem 2rem; + width: 100vw; + margin-left: calc(-50vw + 50%); + box-sizing: border-box; +} + +.container { + max-width: 1200px; + margin: 0 auto; + display: flex; + justify-content: space-between; + align-items: center; +} + +.logo { + display: flex; + align-items: center; + gap: 0.75rem; + text-decoration: none; + transition: transform 0.2s; +} + +.logo:hover { + transform: scale(1.05); +} + +.logoIcon { + width: 50px; + height: auto; + display: block; + filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.2)); +} + +.logoText { + font-size: 1.5rem; + font-weight: 700; + color: #3d2817; + text-shadow: none; +} + +.links { + display: flex; + gap: 0.5rem; + align-items: center; +} + +.link { + padding: 0.75rem 1.5rem; + color: #3d2817; + text-decoration: none; + font-weight: 500; + border-radius: 1.5rem; + transition: all 0.2s; + position: relative; +} + +.link:hover { + background: rgba(234, 144, 118, 0.2); + color: #3d2817; +} + +.linkActive { + background: #ea9076; + color: white; + font-weight: 600; +} + +.linkActive::after { + display: none; +} + +@media (max-width: 768px) { + .container { + padding: 1rem; + flex-direction: column; + gap: 1rem; + } + + .links { + width: 100%; + justify-content: center; + } + + .link { + padding: 0.5rem 1rem; + font-size: 0.875rem; + } + + .logoText { + font-size: 1.25rem; + } +} diff --git a/src/components/navigation/Navigation.tsx b/src/components/navigation/Navigation.tsx new file mode 100644 index 00000000..47bce6d4 --- /dev/null +++ b/src/components/navigation/Navigation.tsx @@ -0,0 +1,50 @@ +import { Link } from 'react-router-dom'; + +import useNavigation from './useNavigation'; + +import C from './constants'; + +import styles from './Navigation.module.css'; + +const Navigation = () => { + const { isGalleryActive, isBreedsActive, isFavouritesActive } = + useNavigation(); + + return ( + + ); +}; + +export default Navigation; diff --git a/src/components/navigation/constants.ts b/src/components/navigation/constants.ts new file mode 100644 index 00000000..e8765870 --- /dev/null +++ b/src/components/navigation/constants.ts @@ -0,0 +1,23 @@ +const BREEDS = 'Breeds'; +const BREEDS_PATH = '/breeds'; +const CAT_PATH = '/cat/'; +const DEFAULT_PATH = '/'; +const GALLERY = 'Gallery'; +const IMAGE_SOURCE = '/cat.png'; +const IMAGE_ALT = 'Cat logo'; +const FAVOURITES = 'Favourites'; +const FAVOURITES_PATH = '/favourites'; +const TITLE = 'Cat Lover'; + +export default { + BREEDS, + BREEDS_PATH, + CAT_PATH, + DEFAULT_PATH, + GALLERY, + IMAGE_SOURCE, + IMAGE_ALT, + FAVOURITES, + FAVOURITES_PATH, + TITLE, +}; diff --git a/src/components/navigation/index.ts b/src/components/navigation/index.ts new file mode 100644 index 00000000..7bc6dc0e --- /dev/null +++ b/src/components/navigation/index.ts @@ -0,0 +1 @@ +export { default } from './Navigation'; diff --git a/src/components/navigation/useNavigation.ts b/src/components/navigation/useNavigation.ts new file mode 100644 index 00000000..05806d4a --- /dev/null +++ b/src/components/navigation/useNavigation.ts @@ -0,0 +1,36 @@ +import { useLocation } from 'react-router-dom'; + +import C from './constants'; + +type UseNavigationState = { + isGalleryActive: boolean; + isBreedsActive: boolean; + isFavouritesActive: boolean; +}; + +const useNavigation = (): UseNavigationState => { + const location = useLocation(); + + const isActive = (path: string) => { + if (path === C.DEFAULT_PATH && location.pathname === C.DEFAULT_PATH) + return true; + if (path !== C.DEFAULT_PATH && location.pathname.startsWith(path)) + return true; + return false; + }; + + const isGalleryActive = + location.pathname === C.DEFAULT_PATH || + location.pathname.startsWith(C.CAT_PATH); + + const isBreedsActive = isActive(C.BREEDS_PATH); + const isFavouritesActive = isActive(C.FAVOURITES_PATH); + + return { + isGalleryActive, + isBreedsActive, + isFavouritesActive, + }; +}; + +export default useNavigation; diff --git a/src/index.css b/src/index.css index 08a3ac9e..36938c96 100644 --- a/src/index.css +++ b/src/index.css @@ -24,10 +24,13 @@ a:hover { body { margin: 0; - display: flex; - place-items: center; min-width: 320px; min-height: 100vh; + overflow-x: hidden; +} + +html { + overflow-x: hidden; } h1 { From 82993fd3effe3e1590e0a356f5288be6f3128769 Mon Sep 17 00:00:00 2001 From: mkour Date: Sat, 15 Nov 2025 18:29:31 +0200 Subject: [PATCH 05/17] add redux store - Implement domain-based slices for gallery, favourites, and breeds --- src/store/breedsSlice.ts | 23 +++++++++++++++++++++++ src/store/favouritesSlice.ts | 26 ++++++++++++++++++++++++++ src/store/gallerySlice.ts | 27 +++++++++++++++++++++++++++ src/store/hooks.ts | 16 ++++++++++++++++ src/store/index.ts | 18 ++++++++++++++++++ src/store/store.ts | 16 ++++++++++++++++ src/store/types.ts | 7 +++++++ 7 files changed, 133 insertions(+) create mode 100644 src/store/breedsSlice.ts create mode 100644 src/store/favouritesSlice.ts create mode 100644 src/store/gallerySlice.ts create mode 100644 src/store/hooks.ts create mode 100644 src/store/index.ts create mode 100644 src/store/store.ts create mode 100644 src/store/types.ts diff --git a/src/store/breedsSlice.ts b/src/store/breedsSlice.ts new file mode 100644 index 00000000..7fb12267 --- /dev/null +++ b/src/store/breedsSlice.ts @@ -0,0 +1,23 @@ +import type { PayloadAction } from '@reduxjs/toolkit'; +import { createSlice } from '@reduxjs/toolkit'; + +import type { Breed } from '../types'; +import type { BreedsState } from './types'; + +const initialBreedsState: BreedsState = {}; + +const breedsSlice = createSlice({ + name: 'breeds', + initialState: initialBreedsState, + reducers: { + addBreeds: (state, action: PayloadAction) => { + action.payload.forEach((breed) => { + state[breed.id] = breed; + }); + }, + clearBreeds: () => initialBreedsState, + }, +}); + +export const breedsActions = breedsSlice.actions; +export default breedsSlice.reducer; diff --git a/src/store/favouritesSlice.ts b/src/store/favouritesSlice.ts new file mode 100644 index 00000000..d450d73e --- /dev/null +++ b/src/store/favouritesSlice.ts @@ -0,0 +1,26 @@ +import type { PayloadAction } from '@reduxjs/toolkit'; +import { createSlice } from '@reduxjs/toolkit'; + +import type { Cat } from '../types'; +import type { FavouritesState } from './types'; + +const initialFavouritesState: FavouritesState = {}; + +const favouritesSlice = createSlice({ + name: 'favourites', + initialState: initialFavouritesState, + reducers: { + toggleFavourite: (state, action: PayloadAction) => { + const cat = action.payload; + if (state[cat.id]) { + delete state[cat.id]; + } else { + state[cat.id] = cat; + } + }, + clearFavourites: () => initialFavouritesState, + }, +}); + +export const favouritesActions = favouritesSlice.actions; +export default favouritesSlice.reducer; diff --git a/src/store/gallerySlice.ts b/src/store/gallerySlice.ts new file mode 100644 index 00000000..9f0df121 --- /dev/null +++ b/src/store/gallerySlice.ts @@ -0,0 +1,27 @@ +import type { PayloadAction } from '@reduxjs/toolkit'; +import { createSlice } from '@reduxjs/toolkit'; + +import type { Cat } from '../types'; +import type { CatsState } from './types'; + +const initialGalleryState: CatsState = {}; + +const gallerySlice = createSlice({ + name: 'gallery', + initialState: initialGalleryState, + reducers: { + addCats: (state, action: PayloadAction) => { + console.log('REDUCER CALLED - addCats action payload:', action.payload); + const newState = { ...state }; + action.payload.forEach((cat) => { + newState[cat.id] = cat; + }); + console.log('REDUCER - State after update:', newState); + return newState; + }, + clearCats: () => initialGalleryState, + }, +}); + +export const galleryActions = gallerySlice.actions; +export default gallerySlice.reducer; diff --git a/src/store/hooks.ts b/src/store/hooks.ts new file mode 100644 index 00000000..c370669b --- /dev/null +++ b/src/store/hooks.ts @@ -0,0 +1,16 @@ +import { + type TypedUseSelectorHook, + useDispatch, + useSelector, +} from 'react-redux'; + +import type { AppDispatch, RootState } from './index'; + +export const useCatsDispatch = useDispatch.withTypes(); +export const useCatsSelector: TypedUseSelectorHook = useSelector; + +export const selectFavourites = (state: RootState) => state.favourites; + +export const selectGallery = (state: RootState) => state.gallery; + +export const selectBreeds = (state: RootState) => state.breeds.breeds; diff --git a/src/store/index.ts b/src/store/index.ts new file mode 100644 index 00000000..ad5121ba --- /dev/null +++ b/src/store/index.ts @@ -0,0 +1,18 @@ +import { configureStore } from '@reduxjs/toolkit'; + +import breedsReducer from './breedsSlice'; +import favouritesReducer from './favouritesSlice'; +import galleryReducer from './gallerySlice'; + +const store = configureStore({ + reducer: { + gallery: galleryReducer, + favourites: favouritesReducer, + breeds: breedsReducer, + }, +}); + +export type RootState = ReturnType; +export type AppDispatch = typeof store.dispatch; + +export default store; diff --git a/src/store/store.ts b/src/store/store.ts new file mode 100644 index 00000000..e682a512 --- /dev/null +++ b/src/store/store.ts @@ -0,0 +1,16 @@ +import { configureStore } from '@reduxjs/toolkit'; + +import breedsReducer from './breedsSlice'; +import favouritesReducer from './favouritesSlice'; +import galleryReducer from './gallerySlice'; + +export const store = configureStore({ + reducer: { + favourites: favouritesReducer, + gallery: galleryReducer, + breeds: breedsReducer, + }, +}); + +export type RootState = ReturnType; +export type AppDispatch = typeof store.dispatch; diff --git a/src/store/types.ts b/src/store/types.ts new file mode 100644 index 00000000..61898214 --- /dev/null +++ b/src/store/types.ts @@ -0,0 +1,7 @@ +import type { Breed, Cat } from '../types'; + +export type FavouritesState = Record; + +export type CatsState = Record; + +export type BreedsState = Record; From f3cd6aa99ba5a8d787948ed0aba5be674eaa8ee1 Mon Sep 17 00:00:00 2001 From: mkour Date: Sat, 15 Nov 2025 21:16:06 +0200 Subject: [PATCH 06/17] add gallery view with reusable card reusable loading and reusable error components - Implement Gallery view with lazy loading and Redux integration - Add modular Card component - Add reusable ErrorMessage and LoadingSpinner components --- src/components/card/Card.module.css | 102 ++++++++++++++++++ src/components/card/Card.tsx | 36 +++++++ src/components/card/CardImage.tsx | 23 ++++ src/components/card/CardSkeleton.tsx | 5 + src/components/card/constants.ts | 7 ++ src/components/card/index.ts | 1 + src/components/card/useCard.ts | 41 +++++++ .../errorMessage/ErrorMessage.module.css | 7 ++ src/components/errorMessage/ErrorMessage.tsx | 11 ++ src/components/errorMessage/index.ts | 1 + src/components/index.ts | 3 + .../loadingSpinner/LoadingSpinner.module.css | 32 ++++++ .../loadingSpinner/LoadingSpinner.tsx | 13 +++ src/components/loadingSpinner/constants.ts | 3 + src/components/loadingSpinner/index.ts | 1 + src/main.tsx | 10 +- src/views/gallery/Gallery.module.css | 101 +++++++++++++++++ src/views/gallery/Gallery.tsx | 51 ++++++++- src/views/gallery/GalleryButtons.tsx | 38 +++++++ src/views/gallery/constants.ts | 13 +++ src/views/gallery/useGallery.ts | 61 +++++++++++ 21 files changed, 556 insertions(+), 4 deletions(-) create mode 100644 src/components/card/Card.module.css create mode 100644 src/components/card/Card.tsx create mode 100644 src/components/card/CardImage.tsx create mode 100644 src/components/card/CardSkeleton.tsx create mode 100644 src/components/card/constants.ts create mode 100644 src/components/card/index.ts create mode 100644 src/components/card/useCard.ts create mode 100644 src/components/errorMessage/ErrorMessage.module.css create mode 100644 src/components/errorMessage/ErrorMessage.tsx create mode 100644 src/components/errorMessage/index.ts create mode 100644 src/components/loadingSpinner/LoadingSpinner.module.css create mode 100644 src/components/loadingSpinner/LoadingSpinner.tsx create mode 100644 src/components/loadingSpinner/constants.ts create mode 100644 src/components/loadingSpinner/index.ts create mode 100644 src/views/gallery/Gallery.module.css create mode 100644 src/views/gallery/GalleryButtons.tsx create mode 100644 src/views/gallery/constants.ts create mode 100644 src/views/gallery/useGallery.ts diff --git a/src/components/card/Card.module.css b/src/components/card/Card.module.css new file mode 100644 index 00000000..e4d42a99 --- /dev/null +++ b/src/components/card/Card.module.css @@ -0,0 +1,102 @@ +.card { + background: #fff; + border-radius: 1rem; + overflow: hidden; + box-shadow: 0 2px 8px rgba(61, 40, 23, 0.1); + cursor: pointer; + transition: + transform 0.2s, + box-shadow 0.2s; + position: relative; + border: 3px solid #3d2817; +} + +.card:hover { + transform: translateY(-8px) rotate(1deg); + box-shadow: 0 8px 24px rgba(255, 107, 157, 0.3); +} + +.card:nth-child(4n + 1) { + background-color: #fce8b8; +} + +.card:nth-child(4n + 2) { + background-color: #a8d8d4; +} + +.card:nth-child(4n + 3) { + background-color: #f5c9b4; +} + +.card:nth-child(4n + 4) { + background-color: #f5a973; +} + +.imageContainer { + position: relative; + width: 100%; + height: 250px; + overflow: hidden; + background-color: inherit; + padding: 10px; + box-sizing: border-box; +} + +.image { + width: 100%; + height: 100%; + object-fit: cover; + display: block; + border-radius: 0.5rem; + transition: opacity 0.3s ease; +} + +.skeleton { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + border-radius: 0.5rem; + background: linear-gradient( + 90deg, + rgba(61, 40, 23, 0.05) 25%, + rgba(61, 40, 23, 0.1) 50%, + rgba(61, 40, 23, 0.05) 75% + ); + background-size: 200% 100%; + animation: shimmer 1.5s infinite; +} + +@keyframes shimmer { + 0% { + background-position: 200% 0; + } + 100% { + background-position: -200% 0; + } +} + +.favouriteButtonWrapper { + position: absolute; + top: 0.75rem; + right: 0.75rem; + z-index: 10; +} + +.breedName { + position: absolute; + bottom: 1rem; + left: 3rem; + right: 3rem; + font-size: 1.25rem; + font-weight: 800; + color: #3d2817; + background: rgba(255, 255, 255, 0.9); + padding: 0.5rem 0.75rem; + border-radius: 0.5rem; + margin: 0; + text-align: center; + backdrop-filter: blur(4px); + z-index: 5; +} diff --git a/src/components/card/Card.tsx b/src/components/card/Card.tsx new file mode 100644 index 00000000..a4b9c567 --- /dev/null +++ b/src/components/card/Card.tsx @@ -0,0 +1,36 @@ +import useCard from './useCard'; + +import type { Cat } from '../../types'; +import CardImage from './CardImage'; +import CardSkeleton from './CardSkeleton'; + +import styles from './Card.module.css'; + +type CardProps = { + cat: Cat; + hasTitle: boolean; +}; + +const Card = ({ cat, hasTitle }: CardProps) => { + const { breedText, isLoaded, getImagePosition, handleImageLoaded } = useCard({ + cat, + }); + + return ( +
+
+ {!isLoaded && } + + {hasTitle && breedText && ( +
{breedText}
+ )} +
+
+ ); +}; + +export default Card; diff --git a/src/components/card/CardImage.tsx b/src/components/card/CardImage.tsx new file mode 100644 index 00000000..e660358e --- /dev/null +++ b/src/components/card/CardImage.tsx @@ -0,0 +1,23 @@ +import type { Cat } from '../../types'; + +import styles from './Card.module.css'; + +type CardImageProps = { + cat: Cat; + imagePosition: string; + onLoad: () => void; +}; + +const CardImage = ({ cat, imagePosition, onLoad }: CardImageProps) => ( + {cat.id} +); + +export default CardImage; diff --git a/src/components/card/CardSkeleton.tsx b/src/components/card/CardSkeleton.tsx new file mode 100644 index 00000000..40fbf0dc --- /dev/null +++ b/src/components/card/CardSkeleton.tsx @@ -0,0 +1,5 @@ +import styles from './Card.module.css'; + +const CardSkeleton = () =>
; + +export default CardSkeleton; diff --git a/src/components/card/constants.ts b/src/components/card/constants.ts new file mode 100644 index 00000000..391bef1e --- /dev/null +++ b/src/components/card/constants.ts @@ -0,0 +1,7 @@ +const HIGH_IMAGE_RATIO_THRESHOLD = 1.2; +const MEDIUM_IMAGE_RATIO_THRESHOLD = 1.1; + +export default { + HIGH_IMAGE_RATIO_THRESHOLD, + MEDIUM_IMAGE_RATIO_THRESHOLD, +}; diff --git a/src/components/card/index.ts b/src/components/card/index.ts new file mode 100644 index 00000000..c68311df --- /dev/null +++ b/src/components/card/index.ts @@ -0,0 +1 @@ +export { default } from './Card'; diff --git a/src/components/card/useCard.ts b/src/components/card/useCard.ts new file mode 100644 index 00000000..129735ee --- /dev/null +++ b/src/components/card/useCard.ts @@ -0,0 +1,41 @@ +import { useState } from 'react'; + +import type { Breed, Cat } from '../../types'; + +import C from './constants'; + +type UseCardProps = { + cat: Cat; +}; + +const useCard = ({ cat }: UseCardProps) => { + const [isLoaded, setIsLoaded] = useState(false); + + const getImagePosition = () => { + const imageRatio = cat.height / cat.width; + if (imageRatio > C.HIGH_IMAGE_RATIO_THRESHOLD) return 'center 15%'; + if (imageRatio > C.MEDIUM_IMAGE_RATIO_THRESHOLD) return 'center 25%'; + return 'center'; + }; + + const formatBreedNames = (breeds: Breed[]): string => { + return breeds.map((breed) => breed.name).join(' - '); + }; + + const handleImageLoaded = () => { + setIsLoaded(true); + }; + + const hasBreeds = !!cat.breeds?.length; + + const breedText = hasBreeds ? formatBreedNames(cat.breeds!) : ''; + + return { + breedText, + isLoaded, + getImagePosition, + handleImageLoaded, + }; +}; + +export default useCard; diff --git a/src/components/errorMessage/ErrorMessage.module.css b/src/components/errorMessage/ErrorMessage.module.css new file mode 100644 index 00000000..cafb8621 --- /dev/null +++ b/src/components/errorMessage/ErrorMessage.module.css @@ -0,0 +1,7 @@ +.errorMessage { + color: #dc3545; + text-align: center; + margin: 1.5rem 0; + font-size: 1rem; + font-weight: 500; +} diff --git a/src/components/errorMessage/ErrorMessage.tsx b/src/components/errorMessage/ErrorMessage.tsx new file mode 100644 index 00000000..41f9fa79 --- /dev/null +++ b/src/components/errorMessage/ErrorMessage.tsx @@ -0,0 +1,11 @@ +import styles from './ErrorMessage.module.css'; + +type ErrorMessageProps = { + message: string; +}; + +const ErrorMessage = ({ message }: ErrorMessageProps) => { + return

{message}

; +}; + +export default ErrorMessage; diff --git a/src/components/errorMessage/index.ts b/src/components/errorMessage/index.ts new file mode 100644 index 00000000..43c8c9cc --- /dev/null +++ b/src/components/errorMessage/index.ts @@ -0,0 +1 @@ +export { default } from './ErrorMessage'; diff --git a/src/components/index.ts b/src/components/index.ts index df511070..24f98502 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -1,3 +1,6 @@ export { default as BreedModal } from './breedModal'; +export { default as Card } from './card'; +export { default as ErrorMessage } from './errorMessage'; +export { default as LoadingSpinner } from './loadingSpinner'; export { default as Modal } from './modal'; export { default as Navigation } from './navigation'; diff --git a/src/components/loadingSpinner/LoadingSpinner.module.css b/src/components/loadingSpinner/LoadingSpinner.module.css new file mode 100644 index 00000000..14ac0590 --- /dev/null +++ b/src/components/loadingSpinner/LoadingSpinner.module.css @@ -0,0 +1,32 @@ +.loadingContainer { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 3rem 0; +} + +.spinner { + width: 3rem; + height: 3rem; + border: 4px solid #e5d5b8; + border-top: 4px solid #fcc76f; + border-radius: 50%; + animation: spin 0.8s linear infinite; +} + +@keyframes spin { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } +} + +.loadingMessage { + margin-top: 1rem; + color: #e5d5b8; + font-size: 1.5rem; + font-weight: 600; +} diff --git a/src/components/loadingSpinner/LoadingSpinner.tsx b/src/components/loadingSpinner/LoadingSpinner.tsx new file mode 100644 index 00000000..3d8a685f --- /dev/null +++ b/src/components/loadingSpinner/LoadingSpinner.tsx @@ -0,0 +1,13 @@ +import styles from './LoadingSpinner.module.css'; +import C from './constants'; + +const LoadingSpinner = () => { + return ( +
+
+

{C.LOADING_TEXT}

+
+ ); +}; + +export default LoadingSpinner; diff --git a/src/components/loadingSpinner/constants.ts b/src/components/loadingSpinner/constants.ts new file mode 100644 index 00000000..2d1d687e --- /dev/null +++ b/src/components/loadingSpinner/constants.ts @@ -0,0 +1,3 @@ +const LOADING_TEXT = 'Loading ...'; + +export default { LOADING_TEXT }; diff --git a/src/components/loadingSpinner/index.ts b/src/components/loadingSpinner/index.ts new file mode 100644 index 00000000..6513c5cb --- /dev/null +++ b/src/components/loadingSpinner/index.ts @@ -0,0 +1 @@ +export { default } from './LoadingSpinner'; diff --git a/src/main.tsx b/src/main.tsx index f8a2b612..96449e8f 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,13 +1,17 @@ import { BrowserRouter } from 'react-router-dom'; import { createRoot } from 'react-dom/client'; +import { Provider } from 'react-redux'; import App from './App'; +import { store } from './store/store'; import './index.css'; createRoot(document.getElementById('root')!).render( - - - + + + + + ); diff --git a/src/views/gallery/Gallery.module.css b/src/views/gallery/Gallery.module.css new file mode 100644 index 00000000..8f69acfc --- /dev/null +++ b/src/views/gallery/Gallery.module.css @@ -0,0 +1,101 @@ +.container { + padding: 2rem; + max-width: 1200px; + margin: 0 auto; + background: transparent; +} + +.title { + margin-bottom: 2rem; + text-align: center; + color: #3d2817; + font-weight: 800; + font-size: 2rem; +} + +.grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 1.5rem; + margin-bottom: 2rem; +} + +@media (max-width: 768px) { + .grid { + grid-template-columns: repeat(2, 1fr); + gap: 1rem; + } +} + +@media (max-width: 480px) { + .grid { + grid-template-columns: 1fr; + } +} + +/* Button Row for side-by-side buttons */ +.buttonRow { + display: flex; + justify-content: center; + gap: 1.5rem; + margin: 2rem 0; +} + +.button { + padding: 1rem 2.5rem; + font-size: 1.1rem; + background: #fcc76f; + color: #3d2817; + border: none; + border-radius: 2rem; + cursor: pointer; + transition: all 0.2s ease; + box-shadow: 0 4px 8px rgba(61, 40, 23, 0.15); + font-weight: 600; +} + +.button:hover:not(:disabled) { + transform: translateY(-2px); + box-shadow: 0 6px 12px rgba(61, 40, 23, 0.2); + background: #fbb94e; +} + +.button:disabled { + background: #e5d5b8; + cursor: not-allowed; + box-shadow: none; + color: #8b7355; +} + +.spinner { + display: inline-block; + width: 1.2em; + height: 1.2em; + border: 3px solid #fcc76f; + border-top: 3px solid #fbb94e; + border-radius: 50%; + animation: spin 0.7s linear infinite; + vertical-align: middle; + margin-right: 0.5em; +} + +@keyframes spin { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } +} + +.error-message { + color: #dc3545; + text-align: center; + margin-bottom: 1rem; +} + +.errorMessage { + color: #dc3545; + text-align: center; + margin-bottom: 1rem; +} diff --git a/src/views/gallery/Gallery.tsx b/src/views/gallery/Gallery.tsx index f3193599..ec183d86 100644 --- a/src/views/gallery/Gallery.tsx +++ b/src/views/gallery/Gallery.tsx @@ -1,5 +1,54 @@ +import { useEffect } from 'react'; +import { Outlet } from 'react-router-dom'; + +import useGallery from './useGallery'; + +import { Card } from '../../components'; +import GalleryButtons from './GalleryButtons'; +import { ErrorMessage, LoadingSpinner } from '../../components'; + +import C from './constants'; + +import styles from './gallery.module.css'; + const Gallery = () => { - return <>Gallery; + const { + cachedCats, + isInitialLoading, + isLoading, + isGalleryEmpty, + error, + loadCats, + clearCats, + } = useGallery(); + + useEffect(() => { + if (isGalleryEmpty) { + loadCats(); + } + }, []); + + if (isInitialLoading) { + return ; + } + + return ( +
+

{C.TITLE_TEXT}

+
+ {cachedCats.map((cat) => ( + + ))} +
+ {error && } + + +
+ ); }; export default Gallery; diff --git a/src/views/gallery/GalleryButtons.tsx b/src/views/gallery/GalleryButtons.tsx new file mode 100644 index 00000000..e7fd9a8f --- /dev/null +++ b/src/views/gallery/GalleryButtons.tsx @@ -0,0 +1,38 @@ +import C from './constants'; +import styles from './gallery.module.css'; + +type GalleryButtonsProps = { + isLoading: boolean; + loadCats: () => void; + clearCats: () => void; +}; + +const GalleryButtons = ({ + isLoading, + loadCats, + clearCats, +}: GalleryButtonsProps) => { + return ( +
+ + +
+ ); +}; + +export default GalleryButtons; diff --git a/src/views/gallery/constants.ts b/src/views/gallery/constants.ts new file mode 100644 index 00000000..12e47f15 --- /dev/null +++ b/src/views/gallery/constants.ts @@ -0,0 +1,13 @@ +const CLEAR_BUTTON_TEXT = 'Clear cats'; +const ERROR_MESSAGE = 'Failed to load cats. Please try again'; +const LOAD_BUTTON_TEXT = 'Load more cats'; +const NUMBER_OF_CATS_TO_BE_FETCHED = 10; +const TITLE_TEXT = 'Meet the cats'; + +export default { + CLEAR_BUTTON_TEXT, + ERROR_MESSAGE, + LOAD_BUTTON_TEXT, + NUMBER_OF_CATS_TO_BE_FETCHED, + TITLE_TEXT, +}; diff --git a/src/views/gallery/useGallery.ts b/src/views/gallery/useGallery.ts new file mode 100644 index 00000000..d9425087 --- /dev/null +++ b/src/views/gallery/useGallery.ts @@ -0,0 +1,61 @@ +import { useCallback, useState } from 'react'; + +import { fetchRandomCats } from '../../api'; +import { galleryActions } from '../../store/gallerySlice'; +import { useCatsDispatch, useCatsSelector } from '../../store/hooks'; +import { selectFavourites, selectGallery } from '../../store/hooks'; + +import C from './constants'; +import type { Cat } from '../../types'; + +const useGallery = () => { + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + + const dispatch = useCatsDispatch(); + + const cachedCats: Cat[] = Object.values(useCatsSelector(selectGallery)); + const favouriteCats: Record = useCatsSelector(selectFavourites); + + const isGalleryEmpty = cachedCats.length === 0; + const isInitialLoading = isLoading && isGalleryEmpty; + + const loadCats = useCallback(() => { + setIsLoading(true); + setError(null); + + fetchRandomCats(C.NUMBER_OF_CATS_TO_BE_FETCHED) + .then((newCats) => { + const existingIds = new Set(cachedCats.map((cat) => cat.id)); + const uniqueNewCats = newCats.filter((cat) => !existingIds.has(cat.id)); + + if (uniqueNewCats.length) { + dispatch(galleryActions.addCats(uniqueNewCats)); + } + }) + .catch((error) => { + setError(C.ERROR_MESSAGE); + console.error(error); + }) + .finally(() => { + setIsLoading(false); + }); + }, [cachedCats, dispatch]); + + const clearCats = useCallback(() => { + dispatch(galleryActions.clearCats()); + }, [dispatch]); + + return { + cachedCats, + favouriteCats, + isInitialLoading, + isLoading, + isGalleryEmpty, + error, + loadCats, + clearCats, + }; +}; + +export default useGallery; From 6010acb228aeaac406a2ca19f70f918f1b166c2f Mon Sep 17 00:00:00 2001 From: mkour Date: Sat, 15 Nov 2025 21:35:08 +0200 Subject: [PATCH 07/17] add reusable favourite button component --- src/components/card/Card.tsx | 13 ++- .../FavouriteButton.module.css | 92 +++++++++++++++++++ .../favouriteButton/FavouriteButton.tsx | 32 +++++++ .../favouriteButton/FavouriteButtonImage.tsx | 29 ++++++ src/components/favouriteButton/constants.ts | 13 +++ src/components/favouriteButton/index.tsx | 1 + .../favouriteButton/useFavouriteButton.ts | 39 ++++++++ src/components/favouriteButton/utils.ts | 31 +++++++ src/views/gallery/Gallery.tsx | 8 +- 9 files changed, 256 insertions(+), 2 deletions(-) create mode 100644 src/components/favouriteButton/FavouriteButton.module.css create mode 100644 src/components/favouriteButton/FavouriteButton.tsx create mode 100644 src/components/favouriteButton/FavouriteButtonImage.tsx create mode 100644 src/components/favouriteButton/constants.ts create mode 100644 src/components/favouriteButton/index.tsx create mode 100644 src/components/favouriteButton/useFavouriteButton.ts create mode 100644 src/components/favouriteButton/utils.ts diff --git a/src/components/card/Card.tsx b/src/components/card/Card.tsx index a4b9c567..5bbd6e5f 100644 --- a/src/components/card/Card.tsx +++ b/src/components/card/Card.tsx @@ -3,15 +3,17 @@ import useCard from './useCard'; import type { Cat } from '../../types'; import CardImage from './CardImage'; import CardSkeleton from './CardSkeleton'; +import FavouriteButton from '../favouriteButton'; import styles from './Card.module.css'; type CardProps = { cat: Cat; + isFavourite: boolean; hasTitle: boolean; }; -const Card = ({ cat, hasTitle }: CardProps) => { +const Card = ({ cat, isFavourite, hasTitle }: CardProps) => { const { breedText, isLoaded, getImagePosition, handleImageLoaded } = useCard({ cat, }); @@ -25,6 +27,15 @@ const Card = ({ cat, hasTitle }: CardProps) => { imagePosition={getImagePosition()} onLoad={handleImageLoaded} /> + {!hasTitle && ( +
+ +
+ )} {hasTitle && breedText && (
{breedText}
)} diff --git a/src/components/favouriteButton/FavouriteButton.module.css b/src/components/favouriteButton/FavouriteButton.module.css new file mode 100644 index 00000000..ba3c4641 --- /dev/null +++ b/src/components/favouriteButton/FavouriteButton.module.css @@ -0,0 +1,92 @@ +.compactButton, +.expandedButton { + display: flex; + align-items: center; + cursor: pointer; +} + +.compactButton { + background: none; + border: none; + box-shadow: none; + padding: 0.25rem; + justify-content: center; + transition: transform 0.2s; + color: #3d2817; + position: relative; +} +.compactButton:focus { + outline: none; + box-shadow: none; +} +.compactButton::after { + content: attr(data-tooltip); + position: absolute; + bottom: 100%; + left: 50%; + transform: translateX(-50%); + background: #3d2817; + color: #fff; + padding: 0.5rem 0.75rem; + border-radius: 0.5rem; + font-size: 0.75rem; + font-weight: 600; + white-space: nowrap; + opacity: 0; + pointer-events: none; + transition: opacity 0.2s; + z-index: 100; + margin-bottom: 0.5rem; + display: none; +} +.compactButton[data-tooltip]:not([data-tooltip=''])::after { + display: block; +} +.compactButton:hover { + transform: scale(1.15); +} +.compactButton:hover::after { + opacity: 1; +} +.compactButton.active:hover { + animation: heartBeat 0.3s ease-in-out; +} + +@keyframes heartBeat { + 0%, + 100% { + transform: scale(1); + } + 50% { + transform: scale(1.2); + } +} + +.expandedButton { + gap: 0.5rem; + padding: 0.75rem 1.5rem; + font-size: 1rem; + background: #fff; + color: #3d2817; + border: 2px solid #3d2817; + border-radius: 2rem; + transition: all 0.2s; + font-weight: 600; + display: flex; + align-items: center; + justify-content: center; +} +.expandedButton:hover { + transform: translateY(-2px); + box-shadow: 0 4px 12px rgba(61, 40, 23, 0.2); + background: #fce8b8; +} +.expandedButton.active { + background: #ff6b9d; + color: #fff; + border-color: #ff6b9d; +} +.expandedButton.active:hover { + background: #ff4d88; + border-color: #ff4d88; +} diff --git a/src/components/favouriteButton/FavouriteButton.tsx b/src/components/favouriteButton/FavouriteButton.tsx new file mode 100644 index 00000000..89f84ad5 --- /dev/null +++ b/src/components/favouriteButton/FavouriteButton.tsx @@ -0,0 +1,32 @@ +import useFavouriteButton from './useFavouriteButton'; + +import type { Cat } from '../../types'; +import FavouriteButtonImage from './FavouriteButtonImage'; + +type FavouriteButtonProps = { + cat: Cat; + isFavourite: boolean; + variant: 'compact' | 'expanded'; +}; + +const FavouriteButton = ({ + cat, + isFavourite, + variant, +}: FavouriteButtonProps) => { + const { text, buttonClass, fillColor, showText, handleFavouriteToggle } = + useFavouriteButton({ cat, isFavourite, variant }); + + return ( + + ); +}; + +export default FavouriteButton; diff --git a/src/components/favouriteButton/FavouriteButtonImage.tsx b/src/components/favouriteButton/FavouriteButtonImage.tsx new file mode 100644 index 00000000..6919e7e8 --- /dev/null +++ b/src/components/favouriteButton/FavouriteButtonImage.tsx @@ -0,0 +1,29 @@ +import C from './constants'; + +type FavouriteButtonImageProps = { + fillColor: string; + stroke?: string; +}; + +const FavouriteButtonImage = ({ + fillColor, + stroke = C.HEART_STROKE, +}: FavouriteButtonImageProps) => ( + +); + +export default FavouriteButtonImage; diff --git a/src/components/favouriteButton/constants.ts b/src/components/favouriteButton/constants.ts new file mode 100644 index 00000000..d5994cd4 --- /dev/null +++ b/src/components/favouriteButton/constants.ts @@ -0,0 +1,13 @@ +const ADD_TEXT = 'Add to favorites'; +const HEART_FILL_COMPACT = '#ff4d88'; +const HEART_FILL_EXPANDED = '#fff'; +const HEART_STROKE = '#ff4d88'; +const REMOVE_TEXT = 'Remove from favorites'; + +export default { + ADD_TEXT, + HEART_FILL_COMPACT, + HEART_FILL_EXPANDED, + HEART_STROKE, + REMOVE_TEXT, +}; diff --git a/src/components/favouriteButton/index.tsx b/src/components/favouriteButton/index.tsx new file mode 100644 index 00000000..8b45f738 --- /dev/null +++ b/src/components/favouriteButton/index.tsx @@ -0,0 +1 @@ +export { default } from './FavouriteButton'; diff --git a/src/components/favouriteButton/useFavouriteButton.ts b/src/components/favouriteButton/useFavouriteButton.ts new file mode 100644 index 00000000..4b1c148c --- /dev/null +++ b/src/components/favouriteButton/useFavouriteButton.ts @@ -0,0 +1,39 @@ +import { favouritesActions } from '../../store/favouritesSlice'; +import { useCatsDispatch } from '../../store/hooks'; +import type { Cat } from '../../types'; + +import U from './utils'; + +type UseFavouriteButtonProps = { + cat: Cat; + isFavourite: boolean; + variant: 'compact' | 'expanded'; +}; + +const useFavouriteButton = ({ + cat, + isFavourite, + variant, +}: UseFavouriteButtonProps) => { + const dispatch = useCatsDispatch(); + + const handleFavouriteToggle = (event: React.MouseEvent) => { + event.stopPropagation(); + dispatch(favouritesActions.toggleFavourite(cat)); + }; + + const isCompact = variant === 'compact'; + const text = U.getButtonText(isFavourite); + const fillColor = U.getHeartFillColor(isCompact, isFavourite); + const buttonClass = U.getButtonClassName(isCompact, isFavourite); + + return { + text, + buttonClass, + fillColor, + showText: !isCompact, + handleFavouriteToggle, + }; +}; + +export default useFavouriteButton; diff --git a/src/components/favouriteButton/utils.ts b/src/components/favouriteButton/utils.ts new file mode 100644 index 00000000..2e763b2c --- /dev/null +++ b/src/components/favouriteButton/utils.ts @@ -0,0 +1,31 @@ +import C from './constants'; + +import styles from './FavouriteButton.module.css'; + +const getHeartFillColor = ( + isCompact: boolean, + isFavourite: boolean +): string => { + if (!isFavourite) return 'none'; + return isCompact ? C.HEART_FILL_COMPACT : C.HEART_FILL_EXPANDED; +}; + +const getButtonClassName = ( + isCompact: boolean, + isFavourite: boolean +): string => { + const baseClass = isCompact ? styles.compactButton : styles.expandedButton; + return isFavourite ? `${baseClass} ${styles.active}` : baseClass; +}; + +const getButtonText = (isFavourite: boolean): string => { + return isFavourite ? C.REMOVE_TEXT : C.ADD_TEXT; +}; + +const U = { + getHeartFillColor, + getButtonClassName, + getButtonText, +}; + +export default U; diff --git a/src/views/gallery/Gallery.tsx b/src/views/gallery/Gallery.tsx index ec183d86..cd6bd893 100644 --- a/src/views/gallery/Gallery.tsx +++ b/src/views/gallery/Gallery.tsx @@ -14,6 +14,7 @@ import styles from './gallery.module.css'; const Gallery = () => { const { cachedCats, + favouriteCats, isInitialLoading, isLoading, isGalleryEmpty, @@ -37,7 +38,12 @@ const Gallery = () => {

{C.TITLE_TEXT}

{cachedCats.map((cat) => ( - + ))}
{error && } From afc0089fd2c001261d3c2dd2d3b4b61fec2ccc5e Mon Sep 17 00:00:00 2001 From: mkour Date: Sat, 15 Nov 2025 21:57:53 +0200 Subject: [PATCH 08/17] implement favourites view --- .../loadingSpinner/LoadingSpinner.module.css | 2 +- src/views/favourites/Favourites.module.css | 42 +++++++++++++++++++ src/views/favourites/Favourites.tsx | 30 ++++++++++++- src/views/favourites/constants.ts | 7 ++++ src/views/favourites/useFavourites.ts | 14 +++++++ 5 files changed, 93 insertions(+), 2 deletions(-) create mode 100644 src/views/favourites/Favourites.module.css create mode 100644 src/views/favourites/constants.ts create mode 100644 src/views/favourites/useFavourites.ts diff --git a/src/components/loadingSpinner/LoadingSpinner.module.css b/src/components/loadingSpinner/LoadingSpinner.module.css index 14ac0590..da749d6d 100644 --- a/src/components/loadingSpinner/LoadingSpinner.module.css +++ b/src/components/loadingSpinner/LoadingSpinner.module.css @@ -26,7 +26,7 @@ .loadingMessage { margin-top: 1rem; - color: #e5d5b8; + color: #3d2817; font-size: 1.5rem; font-weight: 600; } diff --git a/src/views/favourites/Favourites.module.css b/src/views/favourites/Favourites.module.css new file mode 100644 index 00000000..1d0020a0 --- /dev/null +++ b/src/views/favourites/Favourites.module.css @@ -0,0 +1,42 @@ +.container { + padding: 2rem; + max-width: 1200px; + margin: 0 auto; + background: transparent; +} + +.title { + margin-bottom: 2rem; + text-align: center; + color: #3d2817; + font-weight: 800; + font-size: 2rem; +} + +.grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 1.5rem; + margin-bottom: 2rem; +} + +@media (max-width: 768px) { + .grid { + grid-template-columns: repeat(2, 1fr); + gap: 1rem; + } +} + +@media (max-width: 480px) { + .grid { + grid-template-columns: 1fr; + } +} + +.emptyMessage { + text-align: center; + color: #8b7355; + font-size: 1.2rem; + margin: 3rem 0; + font-weight: 500; +} diff --git a/src/views/favourites/Favourites.tsx b/src/views/favourites/Favourites.tsx index 27d09f1c..e8000571 100644 --- a/src/views/favourites/Favourites.tsx +++ b/src/views/favourites/Favourites.tsx @@ -1,5 +1,33 @@ +import { Outlet } from 'react-router-dom'; +import Card from '../../components/card'; +import C from './constants'; +import styles from './favourites.module.css'; +import useFavourites from './useFavourites'; + const Favourites = () => { - return <>Favourites; + const { favouriteCats, isFavouritesEmpty } = useFavourites(); + + if (isFavouritesEmpty) { + return ( +
+

{C.TITLE_TEXT}

+

{C.EMPTY_MESSAGE}

+ +
+ ); + } + + return ( +
+

{C.TITLE_TEXT}

+
+ {favouriteCats.map((cat) => ( + + ))} +
+ +
+ ); }; export default Favourites; diff --git a/src/views/favourites/constants.ts b/src/views/favourites/constants.ts new file mode 100644 index 00000000..1fbf2f2f --- /dev/null +++ b/src/views/favourites/constants.ts @@ -0,0 +1,7 @@ +const EMPTY_MESSAGE = 'No favourite cats yet. Start exploring and add some!'; +const TITLE_TEXT = 'Your Favourite Cats'; + +export default { + EMPTY_MESSAGE, + TITLE_TEXT, +}; diff --git a/src/views/favourites/useFavourites.ts b/src/views/favourites/useFavourites.ts new file mode 100644 index 00000000..3dbad2b9 --- /dev/null +++ b/src/views/favourites/useFavourites.ts @@ -0,0 +1,14 @@ +import { selectFavourites, useCatsSelector } from '../../store/hooks'; +import type { Cat } from '../../types'; + +const useFavourites = () => { + const favouriteCats: Cat[] = Object.values(useCatsSelector(selectFavourites)); + const isFavouritesEmpty: boolean = favouriteCats.length === 0; + + return { + favouriteCats, + isFavouritesEmpty, + }; +}; + +export default useFavourites; From f614fcfff072069ea2c84ebc67919a7b67216905 Mon Sep 17 00:00:00 2001 From: mkour Date: Sat, 15 Nov 2025 22:30:31 +0200 Subject: [PATCH 09/17] implement breeds view - minor redux store fixes --- src/main.tsx | 2 +- src/store/hooks.ts | 2 +- src/store/store.ts | 16 ---------- src/views/breeds/Breeds.module.css | 48 ++++++++++++++++++++++++++++ src/views/breeds/Breeds.tsx | 39 ++++++++++++++++++++++- src/views/breeds/constants.ts | 7 +++++ src/views/breeds/useBreeds.ts | 50 ++++++++++++++++++++++++++++++ src/views/breeds/utils.ts | 10 ++++++ 8 files changed, 155 insertions(+), 19 deletions(-) delete mode 100644 src/store/store.ts create mode 100644 src/views/breeds/Breeds.module.css create mode 100644 src/views/breeds/constants.ts create mode 100644 src/views/breeds/useBreeds.ts create mode 100644 src/views/breeds/utils.ts diff --git a/src/main.tsx b/src/main.tsx index 96449e8f..194761a1 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -4,7 +4,7 @@ import { createRoot } from 'react-dom/client'; import { Provider } from 'react-redux'; import App from './App'; -import { store } from './store/store'; +import store from './store'; import './index.css'; diff --git a/src/store/hooks.ts b/src/store/hooks.ts index c370669b..d098d10a 100644 --- a/src/store/hooks.ts +++ b/src/store/hooks.ts @@ -13,4 +13,4 @@ export const selectFavourites = (state: RootState) => state.favourites; export const selectGallery = (state: RootState) => state.gallery; -export const selectBreeds = (state: RootState) => state.breeds.breeds; +export const selectBreeds = (state: RootState) => state.breeds; diff --git a/src/store/store.ts b/src/store/store.ts deleted file mode 100644 index e682a512..00000000 --- a/src/store/store.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { configureStore } from '@reduxjs/toolkit'; - -import breedsReducer from './breedsSlice'; -import favouritesReducer from './favouritesSlice'; -import galleryReducer from './gallerySlice'; - -export const store = configureStore({ - reducer: { - favourites: favouritesReducer, - gallery: galleryReducer, - breeds: breedsReducer, - }, -}); - -export type RootState = ReturnType; -export type AppDispatch = typeof store.dispatch; diff --git a/src/views/breeds/Breeds.module.css b/src/views/breeds/Breeds.module.css new file mode 100644 index 00000000..955a4d7a --- /dev/null +++ b/src/views/breeds/Breeds.module.css @@ -0,0 +1,48 @@ +.container { + padding: 2rem; + max-width: 1200px; + margin: 0 auto; + background: transparent; +} + +.title { + margin-bottom: 2rem; + text-align: center; + color: #3d2817; + font-weight: 800; + font-size: 2rem; +} + +.grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 1.5rem; + margin-bottom: 2rem; +} + +@media (max-width: 768px) { + .grid { + grid-template-columns: repeat(2, 1fr); + gap: 1rem; + } +} + +@media (max-width: 480px) { + .grid { + grid-template-columns: 1fr; + } +} + +.errorMessage { + color: #dc3545; + text-align: center; + margin-bottom: 1rem; + font-size: 1.1rem; +} + +.loading { + text-align: center; + padding: 3rem; + font-size: 1.125rem; + color: #3d2817; +} diff --git a/src/views/breeds/Breeds.tsx b/src/views/breeds/Breeds.tsx index a51fe6c0..1b23f2e4 100644 --- a/src/views/breeds/Breeds.tsx +++ b/src/views/breeds/Breeds.tsx @@ -1,5 +1,42 @@ +import { useEffect } from 'react'; +import { Outlet } from 'react-router-dom'; +import useBreeds from './useBreeds'; +import { Card, ErrorMessage, LoadingSpinner } from '../../components'; +import U from './utils'; +import C from './constants'; +import styles from './breeds.module.css'; + const Breeds = () => { - return <>Breeds; + const { cachedBreeds, areBreedsEmpty, isInitialLoading, error, loadBreeds } = + useBreeds(); + + useEffect(() => { + if (areBreedsEmpty) { + loadBreeds(); + } + }, []); + + if (isInitialLoading) { + return ; + } + + return ( +
+

{C.TITLE_TEXT}

+
+ {cachedBreeds.map((breed) => ( + + ))} +
+ {error && } + +
+ ); }; export default Breeds; diff --git a/src/views/breeds/constants.ts b/src/views/breeds/constants.ts new file mode 100644 index 00000000..4368fef6 --- /dev/null +++ b/src/views/breeds/constants.ts @@ -0,0 +1,7 @@ +const TITLE_TEXT = 'Cat Breeds'; +const ERROR_MESSAGE = 'Failed to load breeds. Please try again.'; + +export default { + TITLE_TEXT, + ERROR_MESSAGE, +}; diff --git a/src/views/breeds/useBreeds.ts b/src/views/breeds/useBreeds.ts new file mode 100644 index 00000000..317539a8 --- /dev/null +++ b/src/views/breeds/useBreeds.ts @@ -0,0 +1,50 @@ +import { useCallback, useState } from 'react'; + +import { fetchAllBreeds } from '../../api'; +import { breedsActions } from '../../store/breedsSlice'; +import { + selectBreeds, + useCatsDispatch, + useCatsSelector, +} from '../../store/hooks'; +import type { Breed } from '../../types'; + +import C from './constants'; + +const useBreeds = () => { + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + + const dispatch = useCatsDispatch(); + const cachedBreeds: Breed[] = Object.values(useCatsSelector(selectBreeds)); + + const areBreedsEmpty: boolean = cachedBreeds.length === 0; + const isInitialLoading: boolean = areBreedsEmpty && isLoading; + + const loadBreeds = useCallback(() => { + setIsLoading(true); + setError(null); + + fetchAllBreeds() + .then((breeds: Breed[]) => { + dispatch(breedsActions.addBreeds(breeds)); + }) + .catch((error) => { + setError(C.ERROR_MESSAGE); + console.error(error); + }) + .finally(() => { + setIsLoading(false); + }); + }, [dispatch]); + + return { + cachedBreeds, + areBreedsEmpty, + isInitialLoading, + error, + loadBreeds, + }; +}; + +export default useBreeds; diff --git a/src/views/breeds/utils.ts b/src/views/breeds/utils.ts new file mode 100644 index 00000000..a5cf228a --- /dev/null +++ b/src/views/breeds/utils.ts @@ -0,0 +1,10 @@ +import type { Breed, Cat } from '../../types'; + +const transformBreedToCatObject = (breed: Breed): Cat => { + return { + ...breed.image, + breeds: [breed], + }; +}; + +export default { transformBreedToCatObject }; From 6db7db6c405df8aca54d079a004e61c70c2ec7a4 Mon Sep 17 00:00:00 2001 From: mkour Date: Sat, 15 Nov 2025 23:23:33 +0200 Subject: [PATCH 10/17] refactored code - Updated import order rules in eslint.config.js for consistent imports - Extracted shared view layout styles to viewsLayout.module.css and updated all views - Removed console.log statements from Redux slices - Refactored custom hooks for clarity and best practices --- eslint.config.js | 30 ++++---- src/App.tsx | 4 +- src/api/services.ts | 2 +- src/components/card/Card.tsx | 5 +- src/components/card/CardImage.tsx | 3 +- src/components/card/useCard.ts | 3 +- .../favouriteButton/FavouriteButton.tsx | 2 +- .../favouriteButton/useFavouriteButton.ts | 2 +- src/components/favouriteButton/utils.ts | 1 - .../loadingSpinner/LoadingSpinner.tsx | 2 +- src/components/navigation/Navigation.tsx | 1 - src/components/navigation/useNavigation.ts | 3 +- src/main.tsx | 6 +- src/store/gallerySlice.ts | 2 - src/store/index.ts | 1 - src/views/breeds/Breeds.tsx | 10 +-- src/views/breeds/useBreeds.ts | 2 +- src/views/favourites/Favourites.module.css | 35 ---------- src/views/favourites/Favourites.tsx | 15 ++-- src/views/favourites/useFavourites.ts | 1 + src/views/gallery/Gallery.module.css | 68 ------------------- src/views/gallery/Gallery.tsx | 12 ++-- src/views/gallery/useGallery.ts | 19 +++--- .../viewsLayout.module.css} | 14 ---- vite.config.ts | 2 +- 25 files changed, 61 insertions(+), 184 deletions(-) rename src/views/{breeds/Breeds.module.css => shared/viewsLayout.module.css} (71%) diff --git a/eslint.config.js b/eslint.config.js index b6fd5b07..0efc9b37 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -33,23 +33,23 @@ export default defineConfig([ 'error', { groups: [ - // 1. React and React Router DOM - ['^react$', '^react-router-dom'], - // 2. Redux hooks and third-party libraries (excluding type imports) - ['^(?!.*\\btype\\b)@?\\w'], - // 3. Custom hooks (relative imports starting with use*.ts) - ['^\\.+\\/use[A-Z]\\w*$'], - // 4. Component imports (not utils, constants, hooks, or types) + // 1. React & React Router + ['^react', '^react-router'], + // 2. Third-party libraries & Relative imports (no empty line between) [ - '^(?!.*\\btype\\b)(?!.*(?:\\/utils|\\/constants|\\/use[A-Z])).*\\/components\\/', + '^[^./@]', + '^@(?!.*/types)', + '^\\./(?!(?:utils|constants))', + '^\\.\\./(?!(?:utils|constants))', + ], + // 3. Utils, Constants, CSS modules, Types (all together after empty line) + [ + 'utils', + 'constants', + '\\.module\\.css$', + '^.*/types', + '^@.*/types', ], - ['^(?!.*\\btype\\b)\\.\\.?\\/(?!(?:utils|constants|use[A-Z]))'], - // 5. Utils and Constants - ['^\\.+\\/(?:utils|constants)$'], - // 6. CSS modules - ['\\.module\\.css$'], - // 7. Type imports - ['^.*\\btype\\b'], ], }, ], diff --git a/src/App.tsx b/src/App.tsx index 0ef188e3..948f210b 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,9 +1,7 @@ +import './App.css'; import Navigation from './components/navigation'; - import AppRouter from './router'; -import './App.css'; - const App = () => { return (
diff --git a/src/api/services.ts b/src/api/services.ts index fadf88f9..4ec0df09 100644 --- a/src/api/services.ts +++ b/src/api/services.ts @@ -1,8 +1,8 @@ -import type { Breed, Cat } from '../types'; import api from './api'; import endpoints from './endpoints'; import C from './constants'; +import type { Breed, Cat } from '../types'; const fetchRandomCats = async ( limit: number = C.DEFAULT_CATS_LIMIT diff --git a/src/components/card/Card.tsx b/src/components/card/Card.tsx index 5bbd6e5f..0a75634e 100644 --- a/src/components/card/Card.tsx +++ b/src/components/card/Card.tsx @@ -1,11 +1,10 @@ -import useCard from './useCard'; - -import type { Cat } from '../../types'; import CardImage from './CardImage'; import CardSkeleton from './CardSkeleton'; +import useCard from './useCard'; import FavouriteButton from '../favouriteButton'; import styles from './Card.module.css'; +import type { Cat } from '../../types'; type CardProps = { cat: Cat; diff --git a/src/components/card/CardImage.tsx b/src/components/card/CardImage.tsx index e660358e..4731ea3f 100644 --- a/src/components/card/CardImage.tsx +++ b/src/components/card/CardImage.tsx @@ -1,6 +1,5 @@ -import type { Cat } from '../../types'; - import styles from './Card.module.css'; +import type { Cat } from '../../types'; type CardImageProps = { cat: Cat; diff --git a/src/components/card/useCard.ts b/src/components/card/useCard.ts index 129735ee..7a48103f 100644 --- a/src/components/card/useCard.ts +++ b/src/components/card/useCard.ts @@ -1,8 +1,7 @@ import { useState } from 'react'; -import type { Breed, Cat } from '../../types'; - import C from './constants'; +import type { Breed, Cat } from '../../types'; type UseCardProps = { cat: Cat; diff --git a/src/components/favouriteButton/FavouriteButton.tsx b/src/components/favouriteButton/FavouriteButton.tsx index 89f84ad5..7772d466 100644 --- a/src/components/favouriteButton/FavouriteButton.tsx +++ b/src/components/favouriteButton/FavouriteButton.tsx @@ -1,7 +1,7 @@ +import FavouriteButtonImage from './FavouriteButtonImage'; import useFavouriteButton from './useFavouriteButton'; import type { Cat } from '../../types'; -import FavouriteButtonImage from './FavouriteButtonImage'; type FavouriteButtonProps = { cat: Cat; diff --git a/src/components/favouriteButton/useFavouriteButton.ts b/src/components/favouriteButton/useFavouriteButton.ts index 4b1c148c..147d60d5 100644 --- a/src/components/favouriteButton/useFavouriteButton.ts +++ b/src/components/favouriteButton/useFavouriteButton.ts @@ -1,8 +1,8 @@ import { favouritesActions } from '../../store/favouritesSlice'; import { useCatsDispatch } from '../../store/hooks'; -import type { Cat } from '../../types'; import U from './utils'; +import type { Cat } from '../../types'; type UseFavouriteButtonProps = { cat: Cat; diff --git a/src/components/favouriteButton/utils.ts b/src/components/favouriteButton/utils.ts index 2e763b2c..1b8bad80 100644 --- a/src/components/favouriteButton/utils.ts +++ b/src/components/favouriteButton/utils.ts @@ -1,5 +1,4 @@ import C from './constants'; - import styles from './FavouriteButton.module.css'; const getHeartFillColor = ( diff --git a/src/components/loadingSpinner/LoadingSpinner.tsx b/src/components/loadingSpinner/LoadingSpinner.tsx index 3d8a685f..f09172db 100644 --- a/src/components/loadingSpinner/LoadingSpinner.tsx +++ b/src/components/loadingSpinner/LoadingSpinner.tsx @@ -1,5 +1,5 @@ -import styles from './LoadingSpinner.module.css'; import C from './constants'; +import styles from './LoadingSpinner.module.css'; const LoadingSpinner = () => { return ( diff --git a/src/components/navigation/Navigation.tsx b/src/components/navigation/Navigation.tsx index 47bce6d4..94ccb61b 100644 --- a/src/components/navigation/Navigation.tsx +++ b/src/components/navigation/Navigation.tsx @@ -3,7 +3,6 @@ import { Link } from 'react-router-dom'; import useNavigation from './useNavigation'; import C from './constants'; - import styles from './Navigation.module.css'; const Navigation = () => { diff --git a/src/components/navigation/useNavigation.ts b/src/components/navigation/useNavigation.ts index 05806d4a..0b2928b2 100644 --- a/src/components/navigation/useNavigation.ts +++ b/src/components/navigation/useNavigation.ts @@ -20,8 +20,7 @@ const useNavigation = (): UseNavigationState => { }; const isGalleryActive = - location.pathname === C.DEFAULT_PATH || - location.pathname.startsWith(C.CAT_PATH); + isActive(C.DEFAULT_PATH) || location.pathname.startsWith(C.CAT_PATH); const isBreedsActive = isActive(C.BREEDS_PATH); const isFavouritesActive = isActive(C.FAVOURITES_PATH); diff --git a/src/main.tsx b/src/main.tsx index 194761a1..5e88c23f 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,13 +1,11 @@ -import { BrowserRouter } from 'react-router-dom'; - import { createRoot } from 'react-dom/client'; import { Provider } from 'react-redux'; +import { BrowserRouter } from 'react-router-dom'; +import './index.css'; import App from './App'; import store from './store'; -import './index.css'; - createRoot(document.getElementById('root')!).render( diff --git a/src/store/gallerySlice.ts b/src/store/gallerySlice.ts index 9f0df121..5fbb7410 100644 --- a/src/store/gallerySlice.ts +++ b/src/store/gallerySlice.ts @@ -11,12 +11,10 @@ const gallerySlice = createSlice({ initialState: initialGalleryState, reducers: { addCats: (state, action: PayloadAction) => { - console.log('REDUCER CALLED - addCats action payload:', action.payload); const newState = { ...state }; action.payload.forEach((cat) => { newState[cat.id] = cat; }); - console.log('REDUCER - State after update:', newState); return newState; }, clearCats: () => initialGalleryState, diff --git a/src/store/index.ts b/src/store/index.ts index ad5121ba..e4287c59 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -1,5 +1,4 @@ import { configureStore } from '@reduxjs/toolkit'; - import breedsReducer from './breedsSlice'; import favouritesReducer from './favouritesSlice'; import galleryReducer from './gallerySlice'; diff --git a/src/views/breeds/Breeds.tsx b/src/views/breeds/Breeds.tsx index 1b23f2e4..58fd1744 100644 --- a/src/views/breeds/Breeds.tsx +++ b/src/views/breeds/Breeds.tsx @@ -1,10 +1,12 @@ import { useEffect } from 'react'; import { Outlet } from 'react-router-dom'; + import useBreeds from './useBreeds'; import { Card, ErrorMessage, LoadingSpinner } from '../../components'; + import U from './utils'; import C from './constants'; -import styles from './breeds.module.css'; +import layoutStyles from '../shared/viewsLayout.module.css'; const Breeds = () => { const { cachedBreeds, areBreedsEmpty, isInitialLoading, error, loadBreeds } = @@ -21,9 +23,9 @@ const Breeds = () => { } return ( -
-

{C.TITLE_TEXT}

-
+
+

{C.TITLE_TEXT}

+
{cachedBreeds.map((breed) => ( { const [isLoading, setIsLoading] = useState(false); diff --git a/src/views/favourites/Favourites.module.css b/src/views/favourites/Favourites.module.css index 1d0020a0..e45e2643 100644 --- a/src/views/favourites/Favourites.module.css +++ b/src/views/favourites/Favourites.module.css @@ -1,38 +1,3 @@ -.container { - padding: 2rem; - max-width: 1200px; - margin: 0 auto; - background: transparent; -} - -.title { - margin-bottom: 2rem; - text-align: center; - color: #3d2817; - font-weight: 800; - font-size: 2rem; -} - -.grid { - display: grid; - grid-template-columns: repeat(3, 1fr); - gap: 1.5rem; - margin-bottom: 2rem; -} - -@media (max-width: 768px) { - .grid { - grid-template-columns: repeat(2, 1fr); - gap: 1rem; - } -} - -@media (max-width: 480px) { - .grid { - grid-template-columns: 1fr; - } -} - .emptyMessage { text-align: center; color: #8b7355; diff --git a/src/views/favourites/Favourites.tsx b/src/views/favourites/Favourites.tsx index e8000571..3e9218c9 100644 --- a/src/views/favourites/Favourites.tsx +++ b/src/views/favourites/Favourites.tsx @@ -1,16 +1,19 @@ import { Outlet } from 'react-router-dom'; + +import useFavourites from './useFavourites'; import Card from '../../components/card'; + import C from './constants'; +import layoutStyles from '../shared/viewsLayout.module.css'; import styles from './favourites.module.css'; -import useFavourites from './useFavourites'; const Favourites = () => { const { favouriteCats, isFavouritesEmpty } = useFavourites(); if (isFavouritesEmpty) { return ( -
-

{C.TITLE_TEXT}

+
+

{C.TITLE_TEXT}

{C.EMPTY_MESSAGE}

@@ -18,9 +21,9 @@ const Favourites = () => { } return ( -
-

{C.TITLE_TEXT}

-
+
+

{C.TITLE_TEXT}

+
{favouriteCats.map((cat) => ( ))} diff --git a/src/views/favourites/useFavourites.ts b/src/views/favourites/useFavourites.ts index 3dbad2b9..3f2a87ab 100644 --- a/src/views/favourites/useFavourites.ts +++ b/src/views/favourites/useFavourites.ts @@ -1,4 +1,5 @@ import { selectFavourites, useCatsSelector } from '../../store/hooks'; + import type { Cat } from '../../types'; const useFavourites = () => { diff --git a/src/views/gallery/Gallery.module.css b/src/views/gallery/Gallery.module.css index 8f69acfc..cf9790ff 100644 --- a/src/views/gallery/Gallery.module.css +++ b/src/views/gallery/Gallery.module.css @@ -1,38 +1,3 @@ -.container { - padding: 2rem; - max-width: 1200px; - margin: 0 auto; - background: transparent; -} - -.title { - margin-bottom: 2rem; - text-align: center; - color: #3d2817; - font-weight: 800; - font-size: 2rem; -} - -.grid { - display: grid; - grid-template-columns: repeat(3, 1fr); - gap: 1.5rem; - margin-bottom: 2rem; -} - -@media (max-width: 768px) { - .grid { - grid-template-columns: repeat(2, 1fr); - gap: 1rem; - } -} - -@media (max-width: 480px) { - .grid { - grid-template-columns: 1fr; - } -} - /* Button Row for side-by-side buttons */ .buttonRow { display: flex; @@ -66,36 +31,3 @@ box-shadow: none; color: #8b7355; } - -.spinner { - display: inline-block; - width: 1.2em; - height: 1.2em; - border: 3px solid #fcc76f; - border-top: 3px solid #fbb94e; - border-radius: 50%; - animation: spin 0.7s linear infinite; - vertical-align: middle; - margin-right: 0.5em; -} - -@keyframes spin { - 0% { - transform: rotate(0deg); - } - 100% { - transform: rotate(360deg); - } -} - -.error-message { - color: #dc3545; - text-align: center; - margin-bottom: 1rem; -} - -.errorMessage { - color: #dc3545; - text-align: center; - margin-bottom: 1rem; -} diff --git a/src/views/gallery/Gallery.tsx b/src/views/gallery/Gallery.tsx index cd6bd893..56029550 100644 --- a/src/views/gallery/Gallery.tsx +++ b/src/views/gallery/Gallery.tsx @@ -1,15 +1,13 @@ import { useEffect } from 'react'; import { Outlet } from 'react-router-dom'; +import GalleryButtons from './GalleryButtons'; import useGallery from './useGallery'; - import { Card } from '../../components'; -import GalleryButtons from './GalleryButtons'; import { ErrorMessage, LoadingSpinner } from '../../components'; import C from './constants'; - -import styles from './gallery.module.css'; +import layoutStyles from '../shared/viewsLayout.module.css'; const Gallery = () => { const { @@ -34,9 +32,9 @@ const Gallery = () => { } return ( -
-

{C.TITLE_TEXT}

-
+
+

{C.TITLE_TEXT}

+
{cachedCats.map((cat) => ( { + const [cats, setCats] = useState([]); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); @@ -26,24 +27,26 @@ const useGallery = () => { fetchRandomCats(C.NUMBER_OF_CATS_TO_BE_FETCHED) .then((newCats) => { - const existingIds = new Set(cachedCats.map((cat) => cat.id)); - const uniqueNewCats = newCats.filter((cat) => !existingIds.has(cat.id)); + const existingCats = new Set(cats.map((cat) => cat.id)); + const uniqueNewCats = newCats.filter( + (cat) => !existingCats.has(cat.id) + ); - if (uniqueNewCats.length) { - dispatch(galleryActions.addCats(uniqueNewCats)); - } + dispatch(galleryActions.addCats(uniqueNewCats)); + setCats((prev) => [...prev, ...uniqueNewCats]); }) - .catch((error) => { + .catch((err) => { setError(C.ERROR_MESSAGE); - console.error(error); + console.error(err); }) .finally(() => { setIsLoading(false); }); - }, [cachedCats, dispatch]); + }, [cats, dispatch]); const clearCats = useCallback(() => { dispatch(galleryActions.clearCats()); + setCats([]); }, [dispatch]); return { diff --git a/src/views/breeds/Breeds.module.css b/src/views/shared/viewsLayout.module.css similarity index 71% rename from src/views/breeds/Breeds.module.css rename to src/views/shared/viewsLayout.module.css index 955a4d7a..f220f9dd 100644 --- a/src/views/breeds/Breeds.module.css +++ b/src/views/shared/viewsLayout.module.css @@ -32,17 +32,3 @@ grid-template-columns: 1fr; } } - -.errorMessage { - color: #dc3545; - text-align: center; - margin-bottom: 1rem; - font-size: 1.1rem; -} - -.loading { - text-align: center; - padding: 3rem; - font-size: 1.125rem; - color: #3d2817; -} diff --git a/vite.config.ts b/vite.config.ts index 9982072c..8b0f57b9 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,5 +1,5 @@ -import react from '@vitejs/plugin-react' import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' // https://vite.dev/config/ export default defineConfig({ From 8a3108d99f0ff360c2479600e84c8e0433dbd3b0 Mon Sep 17 00:00:00 2001 From: mkour Date: Sun, 16 Nov 2025 11:43:25 +0200 Subject: [PATCH 11/17] implement cat and breed modals - Add Modal and BreedModal with custom hooks for state management - Separate UI into subcomponents - Centralize constants and utility functions for maintainability - Handle loading, error, and empty states --- .../breedModal/BreedModal.module.css | 256 ++++++++++++++++++ src/components/breedModal/BreedModal.tsx | 53 +++- .../breedModal/components/Details.tsx | 27 ++ src/components/breedModal/components/Grid.tsx | 31 +++ .../breedModal/components/Wrapper.tsx | 20 ++ src/components/breedModal/components/index.ts | 3 + src/components/breedModal/constants.ts | 37 +++ src/components/breedModal/useBreedModal.ts | 61 +++++ src/components/breedModal/utils.ts | 37 +++ src/components/card/Card.tsx | 16 +- src/components/card/useCard.ts | 40 +-- src/components/card/utils.ts | 18 ++ .../favouriteButton/useFavouriteButton.ts | 1 - src/components/modal/Modal.module.css | 181 +++++++++++++ src/components/modal/Modal.tsx | 55 +++- src/components/modal/components/Details.tsx | 35 +++ src/components/modal/components/Image.tsx | 14 + src/components/modal/components/Link.tsx | 23 ++ src/components/modal/components/Wrapper.tsx | 20 ++ src/components/modal/components/index.ts | 4 + src/components/modal/constants.ts | 25 ++ src/components/modal/useModal.ts | 80 ++++++ src/components/modal/utils.ts | 30 ++ src/components/navigation/Navigation.tsx | 2 +- src/components/navigation/constants.ts | 4 - src/components/navigation/useNavigation.ts | 4 +- src/views/breeds/Breeds.tsx | 2 +- src/views/favourites/Favourites.tsx | 2 +- src/views/gallery/Gallery.tsx | 2 +- 29 files changed, 1045 insertions(+), 38 deletions(-) create mode 100644 src/components/breedModal/BreedModal.module.css create mode 100644 src/components/breedModal/components/Details.tsx create mode 100644 src/components/breedModal/components/Grid.tsx create mode 100644 src/components/breedModal/components/Wrapper.tsx create mode 100644 src/components/breedModal/components/index.ts create mode 100644 src/components/breedModal/constants.ts create mode 100644 src/components/breedModal/useBreedModal.ts create mode 100644 src/components/breedModal/utils.ts create mode 100644 src/components/card/utils.ts create mode 100644 src/components/modal/Modal.module.css create mode 100644 src/components/modal/components/Details.tsx create mode 100644 src/components/modal/components/Image.tsx create mode 100644 src/components/modal/components/Link.tsx create mode 100644 src/components/modal/components/Wrapper.tsx create mode 100644 src/components/modal/components/index.ts create mode 100644 src/components/modal/constants.ts create mode 100644 src/components/modal/useModal.ts create mode 100644 src/components/modal/utils.ts diff --git a/src/components/breedModal/BreedModal.module.css b/src/components/breedModal/BreedModal.module.css new file mode 100644 index 00000000..f82951a4 --- /dev/null +++ b/src/components/breedModal/BreedModal.module.css @@ -0,0 +1,256 @@ +.overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(61, 40, 23, 0.8); + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; + padding: 2rem; + backdrop-filter: blur(4px); + overflow: hidden; +} + +html:has(.overlay) { + overflow: hidden; +} + +.modal { + background: #fff; + border-radius: 1rem; + max-width: 1200px; + width: 100%; + max-height: 90vh; + overflow-y: auto; + position: relative; + border: 3px solid #3d2817; + box-shadow: 0 8px 32px rgba(61, 40, 23, 0.3); + padding: 3rem; +} + +.closeButton { + position: absolute; + top: 1rem; + right: 1rem; + background: none; + border: none; + border-radius: 0; + width: auto; + height: auto; + font-size: 2rem; + cursor: pointer; + color: #3d2817; + font-weight: bold; + z-index: 10; + padding: 0; + line-height: 1; +} + +.closeButton:hover { + color: #ff6b9d; + background: none; + transform: none; +} + +.content { + width: 100%; +} + +.breedDetails { + text-align: center; + margin-bottom: 2rem; +} + +.breedHeader { + margin-bottom: 1.5rem; +} + +.breedName { + margin: 0 0 0.5rem 0; + font-size: 2.5rem; + font-weight: 800; + color: #3d2817; +} + +.breedOrigin { + margin: 0; + font-size: 1.125rem; + color: #5a4a3a; + font-weight: 600; +} + +.breedDescription { + margin: 0 0 2rem 0; + font-size: 1rem; + color: #5a4a3a; + line-height: 1.6; + text-align: center; +} + +.detailsGrid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 1rem; + background: #fce8b8; + padding: 1.5rem; + border-radius: 0.75rem; + text-align: left; +} + +.detailItem { + display: flex; + flex-direction: column; + gap: 0.25rem; + font-size: 0.9375rem; + color: #3d2817; +} + +.detailItem strong { + font-weight: 700; + font-size: 0.875rem; + text-transform: uppercase; + letter-spacing: 0.5px; + color: #3d2817; +} + +.detailItem span { + font-size: 1rem; + color: #5a4a3a; +} + +.imagesSection { + margin-top: 2.5rem; + padding-top: 2rem; + border-top: 2px solid #3d2817; +} + +.imagesSectionTitle { + margin: 0 0 1.5rem 0; + font-size: 1.5rem; + font-weight: 700; + color: #3d2817; + text-align: center; +} + +.imagesGrid { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 1rem; +} + +.imageCard { + background: #fff; + border-radius: 1rem; + overflow: hidden; + box-shadow: 0 2px 8px rgba(61, 40, 23, 0.1); + cursor: pointer; + transition: + transform 0.2s, + box-shadow 0.2s; + position: relative; + border: 3px solid #3d2817; + height: 200px; + display: block; + text-decoration: none; +} + +.imageCard:hover { + transform: translateY(-4px); + box-shadow: 0 6px 16px rgba(255, 107, 157, 0.3); +} + +.imageCard:nth-child(1) { + background-color: #fce8b8; +} + +.imageCard:nth-child(2) { + background-color: #a8d8d4; +} + +.imageCard:nth-child(3) { + background-color: #f5c9b4; +} + +.imageCard:nth-child(4) { + background-color: #f5a973; +} + +.gridImage { + width: 100%; + height: 100%; + object-fit: cover; + display: block; + padding: 10px; + box-sizing: border-box; + border-radius: 1rem; +} + +.loading, +.error { + text-align: center; + padding: 3rem; + font-size: 1.125rem; + color: #3d2817; +} + +.error { + color: #dc3545; +} + +.noImages { + text-align: center; + color: #8b7355; + font-style: italic; + padding: 2rem; + font-size: 1.1rem; +} + +@media (max-width: 768px) { + .overlay { + padding: 1rem; + } + + .modal { + max-height: 95vh; + padding: 2rem 1.5rem; + } + + .breedName { + font-size: 2rem; + } + + .detailsGrid { + grid-template-columns: 1fr; + padding: 1rem; + } + + .imagesGrid { + grid-template-columns: repeat(2, 1fr); + gap: 0.75rem; + } + + .imageCard { + height: 150px; + } +} + +@media (max-width: 480px) { + .modal { + padding: 1.5rem 1rem; + } + + .breedName { + font-size: 1.5rem; + } + + .detailsGrid { + gap: 0.75rem; + } + + .imagesGrid { + grid-template-columns: 1fr; + } +} diff --git a/src/components/breedModal/BreedModal.tsx b/src/components/breedModal/BreedModal.tsx index 8aa93bd3..3bb8a355 100644 --- a/src/components/breedModal/BreedModal.tsx +++ b/src/components/breedModal/BreedModal.tsx @@ -1,5 +1,56 @@ +import { useEffect } from 'react'; + +import { Details, Grid, Wrapper } from './components'; +import useBreedModal from './useBreedModal'; + +import C from './constants'; +import styles from './BreedModal.module.css'; + const BreedModal = () => { - return <>BreedModal; + const { + breed, + images, + isLoading, + error, + loadBreedImages, + handleBackdropClick, + handleClose, + } = useBreedModal(); + + useEffect(() => { + loadBreedImages(); + }, [loadBreedImages]); + + const renderContent = () => { + if (isLoading) { + return
{C.LOADING_TEXT}
; + } + + if (error || !breed) { + return
{error || C.ERROR_TEXT}
; + } + + return ( +
+
+ + {images.length > 0 && ( +
+

+ {C.EXAMPLE_IMAGES_TITLE} +

+ +
+ )} +
+ ); + }; + + return ( + + {renderContent()} + + ); }; export default BreedModal; diff --git a/src/components/breedModal/components/Details.tsx b/src/components/breedModal/components/Details.tsx new file mode 100644 index 00000000..0e92c5a7 --- /dev/null +++ b/src/components/breedModal/components/Details.tsx @@ -0,0 +1,27 @@ +import U from '../utils'; +import styles from '../BreedModal.module.css'; +import type { Breed } from '../../../types'; + +type DetailsProps = { + breed: Breed; +}; + +const Details = ({ breed }: DetailsProps) => ( +
+
+

{breed.name}

+

{breed.origin}

+
+

{breed.description}

+
+ {U.detailsData(breed).map(([label, value]) => ( +
+ {label} + {value} +
+ ))} +
+
+); + +export default Details; diff --git a/src/components/breedModal/components/Grid.tsx b/src/components/breedModal/components/Grid.tsx new file mode 100644 index 00000000..d0ca8450 --- /dev/null +++ b/src/components/breedModal/components/Grid.tsx @@ -0,0 +1,31 @@ +import { Link } from 'react-router-dom'; + +import C from '../constants'; +import styles from '../BreedModal.module.css'; +import type { Cat } from '../../../types'; + +type BreedImageGridProps = { + images: Cat[]; +}; + +const BreedImageGrid = ({ images }: BreedImageGridProps) => { + return ( +
+ {images.slice(0, C.MAX_IMAGES_TO_DISPLAY).map((image) => ( + + {`Cat + + ))} +
+ ); +}; + +export default BreedImageGrid; diff --git a/src/components/breedModal/components/Wrapper.tsx b/src/components/breedModal/components/Wrapper.tsx new file mode 100644 index 00000000..58886457 --- /dev/null +++ b/src/components/breedModal/components/Wrapper.tsx @@ -0,0 +1,20 @@ +import styles from '../BreedModal.module.css'; + +type WrapperProps = { + onBackdropClick: (e: React.MouseEvent) => void; + onClose: () => void; + children: React.ReactNode; +}; + +const Wrapper = ({ onBackdropClick, onClose, children }: WrapperProps) => ( +
+
+ + {children} +
+
+); + +export default Wrapper; diff --git a/src/components/breedModal/components/index.ts b/src/components/breedModal/components/index.ts new file mode 100644 index 00000000..3e6bf31d --- /dev/null +++ b/src/components/breedModal/components/index.ts @@ -0,0 +1,3 @@ +export { default as Details } from './Details'; +export { default as Grid } from './Grid'; +export { default as Wrapper } from './Wrapper'; diff --git a/src/components/breedModal/constants.ts b/src/components/breedModal/constants.ts new file mode 100644 index 00000000..27080afb --- /dev/null +++ b/src/components/breedModal/constants.ts @@ -0,0 +1,37 @@ +const DETAIL_LABELS_AFFECTION_LEVEL = 'Affection Level'; +const DETAIL_LABELS_CHILD_FRIENDLY = 'Child Friendly'; +const DETAIL_LABELS_DOG_FRIENDLY = 'Dog Friendly'; +const DETAIL_LABELS_LIFE_SPAN = 'Life Span'; +const DETAIL_LABELS_TEMPERAMENT = 'Temperament'; +const DETAIL_LABELS_WEIGHT = 'Weight'; +const ERROR_TEXT = 'Failed to load breed images'; +const EXAMPLE_IMAGES_TITLE = 'Breed Cats'; +const LOADING_TEXT = 'Loading breed images...'; +const MAX_IMAGES_TO_DISPLAY = 4; +const MAX_RATING = 5; +const NO_IMAGES_TEXT = 'No images found for this breed'; +const STAR_EMPTY = 'โ˜†'; +const STAR_FILLED = 'โ˜…'; +const WEIGHT_UNITS_KG = 'kg'; +const WEIGHT_UNITS_LBS = 'lbs'; +const YEARS_SUFFIX = 'years'; + +export default { + DETAIL_LABELS_AFFECTION_LEVEL, + DETAIL_LABELS_CHILD_FRIENDLY, + DETAIL_LABELS_DOG_FRIENDLY, + DETAIL_LABELS_LIFE_SPAN, + DETAIL_LABELS_TEMPERAMENT, + DETAIL_LABELS_WEIGHT, + ERROR_TEXT, + EXAMPLE_IMAGES_TITLE, + LOADING_TEXT, + MAX_IMAGES_TO_DISPLAY, + MAX_RATING, + NO_IMAGES_TEXT, + STAR_EMPTY, + STAR_FILLED, + WEIGHT_UNITS_KG, + WEIGHT_UNITS_LBS, + YEARS_SUFFIX, +}; diff --git a/src/components/breedModal/useBreedModal.ts b/src/components/breedModal/useBreedModal.ts new file mode 100644 index 00000000..cbd6a028 --- /dev/null +++ b/src/components/breedModal/useBreedModal.ts @@ -0,0 +1,61 @@ +import { useCallback, useState } from 'react'; +import { useNavigate, useParams } from 'react-router-dom'; + +import { fetchImagesByBreed } from '../../api'; +import { selectBreeds, useCatsSelector } from '../../store/hooks'; + +import C from './constants'; +import type { Breed, Cat } from '../../types'; + +const useBreedModal = () => { + const [images, setImages] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + + const navigate = useNavigate(); + const { breedId } = useParams<{ breedId: string }>(); + + const breeds = useCatsSelector(selectBreeds); + const breed: Breed | null = breedId ? breeds[breedId] : null; + + const loadBreedImages = useCallback(() => { + if (!breedId) { + return; + } + setIsLoading(true); + setError(null); + + fetchImagesByBreed(breedId, C.MAX_IMAGES_TO_DISPLAY) + .then((data: Cat[]) => { + setImages(data); + }) + .catch(() => { + setError(C.ERROR_TEXT); + }) + .finally(() => { + setIsLoading(false); + }); + }, [breedId]); + + const handleClose = (): void => { + navigate('/breeds'); + }; + + const handleBackdropClick = (event: React.MouseEvent): void => { + if (event.target === event.currentTarget) { + handleClose(); + } + }; + + return { + breed, + images, + isLoading, + error, + loadBreedImages, + handleBackdropClick, + handleClose, + }; +}; + +export default useBreedModal; diff --git a/src/components/breedModal/utils.ts b/src/components/breedModal/utils.ts new file mode 100644 index 00000000..6fdb93ed --- /dev/null +++ b/src/components/breedModal/utils.ts @@ -0,0 +1,37 @@ +import C from './constants'; +import type { Breed } from '../../types'; + +const createStarRating = (rating: number): string => { + return ( + C.STAR_FILLED.repeat(rating) + C.STAR_EMPTY.repeat(C.MAX_RATING - rating) + ); +}; + +const formatWeight = (breed: Breed): string => { + return `${breed.weight.metric} ${C.WEIGHT_UNITS_KG} (${breed.weight.imperial} ${C.WEIGHT_UNITS_LBS})`; +}; + +const formatLifeSpan = (lifeSpan: string): string => { + return `${lifeSpan} ${C.YEARS_SUFFIX}`; +}; + +const getImageAltText = (imageId: string): string => { + return `Cat image ${imageId}`; +}; + +const detailsData = (breed: Breed): Array<[string, string]> => [ + [C.DETAIL_LABELS_TEMPERAMENT, breed.temperament], + [C.DETAIL_LABELS_LIFE_SPAN, formatLifeSpan(breed.life_span)], + [C.DETAIL_LABELS_WEIGHT, formatWeight(breed)], + [C.DETAIL_LABELS_AFFECTION_LEVEL, createStarRating(breed.affection_level)], + [C.DETAIL_LABELS_CHILD_FRIENDLY, createStarRating(breed.child_friendly)], + [C.DETAIL_LABELS_DOG_FRIENDLY, createStarRating(breed.dog_friendly)], +]; + +export default { + createStarRating, + formatWeight, + formatLifeSpan, + getImageAltText, + detailsData, +}; diff --git a/src/components/card/Card.tsx b/src/components/card/Card.tsx index 0a75634e..ace62c21 100644 --- a/src/components/card/Card.tsx +++ b/src/components/card/Card.tsx @@ -3,30 +3,32 @@ import CardSkeleton from './CardSkeleton'; import useCard from './useCard'; import FavouriteButton from '../favouriteButton'; +import U from './utils'; import styles from './Card.module.css'; import type { Cat } from '../../types'; type CardProps = { cat: Cat; isFavourite: boolean; - hasTitle: boolean; + isBreed: boolean; }; -const Card = ({ cat, isFavourite, hasTitle }: CardProps) => { - const { breedText, isLoaded, getImagePosition, handleImageLoaded } = useCard({ +const Card = ({ cat, isFavourite, isBreed }: CardProps) => { + const { breedText, isLoaded, handleCardClick, handleImageLoaded } = useCard({ cat, + isBreed, }); return ( -
+
{!isLoaded && } - {!hasTitle && ( + {!isBreed && (
{ />
)} - {hasTitle && breedText && ( + {isBreed && breedText && (
{breedText}
)}
diff --git a/src/components/card/useCard.ts b/src/components/card/useCard.ts index 7a48103f..5d3ca00c 100644 --- a/src/components/card/useCard.ts +++ b/src/components/card/useCard.ts @@ -1,38 +1,42 @@ import { useState } from 'react'; +import { useLocation, useNavigate } from 'react-router-dom'; -import C from './constants'; -import type { Breed, Cat } from '../../types'; +import U from './utils'; +import type { Cat } from '../../types'; type UseCardProps = { cat: Cat; + isBreed: boolean; }; -const useCard = ({ cat }: UseCardProps) => { +const useCard = ({ cat, isBreed }: UseCardProps) => { const [isLoaded, setIsLoaded] = useState(false); - - const getImagePosition = () => { - const imageRatio = cat.height / cat.width; - if (imageRatio > C.HIGH_IMAGE_RATIO_THRESHOLD) return 'center 15%'; - if (imageRatio > C.MEDIUM_IMAGE_RATIO_THRESHOLD) return 'center 25%'; - return 'center'; - }; - - const formatBreedNames = (breeds: Breed[]): string => { - return breeds.map((breed) => breed.name).join(' - '); + const navigate = useNavigate(); + const location = useLocation(); + + const hasBreeds: boolean = !!cat.breeds?.length; + const breedId: string = cat.breeds?.[0]?.id ?? ''; + const breedText: string = hasBreeds ? U.formatBreedNames(cat.breeds!) : ''; + + const handleCardClick = () => { + if (isBreed && breedId) { + navigate(`/breeds/${breedId}`); + } else { + const basePath = location.pathname.startsWith('/favourites') + ? '/favourites' + : ''; + navigate(`${basePath}/cat/${cat.id}`); + } }; const handleImageLoaded = () => { setIsLoaded(true); }; - const hasBreeds = !!cat.breeds?.length; - - const breedText = hasBreeds ? formatBreedNames(cat.breeds!) : ''; - return { breedText, isLoaded, - getImagePosition, + handleCardClick, handleImageLoaded, }; }; diff --git a/src/components/card/utils.ts b/src/components/card/utils.ts new file mode 100644 index 00000000..231ca780 --- /dev/null +++ b/src/components/card/utils.ts @@ -0,0 +1,18 @@ +import C from './constants'; +import type { Breed,Cat } from '../../types'; + +const getImagePosition = (cat: Cat): string => { + const imageRatio = cat.height / cat.width; + if (imageRatio > C.HIGH_IMAGE_RATIO_THRESHOLD) return 'center 15%'; + if (imageRatio > C.MEDIUM_IMAGE_RATIO_THRESHOLD) return 'center 25%'; + return 'center'; +}; + +const formatBreedNames = (breeds: Breed[]): string => { + return breeds.map((breed) => breed.name).join(' - '); +}; + +export default { + formatBreedNames, + getImagePosition, +}; diff --git a/src/components/favouriteButton/useFavouriteButton.ts b/src/components/favouriteButton/useFavouriteButton.ts index 147d60d5..14dc0b9a 100644 --- a/src/components/favouriteButton/useFavouriteButton.ts +++ b/src/components/favouriteButton/useFavouriteButton.ts @@ -21,7 +21,6 @@ const useFavouriteButton = ({ event.stopPropagation(); dispatch(favouritesActions.toggleFavourite(cat)); }; - const isCompact = variant === 'compact'; const text = U.getButtonText(isFavourite); const fillColor = U.getHeartFillColor(isCompact, isFavourite); diff --git a/src/components/modal/Modal.module.css b/src/components/modal/Modal.module.css new file mode 100644 index 00000000..6b260303 --- /dev/null +++ b/src/components/modal/Modal.module.css @@ -0,0 +1,181 @@ +.overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(61, 40, 23, 0.8); + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; + padding: 2rem; + backdrop-filter: blur(4px); +} + +.modal { + background: #fff; + border-radius: 1rem; + max-width: 900px; + width: 100%; + max-height: 90vh; + overflow-y: auto; + position: relative; + border: 3px solid #3d2817; + box-shadow: 0 8px 32px rgba(61, 40, 23, 0.3); + padding: 3rem 3rem 0 3rem; +} + +.closeButton { + position: absolute; + top: 1rem; + right: 1rem; + background: none; + border: none; + border-radius: 0; + width: auto; + height: auto; + font-size: 2rem; + cursor: pointer; + color: #3d2817; + font-weight: bold; + z-index: 10; + padding: 0; + line-height: 1; +} +.closeButton:hover { + color: #ff6b9d; + background: none; + transform: none; +} + +.imageContainer { + width: 100%; + max-height: 500px; + overflow: hidden; + background: #fff; + display: flex; + align-items: center; + justify-content: center; + margin: 0; + border-radius: 1rem 1rem 0 0; +} + +.image { + width: 100%; + height: auto; + max-height: 500px; + object-fit: contain; + display: block; +} + +.content { + padding: 2rem 3rem; + display: flex; + flex-direction: column; + align-items: center; +} + +.favouriteButtonContainer { + display: flex; + justify-content: center; + margin-bottom: 1rem; +} + +.loading, +.error { + text-align: center; + padding: 3rem; + font-size: 1.125rem; + color: #3d2817; +} + +.error { + color: #dc3545; +} + +.breedInfo { + text-align: center; +} + +.breedName { + margin: 0 0 1rem 0; + font-size: 2rem; + font-weight: 800; + color: #3d2817; +} + +.breedDescription { + margin: 0 0 1.5rem 0; + font-size: 1rem; + color: #5a4a3a; + line-height: 1.6; +} + +.breedDetails { + display: flex; + flex-direction: column; + gap: 0.75rem; + margin-bottom: 1.5rem; + text-align: left; + background: #fce8b8; + padding: 1.5rem; + border-radius: 0.5rem; +} + +.detailItem { + font-size: 0.9375rem; + color: #3d2817; + line-height: 1.5; +} + +.detailItem strong { + font-weight: 700; + margin-right: 0.5rem; +} + +.breedLink { + display: inline-block; + padding: 0.75rem 1.5rem; + background: #fcc76f; + color: #3d2817; + text-decoration: none; + border-radius: 2rem; + font-weight: 600; + transition: all 0.2s; + border: 2px solid #3d2817; +} + +.breedLink:hover { + transform: translateY(-2px); + box-shadow: 0 4px 12px rgba(61, 40, 23, 0.2); + background: #fbb94e; +} + +.noBreed { + text-align: center; + color: #8b7355; + font-style: italic; +} + +@media (max-width: 768px) { + .overlay { + padding: 1rem; + } + + .modal { + max-height: 95vh; + } + + .content { + padding: 1.5rem; + } + + .breedName { + font-size: 1.5rem; + } + + .breedDetails { + padding: 1rem; + } +} diff --git a/src/components/modal/Modal.tsx b/src/components/modal/Modal.tsx index 57143884..a457aac0 100644 --- a/src/components/modal/Modal.tsx +++ b/src/components/modal/Modal.tsx @@ -1,5 +1,58 @@ +import { Details, Image, Link, Wrapper } from './components'; +import useModal from './useModal'; +import FavouriteButton from '../favouriteButton'; + +import U from './utils'; +import C from './constants'; +import styles from './Modal.module.css'; + const Modal = () => { - return <>Modal; + const { + image, + isLoading, + error, + isFavourite, + breedLinkPath, + handleClose, + handleBackdropClick, + } = useModal(); + + const renderContent = () => { + if (isLoading) { + return
{C.LOADING_TEXT}
; + } + + if (error || !image) { + return
{error || C.NOT_FOUND_TEXT}
; + } + + const breed = U.getBreedInfo(image); + + return ( + <> + +
+
+ +
+
+
+ +
+
+ + ); + }; + + return ( + + {renderContent()} + + ); }; export default Modal; diff --git a/src/components/modal/components/Details.tsx b/src/components/modal/components/Details.tsx new file mode 100644 index 00000000..eb8fa672 --- /dev/null +++ b/src/components/modal/components/Details.tsx @@ -0,0 +1,35 @@ +import C from '../constants'; +import styles from '../Modal.module.css'; +import type { Breed } from '../../../types'; + +type DetailsProps = { + breed: Breed | null; +}; + +const Details = ({ breed }: DetailsProps) => { + if (!breed) { + return

{C.NO_BREED_TEXT}

; + } + + return ( +
+

{breed.name}

+

{breed.description}

+ +
+
+ {C.DETAIL_LABELS_ORIGIN}: {breed.origin} +
+
+ {C.DETAIL_LABELS_TEMPERAMENT}: {breed.temperament} +
+
+ {C.DETAIL_LABELS_LIFE_SPAN}: {breed.life_span}{' '} + {C.YEARS_SUFFIX} +
+
+
+ ); +}; + +export default Details; diff --git a/src/components/modal/components/Image.tsx b/src/components/modal/components/Image.tsx new file mode 100644 index 00000000..02e83939 --- /dev/null +++ b/src/components/modal/components/Image.tsx @@ -0,0 +1,14 @@ +import C from '../constants'; +import styles from '../Modal.module.css'; + +type ModalImageProps = { + imageUrl: string; +}; + +const ModalImage = ({ imageUrl }: ModalImageProps) => ( +
+ {C.CAT_IMAGE} +
+); + +export default ModalImage; diff --git a/src/components/modal/components/Link.tsx b/src/components/modal/components/Link.tsx new file mode 100644 index 00000000..6d767e04 --- /dev/null +++ b/src/components/modal/components/Link.tsx @@ -0,0 +1,23 @@ +import { Link as LinkToBreed } from 'react-router-dom'; + +import C from '../constants'; +import styles from '../Modal.module.css'; +import type { Breed } from '../../../types'; + +type LinkProps = { + breed: Breed | null; + breedLinkPath: string; +}; + +const Link = ({ breed, breedLinkPath }: LinkProps) => { + if (!breed) { + return; + } + return ( + + {C.VIEW_ALL_PREFIX} {breed.name} {C.VIEW_ALL_SUFFIX} + + ); +}; + +export default Link; diff --git a/src/components/modal/components/Wrapper.tsx b/src/components/modal/components/Wrapper.tsx new file mode 100644 index 00000000..d72e92a1 --- /dev/null +++ b/src/components/modal/components/Wrapper.tsx @@ -0,0 +1,20 @@ +import styles from '../Modal.module.css'; + +type WrapperProps = { + onBackdropClick: (e: React.MouseEvent) => void; + onClose: () => void; + children: React.ReactNode; +}; + +const Wrapper = ({ onBackdropClick, onClose, children }: WrapperProps) => ( +
+
+ + {children} +
+
+); + +export default Wrapper; diff --git a/src/components/modal/components/index.ts b/src/components/modal/components/index.ts new file mode 100644 index 00000000..f5944c0e --- /dev/null +++ b/src/components/modal/components/index.ts @@ -0,0 +1,4 @@ +export { default as Details } from './Details'; +export { default as Image } from './Image'; +export { default as Link } from './Link'; +export { default as Wrapper } from './Wrapper'; diff --git a/src/components/modal/constants.ts b/src/components/modal/constants.ts new file mode 100644 index 00000000..4b711d97 --- /dev/null +++ b/src/components/modal/constants.ts @@ -0,0 +1,25 @@ +const CAT_IMAGE = 'cat image'; +const ERROR_TEXT = 'Failed to load cat image'; +const LOADING_TEXT = 'Loading...'; +const NOT_FOUND_TEXT = 'Image not found'; +const NO_BREED_TEXT = 'No breed information available'; +const VIEW_ALL_PREFIX = 'View all'; +const VIEW_ALL_SUFFIX = 'cats โ†’'; +const DETAIL_LABELS_ORIGIN = 'Origin'; +const DETAIL_LABELS_TEMPERAMENT = 'Temperament'; +const DETAIL_LABELS_LIFE_SPAN = 'Life Span'; +const YEARS_SUFFIX = 'years'; + +export default { + CAT_IMAGE, + ERROR_TEXT, + LOADING_TEXT, + NOT_FOUND_TEXT, + NO_BREED_TEXT, + VIEW_ALL_PREFIX, + VIEW_ALL_SUFFIX, + DETAIL_LABELS_ORIGIN, + DETAIL_LABELS_TEMPERAMENT, + DETAIL_LABELS_LIFE_SPAN, + YEARS_SUFFIX, +}; diff --git a/src/components/modal/useModal.ts b/src/components/modal/useModal.ts new file mode 100644 index 00000000..df28dbe5 --- /dev/null +++ b/src/components/modal/useModal.ts @@ -0,0 +1,80 @@ +import { useCallback, useEffect, useState } from 'react'; +import { useLocation, useNavigate, useParams } from 'react-router-dom'; + +import { fetchCatImageById } from '../../api'; +import { + selectFavourites, + selectGallery, + useCatsSelector, +} from '../../store/hooks'; + +import U from './utils'; +import C from './constants'; +import type { Cat } from '../../types'; + +export const useModal = () => { + const imageId = useParams<{ imageId: string }>().imageId || ''; + const navigate = useNavigate(); + const location = useLocation(); + + const cachedCats: Record = useCatsSelector(selectGallery); + const favouriteCats: Record = useCatsSelector(selectFavourites); + const cachedCat: Cat = cachedCats?.[imageId]; + const isFavourite: boolean = !!favouriteCats[imageId]; + + const [image, setImage] = useState(cachedCat ?? null); + const [isLoading, setIsLoading] = useState(!cachedCat); + const [error, setError] = useState(null); + + const loadImage = useCallback(() => { + if (!imageId || cachedCat) return; + + fetchCatImageById(imageId) + .then((data: Cat) => { + setImage(data); + setError(null); + setIsLoading(false); + }) + .catch(() => { + setError(C.ERROR_TEXT); + setIsLoading(false); + }); + }, [imageId, cachedCat]); + + const getBreedLinkPath = useCallback((): string => { + const breed = U.getBreedInfo(image); + if (!breed) return ''; + + return U.calculateBreedLinkPath(breed.id, location.pathname); + }, [image, location.pathname]); + + const handleClose = useCallback(() => { + const parentPath = U.getParentPath(location.pathname); + navigate(parentPath); + }, [location.pathname, navigate]); + + const handleBackdropClick = useCallback( + (event: React.MouseEvent) => { + if (event.target === event.currentTarget) { + handleClose(); + } + }, + [handleClose] + ); + + useEffect(() => { + loadImage(); + }, [loadImage]); + + return { + image, + isLoading, + error, + isFavourite, + breedLinkPath: getBreedLinkPath(), + handleBackdropClick, + handleClose, + }; +}; + +export default useModal; diff --git a/src/components/modal/utils.ts b/src/components/modal/utils.ts new file mode 100644 index 00000000..7d1a06d9 --- /dev/null +++ b/src/components/modal/utils.ts @@ -0,0 +1,30 @@ +import type { Cat } from '../../types'; + +const getBreedInfo = (image: Cat | null) => { + if (!image?.breeds?.length) return null; + return image.breeds[0]; +}; + +const getBreedLinkPath = (breedId: string): string => { + return `/breeds/${breedId}`; +}; + +const calculateBreedLinkPath = ( + breedId: string, + currentPath: string +): string => { + const isInBreedModal = currentPath.startsWith(`/breeds/${breedId}`); + return isInBreedModal ? `/breeds/${breedId}` : getBreedLinkPath(breedId); +}; + +const getParentPath = (pathname: string): string => { + const pathParts = pathname.split('/cat/')[0]; + return pathParts || '/'; +}; + +export default { + getBreedInfo, + getBreedLinkPath, + calculateBreedLinkPath, + getParentPath, +}; diff --git a/src/components/navigation/Navigation.tsx b/src/components/navigation/Navigation.tsx index 94ccb61b..012430b9 100644 --- a/src/components/navigation/Navigation.tsx +++ b/src/components/navigation/Navigation.tsx @@ -29,7 +29,7 @@ const Navigation = () => { {C.GALLERY} {C.BREEDS} diff --git a/src/components/navigation/constants.ts b/src/components/navigation/constants.ts index e8765870..bfb39f09 100644 --- a/src/components/navigation/constants.ts +++ b/src/components/navigation/constants.ts @@ -1,6 +1,4 @@ const BREEDS = 'Breeds'; -const BREEDS_PATH = '/breeds'; -const CAT_PATH = '/cat/'; const DEFAULT_PATH = '/'; const GALLERY = 'Gallery'; const IMAGE_SOURCE = '/cat.png'; @@ -11,8 +9,6 @@ const TITLE = 'Cat Lover'; export default { BREEDS, - BREEDS_PATH, - CAT_PATH, DEFAULT_PATH, GALLERY, IMAGE_SOURCE, diff --git a/src/components/navigation/useNavigation.ts b/src/components/navigation/useNavigation.ts index 0b2928b2..d19b9788 100644 --- a/src/components/navigation/useNavigation.ts +++ b/src/components/navigation/useNavigation.ts @@ -20,9 +20,9 @@ const useNavigation = (): UseNavigationState => { }; const isGalleryActive = - isActive(C.DEFAULT_PATH) || location.pathname.startsWith(C.CAT_PATH); + isActive(C.DEFAULT_PATH) || location.pathname.startsWith('/cat/'); - const isBreedsActive = isActive(C.BREEDS_PATH); + const isBreedsActive = isActive('/breeds'); const isFavouritesActive = isActive(C.FAVOURITES_PATH); return { diff --git a/src/views/breeds/Breeds.tsx b/src/views/breeds/Breeds.tsx index 58fd1744..8fd16cf6 100644 --- a/src/views/breeds/Breeds.tsx +++ b/src/views/breeds/Breeds.tsx @@ -31,7 +31,7 @@ const Breeds = () => { key={breed.id} cat={U.transformBreedToCatObject(breed)} isFavourite={false} - hasTitle + isBreed /> ))}
diff --git a/src/views/favourites/Favourites.tsx b/src/views/favourites/Favourites.tsx index 3e9218c9..f0cbe3c3 100644 --- a/src/views/favourites/Favourites.tsx +++ b/src/views/favourites/Favourites.tsx @@ -25,7 +25,7 @@ const Favourites = () => {

{C.TITLE_TEXT}

{favouriteCats.map((cat) => ( - + ))}
diff --git a/src/views/gallery/Gallery.tsx b/src/views/gallery/Gallery.tsx index 56029550..6b0212b0 100644 --- a/src/views/gallery/Gallery.tsx +++ b/src/views/gallery/Gallery.tsx @@ -39,7 +39,7 @@ const Gallery = () => { ))} From b9913a35946e4a548e0281cad5918c68f2b053d0 Mon Sep 17 00:00:00 2001 From: mkour Date: Sun, 16 Nov 2025 18:36:24 +0200 Subject: [PATCH 12/17] add analytics view with persistence and user behavior charts - Added analytics dashboard view - Implemented localStorage persistence for analytics, breeds, and favourites - Integrated multiple charts in analytics to showcase user behavior --- package-lock.json | 297 +++++++++++++++++- package.json | 2 +- src/App.tsx | 9 + src/components/breedModal/useBreedModal.ts | 17 +- .../favouriteButton/FavouriteButton.tsx | 12 +- .../favouriteButton/FavouriteButtonImage.tsx | 8 +- src/components/favouriteButton/constants.ts | 9 +- .../favouriteButton/useFavouriteButton.ts | 4 +- src/components/favouriteButton/utils.ts | 12 +- src/components/modal/Modal.tsx | 4 +- src/components/navigation/Navigation.tsx | 16 +- src/components/navigation/constants.ts | 4 + src/components/navigation/useNavigation.ts | 3 + src/hooks/constants.ts | 28 ++ src/hooks/index.ts | 3 + src/hooks/useAnalyticsPersistence.ts | 45 +++ src/hooks/useBreedsPersistence.ts | 49 +++ src/hooks/useFavouritesPersistence.ts | 48 +++ src/router.tsx | 3 +- src/store/analyticsSlice.ts | 33 ++ src/store/breedsSlice.ts | 18 +- src/store/favouritesSlice.ts | 3 + src/store/hooks.ts | 6 +- src/store/index.ts | 2 + src/store/types.ts | 9 + src/views/analytics/Analytics.module.css | 107 +++++++ src/views/analytics/Analytics.tsx | 24 ++ .../components/BreedDistributionChart.tsx | 53 ++++ src/views/analytics/components/StatCards.tsx | 25 ++ .../components/ViewsVsFavoritesChart.tsx | 36 +++ src/views/analytics/components/index.ts | 3 + src/views/analytics/components/useStats.ts | 43 +++ src/views/analytics/constants.ts | 48 +++ src/views/analytics/index.ts | 1 + src/views/analytics/utils.ts | 84 +++++ src/views/breeds/constants.ts | 4 +- src/views/breeds/useBreeds.ts | 11 +- src/views/breeds/utils.ts | 15 +- src/views/gallery/Gallery.module.css | 1 - src/views/gallery/useGallery.ts | 2 + src/views/index.ts | 1 + 41 files changed, 1055 insertions(+), 47 deletions(-) create mode 100644 src/hooks/constants.ts create mode 100644 src/hooks/index.ts create mode 100644 src/hooks/useAnalyticsPersistence.ts create mode 100644 src/hooks/useBreedsPersistence.ts create mode 100644 src/hooks/useFavouritesPersistence.ts create mode 100644 src/store/analyticsSlice.ts create mode 100644 src/views/analytics/Analytics.module.css create mode 100644 src/views/analytics/Analytics.tsx create mode 100644 src/views/analytics/components/BreedDistributionChart.tsx create mode 100644 src/views/analytics/components/StatCards.tsx create mode 100644 src/views/analytics/components/ViewsVsFavoritesChart.tsx create mode 100644 src/views/analytics/components/index.ts create mode 100644 src/views/analytics/components/useStats.ts create mode 100644 src/views/analytics/constants.ts create mode 100644 src/views/analytics/index.ts create mode 100644 src/views/analytics/utils.ts diff --git a/package-lock.json b/package-lock.json index 48cea43c..7da0600f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,7 +14,7 @@ "react-dom": "^19.2.0", "react-redux": "^9.2.0", "react-router-dom": "^7.9.5", - "redux-persist": "^6.0.0" + "recharts": "^3.4.1" }, "devDependencies": { "@eslint/js": "^9.39.1", @@ -1791,6 +1791,69 @@ "assertion-error": "^2.0.1" } }, + "node_modules/@types/d3-array": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", + "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==", + "license": "MIT" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "license": "MIT" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", + "license": "MIT" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-shape": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz", + "integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==", + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", + "license": "MIT" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", + "license": "MIT" + }, "node_modules/@types/deep-eql": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", @@ -2556,6 +2619,15 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -2669,6 +2741,127 @@ "devOptional": true, "license": "MIT" }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", + "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "license": "ISC", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "license": "ISC", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/data-urls": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-6.0.0.tgz", @@ -2708,6 +2901,12 @@ "dev": true, "license": "MIT" }, + "node_modules/decimal.js-light": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", + "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==", + "license": "MIT" + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -2827,6 +3026,16 @@ "node": ">= 0.4" } }, + "node_modules/es-toolkit": { + "version": "1.41.0", + "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.41.0.tgz", + "integrity": "sha512-bDd3oRmbVgqZCJS6WmeQieOrzpl3URcWBUVDXxOELlUW2FuW+0glPOz1n0KnRie+PdyvUZcXz2sOn00c6pPRIA==", + "license": "MIT", + "workspaces": [ + "docs", + "benchmarks" + ] + }, "node_modules/esbuild": { "version": "0.25.12", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", @@ -3113,6 +3322,12 @@ "node": ">=0.10.0" } }, + "node_modules/eventemitter3": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", + "license": "MIT" + }, "node_modules/expect-type": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.2.2.tgz", @@ -3584,6 +3799,15 @@ "node": ">=8" } }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -4220,8 +4444,8 @@ "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", - "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/react-redux": { "version": "9.2.0", @@ -4295,6 +4519,36 @@ "react-dom": ">=18" } }, + "node_modules/recharts": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-3.4.1.tgz", + "integrity": "sha512-35kYg6JoOgwq8sE4rhYkVWwa6aAIgOtT+Ob0gitnShjwUwZmhrmy7Jco/5kJNF4PnLXgt9Hwq+geEMS+WrjU1g==", + "license": "MIT", + "workspaces": [ + "www" + ], + "dependencies": { + "@reduxjs/toolkit": "1.x.x || 2.x.x", + "clsx": "^2.1.1", + "decimal.js-light": "^2.5.1", + "es-toolkit": "^1.39.3", + "eventemitter3": "^5.0.1", + "immer": "^10.1.1", + "react-redux": "8.x.x || 9.x.x", + "reselect": "5.1.1", + "tiny-invariant": "^1.3.3", + "use-sync-external-store": "^1.2.2", + "victory-vendor": "^37.0.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/redent": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", @@ -4316,15 +4570,6 @@ "license": "MIT", "peer": true }, - "node_modules/redux-persist": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/redux-persist/-/redux-persist-6.0.0.tgz", - "integrity": "sha512-71LLMbUq2r02ng2We9S215LtPu3fY0KgaGE0k8WRgl6RkqxtGfl7HUozz1Dftwsb0D/5mZ8dwAaPbtnzfvbEwQ==", - "license": "MIT", - "peerDependencies": { - "redux": ">4.0.0" - } - }, "node_modules/redux-thunk": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz", @@ -4594,6 +4839,12 @@ "dev": true, "license": "MIT" }, + "node_modules/tiny-invariant": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", + "license": "MIT" + }, "node_modules/tinybench": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", @@ -4858,6 +5109,28 @@ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/victory-vendor": { + "version": "37.3.6", + "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-37.3.6.tgz", + "integrity": "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==", + "license": "MIT AND ISC", + "dependencies": { + "@types/d3-array": "^3.0.3", + "@types/d3-ease": "^3.0.0", + "@types/d3-interpolate": "^3.0.1", + "@types/d3-scale": "^4.0.2", + "@types/d3-shape": "^3.1.0", + "@types/d3-time": "^3.0.0", + "@types/d3-timer": "^3.0.0", + "d3-array": "^3.1.6", + "d3-ease": "^3.0.1", + "d3-interpolate": "^3.0.1", + "d3-scale": "^4.0.2", + "d3-shape": "^3.1.0", + "d3-time": "^3.0.0", + "d3-timer": "^3.0.1" + } + }, "node_modules/vite": { "version": "7.2.2", "resolved": "https://registry.npmjs.org/vite/-/vite-7.2.2.tgz", diff --git a/package.json b/package.json index 6611739e..edf9a67c 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "react-dom": "^19.2.0", "react-redux": "^9.2.0", "react-router-dom": "^7.9.5", - "redux-persist": "^6.0.0" + "recharts": "^3.4.1" }, "devDependencies": { "@eslint/js": "^9.39.1", diff --git a/src/App.tsx b/src/App.tsx index 948f210b..67776061 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,8 +1,17 @@ import './App.css'; import Navigation from './components/navigation'; +import { + useAnalyticsPersistence, + useBreedsPersistence, + useFavouritesPersistence, +} from './hooks'; import AppRouter from './router'; const App = () => { + useAnalyticsPersistence(); + useBreedsPersistence(); + useFavouritesPersistence(); + return (
diff --git a/src/components/breedModal/useBreedModal.ts b/src/components/breedModal/useBreedModal.ts index cbd6a028..fee8054c 100644 --- a/src/components/breedModal/useBreedModal.ts +++ b/src/components/breedModal/useBreedModal.ts @@ -1,8 +1,13 @@ -import { useCallback, useState } from 'react'; +import { useCallback, useEffect, useState } from 'react'; import { useNavigate, useParams } from 'react-router-dom'; import { fetchImagesByBreed } from '../../api'; -import { selectBreeds, useCatsSelector } from '../../store/hooks'; +import { analyticsActions } from '../../store/analyticsSlice'; +import { + selectBreeds, + useCatsDispatch, + useCatsSelector, +} from '../../store/hooks'; import C from './constants'; import type { Breed, Cat } from '../../types'; @@ -14,9 +19,11 @@ const useBreedModal = () => { const navigate = useNavigate(); const { breedId } = useParams<{ breedId: string }>(); + const dispatch = useCatsDispatch(); const breeds = useCatsSelector(selectBreeds); const breed: Breed | null = breedId ? breeds[breedId] : null; + const breedName: string = breed?.name || ''; const loadBreedImages = useCallback(() => { if (!breedId) { @@ -47,6 +54,12 @@ const useBreedModal = () => { } }; + useEffect(() => { + if (breedName) { + dispatch(analyticsActions.incrementBreedView({ breedName })); + } + }, [breedName, dispatch]); + return { breed, images, diff --git a/src/components/favouriteButton/FavouriteButton.tsx b/src/components/favouriteButton/FavouriteButton.tsx index 7772d466..da57e643 100644 --- a/src/components/favouriteButton/FavouriteButton.tsx +++ b/src/components/favouriteButton/FavouriteButton.tsx @@ -14,8 +14,14 @@ const FavouriteButton = ({ isFavourite, variant, }: FavouriteButtonProps) => { - const { text, buttonClass, fillColor, showText, handleFavouriteToggle } = - useFavouriteButton({ cat, isFavourite, variant }); + const { + text, + buttonClass, + fillColor, + strokeColor, + showText, + handleFavouriteToggle, + } = useFavouriteButton({ cat, isFavourite, variant }); return ( ); diff --git a/src/components/favouriteButton/FavouriteButtonImage.tsx b/src/components/favouriteButton/FavouriteButtonImage.tsx index 6919e7e8..d6aa5d98 100644 --- a/src/components/favouriteButton/FavouriteButtonImage.tsx +++ b/src/components/favouriteButton/FavouriteButtonImage.tsx @@ -1,20 +1,18 @@ -import C from './constants'; - type FavouriteButtonImageProps = { fillColor: string; - stroke?: string; + strokeColor: string; }; const FavouriteButtonImage = ({ fillColor, - stroke = C.HEART_STROKE, + strokeColor, }: FavouriteButtonImageProps) => ( { +const getFillColor = (isCompact: boolean, isFavourite: boolean): string => { if (!isFavourite) return 'none'; return isCompact ? C.HEART_FILL_COMPACT : C.HEART_FILL_EXPANDED; }; +const getStrokeColor = (isCompact: Boolean): string => { + return isCompact ? C.HEART_STROKE_COMPACT : C.HEART_STROKE_EXPANDED; +}; + const getButtonClassName = ( isCompact: boolean, isFavourite: boolean @@ -22,7 +23,8 @@ const getButtonText = (isFavourite: boolean): string => { }; const U = { - getHeartFillColor, + getFillColor, + getStrokeColor, getButtonClassName, getButtonText, }; diff --git a/src/components/modal/Modal.tsx b/src/components/modal/Modal.tsx index a457aac0..d8f4f0fa 100644 --- a/src/components/modal/Modal.tsx +++ b/src/components/modal/Modal.tsx @@ -40,9 +40,7 @@ const Modal = () => { />
-
- -
+
); diff --git a/src/components/navigation/Navigation.tsx b/src/components/navigation/Navigation.tsx index 012430b9..ef1ae85f 100644 --- a/src/components/navigation/Navigation.tsx +++ b/src/components/navigation/Navigation.tsx @@ -6,8 +6,12 @@ import C from './constants'; import styles from './Navigation.module.css'; const Navigation = () => { - const { isGalleryActive, isBreedsActive, isFavouritesActive } = - useNavigation(); + const { + isGalleryActive, + isBreedsActive, + isFavouritesActive, + isDemographicsActive, + } = useNavigation(); return (
diff --git a/src/components/navigation/constants.ts b/src/components/navigation/constants.ts index bfb39f09..888f2154 100644 --- a/src/components/navigation/constants.ts +++ b/src/components/navigation/constants.ts @@ -5,6 +5,8 @@ const IMAGE_SOURCE = '/cat.png'; const IMAGE_ALT = 'Cat logo'; const FAVOURITES = 'Favourites'; const FAVOURITES_PATH = '/favourites'; +const ANALYTICS = 'Analytics'; +const ANALYTICS_PATH = '/analytics'; const TITLE = 'Cat Lover'; export default { @@ -15,5 +17,7 @@ export default { IMAGE_ALT, FAVOURITES, FAVOURITES_PATH, + ANALYTICS, + ANALYTICS_PATH, TITLE, }; diff --git a/src/components/navigation/useNavigation.ts b/src/components/navigation/useNavigation.ts index d19b9788..d9f65ef7 100644 --- a/src/components/navigation/useNavigation.ts +++ b/src/components/navigation/useNavigation.ts @@ -6,6 +6,7 @@ type UseNavigationState = { isGalleryActive: boolean; isBreedsActive: boolean; isFavouritesActive: boolean; + isDemographicsActive: boolean; }; const useNavigation = (): UseNavigationState => { @@ -24,11 +25,13 @@ const useNavigation = (): UseNavigationState => { const isBreedsActive = isActive('/breeds'); const isFavouritesActive = isActive(C.FAVOURITES_PATH); + const isDemographicsActive = isActive(C.ANALYTICS_PATH); return { isGalleryActive, isBreedsActive, isFavouritesActive, + isDemographicsActive, }; }; diff --git a/src/hooks/constants.ts b/src/hooks/constants.ts new file mode 100644 index 00000000..004f8f02 --- /dev/null +++ b/src/hooks/constants.ts @@ -0,0 +1,28 @@ +const ANALYTICS_LOAD_ERROR = 'Failed to load analytics from localStorage'; +const ANALYTICS_SAVE_ERROR = 'Failed to save analytics to localStorage'; +const BREEDS_LOAD_ERROR = 'Failed to load breeds from localStorage'; +const BREEDS_SAVE_ERROR = 'Failed to save breeds to localStorage'; +const FAVOURITES_INVALID_FORMAT = + 'Invalid favourites data format, resetting to empty object'; +const FAVOURITES_LOAD_ERROR = 'Failed to load favourites from localStorage'; +const FAVOURITES_SAVE_ERROR = 'Failed to save favourites to localStorage'; + +const STORAGE_KEY_ANALYTICS = 'cat-lover-analytics'; +const STORAGE_KEY_BREEDS = 'cat-lover-breeds'; +const STORAGE_KEY_FAVOURITES = 'cat_app_favourites'; + +const ONE_DAY_MS = 1000 * 60 * 60 * 24; + +export default { + ANALYTICS_LOAD_ERROR, + ANALYTICS_SAVE_ERROR, + BREEDS_LOAD_ERROR, + BREEDS_SAVE_ERROR, + FAVOURITES_INVALID_FORMAT, + FAVOURITES_LOAD_ERROR, + FAVOURITES_SAVE_ERROR, + ONE_DAY_MS, + STORAGE_KEY_ANALYTICS, + STORAGE_KEY_BREEDS, + STORAGE_KEY_FAVOURITES, +}; diff --git a/src/hooks/index.ts b/src/hooks/index.ts new file mode 100644 index 00000000..a7c15272 --- /dev/null +++ b/src/hooks/index.ts @@ -0,0 +1,3 @@ +export { default as useAnalyticsPersistence } from './useAnalyticsPersistence'; +export { default as useBreedsPersistence } from './useBreedsPersistence'; +export { default as useFavouritesPersistence } from './useFavouritesPersistence'; diff --git a/src/hooks/useAnalyticsPersistence.ts b/src/hooks/useAnalyticsPersistence.ts new file mode 100644 index 00000000..87d74e7d --- /dev/null +++ b/src/hooks/useAnalyticsPersistence.ts @@ -0,0 +1,45 @@ +import { useEffect } from 'react'; + +import { analyticsActions } from '../store/analyticsSlice'; + +import { + selectAnalytics, + useCatsDispatch, + useCatsSelector, +} from '../store/hooks'; +import C from './constants'; + +const useAnalyticsPersistence = () => { + const dispatch = useCatsDispatch(); + const analytics = useCatsSelector(selectAnalytics); + + useEffect(() => { + const savedData = localStorage.getItem(C.STORAGE_KEY_ANALYTICS); + if (savedData) { + Promise.resolve() + .then(() => JSON.parse(savedData)) + .then((parsed) => { + const validatedData = { + galleryCats: parsed.galleryCats || 0, + breedsViewed: parsed.breedsViewed || {}, + }; + dispatch(analyticsActions.loadAnalytics(validatedData)); + }) + .catch((error) => { + console.error(C.ANALYTICS_LOAD_ERROR, error); + }); + } + }, [dispatch]); + + useEffect(() => { + Promise.resolve() + .then(() => + localStorage.setItem(C.STORAGE_KEY_ANALYTICS, JSON.stringify(analytics)) + ) + .catch((error) => { + console.error(C.ANALYTICS_SAVE_ERROR, error); + }); + }, [analytics]); +}; + +export default useAnalyticsPersistence; diff --git a/src/hooks/useBreedsPersistence.ts b/src/hooks/useBreedsPersistence.ts new file mode 100644 index 00000000..77175017 --- /dev/null +++ b/src/hooks/useBreedsPersistence.ts @@ -0,0 +1,49 @@ +import { useEffect } from 'react'; + +import type { BreedsStateWithMeta } from '../store/breedsSlice'; +import { breedsActions } from '../store/breedsSlice'; +import { + selectBreedsWithMeta, + useCatsDispatch, + useCatsSelector, +} from '../store/hooks'; +import C from './constants'; + +const useBreedsPersistence = () => { + const dispatch = useCatsDispatch(); + const breeds = useCatsSelector(selectBreedsWithMeta); + + useEffect(() => { + const savedData = localStorage.getItem(C.STORAGE_KEY_BREEDS); + if (savedData) { + Promise.resolve() + .then(() => JSON.parse(savedData) as BreedsStateWithMeta) + .then((parsed) => { + const now = Date.now(); + const isFresh = + parsed.lastFetched && now - parsed.lastFetched < C.ONE_DAY_MS; + + if (isFresh && Object.keys(parsed.data).length > 0) { + dispatch(breedsActions.loadBreeds(parsed)); + } + }) + .catch((error) => { + console.error(C.BREEDS_LOAD_ERROR, error); + }); + } + }, [dispatch]); + + useEffect(() => { + if (Object.keys(breeds.data).length > 0) { + Promise.resolve() + .then(() => + localStorage.setItem(C.STORAGE_KEY_BREEDS, JSON.stringify(breeds)) + ) + .catch((error) => { + console.error(C.BREEDS_SAVE_ERROR, error); + }); + } + }, [breeds]); +}; + +export default useBreedsPersistence; diff --git a/src/hooks/useFavouritesPersistence.ts b/src/hooks/useFavouritesPersistence.ts new file mode 100644 index 00000000..70d1387a --- /dev/null +++ b/src/hooks/useFavouritesPersistence.ts @@ -0,0 +1,48 @@ +import { useEffect } from 'react'; + +import { favouritesActions } from '../store/favouritesSlice'; +import { + selectFavourites, + useCatsDispatch, + useCatsSelector, +} from '../store/hooks'; +import C from './constants'; + +const useFavouritesPersistence = () => { + const dispatch = useCatsDispatch(); + const favourites = useCatsSelector(selectFavourites); + + useEffect(() => { + const savedData = localStorage.getItem(C.STORAGE_KEY_FAVOURITES); + if (savedData) { + Promise.resolve() + .then(() => JSON.parse(savedData)) + .then((parsed) => { + if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) { + dispatch(favouritesActions.loadFavourites(parsed)); + } else { + console.warn(C.FAVOURITES_INVALID_FORMAT); + localStorage.removeItem(C.STORAGE_KEY_FAVOURITES); + } + }) + .catch((error) => { + console.error(C.FAVOURITES_LOAD_ERROR, error); + }); + } + }, [dispatch]); + + useEffect(() => { + Promise.resolve() + .then(() => + localStorage.setItem( + C.STORAGE_KEY_FAVOURITES, + JSON.stringify(favourites) + ) + ) + .catch((error) => { + console.error(C.FAVOURITES_SAVE_ERROR, error); + }); + }, [favourites]); +}; + +export default useFavouritesPersistence; diff --git a/src/router.tsx b/src/router.tsx index 6a459eb6..b6dfbbcf 100644 --- a/src/router.tsx +++ b/src/router.tsx @@ -1,7 +1,7 @@ import { Navigate, Route, Routes } from 'react-router-dom'; import { BreedModal, Modal } from './components'; -import { Breeds, Favourites, Gallery } from './views'; +import { Breeds, Analytics, Favourites, Gallery } from './views'; const AppRouter = () => { return ( @@ -16,6 +16,7 @@ const AppRouter = () => { }> } /> + } /> } /> ); diff --git a/src/store/analyticsSlice.ts b/src/store/analyticsSlice.ts new file mode 100644 index 00000000..cd73c1a4 --- /dev/null +++ b/src/store/analyticsSlice.ts @@ -0,0 +1,33 @@ +import type { PayloadAction } from '@reduxjs/toolkit'; +import { createSlice } from '@reduxjs/toolkit'; + +import type { AnalyticsState } from './types'; + +const initialAnalyticsState: AnalyticsState = { + galleryCats: 0, + breedsViewed: {}, +}; + +const analyticsSlice = createSlice({ + name: 'analytics', + initialState: initialAnalyticsState, + reducers: { + incrementGalleryCats: (state, action: PayloadAction) => { + state.galleryCats += action.payload; + }, + incrementBreedView: ( + state, + action: PayloadAction<{ breedName: string }> + ) => { + const breedName = action.payload.breedName; + state.breedsViewed[breedName] = (state.breedsViewed[breedName] || 0) + 1; + }, + loadAnalytics: (_state, action: PayloadAction) => { + return action.payload; + }, + resetAnalytics: () => initialAnalyticsState, + }, +}); + +export const analyticsActions = analyticsSlice.actions; +export default analyticsSlice.reducer; diff --git a/src/store/breedsSlice.ts b/src/store/breedsSlice.ts index 7fb12267..1fd8f680 100644 --- a/src/store/breedsSlice.ts +++ b/src/store/breedsSlice.ts @@ -2,9 +2,16 @@ import type { PayloadAction } from '@reduxjs/toolkit'; import { createSlice } from '@reduxjs/toolkit'; import type { Breed } from '../types'; -import type { BreedsState } from './types'; -const initialBreedsState: BreedsState = {}; +interface BreedsStateWithMeta { + data: { [key: string]: Breed }; + lastFetched: number | null; +} + +const initialBreedsState: BreedsStateWithMeta = { + data: {}, + lastFetched: null, +}; const breedsSlice = createSlice({ name: 'breeds', @@ -12,8 +19,12 @@ const breedsSlice = createSlice({ reducers: { addBreeds: (state, action: PayloadAction) => { action.payload.forEach((breed) => { - state[breed.id] = breed; + state.data[breed.id] = breed; }); + state.lastFetched = Date.now(); + }, + loadBreeds: (_state, action: PayloadAction) => { + return action.payload; }, clearBreeds: () => initialBreedsState, }, @@ -21,3 +32,4 @@ const breedsSlice = createSlice({ export const breedsActions = breedsSlice.actions; export default breedsSlice.reducer; +export type { BreedsStateWithMeta }; diff --git a/src/store/favouritesSlice.ts b/src/store/favouritesSlice.ts index d450d73e..bbd77fd8 100644 --- a/src/store/favouritesSlice.ts +++ b/src/store/favouritesSlice.ts @@ -18,6 +18,9 @@ const favouritesSlice = createSlice({ state[cat.id] = cat; } }, + loadFavourites: (_state, action: PayloadAction) => { + return action.payload; + }, clearFavourites: () => initialFavouritesState, }, }); diff --git a/src/store/hooks.ts b/src/store/hooks.ts index d098d10a..624e52fb 100644 --- a/src/store/hooks.ts +++ b/src/store/hooks.ts @@ -13,4 +13,8 @@ export const selectFavourites = (state: RootState) => state.favourites; export const selectGallery = (state: RootState) => state.gallery; -export const selectBreeds = (state: RootState) => state.breeds; +export const selectBreeds = (state: RootState) => state.breeds.data; + +export const selectBreedsWithMeta = (state: RootState) => state.breeds; + +export const selectAnalytics = (state: RootState) => state.analytics; diff --git a/src/store/index.ts b/src/store/index.ts index e4287c59..0f11437f 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -1,4 +1,5 @@ import { configureStore } from '@reduxjs/toolkit'; +import analyticsReducer from './analyticsSlice'; import breedsReducer from './breedsSlice'; import favouritesReducer from './favouritesSlice'; import galleryReducer from './gallerySlice'; @@ -8,6 +9,7 @@ const store = configureStore({ gallery: galleryReducer, favourites: favouritesReducer, breeds: breedsReducer, + analytics: analyticsReducer, }, }); diff --git a/src/store/types.ts b/src/store/types.ts index 61898214..abd2907e 100644 --- a/src/store/types.ts +++ b/src/store/types.ts @@ -5,3 +5,12 @@ export type FavouritesState = Record; export type CatsState = Record; export type BreedsState = Record; + +export type BreedsViewed = { + [breedName: string]: number; +}; + +export type AnalyticsState = { + galleryCats: number; + breedsViewed: BreedsViewed; +}; diff --git a/src/views/analytics/Analytics.module.css b/src/views/analytics/Analytics.module.css new file mode 100644 index 00000000..ec8f1f31 --- /dev/null +++ b/src/views/analytics/Analytics.module.css @@ -0,0 +1,107 @@ +.container { + max-width: 1200px; + margin: 0 auto; + padding: 2rem; +} + +.title { + font-size: 2.5rem; + font-weight: 800; + color: #3d2817; + margin: 0 0 0.5rem 0; + text-align: center; +} + +.subtitle { + font-size: 1.125rem; + color: #5a4a3a; + text-align: center; + margin: 0 0 2rem 0; +} + +.statsGrid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); + gap: 1.5rem; + margin-bottom: 2rem; +} + +.statCard { + background: #fff; + border: 3px solid; + border-radius: 1rem; + padding: 2rem; + text-align: center; + box-shadow: 0 4px 12px rgba(61, 40, 23, 0.1); + transition: transform 0.2s; +} + +.statCard:hover { + transform: translateY(-4px); + box-shadow: 0 8px 16px rgba(61, 40, 23, 0.15); +} + +.statValue { + font-size: 3rem; + font-weight: 800; + margin-bottom: 0.5rem; + line-height: 1; +} + +.statLabel { + font-size: 1rem; + color: #5a4a3a; + font-weight: 600; +} + +.chartsGrid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(450px, 1fr)); + gap: 2rem; +} + +.chartContainer { + background: #fff; + border: 3px solid #3d2817; + border-radius: 1rem; + padding: 1.5rem; + box-shadow: 0 4px 12px rgba(61, 40, 23, 0.1); +} + +.chartTitle { + font-size: 1.5rem; + font-weight: 700; + color: #3d2817; + margin: 0 0 1.5rem 0; + text-align: center; +} + +.noData { + text-align: center; + color: #8b7355; + font-style: italic; + padding: 2rem; + font-size: 1.125rem; +} + +@media (max-width: 768px) { + .container { + padding: 1rem; + } + + .title { + font-size: 2rem; + } + + .statsGrid { + grid-template-columns: 1fr; + } + + .chartsGrid { + grid-template-columns: 1fr; + } + + .statValue { + font-size: 2.5rem; + } +} diff --git a/src/views/analytics/Analytics.tsx b/src/views/analytics/Analytics.tsx new file mode 100644 index 00000000..fd1a3fa2 --- /dev/null +++ b/src/views/analytics/Analytics.tsx @@ -0,0 +1,24 @@ +import { + BreedDistributionChart, + StatCards, + ViewsVsFavoritesChart, +} from './components'; + +import C from './constants'; +import styles from './Analytics.module.css'; + +const Analytics = () => { + return ( +
+

{C.TITLE}

+

{C.SUBTITLE}

+ +
+ + +
+
+ ); +}; + +export default Analytics; diff --git a/src/views/analytics/components/BreedDistributionChart.tsx b/src/views/analytics/components/BreedDistributionChart.tsx new file mode 100644 index 00000000..75518771 --- /dev/null +++ b/src/views/analytics/components/BreedDistributionChart.tsx @@ -0,0 +1,53 @@ +import { + Cell, + Legend, + Pie, + PieChart, + ResponsiveContainer, + Tooltip, +} from 'recharts'; +import U from '../utils'; +import C from '../constants'; + +import styles from '../Analytics.module.css'; +import useStats from './useStats'; + +const BreedDistributionChart = () => { + const { pieChartData } = useStats(); + if (!pieChartData.length) { + return ( +
+

{C.PIE_CHART_TITLE}

+

{C.PIE_CHART_EMPTY_TITLE}

+
+ ); + } + + return ( +
+

{C.PIE_CHART_TITLE}

+ + + U.getPieLabelText(name, percent)} + outerRadius={80} + fill="#8884d8" + dataKey="value" + > + {pieChartData.map((_, index) => ( + + ))} + + + + + +
+ ); +}; + +export default BreedDistributionChart; diff --git a/src/views/analytics/components/StatCards.tsx b/src/views/analytics/components/StatCards.tsx new file mode 100644 index 00000000..82a43d78 --- /dev/null +++ b/src/views/analytics/components/StatCards.tsx @@ -0,0 +1,25 @@ +import styles from '../Analytics.module.css'; +import useStats from './useStats'; + +const StatCards = () => { + const { stats } = useStats(); + + return ( +
+ {stats.map((stat) => ( +
+
+ {stat.value} +
+
{stat.label}
+
+ ))} +
+ ); +}; + +export default StatCards; diff --git a/src/views/analytics/components/ViewsVsFavoritesChart.tsx b/src/views/analytics/components/ViewsVsFavoritesChart.tsx new file mode 100644 index 00000000..9a88fab3 --- /dev/null +++ b/src/views/analytics/components/ViewsVsFavoritesChart.tsx @@ -0,0 +1,36 @@ +import { + Bar, + BarChart, + CartesianGrid, + Legend, + ResponsiveContainer, + Tooltip, + XAxis, + YAxis, +} from 'recharts'; + +import styles from '../Analytics.module.css'; +import useStats from './useStats'; +import C from '../constants'; + +const ViewsVsFavoritesChart = () => { + const { barStats } = useStats(); + return ( +
+

{C.BAR_TITLE}

+ + + + + + + + + + + +
+ ); +}; + +export default ViewsVsFavoritesChart; diff --git a/src/views/analytics/components/index.ts b/src/views/analytics/components/index.ts new file mode 100644 index 00000000..9a2fc7db --- /dev/null +++ b/src/views/analytics/components/index.ts @@ -0,0 +1,3 @@ +export { default as BreedDistributionChart } from './BreedDistributionChart'; +export { default as StatCards } from './StatCards'; +export { default as ViewsVsFavoritesChart } from './ViewsVsFavoritesChart'; diff --git a/src/views/analytics/components/useStats.ts b/src/views/analytics/components/useStats.ts new file mode 100644 index 00000000..822e9caf --- /dev/null +++ b/src/views/analytics/components/useStats.ts @@ -0,0 +1,43 @@ +import { useMemo } from 'react'; +import { + selectAnalytics, + selectFavourites, + useCatsSelector, +} from '../../../store/hooks'; + +import U from '../utils'; + +const useStats = () => { + const analytics = useCatsSelector(selectAnalytics); + const favourites = useCatsSelector(selectFavourites); + + const totalImagesViewed: number = analytics?.galleryCats || 0; + const breedsViewed = analytics?.breedsViewed; + const totalFavourites: number = favourites + ? Object.keys(favourites).length + : 0; + + const favoriteRate = useMemo((): string => { + if (totalImagesViewed === 0) return '0'; + return ((totalFavourites / totalImagesViewed) * 100).toFixed(2); + }, [totalImagesViewed, totalFavourites]); + + const stats = useMemo( + () => U.formatStats(totalImagesViewed, totalFavourites, favoriteRate), + [totalImagesViewed, totalFavourites, favoriteRate] + ); + + const barStats = useMemo( + () => U.formatBarStats(totalImagesViewed, totalFavourites), + [totalImagesViewed, totalFavourites] + ); + + const pieChartData = useMemo( + () => U.formatPieChartData(breedsViewed), + [analytics] + ); + + return { barStats, stats, pieChartData }; +}; + +export default useStats; diff --git a/src/views/analytics/constants.ts b/src/views/analytics/constants.ts new file mode 100644 index 00000000..3fdf1638 --- /dev/null +++ b/src/views/analytics/constants.ts @@ -0,0 +1,48 @@ +const BAR_COLOR_1 = '#fcc76f'; +const BAR_COLOR_2 = '#ff6b9d'; +const BAR_TITLE = 'Views vs Favorites'; +const CARD_COLOR_1 = '#fcc76f'; +const CARD_COLOR_2 = '#ff6b9d'; +const CARD_COLOR_3 = '#fbb94e'; +const FAVOURITES = 'Favourites'; +const FAVOURITES_RATE = 'Favourites Rate'; +const MAX_BREEDS_DISPLAYED = 6; +const NAME = 'name'; +const PIE_CHART_COLORS = [ + '#fcc76f', + '#ff6b9d', + '#fbb94e', + '#3d2817', + '#fce8b8', + '#8b7355', +]; +const PIE_CHART_EMPTY_TITLE = 'No breed data yet. Start exploring!'; +const PIE_CHART_TITLE = 'Most Viewed Breeds'; +const SUBTITLE = 'Track your cat viewing and favoriting activity'; +const TITLE = 'Analytics Dashboard'; +const TOTAL = 'Total'; +const TOTAL_FAVOURITES = 'Total Favourites'; +const TOTAL_IMAGES_VIEWED = 'Total Images Viewed'; +const VIEWS = 'Views'; + +export default { + BAR_COLOR_1, + BAR_COLOR_2, + BAR_TITLE, + CARD_COLOR_1, + CARD_COLOR_2, + CARD_COLOR_3, + FAVOURITES, + FAVOURITES_RATE, + MAX_BREEDS_DISPLAYED, + NAME, + PIE_CHART_COLORS, + PIE_CHART_EMPTY_TITLE, + PIE_CHART_TITLE, + SUBTITLE, + TITLE, + TOTAL, + TOTAL_FAVOURITES, + TOTAL_IMAGES_VIEWED, + VIEWS, +}; diff --git a/src/views/analytics/index.ts b/src/views/analytics/index.ts new file mode 100644 index 00000000..2cba8b9f --- /dev/null +++ b/src/views/analytics/index.ts @@ -0,0 +1 @@ +export { default } from './Analytics'; diff --git a/src/views/analytics/utils.ts b/src/views/analytics/utils.ts new file mode 100644 index 00000000..606d9681 --- /dev/null +++ b/src/views/analytics/utils.ts @@ -0,0 +1,84 @@ +import C from './constants'; + +type StatItem = { + label: string; + value: number | string; + color: string; +}; + +type BarChartItem = { + name: string; + Views: number; + Favourites: number; +}; + +type PieChartItem = { + name: string; + value: number; +}; + +const formatStats = ( + totalImagesViewed: number, + totalFavourites: number, + favoriteRate: string +): StatItem[] => { + return [ + { + label: C.TOTAL_IMAGES_VIEWED, + value: totalImagesViewed, + color: C.CARD_COLOR_1, + }, + { + label: C.TOTAL_FAVOURITES, + value: totalFavourites, + color: C.CARD_COLOR_2, + }, + { + label: C.FAVOURITES_RATE, + value: `${favoriteRate}%`, + color: C.CARD_COLOR_3, + }, + ]; +}; + +const formatBarStats = ( + totalImagesViewed: number, + totalFavourites: number +): BarChartItem[] => { + return [ + { + name: C.TOTAL, + Views: totalImagesViewed, + Favourites: totalFavourites, + }, + ]; +}; + +const getPieLabelText = ( + name: string | undefined, + percent: number | undefined +): string => `${name} ${((percent || 0) * 100).toFixed(2)}%`; + +const getPieFillColor = (index: number): string => + C.PIE_CHART_COLORS[index % C.PIE_CHART_COLORS.length]; + +const formatPieChartData = ( + breedsViewed?: Record +): PieChartItem[] => { + if (!breedsViewed) return []; + return Object.entries(breedsViewed) + .map(([breedName, count]) => ({ + name: breedName, + value: count as number, + })) + .sort((a, b) => b.value - a.value) + .slice(0, C.MAX_BREEDS_DISPLAYED); +}; + +export default { + formatBarStats, + formatStats, + getPieLabelText, + getPieFillColor, + formatPieChartData, +}; diff --git a/src/views/breeds/constants.ts b/src/views/breeds/constants.ts index 4368fef6..eee86956 100644 --- a/src/views/breeds/constants.ts +++ b/src/views/breeds/constants.ts @@ -1,7 +1,9 @@ const TITLE_TEXT = 'Cat Breeds'; const ERROR_MESSAGE = 'Failed to load breeds. Please try again.'; +const ONE_DAY_MS = 24 * 60 * 60 * 1000; export default { - TITLE_TEXT, ERROR_MESSAGE, + ONE_DAY_MS, + TITLE_TEXT, }; diff --git a/src/views/breeds/useBreeds.ts b/src/views/breeds/useBreeds.ts index db1e114f..ed2f8ba2 100644 --- a/src/views/breeds/useBreeds.ts +++ b/src/views/breeds/useBreeds.ts @@ -4,10 +4,12 @@ import { fetchAllBreeds } from '../../api'; import { breedsActions } from '../../store/breedsSlice'; import { selectBreeds, + selectBreedsWithMeta, useCatsDispatch, useCatsSelector, } from '../../store/hooks'; +import U from './utils'; import C from './constants'; import type { Breed } from '../../types'; @@ -17,11 +19,18 @@ const useBreeds = () => { const dispatch = useCatsDispatch(); const cachedBreeds: Breed[] = Object.values(useCatsSelector(selectBreeds)); + const breedsWithMeta = useCatsSelector(selectBreedsWithMeta); + const lastFetched = breedsWithMeta?.lastFetched; const areBreedsEmpty: boolean = cachedBreeds.length === 0; const isInitialLoading: boolean = areBreedsEmpty && isLoading; + const shouldFetch = U.shouldFetchBreeds(areBreedsEmpty, lastFetched); const loadBreeds = useCallback(() => { + if (!shouldFetch) { + return; + } + setIsLoading(true); setError(null); @@ -36,7 +45,7 @@ const useBreeds = () => { .finally(() => { setIsLoading(false); }); - }, [dispatch]); + }, [dispatch, shouldFetch]); return { cachedBreeds, diff --git a/src/views/breeds/utils.ts b/src/views/breeds/utils.ts index a5cf228a..2297dcf0 100644 --- a/src/views/breeds/utils.ts +++ b/src/views/breeds/utils.ts @@ -1,4 +1,5 @@ import type { Breed, Cat } from '../../types'; +import C from './constants'; const transformBreedToCatObject = (breed: Breed): Cat => { return { @@ -7,4 +8,16 @@ const transformBreedToCatObject = (breed: Breed): Cat => { }; }; -export default { transformBreedToCatObject }; +const shouldFetchBreeds = ( + isDataEmpty: boolean, + lastFetched: number | null +): boolean => { + if (isDataEmpty) return true; + if (!lastFetched) return true; + + const now = Date.now(); + const daysSinceLastFetch = (now - lastFetched) / C.ONE_DAY_MS; + return daysSinceLastFetch >= 1; +}; + +export default { shouldFetchBreeds, transformBreedToCatObject }; diff --git a/src/views/gallery/Gallery.module.css b/src/views/gallery/Gallery.module.css index cf9790ff..3bcd6d11 100644 --- a/src/views/gallery/Gallery.module.css +++ b/src/views/gallery/Gallery.module.css @@ -1,4 +1,3 @@ -/* Button Row for side-by-side buttons */ .buttonRow { display: flex; justify-content: center; diff --git a/src/views/gallery/useGallery.ts b/src/views/gallery/useGallery.ts index 3f3ebfbf..1c13be27 100644 --- a/src/views/gallery/useGallery.ts +++ b/src/views/gallery/useGallery.ts @@ -1,6 +1,7 @@ import { useCallback, useState } from 'react'; import { fetchRandomCats } from '../../api'; +import { analyticsActions } from '../../store/analyticsSlice'; import { galleryActions } from '../../store/gallerySlice'; import { useCatsDispatch, useCatsSelector } from '../../store/hooks'; import { selectFavourites, selectGallery } from '../../store/hooks'; @@ -33,6 +34,7 @@ const useGallery = () => { ); dispatch(galleryActions.addCats(uniqueNewCats)); + dispatch(analyticsActions.incrementGalleryCats(uniqueNewCats.length)); setCats((prev) => [...prev, ...uniqueNewCats]); }) .catch((err) => { diff --git a/src/views/index.ts b/src/views/index.ts index 263670b2..28c4dce6 100644 --- a/src/views/index.ts +++ b/src/views/index.ts @@ -1,3 +1,4 @@ export { default as Breeds } from './breeds'; +export { default as Analytics } from './analytics'; export { default as Favourites } from './favourites'; export { default as Gallery } from './gallery'; From f47daaef068b848c22a36b2026cca20164ef9770 Mon Sep 17 00:00:00 2001 From: mkour Date: Mon, 17 Nov 2025 11:16:03 +0200 Subject: [PATCH 13/17] add tests for edge cases and error-prone logic in Redux slices (analytics, breeds, favourites) and some utility functions (breeds, analytics) --- __tests__/unit/store/analyticsSlice.test.ts | 124 +++++++++++ __tests__/unit/store/breedsSlice.test.ts | 186 ++++++++++++++++ __tests__/unit/store/favouritesSlice.test.ts | 79 +++++++ __tests__/unit/utils/breeds.test.ts | 40 ++++ __tests__/unit/utils/graphs.test.ts | 158 ++++++++++++++ package-lock.json | 210 ++++++++----------- package.json | 3 +- 7 files changed, 675 insertions(+), 125 deletions(-) create mode 100644 __tests__/unit/store/analyticsSlice.test.ts create mode 100644 __tests__/unit/store/breedsSlice.test.ts create mode 100644 __tests__/unit/store/favouritesSlice.test.ts create mode 100644 __tests__/unit/utils/breeds.test.ts create mode 100644 __tests__/unit/utils/graphs.test.ts diff --git a/__tests__/unit/store/analyticsSlice.test.ts b/__tests__/unit/store/analyticsSlice.test.ts new file mode 100644 index 00000000..862d8548 --- /dev/null +++ b/__tests__/unit/store/analyticsSlice.test.ts @@ -0,0 +1,124 @@ +import { describe, it, expect } from 'vitest'; +import analyticsReducer, { + analyticsActions, +} from '../../../src/store/analyticsSlice'; +import type { AnalyticsState } from '../../../src/store/types'; + +const { + incrementBreedView, + incrementGalleryCats, + resetAnalytics, + loadAnalytics, +} = analyticsActions; + +describe('analyticsSlice', () => { + const initialState: AnalyticsState = { + galleryCats: 0, + breedsViewed: {}, + }; + + describe('incrementGalleryCats', () => { + it('should increment gallery cats count by the number given', () => { + const state = { ...initialState }; + const action = incrementGalleryCats(8); + const newState = analyticsReducer(state, action); + expect(newState.galleryCats).toBe(8); + }); + + it('should add to existing gallery cats count', () => { + const state: AnalyticsState = { galleryCats: 5, breedsViewed: {} }; + const action = incrementGalleryCats(10); + const newState = analyticsReducer(state, action); + expect(newState.galleryCats).toBe(15); + }); + }); + + describe('incrementBreedView', () => { + it('should add new breed to breedsViewed', () => { + const state = { ...initialState }; + const action = incrementBreedView({ breedName: 'Randrom Breed Name' }); + const newState = analyticsReducer(state, action); + expect(newState.breedsViewed['Randrom Breed Name']).toBe(1); + }); + + it('should increment existing breed view count', () => { + const state: AnalyticsState = { + galleryCats: 0, + breedsViewed: { 'Random Breed Name': 2 }, + }; + const action = incrementBreedView({ breedName: 'Random Breed Name' }); + const newState = analyticsReducer(state, action); + expect(newState.breedsViewed['Random Breed Name']).toBe(3); + }); + + it('should track multiple different breeds and increment correctly all of them', () => { + let state: AnalyticsState = { ...initialState }; + state = analyticsReducer( + state, + incrementBreedView({ breedName: 'Breed 2' }) + ); + state = analyticsReducer( + state, + incrementBreedView({ breedName: 'Breed 1' }) + ); + state = analyticsReducer( + state, + incrementBreedView({ breedName: 'Breed 2' }) + ); + + expect(state.breedsViewed['Breed 2']).toBe(2); + expect(state.breedsViewed['Breed 1']).toBe(1); + }); + + it('should handle breed names with spaces', () => { + const state = { ...initialState }; + const action = incrementBreedView({ breedName: 'Maine Coon' }); + const newState = analyticsReducer(state, action); + expect(newState.breedsViewed['Maine Coon']).toBe(1); + }); + }); + + describe('resetAnalytics', () => { + it('should reset analytics to initial state', () => { + const state: AnalyticsState = { + galleryCats: 100, + breedsViewed: { Siamese: 10, Persian: 5 }, + }; + const action = resetAnalytics(); + const newState = analyticsReducer(state, action); + expect(newState.galleryCats).toBe(0); + expect(newState.breedsViewed).toEqual({}); + }); + }); + + describe('loadAnalytics', () => { + it('should load analytics state from payload', () => { + const state = { ...initialState }; + const payload: AnalyticsState = { + galleryCats: 22, + breedsViewed: { 'Random Breed': 15 }, + }; + const action = loadAnalytics(payload); + const newState = analyticsReducer(state, action); + expect(newState.galleryCats).toBe(22); + expect(newState.breedsViewed['Random Breed']).toBe(15); + }); + + it('should replace entire analytics state', () => { + const state: AnalyticsState = { + galleryCats: 100, + breedsViewed: { Persian: 10 }, + }; + const payload: AnalyticsState = { + galleryCats: 25, + breedsViewed: { Siamese: 8, Bengal: 3 }, + }; + const action = loadAnalytics(payload); + const newState = analyticsReducer(state, action); + expect(newState.galleryCats).toBe(25); + expect(newState.breedsViewed['Persian']).toBeUndefined(); + expect(newState.breedsViewed['Siamese']).toBe(8); + expect(newState.breedsViewed['Bengal']).toBe(3); + }); + }); +}); diff --git a/__tests__/unit/store/breedsSlice.test.ts b/__tests__/unit/store/breedsSlice.test.ts new file mode 100644 index 00000000..a85237f5 --- /dev/null +++ b/__tests__/unit/store/breedsSlice.test.ts @@ -0,0 +1,186 @@ +import { describe, it, expect } from 'vitest'; +import breedsReducer, { + breedsActions, + type BreedsStateWithMeta, +} from '../../../src/store/breedsSlice'; +import type { Breed } from '../../../src/types'; + +const { addBreeds, clearBreeds, loadBreeds } = breedsActions; + +const mockBreed: Breed = { + id: 'bengal', + name: 'Bengal', + temperament: 'Alert, Agile, Active', + origin: 'United States', + description: 'The Bengal is a leopard cat hybrid', + life_span: '10 - 16', + affection_level: 4, + child_friendly: 3, + dog_friendly: 3, + weight: { + imperial: '8 - 15', + metric: '4 - 7', + }, + image: { + id: 'img1', + url: 'http://example.com/cat.png', + width: 400, + height: 300, + }, +}; + +describe('breedsSlice', () => { + const initialState: BreedsStateWithMeta = { + data: {}, + lastFetched: null, + }; + + describe('addBreeds', () => { + it('should add a single breed', () => { + const state: BreedsStateWithMeta = { ...initialState }; + const action = addBreeds([mockBreed]); + const newState = breedsReducer(state, action); + expect(newState.data['bengal']).toBe(mockBreed); + }); + + it('should add multiple breeds', () => { + const state: BreedsStateWithMeta = { ...initialState }; + const siamese: Breed = { ...mockBreed, id: 'siamese', name: 'Siamese' }; + const persian: Breed = { ...mockBreed, id: 'persian', name: 'Persian' }; + const action = addBreeds([mockBreed, siamese, persian]); + const newState = breedsReducer(state, action); + expect(Object.keys(newState.data)).toHaveLength(3); + expect(newState.data['bengal']).toBe(mockBreed); + expect(newState.data['siamese']).toBe(siamese); + expect(newState.data['persian']).toBe(persian); + }); + + it('should update existing breed when adding duplicate id', () => { + const state: BreedsStateWithMeta = { + data: { bengal: mockBreed }, + lastFetched: Date.now(), + }; + const updatedBreed: Breed = { ...mockBreed, temperament: 'Updated' }; + const action = addBreeds([updatedBreed]); + const newState = breedsReducer(state, action); + expect(newState.data['bengal'].temperament).toBe('Updated'); + }); + + it('should preserve existing breeds when adding new ones', () => { + const siamese: Breed = { ...mockBreed, id: 'siamese', name: 'Siamese' }; + const state: BreedsStateWithMeta = { + data: { siamese: siamese }, + lastFetched: Date.now(), + }; + const action = addBreeds([mockBreed]); + const newState = breedsReducer(state, action); + expect(Object.keys(newState.data)).toHaveLength(2); + expect(newState.data['bengal']).toBe(mockBreed); + expect(newState.data['siamese']).toBe(siamese); + }); + + it('should handle empty breed array', () => { + const state: BreedsStateWithMeta = { + data: { bengal: mockBreed }, + lastFetched: Date.now(), + }; + const action = addBreeds([]); + const newState = breedsReducer(state, action); + expect(newState.data['bengal']).toBe(mockBreed); + }); + + it('should update lastFetched if 24 hours passed', () => { + const ONE_DAY_MS = 24 * 60 * 60 * 1000; + const oldTimestamp = Date.now() - ONE_DAY_MS - 1000; + const state: BreedsStateWithMeta = { + data: {}, + lastFetched: oldTimestamp, + }; + const action = addBreeds([mockBreed]); + const newState = breedsReducer(state, action); + expect(newState.lastFetched).toBeGreaterThan(oldTimestamp); + }); + }); + + describe('clearBreeds', () => { + it('should clear all breeds', () => { + const state: BreedsStateWithMeta = { + data: { + bengal: mockBreed, + siamese: { ...mockBreed, id: 'siamese' }, + }, + lastFetched: Date.now(), + }; + const action = clearBreeds(); + const newState = breedsReducer(state, action); + expect(newState).toEqual(initialState); + }); + + it('should work on empty state', () => { + const state: BreedsStateWithMeta = { ...initialState }; + const action = clearBreeds(); + const newState = breedsReducer(state, action); + expect(newState).toEqual(initialState); + }); + }); + + describe('loadBreeds', () => { + it('should load breeds from payload', () => { + const state: BreedsStateWithMeta = { ...initialState }; + const payload: BreedsStateWithMeta = { + data: { bengal: mockBreed }, + lastFetched: Date.now(), + }; + const action = loadBreeds(payload); + const newState = breedsReducer(state, action); + expect(newState.data['bengal']).toBe(mockBreed); + }); + + it('should replace existing breeds', () => { + const state: BreedsStateWithMeta = { + data: { bengal: mockBreed }, + lastFetched: Date.now(), + }; + const siamese: Breed = { ...mockBreed, id: 'siamese', name: 'Siamese' }; + const payload: BreedsStateWithMeta = { + data: { siamese: siamese }, + lastFetched: Date.now(), + }; + const action = loadBreeds(payload); + const newState = breedsReducer(state, action); + expect(newState.data['bengal']).toBeUndefined(); + expect(newState.data['siamese']).toBe(siamese); + }); + + it('should load multiple breeds at once', () => { + const state: BreedsStateWithMeta = { ...initialState }; + const siamese: Breed = { ...mockBreed, id: 'siamese', name: 'Siamese' }; + const persian: Breed = { ...mockBreed, id: 'persian', name: 'Persian' }; + const payload: BreedsStateWithMeta = { + data: { + bengal: mockBreed, + siamese: siamese, + persian: persian, + }, + lastFetched: Date.now(), + }; + const action = loadBreeds(payload); + const newState = breedsReducer(state, action); + expect(Object.keys(newState.data)).toHaveLength(3); + }); + + it('should load empty state', () => { + const state: BreedsStateWithMeta = { + data: { bengal: mockBreed }, + lastFetched: Date.now(), + }; + const payload: BreedsStateWithMeta = { + data: {}, + lastFetched: null, + }; + const action = loadBreeds(payload); + const newState = breedsReducer(state, action); + expect(newState).toEqual(payload); + }); + }); +}); diff --git a/__tests__/unit/store/favouritesSlice.test.ts b/__tests__/unit/store/favouritesSlice.test.ts new file mode 100644 index 00000000..8551d550 --- /dev/null +++ b/__tests__/unit/store/favouritesSlice.test.ts @@ -0,0 +1,79 @@ +import { describe, it, expect } from 'vitest'; +import favouritesReducer, { + favouritesActions, +} from '../../../src/store/favouritesSlice'; +import type { FavouritesState } from '../../../src/store/types'; +import type { Cat } from '../../../src/types'; + +const { toggleFavourite, clearFavourites, loadFavourites } = favouritesActions; + +const mockCat: Cat = { + id: 'cat1', + url: 'http://example.com/cat1.jpg', + width: 400, + height: 300, + breeds: [], +}; + +describe('favouritesSlice', () => { + const initialState: FavouritesState = {}; + + describe('toggleFavourite', () => { + it('should add a cat to favourites', () => { + const state: FavouritesState = { ...initialState }; + const action = toggleFavourite(mockCat); + const newState = favouritesReducer(state, action); + expect(newState['cat1']).toBe(mockCat); + }); + + it('should remove a cat from favourites', () => { + const state: FavouritesState = { cat1: mockCat }; + const action = toggleFavourite(mockCat); + const newState = favouritesReducer(state, action); + expect(newState['cat1']).toBeUndefined(); + }); + }); + + describe('clearFavourites', () => { + it('should work on empty state', () => { + const state: FavouritesState = { ...initialState }; + const action = clearFavourites(); + const newState = favouritesReducer(state, action); + expect(newState).toEqual({}); + }); + }); + + describe('loadFavourites', () => { + it('should load favourites from payload', () => { + const state: FavouritesState = { ...initialState }; + const payload: FavouritesState = { cat1: mockCat }; + const action = loadFavourites(payload); + const newState = favouritesReducer(state, action); + expect(newState['cat1']).toBe(mockCat); + }); + + it('should replace existing favourites', () => { + const state: FavouritesState = { cat1: mockCat }; + const cat2: Cat = { ...mockCat, id: 'cat2' }; + const payload: FavouritesState = { cat2: cat2 }; + const action = loadFavourites(payload); + const newState = favouritesReducer(state, action); + expect(newState['cat1']).toBeUndefined(); + expect(newState['cat2']).toBe(cat2); + }); + + it('should load multiple favourites at once', () => { + const state: FavouritesState = { ...initialState }; + const cat2: Cat = { ...mockCat, id: 'cat2' }; + const cat3: Cat = { ...mockCat, id: 'cat3' }; + const payload: FavouritesState = { + cat1: mockCat, + cat2: cat2, + cat3: cat3, + }; + const action = loadFavourites(payload); + const newState = favouritesReducer(state, action); + expect(Object.keys(newState)).toHaveLength(3); + }); + }); +}); diff --git a/__tests__/unit/utils/breeds.test.ts b/__tests__/unit/utils/breeds.test.ts new file mode 100644 index 00000000..cf5e0113 --- /dev/null +++ b/__tests__/unit/utils/breeds.test.ts @@ -0,0 +1,40 @@ +import { describe, it, expect } from 'vitest'; +import U from '../../../src/views/breeds/utils'; + +describe('breeds utils fetch', () => { + describe('shouldFetchBreeds', () => { + it('should return true when data is empty', () => { + const result = U.shouldFetchBreeds(true, null); + expect(result).toBe(true); + }); + + it('should return true when lastFetched is null', () => { + const result = U.shouldFetchBreeds(false, null); + expect(result).toBe(true); + }); + + it('should return false when cache is newer than 24 hours ago', () => { + const oneHourAgo = Date.now() - 60 * 60 * 1000; + const result = U.shouldFetchBreeds(false, oneHourAgo); + expect(result).toBe(false); + }); + + it('should return true when cache is older than 1 day', () => { + const twentyFiveHoursAgo = Date.now() - 25 * 60 * 60 * 1000; + const result = U.shouldFetchBreeds(false, twentyFiveHoursAgo); + expect(result).toBe(true); + }); + + it('should return true when cache is exactly 1 day old', () => { + const oneDayAgo = Date.now() - 24 * 60 * 60 * 1000; + const result = U.shouldFetchBreeds(false, oneDayAgo); + expect(result).toBe(true); + }); + + it('should handle empty data and old cache', () => { + const oldTimestamp = Date.now() - 48 * 60 * 60 * 1000; + const result = U.shouldFetchBreeds(true, oldTimestamp); + expect(result).toBe(true); + }); + }); +}); diff --git a/__tests__/unit/utils/graphs.test.ts b/__tests__/unit/utils/graphs.test.ts new file mode 100644 index 00000000..6f39bdd7 --- /dev/null +++ b/__tests__/unit/utils/graphs.test.ts @@ -0,0 +1,158 @@ +import { describe, it, expect } from 'vitest'; +import U from '../../../src/views/analytics/utils'; + +const { formatStats, formatBarStats, formatPieChartData } = U; + +describe('Analytics Stats', () => { + describe('formatStats', () => { + it('should format stats with correct structure', () => { + const result = formatStats(50, 25, '50.0'); + expect(result).toHaveLength(3); + expect(result[0]).toMatchObject({ + label: expect.any(String), + value: expect.any(Number), + color: expect.any(String), + }); + }); + + it('should include total images viewed stat', () => { + const result = formatStats(100, 25, '25.0'); + expect(result[0].value).toBe(100); + }); + + it('should include total favourites stat', () => { + const result = formatStats(100, 25, '25.0'); + expect(result[1].value).toBe(25); + }); + + it('should include favourite rate stat', () => { + const result = formatStats(100, 25, '25.0'); + expect(result[2].value).toBe('25.0%'); + }); + + it('should handle zero values', () => { + const result = formatStats(0, 0, '0'); + expect(result[0].value).toBe(0); + expect(result[1].value).toBe(0); + expect(result[2].value).toBe('0%'); + }); + + it('should handle high values', () => { + const result = formatStats(10000, 5000, '50.0'); + expect(result[0].value).toBe(10000); + expect(result[1].value).toBe(5000); + }); + }); + + describe('formatBarStats', () => { + it('should format bar stats with correct structure', () => { + const result = formatBarStats(100, 25); + expect(result).toHaveLength(1); + expect(result[0]).toMatchObject({ + name: expect.any(String), + Views: expect.any(Number), + Favourites: expect.any(Number), + }); + }); + + it('should include views count', () => { + const result = formatBarStats(150, 50); + expect(result[0].Views).toBe(150); + }); + + it('should include favourites count', () => { + const result = formatBarStats(150, 50); + expect(result[0].Favourites).toBe(50); + }); + + it('should have name property for chart', () => { + const result = formatBarStats(100, 25); + expect(result[0].name).toBe('Total'); + }); + + it('should handle zero values', () => { + const result = formatBarStats(0, 0); + expect(result[0].Views).toBe(0); + expect(result[0].Favourites).toBe(0); + }); + }); + + describe('formatPieChartData', () => { + it('should return empty array when breedsViewed is undefined', () => { + const result = formatPieChartData(undefined); + expect(result).toEqual([]); + }); + + it('should return empty array when breedsViewed is empty', () => { + const result = formatPieChartData({}); + expect(result).toEqual([]); + }); + + it('should format single breed correctly', () => { + const breedsViewed = { Siamese: 5 }; + const result = formatPieChartData(breedsViewed); + + expect(result).toHaveLength(1); + expect(result[0]).toMatchObject({ + name: 'Siamese', + value: 5, + }); + }); + + it('should format multiple breeds correctly', () => { + const breedsViewed = { + Siamese: 10, + Persian: 8, + 'Maine Coon': 6, + }; + const result = formatPieChartData(breedsViewed); + + expect(result).toHaveLength(3); + }); + + it('should sort breeds by view count (descending)', () => { + const breedsViewed = { + Siamese: 5, + Persian: 15, + 'Maine Coon': 10, + }; + const result = formatPieChartData(breedsViewed); + + expect(result[0].name).toBe('Persian'); + expect(result[0].value).toBe(15); + expect(result[1].name).toBe('Maine Coon'); + expect(result[2].name).toBe('Siamese'); + }); + + it('should limit to top 6 breeds', () => { + const breedsViewed = { + Breed1: 10, + Breed2: 9, + Breed3: 8, + Breed4: 7, + Breed5: 6, + Breed6: 5, + Breed7: 4, + Breed8: 3, + }; + const result = formatPieChartData(breedsViewed); + + expect(result).toHaveLength(6); + expect(result[result.length - 1].name).toBe('Breed6'); + }); + + it('should handle breeds with same view count', () => { + const breedsViewed = { + Siamese: 10, + Persian: 10, + 'Maine Coon': 10, + }; + const result = formatPieChartData(breedsViewed); + + expect(result).toHaveLength(3); + expect(result.every((item: { value: number }) => item.value === 10)).toBe( + true + ); + }); + }); +}); diff --git a/package-lock.json b/package-lock.json index 7da0600f..8cb8ea65 100644 --- a/package-lock.json +++ b/package-lock.json @@ -964,23 +964,6 @@ "node": ">=18" } }, - "node_modules/@esbuild/win32-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", - "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, "node_modules/@eslint-community/eslint-utils": { "version": "4.9.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", @@ -1612,20 +1595,6 @@ "win32" ] }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.53.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.53.2.tgz", - "integrity": "sha512-k+/Rkcyx//P6fetPoLMb8pBeqJBNGx81uuf7iljX9++yNBVRDQgD04L+SVXmXmh5ZP4/WOp4mWF0kmi06PW2tA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, "node_modules/@standard-schema/spec": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz", @@ -2572,9 +2541,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001754", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001754.tgz", - "integrity": "sha512-x6OeBXueoAceOmotzx3PO4Zpt4rzpeIFsSr6AAePTZxSkXiYDUmpypEl7e2+8NCd9bD7bXjqyef8CJYPC1jfxg==", + "version": "1.0.30001755", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001755.tgz", + "integrity": "sha512-44V+Jm6ctPj7R52Na4TLi3Zri4dWUljJd+RDm+j8LtNCc/ihLCT+X1TzoOAkRETEWqjuLnh9581Tl80FvK7jVA==", "dev": true, "funding": [ { @@ -2735,9 +2704,9 @@ } }, "node_modules/csstype": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.1.tgz", - "integrity": "sha512-98XGutrXoh75MlgLihlNxAGbUuFQc7l1cqcnEZlLNKc0UrVdPndgmaDmYTDDh929VS/eqTZV0rozmhu2qqT1/g==", + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.2.tgz", + "integrity": "sha512-D80T+tiqkd/8B0xNlbstWDG4x6aqVfO52+OlSUNIdkTvmNw0uQpJLeos2J/2XvpyidAFuTPmpad+tUxLndwj6g==", "devOptional": true, "license": "MIT" }, @@ -2955,9 +2924,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.253", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.253.tgz", - "integrity": "sha512-O0tpQ/35rrgdiGQ0/OFWhy1itmd9A6TY9uQzlqj3hKSu/aYpe7UIn5d7CU2N9myH6biZiWF3VMZVuup8pw5U9w==", + "version": "1.5.254", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.254.tgz", + "integrity": "sha512-DcUsWpVhv9svsKRxnSCZ86SjD+sp32SGidNB37KpqXJncp1mfUgKbHvBomE89WJDbfVKw1mdv5+ikrvd43r+Bg==", "dev": true, "license": "ISC" }, @@ -3078,6 +3047,23 @@ "@esbuild/win32-x64": "0.25.12" } }, + "node_modules/esbuild/node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -3399,6 +3385,24 @@ "reusify": "^1.0.4" } }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, "node_modules/fflate": { "version": "0.8.2", "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", @@ -4080,6 +4084,19 @@ "node": ">=8.6" } }, + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/mime-db": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", @@ -4285,13 +4302,14 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { - "node": ">=8.6" + "node": ">=12" }, "funding": { "url": "https://github.com/sponsors/jonschlinkert" @@ -4380,6 +4398,13 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/pretty-format/node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true, + "license": "MIT" + }, "node_modules/proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", @@ -4441,9 +4466,9 @@ } }, "node_modules/react-is": { - "version": "17.0.2", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", - "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "version": "19.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.0.tgz", + "integrity": "sha512-x3Ax3kNSMIIkyVYhWPyO09bu0uttcAIoecO/um/rKGQ4EltYWVYtyiGkS/3xMynrbVQdS69Jhlv8FXUEZehlzA==", "license": "MIT", "peer": true }, @@ -4658,6 +4683,20 @@ "fsevents": "~2.3.2" } }, + "node_modules/rollup/node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.53.2.tgz", + "integrity": "sha512-k+/Rkcyx//P6fetPoLMb8pBeqJBNGx81uuf7iljX9++yNBVRDQgD04L+SVXmXmh5ZP4/WOp4mWF0kmi06PW2tA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -4876,38 +4915,6 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, - "node_modules/tinyglobby/node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "node_modules/tinyglobby/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "node_modules/tinyrainbow": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.0.3.tgz", @@ -5207,38 +5214,6 @@ } } }, - "node_modules/vite/node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "node_modules/vite/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "node_modules/vitest": { "version": "4.0.9", "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.0.9.tgz", @@ -5318,19 +5293,6 @@ } } }, - "node_modules/vitest/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "node_modules/w3c-xmlserializer": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", diff --git a/package.json b/package.json index edf9a67c..b43001f4 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,8 @@ "dev": "vite", "build": "tsc -b && vite build", "lint": "eslint .", - "preview": "vite preview" + "preview": "vite preview", + "test": "vitest" }, "dependencies": { "@reduxjs/toolkit": "^2.10.1", From 3fd498d5ff316ca62c881bc00a028b2b87b6e45f Mon Sep 17 00:00:00 2001 From: mkour Date: Mon, 17 Nov 2025 11:27:31 +0200 Subject: [PATCH 14/17] minor fixes - Fix navigation bug - Modify CSS for improved layout and appearance - Update favicon --- .env.example | 5 ++++ public/favicon-dark.png | Bin 4307 -> 5407 bytes public/favicon-light.png | Bin 4228 -> 4299 bytes .../breedModal/BreedModal.module.css | 4 +-- src/components/card/Card.module.css | 2 +- .../FavouriteButton.module.css | 2 +- src/components/favouriteButton/constants.ts | 2 +- src/components/modal/Modal.module.css | 6 ++--- src/components/navigation/Navigation.tsx | 2 +- src/components/navigation/constants.ts | 2 ++ src/components/navigation/useNavigation.ts | 2 +- src/index.css | 25 ++++++++++-------- src/views/analytics/Analytics.module.css | 2 +- src/views/shared/viewsLayout.module.css | 2 +- 14 files changed, 33 insertions(+), 23 deletions(-) create mode 100644 .env.example diff --git a/.env.example b/.env.example new file mode 100644 index 00000000..ae78aa74 --- /dev/null +++ b/.env.example @@ -0,0 +1,5 @@ +# TheCatAPI Configuration +# Get your free API key at https://thecatapi.com/signup + +VITE_CAT_API_KEY=your_api_key_here +VITE_CAT_API_BASE_URL=https://api.thecatapi.com/v1 diff --git a/public/favicon-dark.png b/public/favicon-dark.png index d4f11f7b65fed9b35fd86ac9e560871d42928a36..ba0028d06a623eadaa736c434dffff56f30bde5e 100644 GIT binary patch delta 5172 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 delta 4063 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 SizTpChZyOhbgQ(T;{FFMWZ$3w delta 3982 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 { {C.GALLERY} {C.BREEDS} diff --git a/src/components/navigation/constants.ts b/src/components/navigation/constants.ts index 888f2154..e47cb16f 100644 --- a/src/components/navigation/constants.ts +++ b/src/components/navigation/constants.ts @@ -1,4 +1,5 @@ const BREEDS = 'Breeds'; +const BREEDS_PATH = '/breeds'; const DEFAULT_PATH = '/'; const GALLERY = 'Gallery'; const IMAGE_SOURCE = '/cat.png'; @@ -11,6 +12,7 @@ const TITLE = 'Cat Lover'; export default { BREEDS, + BREEDS_PATH, DEFAULT_PATH, GALLERY, IMAGE_SOURCE, diff --git a/src/components/navigation/useNavigation.ts b/src/components/navigation/useNavigation.ts index d9f65ef7..978c8b8e 100644 --- a/src/components/navigation/useNavigation.ts +++ b/src/components/navigation/useNavigation.ts @@ -23,7 +23,7 @@ const useNavigation = (): UseNavigationState => { const isGalleryActive = isActive(C.DEFAULT_PATH) || location.pathname.startsWith('/cat/'); - const isBreedsActive = isActive('/breeds'); + const isBreedsActive = isActive(C.BREEDS_PATH); const isFavouritesActive = isActive(C.FAVOURITES_PATH); const isDemographicsActive = isActive(C.ANALYTICS_PATH); diff --git a/src/index.css b/src/index.css index 36938c96..72a58ca5 100644 --- a/src/index.css +++ b/src/index.css @@ -11,15 +11,12 @@ text-rendering: optimizeLegibility; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; + --scale: 0.2; } -a { - font-weight: 500; - color: #646cff; - text-decoration: inherit; -} -a:hover { - color: #535bf2; +html { + overflow-x: hidden; + font-size: 70%; } body { @@ -29,10 +26,6 @@ body { overflow-x: hidden; } -html { - overflow-x: hidden; -} - h1 { font-size: 3.2em; line-height: 1.1; @@ -57,6 +50,16 @@ button:focus-visible { outline: 4px auto -webkit-focus-ring-color; } +a { + font-weight: 500; + color: #646cff; + text-decoration: inherit; +} + +a:hover { + color: #535bf2; +} + @media (prefers-color-scheme: light) { :root { color: #213547; diff --git a/src/views/analytics/Analytics.module.css b/src/views/analytics/Analytics.module.css index ec8f1f31..67d457c5 100644 --- a/src/views/analytics/Analytics.module.css +++ b/src/views/analytics/Analytics.module.css @@ -1,5 +1,5 @@ .container { - max-width: 1200px; + max-width: 950px; margin: 0 auto; padding: 2rem; } diff --git a/src/views/shared/viewsLayout.module.css b/src/views/shared/viewsLayout.module.css index f220f9dd..23203253 100644 --- a/src/views/shared/viewsLayout.module.css +++ b/src/views/shared/viewsLayout.module.css @@ -1,6 +1,6 @@ .container { padding: 2rem; - max-width: 1200px; + max-width: 900px; margin: 0 auto; background: transparent; } From bd38624657c58ba5a0bd6092f35354ad4da05134 Mon Sep 17 00:00:00 2001 From: mkour Date: Mon, 17 Nov 2025 11:39:54 +0200 Subject: [PATCH 15/17] - add README file - fix import order by running eslint fix --- README.md | 310 ++++++++++++++---- __tests__/unit/store/analyticsSlice.test.ts | 3 +- __tests__/unit/store/breedsSlice.test.ts | 3 +- __tests__/unit/store/favouritesSlice.test.ts | 3 +- __tests__/unit/utils/breeds.test.ts | 3 +- __tests__/unit/utils/graphs.test.ts | 3 +- src/components/favouriteButton/utils.ts | 2 +- src/hooks/useAnalyticsPersistence.ts | 2 +- src/hooks/useBreedsPersistence.ts | 1 + src/hooks/useFavouritesPersistence.ts | 1 + src/router.tsx | 2 +- .../components/BreedDistributionChart.tsx | 4 +- src/views/analytics/components/StatCards.tsx | 3 +- .../components/ViewsVsFavoritesChart.tsx | 4 +- src/views/analytics/components/useStats.ts | 3 +- src/views/breeds/utils.ts | 2 +- src/views/index.ts | 2 +- 17 files changed, 275 insertions(+), 76 deletions(-) diff --git a/README.md b/README.md index d2e77611..1d3eeaa9 100644 --- a/README.md +++ b/README.md @@ -1,73 +1,263 @@ -# React + TypeScript + Vite +# ๐Ÿฑ Cat Lover - React Cat Gallery Application -This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. +**A React/TypeScript application for cat enthusiasts built with TheCatAPI** -Currently, two official plugins are available: +> ๐ŸŽฎ **[Live Demo](#)** | ๐Ÿ”„ **Smart Caching** | ๐Ÿ“Š **Analytics Dashboard** | ๐Ÿ”— **Shareable URLs** -- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh -- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh +
-## React Compiler + -The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation). +
-## Expanding the ESLint configuration +## โœ… Challenge Requirements Completed -If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules: +| Requirement | Status | Implementation | +| ----------------------------- | ------ | ---------------------------------------------- | +| **Random Cat Gallery** | โœ… | 10 cats per load with infinite loading | +| **Shareable Image Modals** | โœ… | Copy-paste URLs that maintain image state | +| **Breed Information Display** | โœ… | Full breed details with temperament and origin | +| **Breed List View** | โœ… | Complete catalogue with images | +| **Favorites Management** | โœ… | Add/remove favorites from gallery or modal | -```js -export default defineConfig([ - globalIgnores(['dist']), - { - files: ['**/*.{ts,tsx}'], - extends: [ - // Other configs... +## ๐ŸŽฏ Extra Features (Beyond Requirements) - // Remove tseslint.configs.recommended and replace with this - tseslint.configs.recommendedTypeChecked, - // Alternatively, use this for stricter rules - tseslint.configs.strictTypeChecked, - // Optionally, add this for stylistic rules - tseslint.configs.stylisticTypeChecked, +### Analytics Dashboard - // Other configs... - ], - languageOptions: { - parserOptions: { - project: ['./tsconfig.node.json', './tsconfig.app.json'], - tsconfigRootDir: import.meta.dirname, - }, - // other options... - }, - }, -]) +A complete analytics view tracking user behavior: + +- **Gallery Statistics**: Total cats loaded +- **Breed Views**: Most popular breeds viewed +- **Favorites Tracking**: Comparison between views and favorites +- **Visual Charts**: Bar charts and pie charts using Recharts + +### Smart Breed Caching + +Intelligent data management for optimal performance: + +- **24-Hour Cache**: Breed data fetched once per day +- **Timestamp Validation**: Automatic cache invalidation after 24 hours +- **LocalStorage Integration**: Persistent cache across browser sessions +- **Efficient API Usage**: Minimizes unnecessary API calls + +### Dual Favorite Actions + +Users can favorite cats in two ways: + +1. **From Gallery**: Quick-add button on each card +2. **From Modal**: Expanded favorite button with visual feedback + +## Getting Started + +```bash +# Clone the repository +git clone https://github.com/mkourogiorgas/cat-lover.git + +# Navigate to project directory +cd cat-lover + +# Install dependencies +npm install + +# Create .env file with your API key +echo "VITE_CAT_API_KEY=your_api_key_here" > .env + +# Start development server +npm run dev +``` + +**Requirements:** + +- Node.js 18+ +- npm 9+ +- TheCatAPI key (free at [thecatapi.com](https://thecatapi.com/)) + +## Technical Stack + +| Technology | Purpose | Version | +| ----------------- | -------------------- | ------- | +| **React** | UI Framework | 19.2.0 | +| **TypeScript** | Type Safety | 5.9.3 | +| **Vite** | Build Tool | 7.2.2 | +| **Redux Toolkit** | State Management | 2.10.1 | +| **React Router** | Navigation & Routing | 7.9.5 | +| **Axios** | HTTP Client | 1.13.2 | +| **Recharts** | Data Visualization | 3.4.1 | +| **CSS Modules** | Scoped Styling | Native | +| **Vitest** | Testing Framework | 4.0.8 | + +## Architecture Highlights + +### State Management Strategy + +``` +Redux Store +โ”œโ”€โ”€ gallerySlice โ†’ Random cat images (normalized by ID) +โ”œโ”€โ”€ breedsSlice โ†’ Breed catalog with timestamp metadata +โ”œโ”€โ”€ favouritesSlice โ†’ User favorites (persisted to localStorage) +โ””โ”€โ”€ analyticsSlice โ†’ User interaction tracking ``` -You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules: - -```js -// eslint.config.js -import reactX from 'eslint-plugin-react-x' -import reactDom from 'eslint-plugin-react-dom' - -export default defineConfig([ - globalIgnores(['dist']), - { - files: ['**/*.{ts,tsx}'], - extends: [ - // Other configs... - // Enable lint rules for React - reactX.configs['recommended-typescript'], - // Enable lint rules for React DOM - reactDom.configs.recommended, - ], - languageOptions: { - parserOptions: { - project: ['./tsconfig.node.json', './tsconfig.app.json'], - tsconfigRootDir: import.meta.dirname, - }, - // other options... - }, - }, -]) +### Smart Caching Logic + +**Why This Matters:** + +- โœ… Reduces API calls (breed data rarely changes) +- โœ… Faster load times (data served from localStorage) +- โœ… Better UX (instant navigation between views) + +### Project Structure + +``` +src/ +โ”œโ”€โ”€ api/ # API client and services +โ”‚ โ”œโ”€โ”€ api.ts # Axios instance with interceptors +โ”‚ โ”œโ”€โ”€ services.ts # API methods (fetchCats, fetchBreeds) +โ”‚ โ””โ”€โ”€ endpoints.ts # API endpoint constants +โ”œโ”€โ”€ components/ # Reusable UI components +โ”‚ โ”œโ”€โ”€ card/ # Cat card with image and favorite button +โ”‚ โ”œโ”€โ”€ modal/ # Image detail modal (shareable) +โ”‚ โ”œโ”€โ”€ breedModal/ # Breed detail modal with image grid +โ”‚ โ”œโ”€โ”€ navigation/ # Top navigation bar +โ”‚ โ””โ”€โ”€ ... +โ”œโ”€โ”€ views/ # Page-level components +โ”‚ โ”œโ”€โ”€ gallery/ # Random cat images view +โ”‚ โ”œโ”€โ”€ breeds/ # Breed catalog view +โ”‚ โ”œโ”€โ”€ favourites/ # User favorites view +โ”‚ โ””โ”€โ”€ analytics/ # Analytics dashboard +โ”œโ”€โ”€ store/ # Redux state management +โ”‚ โ”œโ”€โ”€ gallerySlice.ts +โ”‚ โ”œโ”€โ”€ breedsSlice.ts +โ”‚ โ”œโ”€โ”€ favouritesSlice.ts +โ”‚ โ””โ”€โ”€ analyticsSlice.ts +โ”œโ”€โ”€ hooks/ # Custom React hooks +โ”‚ โ”œโ”€โ”€ useBreedsPersistence.ts +โ”‚ โ”œโ”€โ”€ useFavouritesPersistence.ts +โ”‚ โ””โ”€โ”€ useAnalyticsPersistence.ts +โ””โ”€โ”€ types.ts # TypeScript type definitions ``` + +## Key Features in Detail + +### 1. Shareable Modal URLs + +Each cat image has a unique URL that can be shared: + +``` +/cat/j6oFGLpRG โ†’ Opens modal in gallery view +/breeds/cat/j6oFGLpRG โ†’ Opens modal in breeds view +/favourites/cat/j6oFGLpRG โ†’ Opens modal in favourites view +``` + +Uses React Router nested routes for clean URL structure. + +### 2. Persistent Favorites + +Favorites are saved to `localStorage` and restored on page load: + +- Survives browser refresh +- Works offline +- Synced with Redux state + +### 3. Analytics Tracking + +Automatically tracks: + +- Number of cats loaded in gallery +- Which breeds are viewed most often +- Favorite/view ratios + +### 4. Optimistic UI Updates + +Favorite button responds instantly without waiting for state updates, providing a smooth user experience. + +## Development + +```bash +# Run tests +npm run test + +# Lint code +npm run lint + +# Build for production +npm run build + +# Preview production build +npm run preview +``` + +## Testing + +Unit tests cover: + +- Redux slice reducers (gallery, breeds, favourites, analytics) +- Utility functions (mostly edge cases and error-prone utility functions) + +```bash +# Run all tests +npm run test + +``` + +## Deployment + +This project is configured for easy deployment on Vercel: + +1. Push to GitHub +2. Import project in Vercel +3. Add `VITE_CAT_API_KEY` environment variable +4. Deploy + +## Browser Support + +- Chrome/Edge 90+ +- Firefox 88+ +- Safari 15+ + +## API Usage + +This application uses [TheCatAPI](https://thecatapi.com/): + +- **Random Images**: `/v1/images/search` +- **Breeds List**: `/v1/breeds` +- **Image by ID**: `/v1/images/{id}` +- **Breed Images**: `/v1/images/search?breed_ids={id}` + +## Project Decisions + +### Why Redux Toolkit? + +- Centralized state management for complex interactions +- Easy integration with localStorage persistence +- Excellent TypeScript support +- Built-in performance optimizations + +### Why CSS Modules? + +- Scoped styles prevent naming conflicts +- Better tree-shaking than global CSS +- Type-safe with TypeScript + +### Why 24-Hour Cache? + +- Breed data is static and rarely changes +- Reduces API load and costs +- Improves application responsiveness +- Balances freshness with performance + +### Why Recharts? + +- React-native charting library +- Responsive out of the box +- Simple API for common chart types +- Good TypeScript support + +## License + +This project is licensed under the MIT License. + +## Acknowledgments + +- [TheCatAPI](https://thecatapi.com/) for providing the cat data +- Challenge provided by GlobalWebIndex Engineering diff --git a/__tests__/unit/store/analyticsSlice.test.ts b/__tests__/unit/store/analyticsSlice.test.ts index 862d8548..54c9e1b1 100644 --- a/__tests__/unit/store/analyticsSlice.test.ts +++ b/__tests__/unit/store/analyticsSlice.test.ts @@ -1,7 +1,8 @@ -import { describe, it, expect } from 'vitest'; +import { describe, expect,it } from 'vitest'; import analyticsReducer, { analyticsActions, } from '../../../src/store/analyticsSlice'; + import type { AnalyticsState } from '../../../src/store/types'; const { diff --git a/__tests__/unit/store/breedsSlice.test.ts b/__tests__/unit/store/breedsSlice.test.ts index a85237f5..044b8514 100644 --- a/__tests__/unit/store/breedsSlice.test.ts +++ b/__tests__/unit/store/breedsSlice.test.ts @@ -1,8 +1,9 @@ -import { describe, it, expect } from 'vitest'; +import { describe, expect,it } from 'vitest'; import breedsReducer, { breedsActions, type BreedsStateWithMeta, } from '../../../src/store/breedsSlice'; + import type { Breed } from '../../../src/types'; const { addBreeds, clearBreeds, loadBreeds } = breedsActions; diff --git a/__tests__/unit/store/favouritesSlice.test.ts b/__tests__/unit/store/favouritesSlice.test.ts index 8551d550..d60220bf 100644 --- a/__tests__/unit/store/favouritesSlice.test.ts +++ b/__tests__/unit/store/favouritesSlice.test.ts @@ -1,7 +1,8 @@ -import { describe, it, expect } from 'vitest'; +import { describe, expect,it } from 'vitest'; import favouritesReducer, { favouritesActions, } from '../../../src/store/favouritesSlice'; + import type { FavouritesState } from '../../../src/store/types'; import type { Cat } from '../../../src/types'; diff --git a/__tests__/unit/utils/breeds.test.ts b/__tests__/unit/utils/breeds.test.ts index cf5e0113..79366c1a 100644 --- a/__tests__/unit/utils/breeds.test.ts +++ b/__tests__/unit/utils/breeds.test.ts @@ -1,4 +1,5 @@ -import { describe, it, expect } from 'vitest'; +import { describe, expect,it } from 'vitest'; + import U from '../../../src/views/breeds/utils'; describe('breeds utils fetch', () => { diff --git a/__tests__/unit/utils/graphs.test.ts b/__tests__/unit/utils/graphs.test.ts index 6f39bdd7..0f723c7b 100644 --- a/__tests__/unit/utils/graphs.test.ts +++ b/__tests__/unit/utils/graphs.test.ts @@ -1,4 +1,5 @@ -import { describe, it, expect } from 'vitest'; +import { describe, expect,it } from 'vitest'; + import U from '../../../src/views/analytics/utils'; const { formatStats, formatBarStats, formatPieChartData } = U; diff --git a/src/components/favouriteButton/utils.ts b/src/components/favouriteButton/utils.ts index 9545e3f1..8f5d9813 100644 --- a/src/components/favouriteButton/utils.ts +++ b/src/components/favouriteButton/utils.ts @@ -6,7 +6,7 @@ const getFillColor = (isCompact: boolean, isFavourite: boolean): string => { return isCompact ? C.HEART_FILL_COMPACT : C.HEART_FILL_EXPANDED; }; -const getStrokeColor = (isCompact: Boolean): string => { +const getStrokeColor = (isCompact: boolean): string => { return isCompact ? C.HEART_STROKE_COMPACT : C.HEART_STROKE_EXPANDED; }; diff --git a/src/hooks/useAnalyticsPersistence.ts b/src/hooks/useAnalyticsPersistence.ts index 87d74e7d..a613ea9f 100644 --- a/src/hooks/useAnalyticsPersistence.ts +++ b/src/hooks/useAnalyticsPersistence.ts @@ -1,12 +1,12 @@ import { useEffect } from 'react'; import { analyticsActions } from '../store/analyticsSlice'; - import { selectAnalytics, useCatsDispatch, useCatsSelector, } from '../store/hooks'; + import C from './constants'; const useAnalyticsPersistence = () => { diff --git a/src/hooks/useBreedsPersistence.ts b/src/hooks/useBreedsPersistence.ts index 77175017..d3ab1dbe 100644 --- a/src/hooks/useBreedsPersistence.ts +++ b/src/hooks/useBreedsPersistence.ts @@ -7,6 +7,7 @@ import { useCatsDispatch, useCatsSelector, } from '../store/hooks'; + import C from './constants'; const useBreedsPersistence = () => { diff --git a/src/hooks/useFavouritesPersistence.ts b/src/hooks/useFavouritesPersistence.ts index 70d1387a..5802729b 100644 --- a/src/hooks/useFavouritesPersistence.ts +++ b/src/hooks/useFavouritesPersistence.ts @@ -6,6 +6,7 @@ import { useCatsDispatch, useCatsSelector, } from '../store/hooks'; + import C from './constants'; const useFavouritesPersistence = () => { diff --git a/src/router.tsx b/src/router.tsx index b6dfbbcf..2c9a2345 100644 --- a/src/router.tsx +++ b/src/router.tsx @@ -1,7 +1,7 @@ import { Navigate, Route, Routes } from 'react-router-dom'; import { BreedModal, Modal } from './components'; -import { Breeds, Analytics, Favourites, Gallery } from './views'; +import { Analytics, Breeds, Favourites, Gallery } from './views'; const AppRouter = () => { return ( diff --git a/src/views/analytics/components/BreedDistributionChart.tsx b/src/views/analytics/components/BreedDistributionChart.tsx index 75518771..13b290c7 100644 --- a/src/views/analytics/components/BreedDistributionChart.tsx +++ b/src/views/analytics/components/BreedDistributionChart.tsx @@ -6,11 +6,11 @@ import { ResponsiveContainer, Tooltip, } from 'recharts'; +import useStats from './useStats'; + import U from '../utils'; import C from '../constants'; - import styles from '../Analytics.module.css'; -import useStats from './useStats'; const BreedDistributionChart = () => { const { pieChartData } = useStats(); diff --git a/src/views/analytics/components/StatCards.tsx b/src/views/analytics/components/StatCards.tsx index 82a43d78..16543d30 100644 --- a/src/views/analytics/components/StatCards.tsx +++ b/src/views/analytics/components/StatCards.tsx @@ -1,6 +1,7 @@ -import styles from '../Analytics.module.css'; import useStats from './useStats'; +import styles from '../Analytics.module.css'; + const StatCards = () => { const { stats } = useStats(); diff --git a/src/views/analytics/components/ViewsVsFavoritesChart.tsx b/src/views/analytics/components/ViewsVsFavoritesChart.tsx index 9a88fab3..78da605a 100644 --- a/src/views/analytics/components/ViewsVsFavoritesChart.tsx +++ b/src/views/analytics/components/ViewsVsFavoritesChart.tsx @@ -8,10 +8,10 @@ import { XAxis, YAxis, } from 'recharts'; - -import styles from '../Analytics.module.css'; import useStats from './useStats'; + import C from '../constants'; +import styles from '../Analytics.module.css'; const ViewsVsFavoritesChart = () => { const { barStats } = useStats(); diff --git a/src/views/analytics/components/useStats.ts b/src/views/analytics/components/useStats.ts index 822e9caf..4601eb12 100644 --- a/src/views/analytics/components/useStats.ts +++ b/src/views/analytics/components/useStats.ts @@ -1,4 +1,5 @@ import { useMemo } from 'react'; + import { selectAnalytics, selectFavourites, @@ -34,7 +35,7 @@ const useStats = () => { const pieChartData = useMemo( () => U.formatPieChartData(breedsViewed), - [analytics] + [breedsViewed] ); return { barStats, stats, pieChartData }; diff --git a/src/views/breeds/utils.ts b/src/views/breeds/utils.ts index 2297dcf0..f13e245e 100644 --- a/src/views/breeds/utils.ts +++ b/src/views/breeds/utils.ts @@ -1,5 +1,5 @@ -import type { Breed, Cat } from '../../types'; import C from './constants'; +import type { Breed, Cat } from '../../types'; const transformBreedToCatObject = (breed: Breed): Cat => { return { diff --git a/src/views/index.ts b/src/views/index.ts index 28c4dce6..a09dc5bb 100644 --- a/src/views/index.ts +++ b/src/views/index.ts @@ -1,4 +1,4 @@ -export { default as Breeds } from './breeds'; export { default as Analytics } from './analytics'; +export { default as Breeds } from './breeds'; export { default as Favourites } from './favourites'; export { default as Gallery } from './gallery'; From 9ef3aab43ace8ffc8d7cb9984e1712efdc3de6ef Mon Sep 17 00:00:00 2001 From: mkour Date: Mon, 17 Nov 2025 13:10:18 +0200 Subject: [PATCH 16/17] update build and deployment configuration for Vercel - Added vercel.json for proper SPA routing - Ensured production build works for all client-side routes - Fixed deployment issues on Vercel --- src/views/breeds/Breeds.tsx | 2 +- src/views/favourites/Favourites.tsx | 2 +- src/views/gallery/GalleryButtons.tsx | 2 +- vercel.json | 8 ++++++++ 4 files changed, 11 insertions(+), 3 deletions(-) create mode 100644 vercel.json diff --git a/src/views/breeds/Breeds.tsx b/src/views/breeds/Breeds.tsx index 8fd16cf6..d351340d 100644 --- a/src/views/breeds/Breeds.tsx +++ b/src/views/breeds/Breeds.tsx @@ -16,7 +16,7 @@ const Breeds = () => { if (areBreedsEmpty) { loadBreeds(); } - }, []); + }, [areBreedsEmpty, loadBreeds]); if (isInitialLoading) { return ; diff --git a/src/views/favourites/Favourites.tsx b/src/views/favourites/Favourites.tsx index f0cbe3c3..cf7f511f 100644 --- a/src/views/favourites/Favourites.tsx +++ b/src/views/favourites/Favourites.tsx @@ -5,7 +5,7 @@ import Card from '../../components/card'; import C from './constants'; import layoutStyles from '../shared/viewsLayout.module.css'; -import styles from './favourites.module.css'; +import styles from './Favourites.module.css'; const Favourites = () => { const { favouriteCats, isFavouritesEmpty } = useFavourites(); diff --git a/src/views/gallery/GalleryButtons.tsx b/src/views/gallery/GalleryButtons.tsx index e7fd9a8f..e913b55b 100644 --- a/src/views/gallery/GalleryButtons.tsx +++ b/src/views/gallery/GalleryButtons.tsx @@ -1,5 +1,5 @@ import C from './constants'; -import styles from './gallery.module.css'; +import styles from './Gallery.module.css'; type GalleryButtonsProps = { isLoading: boolean; diff --git a/vercel.json b/vercel.json new file mode 100644 index 00000000..1323cdac --- /dev/null +++ b/vercel.json @@ -0,0 +1,8 @@ +{ + "rewrites": [ + { + "source": "/(.*)", + "destination": "/index.html" + } + ] +} From 7e9538461598bed066675b1839c4cd3d4306f07e Mon Sep 17 00:00:00 2001 From: mkourogiorgas Date: Mon, 17 Nov 2025 13:22:56 +0200 Subject: [PATCH 17/17] Update README with images and live demo link Updated the README to include images and live demo link. --- README.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 1d3eeaa9..4b6a1b46 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,16 @@ # ๐Ÿฑ Cat Lover - React Cat Gallery Application +

+ cat_lover_breed_modal + cat_lover_cat_modal + cat_lover_analytics +

+ + + **A React/TypeScript application for cat enthusiasts built with TheCatAPI** -> ๐ŸŽฎ **[Live Demo](#)** | ๐Ÿ”„ **Smart Caching** | ๐Ÿ“Š **Analytics Dashboard** | ๐Ÿ”— **Shareable URLs** +> ๐ŸŽฎ **[Live Demo](https://mkour-cat-lover.vercel.app/)** | ๐Ÿ”„ **Smart Caching** | ๐Ÿ“Š **Analytics Dashboard** | ๐Ÿ”— **Shareable URLs**