diff --git a/.eslintignore b/.eslintignore index 7d5b7a94f4d..c02605c9897 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,2 +1,10 @@ /build /node_modules +node_modules +dist +build +reports +cypress +cypress.config.ts +eslint.config.js +phone_catalog diff --git a/index.html b/index.html index 095fb3a4537..efc00422262 100644 --- a/index.html +++ b/index.html @@ -2,11 +2,16 @@ + + + + - Vite + React + TS + Nice Gadgets
+ diff --git a/package-lock.json b/package-lock.json index 836b9e63b46..0ff0e0cefa7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,16 +11,18 @@ "license": "GPL-3.0", "dependencies": { "@fortawesome/fontawesome-free": "^6.5.2", + "@reduxjs/toolkit": "^2.11.2", "bulma": "^1.0.1", "classnames": "^2.5.1", "react": "^18.3.1", "react-dom": "^18.3.1", + "react-redux": "^9.2.0", "react-router-dom": "^6.25.1", "react-transition-group": "^4.4.5" }, "devDependencies": { "@cypress/react18": "^2.0.1", - "@mate-academy/scripts": "^1.8.5", + "@mate-academy/scripts": "^2.1.3", "@mate-academy/students-ts-config": "*", "@mate-academy/stylelint-config": "*", "@types/node": "^20.14.10", @@ -1184,10 +1186,11 @@ } }, "node_modules/@mate-academy/scripts": { - "version": "1.8.5", - "resolved": "https://registry.npmjs.org/@mate-academy/scripts/-/scripts-1.8.5.tgz", - "integrity": "sha512-mHRY2FkuoYCf5U0ahIukkaRo5LSZsxrTSgMJheFoyf3VXsTvfM9OfWcZIDIDB521kdPrScHHnRp+JRNjCfUO5A==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@mate-academy/scripts/-/scripts-2.1.3.tgz", + "integrity": "sha512-a07wHTj/1QUK2Aac5zHad+sGw4rIvcNl5lJmJpAD7OxeSbnCdyI6RXUHwXhjF5MaVo9YHrJ0xVahyERS2IIyBQ==", "dev": true, + "license": "MIT", "dependencies": { "@octokit/rest": "^17.11.2", "@types/get-port": "^4.2.0", @@ -1875,6 +1878,32 @@ "url": "https://opencollective.com/unts" } }, + "node_modules/@reduxjs/toolkit": { + "version": "2.11.2", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.11.2.tgz", + "integrity": "sha512-Kd6kAHTA6/nUpp8mySPqj3en3dm0tdMIgbttnQ1xFMVpufoj+ADi8pXLBsd4xzTRHQa7t/Jv8W5UnCuW4kuWMQ==", + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@standard-schema/utils": "^0.3.0", + "immer": "^11.0.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/@remix-run/router": { "version": "1.18.0", "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.18.0.tgz", @@ -2126,6 +2155,18 @@ "integrity": "sha512-sXXKG+uL9IrKqViTtao2Ws6dy0znu9sOaP1di/jKGW1M6VssO8vlpXCQcpZ+jisQ1tTFAC5Jo/EOzFbggBagFQ==", "dev": true }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "license": "MIT" + }, + "node_modules/@standard-schema/utils": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz", + "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", + "license": "MIT" + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -2216,13 +2257,13 @@ "version": "15.7.12", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.12.tgz", "integrity": "sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==", - "dev": true + "devOptional": true }, "node_modules/@types/react": { "version": "18.3.3", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.3.tgz", "integrity": "sha512-hti/R0pS0q1/xx+TsI73XIqk26eBsISZ2R0wUijXIngRK9R/e7Xw/cXVxQK7R5JjW+SV4zGcn5hXjudkN/pLIw==", - "dev": true, + "devOptional": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.0.2" @@ -2258,6 +2299,12 @@ "integrity": "sha512-0vWLNK2D5MT9dg0iOo8GlKguPAU02QjmZitPEsXRuJXU/OGIOt9vT9Fc26wtYuavLxtO45v9PGleoL9Z0k1LHg==", "dev": true }, + "node_modules/@types/use-sync-external-store": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", + "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", + "license": "MIT" + }, "node_modules/@types/yauzl": { "version": "2.10.3", "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", @@ -5936,6 +5983,16 @@ "node": ">= 4" } }, + "node_modules/immer": { + "version": "11.1.4", + "resolved": "https://registry.npmjs.org/immer/-/immer-11.1.4.tgz", + "integrity": "sha512-XREFCPo6ksxVzP4E0ekD5aMdf8WMwmdNaz6vuvxgI40UaEiu6q3p8X52aU6GdyvLY3XXX/8R7JOTXStz/nBbRw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, "node_modules/immutable": { "version": "4.3.6", "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.6.tgz", @@ -8734,6 +8791,29 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, + "node_modules/react-redux": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", + "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", + "license": "MIT", + "dependencies": { + "@types/use-sync-external-store": "^0.0.6", + "use-sync-external-store": "^1.4.0" + }, + "peerDependencies": { + "@types/react": "^18.2.25 || ^19", + "react": "^18.0 || ^19", + "redux": "^5.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "redux": { + "optional": true + } + } + }, "node_modules/react-refresh": { "version": "0.14.2", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz", @@ -8893,6 +8973,21 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/redux": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", + "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", + "license": "MIT" + }, + "node_modules/redux-thunk": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz", + "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==", + "license": "MIT", + "peerDependencies": { + "redux": "^5.0.0" + } + }, "node_modules/reflect.getprototypeof": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.6.tgz", @@ -8976,6 +9071,12 @@ "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", "dev": true }, + "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": { "version": "1.22.8", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", @@ -10438,6 +10539,15 @@ "requires-port": "^1.0.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/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", diff --git a/package.json b/package.json index ae251685c8b..fc2e1a8b375 100644 --- a/package.json +++ b/package.json @@ -7,16 +7,18 @@ "license": "GPL-3.0", "dependencies": { "@fortawesome/fontawesome-free": "^6.5.2", + "@reduxjs/toolkit": "^2.11.2", "bulma": "^1.0.1", "classnames": "^2.5.1", "react": "^18.3.1", "react-dom": "^18.3.1", + "react-redux": "^9.2.0", "react-router-dom": "^6.25.1", "react-transition-group": "^4.4.5" }, "devDependencies": { "@cypress/react18": "^2.0.1", - "@mate-academy/scripts": "^1.8.5", + "@mate-academy/scripts": "^2.1.3", "@mate-academy/students-ts-config": "*", "@mate-academy/stylelint-config": "*", "@types/node": "^20.14.10", diff --git a/phone_catalog/.gitignore b/phone_catalog/.gitignore new file mode 100644 index 00000000000..a547bf36d8d --- /dev/null +++ b/phone_catalog/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/phone_catalog/README.md b/phone_catalog/README.md new file mode 100644 index 00000000000..d2e77611fd3 --- /dev/null +++ b/phone_catalog/README.md @@ -0,0 +1,73 @@ +# React + TypeScript + Vite + +This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. + +Currently, two official plugins are available: + +- [@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 + +If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules: + +```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/phone_catalog/eslint.config.js b/phone_catalog/eslint.config.js new file mode 100644 index 00000000000..5e6b472f583 --- /dev/null +++ b/phone_catalog/eslint.config.js @@ -0,0 +1,23 @@ +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' + +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, + }, + }, +]) diff --git a/phone_catalog/index.html b/phone_catalog/index.html new file mode 100644 index 00000000000..bb9e9804c57 --- /dev/null +++ b/phone_catalog/index.html @@ -0,0 +1,13 @@ + + + + + + + phone_catalog + + +
+ + + diff --git a/phone_catalog/package-lock.json b/phone_catalog/package-lock.json new file mode 100644 index 00000000000..d66d3c2d79c --- /dev/null +++ b/phone_catalog/package-lock.json @@ -0,0 +1,3224 @@ +{ + "name": "phone_catalog", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "phone_catalog", + "version": "0.0.0", + "dependencies": { + "react": "^19.2.0", + "react-dom": "^19.2.0" + }, + "devDependencies": { + "@eslint/js": "^9.39.1", + "@types/node": "^24.10.1", + "@types/react": "^19.2.5", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^5.1.1", + "eslint": "^9.39.1", + "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-react-refresh": "^0.4.24", + "globals": "^16.5.0", + "typescript": "~5.9.3", + "typescript-eslint": "^8.46.4", + "vite": "^7.2.4" + } + }, + "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/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/@esbuild/aix-ppc64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz", + "integrity": "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.2.tgz", + "integrity": "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.2.tgz", + "integrity": "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.2.tgz", + "integrity": "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz", + "integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.2.tgz", + "integrity": "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.2.tgz", + "integrity": "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.2.tgz", + "integrity": "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.2.tgz", + "integrity": "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz", + "integrity": "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.2.tgz", + "integrity": "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.2.tgz", + "integrity": "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.2.tgz", + "integrity": "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz", + "integrity": "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz", + "integrity": "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz", + "integrity": "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.2.tgz", + "integrity": "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.2.tgz", + "integrity": "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.2.tgz", + "integrity": "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.2.tgz", + "integrity": "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.2.tgz", + "integrity": "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.2.tgz", + "integrity": "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz", + "integrity": "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.2.tgz", + "integrity": "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.2.tgz", + "integrity": "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz", + "integrity": "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==", + "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.3", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.3.tgz", + "integrity": "sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==", + "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.1", + "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.2", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.2.tgz", + "integrity": "sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==", + "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/@rolldown/pluginutils": { + "version": "1.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.53.tgz", + "integrity": "sha512-vENRlFU4YbrwVqNDZ7fLvy+JR1CRkyr01jhSiDpE1u6py3OMzQfztQU2jxykW3ALNxO4kSlqIDeYyD0Y9RcQeQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.54.0.tgz", + "integrity": "sha512-OywsdRHrFvCdvsewAInDKCNyR3laPA2mc9bRYJ6LBp5IyvF3fvXbbNR0bSzHlZVFtn6E0xw2oZlyjg4rKCVcng==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.54.0.tgz", + "integrity": "sha512-Skx39Uv+u7H224Af+bDgNinitlmHyQX1K/atIA32JP3JQw6hVODX5tkbi2zof/E69M1qH2UoN3Xdxgs90mmNYw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.54.0.tgz", + "integrity": "sha512-k43D4qta/+6Fq+nCDhhv9yP2HdeKeP56QrUUTW7E6PhZP1US6NDqpJj4MY0jBHlJivVJD5P8NxrjuobZBJTCRw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.54.0.tgz", + "integrity": "sha512-cOo7biqwkpawslEfox5Vs8/qj83M/aZCSSNIWpVzfU2CYHa2G3P1UN5WF01RdTHSgCkri7XOlTdtk17BezlV3A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.54.0.tgz", + "integrity": "sha512-miSvuFkmvFbgJ1BevMa4CPCFt5MPGw094knM64W9I0giUIMMmRYcGW/JWZDriaw/k1kOBtsWh1z6nIFV1vPNtA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.54.0.tgz", + "integrity": "sha512-KGXIs55+b/ZfZsq9aR026tmr/+7tq6VG6MsnrvF4H8VhwflTIuYh+LFUlIsRdQSgrgmtM3fVATzEAj4hBQlaqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.54.0.tgz", + "integrity": "sha512-EHMUcDwhtdRGlXZsGSIuXSYwD5kOT9NVnx9sqzYiwAc91wfYOE1g1djOEDseZJKKqtHAHGwnGPQu3kytmfaXLQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.54.0.tgz", + "integrity": "sha512-+pBrqEjaakN2ySv5RVrj/qLytYhPKEUwk+e3SFU5jTLHIcAtqh2rLrd/OkbNuHJpsBgxsD8ccJt5ga/SeG0JmA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.54.0.tgz", + "integrity": "sha512-NSqc7rE9wuUaRBsBp5ckQ5CVz5aIRKCwsoa6WMF7G01sX3/qHUw/z4pv+D+ahL1EIKy6Enpcnz1RY8pf7bjwng==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.54.0.tgz", + "integrity": "sha512-gr5vDbg3Bakga5kbdpqx81m2n9IX8M6gIMlQQIXiLTNeQW6CucvuInJ91EuCJ/JYvc+rcLLsDFcfAD1K7fMofg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.54.0.tgz", + "integrity": "sha512-gsrtB1NA3ZYj2vq0Rzkylo9ylCtW/PhpLEivlgWe0bpgtX5+9j9EZa0wtZiCjgu6zmSeZWyI/e2YRX1URozpIw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.54.0.tgz", + "integrity": "sha512-y3qNOfTBStmFNq+t4s7Tmc9hW2ENtPg8FeUD/VShI7rKxNW7O4fFeaYbMsd3tpFlIg1Q8IapFgy7Q9i2BqeBvA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.54.0.tgz", + "integrity": "sha512-89sepv7h2lIVPsFma8iwmccN7Yjjtgz0Rj/Ou6fEqg3HDhpCa+Et+YSufy27i6b0Wav69Qv4WBNl3Rs6pwhebQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.54.0.tgz", + "integrity": "sha512-ZcU77ieh0M2Q8Ur7D5X7KvK+UxbXeDHwiOt/CPSBTI1fBmeDMivW0dPkdqkT4rOgDjrDDBUed9x4EgraIKoR2A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.54.0.tgz", + "integrity": "sha512-2AdWy5RdDF5+4YfG/YesGDDtbyJlC9LHmL6rZw6FurBJ5n4vFGupsOBGfwMRjBYH7qRQowT8D/U4LoSvVwOhSQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.54.0.tgz", + "integrity": "sha512-WGt5J8Ij/rvyqpFexxk3ffKqqbLf9AqrTBbWDk7ApGUzaIs6V+s2s84kAxklFwmMF/vBNGrVdYgbblCOFFezMQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.54.0.tgz", + "integrity": "sha512-JzQmb38ATzHjxlPHuTH6tE7ojnMKM2kYNzt44LO/jJi8BpceEC8QuXYA908n8r3CNuG/B3BV8VR3Hi1rYtmPiw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.54.0.tgz", + "integrity": "sha512-huT3fd0iC7jigGh7n3q/+lfPcXxBi+om/Rs3yiFxjvSxbSB6aohDFXbWvlspaqjeOh+hx7DDHS+5Es5qRkWkZg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.54.0.tgz", + "integrity": "sha512-c2V0W1bsKIKfbLMBu/WGBz6Yci8nJ/ZJdheE0EwB73N3MvHYKiKGs3mVilX4Gs70eGeDaMqEob25Tw2Gb9Nqyw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.54.0.tgz", + "integrity": "sha512-woEHgqQqDCkAzrDhvDipnSirm5vxUXtSKDYTVpZG3nUdW/VVB5VdCYA2iReSj/u3yCZzXID4kuKG7OynPnB3WQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.54.0.tgz", + "integrity": "sha512-dzAc53LOuFvHwbCEOS0rPbXp6SIhAf2txMP5p6mGyOXXw5mWY8NGGbPMPrs4P1WItkfApDathBj/NzMLUZ9rtQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.54.0.tgz", + "integrity": "sha512-hYT5d3YNdSh3mbCU1gwQyPgQd3T2ne0A3KG8KSBdav5TiBg6eInVmV+TeR5uHufiIgSFg0XsOWGW5/RhNcSvPg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "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/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.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.4.tgz", + "integrity": "sha512-vnDVpYPMzs4wunl27jHrfmwojOGKya0xyM3sH+UE5iv5uPS6vX7UIoh6m+vQc5LGBq52HBKPIn/zcSZVzeDEZg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@types/react": { + "version": "19.2.7", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.7.tgz", + "integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "csstype": "^3.2.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", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.50.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.50.1.tgz", + "integrity": "sha512-PKhLGDq3JAg0Jk/aK890knnqduuI/Qj+udH7wCf0217IGi4gt+acgCyPVe79qoT+qKUvHMDQkwJeKW9fwl8Cyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "8.50.1", + "@typescript-eslint/type-utils": "8.50.1", + "@typescript-eslint/utils": "8.50.1", + "@typescript-eslint/visitor-keys": "8.50.1", + "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.50.1", + "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.50.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.50.1.tgz", + "integrity": "sha512-hM5faZwg7aVNa819m/5r7D0h0c9yC4DUlWAOvHAtISdFTc8xB86VmX5Xqabrama3wIPJ/q9RbGS1worb6JfnMg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@typescript-eslint/scope-manager": "8.50.1", + "@typescript-eslint/types": "8.50.1", + "@typescript-eslint/typescript-estree": "8.50.1", + "@typescript-eslint/visitor-keys": "8.50.1", + "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.50.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.50.1.tgz", + "integrity": "sha512-E1ur1MCVf+YiP89+o4Les/oBAVzmSbeRB0MQLfSlYtbWU17HPxZ6Bhs5iYmKZRALvEuBoXIZMOIRRc/P++Ortg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.50.1", + "@typescript-eslint/types": "^8.50.1", + "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.50.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.50.1.tgz", + "integrity": "sha512-mfRx06Myt3T4vuoHaKi8ZWNTPdzKPNBhiblze5N50//TSHOAQQevl/aolqA/BcqqbJ88GUnLqjjcBc8EWdBcVw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.50.1", + "@typescript-eslint/visitor-keys": "8.50.1" + }, + "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.50.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.50.1.tgz", + "integrity": "sha512-ooHmotT/lCWLXi55G4mvaUF60aJa012QzvLK0Y+Mp4WdSt17QhMhWOaBWeGTFVkb2gDgBe19Cxy1elPXylslDw==", + "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.50.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.50.1.tgz", + "integrity": "sha512-7J3bf022QZE42tYMO6SL+6lTPKFk/WphhRPe9Tw/el+cEwzLz1Jjz2PX3GtGQVxooLDKeMVmMt7fWpYRdG5Etg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.50.1", + "@typescript-eslint/typescript-estree": "8.50.1", + "@typescript-eslint/utils": "8.50.1", + "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.50.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.50.1.tgz", + "integrity": "sha512-v5lFIS2feTkNyMhd7AucE/9j/4V9v5iIbpVRncjk/K0sQ6Sb+Np9fgYS/63n6nwqahHQvbmujeBL7mp07Q9mlA==", + "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.50.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.50.1.tgz", + "integrity": "sha512-woHPdW+0gj53aM+cxchymJCrh0cyS7BTIdcDxWUNsclr9VDkOSbqC13juHzxOmQ22dDkMZEpZB+3X1WpUvzgVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.50.1", + "@typescript-eslint/tsconfig-utils": "8.50.1", + "@typescript-eslint/types": "8.50.1", + "@typescript-eslint/visitor-keys": "8.50.1", + "debug": "^4.3.4", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "tinyglobby": "^0.2.15", + "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.50.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.50.1.tgz", + "integrity": "sha512-lCLp8H1T9T7gPbEuJSnHwnSuO9mDf8mfK/Nion5mZmiEaQD9sWf9W4dfeFqRyqRjF06/kBuTmAqcs9sewM2NbQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.7.0", + "@typescript-eslint/scope-manager": "8.50.1", + "@typescript-eslint/types": "8.50.1", + "@typescript-eslint/typescript-estree": "8.50.1" + }, + "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.50.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.50.1.tgz", + "integrity": "sha512-IrDKrw7pCRUR94zeuCSUWQ+w8JEf5ZX5jl/e6AHGSLi1/zIr0lgutfn/7JpfCey+urpgQEdrZVYzCaVVKiTwhQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.50.1", + "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.2", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.1.2.tgz", + "integrity": "sha512-EcA07pHJouywpzsoTUqNh5NwGayl2PPVEJKUSinGGSxFGYn+shYbqMGBg6FXDqgXum9Ou/ecb+411ssw8HImJQ==", + "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.53", + "@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/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/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-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/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.9.11", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.11.tgz", + "integrity": "sha512-Sg0xJUNDU1sJNGdfGWhVHX0kkZ+HWcvmVymJbj6NSgZZmW/8S9Y2HQ5euytnIgakgxN6papOAWiwDo1ctFDcoQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, + "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/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "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.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "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.30001761", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001761.tgz", + "integrity": "sha512-JF9ptu1vP2coz98+5051jZ4PwQgd2ni8A+gYSN7EA7dPKIMf0pDlSUxhdmVOaV3/fYK5uWBkgSXJaRLr4+3A6g==", + "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/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/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/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/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "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/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/electron-to-chromium": { + "version": "1.5.267", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz", + "integrity": "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==", + "dev": true, + "license": "ISC" + }, + "node_modules/esbuild": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz", + "integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.2", + "@esbuild/android-arm": "0.27.2", + "@esbuild/android-arm64": "0.27.2", + "@esbuild/android-x64": "0.27.2", + "@esbuild/darwin-arm64": "0.27.2", + "@esbuild/darwin-x64": "0.27.2", + "@esbuild/freebsd-arm64": "0.27.2", + "@esbuild/freebsd-x64": "0.27.2", + "@esbuild/linux-arm": "0.27.2", + "@esbuild/linux-arm64": "0.27.2", + "@esbuild/linux-ia32": "0.27.2", + "@esbuild/linux-loong64": "0.27.2", + "@esbuild/linux-mips64el": "0.27.2", + "@esbuild/linux-ppc64": "0.27.2", + "@esbuild/linux-riscv64": "0.27.2", + "@esbuild/linux-s390x": "0.27.2", + "@esbuild/linux-x64": "0.27.2", + "@esbuild/netbsd-arm64": "0.27.2", + "@esbuild/netbsd-x64": "0.27.2", + "@esbuild/openbsd-arm64": "0.27.2", + "@esbuild/openbsd-x64": "0.27.2", + "@esbuild/openharmony-arm64": "0.27.2", + "@esbuild/sunos-x64": "0.27.2", + "@esbuild/win32-arm64": "0.27.2", + "@esbuild/win32-ia32": "0.27.2", + "@esbuild/win32-x64": "0.27.2" + } + }, + "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.2", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.2.tgz", + "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", + "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.2", + "@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-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.26", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.26.tgz", + "integrity": "sha512-1RETEylht2O6FM/MvgnyvT+8K21wLqDNg4qD51Zj3guhjt433XbnnkVttHMyaVyAFD03QSV4LPS5iE3VQmO7XQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": ">=8.40" + } + }, + "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/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/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-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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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": "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/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/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/react": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz", + "integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.3" + } + }, + "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/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/rollup": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.54.0.tgz", + "integrity": "sha512-3nk8Y3a9Ea8szgKhinMlGMhGMw89mqule3KWczxhIzqudyHdCIOHw8WJlj/r329fACjKLEh13ZSk7oE22kyeIw==", + "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.54.0", + "@rollup/rollup-android-arm64": "4.54.0", + "@rollup/rollup-darwin-arm64": "4.54.0", + "@rollup/rollup-darwin-x64": "4.54.0", + "@rollup/rollup-freebsd-arm64": "4.54.0", + "@rollup/rollup-freebsd-x64": "4.54.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.54.0", + "@rollup/rollup-linux-arm-musleabihf": "4.54.0", + "@rollup/rollup-linux-arm64-gnu": "4.54.0", + "@rollup/rollup-linux-arm64-musl": "4.54.0", + "@rollup/rollup-linux-loong64-gnu": "4.54.0", + "@rollup/rollup-linux-ppc64-gnu": "4.54.0", + "@rollup/rollup-linux-riscv64-gnu": "4.54.0", + "@rollup/rollup-linux-riscv64-musl": "4.54.0", + "@rollup/rollup-linux-s390x-gnu": "4.54.0", + "@rollup/rollup-linux-x64-gnu": "4.54.0", + "@rollup/rollup-linux-x64-musl": "4.54.0", + "@rollup/rollup-openharmony-arm64": "4.54.0", + "@rollup/rollup-win32-arm64-msvc": "4.54.0", + "@rollup/rollup-win32-ia32-msvc": "4.54.0", + "@rollup/rollup-win32-x64-gnu": "4.54.0", + "@rollup/rollup-win32-x64-msvc": "4.54.0", + "fsevents": "~2.3.2" + } + }, + "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/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/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/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/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/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.50.1", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.50.1.tgz", + "integrity": "sha512-ytTHO+SoYSbhAH9CrYnMhiLx8To6PSSvqnvXyPUgPETCvB6eBKmTI9w6XMPS3HsBRGkwTVBX+urA8dYQx6bHfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.50.1", + "@typescript-eslint/parser": "8.50.1", + "@typescript-eslint/typescript-estree": "8.50.1", + "@typescript-eslint/utils": "8.50.1" + }, + "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.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "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/vite": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.0.tgz", + "integrity": "sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "esbuild": "^0.27.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/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/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/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.2.1", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.2.1.tgz", + "integrity": "sha512-0wZ1IRqGGhMP76gLqz8EyfBXKk0J2qo2+H3fi4mcUP/KtTocoX08nmIAHl1Z2kJIZbZee8KOpBCSNPRgauucjw==", + "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/phone_catalog/package.json b/phone_catalog/package.json new file mode 100644 index 00000000000..d75b1daafb6 --- /dev/null +++ b/phone_catalog/package.json @@ -0,0 +1,30 @@ +{ + "name": "phone_catalog", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "lint": "eslint .", + "preview": "vite preview" + }, + "dependencies": { + "react": "^19.2.0", + "react-dom": "^19.2.0" + }, + "devDependencies": { + "@eslint/js": "^9.39.1", + "@types/node": "^24.10.1", + "@types/react": "^19.2.5", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^5.1.1", + "eslint": "^9.39.1", + "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-react-refresh": "^0.4.24", + "globals": "^16.5.0", + "typescript": "~5.9.3", + "typescript-eslint": "^8.46.4", + "vite": "^7.2.4" + } +} diff --git a/phone_catalog/public/vite.svg b/phone_catalog/public/vite.svg new file mode 100644 index 00000000000..e7b8dfb1b2a --- /dev/null +++ b/phone_catalog/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/phone_catalog/src/App.css b/phone_catalog/src/App.css new file mode 100644 index 00000000000..b9d355df2a5 --- /dev/null +++ b/phone_catalog/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/phone_catalog/src/App.tsx b/phone_catalog/src/App.tsx new file mode 100644 index 00000000000..3d7ded3ff62 --- /dev/null +++ b/phone_catalog/src/App.tsx @@ -0,0 +1,35 @@ +import { useState } from 'react' +import reactLogo from './assets/react.svg' +import viteLogo from '/vite.svg' +import './App.css' + +function App() { + const [count, setCount] = useState(0) + + return ( + <> +
+ + Vite logo + + + React logo + +
+

Vite + React

+
+ +

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

+
+

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

+ + ) +} + +export default App diff --git a/phone_catalog/src/assets/react.svg b/phone_catalog/src/assets/react.svg new file mode 100644 index 00000000000..6c87de9bb33 --- /dev/null +++ b/phone_catalog/src/assets/react.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/phone_catalog/src/index.css b/phone_catalog/src/index.css new file mode 100644 index 00000000000..08a3ac9e1e5 --- /dev/null +++ b/phone_catalog/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/phone_catalog/src/main.tsx b/phone_catalog/src/main.tsx new file mode 100644 index 00000000000..bef5202a32c --- /dev/null +++ b/phone_catalog/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/phone_catalog/tsconfig.app.json b/phone_catalog/tsconfig.app.json new file mode 100644 index 00000000000..a9b5a59ca64 --- /dev/null +++ b/phone_catalog/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/phone_catalog/tsconfig.json b/phone_catalog/tsconfig.json new file mode 100644 index 00000000000..1ffef600d95 --- /dev/null +++ b/phone_catalog/tsconfig.json @@ -0,0 +1,7 @@ +{ + "files": [], + "references": [ + { "path": "./tsconfig.app.json" }, + { "path": "./tsconfig.node.json" } + ] +} diff --git a/phone_catalog/tsconfig.node.json b/phone_catalog/tsconfig.node.json new file mode 100644 index 00000000000..8a67f62f4ce --- /dev/null +++ b/phone_catalog/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/phone_catalog/vite.config.ts b/phone_catalog/vite.config.ts new file mode 100644 index 00000000000..8b0f57b91ae --- /dev/null +++ b/phone_catalog/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()], +}) diff --git a/public/img/Accessories.png b/public/img/Accessories.png new file mode 100644 index 00000000000..5612ec1b230 Binary files /dev/null and b/public/img/Accessories.png differ diff --git a/public/img/Arrow_Down_Grey.svg b/public/img/Arrow_Down_Grey.svg new file mode 100644 index 00000000000..2ab0f27592b --- /dev/null +++ b/public/img/Arrow_Down_Grey.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/img/Arrow_Left.svg b/public/img/Arrow_Left.svg new file mode 100644 index 00000000000..f811fb03a36 --- /dev/null +++ b/public/img/Arrow_Left.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/img/Arrow_Right.svg b/public/img/Arrow_Right.svg new file mode 100644 index 00000000000..b4f46876718 --- /dev/null +++ b/public/img/Arrow_Right.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/img/Arrow_Right_Grey.svg b/public/img/Arrow_Right_Grey.svg new file mode 100644 index 00000000000..a15457b9ad6 --- /dev/null +++ b/public/img/Arrow_Right_Grey.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/img/Arrow_Top.svg b/public/img/Arrow_Top.svg new file mode 100644 index 00000000000..f7a3faccf24 --- /dev/null +++ b/public/img/Arrow_Top.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/img/Banner.png b/public/img/Banner.png new file mode 100644 index 00000000000..247b5a20c10 Binary files /dev/null and b/public/img/Banner.png differ diff --git a/public/img/Banner.svg b/public/img/Banner.svg new file mode 100644 index 00000000000..934b1408ecf --- /dev/null +++ b/public/img/Banner.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/public/img/Carousel_Dot.svg b/public/img/Carousel_Dot.svg new file mode 100644 index 00000000000..9cef129ea3f --- /dev/null +++ b/public/img/Carousel_Dot.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/img/Carousel_Dot_Active.svg b/public/img/Carousel_Dot_Active.svg new file mode 100644 index 00000000000..c7692fbb194 --- /dev/null +++ b/public/img/Carousel_Dot_Active.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/img/Cart.svg b/public/img/Cart.svg new file mode 100644 index 00000000000..6deb3bf9b71 --- /dev/null +++ b/public/img/Cart.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/public/img/Close.svg b/public/img/Close.svg new file mode 100644 index 00000000000..61e7a408321 --- /dev/null +++ b/public/img/Close.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/img/Close_black.svg b/public/img/Close_black.svg new file mode 100644 index 00000000000..aadcc91fb1f --- /dev/null +++ b/public/img/Close_black.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/img/HeartFilled.svg b/public/img/HeartFilled.svg new file mode 100644 index 00000000000..7138d7522bf --- /dev/null +++ b/public/img/HeartFilled.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/img/Home.svg b/public/img/Home.svg new file mode 100644 index 00000000000..474476cb027 --- /dev/null +++ b/public/img/Home.svg @@ -0,0 +1,4 @@ + + + + diff --git a/public/img/Like.svg b/public/img/Like.svg new file mode 100644 index 00000000000..d29d2a36aab --- /dev/null +++ b/public/img/Like.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/img/Logo.svg b/public/img/Logo.svg new file mode 100644 index 00000000000..a55db4848f6 --- /dev/null +++ b/public/img/Logo.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/img/Menu.svg b/public/img/Menu.svg new file mode 100644 index 00000000000..2c535f4586b --- /dev/null +++ b/public/img/Menu.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/public/img/Minus.svg b/public/img/Minus.svg new file mode 100644 index 00000000000..762e04664ee --- /dev/null +++ b/public/img/Minus.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/img/Phones.png b/public/img/Phones.png new file mode 100644 index 00000000000..f70d88d553e Binary files /dev/null and b/public/img/Phones.png differ diff --git a/public/img/Plus.svg b/public/img/Plus.svg new file mode 100644 index 00000000000..338f8c2f87d --- /dev/null +++ b/public/img/Plus.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/img/Tablets.png b/public/img/Tablets.png new file mode 100644 index 00000000000..a51fccd98fd Binary files /dev/null and b/public/img/Tablets.png differ diff --git a/public/img/banner-mobile.svg b/public/img/banner-mobile.svg new file mode 100644 index 00000000000..9d893c123d4 --- /dev/null +++ b/public/img/banner-mobile.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/public/img/banner2.png b/public/img/banner2.png new file mode 100644 index 00000000000..fd01b432d5c Binary files /dev/null and b/public/img/banner2.png differ diff --git a/public/img/banner3.png b/public/img/banner3.png new file mode 100644 index 00000000000..cf8f73d2178 Binary files /dev/null and b/public/img/banner3.png differ diff --git a/public/img/home_banner1.png b/public/img/home_banner1.png new file mode 100644 index 00000000000..49ed2484a2e Binary files /dev/null and b/public/img/home_banner1.png differ diff --git a/public/types.ts b/public/types.ts new file mode 100644 index 00000000000..f28b3717a40 --- /dev/null +++ b/public/types.ts @@ -0,0 +1,85 @@ +export interface Phone { + id: string; + namespaceId: string; + name: string; + capacityAvailable: string[]; + capacity: string; + priceRegular: number; + priceDiscount: number; + colorsAvailable: string[]; + color: string; + images: string[]; + description: DescriptionBlock[]; + screen: string; + resolution: string; + processor: string; + ram: string; + camera: string; + zoom: string; + cell: string[]; + category: 'phones'; +} + +export interface Tablet { + id: string; + namespaceId: string; + name: string; + capacityAvailable: string[]; + capacity: string; + priceRegular: number; + priceDiscount: number; + colorsAvailable: string[]; + color: string; + images: string[]; + description: DescriptionBlock[]; + screen: string; + resolution: string; + processor: string; + ram: string; + camera: string; + zoom: string; + cell: string[]; + category: 'tablets'; +} + +export interface DescriptionBlock { + title: string; + text: string[]; +} + +export interface Accessory { + id: string; + namespaceId: string; + name: string; + capacityAvailable: string[]; + capacity: string; + priceRegular: number; + priceDiscount: number; + colorsAvailable: string[]; + color: string; + images: string[]; + description: DescriptionBlock[]; + screen: string; + resolution: string; + processor: string; + ram: string; + cell: string[]; + category: 'accessories'; +} + +export interface CatalogProduct { + id: number; + category: string; + itemId: string; + name: string; + fullPrice: number; + price: number; + screen: string; + capacity: string; + color: string; + ram: string; + year: number; + image: string; +} + +export type Category = 'phones' | 'tablets' | 'accessories'; diff --git a/public/utils/colorMap.ts b/public/utils/colorMap.ts new file mode 100644 index 00000000000..52a138e035f --- /dev/null +++ b/public/utils/colorMap.ts @@ -0,0 +1,26 @@ +export const colorMap: Record = { + black: '#27272a', + green: '#caebc2', + yellow: '#ffe681', + white: '#f9f9f9', + purple: '#d1c4e9', + red: '#ba0c2f', + midnightgreen: '#4e5851', + 'space gray': '#35363a', + spacegray: '#35363a', + silver: '#cfcfcf', + gold: '#fad7bd', + graphite: '#41424c', + sierrablue: '#9fb5c9', + pacificblue: '#28475c', + midnight: '#090927', + starlight: '#f8f3f0', + pink: '#fae0e4', + blue: '#437792', + 'midnight-blue': '#191970', + 'space-grey': '#535150', + 'rose gold': '#ead3d1', + 'sky blue': '#bdd4ec', + spaceblack: '#2e2c2b', + 'deep-purple': '#524a5c', +}; diff --git a/src/App.css b/src/App.css new file mode 100644 index 00000000000..c52b62cb2b4 --- /dev/null +++ b/src/App.css @@ -0,0 +1,79 @@ +@charset "UTF-8"; +@font-face { + font-family: "Mont"; + src: url("/fonts/Mont-Regular.otf") format("opentype"); + font-weight: 400; + font-style: normal; +} +@font-face { + font-family: "Mont"; + src: url("/fonts/Mont-SemiBold.otf") format("opentype"); + font-weight: 600; + font-style: normal; +} +@font-face { + font-family: "Mont"; + src: url("/fonts/Mont-Bold.otf") format("opentype"); + font-weight: 700; + font-style: normal; +} +@font-face { + font-family: "Mont"; + src: url("/fonts/Mont-Regular.otf") format("opentype"); + font-weight: 400; + font-style: normal; +} +@font-face { + font-family: "Mont"; + src: url("/fonts/Mont-SemiBold.otf") format("opentype"); + font-weight: 600; + font-style: normal; +} +@font-face { + font-family: "Mont"; + src: url("/fonts/Mont-Bold.otf") format("opentype"); + font-weight: 700; + font-style: normal; +} +/* Додатковий reset для специфічних елементів */ +div, section, article, header, footer, main, aside, nav { + margin: 0; + padding: 0; +} + +html { + margin: 0; + padding: 0; + box-sizing: border-box; + font-family: "Mont", sans-serif; +} + +* { + margin: 0; + padding: 0; +} + +html, body, #root { + margin: 0; + overflow-x: hidden; +} + +main { + max-width: 1200px; + margin: 0 auto; + padding: 0 60px; + /* Для центрування контенту всередині */ + display: flex; + flex-direction: column; + align-items: center; +} +@media (min-width: 320px) and (max-width: 639px) { + main { + padding: 0 16px; + } +} +@media (min-width: 640px) and (max-width: 1199px) { + main { + padding: 0 24px; + } +}/*# sourceMappingURL=App.css.map */ \ No newline at end of file diff --git a/src/App.css.map b/src/App.css.map new file mode 100644 index 00000000000..7c00caefa85 --- /dev/null +++ b/src/App.css.map @@ -0,0 +1 @@ +{"version":3,"sources":["App.css","variables.scss","App.scss","mixins.scss"],"names":[],"mappings":"AAAA,gBAAgB;ACUhB;EACE,mBAAA;EACA,sDAAA;EACA,gBAAA;EACA,kBAAA;ADRF;ACWA;EACE,mBAAA;EACA,uDAAA;EACA,gBAAA;EACA,kBAAA;ADTF;ACYA;EACE,mBAAA;EACA,mDAAA;EACA,gBAAA;EACA,kBAAA;ADVF;ACRA;EACE,mBAAA;EACA,sDAAA;EACA,gBAAA;EACA,kBAAA;ADUF;ACPA;EACE,mBAAA;EACA,uDAAA;EACA,gBAAA;EACA,kBAAA;ADSF;ACNA;EACE,mBAAA;EACA,mDAAA;EACA,gBAAA;EACA,kBAAA;ADQF;AEhCA,+CAAA;AACA;EACE,SAAA;EACA,UAAA;AFkCF;;AE7BA;EACE,SAAA;EACA,UAAA;EACA,sBAAA;EACA,+BAAA;AFgCF;;AE7BA;EACE,SAAA;EACA,UAAA;AFgCF;;AE7BA;EACE,SAAA;EACA,kBAAA;AFgCF;;AE7BA;EACE,iBAAA;EACA,cAAA;EACA,eAAA;EAEA,uCAAA;EACA,aAAA;EACA,sBAAA;EACA,mBAAA;AF+BF;AGjEE;ED0BF;IAWI,eAAA;EFgCF;AACF;AGjEE;EDqBF;IAeI,eAAA;EFiCF;AACF","file":"App.css"} \ No newline at end of file diff --git a/src/App.module.scss b/src/App.module.scss new file mode 100644 index 00000000000..0c7d6087433 --- /dev/null +++ b/src/App.module.scss @@ -0,0 +1,54 @@ +@import 'variables'; +@import 'mixins'; + +div, section, article, header, footer, main, aside, nav { + margin: 0; + padding: 0; +} + +html { + margin: 0; + padding: 0; + box-sizing: border-box; + font-family: Mont, sans-serif; + overflow-y: scroll; +} + +* { + margin: 0; + padding: 0; +} + +html, body, #root { + margin: 0; + overflow-x: hidden; +} + +main { + max-width: 1200px; + margin: 0 auto; + padding: 0 60px; + display: flex; + flex-direction: column; + align-items: center; + + @include mobile { + padding: 0 16px; + } + + @include tablet { + padding: 0 24px; + } +} + + +.appWrapper { + display: flex; + flex-direction: column; + min-height: 100vh; +} + +main { + flex: 1; +} + diff --git a/src/App.scss b/src/App.scss deleted file mode 100644 index 71bc413aade..00000000000 --- a/src/App.scss +++ /dev/null @@ -1 +0,0 @@ -// not empty diff --git a/src/App.tsx b/src/App.tsx index 372e4b42066..a2882054dce 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,7 +1,63 @@ -import './App.scss'; +import { Routes, Route } from 'react-router-dom'; +import { useEffect } from 'react'; -export const App = () => ( -
-

Product Catalog

-
-); +import { Header } from './components/Header'; +import { Footer } from './components/Footer'; +import { Layout } from './Layout'; + +import { Home } from './pages/Home'; +import { ProductPage } from './pages/ProductPage'; +import { CartPage } from './pages/CartPage'; +import { FavoritesPage } from './pages/FavoritesPage'; + +import { Catalog } from './components/Catalog'; +import { NotFoundPage } from './pages/NotFoundPage'; + +import s from './App.module.scss'; + +import { useAppDispatch, useAppSelector } from './app/hooks'; +import { fetchProducts } from './features/products/productsSlice'; + +function App() { + const dispatch = useAppDispatch(); + const { loading, error } = useAppSelector(state => state.products); + + useEffect(() => { + dispatch(fetchProducts()); + }, [dispatch]); + + if (loading) { + return
Loading products...
; + } + + if (error) { + return
Error: {error}
; + } + + return ( +
+
+
+ + + } /> + } /> + } /> + + } /> + } /> + + } /> + } /> + } /> + + } /> + + +
+
+
+ ); +} + +export default App; diff --git a/src/Layout.module.scss b/src/Layout.module.scss new file mode 100644 index 00000000000..c1d68e90095 --- /dev/null +++ b/src/Layout.module.scss @@ -0,0 +1,29 @@ +@import './variables'; +@import './mixins'; + +.layout { + display: grid; + width: 100%; + box-sizing: border-box; + max-width: 1200px; + margin: 0 auto; + padding: 64px 20px 0; + + grid-template-columns: repeat(24, 1fr); + column-gap: 40px; + + justify-content: center; + align-items: stretch; + + @include tablet { + padding: 49px 0 0; + max-width: 1199px; + grid-template-columns: repeat(12, 1fr); + } + + @include mobile { + padding: 49px 0 0; + max-width: 639px; + grid-template-columns: repeat(4, 1fr); + } +} diff --git a/src/Layout.tsx b/src/Layout.tsx new file mode 100644 index 00000000000..81f72146a2b --- /dev/null +++ b/src/Layout.tsx @@ -0,0 +1,11 @@ +import React, { ReactNode } from 'react'; + +import s from './Layout.module.scss'; + +interface LayoutProps { + children: ReactNode; +} + +export const Layout: React.FC = ({ children }) => { + return
{children}
; +}; diff --git a/src/app/hooks.ts b/src/app/hooks.ts new file mode 100644 index 00000000000..9f644dfca71 --- /dev/null +++ b/src/app/hooks.ts @@ -0,0 +1,6 @@ +import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux'; +import type { RootState, AppDispatch } from './store'; + +export const useAppDispatch = () => useDispatch(); + +export const useAppSelector: TypedUseSelectorHook = useSelector; diff --git a/src/app/store.ts b/src/app/store.ts new file mode 100644 index 00000000000..38db526cb96 --- /dev/null +++ b/src/app/store.ts @@ -0,0 +1,15 @@ +import { configureStore } from '@reduxjs/toolkit'; +import favoritesReducer from '../features/favorites/favoritesSlice'; +import cartReducer from '../features/cart/cartSlice'; +import productsReducer from '../features/products/productsSlice'; + +export const store = configureStore({ + reducer: { + favorites: favoritesReducer, + cart: cartReducer, + products: productsReducer, + }, +}); + +export type RootState = ReturnType; +export type AppDispatch = typeof store.dispatch; diff --git a/src/components/Breadcrumbs/Breadcrumbs.css b/src/components/Breadcrumbs/Breadcrumbs.css new file mode 100644 index 00000000000..8f0435b6d1c --- /dev/null +++ b/src/components/Breadcrumbs/Breadcrumbs.css @@ -0,0 +1,52 @@ +@font-face { + font-family: "Mont"; + src: url("/fonts/Mont-Regular.otf") format("opentype"); + font-weight: 400; + font-style: normal; +} +@font-face { + font-family: "Mont"; + src: url("/fonts/Mont-SemiBold.otf") format("opentype"); + font-weight: 600; + font-style: normal; +} +@font-face { + font-family: "Mont"; + src: url("/fonts/Mont-Bold.otf") format("opentype"); + font-weight: 700; + font-style: normal; +} +.breadcrumbs { + display: flex; + align-items: center; + gap: 8px; +} +.breadcrumbs__home img { + width: 16px; + max-height: 16px; +} +.breadcrumbs__arrow { + color: #89939A; +} +.breadcrumbs__current { + font-size: 12px; + font-weight: 500; + color: #B4BDC3; + line-height: 16px; +} + +.breadcrumbs a { + display: inline-flex; + align-items: center; + gap: 4px; + text-decoration: none; + color: inherit; +} + +.breadcrumbs a img { + width: 16px; + height: 16px; + -o-object-fit: contain; + object-fit: contain; + display: block; +}/*# sourceMappingURL=Breadcrumbs.css.map */ \ No newline at end of file diff --git a/src/components/Breadcrumbs/Breadcrumbs.css.map b/src/components/Breadcrumbs/Breadcrumbs.css.map new file mode 100644 index 00000000000..c4252481ca9 --- /dev/null +++ b/src/components/Breadcrumbs/Breadcrumbs.css.map @@ -0,0 +1 @@ +{"version":3,"sources":["../../variables.scss","Breadcrumbs.css","Breadcrumbs.scss"],"names":[],"mappings":"AAUA;EACE,mBAAA;EACA,sDAAA;EACA,gBAAA;EACA,kBAAA;ACTF;ADYA;EACE,mBAAA;EACA,uDAAA;EACA,gBAAA;EACA,kBAAA;ACVF;ADaA;EACE,mBAAA;EACA,mDAAA;EACA,gBAAA;EACA,kBAAA;ACXF;ACfA;EACE,aAAA;EACA,mBAAA;EACA,QAAA;ADiBF;ACfE;EACE,WAAA;EACA,gBAAA;ADiBJ;ACdE;EACE,cFuBc;ACPlB;ACbI;EACA,eAAA;EACA,gBAAA;EACA,cFkBU;EEjBV,iBAAA;ADeJ;;ACXA;EACE,oBAAA;EACA,mBAAA;EACA,QAAA;EACA,qBAAA;EACA,cAAA;ADcF;;ACXA;EACE,WAAA;EACA,YAAA;EACA,sBAAA;KAAA,mBAAA;EACA,cAAA;ADcF","file":"Breadcrumbs.css"} \ No newline at end of file diff --git a/src/components/Breadcrumbs/Breadcrumbs.module.scss b/src/components/Breadcrumbs/Breadcrumbs.module.scss new file mode 100644 index 00000000000..7d3e2d69a96 --- /dev/null +++ b/src/components/Breadcrumbs/Breadcrumbs.module.scss @@ -0,0 +1,52 @@ +@import '../../variables'; +@import '../../mixins'; + +.breadcrumbs { + display: flex; + align-items: center; + gap: 8px; + margin: 24px 0; + + a { + display: inline-flex; + align-items: center; + gap: 4px; + text-decoration: none; + color: inherit; + flex-shrink: 0; + + img { + width: 16px; + height: 16px; + object-fit: contain; + display: block; + } + } + + .breadcrumbsHome { + img { + width: 16px; + max-height: 16px; + } + } + + .breadcrumbsArrow { + opacity: 1; + } + + .breadcrumbsCurrent { + font-size: 12px; + font-weight: 600; + line-height: 16px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + + color: $color-primary; + + + &:last-child { + color: $color-secondary; + } + } +} diff --git a/src/components/Breadcrumbs/Breadcrumbs.tsx b/src/components/Breadcrumbs/Breadcrumbs.tsx new file mode 100644 index 00000000000..002df3a879c --- /dev/null +++ b/src/components/Breadcrumbs/Breadcrumbs.tsx @@ -0,0 +1,52 @@ +import { Link, useParams } from 'react-router-dom'; +import s from './Breadcrumbs.module.scss'; + +const TITLE_MAP: Record = { + phones: 'Phones', + tablets: 'Tablets', + accessories: 'Accessories', +}; + +interface BreadcrumbsProps { + productName?: string; +} + +const Breadcrumbs = ({ productName }: BreadcrumbsProps) => { + const { category } = useParams(); + + const title = category ? TITLE_MAP[category] : ''; + + return ( + + ); +}; + +export default Breadcrumbs; diff --git a/src/components/Breadcrumbs/index.ts b/src/components/Breadcrumbs/index.ts new file mode 100644 index 00000000000..ce977548b14 --- /dev/null +++ b/src/components/Breadcrumbs/index.ts @@ -0,0 +1 @@ +export * from './Breadcrumbs'; diff --git a/src/components/Catalog/Catalog.css b/src/components/Catalog/Catalog.css new file mode 100644 index 00000000000..3a4da1f52c7 --- /dev/null +++ b/src/components/Catalog/Catalog.css @@ -0,0 +1,106 @@ +@font-face { + font-family: "Mont"; + src: url("/fonts/Mont-Regular.otf") format("opentype"); + font-weight: 400; + font-style: normal; +} +@font-face { + font-family: "Mont"; + src: url("/fonts/Mont-SemiBold.otf") format("opentype"); + font-weight: 600; + font-style: normal; +} +@font-face { + font-family: "Mont"; + src: url("/fonts/Mont-Bold.otf") format("opentype"); + font-weight: 700; + font-style: normal; +} +@font-face { + font-family: "Mont"; + src: url("/fonts/Mont-Regular.otf") format("opentype"); + font-weight: 400; + font-style: normal; +} +@font-face { + font-family: "Mont"; + src: url("/fonts/Mont-SemiBold.otf") format("opentype"); + font-weight: 600; + font-style: normal; +} +@font-face { + font-family: "Mont"; + src: url("/fonts/Mont-Bold.otf") format("opentype"); + font-weight: 700; + font-style: normal; +} +.catalog { + grid-column: 1/-1; + margin-top: 24px; + margin-bottom: 80px; +} +.catalog .catalog-title { + grid-column: 1/-1; + font-size: 48px; + font-weight: 700; + line-height: 56px; + letter-spacing: -0.01em; + color: #313237; + margin-top: 40px; +} +@media (min-width: 320px) and (max-width: 639px) { + .catalog .catalog-title { + font-size: 32px; + font-weight: 700; + line-height: 41px; + letter-spacing: -0.01em; + } +} +.catalog .catalog-grid { + grid-column: 1/-1; + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 16px; +} +@media (min-width: 320px) and (max-width: 639px) { + .catalog .catalog-grid { + grid-template-columns: 1fr; + gap: 40px; + } +} +.catalog .catalog-count { + font-size: 14px; + color: #89939a; + margin-top: 8px; + margin-bottom: 40px; +} + +.catalog-pagination { + grid-column: 1/-1; + display: flex; + justify-content: center; + align-items: center; + gap: 8px; + margin-top: 40px; +} + +.pagination-arrow, +.pagination-button { + width: 32px; + height: 32px; + border: 1px solid #E2E6E9; + background-color: #FFF; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; +} +.pagination-arrow:hover:not(:disabled), +.pagination-button:hover:not(:disabled) { + border-color: #313237; +} +.pagination-arrow:disabled, +.pagination-button:disabled { + opacity: 0.4; + cursor: default; +}/*# sourceMappingURL=Catalog.css.map */ \ No newline at end of file diff --git a/src/components/Catalog/Catalog.css.map b/src/components/Catalog/Catalog.css.map new file mode 100644 index 00000000000..0d6f3b68c5a --- /dev/null +++ b/src/components/Catalog/Catalog.css.map @@ -0,0 +1 @@ +{"version":3,"sources":["../../variables.scss","Catalog.css","Catalog.scss","../../mixins.scss"],"names":[],"mappings":"AAUA;EACE,mBAAA;EACA,sDAAA;EACA,gBAAA;EACA,kBAAA;ACTF;ADYA;EACE,mBAAA;EACA,uDAAA;EACA,gBAAA;EACA,kBAAA;ACVF;ADaA;EACE,mBAAA;EACA,mDAAA;EACA,gBAAA;EACA,kBAAA;ACXF;ADPA;EACE,mBAAA;EACA,sDAAA;EACA,gBAAA;EACA,kBAAA;ACSF;ADNA;EACE,mBAAA;EACA,uDAAA;EACA,gBAAA;EACA,kBAAA;ACQF;ADLA;EACE,mBAAA;EACA,mDAAA;EACA,gBAAA;EACA,kBAAA;ACOF;AChCA;EACE,iBAAA;EACA,gBAAA;EACA,mBAAA;ADkCF;AC/BE;EACE,iBAAA;EACA,eAAA;EACA,gBAAA;EACA,iBAAA;EACA,uBAAA;EACA,cFoBY;EEnBZ,gBAAA;ADiCJ;AE9CE;EDMA;IAWI,eAAA;IACA,gBAAA;IACA,iBAAA;IACA,uBAAA;EDiCJ;AACF;AC9BE;EACE,iBAAA;EACA,aAAA;EACA,qCAAA;EACA,SAAA;ADgCJ;AE5DE;EDwBA;IAOI,0BAAA;IACA,SAAA;EDiCJ;AACF;AC9BE;EACA,eAAA;EACA,cAAA;EACA,eAAA;EACA,mBAAA;ADgCF;;AC3BA;EACE,iBAAA;EACA,aAAA;EACA,uBAAA;EACA,mBAAA;EACA,QAAA;EACA,gBAAA;AD8BF;;AC3BA;;EAEE,WAAA;EACA,YAAA;EACA,yBAAA;EACA,sBFtBY;EEuBZ,eAAA;EAEA,aAAA;EACA,mBAAA;EACA,uBAAA;AD6BF;AC3BE;;EACE,qBAAA;AD8BJ;AC3BE;;EACE,YAAA;EACA,eAAA;AD8BJ","file":"Catalog.css"} \ No newline at end of file diff --git a/src/components/Catalog/Catalog.module.scss b/src/components/Catalog/Catalog.module.scss new file mode 100644 index 00000000000..7a48072024e --- /dev/null +++ b/src/components/Catalog/Catalog.module.scss @@ -0,0 +1,115 @@ +@import '../../variables'; +@import '../../mixins'; + +.catalog { + grid-column: 1 / -1; + margin-bottom: 80px; + + @include mobile { margin-bottom: 64px; } + + @include tablet { margin-bottom: 64px; } + + .catalogTitle { + font-size: map-get($h1-styles, font-size); + font-weight: map-get($h1-styles, font-weight); + line-height: map-get($h1-styles, line-height); + letter-spacing: map-get($h1-styles, letter-spacing); + color: $color-primary; + margin-top: 40px; + margin-bottom: 8px; + + @include mobile { + font-size: map-get($h1-styles-mobile, font-size); + margin-top: 24px; + } + } + + .catalogCount { + font-size: 14px; + color: $color-secondary; + margin-top: 8px; + margin-bottom: 40px; + + @include mobile { + margin-bottom: 32px; + } + } + + .catalogGrid { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 40px 16px; + margin-top: 40px; + + @include tablet { + grid-template-columns: repeat(2, 1fr); + } + + @include mobile { + grid-template-columns: 1fr; + justify-items: center; + gap: 40px; + } + } + + .catalogEmpty { + grid-column: 1 / -1; + text-align: center; + color: $color-secondary; + margin-top: 40px; + } + + .catalogPagination { + display: flex; + justify-content: center; + align-items: center; + gap: 16px; + margin-top: 40px; + + @include mobile { + gap: 16px; + margin-top: 24px; + } + + .paginationList { + display: flex; + gap: 8px; + } + + .paginationButton, + .paginationArrow { + width: 40px; + height: 40px; + display: flex; + justify-content: center; + align-items: center; + border: 1px solid $color-icons; + background-color: $color-white; + cursor: pointer; + transition: all 0.3s; + flex-shrink: 0; + font-family: $font-main; + font-size: map-get($buttons, font-size); + + &:disabled { + opacity: 0.3; + cursor: not-allowed; + } + + &.active { + background-color: $color-primary; + color: $color-white; + border-color: $color-primary; + } + + &:hover:not(:disabled, .active) { + border-color: $color-primary; + } + + @include mobile { + width: 32px; + height: 32px; + } + } + } +} diff --git a/src/components/Catalog/Catalog.tsx b/src/components/Catalog/Catalog.tsx new file mode 100644 index 00000000000..5fdc52d2612 --- /dev/null +++ b/src/components/Catalog/Catalog.tsx @@ -0,0 +1,197 @@ +import React, { useMemo } from 'react'; +import { useParams, useSearchParams } from 'react-router-dom'; +import { useAppSelector } from '../../app/hooks'; +import ProductCard from '../ProductCard/ProductCard'; +import Breadcrumbs from '../Breadcrumbs/Breadcrumbs'; +import CatalogFilters from '../CatalogFilters/CatalogFilters'; + +import s from './Catalog.module.scss'; +import { CatalogProduct } from '../../../public/types'; + +const TITLES_MAP: Record = { + phones: 'Phones', + tablets: 'Tablets', + accessories: 'Accessories', +}; + +export const Catalog: React.FC = () => { + const { category } = useParams<{ category: string }>(); + const [searchParams, setSearchParams] = useSearchParams(); + + // Використовуємо дані з Redux (переконайся, що thunk завантажує саме products.json) + const { + items: allProducts, + loading, + error, + } = useAppSelector(state => state.products); + + // Отримуємо параметри з URL або ставимо дефолтні + const sort = searchParams.get('sort') || 'newest'; + const perPage = searchParams.get('perPage') || 'all'; + const currentPage = Number(searchParams.get('page')) || 1; + + const updateParams = (params: Record) => { + const newParams = new URLSearchParams(searchParams); + + Object.entries(params).forEach(([key, value]) => { + if ( + value === null || + value === 'all' || + (key === 'page' && value === 1) + ) { + newParams.delete(key); + } else { + newParams.set(key, value.toString()); + } + }); + + setSearchParams(newParams); + }; + + const title = category + ? (TITLES_MAP[category] ?? 'Catalog page') + : 'Catalog page'; + + // Фільтрація та сортування + const sortedProducts = useMemo(() => { + // 1. Фільтруємо за категорією + const filtered = allProducts.filter(p => p.category === category); + + // 2. Сортуємо (використовуємо поля з products.json: year та price) + return [...filtered].sort((a, b) => { + switch (sort) { + case 'alphabet': + return a.name.localeCompare(b.name); + case 'cheapest': + return a.price - b.price; + case 'expensive': // Додаємо Most Expensive для повноти + return b.price - a.price; + case 'newest': + // b - a дасть спадний порядок (найновіші перші) + return (b.year || 0) - (a.year || 0); + default: + return 0; + } + }); + }, [allProducts, category, sort]); + + const totalCount = sortedProducts.length; + const isAllSelected = perPage === 'all'; + const itemsPerPage = isAllSelected ? totalCount : Number(perPage); + const totalPages = Math.ceil(totalCount / itemsPerPage); + + // Пагінація: вибираємо товари для поточної сторінки + const visibleProducts = useMemo(() => { + const start = (currentPage - 1) * itemsPerPage; + + return sortedProducts.slice(start, start + itemsPerPage); + }, [sortedProducts, currentPage, itemsPerPage]); + + const getVisiblePages = () => { + const pages: number[] = []; + const maxVisible = 4; + let start = Math.max(1, currentPage - Math.floor(maxVisible / 2)); + let end = start + maxVisible - 1; + + if (end > totalPages) { + end = totalPages; + start = Math.max(1, end - maxVisible + 1); + } + + for (let i = start; i <= end; i++) { + if (i > 0) { + pages.push(i); + } + } + + return pages; + }; + + if (loading) { + return
Loading products...
; + } + + if (error) { + return ( +
+

Something went wrong

+ +
+ ); + } + + if (!category) { + return

No category selected

; + } + + return ( +
+ + +

{title}

+

+ {totalCount} {totalCount === 1 ? 'model' : 'models'} +

+ + updateParams({ sort: val, page: 1 })} + onPerPageChange={val => updateParams({ perPage: val, page: 1 })} + /> + + {/* Контейнер сітки з перевіркою на порожнечу */} +
+ {visibleProducts.length > 0 ? ( + visibleProducts.map(product => ( + + )) + ) : ( +

There are no {category} yet

+ )} +
+ + {/* Пагінація відображається лише якщо не вибрано 'all' і є більше 1 сторінки */} + {!isAllSelected && totalPages > 1 && ( +
+ + +
+ {getVisiblePages().map(page => ( + + ))} +
+ + +
+ )} +
+ ); +}; diff --git a/src/components/Catalog/CatalogWrapper.tsx b/src/components/Catalog/CatalogWrapper.tsx new file mode 100644 index 00000000000..79eb2f6f5a2 --- /dev/null +++ b/src/components/Catalog/CatalogWrapper.tsx @@ -0,0 +1,30 @@ +import React from 'react'; +import { useParams } from 'react-router-dom'; +import { Catalog } from './Catalog'; +import { CatalogProduct } from '../../../public/types'; + +import s from './Catalog.module.scss'; + +interface CatalogWrapperProps { + products: CatalogProduct[]; +} + +const CatalogWrapper: React.FC = ({ products }) => { + const { category } = useParams<{ category: string }>(); + + if (!category) { + return

No category provided

; + } + + if (!products || products.length === 0) { + return ( +
+
Loading products...
+
+ ); + } + + return ; +}; + +export default CatalogWrapper; diff --git a/src/components/Catalog/index.ts b/src/components/Catalog/index.ts new file mode 100644 index 00000000000..d50dc15fe0f --- /dev/null +++ b/src/components/Catalog/index.ts @@ -0,0 +1,2 @@ +export * from './Catalog'; +export * from './CatalogWrapper'; diff --git a/src/components/CatalogFilters/CatalogFilters.css b/src/components/CatalogFilters/CatalogFilters.css new file mode 100644 index 00000000000..05b69ce0354 --- /dev/null +++ b/src/components/CatalogFilters/CatalogFilters.css @@ -0,0 +1,153 @@ +@charset "UTF-8"; +@font-face { + font-family: "Mont"; + src: url("/fonts/Mont-Regular.otf") format("opentype"); + font-weight: 400; + font-style: normal; +} +@font-face { + font-family: "Mont"; + src: url("/fonts/Mont-SemiBold.otf") format("opentype"); + font-weight: 600; + font-style: normal; +} +@font-face { + font-family: "Mont"; + src: url("/fonts/Mont-Bold.otf") format("opentype"); + font-weight: 700; + font-style: normal; +} +.catalog-filters { + display: flex; + gap: 16px; + margin-bottom: 24px; +} +@media (min-width: 320px) and (max-width: 639px) { + .catalog-filters { + flex-direction: column; + gap: 20px; + } +} + +.filter { + display: flex; + flex-direction: column; + gap: 4px; +} + +.filter__label { + font-size: 12px; + color: #89939a; + font-weight: 600; + margin-bottom: 4px; +} + +.filter__control { + position: relative; + width: 176px; +} +@media (min-width: 320px) and (max-width: 639px) { + .filter__control { + width: 100%; + } +} + +/* Кастомний dropdown стилі */ +.custom-dropdown { + position: relative; +} + +.dropdown-toggle { + width: 100%; + height: 40px; + padding: 0 36px 0 12px; + display: flex; + align-items: center; + justify-content: space-between; + font-size: 14px; + font-weight: 600; + line-height: 40px; + border: 1px solid #b4bdc3; + background-color: #ffffff; + font-family: inherit; + color: #313237; + cursor: pointer; + text-align: left; +} +.dropdown-toggle:hover { + border-color: #313237; +} +.dropdown-toggle:focus { + outline: none; + border-color: #313237; + box-shadow: 0 0 0 2px rgba(49, 50, 55, 0.1); +} + +.dropdown-menu { + position: absolute; + top: 100%; + left: 0; + right: 0; + margin-top: 4px; + background-color: #ffffff; + border: 1px solid #e2e6e9; + box-shadow: 0 2px 15px rgba(0, 0, 0, 0.1); + z-index: 1000; + max-height: 200px; + overflow-y: auto; +} + +.dropdown-item { + padding: 8px 12px; + font-size: 14px; + font-weight: 500; + color: #313237; + cursor: pointer; +} +.dropdown-item:hover { + background-color: #f8f8f8; + color: #313237; +} +.dropdown-item.selected { + background-color: #f0f0f0; + color: #313237; + font-weight: 600; +} +.dropdown-item:first-child { + border-top-left-radius: 7px; + border-top-right-radius: 7px; +} +.dropdown-item:last-child { + border-bottom-left-radius: 7px; + border-bottom-right-radius: 7px; +} + +.filter__arrow { + position: absolute; + top: 50%; + right: 12px; + transform: translateY(-50%); + width: 16px; + height: 16px; + background: url("/img/Arrow_Down_Grey.svg") center/contain no-repeat; + pointer-events: none; +} + +/* Старий select для порівняння (можна видалити) */ +.filter__select { + width: 100%; + height: 40px; + padding: 0 36px 0 12px; + font-size: 14px; + line-height: 40px; + border: 1px solid #b4bdc3; + background-color: #fff; + border-radius: 8px; + font-family: inherit; + color: #313237; + font-weight: 600; + appearance: none; + -webkit-appearance: none; + -moz-appearance: none; + cursor: pointer; +}/*# sourceMappingURL=CatalogFilters.css.map */ \ No newline at end of file diff --git a/src/components/CatalogFilters/CatalogFilters.css.map b/src/components/CatalogFilters/CatalogFilters.css.map new file mode 100644 index 00000000000..52e306b8870 --- /dev/null +++ b/src/components/CatalogFilters/CatalogFilters.css.map @@ -0,0 +1 @@ +{"version":3,"sources":["CatalogFilters.css","../../variables.scss","CatalogFilters.scss","../../mixins.scss"],"names":[],"mappings":"AAAA,gBAAgB;ACUhB;EACE,mBAAA;EACA,sDAAA;EACA,gBAAA;EACA,kBAAA;ADRF;ACWA;EACE,mBAAA;EACA,uDAAA;EACA,gBAAA;EACA,kBAAA;ADTF;ACYA;EACE,mBAAA;EACA,mDAAA;EACA,gBAAA;EACA,kBAAA;ADVF;AEhBA;EACE,aAAA;EACA,SAAA;EACA,mBAAA;AFkBF;AGpBE;EDDF;IAMI,sBAAA;IACA,SAAA;EFmBF;AACF;;AEhBA;EACE,aAAA;EACA,sBAAA;EACA,QAAA;AFmBF;;AEhBA;EACE,eAAA;EACA,cAAA;EACA,gBAAA;EACA,kBAAA;AFmBF;;AEhBA;EACE,kBAAA;EACA,YAAA;AFmBF;AG5CE;EDuBF;IAKI,WAAA;EFoBF;AACF;;AEjBA,6BAAA;AACA;EACE,kBAAA;AFoBF;;AEjBA;EACE,WAAA;EACA,YAAA;EACA,sBAAA;EAEA,aAAA;EACA,mBAAA;EACA,8BAAA;EAEA,eAAA;EACA,gBAAA;EACA,iBAAA;EAEA,yBAAA;EACA,yBAAA;EAEA,oBAAA;EACA,cAAA;EAEA,eAAA;EACA,gBAAA;AFeF;AEbE;EACE,qBAAA;AFeJ;AEZE;EACE,aAAA;EACA,qBAAA;EACA,2CAAA;AFcJ;;AEVA;EACE,kBAAA;EACA,SAAA;EACA,OAAA;EACA,QAAA;EACA,eAAA;EAEA,yBAAA;EACA,yBAAA;EACA,yCAAA;EAEA,aAAA;EACA,iBAAA;EACA,gBAAA;AFWF;;AERA;EACE,iBAAA;EACA,eAAA;EACA,gBAAA;EACA,cAAA;EACA,eAAA;AFWF;AETE;EACE,yBAAA;EACA,cAAA;AFWJ;AERE;EACE,yBAAA;EACA,cAAA;EACA,gBAAA;AFUJ;AEPE;EACE,2BAAA;EACA,4BAAA;AFSJ;AENE;EACE,8BAAA;EACA,+BAAA;AFQJ;;AEJA;EACE,kBAAA;EACA,QAAA;EACA,WAAA;EACA,2BAAA;EAEA,WAAA;EACA,YAAA;EAEA,oEAAA;EACA,oBAAA;AFKF;;AEFA,kDAAA;AACA;EACE,WAAA;EACA,YAAA;EACA,sBAAA;EAEA,eAAA;EACA,iBAAA;EAEA,yBAAA;EACA,sBAAA;EACA,kBAAA;EAEA,oBAAA;EACA,cAAA;EACA,gBAAA;EAEA,gBAAA;EACA,wBAAA;EACA,qBAAA;EAEA,eAAA;AFAF","file":"CatalogFilters.css"} \ No newline at end of file diff --git a/src/components/CatalogFilters/CatalogFilters.module.scss b/src/components/CatalogFilters/CatalogFilters.module.scss new file mode 100644 index 00000000000..2007b29edf9 --- /dev/null +++ b/src/components/CatalogFilters/CatalogFilters.module.scss @@ -0,0 +1,114 @@ +@import '../../variables'; +@import '../../mixins'; + +.catalogFilters { + display: flex; + gap: 16px; + margin-bottom: 24px; + align-items: flex-end; + + @include mobile { + flex-flow: row wrap; + } +} + +.filter { + display: flex; + flex-direction: column; + gap: 4px; + + @include mobile { + width: 50%; + } + + &.customDropdown { + position: relative; + } +} + +.filterLabel { + font-size: 12px; + color: $color-secondary; + font-weight: 600; +} + +.filterControl { + position: relative; + width: 176px; + + @include mobile { + width: 100%; + } +} + +.dropdownToggle { + width: 100%; + height: 40px; + padding: 0 36px 0 12px; + display: block; + font-size: 14px; + font-weight: 600; + line-height: 38px; + border: 1px solid $color-icons; + background-color: $color-white; + font-family: inherit; + color: $color-primary; + cursor: pointer; + text-align: left; + white-space: nowrap; + text-overflow: ellipsis; + transition: border-color 0.3s ease; + + &:hover, &.active { + border-color: $color-primary; + } +} + +.dropdownMenu { + position: absolute; + top: 100%; + left: 0; + right: 0; + margin-top: 4px; + background-color: $color-white; + border: 1px solid $color-elements; + box-shadow: 0 2px 15px rgba(0, 0, 0, 0.1); + z-index: 1000; + max-height: 200px; + overflow-y: auto; + border-radius: 8px; +} + +.dropdownItem { + padding: 10px 12px; + font-size: 14px; + font-weight: 500; + color: $color-primary; + cursor: pointer; + transition: background-color 0.2s; + + &:hover { + background-color: $color-hover-BG; + } + + &.selected { + background-color: $color-elements; + font-weight: 600; + } +} + +.filterArrow { + position: absolute; + top: 50%; + right: 12px; + transform: translateY(-50%); + width: 16px; + height: 16px; + background: url('../../../public/img/Arrow_Down_Grey.svg') center / contain no-repeat; + pointer-events: none; + transition: transform 0.3s ease; + + &.active { + transform: translateY(-50%) rotate(180deg); + } +} diff --git a/src/components/CatalogFilters/CatalogFilters.tsx b/src/components/CatalogFilters/CatalogFilters.tsx new file mode 100644 index 00000000000..76f12e15173 --- /dev/null +++ b/src/components/CatalogFilters/CatalogFilters.tsx @@ -0,0 +1,137 @@ +import { useState, useRef, useEffect } from 'react'; +import s from './CatalogFilters.module.scss'; + +interface CatalogFiltersProps { + sort: string; + perPage: number | 'all'; + onSortChange: (value: string) => void; + onPerPageChange: (value: number | 'all') => void; +} + +const CatalogFilters = ({ + sort, + perPage, + onSortChange, + onPerPageChange, +}: CatalogFiltersProps) => { + const [isSortOpen, setIsSortOpen] = useState(false); + const [isPerPageOpen, setIsPerPageOpen] = useState(false); + + const sortRef = useRef(null); + const perPageRef = useRef(null); + + const sortOptions = [ + { value: 'newest', label: 'Newest' }, + { value: 'alphabet', label: 'Alphabetically' }, + { value: 'cheapest', label: 'Cheapest' }, + { value: 'expensive', label: 'Most expensive' }, + ]; + + const perPageOptions = [ + { value: 4, label: '4' }, + { value: 8, label: '8' }, + { value: 16, label: '16' }, + { value: 'all', label: 'All' }, + ]; + + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if (sortRef.current && !sortRef.current.contains(event.target as Node)) { + setIsSortOpen(false); + } + + if ( + perPageRef.current && + !perPageRef.current.contains(event.target as Node) + ) { + setIsPerPageOpen(false); + } + }; + + document.addEventListener('mousedown', handleClickOutside); + + return () => document.removeEventListener('mousedown', handleClickOutside); + }, []); + + return ( +
+ {/* SORT BY */} +
+ Sort by +
+ + + {isSortOpen && ( +
+ {sortOptions.map(option => ( +
{ + e.preventDefault(); + e.stopPropagation(); + onSortChange(option.value); + setIsSortOpen(false); + }} + > + {option.label} +
+ ))} +
+ )} +
+
+ + {/* ITEMS ON PAGE */} +
+ Items on page +
+ + + {isPerPageOpen && ( +
+ {perPageOptions.map(option => ( +
{ + e.preventDefault(); + e.stopPropagation(); + onPerPageChange(option.value as number | 'all'); + setIsPerPageOpen(false); + }} + > + {option.label} +
+ ))} +
+ )} +
+
+
+ ); +}; + +export default CatalogFilters; diff --git a/src/components/CatalogFilters/index.ts b/src/components/CatalogFilters/index.ts new file mode 100644 index 00000000000..d8e6e011f84 --- /dev/null +++ b/src/components/CatalogFilters/index.ts @@ -0,0 +1 @@ +export * from './CatalogFilters'; diff --git a/src/components/ConfirmationModalFavorites/ConfirmationModalFavorites.css b/src/components/ConfirmationModalFavorites/ConfirmationModalFavorites.css new file mode 100644 index 00000000000..22de97d0345 --- /dev/null +++ b/src/components/ConfirmationModalFavorites/ConfirmationModalFavorites.css @@ -0,0 +1,82 @@ +.modal-overlay { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.5); + display: flex; + justify-content: center; + align-items: center; + z-index: 1000; + backdrop-filter: blur(2px); +} + +.modal-content { + background: white; + padding: 32px; + border-radius: 8px; + max-width: 400px; + width: 90%; + position: relative; + text-align: center; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15); + animation: modalFadeIn 0.3s ease; +} + +.modal-close { + position: absolute; + top: 10px; + right: 15px; + background: none; + border: none; + font-size: 24px; + cursor: pointer; + color: #888; +} +.modal-close:hover { + color: #333; +} + +.modal-message { + font-size: 18px; + margin-bottom: 24px; + color: #333; +} + +.modal-actions { + display: flex; + gap: 16px; + justify-content: center; +} + +.modal-btn { + padding: 10px 24px; + border: none; + border-radius: 4px; + font-weight: 600; + cursor: pointer; + transition: opacity 0.2s; +} +.modal-btn:hover { + opacity: 0.9; +} +.modal-btn--confirm { + background-color: #eb5757; + color: white; +} +.modal-btn--cancel { + background-color: #f0f0f0; + color: #333; +} + +@keyframes modalFadeIn { + from { + transform: translateY(-20px); + opacity: 0; + } + to { + transform: translateY(0); + opacity: 1; + } +}/*# sourceMappingURL=ConfirmationModalFavorites.css.map */ \ No newline at end of file diff --git a/src/components/ConfirmationModalFavorites/ConfirmationModalFavorites.css.map b/src/components/ConfirmationModalFavorites/ConfirmationModalFavorites.css.map new file mode 100644 index 00000000000..abc3fbcd8e4 --- /dev/null +++ b/src/components/ConfirmationModalFavorites/ConfirmationModalFavorites.css.map @@ -0,0 +1 @@ +{"version":3,"sources":["ConfirmationModalFavorites.scss","ConfirmationModalFavorites.css"],"names":[],"mappings":"AAAA;EACE,eAAA;EACA,MAAA;EACA,OAAA;EACA,WAAA;EACA,YAAA;EACA,8BAAA;EACA,aAAA;EACA,uBAAA;EACA,mBAAA;EACA,aAAA;EACA,0BAAA;ACCF;;ADEA;EACE,iBAAA;EACA,aAAA;EACA,kBAAA;EACA,gBAAA;EACA,UAAA;EACA,kBAAA;EACA,kBAAA;EACA,0CAAA;EACA,gCAAA;ACCF;;ADEA;EACE,kBAAA;EACA,SAAA;EACA,WAAA;EACA,gBAAA;EACA,YAAA;EACA,eAAA;EACA,eAAA;EACA,WAAA;ACCF;ADAE;EAAU,WAAA;ACGZ;;ADAA;EACE,eAAA;EACA,mBAAA;EACA,WAAA;ACGF;;ADAA;EACE,aAAA;EACA,SAAA;EACA,uBAAA;ACGF;;ADAA;EACE,kBAAA;EACA,YAAA;EACA,kBAAA;EACA,gBAAA;EACA,eAAA;EACA,wBAAA;ACGF;ADDE;EAAU,YAAA;ACIZ;ADFE;EACE,yBAAA;EACA,YAAA;ACIJ;ADDE;EACE,yBAAA;EACA,WAAA;ACGJ;;ADCA;EACE;IAAO,4BAAA;IAA8B,UAAA;ECIrC;EDHA;IAAK,wBAAA;IAA0B,UAAA;ECO/B;AACF","file":"ConfirmationModalFavorites.css"} \ No newline at end of file diff --git a/src/components/ConfirmationModalFavorites/ConfirmationModalFavorites.module.scss b/src/components/ConfirmationModalFavorites/ConfirmationModalFavorites.module.scss new file mode 100644 index 00000000000..fc02df53869 --- /dev/null +++ b/src/components/ConfirmationModalFavorites/ConfirmationModalFavorites.module.scss @@ -0,0 +1,96 @@ +@import '../../variables'; +@import '../../mixins'; + +.modalOverlay { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.5); + display: flex; + justify-content: center; + align-items: center; + z-index: 10000; + backdrop-filter: blur(2px); +} + +.modalContent { + background: $color-white; + padding: 32px; + border-radius: 0; + max-width: 400px; + width: 90%; + position: relative; + text-align: center; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15); + animation: modal-fade-in 0.3s ease; +} + +.modalClose { + position: absolute; + top: 10px; + right: 15px; + background: none; + border: none; + font-size: 24px; + cursor: pointer; + color: $color-secondary; + transition: color 0.2s; + + &:hover { + color: $color-primary; + } +} + +.modalMessage { + font-size: 18px; + margin-bottom: 24px; + color: $color-primary; + font-family: $font-main; +} + +.modalActions { + display: flex; + gap: 16px; + justify-content: center; +} + +.modalBtn { + padding: 10px 24px; + border: none; + border-radius: 0; + font-weight: 600; + font-family: $font-main; + cursor: pointer; + transition: opacity 0.2s, transform 0.1s; + + &:hover { + opacity: 0.9; + } + + &:active { + transform: scale(0.98); + } + + &.confirm { + background-color: $color-red; + color: $color-white; + } + + &.cancel { + background-color: $color-elements; + color: $color-primary; + } +} + +@keyframes modal-fade-in { + from { + transform: translateY(-20px); + opacity: 0; + } + to { + transform: translateY(0); + opacity: 1; + } +} diff --git a/src/components/ConfirmationModalFavorites/ConfirmationModalFavorites.tsx b/src/components/ConfirmationModalFavorites/ConfirmationModalFavorites.tsx new file mode 100644 index 00000000000..3d6f3f46a64 --- /dev/null +++ b/src/components/ConfirmationModalFavorites/ConfirmationModalFavorites.tsx @@ -0,0 +1,51 @@ +import React from 'react'; +import s from './ConfirmationModalFavorites.module.scss'; + +interface Props { + isOpen: boolean; + message: string; + onConfirm: () => void; + onCancel: () => void; + confirmText?: string; + cancelText?: string; +} + +export const ConfirmationModalFavorites: React.FC = ({ + isOpen, + message, + onConfirm, + onCancel, + confirmText = 'Confirm', + cancelText = 'Cancel', +}) => { + if (!isOpen) { + return null; + } + + const handleOverlayClick = (e: React.MouseEvent) => { + if (e.target === e.currentTarget) { + onCancel(); + } + }; + + return ( +
+
+ + +

{message}

+ +
+ + +
+
+
+ ); +}; diff --git a/src/components/ConfirmationModalFavorites/index.ts b/src/components/ConfirmationModalFavorites/index.ts new file mode 100644 index 00000000000..275f061754e --- /dev/null +++ b/src/components/ConfirmationModalFavorites/index.ts @@ -0,0 +1 @@ +export * from './ConfirmationModalFavorites'; diff --git a/src/components/Footer/Footer.css b/src/components/Footer/Footer.css new file mode 100644 index 00000000000..df0e83f1900 --- /dev/null +++ b/src/components/Footer/Footer.css @@ -0,0 +1,165 @@ +@font-face { + font-family: "Mont"; + src: url("/fonts/Mont-Regular.otf") format("opentype"); + font-weight: 400; + font-style: normal; +} +@font-face { + font-family: "Mont"; + src: url("/fonts/Mont-SemiBold.otf") format("opentype"); + font-weight: 600; + font-style: normal; +} +@font-face { + font-family: "Mont"; + src: url("/fonts/Mont-Bold.otf") format("opentype"); + font-weight: 700; + font-style: normal; +} +@font-face { + font-family: "Mont"; + src: url("/fonts/Mont-Regular.otf") format("opentype"); + font-weight: 400; + font-style: normal; +} +@font-face { + font-family: "Mont"; + src: url("/fonts/Mont-SemiBold.otf") format("opentype"); + font-weight: 600; + font-style: normal; +} +@font-face { + font-family: "Mont"; + src: url("/fonts/Mont-Bold.otf") format("opentype"); + font-weight: 700; + font-style: normal; +} +.footer { + grid-column: 1/-1; + width: 100%; + background-color: #FFF; + border-top: 1px solid #E2E6E9; + padding: 32px 0; + display: flex; + justify-content: center; +} +.footer .footer-container { + display: grid; + width: 100%; + max-width: 1200px; + padding: 0 20px; + box-sizing: border-box; + grid-template-columns: repeat(24, 1fr); + -moz-column-gap: 16px; + column-gap: 16px; +} +@media (min-width: 640px) and (max-width: 1199px) { + .footer .footer-container { + grid-template-columns: repeat(12, 1fr); + padding: 0 16px; + max-width: 1199px; + } +} +@media (min-width: 320px) and (max-width: 639px) { + .footer .footer-container { + display: flex; + flex-direction: column; + align-items: center; + gap: 32px; + padding: 0 16px; + max-width: 639px; + } +} +.footer .footer-container .footer-left { + grid-column: 1/5; + display: flex; + align-items: center; +} +@media (min-width: 640px) and (max-width: 1199px) { + .footer .footer-container .footer-left { + grid-column: 1/3; + } +} +@media (min-width: 320px) and (max-width: 639px) { + .footer .footer-container .footer-left { + width: auto; + } +} +.footer .footer-container .footer-left .footer-logo { + width: 89px; + height: auto; + display: block; +} +.footer .footer-container .footer-center { + grid-column: 7/19; + display: flex; + justify-content: center; + align-items: center; + gap: 32px; +} +@media (min-width: 640px) and (max-width: 1199px) { + .footer .footer-container .footer-center { + grid-column: 4/10; + gap: 16px; + } +} +@media (min-width: 320px) and (max-width: 639px) { + .footer .footer-container .footer-center { + flex-direction: column; + gap: 16px; + } +} +.footer .footer-container .footer-center a { + text-decoration: none; + text-transform: uppercase; + color: #89939A; + font-size: 12px; + font-weight: 800; + transition: color 0.3s; + white-space: nowrap; +} +.footer .footer-container .footer-center a:hover { + color: #313237; +} +.footer .footer-container .footer-right { + grid-column: 21/25; + display: flex; + justify-content: flex-end; + align-items: center; + gap: 16px; +} +@media (min-width: 640px) and (max-width: 1199px) { + .footer .footer-container .footer-right { + grid-column: 10/13; + } +} +@media (min-width: 320px) and (max-width: 639px) { + .footer .footer-container .footer-right { + width: 100%; + justify-content: center; + } +} +.footer .footer-container .footer-right .back-to-top-text { + color: #89939A; + font-size: 12px; + white-space: nowrap; +} +.footer .footer-container .footer-right .back-to-top-button { + width: 32px; + height: 32px; + display: flex; + align-items: center; + justify-content: center; + background-color: #FFF; + border: 1px solid #B4BDC3; + cursor: pointer; + transition: border-color 0.3s; +} +.footer .footer-container .footer-right .back-to-top-button img { + width: 16px; + height: 16px; + transform: rotate(-90deg); +} +.footer .footer-container .footer-right .back-to-top-button:hover { + border-color: #313237; +}/*# sourceMappingURL=Footer.css.map */ \ No newline at end of file diff --git a/src/components/Footer/Footer.css.map b/src/components/Footer/Footer.css.map new file mode 100644 index 00000000000..3e2ff223691 --- /dev/null +++ b/src/components/Footer/Footer.css.map @@ -0,0 +1 @@ +{"version":3,"sources":["../../variables.scss","Footer.css","Footer.scss","../../mixins.scss"],"names":[],"mappings":"AAUA;EACE,mBAAA;EACA,sDAAA;EACA,gBAAA;EACA,kBAAA;ACTF;ADYA;EACE,mBAAA;EACA,uDAAA;EACA,gBAAA;EACA,kBAAA;ACVF;ADaA;EACE,mBAAA;EACA,mDAAA;EACA,gBAAA;EACA,kBAAA;ACXF;ADPA;EACE,mBAAA;EACA,sDAAA;EACA,gBAAA;EACA,kBAAA;ACSF;ADNA;EACE,mBAAA;EACA,uDAAA;EACA,gBAAA;EACA,kBAAA;ACQF;ADLA;EACE,mBAAA;EACA,mDAAA;EACA,gBAAA;EACA,kBAAA;ACOF;AChCA;EACE,iBAAA;EACA,WAAA;EACA,sBFkCY;EEjCZ,6BAAA;EACA,eAAA;EACA,aAAA;EACA,uBAAA;ADkCF;AChCE;EACE,aAAA;EACA,WAAA;EACA,iBAAA;EAEA,eAAA;EACA,sBAAA;EAGA,sCAAA;EACA,qBAAA;OAAA,gBAAA;AD+BJ;AE7CE;EDIA;IAcI,sCAAA;IACA,eAAA;IACA,iBAAA;ED+BJ;AACF;AEzDE;EDSA;IAuBI,aAAA;IACA,sBAAA;IACA,mBAAA;IACA,SAAA;IACA,eAAA;IACA,gBAAA;ED6BJ;AACF;AC3BI;EACE,gBAAA;EACA,aAAA;EACA,mBAAA;AD6BN;AEnEE;EDmCE;IAMI,gBAAA;ED8BN;AACF;AE7EE;EDwCE;IAUI,WAAA;ED+BN;AACF;AC7BM;EACE,WAAA;EACA,YAAA;EACA,cAAA;AD+BR;AC3BI;EACE,iBAAA;EACA,aAAA;EACA,uBAAA;EACA,mBAAA;EACA,SAAA;AD6BN;AEzFE;EDuDE;IAQI,iBAAA;IACA,SAAA;ED8BN;AACF;AEpGE;ED4DE;IAaI,sBAAA;IACA,SAAA;ED+BN;AACF;AC7BM;EACE,qBAAA;EACA,yBAAA;EACA,cF/CU;EEgDV,eAAA;EACA,gBAAA;EACA,sBAAA;EACA,mBAAA;AD+BR;AC7BQ;EACE,cFvDM;ACsFhB;AC1BI;EACE,kBAAA;EACA,aAAA;EACA,yBAAA;EACA,mBAAA;EACA,SAAA;AD4BN;AExHE;EDuFE;IAQI,kBAAA;ED6BN;AACF;AElIE;ED4FE;IAYI,WAAA;IACA,uBAAA;ED8BN;AACF;AC5BM;EACE,cF5EU;EE6EV,eAAA;EACA,mBAAA;AD8BR;AC3BM;EACE,WAAA;EACA,YAAA;EACA,aAAA;EACA,mBAAA;EACA,uBAAA;EACA,sBFnFM;EEoFN,yBAAA;EACA,eAAA;EACA,6BAAA;AD6BR;AC3BQ;EACE,WAAA;EACA,YAAA;EAEA,yBAAA;AD4BV;ACzBQ;EACE,qBFrGM;ACgIhB","file":"Footer.css"} \ No newline at end of file diff --git a/src/components/Footer/Footer.module.scss b/src/components/Footer/Footer.module.scss new file mode 100644 index 00000000000..230dd53c642 --- /dev/null +++ b/src/components/Footer/Footer.module.scss @@ -0,0 +1,134 @@ +@import '../../variables'; +@import '../../mixins'; + +.footer { + width: 100%; + background-color: $color-white; + border-top: 1px solid $color-elements; + padding: 32px 0; + display: flex; + justify-content: center; + flex-shrink: 0; + + .footerContainer { + display: grid; + width: 100%; + max-width: 1200px; + padding: 0 20px; + box-sizing: border-box; + + grid-template-columns: repeat(24, 1fr); + column-gap: 16px; + + @include tablet { + grid-template-columns: repeat(12, 1fr); + padding: 0 24px; + max-width: 1199px; + } + + @include mobile { + display: flex; + flex-direction: column; + gap: 32px; + padding: 0 16px; + max-width: 639px; + } + + .footerLeft { + grid-column: 1 / 5; + display: flex; + align-items: center; + + @include tablet { + grid-column: 1 / 3; + } + + @include mobile { + width: auto; + } + + .footerLogo { + width: 89px; + height: auto; + display: block; + } + } + + .footerCenter { + grid-column: 7 / 19; + display: flex; + justify-content: center; + align-items: center; + gap: 32px; + + @include tablet { + grid-column: 4 / 10; + gap: 16px; + } + + @include mobile { + flex-direction: column; + align-items: start; + gap: 16px; + } + + a { + text-decoration: none; + text-transform: uppercase; + color: $color-secondary; + font-size: 12px; + font-weight: 800; + transition: color 0.3s; + white-space: nowrap; + + &:hover { + color: $color-primary; + } + } + } + + .footerRight { + grid-column: 21 / 25; + display: flex; + justify-content: flex-end; + align-items: center; + gap: 16px; + + @include tablet { + grid-column: 10 / 13; + } + + @include mobile { + width: 100%; + justify-content: center; + } + + .backToTopText { + color: $color-secondary; + font-size: 12px; + white-space: nowrap; + } + + .backToTopButton { + width: 32px; + height: 32px; + display: flex; + align-items: center; + justify-content: center; + background-color: $color-white; + border: 1px solid $color-icons; + cursor: pointer; + transition: border-color 0.3s; + + img { + width: 16px; + height: 16px; + } + + &:hover { + border-color: $color-primary; + } + } + } + } +} diff --git a/src/components/Footer/Footer.tsx b/src/components/Footer/Footer.tsx new file mode 100644 index 00000000000..7e07c4617c6 --- /dev/null +++ b/src/components/Footer/Footer.tsx @@ -0,0 +1,51 @@ +import React from 'react'; +import { Link } from 'react-router-dom'; +import s from './Footer.module.scss'; + +export const Footer: React.FC = () => { + const scrollToTop = () => { + window.scrollTo({ top: 0, behavior: 'smooth' }); + }; + + return ( + + ); +}; diff --git a/src/components/Footer/index.ts b/src/components/Footer/index.ts new file mode 100644 index 00000000000..ddcc5a9cd18 --- /dev/null +++ b/src/components/Footer/index.ts @@ -0,0 +1 @@ +export * from './Footer'; diff --git a/src/components/Header/Header.css b/src/components/Header/Header.css new file mode 100644 index 00000000000..e090b123ef6 --- /dev/null +++ b/src/components/Header/Header.css @@ -0,0 +1,272 @@ +@charset "UTF-8"; +@font-face { + font-family: "Mont"; + src: url("/fonts/Mont-Regular.otf") format("opentype"); + font-weight: 400; + font-style: normal; +} +@font-face { + font-family: "Mont"; + src: url("/fonts/Mont-SemiBold.otf") format("opentype"); + font-weight: 600; + font-style: normal; +} +@font-face { + font-family: "Mont"; + src: url("/fonts/Mont-Bold.otf") format("opentype"); + font-weight: 700; + font-style: normal; +} +@font-face { + font-family: "Mont"; + src: url("/fonts/Mont-Regular.otf") format("opentype"); + font-weight: 400; + font-style: normal; +} +@font-face { + font-family: "Mont"; + src: url("/fonts/Mont-SemiBold.otf") format("opentype"); + font-weight: 600; + font-style: normal; +} +@font-face { + font-family: "Mont"; + src: url("/fonts/Mont-Bold.otf") format("opentype"); + font-weight: 700; + font-style: normal; +} +.header { + position: fixed; + top: 0; + left: 0; + width: 100%; + z-index: 10000; + border-bottom: 1px solid #E2E6E9; + background-color: #FFF; + font-size: 12px; + font-weight: 800; + line-height: 11px; + letter-spacing: 0.04em; +} +.header nav { + display: flex; + justify-content: space-between; + align-items: stretch; + width: 100%; +} +.header .logo { + width: 80px; + padding: 18px 24px; + display: flex; + align-items: center; +} +@media (min-width: 320px) and (max-width: 639px) { + .header .logo { + width: 64px; + padding: 13px 16px; + } +} +@media (min-width: 640px) and (max-width: 1199px) { + .header .logo { + width: 64px; + padding: 13px 16px; + } +} +.header .nav-left { + display: flex; + align-items: center; + gap: 64px; +} +@media (min-width: 320px) and (max-width: 639px) { + .header .nav-left { + gap: 175px; + } +} +@media (min-width: 640px) and (max-width: 1199px) { + .header .nav-left { + gap: 16px; + } +} +.header .nav-links { + list-style: none; + display: flex; + gap: 64px; +} +@media (min-width: 320px) and (max-width: 639px) { + .header .nav-links { + gap: 16px; + } +} +@media (min-width: 640px) and (max-width: 1199px) { + .header .nav-links { + gap: 32px; + } +} +.header .nav-links .nav-link { + color: #89939A; + text-decoration: none; + text-transform: uppercase; + position: relative; + padding: 25px 0; + transition: color 0.3s; +} +@media (min-width: 320px) and (max-width: 639px) { + .header .nav-links .nav-link { + padding: 8px 0; + } +} +@media (min-width: 640px) and (max-width: 1199px) { + .header .nav-links .nav-link { + padding: 18px 0; + } +} +.header .nav-links .nav-link { + /* Додаємо псевдоелемент для зайняття простору (опційно) */ +} +.header .nav-links .nav-link::before { + content: ""; + position: absolute; + bottom: -19px; /* 16px + 3px висота риски */ + left: 0; + width: 100%; + height: 19px; /* Висота риски + відступ */ + background: transparent; +} +.header .nav-links .nav-link:hover, .header .nav-links .nav-link.active { + color: #313237; +} +.header .nav-links .nav-link.active::after { + content: ""; + position: absolute; + bottom: 0; /* ← ЗМІНА: риска впритул до тексту */ + left: 0; + width: 100%; + height: 3px; + background-color: #313237; +} +@media (min-width: 320px) and (max-width: 639px) { + .header .nav-links .nav-link.active::after { + height: 2px; + } +} +.header .nav-links { + /* MOBILE MENU */ +} +@media (min-width: 320px) and (max-width: 639px) { + .header .nav-links { + position: fixed; + top: 49px; /* Відступ зверху під хедер */ + left: 0; + right: 0; + bottom: 0; + display: none; + flex-direction: column; + justify-content: flex-start; + align-items: center; + gap: 32px; + padding-top: 48px; + background-color: #FFF; + z-index: 9999; + overflow-y: auto; + } +} +@media (min-width: 320px) and (max-width: 639px) { + .header .nav-links.open { + display: flex; + } +} +.header .button-group-header { + display: flex; + align-items: stretch; + opacity: 1; +} +@media (min-width: 320px) and (max-width: 639px) { + .header .button-group-header { + position: fixed; + bottom: 0; + left: 0; + width: 100%; + height: 64px; + gap: 0; + padding-bottom: env(safe-area-inset-bottom); + background-color: #FFF; + border-top: 1px solid #E2E6E9; + z-index: 10000; + /* по замовчуванню приховані */ + opacity: 0; + pointer-events: none; + transform: translateY(100%); + transition: transform 0.3s ease, opacity 0.3s ease; + } + .header .button-group-header.mobile-visible { + opacity: 1; + pointer-events: auto; + transform: translateY(0); + } +} +.header .button-group-header-block { + width: 64px; + display: flex; + border-left: 1px solid #E2E6E9; + transition: background-color 0.3s; +} +.header .button-group-header-block:hover { + background-color: #FAFBFC; +} +@media (min-width: 320px) and (max-width: 639px) { + .header .button-group-header-block { + width: 50%; + } +} +.header .button-group-header-block a { + display: flex; + align-items: center; + justify-content: center; + width: 100%; + height: 100%; +} +.header .cart, +.header .like { + width: 16px; + height: 16px; +} +.header { + /* BURGER МЕНЮ */ +} +.header .burger-menu { + display: none; +} +@media (min-width: 320px) and (max-width: 639px) { + .header .burger-menu { + display: flex; + justify-content: center; + align-items: center; + width: 48px; /* або 48px */ + height: 100%; /* Займає всю висоту хедера */ + border-left: 1px solid #E2E6E9; + cursor: pointer; + position: relative; + z-index: 10001; + /* Прибираємо зайві стилі для span */ + } + .header .burger-menu span { + display: none; + } + .header .burger-menu img { + width: 16px; + height: 16px; + transition: opacity 0.3s; + padding: 16px; + } + .header .burger-menu { + /* Якщо потрібна анімація зміни іконки */ + } + .header .burger-menu.open img { + opacity: 0.7; + /* або змінити src через JS */ + } +} +.header .header.scrolled { + box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15); + border-bottom-color: transparent; +}/*# sourceMappingURL=Header.css.map */ \ No newline at end of file diff --git a/src/components/Header/Header.css.map b/src/components/Header/Header.css.map new file mode 100644 index 00000000000..1d83de79097 --- /dev/null +++ b/src/components/Header/Header.css.map @@ -0,0 +1 @@ +{"version":3,"sources":["Header.css","../../variables.scss","Header.scss","../../mixins.scss"],"names":[],"mappings":"AAAA,gBAAgB;ACUhB;EACE,mBAAA;EACA,sDAAA;EACA,gBAAA;EACA,kBAAA;ADRF;ACWA;EACE,mBAAA;EACA,uDAAA;EACA,gBAAA;EACA,kBAAA;ADTF;ACYA;EACE,mBAAA;EACA,mDAAA;EACA,gBAAA;EACA,kBAAA;ADVF;ACRA;EACE,mBAAA;EACA,sDAAA;EACA,gBAAA;EACA,kBAAA;ADUF;ACPA;EACE,mBAAA;EACA,uDAAA;EACA,gBAAA;EACA,kBAAA;ADSF;ACNA;EACE,mBAAA;EACA,mDAAA;EACA,gBAAA;EACA,kBAAA;ADQF;AEjCA;EACE,eAAA;EACA,MAAA;EACA,OAAA;EACA,WAAA;EACA,cAAA;EACA,gCAAA;EACA,sBD8BY;EC5BZ,eAAA;EACA,gBAAA;EACA,iBAAA;EACA,sBAAA;AFkCF;AEhCE;EACE,aAAA;EACA,8BAAA;EACA,oBAAA;EACA,WAAA;AFkCJ;AE/BE;EACE,WAAA;EACA,kBAAA;EACA,aAAA;EACA,mBAAA;AFiCJ;AG1DE;EDqBA;IAOI,WAAA;IACA,kBAAA;EFkCJ;AACF;AG3DE;EDgBA;IAYI,WAAA;IACA,kBAAA;EFmCJ;AACF;AEhCE;EACE,aAAA;EACA,mBAAA;EACA,SAAA;AFkCJ;AG3EE;EDsCA;IAME,UAAA;EFmCF;AACF;AG3EE;EDiCA;IAUE,SAAA;EFoCF;AACF;AEhCE;EACE,gBAAA;EACA,aAAA;EACA,SAAA;AFkCJ;AG1FE;EDqDA;IAMI,SAAA;EFmCJ;AACF;AG1FE;EDgDA;IAUI,SAAA;EFoCJ;AACF;AElCI;EACF,cDlCgB;ECmChB,qBAAA;EACA,yBAAA;EACA,kBAAA;EACA,eAAA;EACA,sBAAA;AFoCF;AG5GE;EDkEE;IASE,cAAA;EFqCJ;AACF;AG5GE;ED6DE;IAaE,eAAA;EFsCJ;AACF;AEpDI;EAgBF,0DAAA;AFuCF;AEtCE;EACE,WAAA;EACA,kBAAA;EACA,aAAA,EAAA,4BAAA;EACA,OAAA;EACA,WAAA;EACA,YAAA,EAAA,2BAAA;EACA,uBAAA;AFwCJ;AErCE;EAEE,cD/DY;ADqGhB;AEnCE;EACE,WAAA;EACA,kBAAA;EACA,SAAA,EAAA,qCAAA;EACA,OAAA;EACA,WAAA;EACA,WAAA;EACA,yBDzEY;AD8GhB;AG9IE;EDkGA;IAUI,WAAA;EFsCJ;AACF;AE9FE;EA4DE,gBAAA;AFqCJ;AGtJE;EDqDA;IA8DI,eAAA;IACA,SAAA,EAAA,6BAAA;IACA,OAAA;IACA,QAAA;IACA,SAAA;IACA,aAAA;IACA,sBAAA;IACA,2BAAA;IACA,mBAAA;IACA,SAAA;IACA,iBAAA;IACA,sBDzFQ;IC0FR,aAAA;IACA,gBAAA;EFuCJ;AACF;AGxKE;EDmIE;IAEI,aAAA;EFuCN;AACF;AEnCE;EACE,aAAA;EACA,oBAAA;EACA,UAAA;AFqCJ;AGlLE;ED0IA;IAMI,eAAA;IACA,SAAA;IACA,OAAA;IACA,WAAA;IACA,YAAA;IACA,MAAA;IACA,2CAAA;IACA,sBDlHQ;ICmHR,6BAAA;IACA,cAAA;IAEA,8BAAA;IACA,UAAA;IACA,oBAAA;IACA,2BAAA;IACA,kDAAA;EFqCJ;EEnCI;IACE,UAAA;IACA,oBAAA;IACA,wBAAA;EFqCN;AACF;AEjCE;EACE,WAAA;EACA,aAAA;EACA,8BAAA;EACA,iCAAA;AFmCJ;AEjCI;EACE,yBD5IW;AD+KjB;AGnNE;EDyKA;IAWI,UAAA;EFmCJ;AACF;AEjCI;EACE,aAAA;EACA,mBAAA;EACA,uBAAA;EACA,WAAA;EACA,YAAA;AFmCN;AE/BE;;EAEE,WAAA;EACA,YAAA;AFiCJ;AEpOA;EAsME,gBAAA;AFiCF;AEhCA;EACE,aAAA;AFkCF;AG1OE;EDuMF;IAII,aAAA;IACA,uBAAA;IACA,mBAAA;IACA,WAAA,EAAA,aAAA;IACA,YAAA,EAAA,6BAAA;IACA,8BAAA;IACA,eAAA;IACA,kBAAA;IACA,cAAA;IAEA,oCAAA;EFkCF;EEjCE;IACE,aAAA;EFmCJ;EEhCE;IACE,WAAA;IACA,YAAA;IACA,wBAAA;IACA,aAAA;EFkCJ;EEzDF;IA0BI,wCAAA;EFkCF;EEhCI;IACE,YAAA;IACA,6BAAA;EFkCN;AACF;AE7BA;EACE,yCAAA;EACA,gCAAA;AF+BF","file":"Header.css"} \ No newline at end of file diff --git a/src/components/Header/Header.module.scss b/src/components/Header/Header.module.scss new file mode 100644 index 00000000000..ea6378af3c6 --- /dev/null +++ b/src/components/Header/Header.module.scss @@ -0,0 +1,209 @@ +@use 'sass:map'; +@import '../../variables'; +@import '../../mixins'; + +.header { + position: fixed; + top: 0; + left: 0; + width: 100%; + z-index: 10000; + border-bottom: 1px solid $color-elements; + background-color: $color-white; + + font-size: map.get($uppercase, font-size); + font-weight: map.get($uppercase, font-weight); + line-height: map.get($uppercase, line-height); + letter-spacing: map.get($uppercase, letter-spacing); + transition: box-shadow 0.3s ease; + + &.scrolled { + box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15); + border-bottom-color: transparent; + } + + .nav { + display: flex; + justify-content: space-between; + align-items: stretch; + width: 100%; + } + + .logo { + width: 80px; + padding: 18px 24px; + display: flex; + align-items: center; + + @include mobile { width: 64px; padding: 13px 16px; } + + @include tablet { width: 64px; padding: 13px 24px; } + } + + .navLeft { + display: flex; + align-items: center; + gap: 64px; + + @include mobile { + gap: 0; + width: 100%; + justify-content: space-between; + } + + @include tablet { gap: 16px; } + } + + .navLinks { + list-style: none; + display: flex; + gap: 64px; + + @include mobile { + position: fixed; + inset: 49px 0 0; + display: none; + flex-direction: column; + justify-content: flex-start; + align-items: center; + gap: 32px; + padding-top: 48px; + background-color: $color-white; + z-index: 9999; + overflow-y: auto; + + &.open { + display: flex; + } + } + + @include tablet { gap: 32px; } + + .navLink { + color: $color-secondary; + text-decoration: none; + text-transform: uppercase; + position: relative; + padding: 25px 0; + transition: color 0.3s; + + @include mobile { padding: 8px 0; } + + @include tablet { padding: 18px 0; } + + &:hover, + &.active { + color: $color-primary; + } + + &.active::after { + content: ''; + position: absolute; + bottom: 0; + left: 0; + width: 100%; + height: 3px; + background-color: $color-primary; + + @include mobile { height: 2px; } + } + } + } + + .buttonGroupHeader { + display: flex; + align-items: stretch; + + @include mobile { + position: fixed; + bottom: 0; + left: 0; + width: 100%; + height: 64px; + padding-bottom: env(safe-area-inset-bottom); + background-color: $color-white; + border-top: 1px solid $color-elements; + z-index: 10000; + + opacity: 0; + pointer-events: none; + transform: translateY(100%); + transition: transform 0.3s ease, opacity 0.3s ease; + + &.mobileVisible { + opacity: 1; + pointer-events: auto; + transform: translateY(0); + } + } + } + + .buttonGroupHeaderBlock { + width: 64px; + display: flex; + border-left: 1px solid $color-elements; + transition: background-color 0.3s; + position: relative; + + &:hover { + background-color: $color-hover-BG; + } + + @include mobile { + width: 50%; + } + + a { + display: flex; + align-items: center; + justify-content: center; + position: relative; + width: 100%; + height: 100%; + } + } + + .cart, .like { + width: 16px; + height: 16px; + } + + .burgerMenu { + display: none; + + @include mobile { + display: flex; + justify-content: center; + align-items: center; + width: 48px; + height: 100%; + border-left: 1px solid $color-elements; + cursor: pointer; + z-index: 10001; + } + } + + .badge { + position: absolute; + top: 15px; + right: 15px; + + background-color: $color-red; + color: $color-white; + font-size: 9px; + font-weight: 500; + line-height: 1; + + min-width: 14px; + height: 14px; + border-radius: 50px; + display: flex; + align-items: center; + justify-content: center; + padding-top: 1px; + + border: 1px solid $color-white; + pointer-events: none; + z-index: 10; + } +} diff --git a/src/components/Header/Header.tsx b/src/components/Header/Header.tsx new file mode 100644 index 00000000000..6d0b5da7d59 --- /dev/null +++ b/src/components/Header/Header.tsx @@ -0,0 +1,119 @@ +import React, { useEffect, useState } from 'react'; +import { NavLink } from 'react-router-dom'; +import { useAppSelector } from '../../app/hooks'; +import s from './Header.module.scss'; + +export const Header: React.FC = () => { + const [isScrolled, setIsScrolled] = useState(false); + const [isMenuOpen, setIsMenuOpen] = useState(false); + + useEffect(() => { + const handleScroll = () => { + setIsScrolled(window.scrollY > 20); + }; + + window.addEventListener('scroll', handleScroll); + + return () => window.removeEventListener('scroll', handleScroll); + }, []); + + useEffect(() => { + document.body.style.overflow = isMenuOpen ? 'hidden' : 'auto'; + }, [isMenuOpen]); + + const toggleMenu = () => setIsMenuOpen(!isMenuOpen); + const closeMenu = () => setIsMenuOpen(false); + + const getNavLinkClass = ({ isActive }: { isActive: boolean }) => + `${s.navLink} ${isActive ? s.active : ''}`; + + const cartItems = useAppSelector(state => state.cart.items); + const favoriteItems = useAppSelector(state => state.favorites.items); + + const cartCount = cartItems.reduce((total, item) => total + item.quantity, 0); + const favoritesCount = favoriteItems.length; + + return ( +
+ +
+ ); +}; diff --git a/src/components/Header/index.ts b/src/components/Header/index.ts new file mode 100644 index 00000000000..266dec8a1bc --- /dev/null +++ b/src/components/Header/index.ts @@ -0,0 +1 @@ +export * from './Header'; diff --git a/src/components/ProductCard/ProductCard.css b/src/components/ProductCard/ProductCard.css new file mode 100644 index 00000000000..0ec7e39acdd --- /dev/null +++ b/src/components/ProductCard/ProductCard.css @@ -0,0 +1,225 @@ +@font-face { + font-family: "Mont"; + src: url("/fonts/Mont-Regular.otf") format("opentype"); + font-weight: 400; + font-style: normal; +} +@font-face { + font-family: "Mont"; + src: url("/fonts/Mont-SemiBold.otf") format("opentype"); + font-weight: 600; + font-style: normal; +} +@font-face { + font-family: "Mont"; + src: url("/fonts/Mont-Bold.otf") format("opentype"); + font-weight: 700; + font-style: normal; +} +@font-face { + font-family: "Mont"; + src: url("/fonts/Mont-Regular.otf") format("opentype"); + font-weight: 400; + font-style: normal; +} +@font-face { + font-family: "Mont"; + src: url("/fonts/Mont-SemiBold.otf") format("opentype"); + font-weight: 600; + font-style: normal; +} +@font-face { + font-family: "Mont"; + src: url("/fonts/Mont-Bold.otf") format("opentype"); + font-weight: 700; + font-style: normal; +} +.product-card { + display: flex; + flex-direction: column; + border: 1px solid #E2E6E9; + overflow: hidden; + padding: 32px; + background-color: #FFF; +} +@media (min-width: 320px) and (max-width: 639px) { + .product-card { + width: 223px; + height: 375px; + } +} +@media (min-width: 640px) and (max-width: 1199px) { + .product-card { + width: 173px; + height: 448px; + } +} +.product-card::-webkit-scrollbar { + display: none; +} +.product-card .product-image { + width: 100%; + height: 200px; + -o-object-fit: contain; + object-fit: contain; + margin-bottom: 16px; + border-radius: 8px; +} +@media (min-width: 320px) and (max-width: 639px) { + .product-card .product-image { + max-height: 129px; + margin-bottom: 24px; + } +} +.product-card .product-name { + font-size: 14px; + font-weight: 400; + line-height: 20px; + letter-spacing: 0; + min-height: 40px; + margin-bottom: 8px; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; +} +.product-card .product-price { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 8px; + font-size: 22px; +} +.product-card .product-price .discount { + font-size: 22px; + font-weight: 700; + line-height: 31px; + letter-spacing: 0; + color: #313237; +} +.product-card .product-price .full-price { + font-size: 22px; + font-weight: 700; + line-height: 31px; + letter-spacing: 0; + color: #89939A; + text-decoration: line-through; +} +.product-card hr { + margin: 0; + border: none; + border-top: 1px solid #e2e6e9; +} +.product-card .product-description { + display: grid; + grid-template-columns: 1fr auto; + row-gap: 8px; + -moz-column-gap: 24px; + column-gap: 24px; + margin: 8px 0; +} +.product-card .product-description .characteristic-name { + font-size: 12px; + font-weight: 600; + line-height: 15px; + letter-spacing: 0; + color: #89939A; + padding-bottom: 8px; + text-align: left; +} +.product-card .product-description .characteristic-value { + font-size: 12px; + font-weight: 600; + line-height: 15px; + letter-spacing: 0; + color: #313237; + padding-bottom: 8px; + text-align: right; + justify-self: end; +} +.product-card .product-actions { + display: flex; + align-items: center; + gap: 8px; + height: 40px; +} +@media (min-width: 320px) and (max-width: 639px) { + .product-card .product-actions { + justify-content: center; + } +} +.product-card .product-actions .add-to-cart-button, +.product-card .product-actions .like-button { + height: 40px; + display: flex; + align-items: center; + justify-content: center; +} +.product-card .product-actions .add-to-cart-button { + width: 176px; + height: 40px; + padding: 0 40px; + background-color: #313237; + color: #FFF; + border: 1px solid #313237; + cursor: pointer; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + font-size: 14px; + font-weight: 600; + line-height: 21px; + letter-spacing: 0; + transition: 0.3s; +} +.product-card .product-actions .add-to-cart-button:hover { + box-shadow: 0px 3px 13px 0px rgba(23, 32, 49, 0.4); +} +@media (min-width: 320px) and (max-width: 639px) { + .product-card .product-actions .add-to-cart-button { + width: 148px; + } +} +.product-card .product-actions .add-to-cart-button.added-to-cart { + background-color: #FFF; + border-color: #E2E6E9; + color: #27AE60; +} +.product-card .product-actions .like-button { + width: 40px; + padding: 0; + border: 1px solid #B4BDC3; + background-color: #FFF; + cursor: pointer; + flex-shrink: 0; + display: flex; + align-items: center; + justify-content: center; + margin-left: auto; +} +@media (min-width: 320px) and (max-width: 639px) { + .product-card .product-actions .like-button { + margin-left: 0; + } +} +.product-card .product-actions .like-button { + transition: border-color 0.3s; +} +.product-card .product-actions .like-button img { + width: 16px; + height: 16px; + display: block; +} +.product-card .product-actions .like-button:hover { + border-color: #313237; +} +.product-card .product-actions .like-button.is-liked { + border-color: #E2E6E9; +} +.product-card:hover .product-image { + transition: opacity 0.3s ease; +} + +.product-image { + transition: opacity 0.3s ease; +}/*# sourceMappingURL=ProductCard.css.map */ \ No newline at end of file diff --git a/src/components/ProductCard/ProductCard.css.map b/src/components/ProductCard/ProductCard.css.map new file mode 100644 index 00000000000..52cfab5acd3 --- /dev/null +++ b/src/components/ProductCard/ProductCard.css.map @@ -0,0 +1 @@ +{"version":3,"sources":["../../variables.scss","ProductCard.css","ProductCard.scss","../../mixins.scss"],"names":[],"mappings":"AAUA;EACE,mBAAA;EACA,sDAAA;EACA,gBAAA;EACA,kBAAA;ACTF;ADYA;EACE,mBAAA;EACA,uDAAA;EACA,gBAAA;EACA,kBAAA;ACVF;ADaA;EACE,mBAAA;EACA,mDAAA;EACA,gBAAA;EACA,kBAAA;ACXF;ADPA;EACE,mBAAA;EACA,sDAAA;EACA,gBAAA;EACA,kBAAA;ACSF;ADNA;EACE,mBAAA;EACA,uDAAA;EACA,gBAAA;EACA,kBAAA;ACQF;ADLA;EACE,mBAAA;EACA,mDAAA;EACA,gBAAA;EACA,kBAAA;ACOF;AChCA;EACE,aAAA;EACA,sBAAA;EACA,yBAAA;EACA,gBAAA;EACA,aAAA;EACA,sBF+BY;ACGd;AExCE;EDAF;IAUI,YAAA;IACA,aAAA;EDkCF;AACF;AEzCE;EDLF;IAeI,YAAA;IACA,aAAA;EDmCF;AACF;ACjCE;EACE,aAAA;ADmCJ;AChCE;EACE,WAAA;EACA,aAAA;EACA,sBAAA;KAAA,mBAAA;EACA,mBAAA;EACA,kBAAA;ADkCJ;AE9DE;EDuBA;IAQI,iBAAA;IACA,mBAAA;EDmCJ;AACF;AChCE;EACA,eAAA;EACA,gBAAA;EACA,iBAAA;EACA,iBAAA;EAEA,gBAAA;EACA,kBAAA;EAEA,oBAAA;EACA,qBAAA;EACA,4BAAA;EACA,gBAAA;ADgCF;AC5BA;EACE,aAAA;EACA,mBAAA;EACA,QAAA;EACA,kBAAA;EACA,eAAA;AD8BF;AC5BE;EACE,eAAA;EACA,gBAAA;EACA,iBAAA;EACA,iBAAA;EACA,cFhCY;AC8DhB;AC3BE;EACE,eAAA;EACA,gBAAA;EACA,iBAAA;EACA,iBAAA;EACA,cFvCc;EEwCd,6BAAA;AD6BJ;ACvBE;EACE,SAAA;EACA,YAAA;EACA,6BAAA;ADyBJ;ACtBE;EACI,aAAA;EACA,+BAAA;EACA,YAAA;EACA,qBAAA;OAAA,gBAAA;EACA,aAAA;ADwBN;ACrBI;EACE,eAAA;EACA,gBAAA;EACA,iBAAA;EACA,iBAAA;EACA,cFjEY;EEkEZ,mBAAA;EACA,gBAAA;ADuBN;ACpBI;EACE,eAAA;EACA,gBAAA;EACA,iBAAA;EACA,iBAAA;EACA,cF5EU;EE6EV,mBAAA;EACA,iBAAA;EACA,iBAAA;ADsBN;AClBE;EACA,aAAA;EACA,mBAAA;EACA,QAAA;EACA,YAAA;ADoBF;AE3IE;EDmHA;IAOE,uBAAA;EDqBF;AACF;ACnBE;;EAEE,YAAA;EACA,aAAA;EACA,mBAAA;EACA,uBAAA;ADqBJ;ACjBI;EACA,YAAA;EACA,YAAA;EACA,eAAA;EACA,yBF1GY;EE2GZ,WFtGU;EEuGV,yBAAA;EACA,eAAA;EACC,mBAAA;EACC,gBAAA;EACA,uBAAA;EAEF,eAAA;EACA,gBAAA;EACA,iBAAA;EACA,iBAAA;EAEA,gBAAA;ADiBJ;ACfI;EACE,kDAAA;ADiBN;AE3KE;EDsIE;IAwBE,YAAA;EDiBJ;AACF;ACfK;EACC,sBF7HQ;EE8HR,qBFhIW;EEiIX,cF9HQ;AC+Id;ACZI;EACA,WAAA;EACA,UAAA;EACA,yBAAA;EACA,sBFxIU;EEyIV,eAAA;EACA,cAAA;EAEA,aAAA;EACA,mBAAA;EACA,uBAAA;EACA,iBAAA;ADaJ;AEjME;EDyKE;IAgBE,cAAA;EDYJ;AACF;AC7BI;EAmBA,6BAAA;ADaJ;ACXI;EACE,WAAA;EACA,YAAA;EACA,cAAA;ADaN;ACVI;EACE,qBFrKU;ACiLhB;ACTI;EACE,qBFtKW;ACiLjB;ACLI;EACE,6BAAA;ADON;;ACDA;EACE,6BAAA;ADIF","file":"ProductCard.css"} \ No newline at end of file diff --git a/src/components/ProductCard/ProductCard.module.scss b/src/components/ProductCard/ProductCard.module.scss new file mode 100644 index 00000000000..5e21bc79eaf --- /dev/null +++ b/src/components/ProductCard/ProductCard.module.scss @@ -0,0 +1,184 @@ +@import '../../variables'; +@import '../../mixins'; + +.productCard { + display: flex; + flex-direction: column; + border: 1px solid $color-elements; + overflow: hidden; + padding: 32px; + background-color: $color-white; + box-sizing: border-box; + width: 100%; + transition: box-shadow 0.3s ease; + + &:hover { + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1); + + .productImage { + opacity: 0.8; + } + } + + .productImageLink { + display: block; + text-decoration: none; + } + + .productImage { + width: 100%; + height: 200px; + object-fit: contain; + margin-bottom: 16px; + transition: opacity 0.3s ease; + + @include mobile { + max-height: 129px; + margin-bottom: 24px; + } + } + + .productNameLink { + text-decoration: none; + font-family: $font-main; + font-weight: 500; + color: $color-primary; + font-size: map-get($body-text, font-size); + line-height: map-get($body-text, line-height); + + min-height: 40px; + margin-bottom: 8px; + + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; + } + + .productPrice { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 8px; + + .discount { + font-size: map-get($h3-styles, font-size); + font-weight: map-get($h3-styles, font-weight); + color: $color-primary; + } + + .fullPrice { + font-size: map-get($h3-styles, font-size); + font-weight: map-get($h3-styles, font-weight); + color: $color-secondary; + text-decoration: line-through; + } + } + + .divider { + margin: 0; + border: none; + border-top: 1px solid $color-elements; + } + + .productDescription { + display: flex; + flex-direction: column; + gap: 8px; + margin: 8px 0; + + .characteristicRow { + display: grid; + grid-template-columns: 1fr auto; + gap: 16px; + align-items: baseline; + + .characteristicName { + font-size: map-get($small-text, font-size); + color: $color-secondary; + text-align: left; + line-height: 1.2; + } + + .characteristicValue { + font-size: map-get($small-text, font-size); + color: $color-primary; + text-align: right; + white-space: nowrap; + } + } +} + + .productActions { + display: flex; + align-items: center; + gap: 8px; + height: 40px; + width: 100%; + + @include mobile { + justify-content: center; + } + + .addToCartButton { + width: 80%; + height: 40px; + background-color: $color-primary; + color: $color-white; + border: 1px solid $color-primary; + font-family: $font-main; + font-weight: map-get($buttons, font-weight); + font-size: map-get($buttons, font-size); + cursor: pointer; + transition: 0.3s; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + + &:hover { + box-shadow: 0 3px 13px 0 #17203166; + } + + &.addedToCart { + background-color: $color-white; + border-color: $color-elements; + color: $color-green; + + &:hover { + box-shadow: none; + } + } + } + + .likeButton { + width: 20%; + height: 40px; + border: 1px solid $color-icons; + background-color: $color-white; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + margin-left: auto; + transition: border-color 0.3s; + + @include mobile { + margin-left: 0; + width: 25%; + } + + img { + width: 16px; + height: 16px; + } + + &:hover { + border-color: $color-primary; + } + + &.isLiked { + border-color: $color-elements; + } + } + } +} diff --git a/src/components/ProductCard/ProductCard.tsx b/src/components/ProductCard/ProductCard.tsx new file mode 100644 index 00000000000..60b623cdf7c --- /dev/null +++ b/src/components/ProductCard/ProductCard.tsx @@ -0,0 +1,155 @@ +import React from 'react'; +import { Link } from 'react-router-dom'; +import { + CatalogProduct, + Phone, + Tablet, + Accessory, +} from '../../../public/types'; +import { useAppDispatch, useAppSelector } from '../../app/hooks'; +import { + addToFavorites, + removeFromFavorites, +} from '../../features/favorites/favoritesSlice'; +import { addToCart } from '../../features/cart/cartSlice'; + +import s from './ProductCard.module.scss'; + +type ProductType = Phone | Tablet | Accessory; + +interface ProductCardProps { + product: CatalogProduct | ProductType; + showDiscount?: boolean; + onRemove?: () => void; +} + +const ProductCard: React.FC = ({ + product, + showDiscount = false, + onRemove, +}) => { + const dispatch = useAppDispatch(); + + const productId = (product as CatalogProduct).itemId || product.id; + + const favorites = useAppSelector(state => state.favorites.items); + const isLiked = favorites.some( + item => + item.itemId === productId || + item.id === productId || + (item as any).itemId === productId, + ); + + const cartItems = useAppSelector(state => state.cart.items); + const isInCart = cartItems.some(item => item.id === productId); + + const price = + 'priceDiscount' in product + ? product.priceDiscount + : (product as CatalogProduct).price; + + const fullPrice = + 'priceRegular' in product + ? product.priceRegular + : (product as CatalogProduct).fullPrice; + + const imagePath = + 'images' in product ? product.images[0] : (product as CatalogProduct).image; + + const handleLikeClick = () => { + if (isLiked) { + if (onRemove) { + onRemove(); + } else { + dispatch(removeFromFavorites(productId)); + } + } else { + dispatch(addToFavorites(product as CatalogProduct)); + } + }; + + const handleAddToCart = () => { + if (!isInCart) { + dispatch( + addToCart({ + id: productId, + name: product.name, + price: price, + image: imagePath, + }), + ); + } + }; + + return ( +
+ + {product.name} + + + +
{product.name}
+ + +
+ ${price} + {showDiscount && price !== fullPrice && ( + ${fullPrice} + )} +
+ +
+ +
+
+
Screen
+
{product.screen}
+
+ +
+
Capacity
+
{product.capacity}
+
+ +
+
RAM
+
{product.ram}
+
+
+ +
+ + + +
+
+ ); +}; + +export default ProductCard; diff --git a/src/components/ProductCard/index.ts b/src/components/ProductCard/index.ts new file mode 100644 index 00000000000..7ce031c3820 --- /dev/null +++ b/src/components/ProductCard/index.ts @@ -0,0 +1 @@ +export * from './ProductCard'; diff --git a/src/components/ScrollToTop/ScrollToTop.tsx b/src/components/ScrollToTop/ScrollToTop.tsx new file mode 100644 index 00000000000..d96a7389adb --- /dev/null +++ b/src/components/ScrollToTop/ScrollToTop.tsx @@ -0,0 +1,24 @@ +import { useLayoutEffect } from 'react'; +import { useLocation } from 'react-router-dom'; + +const ScrollToTop = () => { + const { pathname } = useLocation(); + + useLayoutEffect(() => { + const scrollTarget = () => { + window.scrollTo(0, 0); + document.body.scrollTo(0, 0); + document.documentElement.scrollTo(0, 0); + }; + + scrollTarget(); + + const timeoutId = setTimeout(scrollTarget, 0); + + return () => clearTimeout(timeoutId); + }, [pathname]); + + return null; +}; + +export default ScrollToTop; diff --git a/src/components/ScrollToTop/index.ts b/src/components/ScrollToTop/index.ts new file mode 100644 index 00000000000..230bfd29bd3 --- /dev/null +++ b/src/components/ScrollToTop/index.ts @@ -0,0 +1 @@ +export * from './ScrollToTop'; diff --git a/src/features/cart/cartSlice.ts b/src/features/cart/cartSlice.ts new file mode 100644 index 00000000000..383d3006ef8 --- /dev/null +++ b/src/features/cart/cartSlice.ts @@ -0,0 +1,89 @@ +import { createSlice, PayloadAction } from '@reduxjs/toolkit'; + +export interface CartItem { + id: string; + name: string; + price: number; + image: string; + quantity: number; +} + +export interface CartState { + items: CartItem[]; +} + +const loadCart = (): CartItem[] => { + try { + const saved = localStorage.getItem('cart'); + + return saved ? JSON.parse(saved) : []; + } catch (error) { + return []; + } +}; + +const initialState: CartState = { + items: loadCart(), +}; + +const cartSlice = createSlice({ + name: 'cart', + initialState, + reducers: { + addToCart: (state, action: PayloadAction>) => { + const existingItem = state.items.find( + item => item.id === action.payload.id, + ); + + if (!existingItem) { + const newItems = [...state.items, { ...action.payload, quantity: 1 }]; + + localStorage.setItem('cart', JSON.stringify(newItems)); + + return { ...state, items: newItems }; + } + }, + + removeFromCart: (state, action: PayloadAction) => { + const newItems = state.items.filter(item => item.id !== action.payload); + + localStorage.setItem('cart', JSON.stringify(newItems)); + + return { ...state, items: newItems }; + }, + + changeQuantity: ( + state, + action: PayloadAction<{ id: string; amount: number }>, + ) => { + const { id, amount } = action.payload; + + const newItems = state.items.map(item => { + if (item.id === id) { + const nextQuantity = item.quantity + amount; + + return { + ...item, + quantity: nextQuantity >= 1 ? nextQuantity : item.quantity, + }; + } + + return item; + }); + + localStorage.setItem('cart', JSON.stringify(newItems)); + + return { ...state, items: newItems }; + }, + + clearCart: state => { + localStorage.removeItem('cart'); + + return { ...state, items: [] }; + }, + }, +}); + +export const { addToCart, removeFromCart, changeQuantity, clearCart } = + cartSlice.actions; +export default cartSlice.reducer; diff --git a/src/features/favorites/favoritesSlice.ts b/src/features/favorites/favoritesSlice.ts new file mode 100644 index 00000000000..e0de0bf772b --- /dev/null +++ b/src/features/favorites/favoritesSlice.ts @@ -0,0 +1,50 @@ +import { createSlice, PayloadAction } from '@reduxjs/toolkit'; +import { CatalogProduct } from '../../../public/types'; + +export interface FavoritesState { + items: CatalogProduct[]; +} + +const loadFavorites = (): CatalogProduct[] => { + try { + const saved = localStorage.getItem('favorites'); + + return saved ? JSON.parse(saved) : []; + } catch (error) { + return []; + } +}; + +const initialState: FavoritesState = { + items: loadFavorites(), +}; + +const favoritesSlice = createSlice({ + name: 'favorites', + initialState, + reducers: { + addToFavorites: (state, action: PayloadAction) => { + const exists = state.items.find(item => item.id === action.payload.id); + + if (!exists) { + const newItems = [...state.items, action.payload]; + + localStorage.setItem('favorites', JSON.stringify(newItems)); + + return { ...state, items: newItems }; + } + + return state; + }, + removeFromFavorites: (state, action: PayloadAction) => { + const newItems = state.items.filter(item => item.id !== action.payload); + + localStorage.setItem('favorites', JSON.stringify(newItems)); + + return { ...state, items: newItems }; + }, + }, +}); + +export const { addToFavorites, removeFromFavorites } = favoritesSlice.actions; +export default favoritesSlice.reducer; diff --git a/src/features/products/productsSlice.ts b/src/features/products/productsSlice.ts new file mode 100644 index 00000000000..37411a21231 --- /dev/null +++ b/src/features/products/productsSlice.ts @@ -0,0 +1,63 @@ +import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit'; +import { CatalogProduct } from '../../../public/types'; + +export interface ProductsState { + items: CatalogProduct[]; + loading: boolean; + error: string | null; +} + +const initialState: ProductsState = { + items: [], + loading: false, + error: null, +}; + +export const fetchProducts = createAsyncThunk( + 'products/fetchProducts', + async (_, { rejectWithValue }) => { + try { + const response = await fetch('./api/products.json'); + + if (!response.ok) { + throw new Error(`Failed to fetch products: ${response.status}`); + } + + const data = await response.json(); + + return data as CatalogProduct[]; + } catch (error) { + if (error instanceof Error) { + return rejectWithValue(error.message); + } + + return rejectWithValue('An unknown error occurred'); + } + }, +); + +const productsSlice = createSlice({ + name: 'products', + initialState, + reducers: {}, + extraReducers: builder => { + builder + .addCase(fetchProducts.pending, state => { + state.loading = true; + state.error = null; + }) + .addCase( + fetchProducts.fulfilled, + (state, action: PayloadAction) => { + state.loading = false; + state.items = action.payload; + }, + ) + .addCase(fetchProducts.rejected, (state, action) => { + state.loading = false; + state.error = (action.payload as string) || 'Something went wrong'; + }); + }, +}); + +export default productsSlice.reducer; diff --git a/src/index.css b/src/index.css new file mode 100644 index 00000000000..e7a7674b7ba --- /dev/null +++ b/src/index.css @@ -0,0 +1,10 @@ +html { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +* { + margin: 0; + padding: 0; +}/*# sourceMappingURL=index.css.map */ \ No newline at end of file diff --git a/src/index.css.map b/src/index.css.map new file mode 100644 index 00000000000..a620fcfa711 --- /dev/null +++ b/src/index.css.map @@ -0,0 +1 @@ +{"version":3,"sources":["index.scss","index.css"],"names":[],"mappings":"AAAA;EACE,SAAA;EACA,UAAA;EACA,sBAAA;ACCF;;ADEA;EACE,SAAA;EACA,UAAA;ACCF","file":"index.css"} \ No newline at end of file diff --git a/src/index.tsx b/src/index.tsx index 50470f1508d..6f7766a103e 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,4 +1,18 @@ +import { StrictMode } from 'react'; import { createRoot } from 'react-dom/client'; -import { App } from './App'; +import { HashRouter as Router } from 'react-router-dom'; +import { Provider } from 'react-redux'; +import { store } from './app/store'; +import ScrollToTop from './components/ScrollToTop/ScrollToTop'; +import App from './App'; -createRoot(document.getElementById('root') as HTMLElement).render(); +createRoot(document.getElementById('root')!).render( + + + + + + + + , +); diff --git a/src/layout.css b/src/layout.css new file mode 100644 index 00000000000..a5799f5c3d1 --- /dev/null +++ b/src/layout.css @@ -0,0 +1,63 @@ +@font-face { + font-family: "Mont"; + src: url("/fonts/Mont-Regular.otf") format("opentype"); + font-weight: 400; + font-style: normal; +} +@font-face { + font-family: "Mont"; + src: url("/fonts/Mont-SemiBold.otf") format("opentype"); + font-weight: 600; + font-style: normal; +} +@font-face { + font-family: "Mont"; + src: url("/fonts/Mont-Bold.otf") format("opentype"); + font-weight: 700; + font-style: normal; +} +@font-face { + font-family: "Mont"; + src: url("/fonts/Mont-Regular.otf") format("opentype"); + font-weight: 400; + font-style: normal; +} +@font-face { + font-family: "Mont"; + src: url("/fonts/Mont-SemiBold.otf") format("opentype"); + font-weight: 600; + font-style: normal; +} +@font-face { + font-family: "Mont"; + src: url("/fonts/Mont-Bold.otf") format("opentype"); + font-weight: 700; + font-style: normal; +} +.layout { + display: grid; + width: 100%; + box-sizing: border-box; + max-width: 1200px; + margin: 0 auto; + padding: 64px 20px 0; + grid-template-columns: repeat(24, 1fr); + -moz-column-gap: 16px; + column-gap: 16px; + justify-content: center; + align-items: stretch; +} +@media (min-width: 640px) and (max-width: 1199px) { + .layout { + padding: 49px 16px 0; + max-width: 1199px; + grid-template-columns: repeat(12, 1fr); + } +} +@media (min-width: 320px) and (max-width: 639px) { + .layout { + padding: 48px 16px 0; + max-width: 639px; + grid-template-columns: repeat(4, 1fr); + } +}/*# sourceMappingURL=layout.css.map */ \ No newline at end of file diff --git a/src/layout.css.map b/src/layout.css.map new file mode 100644 index 00000000000..f3a8809c397 --- /dev/null +++ b/src/layout.css.map @@ -0,0 +1 @@ +{"version":3,"sources":["variables.scss","layout.css","layout.scss","mixins.scss"],"names":[],"mappings":"AAUA;EACE,mBAAA;EACA,sDAAA;EACA,gBAAA;EACA,kBAAA;ACTF;ADYA;EACE,mBAAA;EACA,uDAAA;EACA,gBAAA;EACA,kBAAA;ACVF;ADaA;EACE,mBAAA;EACA,mDAAA;EACA,gBAAA;EACA,kBAAA;ACXF;ADPA;EACE,mBAAA;EACA,sDAAA;EACA,gBAAA;EACA,kBAAA;ACSF;ADNA;EACE,mBAAA;EACA,uDAAA;EACA,gBAAA;EACA,kBAAA;ACQF;ADLA;EACE,mBAAA;EACA,mDAAA;EACA,gBAAA;EACA,kBAAA;ACOF;AChCA;EACE,aAAA;EACA,WAAA;EAEA,sBAAA;EACA,iBAAA;EACA,cAAA;EACA,oBAAA;EAGA,sCAAA;EACA,qBAAA;OAAA,gBAAA;EAGA,uBAAA;EACA,oBAAA;AD6BF;AEvCE;EDLF;IAkBI,oBAAA;IACA,iBAAA;IACA,sCAAA;ED8BF;AACF;AEnDE;EDAF;IAwBI,oBAAA;IACA,gBAAA;IACA,qCAAA;ED+BF;AACF","file":"layout.css"} \ No newline at end of file diff --git a/src/mixins.css b/src/mixins.css new file mode 100644 index 00000000000..b6b32720fa9 --- /dev/null +++ b/src/mixins.css @@ -0,0 +1,18 @@ +@font-face { + font-family: "Mont"; + src: url("/fonts/Mont-Regular.otf") format("opentype"); + font-weight: 400; + font-style: normal; +} +@font-face { + font-family: "Mont"; + src: url("/fonts/Mont-SemiBold.otf") format("opentype"); + font-weight: 600; + font-style: normal; +} +@font-face { + font-family: "Mont"; + src: url("/fonts/Mont-Bold.otf") format("opentype"); + font-weight: 700; + font-style: normal; +}/*# sourceMappingURL=mixins.css.map */ \ No newline at end of file diff --git a/src/mixins.css.map b/src/mixins.css.map new file mode 100644 index 00000000000..288afbe8293 --- /dev/null +++ b/src/mixins.css.map @@ -0,0 +1 @@ +{"version":3,"sources":["variables.scss","mixins.css"],"names":[],"mappings":"AAUA;EACE,mBAAA;EACA,sDAAA;EACA,gBAAA;EACA,kBAAA;ACTF;ADYA;EACE,mBAAA;EACA,uDAAA;EACA,gBAAA;EACA,kBAAA;ACVF;ADaA;EACE,mBAAA;EACA,mDAAA;EACA,gBAAA;EACA,kBAAA;ACXF","file":"mixins.css"} \ No newline at end of file diff --git a/src/mixins.scss b/src/mixins.scss new file mode 100644 index 00000000000..daebf2ee30c --- /dev/null +++ b/src/mixins.scss @@ -0,0 +1,19 @@ +@import 'variables'; + +@mixin mobile { + @media (min-width: $mobile-min) and (max-width: $mobile-max) { + @content; + } +} + +@mixin tablet { + @media (min-width: $tablet-min) and (max-width: $tablet-max) { + @content; + } +} + +@mixin desktop { + @media (min-width: $desktop-min) and (max-width: $desktop-max) { + @content; + } +} diff --git a/src/pages/CartPage/CartPage.css b/src/pages/CartPage/CartPage.css new file mode 100644 index 00000000000..f393427b11e --- /dev/null +++ b/src/pages/CartPage/CartPage.css @@ -0,0 +1,181 @@ +@font-face { + font-family: "Mont"; + src: url("/fonts/Mont-Regular.otf") format("opentype"); + font-weight: 400; + font-style: normal; +} +@font-face { + font-family: "Mont"; + src: url("/fonts/Mont-SemiBold.otf") format("opentype"); + font-weight: 600; + font-style: normal; +} +@font-face { + font-family: "Mont"; + src: url("/fonts/Mont-Bold.otf") format("opentype"); + font-weight: 700; + font-style: normal; +} +.cart-page { + grid-column: 1/-1; +} + +.cart-container { + display: grid; + grid-template-columns: 2fr 1fr; + align-items: start; + margin-bottom: 80px; +} + +.product-name { + font-size: 14px; + font-weight: 400; + line-height: 21px; + letter-spacing: 0; +} + +.quantity { + margin-left: 24px; + margin-right: 53px; +} + +.cart-items { + display: flex; + flex-direction: column; + gap: 16px; +} + +.price { + font-size: 22px; + font-weight: 700; + line-height: 31px; + letter-spacing: 0; +} + +.cart-title { + font-size: 48px; + font-weight: 700; + line-height: 56px; + letter-spacing: -0.01em; + margin-top: 16px; + margin-bottom: 32px; +} + +.cart-card { + display: flex; + flex-direction: row; + align-items: center; + gap: 16px; + padding: 24px; + border: 1px solid #e2e2e2; + max-height: 128px; +} + +.cart-card img { + width: 80px; + height: 80px; + -o-object-fit: contain; + object-fit: contain; +} + +.cart-info { + display: flex; + flex-direction: row; + align-items: center; +} + +.right-side { + display: flex; + flex-direction: row; + align-items: center; + margin-left: auto; + flex-shrink: 0; +} + +.cart-summary { + position: sticky; + top: 100px; + padding: 24px; + border: 1px solid #e2e2e2; + margin-left: 16px; + text-align: center; +} +.cart-summary .total-price { + font-size: 32px; + font-weight: 700; + line-height: 41px; + letter-spacing: -0.01em; +} +.cart-summary .summary-text { + font-size: 14px; + font-weight: 400; + line-height: 21px; + letter-spacing: 0; + color: #89939a; +} +.cart-summary .checkout-button { + width: 100%; + height: 40px; + padding: 0 40px; + background-color: #313237; + color: #FFF; + border: 1px solid #313237; + cursor: pointer; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + font-size: 14px; + font-weight: 600; + line-height: 21px; + letter-spacing: 0; + margin: 0px; + transition: 0.3s; +} +.cart-summary .checkout-button:hover { + box-shadow: 0px 3px 13px 0px rgba(23, 32, 49, 0.4); +} +.cart-summary hr { + margin: 24px 0; + border: none; + border-top: 1px solid #e2e6e9; + width: 100%; +} + +.checkout-button { + width: 100%; + margin-top: 16px; +} + +.remove { + background: none; + border: none; + cursor: pointer; +} +.remove .remove-icon { + width: 16px; + height: 16px; +} + +.minus { + background: none; + border: 1px solid #313237; + cursor: pointer; + padding: 8px; + margin-right: 14px; +} +.minus .minus-icon { + width: 16px; + height: 16px; +} + +.plus { + background: none; + border: 1px solid #313237; + cursor: pointer; + padding: 8px; + margin-left: 14px; +} +.plus .plus-icon { + width: 16px; + height: 16px; +}/*# sourceMappingURL=CartPage.css.map */ \ No newline at end of file diff --git a/src/pages/CartPage/CartPage.css.map b/src/pages/CartPage/CartPage.css.map new file mode 100644 index 00000000000..8212843aaa2 --- /dev/null +++ b/src/pages/CartPage/CartPage.css.map @@ -0,0 +1 @@ +{"version":3,"sources":["../../variables.scss","CartPage.css","CartPage.scss"],"names":[],"mappings":"AAUA;EACE,mBAAA;EACA,sDAAA;EACA,gBAAA;EACA,kBAAA;ACTF;ADYA;EACE,mBAAA;EACA,uDAAA;EACA,gBAAA;EACA,kBAAA;ACVF;ADaA;EACE,mBAAA;EACA,mDAAA;EACA,gBAAA;EACA,kBAAA;ACXF;ACfA;EACE,iBAAA;ADiBF;;ACdA;EACE,aAAA;EACA,8BAAA;EACA,kBAAA;EACA,mBAAA;ADiBF;;ACdA;EACE,eAAA;EACA,gBAAA;EACA,iBAAA;EACA,iBAAA;ADiBF;;ACdA;EACE,iBAAA;EACA,kBAAA;ADiBF;;ACdA;EACE,aAAA;EACA,sBAAA;EACA,SAAA;ADiBF;;ACdA;EACE,eAAA;EACA,gBAAA;EACA,iBAAA;EACA,iBAAA;ADiBF;;ACdA;EACE,eAAA;EACA,gBAAA;EACA,iBAAA;EACA,uBAAA;EAEA,gBAAA;EACA,mBAAA;ADgBF;;ACbA;EACE,aAAA;EACA,mBAAA;EACA,mBAAA;EACA,SAAA;EACA,aAAA;EACA,yBAAA;EACA,iBAAA;ADgBF;;ACbA;EACE,WAAA;EACA,YAAA;EACA,sBAAA;KAAA,mBAAA;ADgBF;;ACbA;EACE,aAAA;EACA,mBAAA;EACA,mBAAA;ADgBF;;ACbA;EACE,aAAA;EACA,mBAAA;EACA,mBAAA;EACA,iBAAA;EACA,cAAA;ADgBF;;ACbA;EACE,gBAAA;EACA,UAAA;EACA,aAAA;EACA,yBAAA;EACA,iBAAA;EACA,kBAAA;ADgBF;ACdE;EACE,eAAA;EACA,gBAAA;EACA,iBAAA;EACA,uBAAA;ADgBJ;ACbE;EACE,eAAA;EACA,gBAAA;EACA,iBAAA;EACA,iBAAA;EACA,cAAA;ADeJ;ACXE;EACE,WAAA;EACA,YAAA;EACA,eAAA;EACA,yBFvEY;EEwEZ,WFnEU;EEoEV,yBAAA;EACA,eAAA;EACC,mBAAA;EACC,gBAAA;EACA,uBAAA;EAEF,eAAA;EACA,gBAAA;EACA,iBAAA;EACA,iBAAA;EAEA,WAAA;EAEA,gBAAA;ADUJ;ACRI;EACE,kDAAA;ADUN;ACLE;EACE,cAAA;EACA,YAAA;EACA,6BAAA;EACA,WAAA;ADOJ;;ACFA;EACE,WAAA;EACA,gBAAA;ADKF;;ACFA;EACE,gBAAA;EACA,YAAA;EACA,eAAA;ADKF;ACHE;EACA,WAAA;EACA,YAAA;ADKF;;ACDA;EACE,gBAAA;EACA,yBAAA;EACA,eAAA;EACA,YAAA;EACA,kBAAA;ADIF;ACDE;EACE,WAAA;EACA,YAAA;ADGJ;;ACCA;EACE,gBAAA;EACA,yBAAA;EACA,eAAA;EACA,YAAA;EACA,iBAAA;ADEF;ACCE;EACE,WAAA;EACA,YAAA;ADCJ","file":"CartPage.css"} \ No newline at end of file diff --git a/src/pages/CartPage/CartPage.module.scss b/src/pages/CartPage/CartPage.module.scss new file mode 100644 index 00000000000..d5fd5dcac34 --- /dev/null +++ b/src/pages/CartPage/CartPage.module.scss @@ -0,0 +1,222 @@ +@import '../../variables'; +@import '../../mixins'; + +.cartPage { + grid-column: 1 / -1; + padding-bottom: 80px; + + @include mobile { padding-bottom: 56px; } +} + +.backButton { + margin-top: 40px; + display: inline-flex; + align-items: center; + gap: 8px; + cursor: pointer; + color: $color-secondary; + font-size: 14px; + transition: color 0.2s; + + &:hover { color: $color-primary; } + + @include mobile { margin-top: 24px; } + + .backArrow { + width: 16px; + height: 16px; + } +} + +.cartTitle { + font-size: map-get($h1-styles, font-size); + font-weight: map-get($h1-styles, font-weight); + color: $color-primary; + margin-top: 16px; + margin-bottom: 32px; + + @include mobile { + margin-top: 24px; + font-size: map-get($h1-styles-mobile, font-size); + } +} + +.cartContainer { + display: grid; + grid-template-columns: 2fr 1fr; + align-items: start; + gap: 16px; + + @include tablet { grid-template-columns: 1fr; gap: 32px; } + + @include mobile { grid-template-columns: 1fr; gap: 32px; } +} + +.cartItems { + display: flex; + flex-direction: column; + gap: 16px; +} + +.cartCard { + display: flex; + flex-direction: row; + align-items: center; + gap: 16px; + padding: 24px; + border: 1px solid $color-elements; + background-color: $color-white; + + @include mobile { + display: grid; + grid-template-columns: 40px 80px 1fr; + grid-template-rows: auto auto; + padding: 16px; + gap: 16px; + } + + .remove { + background: none; + border: none; + cursor: pointer; + padding: 0; + + @include mobile { grid-column: 1; grid-row: 1; } + + .removeIcon { + width: 16px; + height: 16px; + } + } + + .cartCardImageContainer { + width: 80px; + height: 80px; + flex-shrink: 0; + + @include mobile { grid-column: 2; grid-row: 1; } + + img { + width: 100%; + height: 100%; + object-fit: contain; + } + } + + .productName { + flex: 1; + min-width: 0; + color: $color-primary; + font-size: map-get($body-text, font-size); + font-weight: map-get($body-text, font-weight); + text-decoration: none; + + @include mobile { + grid-column: 3; + grid-row: 1; + font-size: 14px; + } + } + + .rightSide { + display: flex; + align-items: center; + margin-left: auto; + + @include mobile { + grid-column: 1 / -1; + grid-row: 2; + width: 100%; + margin-left: 0; + justify-content: space-between; + } + } +} + +.quantity { + display: flex; + align-items: center; + gap: 12px; + margin-right: 24px; + + .minus, .plus { + display: flex; + align-items: center; + justify-content: center; + width: 32px; + height: 32px; + background: none; + border: 1px solid $color-icons; + cursor: pointer; + transition: border-color 0.2s; + + &:hover:not(:disabled) { border-color: $color-primary; } + &:disabled { opacity: 0.3; cursor: not-allowed; } + + img { width: 16px; height: 16px; } + } + + .quantityValue { + width: 32px; + text-align: center; + color: $color-primary; + } +} + +.price { + font-size: map-get($h3-styles, font-size); + font-weight: map-get($h3-styles, font-weight); + color: $color-primary; + width: 100px; + text-align: right; +} + +.cartSummary { + position: sticky; + top: 100px; + padding: 24px; + border: 1px solid $color-elements; + text-align: center; + background-color: $color-white; + + .totalPrice { + font-size: map-get($h2-styles, font-size); + font-weight: map-get($h2-styles, font-weight); + color: $color-primary; + } + + .summaryText { + font-size: 14px; + color: $color-secondary; + margin-top: 8px; + } + + .divider { + margin: 24px 0; + border: none; + border-top: 1px solid $color-elements; + } + + .checkoutButton { + width: 100%; + height: 48px; + background-color: $color-primary; + color: $color-white; + border: none; + font-weight: 500; + font-family: $font-main; + cursor: pointer; + transition: 0.3s; + + &:hover { + box-shadow: 0 3px 13px 0 rgba(23, 32, 49, 0.4); + } + } +} + +.cartEmpty { + text-align: center; + margin-top: 64px; + font-size: 20px; + color: $color-secondary; +} diff --git a/src/pages/CartPage/CartPage.tsx b/src/pages/CartPage/CartPage.tsx new file mode 100644 index 00000000000..642f91b7062 --- /dev/null +++ b/src/pages/CartPage/CartPage.tsx @@ -0,0 +1,149 @@ +import React, { useState } from 'react'; +import { useNavigate, Link } from 'react-router-dom'; +import { useAppDispatch, useAppSelector } from '../../app/hooks'; +import { + removeFromCart, + changeQuantity, + clearCart, +} from '../../features/cart/cartSlice'; + +import { ConfirmationModalFavorites } from '../../components/ConfirmationModalFavorites/ConfirmationModalFavorites'; + +import s from './CartPage.module.scss'; + +export const CartPage = () => { + const navigate = useNavigate(); + const dispatch = useAppDispatch(); + + const [isCheckoutModalOpen, setIsCheckoutModalOpen] = useState(false); + const [itemToDelete, setItemToDelete] = useState(null); + + const cart = useAppSelector(state => state.cart.items); + + const total = cart.reduce((sum, item) => sum + item.price * item.quantity, 0); + const totalQuantity = cart.reduce((sum, item) => sum + item.quantity, 0); + + const handleCheckout = () => setIsCheckoutModalOpen(true); + + const confirmCheckout = () => { + dispatch(clearCart()); + setIsCheckoutModalOpen(false); + }; + + const handleRemoveClick = (id: string) => setItemToDelete(id); + + const confirmDelete = () => { + if (itemToDelete) { + dispatch(removeFromCart(itemToDelete)); + setItemToDelete(null); + } + }; + + return ( +
+ {/* Кнопка назад */} +
navigate(-1)}> + Back + Back +
+ +

Cart

+ + {cart.length === 0 ? ( +
Your cart is empty
+ ) : ( +
+
+ {cart.map(item => { + + const category = (item as any).category || + (item.id.includes('ipad') ? 'tablets' : + item.id.includes('watch') ? 'accessories' : 'phones'); + + const productPath = `/${category}/${item.id}`; + + return ( +
+ + + + {item.name} + + + + {item.name} + + +
+
+ + + {item.quantity} + + +
+ +
${item.price * item.quantity}
+
+
+ ); + })} +
+ +
+
${total}
+
+ Total for {totalQuantity} {totalQuantity === 1 ? 'item' : 'items'} +
+
+ +
+
+ )} + + setIsCheckoutModalOpen(false)} + /> + + setItemToDelete(null)} + /> +
+ ); +}; diff --git a/src/pages/CartPage/index.ts b/src/pages/CartPage/index.ts new file mode 100644 index 00000000000..90c010237a0 --- /dev/null +++ b/src/pages/CartPage/index.ts @@ -0,0 +1 @@ +export * from './CartPage'; diff --git a/src/pages/FavoritesPage/FavoritesPage.css b/src/pages/FavoritesPage/FavoritesPage.css new file mode 100644 index 00000000000..996be9e8b74 --- /dev/null +++ b/src/pages/FavoritesPage/FavoritesPage.css @@ -0,0 +1,80 @@ +@font-face { + font-family: "Mont"; + src: url("/fonts/Mont-Regular.otf") format("opentype"); + font-weight: 400; + font-style: normal; +} +@font-face { + font-family: "Mont"; + src: url("/fonts/Mont-SemiBold.otf") format("opentype"); + font-weight: 600; + font-style: normal; +} +@font-face { + font-family: "Mont"; + src: url("/fonts/Mont-Bold.otf") format("opentype"); + font-weight: 700; + font-style: normal; +} +@font-face { + font-family: "Mont"; + src: url("/fonts/Mont-Regular.otf") format("opentype"); + font-weight: 400; + font-style: normal; +} +@font-face { + font-family: "Mont"; + src: url("/fonts/Mont-SemiBold.otf") format("opentype"); + font-weight: 600; + font-style: normal; +} +@font-face { + font-family: "Mont"; + src: url("/fonts/Mont-Bold.otf") format("opentype"); + font-weight: 700; + font-style: normal; +} +.favorites-page { + grid-column: 1/-1; +} +.favorites-page .breadcrumbs { + grid-column: 1/-1; + margin-top: 24px; +} +.favorites-page__header { + grid-column: 1/-1; + margin-top: 24px; + margin-bottom: 24px; +} +.favorites-page__header h1 { + margin-bottom: 8px; +} +.favorites-page__count { + color: #89939A; +} +.favorites-page__empty { + grid-column: 1/-1; + padding: 60px 0; + text-align: center; + color: #89939A; +} +.favorites-page .products-grid { + grid-column: 1/-1; + display: grid; + gap: 40px 16px; + margin-bottom: 80px; + grid-template-columns: repeat(4, 1fr); +} +@media (min-width: 640px) and (max-width: 1199px) { + .favorites-page .products-grid { + grid-template-columns: repeat(3, 1fr); + gap: 32px 16px; + } +} +@media (min-width: 320px) and (max-width: 639px) { + .favorites-page .products-grid { + grid-template-columns: repeat(2, 1fr); + gap: 16px; + margin-bottom: 56px; + } +}/*# sourceMappingURL=FavoritesPage.css.map */ \ No newline at end of file diff --git a/src/pages/FavoritesPage/FavoritesPage.css.map b/src/pages/FavoritesPage/FavoritesPage.css.map new file mode 100644 index 00000000000..2cc07b07d0e --- /dev/null +++ b/src/pages/FavoritesPage/FavoritesPage.css.map @@ -0,0 +1 @@ +{"version":3,"sources":["../../variables.scss","FavoritesPage.css","FavoritesPage.scss","../../mixins.scss"],"names":[],"mappings":"AAUA;EACE,mBAAA;EACA,sDAAA;EACA,gBAAA;EACA,kBAAA;ACTF;ADYA;EACE,mBAAA;EACA,uDAAA;EACA,gBAAA;EACA,kBAAA;ACVF;ADaA;EACE,mBAAA;EACA,mDAAA;EACA,gBAAA;EACA,kBAAA;ACXF;ADPA;EACE,mBAAA;EACA,sDAAA;EACA,gBAAA;EACA,kBAAA;ACSF;ADNA;EACE,mBAAA;EACA,uDAAA;EACA,gBAAA;EACA,kBAAA;ACQF;ADLA;EACE,mBAAA;EACA,mDAAA;EACA,gBAAA;EACA,kBAAA;ACOF;AChCA;EACG,iBAAA;ADkCH;AC/BE;EACE,iBAAA;EACA,gBAAA;ADiCJ;AC7BE;EACE,iBAAA;EACA,gBAAA;EACA,mBAAA;AD+BJ;AC7BI;EACE,kBAAA;AD+BN;AC3BE;EACE,cFYc;ACiBlB;ACzBE;EACE,iBAAA;EACA,eAAA;EACA,kBAAA;EACA,cFIc;ACuBlB;ACvBE;EACE,iBAAA;EAGA,aAAA;EACA,cAAA;EACA,mBAAA;EAGA,qCAAA;ADqBJ;AE1DE;ED4BA;IAYI,qCAAA;IACA,cAAA;EDsBJ;AACF;AErEE;EDiCA;IAiBI,qCAAA;IACA,SAAA;IACA,mBAAA;EDuBJ;AACF","file":"FavoritesPage.css"} \ No newline at end of file diff --git a/src/pages/FavoritesPage/FavoritesPage.module.scss b/src/pages/FavoritesPage/FavoritesPage.module.scss new file mode 100644 index 00000000000..0b741ef62e4 --- /dev/null +++ b/src/pages/FavoritesPage/FavoritesPage.module.scss @@ -0,0 +1,60 @@ +@import '../../variables'; +@import '../../mixins'; + +.favoritesPage { + grid-column: 1 / -1; + padding-bottom: 80px; + + @include mobile { + padding-bottom: 56px; + } +} + +.favoritesHeader { + grid-column: 1 / -1; + margin-top: 24px; + margin-bottom: 24px; + + h1 { + margin-bottom: 8px; + font-size: map-get($h1-styles, font-size); + color: $color-primary; + + @include mobile { + font-size: map-get($h1-styles-mobile, font-size); + } + } +} + +.favoritesCount { + color: $color-secondary; + font-size: 14px; +} + +.favoritesEmpty { + grid-column: 1 / -1; + padding: 60px 0; + text-align: center; + color: $color-secondary; + font-size: 18px; +} + + +.productsGrid { + grid-column: 1 / -1; + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 40px 16px; + margin-bottom: 80px; + + @include tablet { + grid-template-columns: repeat(2, 1fr); + gap: 32px 16px; + } + + @include mobile { + grid-template-columns: 1fr; + justify-items: center; + gap: 40px 16px; + } +} diff --git a/src/pages/FavoritesPage/FavoritesPage.tsx b/src/pages/FavoritesPage/FavoritesPage.tsx new file mode 100644 index 00000000000..523e92b34ba --- /dev/null +++ b/src/pages/FavoritesPage/FavoritesPage.tsx @@ -0,0 +1,58 @@ +import { useState } from 'react'; +import { useAppSelector, useAppDispatch } from '../../app/hooks'; +import { removeFromFavorites } from '../../features/favorites/favoritesSlice'; +import ProductCard from '../../components/ProductCard/ProductCard'; +import Breadcrumbs from '../../components/Breadcrumbs/Breadcrumbs'; +// eslint-disable-next-line max-len +import { ConfirmationModalFavorites } from '../../components/ConfirmationModalFavorites/ConfirmationModalFavorites'; + +import s from './FavoritesPage.module.scss'; + +export function FavoritesPage() { + const dispatch = useAppDispatch(); + const favorites = useAppSelector(state => state.favorites.items); + const [idToDelete, setIdToDelete] = useState(null); + + const confirmDelete = () => { + if (idToDelete) { + dispatch(removeFromFavorites(idToDelete)); + setIdToDelete(null); + } + }; + + return ( +
+ + +
+

Favorites

+

{favorites.length} items

+
+ + {favorites.length === 0 ? ( +
+

Your favorites list is empty

+
+ ) : ( +
+ {favorites.map(product => ( + setIdToDelete(product.id)} + /> + ))} +
+ )} + + setIdToDelete(null)} + /> +
+ ); +} + +export default FavoritesPage; diff --git a/src/pages/FavoritesPage/index.ts b/src/pages/FavoritesPage/index.ts new file mode 100644 index 00000000000..b3a884b1889 --- /dev/null +++ b/src/pages/FavoritesPage/index.ts @@ -0,0 +1 @@ +export * from './FavoritesPage'; diff --git a/src/pages/Home/Home.css b/src/pages/Home/Home.css new file mode 100644 index 00000000000..30b352258bd --- /dev/null +++ b/src/pages/Home/Home.css @@ -0,0 +1,310 @@ +@font-face { + font-family: "Mont"; + src: url("/fonts/Mont-Regular.otf") format("opentype"); + font-weight: 400; + font-style: normal; +} +@font-face { + font-family: "Mont"; + src: url("/fonts/Mont-SemiBold.otf") format("opentype"); + font-weight: 600; + font-style: normal; +} +@font-face { + font-family: "Mont"; + src: url("/fonts/Mont-Bold.otf") format("opentype"); + font-weight: 700; + font-style: normal; +} +@font-face { + font-family: "Mont"; + src: url("/fonts/Mont-Regular.otf") format("opentype"); + font-weight: 400; + font-style: normal; +} +@font-face { + font-family: "Mont"; + src: url("/fonts/Mont-SemiBold.otf") format("opentype"); + font-weight: 600; + font-style: normal; +} +@font-face { + font-family: "Mont"; + src: url("/fonts/Mont-Bold.otf") format("opentype"); + font-weight: 700; + font-style: normal; +} +.home { + grid-column: 1/-1; + display: contents; +} +.home__title { + grid-column: 1/-1; + margin-top: 56px; + color: #313237; + font-size: 48px; + font-weight: 700; + line-height: 56px; + letter-spacing: -0.01em; +} +@media (min-width: 640px) and (max-width: 1199px) { + .home__title { + margin-top: 32px; + max-width: 20ch; + } +} +@media (min-width: 320px) and (max-width: 639px) { + .home__title { + margin-top: 24px; + font-size: 32px; + } +} + +section { + grid-column: 1/-1; + margin: 0; +} + +h2 { + grid-column: 1/-1; + color: #313237; + font-size: 32px; + font-weight: 700; + line-height: 41px; + margin: 0; +} +@media (min-width: 320px) and (max-width: 639px) { + h2 { + font-size: 22px; + } +} + +.banner { + grid-column: 1/-1; + width: 100%; + display: flex; + flex-direction: column; + align-items: center; +} +.banner .carousel-wrapper { + width: 100%; + display: flex; + align-items: center; + justify-content: space-between; + margin-top: 56px; + gap: 16px; +} +@media (min-width: 640px) and (max-width: 1199px) { + .banner .carousel-wrapper { + margin-top: 32px; + gap: 19px; + } +} +@media (min-width: 320px) and (max-width: 639px) { + .banner .carousel-wrapper { + margin-top: 24px; + gap: 0; + } +} +.banner .banner-home { + flex: 1; + width: 100%; + height: 400px; + position: relative; + overflow: hidden; + background-color: #E2E6E9; +} +.banner .banner-home picture, .banner .banner-home img { + display: block; + width: 100%; + height: 100%; + -o-object-fit: cover; + object-fit: cover; +} +@media (min-width: 640px) and (max-width: 1199px) { + .banner .banner-home { + height: 300px; + } +} +@media (min-width: 320px) and (max-width: 639px) { + .banner .banner-home { + height: auto; + aspect-ratio: 1/1; + width: 100vw; + position: relative; + border-radius: 0; + margin-left: calc(50% - 50vw); + margin-right: calc(50% - 50vw); + justify-content: center; + } +} +.banner .carousel-arrow { + flex-shrink: 0; + width: 32px; + height: 400px; + display: flex; + align-items: center; + justify-content: center; + background-color: #FFF; + border: 1px solid #B4BDC3; + cursor: pointer; + z-index: 2; +} +@media (min-width: 640px) and (max-width: 1199px) { + .banner .carousel-arrow { + height: 300px; + } +} +@media (min-width: 320px) and (max-width: 639px) { + .banner .carousel-arrow { + display: none; + } +} +.banner .carousel-arrow:hover:not(:disabled) { + border-color: #313237; +} +.banner .carousel-arrow:disabled { + opacity: 0.4; +} +.banner .carousel-arrow:disabled img { + filter: grayscale(100%); +} + +.carousel-dots { + display: flex; + justify-content: center; + gap: 4px; + margin-top: 16px; +} +.carousel-dots .dot { + cursor: pointer; + line-height: 0; +} +.carousel-dots .dot img { + width: 24px; + height: 24px; +} + +.brand-new-models { + margin-top: 80px; +} +@media (min-width: 320px) and (max-width: 639px) { + .brand-new-models { + margin-top: 56px; + } +} + +.hot-prices { + margin-top: 80px; + margin-bottom: 80px; +} +@media (min-width: 320px) and (max-width: 639px) { + .hot-prices { + margin-top: 56px; + margin-bottom: 64px; + } +} + +.container-products, .container-divider { + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; +} + +.products-grid { + margin-top: 24px; + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 16px; +} +@media (min-width: 320px) and (max-width: 639px) { + .products-grid { + display: flex; + overflow-x: auto; + margin-left: -16px; + margin-right: -16px; + padding: 0 16px 16px; + scroll-snap-type: x mandatory; + } + .products-grid::-webkit-scrollbar { + display: none; + } + .products-grid > * { + flex: 0 0 280px; + scroll-snap-align: start; + } +} + +.categories { + margin-top: 80px; +} +@media (min-width: 320px) and (max-width: 639px) { + .categories { + margin-top: 56px; + } +} +.categories .categories__container { + margin-top: 24px; + display: flex; + gap: 16px; +} +@media (min-width: 320px) and (max-width: 639px) { + .categories .categories__container { + flex-direction: column; + gap: 32px; + } +} +.categories .categories__container .categories__category { + flex: 1; + display: flex; + flex-direction: column; + text-decoration: none; + color: inherit; +} +.categories .categories__container .categories__category-img-container { + width: 100%; + aspect-ratio: 1/1; + overflow: hidden; + background-color: #E2E6E9; + cursor: pointer; +} +.categories .categories__container .categories__category img { + width: 100%; + height: 100%; + -o-object-fit: cover; + object-fit: cover; + transition: transform 0.3s ease; + display: block; +} +.categories .categories__container .categories__category img:hover { + transform: scale(1.1); +} +.categories .category__name { + margin-top: 24px; + color: #313237; + font-size: 20px; + font-weight: 600; +} +.categories .number-of-products { + color: #89939A; + margin-top: 4px; + font-size: 14px; +} + +.carousel-buttons { + display: flex; + gap: 16px; +} +.carousel-buttons .carousel-arrow { + width: 32px; + height: 32px; + display: flex; + align-items: center; + justify-content: center; + border: 1px solid #B4BDC3; + background: #FFF; +} +.carousel-buttons .carousel-arrow:disabled { + opacity: 0.4; +}/*# sourceMappingURL=Home.css.map */ \ No newline at end of file diff --git a/src/pages/Home/Home.css.map b/src/pages/Home/Home.css.map new file mode 100644 index 00000000000..f198288c92f --- /dev/null +++ b/src/pages/Home/Home.css.map @@ -0,0 +1 @@ +{"version":3,"sources":["../../variables.scss","Home.css","Home.scss","../../mixins.scss"],"names":[],"mappings":"AAUA;EACE,mBAAA;EACA,sDAAA;EACA,gBAAA;EACA,kBAAA;ACTF;ADYA;EACE,mBAAA;EACA,uDAAA;EACA,gBAAA;EACA,kBAAA;ACVF;ADaA;EACE,mBAAA;EACA,mDAAA;EACA,gBAAA;EACA,kBAAA;ACXF;ADPA;EACE,mBAAA;EACA,sDAAA;EACA,gBAAA;EACA,kBAAA;ACSF;ADNA;EACE,mBAAA;EACA,uDAAA;EACA,gBAAA;EACA,kBAAA;ACQF;ADLA;EACE,mBAAA;EACA,mDAAA;EACA,gBAAA;EACA,kBAAA;ACOF;AC/BA;EACE,iBAAA;EACA,iBAAA;ADiCF;AC7BA;EACE,iBAAA;EACA,gBAAA;EACA,cFsBc;EEpBd,eAAA;EACA,gBAAA;EACA,iBAAA;EACA,uBAAA;AD8BF;AExCE;EDEF;IAUoB,gBAAA;IAAkB,eAAA;EDiCpC;AACF;AEnDE;EDOF;IAYI,gBAAA;IACA,eAAA;EDoCF;AACF;;AChCA;EACE,iBAAA;EACA,SAAA;ADmCF;;AC/BA;EACI,iBAAA;EACA,cFDY;EEEZ,eAAA;EACA,gBAAA;EACA,iBAAA;EACA,SAAA;ADkCJ;AEvEE;ED+BF;IASM,eAAA;EDmCJ;AACF;;AC/BA;EACE,iBAAA;EACA,WAAA;EACA,aAAA;EACA,sBAAA;EACA,mBAAA;ADkCF;AC/BE;EACE,WAAA;EACA,aAAA;EACA,mBAAA;EACA,8BAAA;EACA,gBAAA;EACA,SAAA;ADiCJ;AEvFE;EDgDA;IAQoB,gBAAA;IAAkB,SAAA;EDoCtC;AACF;AElGE;EDqDA;IASoB,gBAAA;IAAkB,MAAA;EDyCtC;AACF;ACvCE;EACE,OAAA;EAEA,WAAA;EACA,aAAA;EACA,kBAAA;EACA,gBAAA;EACA,yBFrCa;AC6EjB;ACtCI;EACE,cAAA;EACA,WAAA;EACA,YAAA;EACA,oBAAA;KAAA,iBAAA;ADwCN;AEjHE;ED4DA;IAgBoB,aAAA;EDyCpB;AACF;AE3HE;EDiEA;IAmBI,YAAA;IACA,iBAAA;IAGA,YAAA;IACA,kBAAA;IACA,gBAAA;IACA,6BAAA;IACA,8BAAA;IACA,uBAAA;EDyCJ;AACF;ACtCE;EACE,cAAA;EACA,WAAA;EACA,aAAA;EACA,aAAA;EACA,mBAAA;EACA,uBAAA;EACA,sBFnEU;EEoEV,yBAAA;EACA,eAAA;EACA,UAAA;ADwCJ;AE9IE;ED4FA;IAYoB,aAAA;ED0CpB;AACF;AExJE;EDiGA;IAaoB,aAAA;ED8CpB;AACF;AC7CI;EAAyB,qBFhFb;ACgIhB;AC/CI;EACE,YAAA;ADiDN;AChDM;EAAM,uBAAA;ADmDZ;;AC9CA;EACE,aAAA;EACA,uBAAA;EACA,QAAA;EACA,gBAAA;ADiDF;AC/CE;EACE,eAAA;EACA,cAAA;ADiDJ;AChDI;EAAM,WAAA;EAAa,YAAA;ADoDvB;;AChDA;EACE,gBAAA;ADmDF;AEzLE;EDqIF;IAGoB,gBAAA;EDqDlB;AACF;;ACnDA;EACE,gBAAA;EACA,mBAAA;ADsDF;AEnME;ED2IF;IAIqB,gBAAA;IAAkB,mBAAA;EDyDrC;AACF;;ACvDE;EACA,aAAA;EACA,mBAAA;EACA,8BAAA;EACA,SAAA;AD0DF;;ACtDA;EACE,gBAAA;EACA,aAAA;EACA,qCAAA;EACA,SAAA;ADyDF;AEvNE;ED0JF;IAOI,aAAA;IACA,gBAAA;IACA,kBAAA;IACA,mBAAA;IACA,oBAAA;IACA,6BAAA;ED0DF;ECzDE;IAAuB,aAAA;ED4DzB;EC1DE;IACE,eAAA;IACA,wBAAA;ED4DJ;AACF;;ACvDA;EACE,gBAAA;AD0DF;AE5OE;EDiLF;IAGoB,gBAAA;ED4DlB;AACF;ACzDA;EACE,gBAAA;EACA,aAAA;EACA,SAAA;AD2DF;AEtPE;EDwLF;IAKoB,sBAAA;IAAwB,SAAA;ED8D1C;AACF;AC7DE;EACE,OAAA;EACA,aAAA;EACA,sBAAA;EACA,qBAAA;EACA,cAAA;AD+DJ;AC5DE;EACE,WAAA;EACA,iBAAA;EACA,gBAAA;EACA,yBFxKa;EEyKb,eAAA;AD8DJ;AC3DE;EACE,WAAA;EACA,YAAA;EACA,oBAAA;KAAA,iBAAA;EACA,+BAAA;EACA,cAAA;AD6DJ;AC1DI;EACE,qBAAA;AD4DN;ACtDA;EACE,gBAAA;EACA,cFhMc;EEiMd,eAAA;EACA,gBAAA;ADwDF;ACrDA;EACE,cFrMgB;EEsMhB,eAAA;EACA,eAAA;ADuDF;;ACjDA;EACE,aAAA;EACA,SAAA;ADoDF;AClDE;EACE,WAAA;EACA,YAAA;EACA,aAAA;EACA,mBAAA;EACA,uBAAA;EACA,yBAAA;EACA,gBFpNU;ACwQd;AClDI;EAAa,YAAA;ADqDjB","file":"Home.css"} \ No newline at end of file diff --git a/src/pages/Home/Home.module.scss b/src/pages/Home/Home.module.scss new file mode 100644 index 00000000000..374e5da5d60 --- /dev/null +++ b/src/pages/Home/Home.module.scss @@ -0,0 +1,223 @@ +@import '../../variables'; +@import '../../mixins'; + +.home { + grid-column: 1 / -1; + display: contents; + + .homeTitle { + grid-column: 1 / -1; + margin-top: 56px; + color: $color-primary; + font-size: map-get($h1-styles, font-size); + font-weight: map-get($h1-styles, font-weight); + line-height: map-get($h1-styles, line-height); + letter-spacing: map-get($h1-styles, letter-spacing); + + @include tablet { margin-top: 32px; max-width: 20ch; } + + @include mobile { + margin-top: 24px; + font-size: map-get($h1-styles-mobile, font-size); + } + } +} + +.visuallyHidden { + position: absolute; + width: 1px; + height: 1px; + margin: -1px; + border: 0; + padding: 0; + white-space: nowrap; + clip-path: inset(100%); + clip: rect(0 0 0 0); + overflow: hidden; +} + +section { + grid-column: 1 / -1; +} + +h3 { + color: $color-primary; + font-size: map-get($h2-styles, font-size); + margin: 0; + + @include mobile { font-size: map-get($h2-styles-mobile, font-size); } +} + +// --- BANNER SECTION --- +.bannerSection { + display: flex; + flex-direction: column; + align-items: center; + + .carouselWrapper { + width: 100%; + display: flex; + align-items: center; + justify-content: space-between; + margin-top: 56px; + gap: 16px; + + @include tablet { margin-top: 32px; } + + @include mobile { margin-top: 24px; gap: 0; } + } + + .bannerHome { + flex: 1; + width: 100%; + height: 400px; + position: relative; + overflow: hidden; + background-color: $color-elements; + + .bannerImage { + display: block; + width: 100%; + height: 100%; + object-fit: cover; + } + + @include tablet { height: 300px; } + + @include mobile { + height: auto; + aspect-ratio: 1 / 1; + width: 100vw; + margin-left: calc(50% - 50vw); + margin-right: calc(50% - 50vw); + } + } + + .carouselArrow { + flex-shrink: 0; + width: 32px; + height: 400px; + display: flex; + align-items: center; + justify-content: center; + background-color: $color-white; + border: 1px solid $color-icons; + cursor: pointer; + + @include tablet { height: 300px; } + + @include mobile { display: none; } + + &:hover:not(:disabled) { border-color: $color-primary; } + &:disabled { opacity: 0.4; } + } +} + +.carouselDots { + display: flex; + justify-content: center; + gap: 4px; + margin-top: 16px; + + .dot { + cursor: pointer; + line-height: 0; + img { width: 24px; height: 24px; } + } +} + +// --- PRODUCTS GRID & SECTIONS --- +.brandNewModels, .hotPrices { + margin-top: 80px; + + @include mobile { margin-top: 56px; } +} + +.hotPrices { margin-bottom: 80px; } + +.containerProducts, .containerDivider { + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; +} + +.productsGrid { + margin-top: 24px; + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 16px; + + @include mobile { + display: flex; + overflow-x: auto; + scroll-snap-type: x mandatory; + &::-webkit-scrollbar { display: none; } + & > * { flex: 0 0 212px; scroll-snap-align: center; } + } +} + +// --- CATEGORIES --- +.categories { + margin-top: 80px; + .categoriesContainer { + margin-top: 24px; + display: flex; + gap: 16px; + + @include mobile { flex-direction: column; gap: 32px; } + } + + .categoriesCategory { + flex: 1; + display: flex; + flex-direction: column; + text-decoration: none; + color: inherit; + + .categoryImgContainer { + width: 100%; + aspect-ratio: 1/1; + overflow: hidden; + background-color: $color-elements; + img { + width: 100%; + height: 100%; + object-fit: cover; + transition: transform 0.3s ease; + &:hover { transform: scale(1.1); } + } + } + } + + .categoryName { + margin-top: 24px; + color: $color-primary; + font-size: map-get($h4-styles, font-size); + } + + .numberOfProducts { + color: $color-secondary; + margin-top: 4px; + font-size: 14px; + } +} + +.carouselButtons { + display: flex; + gap: 16px; + .carouselArrowSmall { + width: 32px; + height: 32px; + display: flex; + align-items: center; + justify-content: center; + border: 1px solid $color-icons; + background: $color-white; + &:disabled { opacity: 0.4; } + + &:hover:not(:disabled) { + border-color: $color-primary; + } +} +} diff --git a/src/pages/Home/Home.tsx b/src/pages/Home/Home.tsx new file mode 100644 index 00000000000..0639369bee8 --- /dev/null +++ b/src/pages/Home/Home.tsx @@ -0,0 +1,232 @@ +import React, { useEffect, useState } from 'react'; +import { Link } from 'react-router-dom'; +import productsData from '../../../public/api/products.json'; +import { CatalogProduct } from '../../../public/types'; +import ProductCard from '../../components/ProductCard/ProductCard'; + +import s from './Home.module.scss'; + +const banners = [ + { id: 1, desktop: './img/Banner.png', mobile: './img/banner-mobile.svg' }, + { id: 2, desktop: './img/banner2.png', mobile: './img/banner2.png' }, + { id: 3, desktop: './img/banner3.png', mobile: './img/banner3.png' }, +]; + +export const Home: React.FC = () => { + const [currentBannerIndex, setCurrentBannerIndex] = useState(0); + const visibleCount = 4; + + const nextBanner = () => { + setCurrentBannerIndex(prev => (prev + 1) % banners.length); + }; + + const prevBanner = () => { + setCurrentBannerIndex(prev => (prev - 1 + banners.length) % banners.length); + }; + + useEffect(() => { + const interval = setInterval(nextBanner, 5000); + + return () => clearInterval(interval); + }, []); + + const sortedProducts: CatalogProduct[] = [ + ...(productsData as CatalogProduct[]), + ] + .sort((a, b) => b.year - a.year) + .slice(0, 10); + + const [startIndex, setStartIndex] = useState(0); + + const handlePrev = () => setStartIndex(prev => Math.max(prev - 1, 0)); + const handleNext = () => + setStartIndex(prev => + Math.min(prev + 1, sortedProducts.length - visibleCount), + ); + + const visibleProducts = sortedProducts.slice( + startIndex, + startIndex + visibleCount, + ); + + const discountProducts: CatalogProduct[] = [ + ...(productsData as CatalogProduct[]), + ] + .filter(product => product.fullPrice && product.fullPrice > product.price) + .sort((a, b) => b.fullPrice - b.price - (a.fullPrice - a.price)) + .slice(0, 10); + + const [hotStartIndex, setHotStartIndex] = useState(0); + + const handleHotPrev = () => setHotStartIndex(prev => Math.max(prev - 1, 0)); + const handleHotNext = () => + setHotStartIndex(prev => + Math.min(prev + 1, discountProducts.length - visibleCount), + ); + + const visibleDiscountProducts = discountProducts.slice( + hotStartIndex, + hotStartIndex + visibleCount, + ); + + const phonesCount = productsData.filter(p => p.category === 'phones').length; + const tabletsCount = productsData.filter( + p => p.category === 'tablets', + ).length; + const accessoriesCount = productsData.filter( + p => p.category === 'accessories', + ).length; + + return ( +
+

Product Catalog

+

Welcome to Nice Gadgets store!

+ +
+
+ + +
+ + + {`Banner + +
+ + +
+ +
+ {banners.map((_, index) => ( + setCurrentBannerIndex(index)} + > + Dot + + ))} +
+
+ +
+
+

Brand new models

+
+ + +
+
+
+ {visibleProducts.map(product => ( + + ))} +
+
+ +
+

Shop by category

+
+ window.scrollTo(0, 0)} + > +
+ Phones +
+
Mobile phones
+
{phonesCount} models
+ + + window.scrollTo(0, 0)} + > +
+ Tablets +
+
Tablets
+
{tabletsCount} models
+ + + window.scrollTo(0, 0)} + > +
+ Accessories +
+
Accessories
+
{accessoriesCount} models
+ +
+
+ +
+
+

Hot prices

+
+ + +
+
+
+ {visibleDiscountProducts.map(product => ( + + ))} +
+
+
+ ); +}; diff --git a/src/pages/Home/index.ts b/src/pages/Home/index.ts new file mode 100644 index 00000000000..6fd0b5ba7dd --- /dev/null +++ b/src/pages/Home/index.ts @@ -0,0 +1 @@ +export * from './Home'; diff --git a/src/pages/NotFoundPage/NotFoundPage.module.scss b/src/pages/NotFoundPage/NotFoundPage.module.scss new file mode 100644 index 00000000000..2dd61e5b8fd --- /dev/null +++ b/src/pages/NotFoundPage/NotFoundPage.module.scss @@ -0,0 +1,38 @@ +@import '../../variables'; +@import '../../mixins'; + +.notFound { + grid-column: 1/-1; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + min-height: 400px; + text-align: center; + padding: 40px 20px; + + .title { + font-size: 48px; + color: $color-primary; + margin-bottom: 24px; + + @include mobile { + font-size: 32px; + } + } + + .backButton { + padding: 12px 24px; + background-color: $color-primary; + color: $color-white; + border: none; + font-family: $font-main; + font-weight: 700; + cursor: pointer; + transition: opacity 0.2s; + + &:hover { + opacity: 0.9; + } + } +} diff --git a/src/pages/NotFoundPage/NotFoundPage.tsx b/src/pages/NotFoundPage/NotFoundPage.tsx new file mode 100644 index 00000000000..8cdf46a1620 --- /dev/null +++ b/src/pages/NotFoundPage/NotFoundPage.tsx @@ -0,0 +1,16 @@ +import React from 'react'; +import { useNavigate } from 'react-router-dom'; +import s from './NotFoundPage.module.scss'; + +export const NotFoundPage: React.FC = () => { + const navigate = useNavigate(); + + return ( +
+

Page not found

+ +
+ ); +}; diff --git a/src/pages/NotFoundPage/index.ts b/src/pages/NotFoundPage/index.ts new file mode 100644 index 00000000000..6197aa75aa8 --- /dev/null +++ b/src/pages/NotFoundPage/index.ts @@ -0,0 +1 @@ +export * from './NotFoundPage'; diff --git a/src/pages/ProductPage/ProductPage.css b/src/pages/ProductPage/ProductPage.css new file mode 100644 index 00000000000..305a0cf1182 --- /dev/null +++ b/src/pages/ProductPage/ProductPage.css @@ -0,0 +1,282 @@ +@charset "UTF-8"; +@font-face { + font-family: "Mont"; + src: url("/fonts/Mont-Regular.otf") format("opentype"); + font-weight: 400; + font-style: normal; +} +@font-face { + font-family: "Mont"; + src: url("/fonts/Mont-SemiBold.otf") format("opentype"); + font-weight: 600; + font-style: normal; +} +@font-face { + font-family: "Mont"; + src: url("/fonts/Mont-Bold.otf") format("opentype"); + font-weight: 700; + font-style: normal; +} +.product-page { + margin-top: 24px; + grid-column: 1/-1; +} +.product-page .product-title { + margin-top: 16px; + font-size: 32px; + font-weight: 700; + line-height: 41px; + letter-spacing: -0.01em; +} + +.product-content { + display: flex; + gap: 64px; + margin-top: 30px; +} + +.back-button { + margin-top: 40px; + background: none; + border: none; + color: #89939a; + font-size: 12px; + display: flex; + align-items: center; + gap: 6px; + cursor: pointer; +} +.back-button:hover { + color: #313237; +} + +/* ===== GALLERY ===== */ +.product-gallery { + display: flex; + gap: 16px; +} + +/* Стовпчик мініатюр */ +.gallery-thumbnails { + display: flex; + flex-direction: column; + gap: 12px; +} + +.thumbnail { + width: 80px; + height: 80px; + border: 1px solid #e2e2e2; + border-radius: 8px; + padding: 4px; + cursor: pointer; + transition: all 0.2s ease; +} +.thumbnail img { + width: 100%; + height: 100%; + -o-object-fit: contain; + object-fit: contain; +} +.thumbnail:hover { + border-color: #333; +} + +.thumbnail--active { + border: 2px solid #333; +} + +/* Велике фото */ +.gallery-main { + height: 356px; + width: 420px; + border: none; + display: flex; + align-items: center; + justify-content: center; + padding: 16px; +} + +.product-main-image { + max-height: 100%; + max-width: 100%; + -o-object-fit: contain; + object-fit: contain; +} + +/* ===== INFO ===== */ +.product-info { + grid-column: 16/-1; + gap: 20px; +} + +.product-colors { + display: flex; + flex-direction: column; + gap: 8px; + margin-bottom: 24px; +} + +.color-dots { + display: flex; + gap: 8px; +} + +.color-dot { + width: 20px; + height: 20px; + border-radius: 50%; + cursor: pointer; + border: 1px solid #e2e6e9; +} +.color-dot--active { + border: 2px solid #313237; +} + +.product-capacity { + display: flex; + flex-direction: column; + gap: 8px; + margin-top: 24px; + margin-bottom: 24px; +} + +.capacity-buttons { + display: flex; + gap: 8px; +} + +.capacity-item { + padding: 8px 16px; + border: 1px solid #e2e6e9; + background-color: #fff; + cursor: pointer; + font-size: 14px; + transition: all 0.2s; +} +.capacity-item:hover { + border-color: #313237; + background-color: #f9f9f9; +} +.capacity-item--active { + border: 2px solid #313237; + background-color: #313237; + color: #fff; +} + +.product-pricing { + display: flex; + align-items: baseline; + gap: 12px; + margin-top: 32px; + margin-bottom: 16px; +} +.product-pricing .current-price { + font-size: 28px; + font-weight: 700; +} +.product-pricing .original-price { + text-decoration: line-through; + color: #999; +} + +.product-actions { + grid-column: 1/-1; + display: flex; + gap: 12px; +} +.product-actions .action-button { + width: 100%; + padding: 12px 20px; + border: none; + background: #333; + color: white; + cursor: pointer; + transition: 0.2s; +} +.product-actions .action-button.added { + background: #2a9d8f; +} +.product-actions .favorite-button { + width: 48px; + border: 1px solid #ccc; + background: white; + cursor: pointer; + font-size: 20px; +} +.product-actions .favorite-button.liked { + color: red; + border-color: red; +} + +/* ===== SPECS ===== */ +.product-specs { + display: flex; + flex-direction: column; + gap: 8px; + font-size: 14px; + margin-top: 32px; +} + +.product-specs2 { + display: flex; + width: 100%; + flex-direction: column; + gap: 8px; + font-size: 14px; + margin-top: 32px; +} +.product-specs2 .spec-row { + display: flex; + justify-content: space-between; +} +.product-specs2 .spec-name { + font-weight: 500; + color: #2541cb; +} + +.spec-row { + display: flex; + justify-content: space-between; +} + +.spec-name { + font-weight: 500; + color: #313237; +} + +.spec-value { + font-weight: 400; + color: #555; +} + +.divider { + width: 320px; +} + +/* ===== DETAILS ===== */ +.product-description { + display: flex; + grid-column: 1/-1; + grid-template-columns: 1fr 1fr; + gap: 64px; + margin-top: 60px; +} +.product-description .divider { + margin: 16px 0; + width: 100%; +} +.product-description .about-section { + margin-bottom: 40px; + width: auto; + min-width: 512px; + max-width: 548px; +} +.product-description .about-section h2 { + margin-bottom: 12px; +} +.product-description .about-section p { + margin-bottom: 8px; + line-height: 1.6; + color: #444; +}/*# sourceMappingURL=ProductPage.css.map */ \ No newline at end of file diff --git a/src/pages/ProductPage/ProductPage.css.map b/src/pages/ProductPage/ProductPage.css.map new file mode 100644 index 00000000000..2d253310db2 --- /dev/null +++ b/src/pages/ProductPage/ProductPage.css.map @@ -0,0 +1 @@ +{"version":3,"sources":["ProductPage.css","../../variables.scss","ProductPage.scss"],"names":[],"mappings":"AAAA,gBAAgB;ACUhB;EACE,mBAAA;EACA,sDAAA;EACA,gBAAA;EACA,kBAAA;ADRF;ACWA;EACE,mBAAA;EACA,uDAAA;EACA,gBAAA;EACA,kBAAA;ADTF;ACYA;EACE,mBAAA;EACA,mDAAA;EACA,gBAAA;EACA,kBAAA;ADVF;AEhBA;EACE,gBAAA;EACA,iBAAA;AFkBF;AEhBE;EACE,gBAAA;EACA,eAAA;EACA,gBAAA;EACA,iBAAA;EACA,uBAAA;AFkBJ;;AEdA;EACE,aAAA;EACA,SAAA;EACA,gBAAA;AFiBF;;AEdA;EACE,gBAAA;EACA,gBAAA;EACA,YAAA;EACA,cAAA;EACA,eAAA;EACA,aAAA;EACA,mBAAA;EACA,QAAA;EACA,eAAA;AFiBF;AEfE;EACE,cAAA;AFiBJ;;AEZA,wBAAA;AAEA;EACE,aAAA;EACA,SAAA;AFcF;;AEXA,sBAAA;AACA;EACE,aAAA;EACA,sBAAA;EACA,SAAA;AFcF;;AEXA;EACE,WAAA;EACA,YAAA;EACA,yBAAA;EACA,kBAAA;EACA,YAAA;EACA,eAAA;EACA,yBAAA;AFcF;AEZE;EACE,WAAA;EACA,YAAA;EACA,sBAAA;KAAA,mBAAA;AFcJ;AEXE;EACE,kBAAA;AFaJ;;AETA;EACE,sBAAA;AFYF;;AETA,gBAAA;AACA;EACE,aAAA;EACA,YAAA;EACA,YAAA;EACA,aAAA;EACA,mBAAA;EACA,uBAAA;EACA,aAAA;AFYF;;AETA;EACE,gBAAA;EACA,eAAA;EACA,sBAAA;KAAA,mBAAA;AFYF;;AETA,qBAAA;AAEA;EACE,kBAAA;EACA,SAAA;AFWF;;AERA;EACE,aAAA;EACA,sBAAA;EACA,QAAA;EACA,mBAAA;AFWF;;AERA;EACE,aAAA;EACA,QAAA;AFWF;;AEPA;EACE,WAAA;EACA,YAAA;EACA,kBAAA;EACA,eAAA;EACA,yBAAA;AFUF;AERE;EACE,yBAAA;AFUJ;;AENA;EACE,aAAA;EACA,sBAAA;EACA,QAAA;EACA,gBAAA;EACA,mBAAA;AFSF;;AENA;EACE,aAAA;EACA,QAAA;AFSF;;AENA;EACE,iBAAA;EACA,yBAAA;EACA,sBAAA;EACA,eAAA;EACA,eAAA;EACA,oBAAA;AFSF;AEPE;EACE,qBAAA;EACA,yBAAA;AFSJ;AENE;EACE,yBAAA;EACA,yBAAA;EACA,WAAA;AFQJ;;AEHA;EACE,aAAA;EACA,qBAAA;EACA,SAAA;EACA,gBAAA;EACA,mBAAA;AFMF;AEJE;EACE,eAAA;EACA,gBAAA;AFMJ;AEHE;EACE,6BAAA;EACA,WAAA;AFKJ;;AEDA;EACE,iBAAA;EACA,aAAA;EACA,SAAA;AFIF;AEFE;EACE,WAAA;EACA,kBAAA;EACA,YAAA;EACA,gBAAA;EACA,YAAA;EACA,eAAA;EACA,gBAAA;AFIJ;AEFI;EACE,mBAAA;AFIN;AEAE;EACE,WAAA;EACA,sBAAA;EACA,iBAAA;EACA,eAAA;EACA,eAAA;AFEJ;AEAI;EACE,UAAA;EACA,iBAAA;AFEN;;AEGA,sBAAA;AAEA;EACE,aAAA;EACA,sBAAA;EACA,QAAA;EACA,eAAA;EACA,gBAAA;AFDF;;AEIA;EACE,aAAA;EACA,WAAA;EACA,sBAAA;EACA,QAAA;EACA,eAAA;EACA,gBAAA;AFDF;AEIE;EACA,aAAA;EACA,8BAAA;AFFF;AEKA;EACE,gBAAA;EACA,cAAA;AFHF;;AEQA;EACE,aAAA;EACA,8BAAA;AFLF;;AEQA;EACE,gBAAA;EACA,cAAA;AFLF;;AEQA;EACE,gBAAA;EACA,WAAA;AFLF;;AEQA;EACE,YAAA;AFLF;;AEQA,wBAAA;AAEA;EACE,aAAA;EACA,iBAAA;EACA,8BAAA;EACA,SAAA;EACA,gBAAA;AFNF;AESE;EACE,cAAA;EACA,WAAA;AFPJ;AEUE;EACE,mBAAA;EACA,WAAA;EACA,gBAAA;EACA,gBAAA;AFRJ;AEUI;EACE,mBAAA;AFRN;AEWI;EACE,kBAAA;EACA,gBAAA;EACA,WAAA;AFTN","file":"ProductPage.css"} \ No newline at end of file diff --git a/src/pages/ProductPage/ProductPage.module.scss b/src/pages/ProductPage/ProductPage.module.scss new file mode 100644 index 00000000000..0cb0c0ec5dd --- /dev/null +++ b/src/pages/ProductPage/ProductPage.module.scss @@ -0,0 +1,431 @@ +@import '../../variables'; +@import '../../mixins'; + +.productPage { + grid-column: 1 / -1; + width: 100%; + display: flex; + flex-direction: column; + + &Loading, &Error { + align-items: center; + justify-content: center; + min-height: 400px; + text-align: center; + } + + .productTitle { + margin-bottom: 40px; + font-size: map-get($h2-styles, font-size); + font-weight: map-get($h2-styles, font-weight); + line-height: map-get($h2-styles, line-height); + color: $color-primary; + + @include mobile { + font-size: map-get($h2-styles-mobile, font-size); + } + } + + .productTwoColumns { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 64px; + align-items: start; + + @include mobile { + display: flex; + flex-direction: column; + gap: 40px; + + .leftSide, .rightSide { + display: contents; + } + + .productGallery { order: 1; } + .productInfoCard { order: 2; } + .aboutSection { order: 3; } + .techSpecsSection { order: 4; } + } + + @include tablet { + gap: 30px; + + .leftSide, .rightSide { + display: contents; + } + + .productGallery { + grid-column: 1; + grid-row: 1; + } + + .productInfoCard { + grid-column: 2; + grid-row: 1; + } + + .aboutSection { + grid-column: 1 / -1; + grid-row: 2; + margin-top: 40px; + } + + .techSpecsSection { + grid-column: 1 / -1; + grid-row: 3; + } + } + } +} + +.leftSide { + display: flex; + flex-direction: column; + gap: 80px; + min-width: 0; + + @include mobile { gap: 56px; } +} + +.rightSide { + display: flex; + flex-direction: column; + gap: 64px; + width: 100%; +} + +.productGallery { + display: flex; + gap: 16px; + + @include mobile { + flex-direction: column-reverse; + width: 100%; + } + + .galleryThumbnails { + display: flex; + flex-direction: column; + gap: 12px; + + @include mobile { + flex-direction: row; + overflow-x: auto; + gap: 8px; + } + + @include tablet { gap: 8px; } + + .thumbnail { + width: 80px; + height: 80px; + border: 1px solid $color-elements; + padding: 7px; + cursor: pointer; + background-color: $color-white; + transition: border-color 0.2s; + + @include mobile { width: 50px; height: 50px; } + + @include tablet { width: 35px; height: 35px; } + + img { + width: 100%; + height: 100%; + object-fit: contain; + } + + &Active { + border-color: $color-primary; + } + } + } + + .galleryMain { + flex: 1; + height: 464px; + display: flex; + align-items: center; + justify-content: center; + + @include mobile { max-height: 288px; } + + @include tablet { max-height: 288px; } + + .productMainImage { + max-height: 100%; + max-width: 100%; + object-fit: contain; + } + } +} + +.productInfoCard { + width: 100%; + + .divider { + margin-top: 24px; + border: none; + border-top: 1px solid $color-elements; + + @include desktop { max-width: 320px; } + } + + .label { + font-size: 12px; + font-weight: 600; + color: $color-secondary; + } + + .topContainer { + display: flex; + justify-content: space-between; + align-items: center; + width: 100%; + margin-bottom: 8px; + } + + .productId { + font-size: 12px; + color: $color-icons; + white-space: nowrap; + + @include mobile { + width: 150px; + overflow: hidden; + text-overflow: ellipsis; + } + } + +.colorDots { + display: flex; + gap: 14px; + margin-top: 8px; + + .colorLabel { + cursor: pointer; + display: block; + line-height: 0; + z-index: 1; // Піднімаємо вище в шарах + } + + .colorDot { + display: block; + width: 28px; + height: 28px; + border-radius: 50%; + border: none; + outline: 2px solid $color-elements; + outline-offset: 2px; + transition: outline-color 0.2s ease; + + &:hover { outline-color: $color-primary; } + &Active { outline-color: $color-primary; } + } + } + + .productCapacity { margin-top: 24px; } +.capacityButtons { + display: flex; + gap: 8px; + margin-top: 8px; + flex-wrap: wrap; + + .capacityLabel { + cursor: pointer; + display: block; + } + + .capacityItem { + display: block; + padding: 10px 12px; + border: 1px solid $color-elements; + background: $color-white; + font-family: $font-main; + transition: all 0.2s; + + &Active { + background: $color-primary; + color: $color-white; + border-color: $color-primary; + } + + &:hover:not(&Active) { + border-color: $color-primary; + } + } + } + + .productPricing { + display: flex; + align-items: center; + gap: 8px; + margin: 32px 0 16px; + + @include desktop { max-width: 320px; } + + .currentPrice { font-size: 32px; font-weight: 700; color: $color-primary; } + .originalPrice { + font-size: 22px; + color: $color-secondary; + text-decoration: line-through; + } + } + + .productActions { + display: flex; + gap: 8px; + margin-bottom: 32px; + width: 100%; + + @include desktop { max-width: 320px; } + } + + .actionButton { + flex: 1; + height: 48px; + background: $color-primary; + color: $color-white; + border: none; + font-weight: 500; + font-family: $font-main; + cursor: pointer; + + &AddedToCart { + background: $color-white; + color: $color-green; + border: 1px solid $color-elements; + } + } + + .favoriteButton { + width: 48px; + height: 48px; + background: $color-white; + border: 1px solid $color-elements; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + + &.liked { border-color: $color-primary; } + } +} + +.aboutSection, .techSpecsSection { + width: 100%; + + .aboutTitle { font-size: 24px; margin-bottom: 16px; color: $color-primary; } + .divider { border: none; border-top: 1px solid $color-elements; margin-bottom: 32px; } + .sectionTitle { font-size: 20px; margin: 32px 0 16px; color: $color-primary; } + .paragraph { + color: $color-secondary; + line-height: 1.5; + margin-bottom: 16px; + font-size: 14px; + font-weight: 500; + } +} + +.specRow { + display: flex; + justify-content: space-between; + margin-bottom: 8px; + width: 100%; + + .specName { font-size: 14px; color: $color-secondary; font-weight: 500; } + .specValue { + font-size: 14px; + font-weight: 500; + color: $color-primary; + text-align: right; + + @include mobile { + flex: 1; + margin-left: 16px; + word-break: break-word; + } + } +} + +.productSpecsShort { + margin-top: 32px; + width: 100%; + + @include desktop { max-width: 320px; } + .specName, .specValue { font-weight: 600; } +} + +.backButton { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 16px; + cursor: pointer; + color: $color-secondary; + font-size: 12px; + &:hover { color: $color-primary; } +} + +.brandNewModels { + margin-top: 80px; + margin-bottom: 80px; + + @include mobile { margin-top: 56px; margin-bottom: 56px; } + + .containerProducts { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 24px; + } + + .carouselButtons { display: flex; gap: 16px; } + + .carouselArrow { + width: 32px; + height: 32px; + display: flex; + align-items: center; + justify-content: center; + border: 1px solid $color-icons; + background: $color-white; + cursor: pointer; + &:disabled { opacity: 0.3; } + } + + + .productsGrid { + margin-top: 24px; + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 16px; + + @include mobile { + display: flex; + overflow-x: auto; + scroll-snap-type: x mandatory; + &::-webkit-scrollbar { display: none; } + & > * { flex: 0 0 212px; scroll-snap-align: center; } + } +} +} + +.visuallyHidden { + position: absolute; + width: 1px; + height: 1px; + margin: -1px; + border: 0; + padding: 0; + white-space: nowrap; + clip-path: inset(100%); + clip: rect(0 0 0 0); + overflow: hidden; +} + +.colorLabel, .capacityLabel { + cursor: pointer; + display: block; +} + diff --git a/src/pages/ProductPage/ProductPage.tsx b/src/pages/ProductPage/ProductPage.tsx new file mode 100644 index 00000000000..3a8ef8ab645 --- /dev/null +++ b/src/pages/ProductPage/ProductPage.tsx @@ -0,0 +1,326 @@ +import { useParams, useNavigate } from 'react-router-dom'; +import { useState, useEffect, useMemo } from 'react'; +import { + CatalogProduct, + Phone, + Tablet, + Accessory, +} from '../../../public/types'; +import Breadcrumbs from '../../components/Breadcrumbs/Breadcrumbs'; +import ProductCard from '../../components/ProductCard/ProductCard'; +import { useAppDispatch, useAppSelector } from '../../app/hooks'; +import { fetchProducts } from '../../features/products/productsSlice'; +import { + addToFavorites, + removeFromFavorites, +} from '../../features/favorites/favoritesSlice'; +import { addToCart } from '../../features/cart/cartSlice'; +import { colorMap } from '../../../public/utils/colorMap'; + +import s from './ProductPage.module.scss'; + +type ProductType = Phone | Tablet | Accessory; + +const normalize = (str: string = '') => str.toLowerCase().replace(/[\s-]/g, ''); + +const getModelBase = (id: string) => { + const parts = id.split('-'); + + if (parts.length > 3) { + return parts.slice(0, -2).join('-'); + } + return parts[0]; +}; + +export const ProductPage = () => { + const { category, productId } = useParams<{ + category: string; + productId: string; + }>(); + const navigate = useNavigate(); + const dispatch = useAppDispatch(); + + const [product, setProduct] = useState(null); + const [selectedImage, setSelectedImage] = useState(0); + const [loading, setLoading] = useState(true); + const [startIndex, setStartIndex] = useState(0); + + const allProducts = useAppSelector(state => state.products.items) as CatalogProduct[]; + const favorites = useAppSelector(state => state.favorites.items); + const cartItems = useAppSelector(state => state.cart.items); + + const isLiked = favorites.some(item => item.id === productId || (item as any).itemId === productId); + const isInCart = cartItems.some(item => item.id === productId); + + const suggestedProducts = useMemo(() => { + return [...allProducts] + .sort(() => Math.random() - 0.5) + .slice(0, 10); + }, [allProducts]); + + useEffect(() => { + if (allProducts.length === 0) { + dispatch(fetchProducts()); + } + }, [dispatch, allProducts.length]); + + useEffect(() => { + window.scrollTo(0, 0); + if (!productId || !category) return; + + setLoading(true); + fetch(`api/${category}.json`) + .then(res => { + if (!res.ok) throw new Error('Product details not found'); + return res.json(); + }) + .then((data: ProductType[]) => { + const foundProduct = data.find(p => p.id === productId); + if (foundProduct) { + setProduct(foundProduct); + } else { + setProduct(null); + } + setLoading(false); + }) + .catch(err => { + console.error(err); + setProduct(null); + setLoading(false); + }); + + setSelectedImage(0); + setStartIndex(0); + }, [productId, category]); + +const findTargetVariant = (targetColor: string, targetCapacity: string) => { + if (!product) return null; + + const baseId = normalize((product as any).namespaceId || getModelBase(productId || '')); + + return allProducts.find(p => { + const pBase = normalize((p as any).namespaceId || getModelBase(p.itemId)); + + const isSameFamily = pBase.includes(baseId) || baseId.includes(pBase); + + const isSameColor = normalize(p.color) === normalize(targetColor); + + const isSameCapacity = normalize(p.capacity) === normalize(targetCapacity); + + return isSameFamily && isSameColor && isSameCapacity; + }); +}; + + const handleLikeClick = () => { + if (!product) return; + if (isLiked) { + dispatch(removeFromFavorites(productId!)); + } else { + const catalogItem = allProducts.find(p => p.itemId === productId) || (product as any); + dispatch(addToFavorites(catalogItem)); + } + }; + + const handleAddToCart = () => { + if (!product || isInCart) return; + dispatch(addToCart({ + id: productId!, + name: product.name, + price: 'priceDiscount' in product ? product.priceDiscount : (product as any).price, + image: product.images[0], + })); + }; + + const handlePrev = () => setStartIndex(prev => Math.max(prev - 1, 0)); + const handleNext = () => setStartIndex(prev => + Math.min(prev + 1, Math.max(0, suggestedProducts.length - 4)) + ); + + if (loading) return
Loading...
; + if (!product) return

Product not found

; + + return ( +
+ + +
navigate(-1)}> + Back + Back +
+ +

{product.name}

+ +
+
+
+
+ {product.images.map((img, index) => ( + + ))} +
+
+ {product.name} +
+
+ +
+

About

+
+ {product.description.map(section => ( +
+

{section.title}

+ {section.text.map((paragraph, i) =>

{paragraph}

)} +
+ ))} +
+
+ +
+
+
+
+ Available colors + ID: {product.id} +
+
+ {product.colorsAvailable.map(color => { + const variant = findTargetVariant(color, product.capacity); + return ( + + ); + })} +
+
+ +
+ +
+ Select capacity +
+ {product.capacityAvailable.map(capacity => { + let variant = findTargetVariant(product.color, capacity); + + if (!variant) { + const baseId = (product as any).namespaceId || getModelBase(productId || ''); + variant = allProducts.find(p => + ((p as any).namespaceId === baseId || getModelBase(p.itemId) === baseId) && + normalize(p.capacity) === normalize(capacity) + ) || null; + } + + return ( + + ); + })} +
+
+ +
+ +
+ + ${'priceDiscount' in product ? product.priceDiscount : (product as any).price} + + {'priceRegular' in product && product.priceRegular !== product.priceDiscount && ( + ${product.priceRegular} + )} +
+ +
+ + +
+ +
+
Screen{product.screen}
+
Resolution{product.resolution}
+
Processor{product.processor}
+
RAM{product.ram}
+
+
+ +
+

Tech specs

+
+
+
Screen{product.screen}
+
Resolution{product.resolution}
+
Processor{product.processor}
+
RAM{product.ram}
+
Built in memory{product.capacity}
+
Camera{'camera' in product ? product.camera : 'N/A'}
+
Zoom{'zoom' in product ? product.zoom : 'N/A'}
+
Cell{'cell' in product ? product.cell.join(', ') : 'N/A'}
+
+
+
+
+ +
+
+

You may also like

+
+ + +
+
+
+ {suggestedProducts.slice(startIndex, startIndex + 4).map(item => ( + + ))} +
+
+
+ ); +}; diff --git a/src/pages/ProductPage/index.ts b/src/pages/ProductPage/index.ts new file mode 100644 index 00000000000..875dce3d23c --- /dev/null +++ b/src/pages/ProductPage/index.ts @@ -0,0 +1 @@ +export * from './ProductPage'; diff --git a/src/variables.css b/src/variables.css new file mode 100644 index 00000000000..32b37df1a3b --- /dev/null +++ b/src/variables.css @@ -0,0 +1,18 @@ +@font-face { + font-family: "Mont"; + src: url("/fonts/Mont-Regular.otf") format("opentype"); + font-weight: 400; + font-style: normal; +} +@font-face { + font-family: "Mont"; + src: url("/fonts/Mont-SemiBold.otf") format("opentype"); + font-weight: 600; + font-style: normal; +} +@font-face { + font-family: "Mont"; + src: url("/fonts/Mont-Bold.otf") format("opentype"); + font-weight: 700; + font-style: normal; +}/*# sourceMappingURL=variables.css.map */ \ No newline at end of file diff --git a/src/variables.css.map b/src/variables.css.map new file mode 100644 index 00000000000..23ce519595d --- /dev/null +++ b/src/variables.css.map @@ -0,0 +1 @@ +{"version":3,"sources":["variables.scss","variables.css"],"names":[],"mappings":"AAUA;EACE,mBAAA;EACA,sDAAA;EACA,gBAAA;EACA,kBAAA;ACTF;ADYA;EACE,mBAAA;EACA,uDAAA;EACA,gBAAA;EACA,kBAAA;ACVF;ADaA;EACE,mBAAA;EACA,mDAAA;EACA,gBAAA;EACA,kBAAA;ACXF","file":"variables.css"} \ No newline at end of file diff --git a/src/variables.scss b/src/variables.scss new file mode 100644 index 00000000000..f83b3404b05 --- /dev/null +++ b/src/variables.scss @@ -0,0 +1,120 @@ +$mobile-min: 320px; +$mobile-max: 639px; +$tablet-min: 640px; +$tablet-max: 1199px; +$desktop-min: 1200px; +$desktop-max: 1440px; + + +@font-face { + font-family: Mont; + src: url('/fonts/Mont-Regular.otf') format('opentype'); + font-weight: 400; + font-style: normal; +} + +@font-face { + font-family: Mont; + src: url('/fonts/Mont-SemiBold.otf') format('opentype'); + font-weight: 600; + font-style: normal; +} + +@font-face { + font-family: Mont; + src: url('/fonts/Mont-Bold.otf') format('opentype'); + font-weight: 700; + font-style: normal; +} + + +$top-padding: 80px; + +// Colors +$color-primary: #313237; +$color-secondary: #89939A; +$color-icons: #B4BDC3; +$color-elements: #E2E6E9; +$color-hover-BG: #FAFBFC; +$color-white: #FFF; +$color-green: #27AE60; +$color-red: #EB5757; + +// Typography (Desktop/Tablet) +$h1-styles: ( + font-size: 48px, + font-weight: 700, + line-height: 56px, + letter-spacing: -0.01em +); +$h2-styles: ( + font-size: 32px, + font-weight: 700, + line-height: 41px, + letter-spacing: -0.01em +); +$h3-styles: ( + font-size: 22px, + font-weight: 700, + line-height: 31px, + letter-spacing: 0 +); +$h4-styles: ( + font-size: 20px, + font-weight: 600, + line-height: 31px, + letter-spacing: 0 +); +$uppercase: ( + font-size: 12px, + font-weight: 800, + line-height: 11px, + letter-spacing: 0.04em +); +$buttons: ( + font-size: 14px, + font-weight: 600, + line-height: 21px, + letter-spacing: 0 +); +$font-main: 'Mont', sans-serif; +$body-text: ( + font-size: 14px, + font-weight: 400, + line-height: 21px, + letter-spacing: 0 +); +$small-text: ( + font-size: 12px, + font-weight: 600, + line-height: 15px, + letter-spacing: 0 +); + + +// Typography (Mobile) +$h1-styles-mobile: ( + font-size: 32px, + font-weight: 700, + line-height: 41px, + letter-spacing: -0.01em +); +$h2-styles-mobile: ( + font-size: 22px, + font-weight: 700, + line-height: 31px, + letter-spacing: 0 +); +$h3-styles-mobile: ( + font-size: 20px, + font-weight: 600, + line-height: 26px, + letter-spacing: 0 +); +$h4-styles-mobile: ( + font-size: 16px, + font-weight: 600, + line-height: 20px, + letter-spacing: 0 +); + diff --git a/tsconfig.json b/tsconfig.json index cfb168bb26c..8b3f573c38a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,8 +1,10 @@ { "extends": "@mate-academy/students-ts-config", "include": [ - "src" + "src/**/*", + "public/api/*.json", ], + "compilerOptions": { "sourceMap": false, "types": ["node", "cypress"]