diff --git a/.eslintrc.cjs b/.eslintrc.cjs
index b51149cf57..b11f948c00 100644
--- a/.eslintrc.cjs
+++ b/.eslintrc.cjs
@@ -5,7 +5,7 @@ module.exports = {
},
extends: [
'plugin:react/recommended',
- "plugin:react-hooks/recommended",
+ 'plugin:react-hooks/recommended',
'airbnb-typescript',
'plugin:@typescript-eslint/eslint-recommended',
'plugin:@typescript-eslint/recommended',
@@ -18,7 +18,7 @@ module.exports = {
'rules': {
'react/jsx-filename-extension': ['off'],
}
- }
+ },
],
parser: '@typescript-eslint/parser',
parserOptions: {
@@ -29,6 +29,7 @@ module.exports = {
project: './tsconfig.json',
sourceType: 'module',
},
+
plugins: [
'jsx-a11y',
'import',
@@ -38,6 +39,7 @@ module.exports = {
],
rules: {
// JS
+ 'import/no-extraneous-dependencies': 'off',
'semi': 'off',
'@typescript-eslint/semi': ['error', 'always'],
'prefer-const': 2,
diff --git a/.stylelintrc.js b/.stylelintrc.js
index f3a4e74272..8b8c13e209 100644
--- a/.stylelintrc.js
+++ b/.stylelintrc.js
@@ -1,4 +1,7 @@
module.exports = {
- extends: "@mate-academy/stylelint-config",
- rules: {}
+ extends: '@mate-academy/stylelint-config',
+ rules: {
+ 'media-feature-range-notation': null,
+ 'no-descending-specificity': null,
+ }
};
diff --git a/README.md b/README.md
deleted file mode 100644
index 98eb0bd1c0..0000000000
--- a/README.md
+++ /dev/null
@@ -1,143 +0,0 @@
-# React Product Catalog
-
-Implement the catalog with a shopping cart and favorites page according to one of the next designs:
-
-- [Original](https://www.figma.com/file/T5ttF21UnT6RRmCQQaZc6L/Phone-catalog-(V2)-Original)
-- [Original Dark](https://www.figma.com/file/BUusqCIMAWALqfBahnyIiH/Phone-catalog-(V2)-Original-Dark)
-- [Rounded Blue](https://www.figma.com/file/FRxncC4lfyhs6og1L6FGEU/Phone-catalog-(V2)-Rounded-Style-2?node-id=0%3A1)
-- [Rounded Purple](https://www.figma.com/file/xMK2Dy0mfBbJJSNctmOuLW/Phone-catalog-(V2)-Rounded-Style-1?node-id=0%3A1)
-- [Rounded Orange](https://www.figma.com/file/7JTa0q8n3dTSAyMNaA0u8o/Phone-catalog-(V2)-Rounded-Style-3?node-id=0%3A1)
-
-You may also implement color theme switching!
-
-## If you work in a team
-
-Follow the [Work in a team guideline](https://github.com/mate-academy/react_task-guideline/blob/master/team-flow.md#how-to-work-in-a-team)
-
-## Project Setup from scratch
-
-Follow the [Instruction](https://github.com/mate-academy/react_phone-catalog/blob/master/setup.md) to setup your project, add Eslint, Prettier, Husky and enable auto deploy.
-
-## Data
-
-Use the data from `/public/api` and images from `/public/img` folders. You can reorganize them the way you like.
-
-## App
-
-1. Put components into the `src/components` folder.
- - Each component should be a folder with `index.ts`, `ComponentName.tsx`, `ComponentName.module.scss` files.
- - Use CSS modules.
- - Keep `.module.scss` files together with their components.
-2. Advanced project structure:
- - `src/modules` folder. Inside per page modules `HomePage`, `CartPage`, etc., and `shared` folder with shared content between modules.
- - Inside each module its own `components` folder with the structure described above. And optionally other files/folders: `hooks`, `constants`, and so on.
-3. Add the sticky header with a logo, navigation, favorites, and cart.
-4. The footer with the link to the GitHub repo and `Back to top` button.
- - The content should be limited to the same width as the page content;
- - `Back to top` button should scroll to the top smoothly;
-5. Add `NotFoundPage` containing text `Page not found` for all the unknown URLs.
-6. All changes the hover effects should be smooth.
-7. Scale all image links by 10% on hover.
-8. Implement all form elements and icons according to the UI Kit.
-
-## Home page
-
-Implement Home page at available at `/`.
-
-1. `
Product Catalog ` should be visually hidden.
-2. `PicturesSlider`:
- - Find your own images to personalize the App;
- - Change pictures automatically every 5 seconds;
- - The next buttons should show the first image after the last one;
- - Dashes at the bottom should allow choosing an exact picture.
-3. `ProductsSlider` for the `Hot prices` block:
- - The products with a discount starting from the biggest absolute value;
- - `<` and `>` buttons should scroll products.
-4. `Shop by category` block with links to `/phones`, `/tablets`, and `/accessories`.
-5. Add Brand new block using ProductsSlider with products that are the newest according to the year field.
-
-## Product pages
-
-There should be 3 separate pages `/phones`, `/tablets`, and `/accessories`.
-
-1. Each page loads the data of the required `type`.
-2. Add an `h1` with `Phones/Tablets/Accessories page` (choose required).
-3. Add `ProductsList` component showing all the `products`.
-4. Implement a `Loader` to show it while waiting for the data from the server.
-5. In case of a loading error show the something went wrong message with a reload button.
-6. If there are no products available show the `There are no phones/tablets/accessories yet` message (choose required).
-7. Add a `` with the `Newest`, `Alphabetically`, and `Cheapest` options to sort products by `age`, `title`, or `price` (after discount).
- - Save the sort value in the URL `?sort=age` and apply it after the page reload.
-8. Add `Pagination` buttons and `Items on page` select element with `4`, `8`, `16`, and `all` options.
- - It should limit the products you show to the user;
- - Save pagination params in the URL `?page=2&perPage=8` (`page=1` and `perPage=all` are the default values and should not be added to the URL;
- - Hide pagination elements if they do not make sense;
- - You can use the logic explained in [the React Pagination task](https://github.com/mate-academy/react_pagination#react-pagination).
-
-## Product details page
-
-Create `ProductDetailsPage` available at `/product/:productId`.
-
-1. `ProductCard` image and title should be links to the product details page.
-2. Use `Loader` when fetching the product details.
-3. Show the details on the page:
- - Display the available colors from colorsAvailable and the capacities from capacityAvailable as radio inputs, allowing the selection of one value from the offered options;
- - `About` section should contain just a description (without any subheaders);
- - Choose `Tech specs` you want to show.
-4. Add the ability to choose a picture.
-5. Implement `You may also like` block with products chosen randomly:
- - Create `getSuggestedProducts` method fetching the suggested products.
-6. Add `Back` button working the same way as a Browser `Back` button.
-7. Add `Breadcrumbs` at the top with:
- - A Home page link;
- - A category page link (`Phones`, `Tablets`, `Accessories`);
- - The name of the product (just a text).
-8. Show `Product was not found` if there is no product with a given id on the server.
-
-## Shopping Cart page
-
-Create a Cart page with a list of `CartItem`s at `/cart`.
-Each item should have an `id`, `quantity`, and a `product`.
-Use React Context or Redux to store Items.
-
-1. `Add to cart` button in the `ProductCard` should add a product to the `Cart`.
-2. If the product is already in the `Cart` the button should say `Added to cart` and do nothing.
-3. Add the ability to remove items from the `Cart` with an `x` button next to a `CartItem`.
-4. Add a message `Your cart is empty` when there are no products in the `Cart`.
-5. Add the ability to change the item quantity in the `Cart` with `-` and `+` buttons (it should be > 0).
-6. Total amount and quantity should be calculated automatically.
-7. Show the quantity at the `Cart` icon in the header.
-8. Save the `Cart` to `localStorage` on each change and read it on page load.
-9. `Checkout` button should show a modal dialog with the text `Checkout is not implemented yet. Do you want to clear the Cart?`:
- - Clear the Cart if the user confirms the order;
- - Keep the Cart items and close the confirmation on cancel;
- - Use the `confirm` function if you don't have a better solution.
-
-## Favorites page
-
-Create `Favorites` page with a `ProductsList` showing favorite products at `/favorites`.
-
-1. Add/remove a product to favorites by pressing a heart button in the `ProductCard` element.
-2. The heart should be highlighted if the product is already added to the favorites.
-3. Use React Context or Redux to store the favorites.
-4. Show the number of favorites at the `Favorites` icon in the header.
-5. Save favorites to `localStorage` on each change and load them on page load.
-
-## Other tasks
-
-1. Add `NotFoundPage` containing text `Page not found` for all the other URLs with the link to `HomePage`.
-2. Implement the `Product was not found` state for the `ProductDetailsPage`.
-
-## (*) Advanced tasks
-
-- Implement color theme switching!
-- Use [skeletons](https://freefrontend.com/css-skeleton-loadings/) to make loading more natural.
-- Add the ability to change page language.
-
-### Search
-
-Show `input:search` in the header when a page contains a `ProductList` to search in.
-
-1. Save the `Search` value in the URL as a `?query=value` to apply on page load.
-2. Show `There are no phones/tablets/accessories/products matching the query` instead of `ProductList` when needed.
-3. Add `debounce` to the search field.
diff --git a/index.html b/index.html
index 095fb3a453..8db8d696d8 100644
--- a/index.html
+++ b/index.html
@@ -3,7 +3,8 @@
- Vite + React + TS
+ Phone Catalog
+
diff --git a/package-lock.json b/package-lock.json
index 836b9e63b4..47c958c43a 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -11,21 +11,37 @@
"license": "GPL-3.0",
"dependencies": {
"@fortawesome/fontawesome-free": "^6.5.2",
+ "@reduxjs/toolkit": "^2.5.0",
"bulma": "^1.0.1",
"classnames": "^2.5.1",
+ "i18next": "^24.1.0",
+ "i18next-browser-languagedetector": "^8.0.2",
+ "lodash": "^4.17.21",
"react": "^18.3.1",
+ "react-content-loader": "^7.0.2",
"react-dom": "^18.3.1",
+ "react-hot-toast": "^2.4.1",
+ "react-i18next": "^15.2.0",
+ "react-redux": "^9.2.0",
+ "react-responsive": "^10.0.0",
"react-router-dom": "^6.25.1",
- "react-transition-group": "^4.4.5"
+ "react-slick": "^0.30.3",
+ "react-toastify": "^11.0.2",
+ "react-transition-group": "^4.4.5",
+ "redux": "^5.0.1",
+ "slick-carousel": "^1.8.1",
+ "swiper": "^11.1.15"
},
"devDependencies": {
"@cypress/react18": "^2.0.1",
- "@mate-academy/scripts": "^1.8.5",
+ "@mate-academy/scripts": "^1.9.12",
"@mate-academy/students-ts-config": "*",
- "@mate-academy/stylelint-config": "*",
+ "@mate-academy/stylelint-config": "^0.0.12",
+ "@types/lodash": "^4.17.13",
"@types/node": "^20.14.10",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
+ "@types/react-slick": "^0.23.13",
"@types/react-transition-group": "^4.4.10",
"@typescript-eslint/parser": "^7.16.0",
"@vitejs/plugin-react": "^4.3.1",
@@ -43,9 +59,13 @@
"mochawesome": "^7.1.3",
"mochawesome-merge": "^4.3.0",
"mochawesome-report-generator": "^6.2.0",
- "prettier": "^3.3.2",
+ "prettier": "^3.4.2",
"sass": "^1.77.8",
- "stylelint": "^16.7.0",
+ "stylelint": "^16.13.2",
+ "stylelint-config-recommended-scss": "^14.1.0",
+ "stylelint-config-sass-guidelines": "^12.1.0",
+ "stylelint-config-standard": "^37.0.0",
+ "stylelint-scss": "^6.10.1",
"typescript": "^5.2.2",
"vite": "^5.3.1"
}
@@ -365,9 +385,10 @@
}
},
"node_modules/@babel/runtime": {
- "version": "7.24.8",
- "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.8.tgz",
- "integrity": "sha512-5F7SDGs1T72ZczbRwbGO9lQi0NLjQxzl6i4lJxLxfW9U5UluCSyEJeniWvnhl3/euNiqQVbo8zruhsDfid0esA==",
+ "version": "7.26.0",
+ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.0.tgz",
+ "integrity": "sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw==",
+ "license": "MIT",
"dependencies": {
"regenerator-runtime": "^0.14.0"
},
@@ -449,6 +470,7 @@
"url": "https://opencollective.com/csstools"
}
],
+ "peer": true,
"engines": {
"node": "^14 || ^16 || >=18"
},
@@ -471,6 +493,7 @@
"url": "https://opencollective.com/csstools"
}
],
+ "peer": true,
"engines": {
"node": "^14 || ^16 || >=18"
}
@@ -490,6 +513,7 @@
"url": "https://opencollective.com/csstools"
}
],
+ "peer": true,
"engines": {
"node": "^14 || ^16 || >=18"
},
@@ -513,6 +537,7 @@
"url": "https://opencollective.com/csstools"
}
],
+ "peer": true,
"engines": {
"node": "^14 || ^16 || >=18"
},
@@ -1183,11 +1208,47 @@
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
+ "node_modules/@keyv/serialize": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/@keyv/serialize/-/serialize-1.0.2.tgz",
+ "integrity": "sha512-+E/LyaAeuABniD/RvUezWVXKpeuvwLEA9//nE9952zBaOdBd2mQ3pPoM8cUe2X6IcMByfuSLzmYqnYshG60+HQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "buffer": "^6.0.3"
+ }
+ },
+ "node_modules/@keyv/serialize/node_modules/buffer": {
+ "version": "6.0.3",
+ "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
+ "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "base64-js": "^1.3.1",
+ "ieee754": "^1.2.1"
+ }
+ },
"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": "1.9.12",
+ "resolved": "https://registry.npmjs.org/@mate-academy/scripts/-/scripts-1.9.12.tgz",
+ "integrity": "sha512-/OcmxMa34lYLFlGx7Ig926W1U1qjrnXbjFJ2TzUcDaLmED+A5se652NcWwGOidXRuMAOYLPU2jNYBEkKyXrFJA==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"@octokit/rest": "^17.11.2",
"@types/get-port": "^4.2.0",
@@ -1215,6 +1276,7 @@
"resolved": "https://registry.npmjs.org/@mate-academy/stylelint-config/-/stylelint-config-0.0.12.tgz",
"integrity": "sha512-KVf6pK0SwFP4zYfNkj68+LuHRPzx/F5GNeCaPQQauDm3X08Crj/X15fu/l9XvUD2ttEAi8dcASSABuGx54rPVA==",
"dev": true,
+ "license": "ISC",
"dependencies": {
"stylelint-config-standard-scss": "^11.1.0",
"stylelint-scss": "^5.3.0"
@@ -1875,6 +1937,30 @@
"url": "https://opencollective.com/unts"
}
},
+ "node_modules/@reduxjs/toolkit": {
+ "version": "2.5.0",
+ "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.5.0.tgz",
+ "integrity": "sha512-awNe2oTodsZ6LmRqmkFhtb/KH03hUhxOamEQy411m3Njj3BbFvoBovxo4Q1cBWnV1ErprVj9MlF0UPXkng0eyg==",
+ "license": "MIT",
+ "dependencies": {
+ "immer": "^10.0.3",
+ "redux": "^5.0.1",
+ "redux-thunk": "^3.1.0",
+ "reselect": "^5.1.0"
+ },
+ "peerDependencies": {
+ "react": "^16.9.0 || ^17.0.0 || ^18 || ^19",
+ "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0"
+ },
+ "peerDependenciesMeta": {
+ "react": {
+ "optional": true
+ },
+ "react-redux": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@remix-run/router": {
"version": "1.18.0",
"resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.18.0.tgz",
@@ -1884,208 +1970,266 @@
}
},
"node_modules/@rollup/rollup-android-arm-eabi": {
- "version": "4.18.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.18.1.tgz",
- "integrity": "sha512-lncuC4aHicncmbORnx+dUaAgzee9cm/PbIqgWz1PpXuwc+sa1Ct83tnqUDy/GFKleLiN7ZIeytM6KJ4cAn1SxA==",
+ "version": "4.28.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.28.1.tgz",
+ "integrity": "sha512-2aZp8AES04KI2dy3Ss6/MDjXbwBzj+i0GqKtWXgw2/Ma6E4jJvujryO6gJAghIRVz7Vwr9Gtl/8na3nDUKpraQ==",
"cpu": [
"arm"
],
"dev": true,
+ "license": "MIT",
"optional": true,
"os": [
"android"
]
},
"node_modules/@rollup/rollup-android-arm64": {
- "version": "4.18.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.18.1.tgz",
- "integrity": "sha512-F/tkdw0WSs4ojqz5Ovrw5r9odqzFjb5LIgHdHZG65dFI1lWTWRVy32KDJLKRISHgJvqUeUhdIvy43fX41znyDg==",
+ "version": "4.28.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.28.1.tgz",
+ "integrity": "sha512-EbkK285O+1YMrg57xVA+Dp0tDBRB93/BZKph9XhMjezf6F4TpYjaUSuPt5J0fZXlSag0LmZAsTmdGGqPp4pQFA==",
"cpu": [
"arm64"
],
"dev": true,
+ "license": "MIT",
"optional": true,
"os": [
"android"
]
},
"node_modules/@rollup/rollup-darwin-arm64": {
- "version": "4.18.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.18.1.tgz",
- "integrity": "sha512-vk+ma8iC1ebje/ahpxpnrfVQJibTMyHdWpOGZ3JpQ7Mgn/3QNHmPq7YwjZbIE7km73dH5M1e6MRRsnEBW7v5CQ==",
+ "version": "4.28.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.28.1.tgz",
+ "integrity": "sha512-prduvrMKU6NzMq6nxzQw445zXgaDBbMQvmKSJaxpaZ5R1QDM8w+eGxo6Y/jhT/cLoCvnZI42oEqf9KQNYz1fqQ==",
"cpu": [
"arm64"
],
"dev": true,
+ "license": "MIT",
"optional": true,
"os": [
"darwin"
]
},
"node_modules/@rollup/rollup-darwin-x64": {
- "version": "4.18.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.18.1.tgz",
- "integrity": "sha512-IgpzXKauRe1Tafcej9STjSSuG0Ghu/xGYH+qG6JwsAUxXrnkvNHcq/NL6nz1+jzvWAnQkuAJ4uIwGB48K9OCGA==",
+ "version": "4.28.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.28.1.tgz",
+ "integrity": "sha512-WsvbOunsUk0wccO/TV4o7IKgloJ942hVFK1CLatwv6TJspcCZb9umQkPdvB7FihmdxgaKR5JyxDjWpCOp4uZlQ==",
"cpu": [
"x64"
],
"dev": true,
+ "license": "MIT",
"optional": true,
"os": [
"darwin"
]
},
+ "node_modules/@rollup/rollup-freebsd-arm64": {
+ "version": "4.28.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.28.1.tgz",
+ "integrity": "sha512-HTDPdY1caUcU4qK23FeeGxCdJF64cKkqajU0iBnTVxS8F7H/7BewvYoG+va1KPSL63kQ1PGNyiwKOfReavzvNA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-x64": {
+ "version": "4.28.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.28.1.tgz",
+ "integrity": "sha512-m/uYasxkUevcFTeRSM9TeLyPe2QDuqtjkeoTpP9SW0XxUWfcYrGDMkO/m2tTw+4NMAF9P2fU3Mw4ahNvo7QmsQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
- "version": "4.18.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.18.1.tgz",
- "integrity": "sha512-P9bSiAUnSSM7EmyRK+e5wgpqai86QOSv8BwvkGjLwYuOpaeomiZWifEos517CwbG+aZl1T4clSE1YqqH2JRs+g==",
+ "version": "4.28.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.28.1.tgz",
+ "integrity": "sha512-QAg11ZIt6mcmzpNE6JZBpKfJaKkqTm1A9+y9O+frdZJEuhQxiugM05gnCWiANHj4RmbgeVJpTdmKRmH/a+0QbA==",
"cpu": [
"arm"
],
"dev": true,
+ "license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
- "version": "4.18.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.18.1.tgz",
- "integrity": "sha512-5RnjpACoxtS+aWOI1dURKno11d7krfpGDEn19jI8BuWmSBbUC4ytIADfROM1FZrFhQPSoP+KEa3NlEScznBTyQ==",
+ "version": "4.28.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.28.1.tgz",
+ "integrity": "sha512-dRP9PEBfolq1dmMcFqbEPSd9VlRuVWEGSmbxVEfiq2cs2jlZAl0YNxFzAQS2OrQmsLBLAATDMb3Z6MFv5vOcXg==",
"cpu": [
"arm"
],
"dev": true,
+ "license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-arm64-gnu": {
- "version": "4.18.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.18.1.tgz",
- "integrity": "sha512-8mwmGD668m8WaGbthrEYZ9CBmPug2QPGWxhJxh/vCgBjro5o96gL04WLlg5BA233OCWLqERy4YUzX3bJGXaJgQ==",
+ "version": "4.28.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.28.1.tgz",
+ "integrity": "sha512-uGr8khxO+CKT4XU8ZUH1TTEUtlktK6Kgtv0+6bIFSeiSlnGJHG1tSFSjm41uQ9sAO/5ULx9mWOz70jYLyv1QkA==",
"cpu": [
"arm64"
],
"dev": true,
+ "license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-arm64-musl": {
- "version": "4.18.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.18.1.tgz",
- "integrity": "sha512-dJX9u4r4bqInMGOAQoGYdwDP8lQiisWb9et+T84l2WXk41yEej8v2iGKodmdKimT8cTAYt0jFb+UEBxnPkbXEQ==",
+ "version": "4.28.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.28.1.tgz",
+ "integrity": "sha512-QF54q8MYGAqMLrX2t7tNpi01nvq5RI59UBNx+3+37zoKX5KViPo/gk2QLhsuqok05sSCRluj0D00LzCwBikb0A==",
"cpu": [
"arm64"
],
"dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loongarch64-gnu": {
+ "version": "4.28.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.28.1.tgz",
+ "integrity": "sha512-vPul4uodvWvLhRco2w0GcyZcdyBfpfDRgNKU+p35AWEbJ/HPs1tOUrkSueVbBS0RQHAf/A+nNtDpvw95PeVKOA==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
- "version": "4.18.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.18.1.tgz",
- "integrity": "sha512-V72cXdTl4EI0x6FNmho4D502sy7ed+LuVW6Ym8aI6DRQ9hQZdp5sj0a2usYOlqvFBNKQnLQGwmYnujo2HvjCxQ==",
+ "version": "4.28.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.28.1.tgz",
+ "integrity": "sha512-pTnTdBuC2+pt1Rmm2SV7JWRqzhYpEILML4PKODqLz+C7Ou2apEV52h19CR7es+u04KlqplggmN9sqZlekg3R1A==",
"cpu": [
"ppc64"
],
"dev": true,
+ "license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
- "version": "4.18.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.18.1.tgz",
- "integrity": "sha512-f+pJih7sxoKmbjghrM2RkWo2WHUW8UbfxIQiWo5yeCaCM0TveMEuAzKJte4QskBp1TIinpnRcxkquY+4WuY/tg==",
+ "version": "4.28.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.28.1.tgz",
+ "integrity": "sha512-vWXy1Nfg7TPBSuAncfInmAI/WZDd5vOklyLJDdIRKABcZWojNDY0NJwruY2AcnCLnRJKSaBgf/GiJfauu8cQZA==",
"cpu": [
"riscv64"
],
"dev": true,
+ "license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-s390x-gnu": {
- "version": "4.18.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.18.1.tgz",
- "integrity": "sha512-qb1hMMT3Fr/Qz1OKovCuUM11MUNLUuHeBC2DPPAWUYYUAOFWaxInaTwTQmc7Fl5La7DShTEpmYwgdt2hG+4TEg==",
+ "version": "4.28.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.28.1.tgz",
+ "integrity": "sha512-/yqC2Y53oZjb0yz8PVuGOQQNOTwxcizudunl/tFs1aLvObTclTwZ0JhXF2XcPT/zuaymemCDSuuUPXJJyqeDOg==",
"cpu": [
"s390x"
],
"dev": true,
+ "license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-x64-gnu": {
- "version": "4.18.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.18.1.tgz",
- "integrity": "sha512-7O5u/p6oKUFYjRbZkL2FLbwsyoJAjyeXHCU3O4ndvzg2OFO2GinFPSJFGbiwFDaCFc+k7gs9CF243PwdPQFh5g==",
+ "version": "4.28.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.28.1.tgz",
+ "integrity": "sha512-fzgeABz7rrAlKYB0y2kSEiURrI0691CSL0+KXwKwhxvj92VULEDQLpBYLHpF49MSiPG4sq5CK3qHMnb9tlCjBw==",
"cpu": [
"x64"
],
"dev": true,
+ "license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-x64-musl": {
- "version": "4.18.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.18.1.tgz",
- "integrity": "sha512-pDLkYITdYrH/9Cv/Vlj8HppDuLMDUBmgsM0+N+xLtFd18aXgM9Nyqupb/Uw+HeidhfYg2lD6CXvz6CjoVOaKjQ==",
+ "version": "4.28.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.28.1.tgz",
+ "integrity": "sha512-xQTDVzSGiMlSshpJCtudbWyRfLaNiVPXt1WgdWTwWz9n0U12cI2ZVtWe/Jgwyv/6wjL7b66uu61Vg0POWVfz4g==",
"cpu": [
"x64"
],
"dev": true,
+ "license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-win32-arm64-msvc": {
- "version": "4.18.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.18.1.tgz",
- "integrity": "sha512-W2ZNI323O/8pJdBGil1oCauuCzmVd9lDmWBBqxYZcOqWD6aWqJtVBQ1dFrF4dYpZPks6F+xCZHfzG5hYlSHZ6g==",
+ "version": "4.28.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.28.1.tgz",
+ "integrity": "sha512-wSXmDRVupJstFP7elGMgv+2HqXelQhuNf+IS4V+nUpNVi/GUiBgDmfwD0UGN3pcAnWsgKG3I52wMOBnk1VHr/A==",
"cpu": [
"arm64"
],
"dev": true,
+ "license": "MIT",
"optional": true,
"os": [
"win32"
]
},
"node_modules/@rollup/rollup-win32-ia32-msvc": {
- "version": "4.18.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.18.1.tgz",
- "integrity": "sha512-ELfEX1/+eGZYMaCIbK4jqLxO1gyTSOIlZr6pbC4SRYFaSIDVKOnZNMdoZ+ON0mrFDp4+H5MhwNC1H/AhE3zQLg==",
+ "version": "4.28.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.28.1.tgz",
+ "integrity": "sha512-ZkyTJ/9vkgrE/Rk9vhMXhf8l9D+eAhbAVbsGsXKy2ohmJaWg0LPQLnIxRdRp/bKyr8tXuPlXhIoGlEB5XpJnGA==",
"cpu": [
"ia32"
],
"dev": true,
+ "license": "MIT",
"optional": true,
"os": [
"win32"
]
},
"node_modules/@rollup/rollup-win32-x64-msvc": {
- "version": "4.18.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.18.1.tgz",
- "integrity": "sha512-yjk2MAkQmoaPYCSu35RLJ62+dz358nE83VfTePJRp8CG7aMg25mEJYpXFiD+NcevhX8LxD5OP5tktPXnXN7GDw==",
+ "version": "4.28.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.28.1.tgz",
+ "integrity": "sha512-ZvK2jBafvttJjoIdKm/Q/Bh7IJ1Ose9IBOwpOXcOvW3ikGTQGmKDgxTC6oCAzW6PynbkKP8+um1du81XJHZ0JA==",
"cpu": [
"x64"
],
"dev": true,
+ "license": "MIT",
"optional": true,
"os": [
"win32"
@@ -2126,6 +2270,110 @@
"integrity": "sha512-sXXKG+uL9IrKqViTtao2Ws6dy0znu9sOaP1di/jKGW1M6VssO8vlpXCQcpZ+jisQ1tTFAC5Jo/EOzFbggBagFQ==",
"dev": true
},
+ "node_modules/@stylistic/stylelint-plugin": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/@stylistic/stylelint-plugin/-/stylelint-plugin-3.1.1.tgz",
+ "integrity": "sha512-XagAHHIa528EvyGybv8EEYGK5zrVW74cHpsjhtovVATbhDRuJYfE+X4HCaAieW9lCkwbX6L+X0I4CiUG3w/hFw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@csstools/css-parser-algorithms": "^3.0.1",
+ "@csstools/css-tokenizer": "^3.0.1",
+ "@csstools/media-query-list-parser": "^3.0.1",
+ "is-plain-object": "^5.0.0",
+ "postcss-selector-parser": "^6.1.2",
+ "postcss-value-parser": "^4.2.0",
+ "style-search": "^0.1.0",
+ "stylelint": "^16.8.2"
+ },
+ "engines": {
+ "node": "^18.12 || >=20.9"
+ },
+ "peerDependencies": {
+ "stylelint": "^16.8.0"
+ }
+ },
+ "node_modules/@stylistic/stylelint-plugin/node_modules/@csstools/css-parser-algorithms": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.4.tgz",
+ "integrity": "sha512-Up7rBoV77rv29d3uKHUIVubz1BTcgyUK72IvCQAbfbMv584xHcGKCKbWh7i8hPrRJ7qU4Y8IO3IY9m+iTB7P3A==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "@csstools/css-tokenizer": "^3.0.3"
+ }
+ },
+ "node_modules/@stylistic/stylelint-plugin/node_modules/@csstools/css-tokenizer": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.3.tgz",
+ "integrity": "sha512-UJnjoFsmxfKUdNYdWgOB0mWUypuLvAfQPH1+pyvRJs6euowbFkFC6P13w1l8mJyi3vxYMxc9kld5jZEGRQs6bw==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@stylistic/stylelint-plugin/node_modules/@csstools/media-query-list-parser": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/@csstools/media-query-list-parser/-/media-query-list-parser-3.0.1.tgz",
+ "integrity": "sha512-HNo8gGD02kHmcbX6PvCoUuOQvn4szyB9ca63vZHKX5A81QytgDG4oxG4IaEfHTlEZSZ6MjPEMWIVU+zF2PZcgw==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "@csstools/css-parser-algorithms": "^3.0.1",
+ "@csstools/css-tokenizer": "^3.0.1"
+ }
+ },
+ "node_modules/@stylistic/stylelint-plugin/node_modules/postcss-selector-parser": {
+ "version": "6.1.2",
+ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz",
+ "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "cssesc": "^3.0.0",
+ "util-deprecate": "^1.0.2"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
"node_modules/@types/babel__core": {
"version": "7.20.5",
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
@@ -2168,10 +2416,11 @@
}
},
"node_modules/@types/estree": {
- "version": "1.0.5",
- "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz",
- "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==",
- "dev": true
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz",
+ "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==",
+ "dev": true,
+ "license": "MIT"
},
"node_modules/@types/get-port": {
"version": "4.2.0",
@@ -2189,6 +2438,13 @@
"integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==",
"dev": true
},
+ "node_modules/@types/lodash": {
+ "version": "4.17.13",
+ "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.13.tgz",
+ "integrity": "sha512-lfx+dftrEZcdBPczf9d0Qv0x+j/rfNCMuC6OcfXmO8gkfeNAY88PgKUbvG56whcN23gc27yenwF6oJZXGFpYxg==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/@types/minimist": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.5.tgz",
@@ -2216,13 +2472,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"
@@ -2237,6 +2493,16 @@
"@types/react": "*"
}
},
+ "node_modules/@types/react-slick": {
+ "version": "0.23.13",
+ "resolved": "https://registry.npmjs.org/@types/react-slick/-/react-slick-0.23.13.tgz",
+ "integrity": "sha512-bNZfDhe/L8t5OQzIyhrRhBr/61pfBcWaYJoq6UDqFtv5LMwfg4NsVDD2J8N01JqdAdxLjOt66OZEp6PX+dGs/A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/react": "*"
+ }
+ },
"node_modules/@types/react-transition-group": {
"version": "4.4.10",
"resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.10.tgz",
@@ -2258,6 +2524,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",
@@ -3080,6 +3352,27 @@
"resolved": "https://registry.npmjs.org/bulma/-/bulma-1.0.1.tgz",
"integrity": "sha512-+xv/BIAEQakHkR0QVz+s+RjNqfC53Mx9ZYexyaFNFo9wx5i76HXArNdwW7bccyJxa5mgV/T5DcVGqsAB19nBJQ=="
},
+ "node_modules/cacheable": {
+ "version": "1.8.7",
+ "resolved": "https://registry.npmjs.org/cacheable/-/cacheable-1.8.7.tgz",
+ "integrity": "sha512-AbfG7dAuYNjYxFUtL1lAqmlWdxczCJ47w7cFjhGcnGnUdwSo6VgmSojfoW3tUI12HUkgTJ5kqj78yyq6TsFtlg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "hookified": "^1.6.0",
+ "keyv": "^5.2.3"
+ }
+ },
+ "node_modules/cacheable/node_modules/keyv": {
+ "version": "5.2.3",
+ "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.2.3.tgz",
+ "integrity": "sha512-AGKecUfzrowabUv0bH1RIR5Vf7w+l4S3xtQAypKaUpTdIR1EbrAcTxHCrpo9Q+IWeUlFE2palRtgIQcgm+PQJw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@keyv/serialize": "^1.0.2"
+ }
+ },
"node_modules/cachedir": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/cachedir/-/cachedir-2.4.0.tgz",
@@ -3331,6 +3624,15 @@
"wrap-ansi": "^7.0.0"
}
},
+ "node_modules/clsx": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
+ "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/color-convert": {
"version": "1.9.3",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
@@ -3463,10 +3765,11 @@
}
},
"node_modules/cross-spawn": {
- "version": "7.0.3",
- "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
- "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
+ "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",
@@ -3477,19 +3780,27 @@
}
},
"node_modules/css-functions-list": {
- "version": "3.2.2",
- "resolved": "https://registry.npmjs.org/css-functions-list/-/css-functions-list-3.2.2.tgz",
- "integrity": "sha512-c+N0v6wbKVxTu5gOBBFkr9BEdBWaqqjQeiJ8QvSRIJOf+UxlJh930m8e6/WNeODIK0mYLFkoONrnj16i2EcvfQ==",
+ "version": "3.2.3",
+ "resolved": "https://registry.npmjs.org/css-functions-list/-/css-functions-list-3.2.3.tgz",
+ "integrity": "sha512-IQOkD3hbR5KrN93MtcYuad6YPuTSUhntLHDuLEbFWE+ff2/XSZNdZG+LcbbIW5AXKg/WFIfYItIzVoHngHXZzA==",
"dev": true,
+ "license": "MIT",
"engines": {
"node": ">=12 || >=16"
}
},
+ "node_modules/css-mediaquery": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/css-mediaquery/-/css-mediaquery-0.1.2.tgz",
+ "integrity": "sha512-COtn4EROW5dBGlE/4PiKnh6rZpAPxDeFLaEEwt4i10jpDMFt2EhQGS79QmmrO+iKCHv0PU/HrOWEhijFd1x99Q==",
+ "license": "BSD"
+ },
"node_modules/css-tree": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz",
"integrity": "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==",
"dev": true,
+ "peer": true,
"dependencies": {
"mdn-data": "2.0.30",
"source-map-js": "^1.0.1"
@@ -3751,12 +4062,13 @@
"dev": true
},
"node_modules/debug": {
- "version": "4.3.5",
- "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz",
- "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==",
+ "version": "4.4.0",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
+ "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
"dev": true,
+ "license": "MIT",
"dependencies": {
- "ms": "2.1.2"
+ "ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
@@ -3992,6 +4304,12 @@
"once": "^1.4.0"
}
},
+ "node_modules/enquire.js": {
+ "version": "2.1.6",
+ "resolved": "https://registry.npmjs.org/enquire.js/-/enquire.js-2.1.6.tgz",
+ "integrity": "sha512-/KujNpO+PT63F7Hlpu4h3pE3TokKRHN26JYmQpPyjkRD/N57R7bPDNojMXdi7uveAKjYB7yQnartCxZnFWr0Xw==",
+ "license": "MIT"
+ },
"node_modules/enquirer": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.4.1.tgz",
@@ -5034,16 +5352,17 @@
"dev": true
},
"node_modules/fast-glob": {
- "version": "3.3.2",
- "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz",
- "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==",
+ "version": "3.3.3",
+ "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz",
+ "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"@nodelib/fs.stat": "^2.0.2",
"@nodelib/fs.walk": "^1.2.3",
"glob-parent": "^5.1.2",
"merge2": "^1.3.0",
- "micromatch": "^4.0.4"
+ "micromatch": "^4.0.8"
},
"engines": {
"node": ">=8.6.0"
@@ -5229,10 +5548,11 @@
}
},
"node_modules/flatted": {
- "version": "3.3.1",
- "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz",
- "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==",
- "dev": true
+ "version": "3.3.2",
+ "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.2.tgz",
+ "integrity": "sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA==",
+ "dev": true,
+ "license": "ISC"
},
"node_modules/for-each": {
"version": "0.3.3",
@@ -5714,6 +6034,15 @@
"integrity": "sha512-xYfnw62CKG8nLkZBfWbhWwDw02CHty86jfPcc2cr3ZfeuK9ysoVPPEUxf21bAD/rWAgk52SuBrLJlefNy8mvFg==",
"dev": true
},
+ "node_modules/goober": {
+ "version": "2.1.16",
+ "resolved": "https://registry.npmjs.org/goober/-/goober-2.1.16.tgz",
+ "integrity": "sha512-erjk19y1U33+XAMe1VTvIONHYoSqE4iS7BYUZfHaqeohLmnC0FdxEh7rQU+6MZ4OajItzjZFSRtVANrQwNq6/g==",
+ "license": "MIT",
+ "peerDependencies": {
+ "csstype": "^3.0.10"
+ }
+ },
"node_modules/gopd": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz",
@@ -5839,6 +6168,13 @@
"he": "bin/he"
}
},
+ "node_modules/hookified": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/hookified/-/hookified-1.6.0.tgz",
+ "integrity": "sha512-se7cpwTA+iA/eY548Bu03JJqBiEZAqU2jnyKdj5B5qurtBg64CZGHTgqCv4Yh7NWu6FGI09W61MCq+NoPj9GXA==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/hosted-git-info": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz",
@@ -5872,6 +6208,15 @@
"dev": true,
"peer": true
},
+ "node_modules/html-parse-stringify": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz",
+ "integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==",
+ "license": "MIT",
+ "dependencies": {
+ "void-elements": "3.1.0"
+ }
+ },
"node_modules/html-tags": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/html-tags/-/html-tags-3.3.1.tgz",
@@ -5907,6 +6252,52 @@
"node": ">=8.12.0"
}
},
+ "node_modules/hyphenate-style-name": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/hyphenate-style-name/-/hyphenate-style-name-1.1.0.tgz",
+ "integrity": "sha512-WDC/ui2VVRrz3jOVi+XtjqkDjiVjTtFaAGiW37k6b+ohyQ5wYDOGkvCZa8+H0nx3gyvv0+BST9xuOgIyGQ00gw==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/i18next": {
+ "version": "24.1.0",
+ "resolved": "https://registry.npmjs.org/i18next/-/i18next-24.1.0.tgz",
+ "integrity": "sha512-suKlX82AlptkMUO5YRfaAeH4FQyyKvR66jNaubTMiyPPMx7INU6PXAiy3PGULc0q6K+t9nxmDf/TRj9KjAivmw==",
+ "funding": [
+ {
+ "type": "individual",
+ "url": "https://locize.com"
+ },
+ {
+ "type": "individual",
+ "url": "https://locize.com/i18next.html"
+ },
+ {
+ "type": "individual",
+ "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.23.2"
+ },
+ "peerDependencies": {
+ "typescript": "^5"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/i18next-browser-languagedetector": {
+ "version": "8.0.2",
+ "resolved": "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-8.0.2.tgz",
+ "integrity": "sha512-shBvPmnIyZeD2VU5jVGIOWP7u9qNG3Lj7mpaiPFpbJ3LVfHZJvVzKR4v1Cb91wAOFpNw442N+LGPzHOHsten2g==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.23.2"
+ }
+ },
"node_modules/ieee754": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
@@ -5936,6 +6327,16 @@
"node": ">= 4"
}
},
+ "node_modules/immer": {
+ "version": "10.1.1",
+ "resolved": "https://registry.npmjs.org/immer/-/immer-10.1.1.tgz",
+ "integrity": "sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==",
+ "license": "MIT",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/immer"
+ }
+ },
"node_modules/immutable": {
"version": "4.3.6",
"resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.6.tgz",
@@ -6553,6 +6954,13 @@
"set-function-name": "^2.0.1"
}
},
+ "node_modules/jquery": {
+ "version": "3.7.1",
+ "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.1.tgz",
+ "integrity": "sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg==",
+ "license": "MIT",
+ "peer": true
+ },
"node_modules/js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@@ -6624,6 +7032,15 @@
"integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==",
"dev": true
},
+ "node_modules/json2mq": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/json2mq/-/json2mq-0.2.0.tgz",
+ "integrity": "sha512-SzoRg7ux5DWTII9J2qkrZrqV1gt+rTaoufMxEzXbS26Uid0NwaJd123HcoB80TgubEppxxIGdNxCx50fEoEWQA==",
+ "license": "MIT",
+ "dependencies": {
+ "string-convert": "^0.2.0"
+ }
+ },
"node_modules/json5": {
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
@@ -6703,10 +7120,11 @@
}
},
"node_modules/known-css-properties": {
- "version": "0.34.0",
- "resolved": "https://registry.npmjs.org/known-css-properties/-/known-css-properties-0.34.0.tgz",
- "integrity": "sha512-tBECoUqNFbyAY4RrbqsBQqDFpGXAEbdD5QKr8kACx3+rnArmuuR22nKQWKazvp07N9yjTyDZaw/20UIH8tL9DQ==",
- "dev": true
+ "version": "0.35.0",
+ "resolved": "https://registry.npmjs.org/known-css-properties/-/known-css-properties-0.35.0.tgz",
+ "integrity": "sha512-a/RAk2BfKk+WFGhhOCAYqSiFLc34k8Mt/6NWRI4joER0EYUzXIcFivjjnoD3+XU1DggLn/tZc3DOAgke7l8a4A==",
+ "dev": true,
+ "license": "MIT"
},
"node_modules/language-subtag-registry": {
"version": "0.3.23",
@@ -6800,7 +7218,13 @@
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
- "dev": true
+ "license": "MIT"
+ },
+ "node_modules/lodash.debounce": {
+ "version": "4.0.8",
+ "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
+ "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==",
+ "license": "MIT"
},
"node_modules/lodash.get": {
"version": "4.4.2",
@@ -7087,6 +7511,15 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/matchmediaquery": {
+ "version": "0.4.2",
+ "resolved": "https://registry.npmjs.org/matchmediaquery/-/matchmediaquery-0.4.2.tgz",
+ "integrity": "sha512-wrZpoT50ehYOudhDjt/YvUJc6eUzcdFPdmbizfgvswCKNHD1/OBOHYJpHie+HXpu6bSkEGieFMYk6VuutaiRfA==",
+ "license": "MIT",
+ "dependencies": {
+ "css-mediaquery": "^0.1.2"
+ }
+ },
"node_modules/mathml-tag-names": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/mathml-tag-names/-/mathml-tag-names-2.1.3.tgz",
@@ -7101,7 +7534,8 @@
"version": "2.0.30",
"resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz",
"integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==",
- "dev": true
+ "dev": true,
+ "peer": true
},
"node_modules/meow": {
"version": "13.2.0",
@@ -7131,10 +7565,11 @@
}
},
"node_modules/micromatch": {
- "version": "4.0.7",
- "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.7.tgz",
- "integrity": "sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==",
+ "version": "4.0.8",
+ "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
+ "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"braces": "^3.0.3",
"picomatch": "^2.3.1"
@@ -7304,13 +7739,6 @@
"node": ">=10"
}
},
- "node_modules/mocha/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,
- "peer": true
- },
"node_modules/mocha/node_modules/supports-color": {
"version": "8.1.1",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
@@ -7828,15 +8256,16 @@
}
},
"node_modules/ms": {
- "version": "2.1.2",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
- "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
- "dev": true
+ "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.7",
- "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz",
- "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==",
+ "version": "3.3.8",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz",
+ "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==",
"dev": true,
"funding": [
{
@@ -7844,6 +8273,7 @@
"url": "https://github.com/sponsors/ai"
}
],
+ "license": "MIT",
"bin": {
"nanoid": "bin/nanoid.cjs"
},
@@ -8277,10 +8707,11 @@
"dev": true
},
"node_modules/path-to-regexp": {
- "version": "1.8.0",
- "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz",
- "integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==",
+ "version": "1.9.0",
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.9.0.tgz",
+ "integrity": "sha512-xIp7/apCFJuUHdDLWe8O1HIkb0kQrOMb/0u6FXQjemHn/ii5LrIzU6bdECnsiTF/GjZkMEKg1xdiZwNqDYlZ6g==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"isarray": "0.0.1"
}
@@ -8313,10 +8744,11 @@
"dev": true
},
"node_modules/picocolors": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz",
- "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==",
- "dev": true
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "dev": true,
+ "license": "ISC"
},
"node_modules/picomatch": {
"version": "2.3.1",
@@ -8434,9 +8866,9 @@
}
},
"node_modules/postcss": {
- "version": "8.4.39",
- "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.39.tgz",
- "integrity": "sha512-0vzE+lAiG7hZl1/9I8yzKLx3aR9Xbof3fBHKunvMfOCYAtMhrsnccJY2iTURb9EZd5+pLuiNV9/c/GZJOHsgIw==",
+ "version": "8.4.49",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz",
+ "integrity": "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==",
"dev": true,
"funding": [
{
@@ -8452,10 +8884,11 @@
"url": "https://github.com/sponsors/ai"
}
],
+ "license": "MIT",
"dependencies": {
"nanoid": "^3.3.7",
- "picocolors": "^1.0.1",
- "source-map-js": "^1.2.0"
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
},
"engines": {
"node": "^10 || ^12 || >=14"
@@ -8468,15 +8901,16 @@
"dev": true
},
"node_modules/postcss-resolve-nested-selector": {
- "version": "0.1.1",
- "resolved": "https://registry.npmjs.org/postcss-resolve-nested-selector/-/postcss-resolve-nested-selector-0.1.1.tgz",
- "integrity": "sha512-HvExULSwLqHLgUy1rl3ANIqCsvMS0WHss2UOsXhXnQaZ9VCc2oBvIpXrl00IUFT5ZDITME0o6oiXeiHr2SAIfw==",
- "dev": true
+ "version": "0.1.6",
+ "resolved": "https://registry.npmjs.org/postcss-resolve-nested-selector/-/postcss-resolve-nested-selector-0.1.6.tgz",
+ "integrity": "sha512-0sglIs9Wmkzbr8lQwEyIzlDOOC9bGmfVKcJTaxv3vMmd3uo4o4DerC3En0bnmgceeql9BfC8hRkp7cg0fjdVqw==",
+ "dev": true,
+ "license": "MIT"
},
"node_modules/postcss-safe-parser": {
- "version": "7.0.0",
- "resolved": "https://registry.npmjs.org/postcss-safe-parser/-/postcss-safe-parser-7.0.0.tgz",
- "integrity": "sha512-ovehqRNVCpuFzbXoTb4qLtyzK3xn3t/CUBxOs8LsnQjQrShaB4lKiHoVqY8ANaC0hBMHq5QVWk77rwGklFUDrg==",
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/postcss-safe-parser/-/postcss-safe-parser-7.0.1.tgz",
+ "integrity": "sha512-0AioNCJZ2DPYz5ABT6bddIqlhgwhpHZ/l65YAYo0BCIn0xiDpsnTHz0gnoTGk0OXZW0JRs+cDwL8u/teRdz+8A==",
"dev": true,
"funding": [
{
@@ -8492,6 +8926,7 @@
"url": "https://github.com/sponsors/ai"
}
],
+ "license": "MIT",
"engines": {
"node": ">=18.0"
},
@@ -8554,10 +8989,11 @@
}
},
"node_modules/prettier": {
- "version": "3.3.3",
- "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz",
- "integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==",
+ "version": "3.4.2",
+ "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.4.2.tgz",
+ "integrity": "sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ==",
"dev": true,
+ "license": "MIT",
"bin": {
"prettier": "bin/prettier.cjs"
},
@@ -8717,6 +9153,18 @@
"node": ">=0.10.0"
}
},
+ "node_modules/react-content-loader": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/react-content-loader/-/react-content-loader-7.0.2.tgz",
+ "integrity": "sha512-773S98JTyC8VB2nu7LXUhpHx8tZMieGxMcx3qTe7IkohT6Br7d9AXnIXs/wQ6IhlUdKQcw6JLKk1QKigYCWDRA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "react": ">=16.0.0"
+ }
+ },
"node_modules/react-dom": {
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
@@ -8729,11 +9177,72 @@
"react": "^18.3.1"
}
},
+ "node_modules/react-hot-toast": {
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/react-hot-toast/-/react-hot-toast-2.4.1.tgz",
+ "integrity": "sha512-j8z+cQbWIM5LY37pR6uZR6D4LfseplqnuAO4co4u8917hBUvXlEqyP1ZzqVLcqoyUesZZv/ImreoCeHVDpE5pQ==",
+ "license": "MIT",
+ "dependencies": {
+ "goober": "^2.1.10"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "react": ">=16",
+ "react-dom": ">=16"
+ }
+ },
+ "node_modules/react-i18next": {
+ "version": "15.2.0",
+ "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-15.2.0.tgz",
+ "integrity": "sha512-iJNc8111EaDtVTVMKigvBtPHyrJV+KblWG73cUxqp+WmJCcwkzhWNFXmkAD5pwP2Z4woeDj/oXDdbjDsb3Gutg==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.25.0",
+ "html-parse-stringify": "^3.0.1"
+ },
+ "peerDependencies": {
+ "i18next": ">= 23.2.3",
+ "react": ">= 16.8.0"
+ },
+ "peerDependenciesMeta": {
+ "react-dom": {
+ "optional": true
+ },
+ "react-native": {
+ "optional": true
+ }
+ }
+ },
"node_modules/react-is": {
"version": "16.13.1",
"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",
@@ -8743,6 +9252,24 @@
"node": ">=0.10.0"
}
},
+ "node_modules/react-responsive": {
+ "version": "10.0.0",
+ "resolved": "https://registry.npmjs.org/react-responsive/-/react-responsive-10.0.0.tgz",
+ "integrity": "sha512-N6/UiRLGQyGUqrarhBZmrSmHi2FXSD++N5VbSKsBBvWfG0ZV7asvUBluSv5lSzdMyEVjzZ6Y8DL4OHABiztDOg==",
+ "license": "MIT",
+ "dependencies": {
+ "hyphenate-style-name": "^1.0.0",
+ "matchmediaquery": "^0.4.2",
+ "prop-types": "^15.6.1",
+ "shallow-equal": "^3.1.0"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "peerDependencies": {
+ "react": ">=16.8.0"
+ }
+ },
"node_modules/react-router": {
"version": "6.25.1",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-6.25.1.tgz",
@@ -8773,6 +9300,36 @@
"react-dom": ">=16.8"
}
},
+ "node_modules/react-slick": {
+ "version": "0.30.3",
+ "resolved": "https://registry.npmjs.org/react-slick/-/react-slick-0.30.3.tgz",
+ "integrity": "sha512-B4x0L9GhkEWUMApeHxr/Ezp2NncpGc+5174R02j+zFiWuYboaq98vmxwlpafZfMjZic1bjdIqqmwLDcQY0QaFA==",
+ "license": "MIT",
+ "dependencies": {
+ "classnames": "^2.2.5",
+ "enquire.js": "^2.1.6",
+ "json2mq": "^0.2.0",
+ "lodash.debounce": "^4.0.8",
+ "resize-observer-polyfill": "^1.5.0"
+ },
+ "peerDependencies": {
+ "react": "^0.14.0 || ^15.0.1 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
+ "react-dom": "^0.14.0 || ^15.0.1 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
+ "node_modules/react-toastify": {
+ "version": "11.0.2",
+ "resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-11.0.2.tgz",
+ "integrity": "sha512-GjHuGaiXMvbls3ywqv8XdWONwrcO4DXCJIY1zVLkHU73gEElKvTTXNI5Vom3s/k/M8hnkrfsqgBSX3OwmlonbA==",
+ "license": "MIT",
+ "dependencies": {
+ "clsx": "^2.1.1"
+ },
+ "peerDependencies": {
+ "react": "^18 || ^19",
+ "react-dom": "^18 || ^19"
+ }
+ },
"node_modules/react-transition-group": {
"version": "4.4.5",
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz",
@@ -8893,6 +9450,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 +9548,18 @@
"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/resize-observer-polyfill": {
+ "version": "1.5.1",
+ "resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz",
+ "integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==",
+ "license": "MIT"
+ },
"node_modules/resolve": {
"version": "1.22.8",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz",
@@ -9091,12 +9675,13 @@
}
},
"node_modules/rollup": {
- "version": "4.18.1",
- "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.18.1.tgz",
- "integrity": "sha512-Elx2UT8lzxxOXMpy5HWQGZqkrQOtrVDDa/bm9l10+U4rQnVzbL/LgZ4NOM1MPIDyHk69W4InuYDF5dzRh4Kw1A==",
+ "version": "4.28.1",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.28.1.tgz",
+ "integrity": "sha512-61fXYl/qNVinKmGSTHAZ6Yy8I3YIJC/r2m9feHo6SwVAVcLT5MPwOUFe7EuURA/4m0NR8lXG4BBXuo/IZEsjMg==",
"dev": true,
+ "license": "MIT",
"dependencies": {
- "@types/estree": "1.0.5"
+ "@types/estree": "1.0.6"
},
"bin": {
"rollup": "dist/bin/rollup"
@@ -9106,22 +9691,25 @@
"npm": ">=8.0.0"
},
"optionalDependencies": {
- "@rollup/rollup-android-arm-eabi": "4.18.1",
- "@rollup/rollup-android-arm64": "4.18.1",
- "@rollup/rollup-darwin-arm64": "4.18.1",
- "@rollup/rollup-darwin-x64": "4.18.1",
- "@rollup/rollup-linux-arm-gnueabihf": "4.18.1",
- "@rollup/rollup-linux-arm-musleabihf": "4.18.1",
- "@rollup/rollup-linux-arm64-gnu": "4.18.1",
- "@rollup/rollup-linux-arm64-musl": "4.18.1",
- "@rollup/rollup-linux-powerpc64le-gnu": "4.18.1",
- "@rollup/rollup-linux-riscv64-gnu": "4.18.1",
- "@rollup/rollup-linux-s390x-gnu": "4.18.1",
- "@rollup/rollup-linux-x64-gnu": "4.18.1",
- "@rollup/rollup-linux-x64-musl": "4.18.1",
- "@rollup/rollup-win32-arm64-msvc": "4.18.1",
- "@rollup/rollup-win32-ia32-msvc": "4.18.1",
- "@rollup/rollup-win32-x64-msvc": "4.18.1",
+ "@rollup/rollup-android-arm-eabi": "4.28.1",
+ "@rollup/rollup-android-arm64": "4.28.1",
+ "@rollup/rollup-darwin-arm64": "4.28.1",
+ "@rollup/rollup-darwin-x64": "4.28.1",
+ "@rollup/rollup-freebsd-arm64": "4.28.1",
+ "@rollup/rollup-freebsd-x64": "4.28.1",
+ "@rollup/rollup-linux-arm-gnueabihf": "4.28.1",
+ "@rollup/rollup-linux-arm-musleabihf": "4.28.1",
+ "@rollup/rollup-linux-arm64-gnu": "4.28.1",
+ "@rollup/rollup-linux-arm64-musl": "4.28.1",
+ "@rollup/rollup-linux-loongarch64-gnu": "4.28.1",
+ "@rollup/rollup-linux-powerpc64le-gnu": "4.28.1",
+ "@rollup/rollup-linux-riscv64-gnu": "4.28.1",
+ "@rollup/rollup-linux-s390x-gnu": "4.28.1",
+ "@rollup/rollup-linux-x64-gnu": "4.28.1",
+ "@rollup/rollup-linux-x64-musl": "4.28.1",
+ "@rollup/rollup-win32-arm64-msvc": "4.28.1",
+ "@rollup/rollup-win32-ia32-msvc": "4.28.1",
+ "@rollup/rollup-win32-x64-msvc": "4.28.1",
"fsevents": "~2.3.2"
}
},
@@ -9303,6 +9891,12 @@
"node": ">= 0.4"
}
},
+ "node_modules/shallow-equal": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/shallow-equal/-/shallow-equal-3.1.0.tgz",
+ "integrity": "sha512-pfVOw8QZIXpMbhBWvzBISicvToTiM5WBF1EeAUZDDSb5Dt29yl4AYbyywbJFSEsRUMr7gJaxqCdr4L3tQf9wVg==",
+ "license": "MIT"
+ },
"node_modules/shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
@@ -9453,11 +10047,21 @@
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"dev": true
},
+ "node_modules/slick-carousel": {
+ "version": "1.8.1",
+ "resolved": "https://registry.npmjs.org/slick-carousel/-/slick-carousel-1.8.1.tgz",
+ "integrity": "sha512-XB9Ftrf2EEKfzoQXt3Nitrt/IPbT+f1fgqBdoxO3W/+JYvtEOW6EgxnWfr9GH6nmULv7Y2tPmEX3koxThVmebA==",
+ "license": "MIT",
+ "peerDependencies": {
+ "jquery": ">=1.8.0"
+ }
+ },
"node_modules/source-map-js": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz",
- "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==",
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
"dev": true,
+ "license": "BSD-3-Clause",
"engines": {
"node": ">=0.10.0"
}
@@ -9535,6 +10139,12 @@
"node": ">= 0.4"
}
},
+ "node_modules/string-convert": {
+ "version": "0.2.1",
+ "resolved": "https://registry.npmjs.org/string-convert/-/string-convert-0.2.1.tgz",
+ "integrity": "sha512-u/1tdPl4yQnPBjnVrmdLo9gtuLvELKsAoRapekWggdiQNvvvum+jYF329d84NAa660KQw7pB2n36KrIKVoXa3A==",
+ "license": "MIT"
+ },
"node_modules/string-width": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
@@ -9733,13 +10343,12 @@
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/style-search/-/style-search-0.1.0.tgz",
"integrity": "sha512-Dj1Okke1C3uKKwQcetra4jSuk0DqbzbYtXipzFlFMZtowbF1x7BKJwB9AayVMyFARvU8EDrZdcax4At/452cAg==",
- "dev": true,
- "peer": true
+ "dev": true
},
"node_modules/stylelint": {
- "version": "16.7.0",
- "resolved": "https://registry.npmjs.org/stylelint/-/stylelint-16.7.0.tgz",
- "integrity": "sha512-Q1ATiXlz+wYr37a7TGsfvqYn2nSR3T/isw3IWlZQzFzCNoACHuGBb6xBplZXz56/uDRJHIygxjh7jbV/8isewA==",
+ "version": "16.13.2",
+ "resolved": "https://registry.npmjs.org/stylelint/-/stylelint-16.13.2.tgz",
+ "integrity": "sha512-wDlgh0mRO9RtSa3TdidqHd0nOG8MmUyVKl+dxA6C1j8aZRzpNeEgdhFmU5y4sZx4Fc6r46p0fI7p1vR5O2DZqA==",
"dev": true,
"funding": [
{
@@ -9751,45 +10360,45 @@
"url": "https://github.com/sponsors/stylelint"
}
],
+ "license": "MIT",
"dependencies": {
- "@csstools/css-parser-algorithms": "^2.7.1",
- "@csstools/css-tokenizer": "^2.4.1",
- "@csstools/media-query-list-parser": "^2.1.13",
- "@csstools/selector-specificity": "^3.1.1",
+ "@csstools/css-parser-algorithms": "^3.0.4",
+ "@csstools/css-tokenizer": "^3.0.3",
+ "@csstools/media-query-list-parser": "^4.0.2",
+ "@csstools/selector-specificity": "^5.0.0",
"@dual-bundle/import-meta-resolve": "^4.1.0",
"balanced-match": "^2.0.0",
"colord": "^2.9.3",
"cosmiconfig": "^9.0.0",
- "css-functions-list": "^3.2.2",
- "css-tree": "^2.3.1",
- "debug": "^4.3.5",
- "fast-glob": "^3.3.2",
+ "css-functions-list": "^3.2.3",
+ "css-tree": "^3.1.0",
+ "debug": "^4.3.7",
+ "fast-glob": "^3.3.3",
"fastest-levenshtein": "^1.0.16",
- "file-entry-cache": "^9.0.0",
+ "file-entry-cache": "^10.0.5",
"global-modules": "^2.0.0",
"globby": "^11.1.0",
"globjoin": "^0.1.4",
"html-tags": "^3.3.1",
- "ignore": "^5.3.1",
+ "ignore": "^7.0.1",
"imurmurhash": "^0.1.4",
"is-plain-object": "^5.0.0",
- "known-css-properties": "^0.34.0",
+ "known-css-properties": "^0.35.0",
"mathml-tag-names": "^2.1.3",
"meow": "^13.2.0",
- "micromatch": "^4.0.7",
+ "micromatch": "^4.0.8",
"normalize-path": "^3.0.0",
- "picocolors": "^1.0.1",
- "postcss": "^8.4.39",
- "postcss-resolve-nested-selector": "^0.1.1",
- "postcss-safe-parser": "^7.0.0",
- "postcss-selector-parser": "^6.1.0",
+ "picocolors": "^1.1.1",
+ "postcss": "^8.4.49",
+ "postcss-resolve-nested-selector": "^0.1.6",
+ "postcss-safe-parser": "^7.0.1",
+ "postcss-selector-parser": "^7.0.0",
"postcss-value-parser": "^4.2.0",
"resolve-from": "^5.0.0",
"string-width": "^4.2.3",
- "strip-ansi": "^7.1.0",
- "supports-hyperlinks": "^3.0.0",
+ "supports-hyperlinks": "^3.1.0",
"svg-tags": "^1.0.0",
- "table": "^6.8.2",
+ "table": "^6.9.0",
"write-file-atomic": "^5.0.1"
},
"bin": {
@@ -9799,16 +10408,274 @@
"node": ">=18.12.0"
}
},
- "node_modules/stylelint/node_modules/ansi-regex": {
- "version": "6.0.1",
- "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz",
- "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==",
+ "node_modules/stylelint-config-recommended": {
+ "version": "15.0.0",
+ "resolved": "https://registry.npmjs.org/stylelint-config-recommended/-/stylelint-config-recommended-15.0.0.tgz",
+ "integrity": "sha512-9LejMFsat7L+NXttdHdTq94byn25TD+82bzGRiV1Pgasl99pWnwipXS5DguTpp3nP1XjvLXVnEJIuYBfsRjRkA==",
"dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/stylelint"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/stylelint"
+ }
+ ],
+ "license": "MIT",
"engines": {
- "node": ">=12"
+ "node": ">=18.12.0"
},
- "funding": {
- "url": "https://github.com/chalk/ansi-regex?sponsor=1"
+ "peerDependencies": {
+ "stylelint": "^16.13.0"
+ }
+ },
+ "node_modules/stylelint-config-recommended-scss": {
+ "version": "14.1.0",
+ "resolved": "https://registry.npmjs.org/stylelint-config-recommended-scss/-/stylelint-config-recommended-scss-14.1.0.tgz",
+ "integrity": "sha512-bhaMhh1u5dQqSsf6ri2GVWWQW5iUjBYgcHkh7SgDDn92ijoItC/cfO/W+fpXshgTQWhwFkP1rVcewcv4jaftRg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "postcss-scss": "^4.0.9",
+ "stylelint-config-recommended": "^14.0.1",
+ "stylelint-scss": "^6.4.0"
+ },
+ "engines": {
+ "node": ">=18.12.0"
+ },
+ "peerDependencies": {
+ "postcss": "^8.3.3",
+ "stylelint": "^16.6.1"
+ },
+ "peerDependenciesMeta": {
+ "postcss": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/stylelint-config-recommended-scss/node_modules/stylelint-config-recommended": {
+ "version": "14.0.1",
+ "resolved": "https://registry.npmjs.org/stylelint-config-recommended/-/stylelint-config-recommended-14.0.1.tgz",
+ "integrity": "sha512-bLvc1WOz/14aPImu/cufKAZYfXs/A/owZfSMZ4N+16WGXLoX5lOir53M6odBxvhgmgdxCVnNySJmZKx73T93cg==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/stylelint"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/stylelint"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=18.12.0"
+ },
+ "peerDependencies": {
+ "stylelint": "^16.1.0"
+ }
+ },
+ "node_modules/stylelint-config-sass-guidelines": {
+ "version": "12.1.0",
+ "resolved": "https://registry.npmjs.org/stylelint-config-sass-guidelines/-/stylelint-config-sass-guidelines-12.1.0.tgz",
+ "integrity": "sha512-NTxEtVT6uNSqRvq+A3ScyKhjUrY/Z845TnpWEwnMgIPZ/+/Waa4+51r6OPuQRMu4XZS3D8DK1UaT4TWFBvuuAw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@stylistic/stylelint-plugin": "^3.0.1",
+ "postcss-scss": "^4.0.9",
+ "stylelint-scss": "^6.2.1"
+ },
+ "engines": {
+ "node": ">=18.12.0"
+ },
+ "peerDependencies": {
+ "postcss": "^8.4.21",
+ "stylelint": "^16.1.0"
+ }
+ },
+ "node_modules/stylelint-config-standard": {
+ "version": "37.0.0",
+ "resolved": "https://registry.npmjs.org/stylelint-config-standard/-/stylelint-config-standard-37.0.0.tgz",
+ "integrity": "sha512-+6eBlbSTrOn/il2RlV0zYGQwRTkr+WtzuVSs1reaWGObxnxLpbcspCUYajVQHonVfxVw2U+h42azGhrBvcg8OA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/stylelint"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/stylelint"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "stylelint-config-recommended": "^15.0.0"
+ },
+ "engines": {
+ "node": ">=18.12.0"
+ },
+ "peerDependencies": {
+ "stylelint": "^16.13.0"
+ }
+ },
+ "node_modules/stylelint-scss": {
+ "version": "6.10.1",
+ "resolved": "https://registry.npmjs.org/stylelint-scss/-/stylelint-scss-6.10.1.tgz",
+ "integrity": "sha512-CBqs0jecftIyhic6xba+4OvZUp4B0wNbX19w6Rq1fPo+lBDmTevk+olo8H7u/WQpTSDCDbBN4f3oocQurvXLTQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "css-tree": "^3.0.1",
+ "is-plain-object": "^5.0.0",
+ "known-css-properties": "^0.35.0",
+ "mdn-data": "^2.14.0",
+ "postcss-media-query-parser": "^0.2.3",
+ "postcss-resolve-nested-selector": "^0.1.6",
+ "postcss-selector-parser": "^7.0.0",
+ "postcss-value-parser": "^4.2.0"
+ },
+ "engines": {
+ "node": ">=18.12.0"
+ },
+ "peerDependencies": {
+ "stylelint": "^16.0.2"
+ }
+ },
+ "node_modules/stylelint-scss/node_modules/css-tree": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.1.0.tgz",
+ "integrity": "sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "mdn-data": "2.12.2",
+ "source-map-js": "^1.0.1"
+ },
+ "engines": {
+ "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0"
+ }
+ },
+ "node_modules/stylelint-scss/node_modules/css-tree/node_modules/mdn-data": {
+ "version": "2.12.2",
+ "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.12.2.tgz",
+ "integrity": "sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==",
+ "dev": true,
+ "license": "CC0-1.0"
+ },
+ "node_modules/stylelint-scss/node_modules/mdn-data": {
+ "version": "2.15.0",
+ "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.15.0.tgz",
+ "integrity": "sha512-KIrS0lFPOqA4DgeO16vI5fkAsy8p++WBlbXtB5P1EQs8ubBgguAInNd1DnrCeTRfGchY0kgThgDOOIPyOLH2dQ==",
+ "dev": true,
+ "license": "CC0-1.0"
+ },
+ "node_modules/stylelint-scss/node_modules/postcss-selector-parser": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.0.0.tgz",
+ "integrity": "sha512-9RbEr1Y7FFfptd/1eEdntyjMwLeghW1bHX9GWjXo19vx4ytPQhANltvVxDggzJl7mnWM+dX28kb6cyS/4iQjlQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "cssesc": "^3.0.0",
+ "util-deprecate": "^1.0.2"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/stylelint/node_modules/@csstools/css-parser-algorithms": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.4.tgz",
+ "integrity": "sha512-Up7rBoV77rv29d3uKHUIVubz1BTcgyUK72IvCQAbfbMv584xHcGKCKbWh7i8hPrRJ7qU4Y8IO3IY9m+iTB7P3A==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "@csstools/css-tokenizer": "^3.0.3"
+ }
+ },
+ "node_modules/stylelint/node_modules/@csstools/css-tokenizer": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.3.tgz",
+ "integrity": "sha512-UJnjoFsmxfKUdNYdWgOB0mWUypuLvAfQPH1+pyvRJs6euowbFkFC6P13w1l8mJyi3vxYMxc9kld5jZEGRQs6bw==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/stylelint/node_modules/@csstools/media-query-list-parser": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/@csstools/media-query-list-parser/-/media-query-list-parser-4.0.2.tgz",
+ "integrity": "sha512-EUos465uvVvMJehckATTlNqGj4UJWkTmdWuDMjqvSUkjGpmOyFZBVwb4knxCm/k2GMTXY+c/5RkdndzFYWeX5A==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "@csstools/css-parser-algorithms": "^3.0.4",
+ "@csstools/css-tokenizer": "^3.0.3"
+ }
+ },
+ "node_modules/stylelint/node_modules/@csstools/selector-specificity": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/@csstools/selector-specificity/-/selector-specificity-5.0.0.tgz",
+ "integrity": "sha512-PCqQV3c4CoVm3kdPhyeZ07VmBRdH2EpMFA/pd9OASpOEC3aXNGoqPDAZ80D0cLpMBxnmk0+yNhGsEx31hq7Gtw==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT-0",
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "postcss-selector-parser": "^7.0.0"
}
},
"node_modules/stylelint/node_modules/balanced-match": {
@@ -9817,29 +10684,71 @@
"integrity": "sha512-1ugUSr8BHXRnK23KfuYS+gVMC3LB8QGH9W1iGtDPsNWoQbgtXSExkBu2aDR4epiGWZOjZsj6lDl/N/AqqTC3UA==",
"dev": true
},
- "node_modules/stylelint/node_modules/file-entry-cache": {
- "version": "9.0.0",
- "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-9.0.0.tgz",
- "integrity": "sha512-6MgEugi8p2tiUhqO7GnPsmbCCzj0YRCwwaTbpGRyKZesjRSzkqkAE9fPp7V2yMs5hwfgbQLgdvSSkGNg1s5Uvw==",
+ "node_modules/stylelint/node_modules/css-tree": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.1.0.tgz",
+ "integrity": "sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==",
"dev": true,
+ "license": "MIT",
"dependencies": {
- "flat-cache": "^5.0.0"
+ "mdn-data": "2.12.2",
+ "source-map-js": "^1.0.1"
},
"engines": {
- "node": ">=18"
+ "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0"
+ }
+ },
+ "node_modules/stylelint/node_modules/file-entry-cache": {
+ "version": "10.0.5",
+ "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-10.0.5.tgz",
+ "integrity": "sha512-umpQsJrBNsdMDgreSryMEXvJh66XeLtZUwA8Gj7rHGearGufUFv6rB/bcXRFsiGWw/VeSUgUofF4Rf2UKEOrTA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "flat-cache": "^6.1.5"
}
},
"node_modules/stylelint/node_modules/flat-cache": {
- "version": "5.0.0",
- "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-5.0.0.tgz",
- "integrity": "sha512-JrqFmyUl2PnPi1OvLyTVHnQvwQ0S+e6lGSwu8OkAZlSaNIZciTY2H/cOOROxsBA1m/LZNHDsqAgDZt6akWcjsQ==",
+ "version": "6.1.5",
+ "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-6.1.5.tgz",
+ "integrity": "sha512-QR+2kN38f8nMfiIQ1LHYjuDEmZNZVjxuxY+HufbS3BW0EX01Q5OnH7iduOYRutmgiXb797HAKcXUeXrvRjjgSQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "cacheable": "^1.8.7",
+ "flatted": "^3.3.2",
+ "hookified": "^1.6.0"
+ }
+ },
+ "node_modules/stylelint/node_modules/ignore": {
+ "version": "7.0.3",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.3.tgz",
+ "integrity": "sha512-bAH5jbK/F3T3Jls4I0SO1hmPR0dKU0a7+SY6n1yzRtG54FLO8d6w/nxLFX2Nb7dBu6cCWXPaAME6cYqFUMmuCA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/stylelint/node_modules/mdn-data": {
+ "version": "2.12.2",
+ "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.12.2.tgz",
+ "integrity": "sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==",
+ "dev": true,
+ "license": "CC0-1.0"
+ },
+ "node_modules/stylelint/node_modules/postcss-selector-parser": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.0.0.tgz",
+ "integrity": "sha512-9RbEr1Y7FFfptd/1eEdntyjMwLeghW1bHX9GWjXo19vx4ytPQhANltvVxDggzJl7mnWM+dX28kb6cyS/4iQjlQ==",
"dev": true,
+ "license": "MIT",
"dependencies": {
- "flatted": "^3.3.1",
- "keyv": "^4.5.4"
+ "cssesc": "^3.0.0",
+ "util-deprecate": "^1.0.2"
},
"engines": {
- "node": ">=18"
+ "node": ">=4"
}
},
"node_modules/stylelint/node_modules/resolve-from": {
@@ -9851,21 +10760,6 @@
"node": ">=8"
}
},
- "node_modules/stylelint/node_modules/strip-ansi": {
- "version": "7.1.0",
- "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
- "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
- "dev": true,
- "dependencies": {
- "ansi-regex": "^6.0.1"
- },
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/chalk/strip-ansi?sponsor=1"
- }
- },
"node_modules/supports-color": {
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
@@ -9879,16 +10773,20 @@
}
},
"node_modules/supports-hyperlinks": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-3.0.0.tgz",
- "integrity": "sha512-QBDPHyPQDRTy9ku4URNGY5Lah8PAaXs6tAAwp55sL5WCsSW7GIfdf6W5ixfziW+t7wh3GVvHyHHyQ1ESsoRvaA==",
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-3.1.0.tgz",
+ "integrity": "sha512-2rn0BZ+/f7puLOHZm1HOJfwBggfaHXUpPUSSG/SWM4TWp5KCfmNYwnC3hruy2rZlMnmWZ+QAGpZfchu3f3695A==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"has-flag": "^4.0.0",
"supports-color": "^7.0.0"
},
"engines": {
"node": ">=14.18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/supports-hyperlinks/node_modules/has-flag": {
@@ -9930,6 +10828,25 @@
"integrity": "sha512-ovssysQTa+luh7A5Weu3Rta6FJlFBBbInjOh722LIt6klpU2/HtdUbszju/G4devcvk8PGt7FCLv5wftu3THUA==",
"dev": true
},
+ "node_modules/swiper": {
+ "version": "11.1.15",
+ "resolved": "https://registry.npmjs.org/swiper/-/swiper-11.1.15.tgz",
+ "integrity": "sha512-IzWeU34WwC7gbhjKsjkImTuCRf+lRbO6cnxMGs88iVNKDwV+xQpBCJxZ4bNH6gSrIbbyVJ1kuGzo3JTtz//CBw==",
+ "funding": [
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/swiperjs"
+ },
+ {
+ "type": "open_collective",
+ "url": "http://opencollective.com/swiper"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4.7.0"
+ }
+ },
"node_modules/synckit": {
"version": "0.8.8",
"resolved": "https://registry.npmjs.org/synckit/-/synckit-0.8.8.tgz",
@@ -9947,10 +10864,11 @@
}
},
"node_modules/table": {
- "version": "6.8.2",
- "resolved": "https://registry.npmjs.org/table/-/table-6.8.2.tgz",
- "integrity": "sha512-w2sfv80nrAh2VCbqR5AK27wswXhqcck2AhfnNW76beQXskGZ1V12GwS//yYVa3d3fcvAip2OUnbDAjW2k3v9fA==",
+ "version": "6.9.0",
+ "resolved": "https://registry.npmjs.org/table/-/table-6.9.0.tgz",
+ "integrity": "sha512-9kY+CygyYM6j02t5YFHbNz2FN5QmYGv9zAjVp4lCDjlCw7amdckXlEt/bjMhUIfj4ThGRE4gCUH5+yGnNuPo5A==",
"dev": true,
+ "license": "BSD-3-Clause",
"dependencies": {
"ajv": "^8.0.1",
"lodash.truncate": "^4.4.2",
@@ -10334,7 +11252,7 @@
"version": "5.5.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.3.tgz",
"integrity": "sha512-/hreyEujaB0w76zKo6717l3L0o/qEUtRgdvUBvlkhoWeOVMjMuHNHk0BRBzikzuGDqNmPQbg5ifMEqsHLiIUcQ==",
- "dev": true,
+ "devOptional": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -10438,6 +11356,15 @@
"requires-port": "^1.0.0"
}
},
+ "node_modules/use-sync-external-store": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.4.0.tgz",
+ "integrity": "sha512-9WXSPC5fMv61vaupRkCKCxsPxBocVnwakBEkMIHHpkTTg6icbJtg6jzgtLDm4bl3cSHAca52rYWih0k4K3PfHw==",
+ "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",
@@ -10488,14 +11415,15 @@
}
},
"node_modules/vite": {
- "version": "5.3.3",
- "resolved": "https://registry.npmjs.org/vite/-/vite-5.3.3.tgz",
- "integrity": "sha512-NPQdeCU0Dv2z5fu+ULotpuq5yfCS1BzKUIPhNbP3YBfAMGJXbt2nS+sbTFu+qchaqWTD+H3JK++nRwr6XIcp6A==",
+ "version": "5.4.11",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.11.tgz",
+ "integrity": "sha512-c7jFQRklXua0mTzneGW9QVyxFjUgwcihC4bXEtujIo2ouWCe1Ajt/amn2PCxYnhYfd5k09JX3SB7OYWFKYqj8Q==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"esbuild": "^0.21.3",
- "postcss": "^8.4.39",
- "rollup": "^4.13.0"
+ "postcss": "^8.4.43",
+ "rollup": "^4.20.0"
},
"bin": {
"vite": "bin/vite.js"
@@ -10514,6 +11442,7 @@
"less": "*",
"lightningcss": "^1.21.0",
"sass": "*",
+ "sass-embedded": "*",
"stylus": "*",
"sugarss": "*",
"terser": "^5.4.0"
@@ -10531,6 +11460,9 @@
"sass": {
"optional": true
},
+ "sass-embedded": {
+ "optional": true
+ },
"stylus": {
"optional": true
},
@@ -10542,6 +11474,15 @@
}
}
},
+ "node_modules/void-elements": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz",
+ "integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/webidl-conversions": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
@@ -10674,10 +11615,11 @@
}
},
"node_modules/windows-release/node_modules/cross-spawn": {
- "version": "6.0.5",
- "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz",
- "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==",
+ "version": "6.0.6",
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.6.tgz",
+ "integrity": "sha512-VqCUuhcd1iB+dsv8gxPttb5iZh/D0iubSP21g36KXdEuf6I5JiioesUVjpCdHV9MZRUfVFlvwtIUyPfxo5trtw==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"nice-try": "^1.0.4",
"path-key": "^2.0.1",
diff --git a/package.json b/package.json
index ae251685c8..e23940f696 100644
--- a/package.json
+++ b/package.json
@@ -7,21 +7,37 @@
"license": "GPL-3.0",
"dependencies": {
"@fortawesome/fontawesome-free": "^6.5.2",
+ "@reduxjs/toolkit": "^2.5.0",
"bulma": "^1.0.1",
"classnames": "^2.5.1",
+ "i18next": "^24.1.0",
+ "i18next-browser-languagedetector": "^8.0.2",
+ "lodash": "^4.17.21",
"react": "^18.3.1",
+ "react-content-loader": "^7.0.2",
"react-dom": "^18.3.1",
+ "react-hot-toast": "^2.4.1",
+ "react-i18next": "^15.2.0",
+ "react-redux": "^9.2.0",
+ "react-responsive": "^10.0.0",
"react-router-dom": "^6.25.1",
- "react-transition-group": "^4.4.5"
+ "react-slick": "^0.30.3",
+ "react-toastify": "^11.0.2",
+ "react-transition-group": "^4.4.5",
+ "redux": "^5.0.1",
+ "slick-carousel": "^1.8.1",
+ "swiper": "^11.1.15"
},
"devDependencies": {
"@cypress/react18": "^2.0.1",
- "@mate-academy/scripts": "^1.8.5",
+ "@mate-academy/scripts": "^1.9.12",
"@mate-academy/students-ts-config": "*",
- "@mate-academy/stylelint-config": "*",
+ "@mate-academy/stylelint-config": "^0.0.12",
+ "@types/lodash": "^4.17.13",
"@types/node": "^20.14.10",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
+ "@types/react-slick": "^0.23.13",
"@types/react-transition-group": "^4.4.10",
"@typescript-eslint/parser": "^7.16.0",
"@vitejs/plugin-react": "^4.3.1",
@@ -39,9 +55,13 @@
"mochawesome": "^7.1.3",
"mochawesome-merge": "^4.3.0",
"mochawesome-report-generator": "^6.2.0",
- "prettier": "^3.3.2",
+ "prettier": "^3.4.2",
"sass": "^1.77.8",
- "stylelint": "^16.7.0",
+ "stylelint": "^16.13.2",
+ "stylelint-config-recommended-scss": "^14.1.0",
+ "stylelint-config-sass-guidelines": "^12.1.0",
+ "stylelint-config-standard": "^37.0.0",
+ "stylelint-scss": "^6.10.1",
"typescript": "^5.2.2",
"vite": "^5.3.1"
},
@@ -49,7 +69,7 @@
"start": "mate-scripts start -l",
"build": "mate-scripts build",
"test": "mate-scripts test -l",
- "style-format": "npx stylelint 'src/**/*.scss' --fix --allow-empty-input",
+ "style-format": "npx stylelint 'src/**/*.{css, scss, module.scss}' --fix --allow-empty-input",
"lint-js": "mate-scripts lint -j",
"lint-css": "mate-scripts lint -s",
"format": "prettier --write './src/**/*.{ts,tsx}'",
diff --git a/src/App.scss b/src/App.scss
index 71bc413aad..69d4fd8ce6 100644
--- a/src/App.scss
+++ b/src/App.scss
@@ -1 +1,24 @@
-// not empty
+@import './styles/main';
+
+body {
+ background-color: var(--c-background);
+}
+
+.App {
+ min-height: 100vh;
+ display: flex;
+ flex-direction: column;
+
+ &__title {
+ display: none;
+ }
+}
+
+.main {
+ flex: 1;
+ margin-bottom: 64px;
+
+ @include on-tablet {
+ margin-bottom: 80px;
+ }
+}
diff --git a/src/App.tsx b/src/App.tsx
index 372e4b4206..b08a80d074 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -1,7 +1,36 @@
+import { Outlet } from 'react-router-dom';
import './App.scss';
+import './i18n';
+import { Header } from './components/Header';
+import { Footer } from './components/Footer';
+import { Toaster } from 'react-hot-toast';
+import { ToastContainer } from 'react-toastify';
+import { useAppSelector } from './hooks/hooks';
-export const App = () => (
-
-
Product Catalog
-
-);
+export const App = () => {
+ const { theme } = useAppSelector(state => state.theme);
+
+ const toastStyle =
+ theme === 'light'
+ ? { background: '#333', color: '#fff' }
+ : { background: '#fff', color: '#000' };
+
+ return (
+
+
+
+
Product Catalog
+
+
+
+
+
+
+ );
+};
diff --git a/src/Root.tsx b/src/Root.tsx
new file mode 100644
index 0000000000..c23812507d
--- /dev/null
+++ b/src/Root.tsx
@@ -0,0 +1,51 @@
+import {
+ HashRouter as Router,
+ Route,
+ Routes,
+ Navigate,
+} from 'react-router-dom';
+import { AccessoriesPage } from './modules/AccessoriesPage';
+import { App } from './App';
+import { CartPage } from './modules/CartPage';
+import { FavoritesPage } from './modules/FavoritesPage';
+import { HomePage } from './modules/HomePage';
+import { NotFoundPage } from './modules/NotFoundPage';
+import { PhonesPage } from './modules/PhonesPage';
+import { ProductDetailsPage } from './modules/ProductDetailsPage';
+import { TabletsPage } from './modules/TabletsPage';
+
+export const Root = () => (
+
+
+ }>
+ } />
+ } />
+
+
+ } />
+ } />
+
+
+
+ } />
+ } />
+
+
+
+ } />
+ } />
+
+
+
+ } />
+
+
+
+ } />
+
+
+ } />
+
+
+
+);
diff --git a/src/app/store.ts b/src/app/store.ts
new file mode 100644
index 0000000000..eff0f212be
--- /dev/null
+++ b/src/app/store.ts
@@ -0,0 +1,18 @@
+import { configureStore } from '@reduxjs/toolkit';
+import themeReducer from '../features/themeSlice';
+import productsReducer from '../features/productsSlice';
+import cartReducer from '../features/cartSlice';
+import favoritesReducer from '../features/favoritesSlice';
+
+const store = configureStore({
+ reducer: {
+ theme: themeReducer,
+ products: productsReducer,
+ cart: cartReducer,
+ favorites: favoritesReducer,
+ },
+});
+
+export default store;
+export type RootState = ReturnType;
+export type AppDispatch = typeof store.dispatch;
diff --git a/src/components/BreadCrumbs/BreadCrumbs.module.scss b/src/components/BreadCrumbs/BreadCrumbs.module.scss
new file mode 100644
index 0000000000..4054477573
--- /dev/null
+++ b/src/components/BreadCrumbs/BreadCrumbs.module.scss
@@ -0,0 +1,52 @@
+@import '../../styles/main';
+
+.breadCrumbs {
+ display: flex;
+ height: 16px;
+ gap: 8px;
+ margin-top: 24px;
+
+ &__content {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ height: 16px;
+ gap: 8px;
+ }
+
+ &__title {
+ font-family: Mont, sans-serif;
+ font-weight: 600;
+ font-size: 12px;
+ line-height: 15.34px;
+ color: var(--header-text-color);
+ text-decoration: none;
+
+ &__active {
+ color: var(--header-text-color-active);
+ }
+ }
+
+ &__arrowRight {
+ &__icon {
+ display: flex;
+ width: 16px;
+ height: 16px;
+ opacity: 0.5;
+ align-items: center;
+ }
+ }
+
+ &__logo {
+ &__link {
+ display: flex;
+ align-items: center;
+
+ &__img {
+ width: 16px;
+ height: 16px;
+ }
+ }
+ }
+}
+
diff --git a/src/components/BreadCrumbs/BreadCrumbs.tsx b/src/components/BreadCrumbs/BreadCrumbs.tsx
new file mode 100644
index 0000000000..98f0814f67
--- /dev/null
+++ b/src/components/BreadCrumbs/BreadCrumbs.tsx
@@ -0,0 +1,92 @@
+import styles from './BreadCrumbs.module.scss';
+import homeLight from '../../images/icon-home-light-theme.svg';
+import homeDark from '../../images/icon-home-dark-theme.svg';
+import arrowLight from '../../images/icon-right-light-theme.svg';
+import arrowDark from '../../images/icon-right-dark-theme.svg';
+import classNames from 'classnames';
+import { useTranslation } from 'react-i18next';
+import { Link, useLocation } from 'react-router-dom';
+import { useAppSelector } from '../../hooks/hooks';
+
+export const BreadCrumbs = () => {
+ const { pathname } = useLocation();
+ const { theme } = useAppSelector(state => state.theme);
+ const { t } = useTranslation();
+ const pathParts = pathname.slice(1).split('/');
+ const categoryName = pathParts[0];
+ const productName = pathParts[1]
+ ?.replace(/[-:]/g, ' ')
+ .split(' ')
+ .map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
+ .join(' ');
+
+ function capitalize(word: string) {
+ return word[0].toUpperCase() + word.slice(1);
+ }
+
+ function translate(category: string) {
+ switch (category) {
+ case 'phones':
+ return capitalize(t('breadCrumbs.phones'));
+ case 'tablets':
+ return capitalize(t('breadCrumbs.tablets'));
+ case 'accessories':
+ return capitalize(t('breadCrumbs.accessories'));
+ case 'favorites':
+ return capitalize(t('breadCrumbs.favorites'));
+ default:
+ return;
+ }
+ }
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+ {!productName ? (
+
{translate(categoryName)}
+ ) : (
+ <>
+
+ {translate(categoryName)}
+
+
+
+
+
+
+
+ {capitalize(productName)}
+
+ >
+ )}
+
+
+ );
+};
diff --git a/src/components/BreadCrumbs/index.ts b/src/components/BreadCrumbs/index.ts
new file mode 100644
index 0000000000..8ffa35f61f
--- /dev/null
+++ b/src/components/BreadCrumbs/index.ts
@@ -0,0 +1 @@
+export * from './BreadCrumbs';
diff --git a/src/components/CapacitySelection/CapacitySelection.module.scss b/src/components/CapacitySelection/CapacitySelection.module.scss
new file mode 100644
index 0000000000..7441c741c7
--- /dev/null
+++ b/src/components/CapacitySelection/CapacitySelection.module.scss
@@ -0,0 +1,56 @@
+@import '../../styles/main';
+
+.capacitySelection {
+ .container {
+ display: flex;
+ justify-content: space-between;
+
+ .title {
+ font-family: Mont, sans-serif;
+ font-size: 12px;
+ font-weight: 700;
+ color: var(--header-text-color);
+ }
+ }
+
+ .list {
+ padding-top: 8px;
+ display: flex;
+ gap: 8px;
+
+ .item {
+ height: 32px;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ font-family: Mont, sans-serif;
+ font-size: 14px;
+ font-weight: 600;
+ border: 1px solid var(--capacity-default-border);
+ background-color: var(--c-background);
+ color: var(--header-text-color-active);
+ padding: 7px 8px 4px;
+ cursor: pointer;
+ transition: all 0.3s ease;
+
+ &__active {
+ pointer-events: none;
+ color: var(--capacity-active-text);
+ background-color: var(--header-text-color-active);
+ }
+
+ &:hover {
+ background-color: rgb(169 169 169 / 30%);
+ border-color: transparent;
+ }
+ }
+
+ .button {
+ width: 100%;
+ height: 100%;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ }
+ }
+}
diff --git a/src/components/CapacitySelection/CapacitySelection.tsx b/src/components/CapacitySelection/CapacitySelection.tsx
new file mode 100644
index 0000000000..7188236b63
--- /dev/null
+++ b/src/components/CapacitySelection/CapacitySelection.tsx
@@ -0,0 +1,66 @@
+import React from 'react';
+import { ProductDetails } from '../../types/ProductDetails';
+import styles from './CapacitySelection.module.scss';
+import classNames from 'classnames';
+import { useTranslation } from 'react-i18next';
+import { Product } from '../../types/Product';
+import { useNavigate, useLocation } from 'react-router-dom';
+
+type Props = {
+ selectedProduct: ProductDetails | undefined;
+ products: Product[];
+};
+
+export const CapacitySelection: React.FC = React.memo(
+ ({ selectedProduct }) => {
+ const capacities = selectedProduct?.capacityAvailable || [];
+ const { t } = useTranslation();
+ const navigate = useNavigate();
+ const { pathname } = useLocation();
+
+ const setCapacity = (newCapacity: string) => {
+ const capacityPattern = /-(\d+[a-z]*(mm|gb|tb))/i;
+ const encodedCapacity = encodeURIComponent(
+ newCapacity.toLowerCase(),
+ ).replace(/%20/g, '-');
+
+ if (capacityPattern.test(pathname)) {
+ const updatedPath = pathname.replace(
+ capacityPattern,
+ `-${encodedCapacity}`,
+ );
+
+ navigate(updatedPath);
+ }
+ };
+
+ return (
+
+
+
{t('capacitySelection.title')}
+
+
+
+
+ );
+ },
+);
+
+CapacitySelection.displayName = 'CapacitySelection';
diff --git a/src/components/CapacitySelection/index.ts b/src/components/CapacitySelection/index.ts
new file mode 100644
index 0000000000..3eae1dae75
--- /dev/null
+++ b/src/components/CapacitySelection/index.ts
@@ -0,0 +1 @@
+export * from './CapacitySelection';
diff --git a/src/components/CartItem/CartItem.module.scss b/src/components/CartItem/CartItem.module.scss
new file mode 100644
index 0000000000..4302a210e1
--- /dev/null
+++ b/src/components/CartItem/CartItem.module.scss
@@ -0,0 +1,180 @@
+@import '../../styles/main';
+
+.cartItem {
+ padding-inline: 16px;
+ width: 100%;
+ height: 160px;
+ background-color: var(--card-bg-color);
+ border: 1px solid var(--border-cart-item);
+ transition: border-color 0.3s ease, box-shadow 0.3s ease;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ gap: 16px;
+
+ &:hover {
+ border-color: var(--card-hover-border);
+ box-shadow: var(--box-shadow-hover);
+ }
+
+ @include on-tablet {
+ height: 128px;
+ display: flex;
+ flex-direction: row;
+ justify-content: space-between;
+ align-items: center;
+ gap: 24px;
+ padding-inline: 24px;
+ }
+
+ @include on-desktop {
+ width: 752px;
+ }
+
+
+
+ .imgTitleBlock {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ justify-content: space-between;
+ gap: 16px;
+
+ @include on-tablet {
+ gap: 24px;
+ }
+
+ .closeBlock {
+ cursor: pointer;
+ background-color: transparent;
+ }
+
+ .productPhotoLink {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+
+ .prodImg {
+ width: 66px;
+ height: 66px;
+ transition: transform 0.3s ease;
+
+ &:hover {
+ transform: scale(1.1);
+ }
+
+ @include on-tablet {
+ width: 80px;
+ height: 80px;
+ }
+ }
+ }
+
+ .title {
+ color: var(--header-text-color-active);
+ font-family: Mont, sans-serif;
+ font-size: 14px;
+ font-weight: 600;
+ }
+ }
+
+ .quantityPriceBlock {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ justify-content: space-between;
+ width: 100%;
+
+ @include on-tablet {
+ gap: 24px;
+ width: auto;
+ }
+
+ .quantityBlock {
+ width: 96px;
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ justify-content: center;
+
+ .minus {
+ width: 32px;
+ height: 32px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ cursor: pointer;
+ border: 1px solid var(--slider-btn-border);
+ background-color: var(--slider-bg);
+ transition: all 0.3s ease;
+
+ &:hover {
+ background-color: var(--btn-pg-bg-hover);
+ border-color: var(--cart-btn-hover-border);
+ }
+
+ &__disabled {
+ border-color: var(--checkout-border);
+ background-color: transparent;
+ opacity: 0.5;
+ cursor: default;
+ transition: all 0.3s ease;
+
+ &:hover {
+ border-color: var(--checkout-border);
+ background-color: transparent;
+ opacity: 0.5;
+ cursor: default;
+ }
+ }
+ }
+
+ .count {
+ width: 32px;
+ height: 32px;
+ text-align: center;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ color: var(--quantity-color);
+ font-family: Mont, sans-serif;
+ font-size: 14px;
+ font-weight: 600;
+ cursor: default;
+ }
+
+ .plus {
+ width: 32px;
+ height: 32px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ cursor: pointer;
+ border: 1px solid var(--slider-btn-border);
+ background-color: var(--slider-bg);
+ transition: all 0.3s ease;
+
+ &:hover {
+ background-color: var(--btn-pg-bg-hover);
+ border-color: var(--cart-btn-hover-border);
+ }
+ }
+ }
+
+ .priceBlock {
+ cursor: default;
+ width: 80px;
+
+
+ .price {
+ color: var(--header-text-color-active);
+ font-family: Mont, sans-serif;
+ font-size: 22px;
+ font-weight: 800;
+ width: 100%;
+ text-align: right;
+ }
+ }
+ }
+}
diff --git a/src/components/CartItem/CartItem.tsx b/src/components/CartItem/CartItem.tsx
new file mode 100644
index 0000000000..0624b4db79
--- /dev/null
+++ b/src/components/CartItem/CartItem.tsx
@@ -0,0 +1,122 @@
+import { useAppDispatch, useAppSelector } from '../../hooks/hooks';
+import { UpdatedProduct } from '../../types/UpdatedProduct';
+import styles from './CartItem.module.scss';
+import closeLight from '../../images/icon-close-light-theme.svg';
+import closeDark from '../../images/icon-close-dark-theme.svg';
+import minusLight from '../../images/icon-minus-light-theme.svg';
+import minusDark from '../../images/icon-minus-dark-theme.svg';
+import plusLight from '../../images/icon-plus-light-theme.svg';
+import plusDark from '../../images/icon-plus-dark-theme.svg';
+import { Link } from 'react-router-dom';
+import {
+ removeItemFromCart,
+ incrementItemQuantity,
+ decrementItemQuantity,
+} from '../../features/cartSlice';
+
+interface Props {
+ product: UpdatedProduct;
+}
+
+export const CartItem: React.FC = ({ product }) => {
+ const { theme } = useAppSelector(state => state.theme);
+ const dispatch = useAppDispatch();
+
+ const handleRemoveItem = (event: React.MouseEvent) => {
+ event.stopPropagation();
+ event.preventDefault();
+ dispatch(removeItemFromCart(product.id));
+ };
+
+ const handleIncreaseQuantity = (event: React.MouseEvent) => {
+ event.stopPropagation();
+ event.preventDefault();
+ dispatch(incrementItemQuantity(product.id));
+ };
+
+ const handleDecreaseQuantity = (event: React.MouseEvent) => {
+ event.stopPropagation();
+ event.preventDefault();
+
+ if (product.quantity > 1) {
+ dispatch(decrementItemQuantity(product.id));
+ }
+ };
+
+ const totalPrice = product.price * product.quantity;
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
{product.name}
+
+
+
+
+
+
+
+
{
+ event.stopPropagation();
+ event.preventDefault();
+ }}
+ >
+ {product.quantity}
+
+
+
+
+
+
+
{
+ event.stopPropagation();
+ event.preventDefault();
+ }}
+ >
+ {`$${totalPrice}`}
+
+
+
+
+ );
+};
diff --git a/src/components/CartItem/index.ts b/src/components/CartItem/index.ts
new file mode 100644
index 0000000000..37a0553540
--- /dev/null
+++ b/src/components/CartItem/index.ts
@@ -0,0 +1 @@
+export * from './CartItem';
diff --git a/src/components/CategoriesSection/CategoriesSection.module.scss b/src/components/CategoriesSection/CategoriesSection.module.scss
new file mode 100644
index 0000000000..470742e58e
--- /dev/null
+++ b/src/components/CategoriesSection/CategoriesSection.module.scss
@@ -0,0 +1,111 @@
+@import '../../styles/main';
+
+.categories {
+ @include padding-content-inline-responsive;
+
+ .container {
+ .content {
+ .mainTitle {
+ padding-bottom: 24px;
+ color: var(--header-text-color-active);
+ font-family: Mont, sans-serif;
+ font-size: 32px;
+ font-weight: 800;
+ }
+
+ .items {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ gap: 32px;
+ width: 100%;
+
+ @include on-tablet {
+ width: 100%;
+ flex-direction: row;
+ gap: 15px;
+ }
+
+ @include on-desktop {
+ width: 100%;
+ gap: 16px;
+ }
+
+ .item {
+ width: 100%;
+ flex-grow: 1;
+
+ .link {
+ display: block;
+ width: 100%;
+ height: 100%;
+ position: relative;
+ cursor: pointer;
+
+ &::after {
+ content: attr(data-title);
+ width: 300px;
+ position: absolute;
+ bottom: 1%;
+ left: 50%;
+ transform: translateX(-50%);
+ background-color: rgb(211 211 211 / 80%);
+ border-radius: 6px;
+ color: darkslategrey;
+ padding: 7px;
+ font-family: Mont, sans-serif;
+ font-weight: 700;
+ font-size: 14px;
+ opacity: 0;
+ pointer-events: none;
+ transition: opacity 0.3s ease;
+ z-index: 1000;
+ }
+
+ &:hover::after {
+ opacity: 1;
+ }
+
+ .img {
+ height: 288px;
+ width: 100%;
+ object-fit: cover;
+ transition: transform 0.5s;
+
+ &:hover {
+ transform: scale(1.04);
+ }
+
+ @include on-tablet {
+ height: 187;
+ }
+
+ @include on-desktop {
+ height: 368px;
+ }
+ }
+ }
+ }
+
+ .details {
+ .title {
+ padding-top: 24px;
+ color: var(--header-text-color-active);
+ font-family: Mont, sans-serif;
+ font-size: 20px;
+ font-weight: 700;
+ }
+
+ .count {
+ padding-top: 4px;
+ color: var(--header-text-color);
+ font-family: Mont, sans-serif;
+ font-size: 14px;
+ font-weight: 600;
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/src/components/CategoriesSection/CategoriesSection.tsx b/src/components/CategoriesSection/CategoriesSection.tsx
new file mode 100644
index 0000000000..bbe73f0e77
--- /dev/null
+++ b/src/components/CategoriesSection/CategoriesSection.tsx
@@ -0,0 +1,113 @@
+import { useTranslation } from 'react-i18next';
+import styles from './CategoriesSection.module.scss';
+import { useAppSelector } from '../../hooks/hooks';
+import phonesImg from '../../images/phones-category.png';
+import tabletsImg from '../../images/tablets-category.png';
+import accessoriesImg from '../../images/accessories-category.png';
+import { Categories } from '../../types/Categories';
+import { Link } from 'react-router-dom';
+
+type CategoryBlock = {
+ title: string;
+ image: string;
+ type: Categories;
+};
+
+type CategoryOptions = {
+ phones: CategoryBlock;
+ tablets: CategoryBlock;
+ accessories: CategoryBlock;
+};
+
+export const CategoriesSection = () => {
+ const { products } = useAppSelector(state => state.products);
+ const { t } = useTranslation();
+
+ const categoryOptions: CategoryOptions = {
+ phones: {
+ title: t('homePage.categories.phonesTitle'),
+ image: phonesImg,
+ type: Categories.Phones,
+ },
+ tablets: {
+ title: t('homePage.categories.tabletsTitle'),
+ image: tabletsImg,
+ type: Categories.Tablets,
+ },
+ accessories: {
+ title: t('homePage.categories.accessoriesTitle'),
+ image: accessoriesImg,
+ type: Categories.Accessories,
+ },
+ };
+
+ const productItemsCount = (type: Categories) => {
+ const countedProducts = products.filter(
+ product => product.category === type,
+ );
+
+ return countedProducts.length;
+ };
+
+ const getCountForm = (count: number) => {
+ if (count === 1) {
+ return 'one';
+ }
+
+ if (count > 1 && count <= 4) {
+ return 'few';
+ }
+
+ if (count === 34 || count === 124) {
+ return 'few';
+ }
+
+ if (count > 4) {
+ return 'many';
+ }
+
+ return 'other';
+ };
+
+ return (
+
+
+
+
+ {t('homePage.categories.mainTitle')}
+
+
+
+ {Object.values(categoryOptions).map(category => {
+ const count = productItemsCount(category.type);
+ const countForm = getCountForm(count);
+
+ return (
+
+
+
+
+
+
+
{category.title}
+
+ {t(`homePage.categories.count_${countForm}`, { count })}
+
+
+
+ );
+ })}
+
+
+
+
+ );
+};
diff --git a/src/components/CategoriesSection/index.ts b/src/components/CategoriesSection/index.ts
new file mode 100644
index 0000000000..39e9f8bdd0
--- /dev/null
+++ b/src/components/CategoriesSection/index.ts
@@ -0,0 +1 @@
+export * from './CategoriesSection';
diff --git a/src/components/ColorSelection/ColorSelection.module.scss b/src/components/ColorSelection/ColorSelection.module.scss
new file mode 100644
index 0000000000..84104eb931
--- /dev/null
+++ b/src/components/ColorSelection/ColorSelection.module.scss
@@ -0,0 +1,60 @@
+@import '../../styles/main';
+
+.colorSelection {
+ .container {
+ display: flex;
+ justify-content: space-between;
+
+ .title {
+ font-family: Mont, sans-serif;
+ font-size: 12px;
+ font-weight: 700;
+ color: var(--header-text-color);
+ }
+
+ .id {
+ font-family: Mont, sans-serif;
+ font-size: 12px;
+ font-weight: 600;
+ color: var(--header-text-color);
+ }
+ }
+
+ .list {
+ padding-top: 8px;
+ display: flex;
+ gap: 8px;
+
+ .item {
+ width: 32px;
+ height: 32px;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ border: 1px solid var(--bg-hover-active);
+ border-radius: 50%;
+ background-color: var(--c-background);
+ cursor: pointer;
+
+ &__active {
+ pointer-events: none;
+ border-color: var(--header-text-color-active);
+ }
+
+ &:hover {
+ border-color: var(--header-text-color);
+ }
+ }
+
+ .circle {
+ width: 100%;
+ height: 100%;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ border: 2px solid var(--c-background);
+ border-radius: 50%;
+ background-color: #000;
+ }
+ }
+}
diff --git a/src/components/ColorSelection/ColorSelection.tsx b/src/components/ColorSelection/ColorSelection.tsx
new file mode 100644
index 0000000000..c79980a7b7
--- /dev/null
+++ b/src/components/ColorSelection/ColorSelection.tsx
@@ -0,0 +1,68 @@
+import React from 'react';
+import { ProductDetails } from '../../types/ProductDetails';
+import styles from './ColorSelection.module.scss';
+import classNames from 'classnames';
+import { useLocation, useNavigate, useParams } from 'react-router-dom';
+import { useTranslation } from 'react-i18next';
+import { ProductColors } from '../../constants/productColors';
+import { Product } from '../../types/Product';
+
+type Props = {
+ selectedProduct: ProductDetails | undefined;
+ products: Product[];
+};
+
+export const ColorSelection: React.FC = React.memo(
+ ({ selectedProduct, products }) => {
+ const colors = selectedProduct?.colorsAvailable || [];
+ const navigate = useNavigate();
+ const { pathname } = useLocation();
+ const { t } = useTranslation();
+ const { productId } = useParams();
+
+ const setColor = (newColor: keyof ProductColors) => {
+ const encodedColor = encodeURIComponent(newColor).replace(/%20/g, '-');
+
+ const updatedPath = pathname.replace(
+ /-([a-zA-Z-]+)$/,
+ `-${encodedColor}`,
+ );
+
+ navigate(updatedPath);
+ };
+
+ const newSelectedItemId = products.find(
+ productItem => productItem.itemId === productId?.slice(1),
+ );
+
+ return (
+
+
+
{t('colorSelection.title')}
+
{`ID: 803${newSelectedItemId?.id}`}
+
+
+
+ {colors.map(color => {
+ return (
+
+ setColor(color)}
+ >
+
+ );
+ })}
+
+
+ );
+ },
+);
+
+ColorSelection.displayName = 'ColorSelection';
diff --git a/src/components/ColorSelection/index.ts b/src/components/ColorSelection/index.ts
new file mode 100644
index 0000000000..db20d1da88
--- /dev/null
+++ b/src/components/ColorSelection/index.ts
@@ -0,0 +1 @@
+export * from './ColorSelection';
diff --git a/src/components/Footer/Footer.module.scss b/src/components/Footer/Footer.module.scss
new file mode 100644
index 0000000000..ad7b17647e
--- /dev/null
+++ b/src/components/Footer/Footer.module.scss
@@ -0,0 +1,121 @@
+@import '../../styles/main';
+
+.footer {
+ width: 100%;
+ border-top: 1px solid var(--footer-border);
+ background-color: var(--footer-background);
+
+ &__container {
+ @include padding-content-inline-responsive;
+
+ &__content {
+ height: 257px;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ gap: 32px;
+
+ @include on-tablet {
+ width: 100%;
+ height: 96px;
+ flex-direction: row;
+ justify-content: space-between;
+ align-items: center;
+ gap: 0;
+ }
+
+ &__logo {
+ @include on-tablet {
+ transition: transform 0.3s ease;
+
+ &:hover {
+ transform: scale(1.1);
+ }
+ }
+
+ &__link {
+ display: inline-block;
+ width: 89px;
+ height: 32px;
+ }
+ }
+ }
+ }
+
+ &__nav {
+ &__list {
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+
+ @include on-tablet {
+ flex-direction: row;
+ gap: 13.5px;
+ }
+
+ @include on-desktop {
+ gap: 106.83px;
+ }
+
+ &__item {
+ width: 70px;
+ font-family: Mont, sans-serif;
+ font-weight: 800;
+ font-size: 12px;
+ line-height: 11px;
+ cursor: pointer;
+ color: var(--footer-text-color);
+ transition: color 0.3s ease;
+
+ &:hover {
+ color: var(--footer-link-hover);
+ }
+
+ &__link {
+ color: var(--footer-text-color);
+ transition: color 0.3s ease;
+
+ &:hover {
+ color: var(--footer-link-hover);
+ }
+ }
+ }
+ }
+ }
+
+ &__scrollToTop {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 16px;
+
+ &__text {
+ font-family: Mont, sans-serif;
+ font-size: 12px;
+ font-weight: 700;
+ color: var(--back-top);
+ }
+
+ &__button {
+ cursor: pointer;
+ width: 32px;
+ height: 32px;
+ background-color: var(--footer-btn-bg-color);
+ border: 1px solid var(--footer-border-btn);
+ transition: background-color 0.3s ease,
+ border 0.3s ease,
+ transform 0.3s ease;
+
+ &:hover {
+ border: 1px solid var(--footer-btn-hover);
+ background-color: var(--footer-button-bg-hover);
+ }
+
+ &__icon {
+ &:hover {
+ transform: scale(1.3);
+ }
+ }
+ }
+ }
+}
diff --git a/src/components/Footer/Footer.tsx b/src/components/Footer/Footer.tsx
new file mode 100644
index 0000000000..e83500bba8
--- /dev/null
+++ b/src/components/Footer/Footer.tsx
@@ -0,0 +1,105 @@
+import { useTranslation } from 'react-i18next';
+import { useAppSelector } from '../../hooks/hooks';
+import { Link } from 'react-router-dom';
+import logoLight from '../../images/footer-logo-light-theme.svg';
+import logoDark from '../../images/footer-logo-dark-theme.svg';
+import arrowUpLight from '../../images/footer-arrowUp-light-theme.svg';
+import arrowUpDark from '../../images/footer-arrowUp-dark-theme.svg';
+import styles from './Footer.module.scss';
+import { toast } from 'react-toastify';
+import 'react-toastify/dist/ReactToastify.css';
+
+export const Footer = () => {
+ const { theme } = useAppSelector(state => state.theme);
+ const { t } = useTranslation();
+
+ const goToTop = () => window.scrollTo({ top: 0, behavior: 'smooth' });
+
+ const logo = theme === 'light' ? logoLight : logoDark;
+ const arrow = theme === 'light' ? arrowUpLight : arrowUpDark;
+
+ const handleLeavePageAlert = (
+ e: React.MouseEvent,
+ ) => {
+ const confirmLeave = window.confirm(t('footer.notificationAlert'));
+
+ if (!confirmLeave) {
+ e.preventDefault();
+ }
+ };
+
+ const handleMockRights = () => {
+ toast.info(t('footer.rightsAlert'));
+ };
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+ GITHUB
+
+
+
+
+ {t('footer.contacts')}
+
+
+
+ {t('footer.rights')}
+
+
+
+
+
+
+ {t('footer.backToTop')}
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/src/components/Footer/index.ts b/src/components/Footer/index.ts
new file mode 100644
index 0000000000..ddcc5a9cd1
--- /dev/null
+++ b/src/components/Footer/index.ts
@@ -0,0 +1 @@
+export * from './Footer';
diff --git a/src/components/GoBackButton/GoBackButton.module.scss b/src/components/GoBackButton/GoBackButton.module.scss
new file mode 100644
index 0000000000..44aa92258b
--- /dev/null
+++ b/src/components/GoBackButton/GoBackButton.module.scss
@@ -0,0 +1,32 @@
+.back {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ gap: 4px;
+
+ .backIcon {
+ opacity: 0.5;
+ cursor: pointer;
+ transition: opacity 0.3s ease;
+
+ &:hover {
+ opacity: 1;
+ }
+ }
+
+ .link {
+ font-family: Mont, sans-serif;
+ font-size: 14px;
+ font-weight: 700;
+ color: var(--header-text-color);
+ text-decoration: none;
+ background-color: transparent;
+ border: none;
+ cursor: pointer;
+ transition: color 0.3s ease;
+
+ &:hover {
+ color: var(--header-text-color-active);
+ }
+ }
+}
diff --git a/src/components/GoBackButton/GoBackButton.tsx b/src/components/GoBackButton/GoBackButton.tsx
new file mode 100644
index 0000000000..b3b87587c7
--- /dev/null
+++ b/src/components/GoBackButton/GoBackButton.tsx
@@ -0,0 +1,58 @@
+import React from 'react';
+import { Link } from 'react-router-dom';
+import { useAppSelector } from '../../hooks/hooks';
+import styles from './GoBackButton.module.scss';
+import arrowLeftLight from '../../images/icon-left-light-theme.svg';
+import arrowLeftDark from '../../images/icon-left-dark-theme.svg';
+
+interface Props {
+ title: string;
+ link?: string;
+ onClick?: () => void;
+ className?: string;
+ iconClassName?: string;
+}
+
+export const GoBackButton: React.FC = ({
+ title,
+ link,
+ onClick,
+ className,
+ iconClassName,
+}) => {
+ const { theme } = useAppSelector(state => state.theme);
+
+ const combinedClassName = `${styles.back} ${className || ''}`;
+ const iconClass = `${styles.backIcon} ${iconClassName || ''}`;
+
+ if (link) {
+ return (
+
+
+
+ {title}
+
+
+ );
+ }
+
+ return (
+
+
+
{title}
+
+ );
+};
diff --git a/src/components/GoBackButton/index.ts b/src/components/GoBackButton/index.ts
new file mode 100644
index 0000000000..ca6eb80073
--- /dev/null
+++ b/src/components/GoBackButton/index.ts
@@ -0,0 +1 @@
+export * from './GoBackButton';
diff --git a/src/components/Header/Header.module.scss b/src/components/Header/Header.module.scss
new file mode 100644
index 0000000000..4c3f09b8c5
--- /dev/null
+++ b/src/components/Header/Header.module.scss
@@ -0,0 +1,553 @@
+@use '../../utils/fonts' as fonts;
+@import '../../styles/main';
+
+.header {
+ width: 100%;
+ position: sticky;
+ top: 0;
+ z-index: 1999;
+ background-color: var(--c-background);
+ overflow: hidden;
+
+ &__top {
+ height: 48px;
+ width: 100%;
+ display: flex;
+ flex-direction: row;
+ justify-content: space-between;
+ align-items: center;
+ border-bottom: 1px solid var(--element-color);
+
+ @include on-desktop {
+ height: 64px;
+ }
+ }
+
+ &__left {
+ display: flex;
+ align-items: center;
+ height: 100%;
+ width: 100%;
+ gap: 16px;
+
+ @include on-tablet {
+ gap: 24px;
+ }
+ }
+
+ &__right {
+ display: flex;
+ flex-direction: row;
+ justify-content: end;
+ width: 100%;
+ height: 100%;
+ }
+
+ &__logo {
+ padding: 13px 16px;
+ transition: transform 0.3s ease;
+
+ &:hover {
+ transform: scale(1.1);
+ }
+
+ @include on-desktop {
+ padding: 18px 24px;
+ }
+
+ &_img {
+ width: 64px;
+ height: 22px;
+
+ @include on-desktop {
+ width: 80px;
+ height: 28px;
+ }
+ }
+ }
+
+ &__nav {
+ display: none;
+
+ @include on-tablet {
+ display: flex;
+ flex-direction: row;
+ gap: 16px;
+ justify-content: space-between;
+ align-items: center;
+ height: 100%;
+ width: 100%;
+ }
+
+ &_list {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ gap: 32px;
+ height: 100%;
+
+ @include on-desktop {
+ gap: 64px;
+ }
+ }
+
+ &_item {
+ gap: 32px;
+ height: 100%;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+
+ &_link {
+ font-family: fonts.$mont-bold;
+ font-weight: 800;
+ font-size: 12px;
+ line-height: 11px;
+ color: var(--header-text-color);
+ text-transform: uppercase;
+ transition: color 0.3s ease;
+ display: flex;
+ height: 100%;
+ text-align: center;
+ align-items: center;
+ justify-content: center;
+
+
+ &_active {
+ height: 100%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ box-sizing: border-box;
+ color: var(--header-text-color-active);
+ border-bottom: 2px solid var(--header-text-color-active);
+
+ @include on-tablet {
+ gap: 48px;
+ height: 100%;
+ }
+
+ @include on-desktop {
+ height: 100%;
+ }
+ }
+
+ &:hover {
+ color: var(--header-text-color-active);
+ }
+ }
+ }
+ }
+
+ &__theme {
+ width: 48px;
+ height: 48px;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ cursor: pointer;
+ border-left: 1px solid var(--element-color);
+
+ &:hover {
+ background-color: var(--bg-hover-active);
+ }
+
+ @include on-desktop {
+ width: 64px;
+ height: 64px;
+ }
+
+ &_btn {
+ font-family: Mont, sans-serif;
+ font-weight: 800;
+ font-size: 12px;
+ line-height: 11px;
+ letter-spacing: 4%;
+ color: var(--primary);
+ cursor: pointer;
+
+ &_icon {
+ display: flex;
+ width: 24px;
+ height: 24px;
+ }
+ }
+ }
+
+ &__icons {
+ display: none;
+
+ @include on-tablet {
+ display: flex;
+ justify-content: end;
+ }
+
+ &_favorites {
+ width: 48px;
+ height: 48px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border-left: 1px solid var(--element-color);
+ position: relative;
+ background-color: none;
+
+ &_active {
+ background-color: var(--bg-hover-active);
+ }
+
+ &:hover {
+ background-color: var(--bg-hover-active);
+ }
+
+ @include on-desktop {
+ width: 64px;
+ height: 64px;
+ }
+
+ &_img {
+ padding: 16px;
+
+
+ &_count {
+ width: 14px;
+ height: 14px;
+ background-color: red;
+ border: 1px solid var(--fav-icon-count-border);
+ border-radius: 50%;
+ position: absolute;
+ bottom: 50%;
+ left: 50%;
+ font-family: Mont, sans-serif;
+ font-weight: 700;
+ font-size: 9px;
+ color: var(--fav-text-count);
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ }
+
+ @include on-desktop {
+ padding: 24px;
+ }
+ }
+ }
+
+ &_cart {
+ width: 48px;
+ height: 48px;
+ border-left: 1px solid var(--element-color);
+ background-color: none;
+ position: relative;
+
+ &_active {
+ background-color: var(--bg-hover-active);
+ }
+
+ &:hover {
+ background-color: var(--bg-hover-active);
+ }
+
+ @include on-desktop {
+ width: 64px;
+ height: 64px;
+ }
+
+ &_img {
+ padding: 16px;
+
+ &_count {
+ width: 14px;
+ height: 14px;
+ background-color: red;
+ border: 1px solid var(--fav-icon-count-border);
+ border-radius: 50%;
+ position: absolute;
+ bottom: 50%;
+ left: 50%;
+ font-family: Mont, sans-serif;
+ font-weight: 700;
+ font-size: 9px;
+ color: var(--fav-text-count);
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ }
+
+
+ @include on-desktop {
+ padding: 24px;
+ }
+ }
+ }
+ }
+
+ &__lng {
+ width: 48px;
+ height: 48px;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ border-left: 1px solid var(--element-color);
+ cursor: pointer;
+
+ &:hover {
+ background-color: var(--bg-hover-active);
+ }
+
+ @include on-desktop {
+ width: 64px;
+ height: 64px;
+ }
+
+ &_btn {
+ font-family: Mont, sans-serif;
+ font-weight: 800;
+ font-size: 12px;
+ line-height: 11px;
+ letter-spacing: 4%;
+ color: var(--primary);
+ }
+ }
+
+ &__burgerIcon {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ cursor: pointer;
+ border-left: 1px solid var(--element-color);
+ width: 48px;
+ height: 48px;
+ transition: transform 0.3s ease;
+
+ &__image {
+ padding: 16px;
+ transition: transform 0.3s ease;
+
+ &:hover {
+ transform: scale(1.3);
+ }
+ }
+ }
+
+ .burgerMenu {
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background-color: var(--c-background);
+ transform: translateX(-100%);
+ transition:
+ transform 0.9s ease-out,
+ opacity 0.9s ease-out;
+ opacity: 0;
+ pointer-events: none;
+ z-index: 2;
+
+ @include on-tablet {
+ display: none;
+ }
+
+ &.open {
+ opacity: 1;
+ pointer-events: all;
+ transform: translateX(0);
+ }
+
+ &__top {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ border-bottom: 1px solid var(--element-color);
+ height: 48px;
+
+ &__logo {
+ padding: 13px 16px;
+
+ &__link {
+ &__img {
+ transition: transform 0.3s ease;
+
+ &:hover {
+ transform: scale(1.1);
+ }
+ }
+ }
+ }
+
+ &__icon {
+ cursor: pointer;
+ width: 48px;
+ height: 48px;
+ border-left: 1px solid var(--element-color);
+
+ &__img {
+ padding: 16px;
+ transition: transform 0.3s ease;
+
+ &:hover {
+ transform: scale(1.3);
+ }
+ }
+ }
+ }
+
+ &__nav {
+ margin-top: 24px;
+ height: 156px;
+
+ &__list {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ gap: 16px;
+ }
+ }
+
+ &__bottom {
+ width: 100%;
+ height: 64px;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ position: fixed;
+ bottom: 0;
+ left: 0;
+ border-top: 1px solid var(--element-color);
+
+ &__theme {
+ height: 100%;
+ width: 100%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+
+ &__btn {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ width: 100%;
+ height: 100%;
+ background-color: var(--c-background);
+
+ &:hover {
+ background-color: var(--bg-hover-active);
+ }
+
+ &__icon {
+ width: 24px;
+ height: 24px;
+ }
+ }
+ }
+
+ &__lng {
+ cursor: pointer;
+ height: 100%;
+ width: 100%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ text-align: center;
+ border-left: 1px solid var(--element-color);
+
+ &__btn {
+ width: 100%;
+ height: 100%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background-color: var(--c-background);
+ font-family: Mont, sans-serif;
+ font-weight: 800;
+ font-size: 12px;
+ line-height: 11px;
+ letter-spacing: 4%;
+ color: var(--primary);
+
+ &:hover {
+ background-color: var(--bg-hover-active);
+ }
+ }
+ }
+
+ &__favorites {
+ width: 100%;
+ height: 100%;
+ border-left: 1px solid var(--element-color);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ position: relative;
+
+ &:hover {
+ background-color: var(--bg-hover-active);
+ }
+
+ &__img {
+ position: relative;
+ width: 16px;
+ height: 16px;
+
+ &_count {
+ width: 14px;
+ height: 14px;
+ background-color: red;
+ border: 1px solid var(--fav-icon-count-border);
+ border-radius: 50%;
+ position: absolute;
+ bottom: 50%;
+ left: 50%;
+ font-family: Mont, sans-serif;
+ font-weight: 700;
+ font-size: 9px;
+ color: var(--fav-text-count);
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ }
+ }
+ }
+
+ &__cart {
+ width: 100%;
+ height: 100%;
+ border-left: 1px solid var(--element-color);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ position: relative;
+
+ &__img {
+ position: relative;
+ width: 16px;
+ height: 16px;
+
+ &_count {
+ width: 14px;
+ height: 14px;
+ background-color: red;
+ border: 1px solid var(--fav-icon-count-border);
+ border-radius: 50%;
+ position: absolute;
+ bottom: 50%;
+ left: 50%;
+ font-family: Mont, sans-serif;
+ font-weight: 700;
+ font-size: 9px;
+ color: var(--fav-text-count);
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ }
+ }
+
+ &:hover {
+ background-color: var(--bg-hover-active);
+ }
+ }
+ }
+ }
+}
diff --git a/src/components/Header/Header.tsx b/src/components/Header/Header.tsx
new file mode 100644
index 0000000000..19da2912ad
--- /dev/null
+++ b/src/components/Header/Header.tsx
@@ -0,0 +1,334 @@
+import { NavLink, Link, useLocation, useNavigate } from 'react-router-dom';
+import { useMediaQuery } from 'react-responsive';
+import classNames from 'classnames';
+import styles from './header.module.scss';
+import logoDarkTheme from '../../images/logo-dark-theme.svg';
+import logoLightTheme from '../../images/logo-light-theme.svg';
+import { Search } from '../Search';
+import sun from '../../images/icon-sun.svg';
+import moon from '../../images/icon-moon.svg';
+import favoritesLight from '../../images/favorites-light-theme.svg';
+import favoritesDark from '../../images/favorites-dark-theme.svg';
+import cartIconDark from '../../images/cart-dark-theme.svg';
+import { useAppDispatch, useAppSelector } from '../../hooks/hooks';
+import { useEffect, useState } from 'react';
+import { toggleTheme } from '../../features/themeSlice';
+import cartIconLight from '../../images/cart-light-theme.svg';
+import { useTranslation } from 'react-i18next';
+import { LanguageType } from '../../types/Language';
+import burgerMenuLight from '../../images/burger-menu-light-theme.svg';
+import burgerMenuDark from '../../images/burger-menu-dark-theme.svg';
+import closeLight from '../../images/icon-close-light-theme.svg';
+import closeDark from '../../images/icon-close-dark-theme.svg';
+
+const getActiveLinkClass = ({ isActive }: { isActive: boolean }) =>
+ classNames(styles.header__nav_item_link, {
+ [styles.header__nav_item_link_active]: isActive,
+ });
+
+export const Header = () => {
+ const location = useLocation();
+ const { theme } = useAppSelector(state => state.theme);
+ const { favoriteProducts } = useAppSelector(state => state.favorites);
+ const { cartProducts } = useAppSelector(state => state.cart);
+ const dispatch = useAppDispatch();
+ const { t, i18n } = useTranslation();
+ const [menuIsOpen, setMenuIsOpen] = useState(false);
+ const navigate = useNavigate();
+
+ const isMobile = useMediaQuery({ maxWidth: 640 });
+
+ const burgerMenu = theme === 'light' ? burgerMenuLight : burgerMenuDark;
+ const changeLanguage = () => {
+ const currentLang: LanguageType = i18n.language as LanguageType;
+ const newLang: LanguageType =
+ currentLang === LanguageType.EN ? LanguageType.UK : LanguageType.EN;
+
+ i18n.changeLanguage(newLang);
+ };
+
+ useEffect(() => {
+ if (menuIsOpen) {
+ document.body.style.overflow = 'hidden';
+ } else {
+ document.body.style.overflow = 'auto';
+ }
+
+ return () => {
+ document.body.style.overflow = 'auto';
+ };
+ }, [menuIsOpen]);
+
+ useEffect(() => {
+ localStorage.setItem('theme', theme);
+ document.body.classList.remove('light', 'dark');
+ document.body.classList.add(theme);
+ }, [theme]);
+
+ const visibleSearch =
+ location.pathname === '/phones' ||
+ location.pathname === '/tablets' ||
+ location.pathname === '/accessories';
+
+ const handleNavLinkClick = (path: string, eve: React.MouseEvent) => {
+ eve.preventDefault();
+ setMenuIsOpen(false);
+ navigate(path);
+ };
+
+ return (
+
+
+
+ {/* Logo Section */}
+
+
+
+
+
+
+ {/* Navigation Links */}
+
+
+
+
+ {t('header.navigation.home')}
+
+
+
+
+ {t('header.navigation.phones')}
+
+
+
+
+ {t('header.navigation.tablets')}
+
+
+
+
+ {t('header.navigation.accessories')}
+
+
+
+
+
+
+
+ {visibleSearch &&
}
+
+ {/* eslint-disable-next-line jsx-a11y/label-has-associated-control */}
+
dispatch(toggleTheme())}
+ >
+
+
+
+
+ {/* eslint-disable-next-line jsx-a11y/label-has-associated-control */}
+
+
+ {t('header.languageSwitcher')}
+
+
+
+
+ classNames(styles.header__icons_favorites, {
+ [styles.header__icons_favorites_active]: isActive,
+ })
+ }
+ >
+
+ {favoriteProducts.length > 0 && (
+
+ {favoriteProducts.length}
+
+ )}
+
+
+
+ classNames(styles.header__icons_cart, {
+ [styles.header__icons_cart_active]: isActive,
+ })
+ }
+ >
+
+ {cartProducts.length > 0 && (
+
+ {cartProducts.reduce(
+ (total, product) => total + product.quantity,
+ 0,
+ )}
+
+ )}
+
+
+
+
+ {isMobile && (
+
setMenuIsOpen(true)}
+ >
+
+
+ )}
+
+
+
+
+ );
+};
diff --git a/src/components/Header/index.ts b/src/components/Header/index.ts
new file mode 100644
index 0000000000..266dec8a1b
--- /dev/null
+++ b/src/components/Header/index.ts
@@ -0,0 +1 @@
+export * from './Header';
diff --git a/src/components/ItemsPerPageDropdown/ItemsPerPageDropdown.module.scss b/src/components/ItemsPerPageDropdown/ItemsPerPageDropdown.module.scss
new file mode 100644
index 0000000000..b2745e0077
--- /dev/null
+++ b/src/components/ItemsPerPageDropdown/ItemsPerPageDropdown.module.scss
@@ -0,0 +1,98 @@
+@import '../../styles/main';
+
+.itemsPerPageDropdown {
+ height: 59px;
+ width: 136px;
+
+ @include on-tablet {
+ width: 176px;
+ }
+
+ &__label {
+ margin-bottom: 8px;
+ font-family: Mont, sans-serif;
+ font-weight: 700;
+ font-size: 12px;
+ color: var(--header-text-color);
+ }
+
+ &__toggle {
+ width: 100%;
+ height: 40px;
+ margin-top: 4px;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ font-family: Mont, sans-serif;
+ font-size: 14px;
+ font-weight: 700;
+ line-height: 21px;
+ padding-inline: 12px;
+ border: 1px solid var(--slider-btn-border);
+ color: var(--header-text-color-active);
+ background-color: var(--dropdown-bg);
+ cursor: pointer;
+ z-index: 200;
+
+ &:focus {
+ border-color: var(--border-focus);
+ }
+
+ &:hover {
+ border-color: var(--border-focus);
+ }
+
+ &__toggleIcon {
+ width: 16px;
+ height: 16px;
+ transition: transform 0.3s ease;
+
+ &--active {
+ transform: rotate(90deg);
+ }
+ }
+ }
+
+ &__list {
+ display: none;
+ background-color: var(--c-background);
+
+ &--active {
+ width: 136px;
+ position: absolute;
+ margin-top: 5px;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ gap: 8px;
+ border: 1px solid var(--bg-hover-active);
+ z-index: 200;
+
+ @include on-tablet {
+ width: 176px;
+ }
+ }
+ }
+
+ &__item {
+ height: 40px;
+ padding-left: 12px;
+ position: relative;
+ display: flex;
+ align-items: center;
+ font-family: Mont, sans-serif;
+ font-weight: 600;
+ font-size: 14px;
+ line-height: 21px;
+ color: var(--back-top);
+ cursor: pointer;
+ transition: background-color 0.3s ease;
+
+
+ &:hover,
+ &:focus {
+ background-color: var(--bg-focus-list);
+ color: var(--header-text-color-active);
+ }
+ }
+}
diff --git a/src/components/ItemsPerPageDropdown/ItemsPerPageDropdown.tsx b/src/components/ItemsPerPageDropdown/ItemsPerPageDropdown.tsx
new file mode 100644
index 0000000000..e4d8df627b
--- /dev/null
+++ b/src/components/ItemsPerPageDropdown/ItemsPerPageDropdown.tsx
@@ -0,0 +1,103 @@
+import arrowRightLight from '../../images/icon-right-light-theme.svg';
+import arrowRightDark from '../../images/icon-right-dark-theme.svg';
+import { useAppSelector } from '../../hooks/hooks';
+import { useTranslation } from 'react-i18next';
+import { useEffect, useRef, useState } from 'react';
+import { useSearchParams } from 'react-router-dom';
+import classNames from 'classnames';
+import styles from './ItemsPerPageDropdown.module.scss';
+
+export const ItemsPerPageDropdown = () => {
+ const { theme } = useAppSelector(state => state.theme);
+ const { t } = useTranslation();
+ const [isDropdownActive, setIsDropdownActive] = useState(false);
+ const [searchParams, setSearchParams] = useSearchParams();
+ const dropdownContainerRef = useRef(null);
+
+ const sortOptions: string[] = ['4', '8', '16', t('itemsPerPageDropdown.all')];
+
+ const handleOutsideClick = (event: MouseEvent) => {
+ if (
+ dropdownContainerRef.current &&
+ !dropdownContainerRef.current.contains(event.target as Node)
+ ) {
+ setIsDropdownActive(false);
+ }
+ };
+
+ const toggleDropdown = () => {
+ setIsDropdownActive(prevState => !prevState);
+ };
+
+ const handleOptionSelect = (option: string) => {
+ const params = new URLSearchParams(searchParams);
+
+ if (option === t('itemsPerPageDropdown.all')) {
+ params.delete('perPage');
+ params.delete('page');
+ } else {
+ params.set('perPage', option);
+ }
+
+ setSearchParams(params);
+ setIsDropdownActive(false);
+ };
+
+ useEffect(() => {
+ if (isDropdownActive) {
+ document.addEventListener('mousedown', handleOutsideClick);
+ } else {
+ document.removeEventListener('mousedown', handleOutsideClick);
+ }
+
+ return () => {
+ document.removeEventListener('mousedown', handleOutsideClick);
+ };
+ }, [isDropdownActive]);
+
+ return (
+
+
+ {t('itemsPerPageDropdown.title')}
+
+
+
+
+ {searchParams.has('perPage')
+ ? searchParams.get('perPage')
+ : `${t('itemsPerPageDropdown.all')}`}
+
+
+
+
+
+ {sortOptions.map(option => (
+ handleOptionSelect(option)}
+ >
+ {option}
+
+ ))}
+
+
+ );
+};
diff --git a/src/components/ItemsPerPageDropdown/index.ts b/src/components/ItemsPerPageDropdown/index.ts
new file mode 100644
index 0000000000..deea8c7fb1
--- /dev/null
+++ b/src/components/ItemsPerPageDropdown/index.ts
@@ -0,0 +1 @@
+export * from './ItemsPerPageDropdown';
diff --git a/src/components/Loader/Loader.module.scss b/src/components/Loader/Loader.module.scss
new file mode 100644
index 0000000000..f028a554f9
--- /dev/null
+++ b/src/components/Loader/Loader.module.scss
@@ -0,0 +1,39 @@
+@import '../../styles/main';
+
+.loader {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ gap: 8px;
+
+ .circle {
+ width: 40px;
+ height: 40px;
+ border-radius: 50%;
+ border: 3px solid var(--loader-color);
+ background-color: transparent;
+ animation: bounce 1.2s infinite ease-in-out;
+ }
+
+ .circle:nth-child(2) {
+ animation-delay: 0.2s;
+ }
+
+ .circle:nth-child(3) {
+ animation-delay: 0.4s;
+ }
+
+ .circle:nth-child(4) {
+ animation-delay: 0.6s;
+ }
+}
+
+@keyframes bounce {
+ 0%, 80%, 100% {
+ transform: scale(0);
+ }
+
+ 40% {
+ transform: scale(1);
+ }
+}
diff --git a/src/components/Loader/Loader.tsx b/src/components/Loader/Loader.tsx
new file mode 100644
index 0000000000..dd295b0069
--- /dev/null
+++ b/src/components/Loader/Loader.tsx
@@ -0,0 +1,10 @@
+import styles from './Loader.module.scss';
+
+export const Loader = () => (
+
+);
diff --git a/src/components/Loader/index.ts b/src/components/Loader/index.ts
new file mode 100644
index 0000000000..d5ce981151
--- /dev/null
+++ b/src/components/Loader/index.ts
@@ -0,0 +1 @@
+export * from './Loader';
diff --git a/src/components/LoaderProductCard/LoaderProductCard.module.scss b/src/components/LoaderProductCard/LoaderProductCard.module.scss
new file mode 100644
index 0000000000..dd8b3a57f2
--- /dev/null
+++ b/src/components/LoaderProductCard/LoaderProductCard.module.scss
@@ -0,0 +1,45 @@
+@import '../../styles/main';
+
+.loaderProductCard {
+ margin-top: 24px;
+
+ .content {
+ width: 100%;
+ height: 100%;
+
+
+ .list {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(287px, 1fr)); // Items stretch dynamically
+ gap: 16px;
+
+ @include on-tablet {
+ grid-template-columns: repeat(auto-fit, minmax(288px, 1fr));
+ }
+
+ @include on-desktop {
+ grid-template-columns: repeat(auto-fit, minmax(272px, 1fr));
+ }
+
+ .item {
+ height: 506px;
+ min-width: 287px;
+ max-width: 1fr;
+ border: 1px solid #9CA3AF;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+
+
+ @include on-tablet {
+ height: 506px;
+ }
+
+ @include on-desktop {
+ height: 506px;
+ }
+ }
+}
+}
+}
+
diff --git a/src/components/LoaderProductCard/LoaderProductCard.tsx b/src/components/LoaderProductCard/LoaderProductCard.tsx
new file mode 100644
index 0000000000..fab22431a5
--- /dev/null
+++ b/src/components/LoaderProductCard/LoaderProductCard.tsx
@@ -0,0 +1,44 @@
+import React from 'react';
+import ContentLoader from 'react-content-loader';
+import { Product } from '../../types/Product';
+import styles from './LoaderProductCard.module.scss';
+
+type Props = {
+ products: Product[];
+};
+
+export const LoaderProductCard: React.FC = React.memo(({ products }) => {
+ return (
+
+
+
+ {products.map(product => (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ))}
+
+
+
+ );
+});
+
+LoaderProductCard.displayName = 'LoaderProductCard';
diff --git a/src/components/LoaderProductCard/index.ts b/src/components/LoaderProductCard/index.ts
new file mode 100644
index 0000000000..7655a8666f
--- /dev/null
+++ b/src/components/LoaderProductCard/index.ts
@@ -0,0 +1 @@
+export * from './LoaderProductCard';
diff --git a/src/components/Modal/Modal.module.scss b/src/components/Modal/Modal.module.scss
new file mode 100644
index 0000000000..0e0ccadaf6
--- /dev/null
+++ b/src/components/Modal/Modal.module.scss
@@ -0,0 +1,94 @@
+@import '../../styles/main';
+
+.cancelBtn,
+.confirmBtn {
+ font-family: Mont, sans-serif;
+ padding: 12px 24px;
+ font-size: 16px;
+ border: none;
+ border-radius: 8px;
+ cursor: pointer;
+ font-weight: 500;
+ transition: all 0.3s ease;
+
+}
+
+.cancelBtn {
+ background-color: var(--button-cancel-bg);
+ color: var(--button-cancel-text);
+
+ &:focus {
+ outline: none;
+ box-shadow: 0 0 0 3px rgb(0 123 255 / 25%);
+ }
+
+ &:hover {
+ background-color: var(--button-cancel-hover);
+ }
+}
+
+.confirmBtn {
+ background-color: var(--button-confirm-bg);
+ color: var(--button-confirm-text);
+
+ &:focus {
+ outline: none;
+ box-shadow: 0 0 0 3px rgb(0 123 255 / 25%);
+ }
+
+&:hover {
+ background-color: var(--button-confirm-hover);
+ box-shadow: var(--btn-hover-shadow);
+
+}
+}
+
+.overlay {
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background: var(--overlay-bg);
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ z-index: 9999;
+
+
+.modal {
+ background: var(--modal-bg);
+ padding: 24px;
+ border-radius: 12px;
+ width: 500px;
+ max-width: 90%;
+ text-align: center;
+ box-shadow: var(--modal-shadow);
+ transition: transform 0.3s ease-in-out, background-color 0.3s ease;
+ transform: scale(0.95);
+ animation: modalAppear 0.3s ease-out forwards;
+
+
+.modalTitle {
+ font-family: Mont, sans-serif;
+ font-size: 20px;
+ font-weight: 600;
+ color: var(--modal-title);
+ margin-bottom: 12px;
+}
+
+.modalMessage {
+ font-family: Mont, sans-serif;
+ font-size: 16px;
+ font-weight: 400;
+ color: var(--modal-message);
+ margin-bottom: 24px;
+}
+
+.modalActions {
+ display: flex;
+ justify-content: space-evenly;
+ gap: 12px;
+}
+}
+}
diff --git a/src/components/Modal/Modal.tsx b/src/components/Modal/Modal.tsx
new file mode 100644
index 0000000000..fc5696ba63
--- /dev/null
+++ b/src/components/Modal/Modal.tsx
@@ -0,0 +1,34 @@
+import React from 'react';
+import styles from './Modal.module.scss';
+import { useTranslation } from 'react-i18next';
+
+interface Props {
+ onClose: () => void;
+ onConfirm: () => void;
+ isOpen: boolean;
+}
+
+export const Modal: React.FC = ({ onClose, onConfirm, isOpen }) => {
+ const { t } = useTranslation();
+
+ if (!isOpen) {
+ return null;
+ }
+
+ return (
+
+
+
{t('modal.title')}
+
{t('modal.message')}
+
+
+ {t('modal.cancelBtn')}
+
+
+ {t('modal.confirmBtn')}
+
+
+
+
+ );
+};
diff --git a/src/components/Modal/index.ts b/src/components/Modal/index.ts
new file mode 100644
index 0000000000..cb89ee1788
--- /dev/null
+++ b/src/components/Modal/index.ts
@@ -0,0 +1 @@
+export * from './Modal';
diff --git a/src/components/Pagination/Pagination.module.scss b/src/components/Pagination/Pagination.module.scss
new file mode 100644
index 0000000000..a507425d9a
--- /dev/null
+++ b/src/components/Pagination/Pagination.module.scss
@@ -0,0 +1,99 @@
+@import '../../styles/main';
+
+.pagination {
+ width: 100%;
+ height: 32px;
+ display: flex;
+ justify-content: center;
+ flex-flow: row wrap;
+ margin-top: 24px;
+
+ @include on-tablet {
+ margin-top: 40px;
+ }
+
+ .button {
+ width: 32px;
+ height: 32px;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ border: 1px solid var(--btn-pg-border);
+ background-color: var(--footer-btn-bg-color);
+ cursor: pointer;
+
+ &:hover {
+ border-color: var(--btn-pg-hover-border);
+ background-color: var(--btn-pg-bg-hover);
+ }
+
+ &__right {
+ &__disabled {
+ border-color: var(--bg-hover-active);
+ background-color: transparent;
+ pointer-events: none;
+ opacity: 0.5;
+ }
+ }
+
+ &__left {
+ &__disabled {
+ border-color: var(--bg-hover-active);
+ background-color: transparent;
+ pointer-events: none;
+ opacity: 0.5;
+ }
+ }
+
+ .iconLeft {
+ transform: rotate(180deg);
+ }
+
+ .iconRight {
+ &__disabled {
+ transform: rotate(180deg);
+ }
+ }
+ }
+
+ &__container {
+ width: 152px;
+ margin-inline: 16px;
+ overflow: hidden;
+ }
+
+ &__pageList {
+ display: flex;
+ max-width: 184px;
+ gap: 8px;
+ }
+
+ &__item {
+ width: 32px;
+ height: 32px;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ flex-shrink: 0;
+ font-family: Mont, sans-serif;
+ font-size: 14px;
+ font-weight: 600;
+ line-height: 21px;
+ border: 1px solid var(--pg-item-border);
+ color: var(--pg-item);
+ background-color: var(--pg-bg-color);
+ cursor: pointer;
+
+ &:hover {
+ border-color: var(--btn-pg-hover-border);
+ background-color: var(--pg-bg-hover);
+ }
+
+ &__active {
+ background-color: var(--pg-selected);
+ color: var(--pg-item-selected);
+ border-color: var(--border-pg-selected);
+ pointer-events: none;
+ }
+ }
+}
diff --git a/src/components/Pagination/Pagination.tsx b/src/components/Pagination/Pagination.tsx
new file mode 100644
index 0000000000..f34c579fb7
--- /dev/null
+++ b/src/components/Pagination/Pagination.tsx
@@ -0,0 +1,142 @@
+import React, { useEffect, useState } from 'react';
+import { useSearchParams } from 'react-router-dom';
+import arrowLight from '../../images/icon-right-light-theme.svg';
+import arrowDark from '../../images/icon-right-dark-theme.svg';
+import { useAppSelector } from '../../hooks/hooks';
+import styles from './Pagination.module.scss';
+
+type Props = {
+ totalItems: number;
+};
+
+export const Pagination: React.FC = ({ totalItems }) => {
+ const [searchParams, setSearchParams] = useSearchParams();
+ const [paginationOffset, setPaginationOffset] = useState(
+ Number(searchParams.get('offset')) || 0,
+ );
+ const currentPage = Number(searchParams.get('page')) || 1;
+ const itemsPerPage = searchParams.get('perPage') || 'All';
+ const totalPages = Math.ceil(totalItems / Number(itemsPerPage));
+ const searchQuery = searchParams.get('query');
+
+ const { theme } = useAppSelector(state => state.theme);
+
+ const generatePageNumbers = (total: number) => {
+ return Array.from({ length: total }, (_, i) => i + 1);
+ };
+
+ const pageNumbers = generatePageNumbers(totalPages);
+
+ const [pageGroupIndex, setPageGroupIndex] = useState(
+ Number(searchParams.get('section')) || 0,
+ );
+
+ const groupSize = 4;
+ const groupStart = pageGroupIndex * groupSize;
+ const groupEnd = groupStart + groupSize;
+ const currentPageGroup = pageNumbers.slice(groupStart, groupEnd);
+
+ const firstPageInGroup = currentPageGroup[0];
+ const lastPageInGroup = currentPageGroup[3];
+
+ const handleNextGroup = () => {
+ const params = new URLSearchParams(searchParams);
+
+ if (currentPage < totalPages) {
+ params.set('page', `${currentPage + 1}`);
+ }
+
+ if (currentPage === lastPageInGroup) {
+ setPageGroupIndex(pageGroupIndex + 1);
+ setPaginationOffset(paginationOffset + 160);
+ params.set('section', `${pageGroupIndex + 1}`);
+ params.set('offset', `${paginationOffset + 160}`);
+ }
+
+ setSearchParams(params);
+ };
+
+ const handlePreviousGroup = () => {
+ const params = new URLSearchParams(searchParams);
+
+ if (currentPage > 1) {
+ params.set('page', `${currentPage - 1}`);
+ }
+
+ if (currentPage === firstPageInGroup) {
+ setPageGroupIndex(pageGroupIndex - 1);
+ setPaginationOffset(paginationOffset - 160);
+ params.set('section', `${pageGroupIndex - 1}`);
+ params.set('offset', `${paginationOffset - 160}`);
+ }
+
+ setSearchParams(params);
+ };
+
+ const handlePageChange = (pageNumber: number) => {
+ const params = new URLSearchParams(searchParams);
+
+ params.set('page', pageNumber.toString());
+ setSearchParams(params);
+ };
+
+ useEffect(() => {
+ if (searchQuery || searchQuery === '') {
+ setPaginationOffset(0);
+ setPageGroupIndex(0);
+ }
+ }, [searchQuery]);
+
+ return (
+
+
+
+
+
+
+
+ {pageNumbers.map(page => (
+ handlePageChange(page)}
+ >
+ {page}
+
+ ))}
+
+
+
+
+
+
+
+ );
+};
+
+Pagination.displayName = 'Pagination';
diff --git a/src/components/Pagination/index.ts b/src/components/Pagination/index.ts
new file mode 100644
index 0000000000..2879418b80
--- /dev/null
+++ b/src/components/Pagination/index.ts
@@ -0,0 +1 @@
+export * from './Pagination.module.scss';
diff --git a/src/components/PhonesSlider/PhonesSlider.scss b/src/components/PhonesSlider/PhonesSlider.scss
new file mode 100644
index 0000000000..fd6a986fcf
--- /dev/null
+++ b/src/components/PhonesSlider/PhonesSlider.scss
@@ -0,0 +1,130 @@
+@import '../../styles/main';
+
+.phonesSlider.container {
+ box-sizing: border-box;
+ position: relative;
+ width: 100%;
+}
+
+.phonesSlider .title {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ gap: 72px;
+ min-height: 41px;
+ padding-bottom: 24px;
+ color: var(--header-text-color-active);
+ font-family: Mont, sans-serif;
+ text-align: left;
+ font-size: 22px;
+ font-weight: 800;
+ padding-right: 16px;
+
+ @include on-tablet {
+ padding-right: 0;
+ }
+
+}
+
+.phoneSlider.swiper {
+ &_container {
+ width: 100%;
+ height: 100%;
+ }
+
+ width: 100%;
+ height: 100%;
+ overflow: hidden;
+}
+
+.phonesSlider .swiper-slide {
+ text-align: center;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ height: 440px;
+ width: 212px;
+ padding: 32px;
+ border: 1px solid var(--card-border);
+ background-color: var(--card-bg-color);
+ transition: border-color 0.3s ease, box-shadow 0.3s ease;
+
+ &:hover {
+ border-color: var(--card-hover-border);
+ box-shadow: var(--box-shadow-hover);
+ }
+
+ @include on-tablet {
+ font-size: 32px;
+ height: 512px;
+ width: 237px;
+ }
+
+ @include on-desktop {
+ height: 506px;
+ width: 272px;
+ }
+}
+
+.phonesSlider .controls {
+ display: flex;
+ gap: 16px;
+ height: 32px;
+}
+
+.phonesSlider .icon_container {
+ width: 32px;
+ height: 32px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ cursor: pointer;
+ transition: border-color 0.3s;
+ border: 1px solid var(--slider-btn-border);
+ background-color: var(--slider-bg);
+
+ &:hover {
+ border-color: var(--primary);
+ }
+
+
+ &.swiper-button-disabled {
+ opacity: 0.5;
+ cursor: not-allowed;
+ background-color: transparent;
+
+ &:hover {
+ border-color: var(--slider-border);
+
+ }
+ }
+}
+
+.phonesSlider .icon {
+ width: 16px;
+ height: 16px;
+ background-repeat: no-repeat;
+ background-size: cover;
+}
+
+.phonesSlider .icon_left {
+ &.light-theme {
+ background-image: url('../../images/icon-left-light-theme.svg');
+ }
+
+ &.dark-theme {
+ background-image: url('../../images/icon-left-dark-theme.svg');
+ }
+
+}
+
+.phonesSlider .icon_right {
+ &.light-theme {
+ background-image: url('../../images/icon-right-light-theme.svg');
+ }
+
+ &.dark-theme {
+ background-image: url('../../images/icon-right-dark-theme.svg');
+ }
+
+}
diff --git a/src/components/PhonesSlider/PhonesSlider.tsx b/src/components/PhonesSlider/PhonesSlider.tsx
new file mode 100644
index 0000000000..84f9869e74
--- /dev/null
+++ b/src/components/PhonesSlider/PhonesSlider.tsx
@@ -0,0 +1,90 @@
+import 'swiper/css';
+import 'swiper/css/pagination';
+import './PhonesSlider.scss';
+
+import classNames from 'classnames';
+import 'swiper/css/navigation';
+
+import React, { useRef } from 'react';
+import { Swiper, SwiperSlide } from 'swiper/react';
+import { Navigation } from 'swiper/modules';
+import { ProductCard } from '../ProductCard';
+import { Product } from '../../types/Product';
+import { useAppSelector } from '../../hooks/hooks';
+
+type Props = {
+ title: string;
+ products: Product[];
+ discount?: boolean;
+ slash?: boolean | undefined;
+};
+
+export const PhonesSlider: React.FC = ({
+ title,
+ products,
+ discount,
+}) => {
+ const sliderRef = useRef>(null);
+ const prevRef = useRef(null);
+ const nextRef = useRef(null);
+
+ const { theme } = useAppSelector(state => state.theme);
+
+ return (
+
+
+
+
+
{
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+ // @ts-ignore
+ // eslint-disable-next-line no-param-reassign
+ swiper.params.navigation.prevEl = prevRef.current;
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+ // @ts-ignore
+ // eslint-disable-next-line no-param-reassign
+ swiper.params.navigation.nextEl = nextRef.current;
+ swiper.navigation.init();
+ swiper.navigation.update();
+ }}
+ >
+ {products.map((product: Product) => (
+
+
+
+ ))}
+
+
+
+ );
+};
diff --git a/src/components/PicturesSlider/PicturesSlider.scss b/src/components/PicturesSlider/PicturesSlider.scss
new file mode 100644
index 0000000000..575e6872a0
--- /dev/null
+++ b/src/components/PicturesSlider/PicturesSlider.scss
@@ -0,0 +1,122 @@
+@import '../../styles/main';
+
+.picturesSwiper {
+ &_container {
+ display: flex;
+ box-sizing: border-box;
+ width: 100%;
+ height: 100%;
+ gap: 19px;
+
+ @include on-tablet {
+ flex-direction: row;
+ }
+
+ & .icon {
+ box-sizing: content-box;
+
+ &_container {
+ cursor: pointer;
+ box-sizing: border-box;
+ justify-content: center;
+ align-items: center;
+ overflow: hidden;
+ transition: border-color 0.3s;
+ border: 1px solid var(--slider-btn-border);
+ width: 32px;
+ height: 32px;
+ display: none;
+ flex-shrink: 0;
+ background-color: var(--slider-bg);
+
+ &:hover {
+ border-color: var(--primary);
+ }
+
+ @include on-tablet {
+ display: flex;
+ height: 189px;
+ }
+
+ @include on-desktop {
+ display: flex;
+ height: 400px;
+ }
+ }
+
+ &_left {
+ width: 16px;
+ height: 16px;
+ background-repeat: no-repeat;
+ background-size: cover;
+
+ &.dark-theme {
+ background-image: url('../../images/icon-left-dark-theme.svg');
+ }
+
+ &.light-theme {
+ background-image: url('../../images/icon-left-light-theme.svg');
+ }
+ }
+
+ &_right {
+ width: 16px;
+ height: 16px;
+ background-repeat: no-repeat;
+ background-size: cover;
+
+ &.dark-theme {
+ background-image: url('../../images/icon-right-dark-theme.svg');
+ }
+
+ &.light-theme {
+ background-image: url('../../images/icon-right-light-theme.svg');
+ }
+ }
+ }
+ }
+
+ &.swiper {
+ display: flex;
+ flex-direction: column;
+ width: 100%;
+ }
+
+ & img {
+ height: 100%;
+ }
+
+ & .swiper-slide {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ overflow: hidden;
+ height: 100vw;
+
+ @include on-tablet {
+ height: 189px;
+ }
+
+ @include on-desktop {
+ height: 400px;
+ }
+ }
+
+ & .swiper-pagination {
+ position: relative;
+ bottom: unset;
+ height: 24px;
+
+ &-bullet {
+ height: 4px;
+ width: 14px;
+ border-radius: 0%;
+ background-color: var(--footer-border);
+ opacity: 1;
+
+ &-active {
+ background-color: var(--primary);
+ }
+ }
+ }
+}
diff --git a/src/components/PicturesSlider/PicturesSlider.tsx b/src/components/PicturesSlider/PicturesSlider.tsx
new file mode 100644
index 0000000000..81b7dec425
--- /dev/null
+++ b/src/components/PicturesSlider/PicturesSlider.tsx
@@ -0,0 +1,125 @@
+import 'swiper/scss';
+import 'swiper/scss/pagination';
+import 'swiper/scss/navigation';
+
+import classNames from 'classnames';
+
+import { useRef } from 'react';
+import { Swiper, SwiperSlide } from 'swiper/react';
+import { Autoplay, Navigation, Pagination } from 'swiper/modules';
+import type SwiperCore from 'swiper';
+import './PicturesSlider.scss';
+
+import bannerAccessories from '../../images/banner-accessories.png';
+import bannerPhones from '../../images/banner-phones.png';
+import bannerTablets from '../../images/banner-tablets.png';
+import { useAppSelector } from '../../hooks/hooks';
+
+const PicturesSlider = () => {
+ const swiperRef = useRef();
+ const { theme } = useAppSelector(state => state.theme);
+
+ return (
+
+
swiperRef.current?.slidePrev()}
+ >
+
+
+
+
{
+ swiperRef.current = swiper;
+ }}
+ className="picturesSwiper"
+ >
+
+ = 1200
+ ? '110%'
+ : window.innerWidth >= 640
+ ? '100%'
+ : '100%',
+ objectFit: 'cover',
+ }}
+ />
+
+
+
+ = 1200
+ ? '110%'
+ : window.innerWidth >= 640
+ ? '90%'
+ : '100%',
+
+ height:
+ window.innerWidth >= 1200
+ ? '120%'
+ : window.innerWidth >= 640
+ ? '100%'
+ : '100%',
+
+ objectFit: 'cover',
+ }}
+ />
+
+
+
+ = 1200
+ ? '104%'
+ : window.innerWidth >= 640
+ ? '100%'
+ : '100%',
+
+ objectFit: 'cover',
+ }}
+ />
+
+
+
+
swiperRef.current?.slideNext()}
+ >
+
+
+
+ );
+};
+
+export default PicturesSlider;
diff --git a/src/components/PicturesSlider/index.ts b/src/components/PicturesSlider/index.ts
new file mode 100644
index 0000000000..81a373f3aa
--- /dev/null
+++ b/src/components/PicturesSlider/index.ts
@@ -0,0 +1 @@
+export * from './PicturesSlider';
diff --git a/src/components/ProductCard/ProductCard.module.scss b/src/components/ProductCard/ProductCard.module.scss
new file mode 100644
index 0000000000..01059f2abc
--- /dev/null
+++ b/src/components/ProductCard/ProductCard.module.scss
@@ -0,0 +1,188 @@
+@import '../../styles/main';
+
+.productCard {
+ width: 100%;
+ height: 100%;
+
+ @include on-tablet {
+ max-width: 288px;
+ }
+
+ @include on-desktop {
+ max-width: 272px;
+ }
+
+ .content {
+ width: 100%;
+ height: 100%;
+ display: flex;
+ flex-direction: column;
+ justify-content: space-between;
+ gap: 8px;
+
+ .link {
+ height: 100%;
+ width: 100%;
+ display: flex;
+ justify-content: center;
+ transition: transform 0.3s;
+
+ &:hover {
+ transform: scale(1.1);
+ }
+
+ .img {
+ height: 129px;
+ width: 148px;
+ object-fit: contain;
+
+ @include on-tablet {
+ height: 173px;
+ width: 202px;
+ }
+
+ @include on-desktop {
+ height: 196px;
+ width: 208px;
+ }
+ }
+ }
+
+ .title {
+ font-family: Mont, sans-serif;
+ font-weight: 600;
+ font-size: 14px;
+ line-height: 21px;
+ color: var(--header-text-color-active);
+ text-decoration: none;
+
+ }
+ }
+
+ .priceBox {
+ display: flex;
+ gap: 8px;
+
+ .currentPrice {
+ font-family: Mont, sans-serif;
+ font-weight: 800;
+ font-size: 22px;
+ line-height: 30.8px;
+ color: var(--header-text-color-active);
+ }
+
+ .fullPrice {
+ font-family: Mont, sans-serif;
+ font-weight: 800;
+ font-size: 22px;
+ line-height: 30.8px;
+ color: var(--header-text-color);
+ position: relative;
+ z-index: 1;
+
+ &__curvyLine {
+ position: absolute;
+ top: 50%;
+ left: 0;
+ width: 100%;
+ height: 10px;
+ transform: translateY(-50%);
+ transform-origin: center;
+ z-index: 2;
+ overflow: visible;
+ }
+ }
+ }
+
+
+ .line {
+ display: flex;
+ height: 1px;
+ width: 100%;
+ background-color: var(--bg-hover-active);
+ }
+
+
+ .specsContainer {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+
+ .specs {
+ display: flex;
+ justify-content: space-between;
+
+ .title {
+ font-family: Mont, sans-serif;
+ font-weight: 700;
+ font-size: 12px;
+ line-height: 15.34px;
+ color: var(--header-text-color);
+ }
+
+ .param {
+ font-family: Mont, sans-serif;
+ font-weight: 700;
+ font-size: 12px;
+ line-height: 15.34px;
+ color: var(--header-text-color-active);
+ }
+
+ }
+ }
+
+.buttons {
+ box-sizing: border-box;
+ display: flex;
+ flex-direction: row;
+ justify-content: center;
+ align-items: center;
+ width: 100%;
+ height: 40px;
+ gap: 8px;
+
+ .cartBtn {
+ background-color: var(--btn-add-cart-bg);
+ font-family: Mont, sans-serif;
+ font-size: 14px;
+ font-weight: 600;
+ color: #fff;
+ width: 100%;
+ height: 40px;
+ cursor: pointer;
+ transition: box-shadow 0.2s ease, background-color 0.2s ease;
+
+ &--active {
+ background-color: var(--cart-added);
+ color: var(--btn-text-cart);
+ border: 1px solid var(--cart-added-border);
+ }
+
+ &:hover {
+ background-color: var(--btn-bg-hover);
+ box-shadow: var(--btn-hover-shadow);
+ }
+ }
+
+ .favBtn {
+ flex-shrink: 0;
+ border: 1px solid var(--fav-icon-border);
+ width: 40px;
+ height: 40px;
+ background-color: var(--fav-bg);
+ cursor: pointer;
+
+ &--active {
+ border-color: var(--bg-hover-active);
+ background-color: var(--fav-active);
+ }
+
+ &:hover {
+ border-color: var(--fav-border-hover);
+ background-color: var(--fav-hover);
+ }
+ }
+
+}
+
+}
diff --git a/src/components/ProductCard/ProductCard.tsx b/src/components/ProductCard/ProductCard.tsx
new file mode 100644
index 0000000000..4380e65f93
--- /dev/null
+++ b/src/components/ProductCard/ProductCard.tsx
@@ -0,0 +1,156 @@
+import React from 'react';
+import { useTranslation } from 'react-i18next';
+import { Link } from 'react-router-dom';
+import classNames from 'classnames';
+import { Product } from '../../types/Product';
+import { useAppDispatch, useAppSelector } from '../../hooks/hooks';
+import { addFavorite, removeFavorite } from '../../features/favoritesSlice';
+import { addItemToCart, removeItemFromCart } from '../../features/cartSlice';
+import heartLight from '../../images/icon-heart-light-theme.svg';
+import heartDark from '../../images/icon-heart-dark-theme.svg';
+import favFilledHeart from '../../images/icon-filled-heart-fav-red.svg';
+import styles from './ProductCard.module.scss';
+import { toast } from 'react-hot-toast';
+
+type Props = {
+ product: Product;
+ discount?: boolean;
+ slash?: boolean;
+};
+
+export const ProductCard: React.FC = React.memo(
+ ({ product, slash, discount }) => {
+ const { image, name, price, screen, capacity, ram, fullPrice } = product;
+
+ const { theme } = useAppSelector(state => state.theme);
+ const { t } = useTranslation();
+ const dispatch = useAppDispatch();
+ const { favoriteProducts } = useAppSelector(state => state.favorites);
+ const { cartProducts } = useAppSelector(state => state.cart);
+
+ const handleFavorites = (prod: Product) => {
+ if (favoriteProducts.some(item => item.id === prod.id)) {
+ dispatch(removeFavorite(prod));
+ } else {
+ dispatch(addFavorite(prod));
+ }
+ };
+
+ const handleCart = (prod: Product) => {
+ if (cartProducts.some(item => item.id === prod.id)) {
+ dispatch(removeItemFromCart(prod.id));
+ toast(t('productCard.toast.removed', { name: prod.name }), {
+ icon: '🛒',
+ });
+ } else {
+ dispatch(addItemToCart(prod));
+ toast.success(t('productCard.toast.added', { name: prod.name }));
+ }
+ };
+
+ const isFavorite = favoriteProducts.some(item => item.id === product.id);
+ const isInCart = cartProducts.some(prod => prod.id === product.id);
+
+ return (
+
+
+
+ {slash ? (
+
+ ) : (
+
+ )}
+
+
+
+ {name}
+
+
+
{`$${price}`}
+ {discount && (
+
+ {`$${fullPrice}`}
+
+
+
+
+ )}
+
+
+
+
+
+
+
+ {t('productCard.specs.screen')}
+
+
{screen}
+
+
+
+
+ {t('productCard.specs.capacity')}
+
+
{capacity}
+
+
+
+
{t('productCard.specs.ram')}
+
{ram}
+
+
+
+
+
handleCart(product)}
+ >
+ {isInCart
+ ? t('productCard.button.added')
+ : t('productCard.button.add')}
+
+
+
handleFavorites(product)}
+ >
+
+
+
+
+
+ );
+ },
+);
+
+ProductCard.displayName = 'ProductCard';
diff --git a/src/components/ProductCard/index.ts b/src/components/ProductCard/index.ts
new file mode 100644
index 0000000000..7ce031c382
--- /dev/null
+++ b/src/components/ProductCard/index.ts
@@ -0,0 +1 @@
+export * from './ProductCard';
diff --git a/src/components/ProductDetailsCard/ProductDetailsCard.module.scss b/src/components/ProductDetailsCard/ProductDetailsCard.module.scss
new file mode 100644
index 0000000000..725f8bd8e1
--- /dev/null
+++ b/src/components/ProductDetailsCard/ProductDetailsCard.module.scss
@@ -0,0 +1,346 @@
+@import '../../styles/main';
+
+.card {
+ padding-bottom: 56px;
+
+ @include on-tablet {
+ padding-bottom: 64px;
+ }
+
+ @include on-desktop {
+ padding-bottom: 80px;
+ }
+
+ .container {
+ display: flex;
+ flex-direction: column;
+ gap: 40px;
+ padding-bottom: 56px;
+
+ @include on-tablet {
+ flex-direction: row;
+ gap: 17px;
+ padding-bottom: 64px;
+ }
+
+ @include on-desktop {
+ gap: 64px;
+ padding-bottom: 80px;
+ }
+
+ .swiper {
+ width: 100%;
+ height: auto;
+
+ @include on-tablet {
+ width: 50%;
+ }
+ }
+
+.wrapper {
+ display: flex;
+ flex-direction: column;
+ width: 100%;
+
+ @include on-tablet {
+ width: 50%;
+ }
+
+ .colorIdSection {
+ .line {
+ display: inline-block;
+ margin-block: 24px;
+ width: 100%;
+ height: 1px;
+ background-color: var(--bg-hover-active);
+
+ @include on-desktop {
+ width: 320px;
+ }
+ }
+ }
+
+ .capacitySection {
+ .lineCapacity {
+ display: inline-block;
+ margin-top: 24px;
+ margin-bottom: 32px;
+ width: 100%;
+ height: 1px;
+ background-color: var(--bg-hover-active);
+
+ @include on-desktop {
+ width: 320px;
+ }
+ }
+ }
+
+
+ .priceBtnsInfo {
+ display: flex;
+ flex-direction: column;
+
+ .price {
+ display: flex;
+ gap: 8px;
+ padding-bottom: 16px;
+
+ .currentPrice {
+ font-family: Mont, sans-serif;
+ font-weight: 800;
+ font-size: 32px;
+ line-height: 30.8px;
+ color: var(--header-text-color-active);
+ }
+
+ .fullPrice {
+ font-family: Mont, sans-serif;
+ font-weight: 600;
+ font-size: 22px;
+ line-height: 30.8px;
+ color: var(--header-text-color);
+ position: relative;
+ z-index: 1;
+
+ &__curvyLine {
+ position: absolute;
+ top: 50%;
+ left: 0;
+ width: 100%;
+ height: 10px;
+ transform: translateY(-50%);
+ transform-origin: center;
+ z-index: 2;
+ overflow: visible;
+ }
+ }
+ }
+
+ .btns {
+ box-sizing: border-box;
+ display: flex;
+ flex-direction: row;
+ justify-content: center;
+ align-items: center;
+ width: 100%;
+ height: 48px;
+ gap: 8px;
+
+ @include on-desktop {
+ width: 320px;
+ }
+
+ .cartBtn {
+ background-color: var(--btn-add-cart-bg);
+ font-family: Mont, sans-serif;
+ font-size: 14px;
+ font-weight: 600;
+ color: #fff;
+ width: 100%;
+ height: 48px;
+ cursor: pointer;
+ transition: box-shadow 0.2s ease, background-color 0.2s ease;
+
+ &--active {
+ background-color: var(--cart-added);
+ color: var(--btn-text-cart);
+ border: 1px solid var(--cart-added-border);
+ }
+
+ &:hover {
+ background-color: var(--btn-bg-hover);
+ box-shadow: var(--btn-hover-shadow);
+ }
+ }
+
+ .favBtn {
+ flex-shrink: 0;
+ border: 1px solid var(--fav-icon-border);
+ width: 48px;
+ height: 48px;
+ background-color: var(--fav-bg);
+ cursor: pointer;
+
+ &--active {
+ border-color: var(--bg-hover-active);
+ background-color: var(--fav-active);
+ }
+
+ &:hover {
+ border-color: var(--fav-border-hover);
+ background-color: var(--fav-hover);
+ }
+ }
+
+ }
+
+ .info {
+ padding-top: 32px;
+ width: 100%;
+ font-family: Mont, sans-serif;
+ font-size: 12px;
+ font-weight: 700;
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+
+ @include on-desktop {
+ width: 320px;
+ }
+
+ .infoBlock {
+ display: flex;
+ justify-content: space-between;
+
+ .title {
+ color: var(--header-text-color);
+ }
+
+ .value {
+ color: var(--header-text-color-active);
+ }
+ }
+ }
+ }
+}
+
+}
+
+ .productDetails {
+ display: flex;
+ flex-direction: column;
+ gap: 56px;
+ width: 100%;
+
+
+ @include on-tablet {
+ gap: 64px;
+ }
+
+ @include on-desktop {
+ flex-direction: row;
+ }
+
+ .about {
+ width: 100%;
+
+ @include on-desktop {
+ width: 50%;
+ }
+
+ .mainTitle {
+ color: var(--header-text-color-active);
+ font-family: Mont, sans-serif;
+ font-size: 22px;
+ font-weight: 700;
+
+ @include on-tablet {
+ font-weight: 800;
+ }
+
+ }
+
+ .lineAbout {
+ display: inline-block;
+ margin-top: 16px;
+ margin-bottom: 32px;
+ width: 100%;
+ height: 1px;
+ background-color: var(--bg-hover-active);
+ }
+
+
+ .contentAbout {
+ display: flex;
+ flex-direction: column;
+ gap: 32px;
+
+ .article {
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+}
+
+ .title {
+ font-family: Mont, sans-serif;
+ font-size: 16px;
+ font-weight: 700;
+ color: var(--header-text-color-active);
+ }
+
+ .text {
+ font-family: Mont, sans-serif;
+ font-size: 14px;
+ font-weight: 600;
+ color: var(--header-text-color);
+ }
+ }
+ }
+
+ .specs {
+ width: 100%;
+
+ @include on-desktop {
+ width: 50%;
+ }
+
+ .mainTitle {
+ color: var(--header-text-color-active);
+ font-family: Mont, sans-serif;
+ font-size: 22px;
+ font-weight: 700;
+
+ @include on-tablet {
+ font-weight: 800;
+ }
+ }
+
+ .lineSpecs {
+ display: inline-block;
+ margin-top: 16px;
+ margin-bottom: 30px;
+ width: 100%;
+ height: 1px;
+ background-color: var(--bg-hover-active);
+
+ @include on-tablet {
+ margin-bottom: 25px;
+ }
+ }
+
+ .contentSpecs {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+
+
+ .specsBlock {
+ display: flex;
+ justify-content: space-between;
+
+ .title {
+ color: var(--header-text-color);
+ font-family: Mont, sans-serif;
+ font-size: 14px;
+ font-weight: 600;
+
+ }
+
+ .value {
+ color: var(--header-text-color-active);
+ font-family: Mont, sans-serif;
+ font-size: 14px;
+ font-weight: 600;
+ }
+ }
+
+ }
+
+ }
+ }
+
+}
+
+.notFound {
+ padding-bottom: 35px;
+}
diff --git a/src/components/ProductDetailsCard/ProductDetailsCard.tsx b/src/components/ProductDetailsCard/ProductDetailsCard.tsx
new file mode 100644
index 0000000000..ea6413704e
--- /dev/null
+++ b/src/components/ProductDetailsCard/ProductDetailsCard.tsx
@@ -0,0 +1,291 @@
+import { Product } from '../../types/Product';
+import { ColorSelection } from '../ColorSelection';
+import ProductImagesSwiper from '../ProductImagesSwiper/ProductImagesSwiper';
+import styles from './ProductDetailsCard.module.scss';
+import { Loader } from '../Loader';
+import { useEffect, useState } from 'react';
+import { CapacitySelection } from '../CapacitySelection';
+import { ProductDetails } from '../../types/ProductDetails';
+import { useAppDispatch, useAppSelector } from '../../hooks/hooks';
+import { addFavorite, removeFavorite } from '../../features/favoritesSlice';
+import { addItemToCart, removeItemFromCart } from '../../features/cartSlice';
+import { useTranslation } from 'react-i18next';
+import { useLocation, useParams } from 'react-router-dom';
+import toast from 'react-hot-toast';
+import classNames from 'classnames';
+import heartLight from '../../images/icon-heart-light-theme.svg';
+import heartDark from '../../images/icon-heart-dark-theme.svg';
+import favFilledHeart from '../../images/icon-filled-heart-fav-red.svg';
+
+type Props = {
+ products: Product[];
+ selectedProduct?: ProductDetails;
+};
+
+export const ProductDetailsCard: React.FC = ({
+ selectedProduct,
+ products,
+}) => {
+ const [loading, setLoading] = useState(true);
+
+ const dispatch = useAppDispatch();
+ const { favoriteProducts } = useAppSelector(state => state.favorites);
+ const { cartProducts } = useAppSelector(state => state.cart);
+ const { theme } = useAppSelector(state => state.theme);
+ const location = useLocation();
+
+ const { t } = useTranslation();
+ const { productId = '' } = useParams();
+ const id = productId.slice(1);
+
+ const handleCartDetails = (prodId: string) => {
+ const index = products.findIndex(item => item.itemId === prodId);
+
+ if (index === -1) {
+ return;
+ }
+
+ const product = products[index];
+
+ if (cartProducts.some(item => item.itemId === prodId)) {
+ dispatch(removeItemFromCart(product.id));
+
+ toast(t('productCard.toast.removed', { name: product.name }), {
+ icon: '🛒',
+ });
+ } else {
+ dispatch(addItemToCart(product));
+ toast.success(t('productCard.toast.added', { name: product.name }));
+ }
+ };
+
+ const handleFavoritesDetails = (prodId: string) => {
+ const index = products.findIndex(item => item.itemId === prodId);
+
+ if (index === -1) {
+ return;
+ }
+
+ const product = products[index];
+
+ if (favoriteProducts.some(item => item.itemId === prodId)) {
+ dispatch(removeFavorite(product));
+ } else {
+ dispatch(addFavorite(product));
+ }
+ };
+
+ useEffect(() => {
+ window.scrollTo(0, 0);
+ }, [location, selectedProduct]);
+
+ useEffect(() => {
+ if (selectedProduct) {
+ setLoading(false);
+ }
+ }, [selectedProduct]);
+
+ if (loading) {
+ return ;
+ }
+
+ const isFavorite = favoriteProducts.some(item => item.itemId === id);
+ const isInCart = cartProducts.some(prod => prod.itemId === id);
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{`$${selectedProduct?.priceRegular}`}
+
+ {`$${selectedProduct?.priceDiscount}`}
+
+
+
+
+
+
+
+
handleCartDetails(id)}
+ >
+ {isInCart
+ ? t('productCard.button.added')
+ : t('productCard.button.add')}
+
+
+
handleFavoritesDetails(id)}
+ >
+
+
+
+
+
+
+
+ {t('productDetailsPage.screen')}
+
+
{selectedProduct?.screen}
+
+
+
+ {t('productDetailsPage.resolution')}
+
+
+ {selectedProduct?.resolution}
+
+
+
+
+ {t('productDetailsPage.processor')}
+
+
{selectedProduct?.processor}
+
+
+
+ {t('productDetailsPage.ram')}
+
+
{selectedProduct?.ram}
+
+
+
+
+
+
+
+
+
+ {t('productDetailsPage.about')}
+
+
+
+
+ {selectedProduct?.description.map(item => (
+
+ {item.title}
+ {item.text}
+
+ ))}
+
+
+
+
+
+ {t('productDetailsPage.techSpecs')}
+
+
+
+
+
+
+ {t('productDetailsPage.screen')}
+
+
{selectedProduct?.screen}
+
+
+
+
+ {t('productDetailsPage.resolution')}
+
+
{selectedProduct?.resolution}
+
+
+
+
+ {t('productDetailsPage.processor')}
+
+
{selectedProduct?.processor}
+
+
+
+
{t('productDetailsPage.ram')}
+
{selectedProduct?.ram}
+
+
+
+
+ {t('productDetailsPage.builtInMemory')}
+
+
{selectedProduct?.capacity}
+
+
+ {selectedProduct?.camera && (
+
+
+ {t('productDetailsPage.camera')}
+
+
{selectedProduct?.camera}
+
+ )}
+
+ {selectedProduct?.zoom && (
+
+
+ {t('productDetailsPage.zoom')}
+
+
{selectedProduct?.zoom}
+
+ )}
+
+
+
{t('productDetailsPage.cell')}
+
+ {selectedProduct?.cell.join(', ')}
+
+
+
+
+
+
+ );
+};
diff --git a/src/components/ProductDetailsCard/index.ts b/src/components/ProductDetailsCard/index.ts
new file mode 100644
index 0000000000..c0493cf5a5
--- /dev/null
+++ b/src/components/ProductDetailsCard/index.ts
@@ -0,0 +1 @@
+export * from './ProductDetailsCard';
diff --git a/src/components/ProductGallery/ProductGallery.module.scss b/src/components/ProductGallery/ProductGallery.module.scss
new file mode 100644
index 0000000000..912104feb9
--- /dev/null
+++ b/src/components/ProductGallery/ProductGallery.module.scss
@@ -0,0 +1,61 @@
+@import '../../styles/main';
+
+.productGallery {
+ margin-top: 24px;
+ display: flex;
+ flex-direction: row;
+ gap: 16px;
+ justify-items: center;
+
+ @include on-tablet {
+ justify-items: start;
+ }
+
+
+ .list {
+ display: flex;
+ gap: 16px;
+ flex-flow: wrap;
+ flex-direction: row;
+ width: 100%;
+ align-items: center;
+ justify-content: center;
+
+ @include on-tablet {
+ justify-content: start;
+ }
+
+ @include on-desktop {
+ gap: 19px;
+ }
+
+ .item {
+ height: 440px;
+ min-width: 287px;
+ max-width: 287px;
+ border: 1px solid var(--card-border);
+ background-color: var(--card-bg-color);
+ padding: 32px;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ transition: border-color 0.3s ease, box-shadow 0.3s ease;
+
+ &:hover {
+ border-color: var(--card-hover-border);
+ box-shadow: var(--box-shadow-hover);
+ }
+
+ @include on-tablet {
+ height: 506px;
+ }
+
+ @include on-desktop {
+ height: 506px;
+ min-width: 272px;
+ max-width: 272px;
+
+ }
+ }
+ }
+}
diff --git a/src/components/ProductGallery/ProductGallery.tsx b/src/components/ProductGallery/ProductGallery.tsx
new file mode 100644
index 0000000000..d484825abe
--- /dev/null
+++ b/src/components/ProductGallery/ProductGallery.tsx
@@ -0,0 +1,27 @@
+import React from 'react';
+import { Product } from '../../types/Product';
+import styles from './ProductGallery.module.scss';
+import { ProductCard } from '../ProductCard';
+
+type ProductGalleryProps = {
+ products: Product[];
+ discount?: boolean;
+};
+
+export const ProductGallery: React.FC = React.memo(
+ ({ products, discount }) => {
+ return (
+
+
+ {products.map(product => (
+
+
+
+ ))}
+
+
+ );
+ },
+);
+
+ProductGallery.displayName = 'ProductGallery';
diff --git a/src/components/ProductGallery/index.ts b/src/components/ProductGallery/index.ts
new file mode 100644
index 0000000000..b74eb50e7e
--- /dev/null
+++ b/src/components/ProductGallery/index.ts
@@ -0,0 +1 @@
+export * from './ProductGallery';
diff --git a/src/components/ProductImagesSwiper/ProductImagesSwiper.module.scss b/src/components/ProductImagesSwiper/ProductImagesSwiper.module.scss
new file mode 100644
index 0000000000..65d96bb6da
--- /dev/null
+++ b/src/components/ProductImagesSwiper/ProductImagesSwiper.module.scss
@@ -0,0 +1,142 @@
+@import '../../styles/main';
+
+.ItemPhoto_container {
+ display: flex;
+ flex-direction: column-reverse;
+ align-items: center;
+ gap: 16px;
+
+ @include on-tablet {
+ flex-direction: row;
+ align-items: flex-start;
+ }
+
+ .ItemPhoto {
+ &_thumbs {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex-direction: row;
+ gap: 8px;
+ overflow: hidden;
+ height: 100%;
+ width: 100%;
+
+ @include on-tablet {
+ flex-direction: column;
+ min-width: 35px;
+ max-width: 35px;
+ }
+
+ @include on-desktop {
+ min-width: 80px;
+ max-width: 80px;
+ }
+
+ .swiper-slide {
+ box-sizing: border-box;
+ overflow: hidden;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ gap: 8px;
+ padding: 8px;
+ }
+ }
+
+ &_main {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 100%;
+ height: 288px;
+
+ @include on-tablet {
+ height: 287px;
+ }
+
+ @include on-desktop {
+ height: 464px;
+ }
+
+ img {
+ height: 100%;
+ width: 100%;
+ object-fit: contain;
+ transition: transform 0.3s ease;
+ cursor: pointer;
+ }
+
+ img:hover {
+ transform: scale(1.05);
+ }
+
+ .swiper-wrapper {
+ height: 100%;
+ }
+
+ .swiper-slide {
+ box-sizing: border-box;
+ display: flex;
+ justify-content: center;
+ height: 100%;
+ width: 288px;
+ overflow: hidden;
+
+ @include on-tablet {
+ width: 287px;
+ }
+
+ @include on-desktop {
+ width: 464px;
+ }
+ }
+ }
+ }
+}
+
+.thumbnailImage {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ height: 49px;
+ width: 49px;
+ padding: 8px;
+ box-sizing: border-box;
+ border: 1px solid var(--item-border);
+ transition: border-color 0.3s ease;
+ cursor: pointer;
+
+ &.activeThumbnailBorder {
+ border-color: var(--header-text-color-active);
+ }
+
+ img {
+ object-fit: contain;
+ width: 100%;
+ height: 100%;
+ transition: transform 0.3s ease;
+
+ &:hover {
+ transform: scale(1.2);
+ }
+ }
+
+ @include on-tablet {
+ height: 35px;
+ width: 35px;
+ }
+
+ @include on-desktop {
+ height: 80px;
+ width: 80px;
+ }
+}
+
+.ItemPhoto_main {
+ transition: transform 1s ease-in-out;
+
+ &:hover {
+ transform: scale(1.1);
+ }
+}
diff --git a/src/components/ProductImagesSwiper/ProductImagesSwiper.tsx b/src/components/ProductImagesSwiper/ProductImagesSwiper.tsx
new file mode 100644
index 0000000000..04127cf964
--- /dev/null
+++ b/src/components/ProductImagesSwiper/ProductImagesSwiper.tsx
@@ -0,0 +1,91 @@
+import React, { useState, useEffect } from 'react';
+import styles from './ProductImagesSwiper.module.scss';
+import { ProductDetails } from '../../types/ProductDetails';
+import { Loader } from '../Loader';
+import { ProductNotFoundPage } from '../../modules/ProductNotFoundPage';
+
+type Props = {
+ selectedProduct?: ProductDetails | null;
+};
+
+const ProductImagesSwiper: React.FC = ({ selectedProduct }) => {
+ const [activeImage, setActiveImage] = useState(null);
+ const [isLoading, setIsLoading] = useState(true);
+ const [rotation, setRotation] = useState({ x: 0, y: 0, z: 0 });
+ const [isAnimating, setIsAnimating] = useState(false);
+
+ useEffect(() => {
+ if (selectedProduct?.images?.length) {
+ setActiveImage(selectedProduct.images[0]);
+ setIsLoading(false);
+ } else {
+ setIsLoading(true);
+ }
+ }, [selectedProduct]);
+
+ const handleImageClick = (imageLink: string) => {
+ setActiveImage(imageLink);
+
+ // Start the rotation animation
+ setRotation({ x: 90, y: 360, z: 360 });
+ setIsAnimating(true);
+
+ // Reset the rotation to the original position after the animation
+ setTimeout(() => {
+ setRotation({ x: 0, y: 0, z: 0 });
+ setIsAnimating(false);
+ }, 1600); // Matches the transition duration
+ };
+
+ if (isLoading) {
+ return ;
+ }
+
+ if (
+ !selectedProduct ||
+ !selectedProduct.images ||
+ selectedProduct.images.length === 0
+ ) {
+ return ;
+ }
+
+ const swiperSlideSet = selectedProduct.images.map((imageLink: string) => (
+ handleImageClick(imageLink)}
+ className={`${styles.thumbnailImage} ${
+ activeImage === imageLink ? styles.activeThumbnailBorder : ''
+ }`}
+ >
+
+
+ ));
+
+ return (
+
+
{swiperSlideSet}
+
+
+ {activeImage && (
+
+ )}
+
+
+ );
+};
+
+export default ProductImagesSwiper;
diff --git a/src/components/Search/Search.module.scss b/src/components/Search/Search.module.scss
new file mode 100644
index 0000000000..0309332174
--- /dev/null
+++ b/src/components/Search/Search.module.scss
@@ -0,0 +1,54 @@
+@import '../../styles/main';
+
+// fix naming styles and dashes
+
+.search {
+ height: 100%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+
+ &__content {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ height: 100%;
+ width: 100%;
+ }
+
+ &__inputWrapper {
+ display: flex;
+ align-items: center;
+ width: 100%;
+ height: 100%;
+ border-left: 1px solid var(--element-color);
+ }
+
+ &__icon {
+ padding-left: 8px;
+ margin-right: 3px;
+ }
+
+ &__input {
+ width: 100%;
+ height: 100%;
+ border-style: none;
+ background-color: var(--c-background);
+ padding-left: 5px;
+ outline: none;
+ font-family: Mont, sans-serif;
+ color: var(--primary);
+
+ &::placeholder {
+ color: var(--header-text-color);
+ font-family: Mont, sans-serif;
+ }
+ }
+
+ &__close {
+ display: flex;
+ align-items: center;
+ padding: 5px;
+ cursor: pointer;
+ }
+}
diff --git a/src/components/Search/Search.tsx b/src/components/Search/Search.tsx
new file mode 100644
index 0000000000..d49982d99a
--- /dev/null
+++ b/src/components/Search/Search.tsx
@@ -0,0 +1,126 @@
+import searchLogoDarkTheme from '../../images/icon-search-dark-theme.svg';
+import searchLogoLightTheme from '../../images/icon-search-light-theme.svg';
+import { useAppSelector } from '../../hooks/hooks';
+import { useCallback, useEffect, useRef, useState } from 'react';
+// eslint-disable-next-line import/no-extraneous-dependencies
+import debounce from 'lodash/debounce';
+import { useLocation, useSearchParams } from 'react-router-dom';
+import classNames from 'classnames';
+import closeLight from '../../images/icon-close-light-theme.svg';
+import closeDark from '../../images/icon-close-dark-theme.svg';
+import styles from './Search.module.scss';
+import { useTranslation } from 'react-i18next';
+
+export const Search = () => {
+ const { theme } = useAppSelector(state => state.theme);
+ const location = useLocation();
+ const firstRender = useRef(true);
+ const { t } = useTranslation();
+ const [searchParams, setSearchParams] = useSearchParams();
+ const [query, setQuery] = useState(searchParams.get('search') || '');
+
+ const categorySearch = location.pathname.slice(1);
+
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ const debouncedSearch = useCallback(
+ debounce((params: URLSearchParams) => {
+ setSearchParams(params);
+ }, 2000),
+ [searchParams],
+ );
+
+ const handleQueryChange = (event: React.ChangeEvent) => {
+ const searchValue = event.target.value.toLowerCase();
+ const params = new URLSearchParams(searchParams);
+
+ params.set('search', searchValue);
+ params.set('page', '1');
+ params.set('section', '0');
+ params.set('offset', '0');
+
+ debouncedSearch(params);
+ setQuery(searchValue);
+ };
+
+ const handleClear = () => {
+ const params = new URLSearchParams(searchParams);
+
+ params.set('search', '');
+ params.set('page', '1');
+ params.set('section', '0');
+ params.set('offset', '0');
+
+ setSearchParams(params);
+ setQuery('');
+ };
+
+ const categoryTrans = (category: string) => {
+ if (category === 'accessories') {
+ return t('search.placeholder', {
+ category: t('search.categories.accessories'),
+ });
+ }
+
+ if (category === 'phones') {
+ return t('search.placeholder', {
+ category: t('search.categories.phones'),
+ });
+ }
+
+ if (category === 'tablets') {
+ return t('search.placeholder', {
+ category: t('search.categories.tablets'),
+ });
+ } else {
+ return t('search.placeholder', { category });
+ }
+ };
+
+ useEffect(() => {
+ if (firstRender.current) {
+ firstRender.current = false;
+
+ return;
+ }
+
+ setQuery('');
+ }, [location.pathname]);
+
+ return (
+
+
0,
+ })}
+ >
+
+
+
+
+
+ {query && (
+
+
+
+ )}
+
+
+ );
+};
diff --git a/src/components/Search/index.ts b/src/components/Search/index.ts
new file mode 100644
index 0000000000..addd53308b
--- /dev/null
+++ b/src/components/Search/index.ts
@@ -0,0 +1 @@
+export * from './Search';
diff --git a/src/components/SortByDropdown/SortByDropdown.module.scss b/src/components/SortByDropdown/SortByDropdown.module.scss
new file mode 100644
index 0000000000..a5ef1ab295
--- /dev/null
+++ b/src/components/SortByDropdown/SortByDropdown.module.scss
@@ -0,0 +1,98 @@
+@import '../../styles/main';
+
+.sortByDropdown {
+ height: 59px;
+ width: 136px;
+
+ @include on-tablet {
+ width: 176px;
+ }
+
+ &__label {
+ margin-bottom: 8px;
+ font-family: Mont, sans-serif;
+ font-weight: 700;
+ font-size: 12px;
+ color: var(--header-text-color);
+ }
+
+ &__toggle {
+ width: 100%;
+ height: 40px;
+ margin-top: 4px;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ font-family: Mont, sans-serif;
+ font-size: 14px;
+ font-weight: 700;
+ line-height: 21px;
+ padding-inline: 12px;
+ border: 1px solid var(--slider-btn-border);
+ color: var(--header-text-color-active);
+ background-color: var(--dropdown-bg);
+ cursor: pointer;
+ z-index: 200;
+
+ &:focus {
+ border-color: var(--border-focus);
+ }
+
+ &:hover {
+ border-color: var(--border-focus);
+ }
+
+ &__toggleIcon {
+ width: 16px;
+ height: 16px;
+ transition: transform 0.3s ease;
+
+ &--active {
+ transform: rotate(90deg);
+ }
+ }
+ }
+
+ &__list {
+ display: none;
+ background-color: var(--c-background);
+
+ &--active {
+ width: 136px;
+ position: absolute;
+ margin-top: 5px;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ gap: 8px;
+ border: 1px solid var(--bg-hover-active);
+ z-index: 200;
+
+ @include on-tablet {
+ width: 176px;
+ }
+ }
+ }
+
+ &__item {
+ height: 40px;
+ padding-left: 12px;
+ position: relative;
+ display: flex;
+ align-items: center;
+ font-family: Mont, sans-serif;
+ font-weight: 600;
+ font-size: 14px;
+ line-height: 21px;
+ color: var(--back-top);
+ cursor: pointer;
+ transition: background-color 0.3s ease;
+
+
+ &:hover,
+ &:focus {
+ background-color: var(--bg-focus-list);
+ color: var(--header-text-color-active);
+ }
+ }
+}
diff --git a/src/components/SortByDropdown/SortByDropdown.tsx b/src/components/SortByDropdown/SortByDropdown.tsx
new file mode 100644
index 0000000000..adf719e411
--- /dev/null
+++ b/src/components/SortByDropdown/SortByDropdown.tsx
@@ -0,0 +1,129 @@
+import arrowRightLight from '../../images/icon-right-light-theme.svg';
+import arrowRightDark from '../../images/icon-right-dark-theme.svg';
+import { useAppSelector } from '../../hooks/hooks';
+import { useTranslation } from 'react-i18next';
+import { useEffect, useRef, useState } from 'react';
+import { useSearchParams } from 'react-router-dom';
+import i18next from 'i18next';
+import classNames from 'classnames';
+import styles from './SortByDropdown.module.scss';
+
+export const SortByDropdown = () => {
+ const { theme } = useAppSelector(state => state.theme);
+ const { t } = useTranslation();
+ const [isDropdownActive, setIsDropdownActive] = useState(false);
+ const [searchParams, setSearchParams] = useSearchParams();
+ const dropdownContainerRef = useRef(null);
+
+ const sortOptions = [
+ t('sortByDropdown.option.newest'),
+ t('sortByDropdown.option.alphabetically'),
+ t('sortByDropdown.option.cheapest'),
+ ];
+
+ const handleOutsideClick = (event: MouseEvent) => {
+ if (
+ dropdownContainerRef.current &&
+ !dropdownContainerRef.current.contains(event.target as Node)
+ ) {
+ setIsDropdownActive(false);
+ }
+ };
+
+ const handleOptionSelect = (option: string) => {
+ const params = new URLSearchParams(searchParams);
+
+ params.set('sort', option);
+ setSearchParams(params);
+ setIsDropdownActive(false);
+ };
+
+ const toggleDropdown = () => {
+ setIsDropdownActive(prevState => !prevState);
+ };
+
+ const translateSortOption = (sort: string | null) => {
+ const params = new URLSearchParams(searchParams);
+
+ if (sort === 'Найновіші' && i18next.language === 'en') {
+ params.set('sort', 'Newest');
+ setSearchParams(params);
+ } else if (sort === 'Newest' && i18next.language === 'uk') {
+ params.set('sort', 'Найновіші');
+ setSearchParams(params);
+ }
+
+ if (sort === 'Алфавітом' && i18next.language === 'en') {
+ params.set('sort', 'Alphabetically');
+ setSearchParams(params);
+ } else if (sort === 'Alphabetically' && i18next.language === 'uk') {
+ params.set('sort', 'Алфавітом');
+ setSearchParams(params);
+ }
+
+ if (sort === 'Найдешевші' && i18next.language === 'en') {
+ params.set('sort', 'Cheapest');
+ setSearchParams(params);
+ } else if (sort === 'Cheapest' && i18next.language === 'uk') {
+ params.set('sort', 'Найдешевші');
+ setSearchParams(params);
+ }
+
+ return sort;
+ };
+
+ useEffect(() => {
+ if (isDropdownActive) {
+ document.addEventListener('mousedown', handleOutsideClick);
+ } else {
+ document.removeEventListener('mousedown', handleOutsideClick);
+ }
+
+ return () => {
+ document.removeEventListener('mousedown', handleOutsideClick);
+ };
+ }, [isDropdownActive]);
+
+ return (
+
+
+ {t('sortByDropdown.title')}
+
+
+
+
+ {searchParams.has('sort')
+ ? translateSortOption(searchParams.get('sort'))
+ : t('sortByDropdown.placeholder')}
+
+
+
+
+
+ {sortOptions.map(option => (
+ handleOptionSelect(option)}
+ >
+ {option}
+
+ ))}
+
+
+ );
+};
diff --git a/src/components/SortByDropdown/index.ts b/src/components/SortByDropdown/index.ts
new file mode 100644
index 0000000000..77f9fa435d
--- /dev/null
+++ b/src/components/SortByDropdown/index.ts
@@ -0,0 +1 @@
+export * from './SortByDropdown';
diff --git a/src/constants/productColors.ts b/src/constants/productColors.ts
new file mode 100644
index 0000000000..d76d7ee3cd
--- /dev/null
+++ b/src/constants/productColors.ts
@@ -0,0 +1,57 @@
+export type ProductColors = {
+ black: string;
+ blue: string;
+ coral: string;
+ gold: string;
+ graphite: string;
+ green: string;
+ midnight: string;
+ midnightgreen: string;
+ pink: string;
+ purple: string;
+ red: string;
+ rosegold: string;
+ sierrablue: string;
+ silver: string;
+ skyblue: string;
+ spaceblack: string;
+ spacegray: string;
+ starlight: string;
+ white: string;
+ yellow: string;
+
+ 'space gray'?: string;
+ 'rose gold'?: string;
+ 'sky blue'?: string;
+ 'midnight green'?: string;
+ 'space black'?: string;
+};
+
+export const productColors: ProductColors = {
+ black: '#3C4042',
+ blue: '#CED5D9',
+ coral: '#FF6E5A',
+ gold: '#F4E8CE',
+ graphite: '#54524F',
+ green: '#576856',
+ midnight: '#232A31',
+ midnightgreen: '#394C38',
+ pink: '#FADDD7',
+ purple: '#594F63',
+ red: '#FC0324',
+ rosegold: '#F7E8DD',
+ silver: '#F1F2ED',
+ skyblue: '#276787',
+ spaceblack: '#403E3D',
+ 'space gray': '#6E6E73',
+ starlight: '#FAF6F2',
+ white: '#F6F2EF',
+ yellow: '#FFE681',
+
+ 'rose gold': '#F7E8DD',
+ 'sky blue': '#276787',
+ 'midnight green': '#394C38',
+ spacegray: '#535150',
+ sierrablue: '#A7C1D9',
+ 'space black': '#403E3D',
+};
diff --git a/src/features/cartSlice.ts b/src/features/cartSlice.ts
new file mode 100644
index 0000000000..dfc1d7fb6a
--- /dev/null
+++ b/src/features/cartSlice.ts
@@ -0,0 +1,88 @@
+import { PayloadAction, createSlice } from '@reduxjs/toolkit';
+import { UpdatedProduct } from '../types/UpdatedProduct';
+import { Product } from '../types/Product';
+
+type Cart = {
+ cartProducts: UpdatedProduct[];
+};
+
+const initialState: Cart = {
+ cartProducts: JSON.parse(localStorage.getItem('cart') || '[]'),
+};
+
+const syncCartWithLocalStorage = (items: UpdatedProduct[]) => {
+ localStorage.setItem('cart', JSON.stringify(items));
+};
+
+const cartSlice = createSlice({
+ name: 'cart',
+ initialState,
+ reducers: {
+ addItemToCart: (state, action: PayloadAction) => {
+ const { id } = action.payload;
+
+ const existingItem = state.cartProducts.find(item => item.id === id);
+
+ if (existingItem) {
+ existingItem.quantity = (existingItem.quantity || 1) + 1;
+ } else {
+ state.cartProducts.push({ ...action.payload, quantity: 1 });
+ }
+
+ syncCartWithLocalStorage(state.cartProducts);
+ },
+
+ removeItemFromCart: (state, action: PayloadAction) => {
+ // eslint-disable-next-line no-param-reassign
+ state.cartProducts = state.cartProducts.filter(
+ item => item.id !== action.payload,
+ );
+ syncCartWithLocalStorage(state.cartProducts);
+ },
+
+ clearCart: state => {
+ // eslint-disable-next-line no-param-reassign
+ state.cartProducts = [];
+ syncCartWithLocalStorage(state.cartProducts);
+ },
+
+ incrementItemQuantity: (state, action: PayloadAction) => {
+ const targetItem = state.cartProducts.find(
+ item => item.id === action.payload,
+ );
+
+ if (targetItem) {
+ targetItem.quantity += 1;
+ syncCartWithLocalStorage(state.cartProducts);
+ }
+ },
+
+ decrementItemQuantity: (state, action: PayloadAction) => {
+ const targetItem = state.cartProducts.find(
+ item => item.id === action.payload,
+ );
+
+ if (targetItem) {
+ if (targetItem.quantity > 1) {
+ targetItem.quantity -= 1;
+ } else {
+ // eslint-disable-next-line no-param-reassign
+ state.cartProducts = state.cartProducts.filter(
+ item => item.id !== action.payload,
+ );
+ }
+
+ syncCartWithLocalStorage(state.cartProducts);
+ }
+ },
+ },
+});
+
+export default cartSlice.reducer;
+export const {
+ addItemToCart,
+ removeItemFromCart,
+ clearCart,
+ incrementItemQuantity,
+ decrementItemQuantity,
+} = cartSlice.actions;
diff --git a/src/features/favoritesSlice.ts b/src/features/favoritesSlice.ts
new file mode 100644
index 0000000000..40509762dd
--- /dev/null
+++ b/src/features/favoritesSlice.ts
@@ -0,0 +1,37 @@
+import { PayloadAction, createSlice } from '@reduxjs/toolkit';
+import { Product } from '../types/Product';
+
+type Favorites = {
+ favoriteProducts: Product[];
+};
+
+const initialState: Favorites = {
+ favoriteProducts: JSON.parse(
+ localStorage.getItem('favorites') || '[]',
+ ).filter(product => product?.id != null && product.id !== ''),
+};
+
+const syncFavoritesWithLocalStorage = (favorites: Product[]) => {
+ localStorage.setItem('favorites', JSON.stringify(favorites));
+};
+
+const favoritesSlice = createSlice({
+ name: 'favorites',
+ initialState,
+ reducers: {
+ addFavorite: (state, action: PayloadAction) => {
+ state.favoriteProducts.push(action.payload);
+ syncFavoritesWithLocalStorage(state.favoriteProducts);
+ },
+ removeFavorite: (state, action: PayloadAction) => {
+ // eslint-disable-next-line no-param-reassign
+ state.favoriteProducts = state.favoriteProducts.filter(
+ item => item.id !== action.payload.id,
+ );
+ syncFavoritesWithLocalStorage(state.favoriteProducts);
+ },
+ },
+});
+
+export default favoritesSlice.reducer;
+export const { addFavorite, removeFavorite } = favoritesSlice.actions;
diff --git a/src/features/productsSlice.ts b/src/features/productsSlice.ts
new file mode 100644
index 0000000000..6fe577412d
--- /dev/null
+++ b/src/features/productsSlice.ts
@@ -0,0 +1,24 @@
+import { PayloadAction, createSlice } from '@reduxjs/toolkit';
+import { Product } from '../types/Product';
+
+type ProductsState = {
+ products: Product[];
+};
+
+const initialState: ProductsState = {
+ products: [],
+};
+
+const productsSlice = createSlice({
+ name: 'products',
+ initialState,
+ reducers: {
+ setProducts: (state, action: PayloadAction) => {
+ // eslint-disable-next-line no-param-reassign
+ state.products = action.payload;
+ },
+ },
+});
+
+export default productsSlice.reducer;
+export const { setProducts } = productsSlice.actions;
diff --git a/src/features/themeSlice.ts b/src/features/themeSlice.ts
new file mode 100644
index 0000000000..8025717523
--- /dev/null
+++ b/src/features/themeSlice.ts
@@ -0,0 +1,33 @@
+// eslint-disable-next-line import/no-extraneous-dependencies
+import { createSlice } from '@reduxjs/toolkit';
+
+type ThemeState = {
+ theme: string;
+};
+
+const initialState: ThemeState = {
+ theme: localStorage.getItem('theme') || 'light',
+};
+
+const themeSlice = createSlice({
+ name: 'theme',
+ initialState,
+ reducers: {
+ toggleTheme: state => {
+ if (state.theme === 'light') {
+ // eslint-disable-next-line no-param-reassign
+ state.theme = 'dark';
+ } else if (state.theme === 'dark') {
+ // eslint-disable-next-line no-param-reassign
+ state.theme = 'light';
+ }
+
+ localStorage.setItem('theme', state.theme);
+ document.body.classList.remove('light', 'dark');
+ document.body.classList.add(state.theme);
+ },
+ },
+});
+
+export default themeSlice.reducer;
+export const { toggleTheme } = themeSlice.actions;
diff --git a/src/helpers/httpClient.ts b/src/helpers/httpClient.ts
new file mode 100644
index 0000000000..e715c29274
--- /dev/null
+++ b/src/helpers/httpClient.ts
@@ -0,0 +1,13 @@
+const BASE_URL = 'https://anna-agerone.github.io/react_phone-catalog/api';
+
+// const BASE_URL = './api/';
+
+export function getData(url: string): Promise {
+ return fetch(BASE_URL + url).then(response => {
+ if (!response.ok) {
+ throw new Error(`${response.status} ${response.text}`);
+ }
+
+ return response.json();
+ });
+}
diff --git a/src/hooks/hooks.ts b/src/hooks/hooks.ts
new file mode 100644
index 0000000000..e0f87e58ac
--- /dev/null
+++ b/src/hooks/hooks.ts
@@ -0,0 +1,9 @@
+// eslint-disable-next-line import/no-extraneous-dependencies
+import { TypedUseSelectorHook, useSelector } from 'react-redux';
+// eslint-disable-next-line import/no-extraneous-dependencies
+import { useDispatch } from 'react-redux';
+import { RootState } from '../app/store';
+import { AppDispatch } from '../app/store';
+
+export const useAppSelector: TypedUseSelectorHook = useSelector;
+export const useAppDispatch: () => AppDispatch = useDispatch;
diff --git a/src/i18n/index.js b/src/i18n/index.js
new file mode 100644
index 0000000000..0219dc43ea
--- /dev/null
+++ b/src/i18n/index.js
@@ -0,0 +1,23 @@
+import i18n from 'i18next';
+import { initReactI18next } from 'react-i18next';
+import LanguageDetector from 'i18next-browser-languagedetector';
+
+import en from './translations/en';
+import uk from './translations/uk';
+import { LanguageType } from '../types/Language';
+
+i18n
+ .use(LanguageDetector)
+ .use(initReactI18next)
+ .init({
+ resources: {
+ [LanguageType.EN]: { translation: en },
+ [LanguageType.UK]: { translation: uk },
+ },
+ fallbackLng: LanguageType.EN,
+ interpolation: {
+ escapeValue: false,
+ },
+ });
+
+export default i18n;
diff --git a/src/i18n/translations/en.js b/src/i18n/translations/en.js
new file mode 100644
index 0000000000..77f0884fd2
--- /dev/null
+++ b/src/i18n/translations/en.js
@@ -0,0 +1,182 @@
+const en = {
+ welcomeMessage: 'Welcome to React and react-i18next',
+ header: {
+ navigation: {
+ home: 'HOME',
+ phones: 'PHONES',
+ tablets: 'TABLETS',
+ accessories: 'ACCESSORIES',
+ },
+ languageSwitcher: 'УКР',
+ themeSwitcher: {
+ light: 'dark',
+ dark: 'light',
+ },
+ },
+
+ search: {
+ placeholder: 'Search in {{category}}',
+ categories: {
+ phones: 'phones',
+ tablets: 'tablets',
+ accessories: 'accessories',
+ },
+ },
+
+ footer: {
+ contacts: 'CONTACTS',
+ rights: 'RIGHTS',
+ backToTop: 'Back to top',
+ notificationAlert:
+ // eslint-disable-next-line max-len
+ 'You are about to leave this page and visit the GitHub profile of the project creator. Do you wish to continue?',
+ rightsAlert: 'This is a mock implementation. Full feature coming soon!',
+ },
+
+ homePage: {
+ title: 'Welcome to Nice Gadgets store!',
+ brandNewModels: 'Brand new models',
+ hotPrices: 'Hot prices',
+ categories: {
+ mainTitle: 'Shop by category',
+ phonesTitle: 'Mobile phones',
+ tabletsTitle: 'Tablets',
+ accessoriesTitle: 'Accessories',
+ count_one: '{{count}} model',
+ count_few: '{{count}} models',
+ count_many: '{{count}} models',
+ count_other: '{{count}} models',
+ shopLatest: 'Shop the latest {{category}}',
+ performanceAndStyle: 'Performance and style in your hands!',
+ },
+ },
+
+ breadCrumbs: {
+ phones: 'Phones',
+ tablets: 'Tablets',
+ accessories: 'Accessories',
+ favorites: 'Favorites',
+ },
+
+ sortByDropdown: {
+ title: 'Sort by',
+ option: {
+ newest: 'Newest',
+ alphabetically: 'Alphabetically',
+ cheapest: 'Cheapest',
+ },
+ placeholder: 'Select an option',
+ },
+
+ itemsPerPageDropdown: {
+ title: 'Items on page',
+ all: 'All',
+ },
+
+ phonesPage: {
+ title: 'Mobile phones',
+ count_one: '{{count}} model',
+ count_other: '{{count}} models',
+ },
+
+ tabletsPage: {
+ title: 'Tablets',
+ count_one: '{{count}} model',
+ count_other: '{{count}} models',
+ },
+
+ accessoriesPage: {
+ title: 'Accessories',
+ count_one: '{{count}} model',
+ count_other: '{{count}} models',
+ },
+
+ favoritesPage: {
+ title: 'Favorites',
+ count_one: '{{count}} model',
+ count_other: '{{count}} models',
+ empty:
+ 'You don’t have any favorites yet. \nExplore and add your top picks!',
+ },
+
+ productDetailsPage: {
+ suggestionsTitle: 'You may also like',
+ screen: 'Screen',
+ processor: 'Processor',
+ resolution: 'Resolution',
+ capacity: 'Capacity',
+ ram: 'RAM',
+ about: 'About',
+ techSpecs: 'Tech specs',
+ builtInMemory: 'Built in memory',
+ camera: 'Camera',
+ zoom: 'Zoom',
+ cell: 'Cell',
+ },
+
+ colorSelection: {
+ title: 'Available colors',
+ },
+
+ capacitySelection: {
+ title: 'Select capacity',
+ },
+
+ cartPage: {
+ title: 'Cart',
+ totalFor: 'Total for {{count}} {{items}}',
+ items: {
+ one: 'item',
+ few: 'items',
+ many: 'items',
+ other: 'items',
+ },
+ emptyCart: 'Your cart is empty',
+ checkout: 'Checkout',
+ movingText: 'Thank you for choosing us!',
+ },
+
+ modal: {
+ title: 'Checkout is not implemented yet.',
+ message: 'Do you want to clear the cart?',
+ confirmBtn: 'Confirm',
+ cancelBtn: 'Cancel',
+ },
+
+ notFoundPage: {
+ message: 'Oops!',
+ title: 'Page Not Found',
+ backHome: 'Go back to Home',
+ },
+
+ productNotFoundPage: {
+ phones: 'No phones found. Please try again!',
+ tablets: 'No tablets found. Please try again!',
+ accessories: 'No accessories found. Please try again!',
+ titleOutOfStock: 'Back Soon: Product Out of Stock',
+ },
+
+ buttonBack: {
+ back: 'Back',
+ },
+
+ productCard: {
+ specs: {
+ screen: 'Screen',
+ capacity: 'Capacity',
+ ram: 'RAM',
+ },
+
+ button: {
+ add: 'Add to cart',
+ added: 'Added to cart',
+ },
+
+ toast: {
+ added: '{{name}} has been added to the cart!',
+ removed: '{{name}} has been removed from the cart!',
+ },
+ },
+};
+
+export default en;
diff --git a/src/i18n/translations/uk.js b/src/i18n/translations/uk.js
new file mode 100644
index 0000000000..69496a274b
--- /dev/null
+++ b/src/i18n/translations/uk.js
@@ -0,0 +1,193 @@
+const uk = {
+ welcomeMessage: 'Ласкаво просимо до React та react-i18next',
+ header: {
+ navigation: {
+ home: 'ГОЛОВНА',
+ phones: 'ТЕЛЕФОНИ',
+ tablets: 'ПЛАНШЕТИ',
+ accessories: 'АКСЕСУАРИ',
+ },
+ languageSwitcher: 'EN',
+ themeSwitcher: {
+ light: 'темна',
+ dark: 'світла',
+ },
+ },
+
+ search: {
+ placeholder: 'Пошук серед {{category}}',
+ categories: {
+ phones: 'телефонів',
+ tablets: 'планшетів',
+ accessories: 'аксесуарів',
+ },
+ },
+
+ footer: {
+ contacts: 'КОНТАКТИ',
+ rights: 'ПРАВА',
+ backToTop: 'На початок',
+ notificationAlert:
+ // eslint-disable-next-line max-len
+ 'Ви збираєтесь покинути цю сторінку і перейти на GitHub профіль розробника цього проєкту. Бажаєте продовжити?',
+ rightsAlert:
+ // eslint-disable-next-line max-len
+ 'Це макет реалізації. Повна функціональність буде доступна найближчим часом!',
+ },
+
+ homePage: {
+ title: 'Ласкаво просимо до магазину "Nice Gadgets"!',
+ brandNewModels: 'Нові моделі',
+ hotPrices: 'Гарячі ціни',
+ categories: {
+ mainTitle: 'Купуйте за категоріями',
+ phonesTitle: 'Мобільні телефони',
+ tabletsTitle: 'Планшети',
+ accessoriesTitle: 'Аксесуари',
+ count_one: '{{count}} модель',
+ count_few: '{{count}} моделі',
+ count_many: '{{count}} моделей',
+ count_other: '{{count}} моделей',
+ shopLatest: 'Купуйте новітні {{category}}',
+ performanceAndStyle: 'Потужність і стиль у ваших руках!',
+ },
+ },
+
+ breadCrumbs: {
+ phones: 'Мобільні телефони',
+ tablets: 'Планшети',
+ accessories: 'Аксесуари',
+ favorites: 'Обрані',
+ },
+
+ sortByDropdown: {
+ title: 'Сортувати за',
+ option: {
+ newest: 'Найновіші',
+ alphabetically: 'Алфавітом',
+ cheapest: 'Найдешевші',
+ },
+ placeholder: 'Оберіть опцію',
+ },
+
+ itemsPerPageDropdown: {
+ title: 'Кількість на сторінці',
+ all: 'Всі',
+ },
+
+ phonesPage: {
+ title: 'Мобільні телефони',
+ count_one: '{{count}} модель',
+ count_few: '{{count}} моделі',
+ count_many: '{{count}} моделей',
+ count_other: '{{count}} моделей',
+ },
+
+ tabletsPage: {
+ title: 'Планшети',
+ count_one: '{{count}} модель',
+ count_few: '{{count}} моделі',
+ count_many: '{{count}} моделей',
+ count_other: '{{count}} моделей',
+ },
+
+ accessoriesPage: {
+ title: 'Аксесуари',
+ count_one: '{{count}} модель',
+ count_few: '{{count}} моделі',
+ count_many: '{{count}} моделей',
+ count_other: '{{count}} моделей',
+ },
+
+ favoritesPage: {
+ title: 'Обрані',
+ count_one: '{{count}} модель',
+ count_few: '{{count}} моделі',
+ count_many: '{{count}} моделей',
+ count_other: '{{count}} моделей',
+ empty:
+ // eslint-disable-next-line max-len
+ 'У вас ще немає обраного. \nДосліджуйте та додавайте свої улюблені варіанти!',
+ },
+
+ productDetailsPage: {
+ suggestionsTitle: 'Вам також може сподобатися',
+ screen: 'Екран',
+ processor: 'Процесор',
+ resolution: 'Роздільна здатність',
+ capacity: 'Ємність',
+ ram: 'ОЗП',
+ about: 'Про продукт',
+ techSpecs: 'Технічні характеристики',
+ builtInMemory: "Вбудована пам'ять",
+ camera: 'Камера',
+ zoom: 'Зум',
+ cell: 'Мережа',
+ },
+
+ colorSelection: {
+ title: 'Доступні кольори',
+ },
+
+ capacitySelection: {
+ title: 'Виберіть ємність',
+ },
+
+ cartPage: {
+ title: 'Кошик',
+ totalFor: 'Загальна вартість для {{count}} {{items}}',
+ items: {
+ one: 'товарy',
+ few: 'товарів',
+ many: 'товарів',
+ other: 'товарів',
+ },
+ emptyCart: 'Ваш кошик порожній',
+ checkout: 'Оплата',
+ movingText: 'Дякуємо, що обрали нас!',
+ },
+
+ modal: {
+ title: 'Оформлення замовлення ще не реалізовано.',
+ message: 'Ви хочете очистити кошик?',
+ confirmBtn: 'Підтвердити',
+ cancelBtn: 'Скасувати',
+ },
+
+ notFoundPage: {
+ message: 'Упс!',
+ title: 'Сторінку не знайдено',
+ backHome: 'Повернутися на головну сторінку',
+ },
+
+ productNotFoundPage: {
+ phones: 'Телефони не знайдено.\nБудь ласка, спробуйте ще раз!',
+ tablets: 'Планшети не знайдено.\nБудь ласка, спробуйте ще раз!',
+ accessories: 'Аксесуари не знайдено.\nБудь ласка, спробуйте ще раз!',
+ titleOutOfStock: 'Незабаром у продажу: товар відсутній на складі',
+ },
+
+ buttonBack: {
+ back: 'Назад',
+ },
+
+ productCard: {
+ specs: {
+ screen: 'Екран',
+ capacity: 'Ємність',
+ ram: 'ОЗП',
+ },
+
+ button: {
+ add: 'Додати до кошика',
+ added: 'Додано до кошика',
+ },
+
+ toast: {
+ added: '{{name}} було додано до кошика!',
+ removed: '{{name}} було видалено з кошика!',
+ },
+ },
+};
+
+export default uk;
diff --git a/src/images/accessories-category.png b/src/images/accessories-category.png
new file mode 100644
index 0000000000..0158d84c66
Binary files /dev/null and b/src/images/accessories-category.png differ
diff --git a/public/img/banner-accessories.png b/src/images/banner-accessories.png
similarity index 100%
rename from public/img/banner-accessories.png
rename to src/images/banner-accessories.png
diff --git a/public/img/banner-phones.png b/src/images/banner-phones.png
similarity index 100%
rename from public/img/banner-phones.png
rename to src/images/banner-phones.png
diff --git a/public/img/banner-tablets.png b/src/images/banner-tablets.png
similarity index 100%
rename from public/img/banner-tablets.png
rename to src/images/banner-tablets.png
diff --git a/src/images/burger-menu-dark-theme.svg b/src/images/burger-menu-dark-theme.svg
new file mode 100644
index 0000000000..c8c52c08a9
--- /dev/null
+++ b/src/images/burger-menu-dark-theme.svg
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/src/images/burger-menu-light-theme.svg b/src/images/burger-menu-light-theme.svg
new file mode 100644
index 0000000000..2c535f4586
--- /dev/null
+++ b/src/images/burger-menu-light-theme.svg
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/src/images/cart-dark-theme.svg b/src/images/cart-dark-theme.svg
new file mode 100644
index 0000000000..425ee63976
--- /dev/null
+++ b/src/images/cart-dark-theme.svg
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/public/img/cart-is-empty.png b/src/images/cart-is-empty.png
similarity index 100%
rename from public/img/cart-is-empty.png
rename to src/images/cart-is-empty.png
diff --git a/src/images/cart-light-theme.svg b/src/images/cart-light-theme.svg
new file mode 100644
index 0000000000..6030970f2e
--- /dev/null
+++ b/src/images/cart-light-theme.svg
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/src/images/favicon.svg b/src/images/favicon.svg
new file mode 100644
index 0000000000..c6b8617056
--- /dev/null
+++ b/src/images/favicon.svg
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/src/images/favorites-dark-theme.svg b/src/images/favorites-dark-theme.svg
new file mode 100644
index 0000000000..8fb5abef51
--- /dev/null
+++ b/src/images/favorites-dark-theme.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/src/images/favorites-light-theme.svg b/src/images/favorites-light-theme.svg
new file mode 100644
index 0000000000..ca57cfedd8
--- /dev/null
+++ b/src/images/favorites-light-theme.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/src/images/footer-arrowUp-dark-theme.svg b/src/images/footer-arrowUp-dark-theme.svg
new file mode 100644
index 0000000000..0d2745b7c1
--- /dev/null
+++ b/src/images/footer-arrowUp-dark-theme.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/src/images/footer-arrowUp-light-theme.svg b/src/images/footer-arrowUp-light-theme.svg
new file mode 100644
index 0000000000..0da5241741
--- /dev/null
+++ b/src/images/footer-arrowUp-light-theme.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/src/images/footer-logo-dark-theme.svg b/src/images/footer-logo-dark-theme.svg
new file mode 100644
index 0000000000..d59f941639
--- /dev/null
+++ b/src/images/footer-logo-dark-theme.svg
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/images/footer-logo-light-theme.svg b/src/images/footer-logo-light-theme.svg
new file mode 100644
index 0000000000..0a2d076bef
--- /dev/null
+++ b/src/images/footer-logo-light-theme.svg
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/images/icon-close-dark-theme.svg b/src/images/icon-close-dark-theme.svg
new file mode 100644
index 0000000000..925e5fce49
--- /dev/null
+++ b/src/images/icon-close-dark-theme.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/src/images/icon-close-light-theme.svg b/src/images/icon-close-light-theme.svg
new file mode 100644
index 0000000000..78d418ab46
--- /dev/null
+++ b/src/images/icon-close-light-theme.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/src/images/icon-filled-heart-fav-red.svg b/src/images/icon-filled-heart-fav-red.svg
new file mode 100644
index 0000000000..be5c1fc994
--- /dev/null
+++ b/src/images/icon-filled-heart-fav-red.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/src/images/icon-heart-dark-theme.svg b/src/images/icon-heart-dark-theme.svg
new file mode 100644
index 0000000000..35c86cc33b
--- /dev/null
+++ b/src/images/icon-heart-dark-theme.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/src/images/icon-heart-light-theme.svg b/src/images/icon-heart-light-theme.svg
new file mode 100644
index 0000000000..a90209fa54
--- /dev/null
+++ b/src/images/icon-heart-light-theme.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/src/images/icon-home-dark-theme.svg b/src/images/icon-home-dark-theme.svg
new file mode 100644
index 0000000000..e16ca7d794
--- /dev/null
+++ b/src/images/icon-home-dark-theme.svg
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/src/images/icon-home-light-theme.svg b/src/images/icon-home-light-theme.svg
new file mode 100644
index 0000000000..474476cb02
--- /dev/null
+++ b/src/images/icon-home-light-theme.svg
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/src/images/icon-left-dark-theme.svg b/src/images/icon-left-dark-theme.svg
new file mode 100644
index 0000000000..e2016da355
--- /dev/null
+++ b/src/images/icon-left-dark-theme.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/src/images/icon-left-light-theme.svg b/src/images/icon-left-light-theme.svg
new file mode 100644
index 0000000000..32c91f685f
--- /dev/null
+++ b/src/images/icon-left-light-theme.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/src/images/icon-minus-dark-theme.svg b/src/images/icon-minus-dark-theme.svg
new file mode 100644
index 0000000000..7ca53e577a
--- /dev/null
+++ b/src/images/icon-minus-dark-theme.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/src/images/icon-minus-light-theme.svg b/src/images/icon-minus-light-theme.svg
new file mode 100644
index 0000000000..97c41038ac
--- /dev/null
+++ b/src/images/icon-minus-light-theme.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/src/images/icon-moon.svg b/src/images/icon-moon.svg
new file mode 100644
index 0000000000..8041dbf83a
--- /dev/null
+++ b/src/images/icon-moon.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/images/icon-plus-dark-theme.svg b/src/images/icon-plus-dark-theme.svg
new file mode 100644
index 0000000000..aa791a47ad
--- /dev/null
+++ b/src/images/icon-plus-dark-theme.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/src/images/icon-plus-light-theme.svg b/src/images/icon-plus-light-theme.svg
new file mode 100644
index 0000000000..ab3c34061b
--- /dev/null
+++ b/src/images/icon-plus-light-theme.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/src/images/icon-right-dark-theme.svg b/src/images/icon-right-dark-theme.svg
new file mode 100644
index 0000000000..efe2edfd36
--- /dev/null
+++ b/src/images/icon-right-dark-theme.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/src/images/icon-right-light-theme.svg b/src/images/icon-right-light-theme.svg
new file mode 100644
index 0000000000..b4f4687671
--- /dev/null
+++ b/src/images/icon-right-light-theme.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/src/images/icon-search-dark-theme.svg b/src/images/icon-search-dark-theme.svg
new file mode 100644
index 0000000000..56a317c46f
--- /dev/null
+++ b/src/images/icon-search-dark-theme.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/src/images/icon-search-light-theme.svg b/src/images/icon-search-light-theme.svg
new file mode 100644
index 0000000000..801f11a548
--- /dev/null
+++ b/src/images/icon-search-light-theme.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/src/images/icon-sun.svg b/src/images/icon-sun.svg
new file mode 100644
index 0000000000..aefc5da19a
--- /dev/null
+++ b/src/images/icon-sun.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/images/logo-dark-theme.svg b/src/images/logo-dark-theme.svg
new file mode 100644
index 0000000000..3cc037d318
--- /dev/null
+++ b/src/images/logo-dark-theme.svg
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/images/logo-light-theme.svg b/src/images/logo-light-theme.svg
new file mode 100644
index 0000000000..6c9de93f01
--- /dev/null
+++ b/src/images/logo-light-theme.svg
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/images/page-not-found.png b/src/images/page-not-found.png
new file mode 100644
index 0000000000..b39a562f7a
Binary files /dev/null and b/src/images/page-not-found.png differ
diff --git a/src/images/phones-category.png b/src/images/phones-category.png
new file mode 100644
index 0000000000..bfd46779d9
Binary files /dev/null and b/src/images/phones-category.png differ
diff --git a/src/images/product-not-found.png b/src/images/product-not-found.png
new file mode 100644
index 0000000000..aa335c9f77
Binary files /dev/null and b/src/images/product-not-found.png differ
diff --git a/src/images/tablets-category.png b/src/images/tablets-category.png
new file mode 100644
index 0000000000..175ca600e5
Binary files /dev/null and b/src/images/tablets-category.png differ
diff --git a/src/index.tsx b/src/index.tsx
index 50470f1508..6d981a67b4 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -1,4 +1,10 @@
import { createRoot } from 'react-dom/client';
-import { App } from './App';
+import { Root } from './Root';
+import { Provider } from 'react-redux';
+import store from './app/store';
-createRoot(document.getElementById('root') as HTMLElement).render( );
+createRoot(document.getElementById('root') as HTMLElement).render(
+
+
+ ,
+);
diff --git a/src/modules/AccessoriesPage/AccessoriesPage.module.scss b/src/modules/AccessoriesPage/AccessoriesPage.module.scss
new file mode 100644
index 0000000000..980629d462
--- /dev/null
+++ b/src/modules/AccessoriesPage/AccessoriesPage.module.scss
@@ -0,0 +1,48 @@
+@import '../../styles/main';
+
+.accessoriesPage {
+ width: 100%;
+ height: 100%;
+
+ .container {
+ @include padding-content-inline-responsive;
+ }
+
+ .title {
+ font-family: Mont, sans-serif;
+ font-size: 48px;
+ font-weight: 800;
+ line-height: 56px;
+ color: var(--primary);
+ margin-top: 24px;
+
+ @include on-tablet {
+ margin-top: 40px;
+ }
+ }
+
+ .count {
+ margin-top: 8px;
+ font-family: Mont, sans-serif;
+ font-size: 14px;
+ font-weight: 600;
+ line-height: 21px;
+ color: var(--header-text-color);
+
+ }
+
+ .dropdownContainer {
+ width: 100%;
+ height: 100%;
+ display: flex;
+ justify-content: center;
+ flex-direction: row;
+ gap: 16px;
+ margin-top: 32px;
+
+ @include on-tablet {
+ justify-content: left;
+ margin-top: 40px;
+ }
+ }
+}
diff --git a/src/modules/AccessoriesPage/AccessoriesPage.tsx b/src/modules/AccessoriesPage/AccessoriesPage.tsx
new file mode 100644
index 0000000000..60d8e28bbb
--- /dev/null
+++ b/src/modules/AccessoriesPage/AccessoriesPage.tsx
@@ -0,0 +1,116 @@
+import { useTranslation } from 'react-i18next';
+import { BreadCrumbs } from '../../components/BreadCrumbs';
+import styles from './AccessoriesPage.module.scss';
+import { useAppSelector } from '../../hooks/hooks';
+import { useDispatch } from 'react-redux';
+import { useSearchParams } from 'react-router-dom';
+import { getNewProducts } from '../../services/getNewProducts';
+import { useEffect, useState } from 'react';
+import { setProducts } from '../../features/productsSlice';
+import { Categories } from '../../types/Categories';
+import { SortByDropdown } from '../../components/SortByDropdown';
+import { ItemsPerPageDropdown } from '../../components/ItemsPerPageDropdown';
+import { Product } from '../../types/Product';
+import { ProductGallery } from '../../components/ProductGallery';
+import { getItemsPerPage } from '../../services/getItemsPerPage';
+import { LoaderProductCard } from '../../components/LoaderProductCard';
+import { Pagination } from '../../components/Pagination/Pagination';
+import { ProductNotFoundPage } from '../ProductNotFoundPage';
+
+export const AccessoriesPage = () => {
+ const [isLoading, setIsLoading] = useState(true);
+ const { t } = useTranslation();
+ const { products } = useAppSelector(state => state.products);
+ const dispatch = useDispatch();
+ const [searchParams] = useSearchParams();
+ const sort = searchParams.get('sort');
+ const query = searchParams.get('search');
+
+ const type = Categories.Accessories;
+
+ const filteredProducts = products.filter(product => {
+ if (query) {
+ return (
+ product.category === type &&
+ product.name.toLowerCase().includes(query.toLowerCase())
+ );
+ } else {
+ return product.category === type;
+ }
+ });
+
+ const sortProducts = (item: Product[], sortType: string) => {
+ switch (sortType) {
+ case `${t('sortByDropdown.option.newest')}`:
+ return item.sort((a, b) => b.year - a.year);
+
+ case `${t('sortByDropdown.option.alphabetically')}`:
+ return item.sort((a, b) => a.name.localeCompare(b.name));
+
+ case `${t('sortByDropdown.option.cheapest')}`:
+ return item.sort((a, b) => a.price - b.price);
+
+ default:
+ return item;
+ }
+ };
+
+ const sortedProducts = sortProducts(filteredProducts, sort as string);
+ const page = searchParams.get('page') || 1;
+ const perPage = searchParams.get('perPage');
+
+ const itemsPerPage = getItemsPerPage(perPage, page, filteredProducts);
+
+ useEffect(() => {
+ setIsLoading(true);
+
+ getNewProducts()
+ .then(resolve => {
+ const newProducts = resolve.map(item => ({ ...item, quantity: 1 }));
+
+ dispatch(setProducts(newProducts));
+ })
+ .catch(
+ () =>
+ // eslint-disable-next-line max-len
+ 'Oops! Something went wrong while loading data. Please try again later.',
+ )
+ .finally(() => {
+ setTimeout(() => {
+ setIsLoading(false);
+ }, 500);
+ });
+ }, [dispatch, searchParams]);
+
+ useEffect(() => {
+ window.scrollTo({ top: 0, behavior: 'smooth' });
+ }, [page]);
+
+ if (!itemsPerPage.length && !isLoading) {
+ return ;
+ }
+
+ return (
+
+
+
+
{t('accessoriesPage.title')}
+
+ {t('accessoriesPage.count', { count: filteredProducts.length })}
+
+
+
+
+
+
+ {isLoading ? (
+
+ ) : (
+
+ )}
+
+ {perPage &&
}
+
+
+ );
+};
diff --git a/src/modules/AccessoriesPage/index.ts b/src/modules/AccessoriesPage/index.ts
new file mode 100644
index 0000000000..486474aa0b
--- /dev/null
+++ b/src/modules/AccessoriesPage/index.ts
@@ -0,0 +1 @@
+export * from './AccessoriesPage';
diff --git a/src/modules/CartPage/CartPage.module.scss b/src/modules/CartPage/CartPage.module.scss
new file mode 100644
index 0000000000..d5effeffbd
--- /dev/null
+++ b/src/modules/CartPage/CartPage.module.scss
@@ -0,0 +1,223 @@
+@import '../../styles/main';
+
+.cartPage {
+ height: 100%;
+ width: 100%;
+ margin-top: 24px;
+ position: relative;
+
+ @include padding-content-inline-responsive;
+
+ @include on-tablet {
+ margin-top: 40px;
+ }
+
+ .container {
+ display: flex;
+
+ .customBackButton {
+ color: var(--back-btn-cart);
+ font-size: 12px;
+ transition: color 0.3s ease;
+
+ &:hover {
+ color: var(--back-btn-cart-hover);
+ }
+
+ .customIcon {
+ opacity: 1;
+ transition: filter 0.3s ease;
+
+ &:hover {
+ filter: invert(29%) sepia(57%) saturate(548%) hue-rotate(270deg) brightness(93%) contrast(92%);
+ }
+ }
+ }
+ }
+
+ .cartTitle {
+ font-family: Mont, sans-serif;
+ font-size: 32px;
+ font-weight: 800;
+ color: var(--header-text-color-active);
+ margin-top: 24px;
+
+ @include on-tablet {
+ font-size: 48px;
+ margin-top: 16px;
+ }
+ }
+
+ .content {
+ @include on-desktop {
+ display: flex;
+ flex-direction: row;
+ gap: 16px;
+ width: 100%;
+ }
+
+ .loader {
+ position: absolute;
+ left: 45%;
+ padding-top: 40px;
+ }
+
+ .list {
+ display: flex;
+ flex-direction: column;
+ margin-top: 32px;
+ gap: 16px;
+ width: 100%;
+
+ @include on-desktop {
+ width: 752px;
+ }
+
+ .cartItem {
+ cursor: pointer;
+ display: flex;
+ width: 100%;
+ }
+ }
+
+ .totalCheckoutBlock {
+ margin-top: 32px;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ width: 100%;
+ height: 190px;
+ padding-inline: 24px;
+ padding-block: 24px;
+ border: 1px solid var(--checkout-border);
+
+ @include on-tablet {
+ height: 206px;
+ }
+
+ @include on-desktop {
+ width: 368px;
+ }
+
+ .containerBtm {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ width: 100%;
+
+ .totalPrice {
+ display: flex;
+ align-items: center;
+ font-family: Mont, sans-serif;
+ font-size: 32px;
+ font-weight: 800;
+ color: var(--header-text-color-active);
+ }
+
+ .totalCount {
+ font-family: Mont, sans-serif;
+ font-size: 14px;
+ font-weight: 600;
+ color: var(--header-text-color);
+ }
+
+ .line {
+ height: 1px;
+ width: 100%;
+ background-color: var(--checkout-border);
+ display: flex;
+ margin-bottom: 16px;
+ margin-top: 16px;
+
+ @include on-desktop {
+ margin-bottom: 24px;
+ margin-top: 25px;
+ }
+ }
+ }
+
+ .checkoutBtn {
+ height: 48px;
+ background-color: var(--pg-selected);
+ color: var(--checkout-btn-color);
+ width: 100%;
+ font-family: Mont, sans-serif;
+ font-size: 14px;
+ font-weight: 700;
+ transition: box-shadow 0.3s ease, background-color 0.3s ease;
+
+ &:hover {
+ background-color: var(--btn-bg-hover);
+ box-shadow: var(--btn-hover-shadow);
+ }
+ }
+
+
+ }
+
+ .emptyContent {
+ padding-top: 7px;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ gap: 7px;
+ text-align: center;
+ height: 100%;
+ width: 100%;
+
+ .emptyCartMessage {
+ font-family: Mont, sans-serif;
+ font-weight: 700;
+ font-size: 21px;
+ line-height: 43px;
+ color: var(--header-text-color);
+ text-align: center;
+
+ @include on-tablet {
+ font-size: 32px;
+ }
+ }
+
+ .emptyImg {
+ width: 100%;
+ height: auto;
+ max-width: 400px;
+ display: block;
+ }
+ }
+ }
+}
+
+.containerLine {
+ margin-top: 20px;
+ position: relative;
+ width: 100%;
+ height: 50px;
+ overflow: hidden;
+}
+
+.movingText {
+ position: absolute;
+ top: 50%;
+ left: 100%;
+ transform: translateY(-50%);
+ white-space: nowrap;
+ color: var(--header-text-color-active);
+ font-family: Mont, sans-serif;
+ font-size: 21px;
+ font-weight: 800;
+ padding: 8px;
+ border: 1px solid var(--checkout-border);
+ border-radius: 10px;
+ animation: scroll 15s linear infinite;
+}
+
+@keyframes scroll {
+ 0% {
+ left: 100%;
+ }
+ 100% {
+ left: -100%;
+ }
+}
diff --git a/src/modules/CartPage/CartPage.tsx b/src/modules/CartPage/CartPage.tsx
new file mode 100644
index 0000000000..9a5cae4d92
--- /dev/null
+++ b/src/modules/CartPage/CartPage.tsx
@@ -0,0 +1,144 @@
+import { useTranslation } from 'react-i18next';
+import { GoBackButton } from '../../components/GoBackButton';
+import styles from './CartPage.module.scss';
+import { Link, useNavigate } from 'react-router-dom';
+import { useAppDispatch, useAppSelector } from '../../hooks/hooks';
+import { CartItem } from '../../components/CartItem';
+import { useEffect, useState } from 'react';
+import emptyCartImg from '../../images/cart-is-empty.png';
+import { Modal } from '../../components/Modal';
+import { clearCart } from '../../features/cartSlice';
+import { Loader } from '../../components/Loader';
+
+export const CartPage = () => {
+ const { t } = useTranslation();
+ const navigate = useNavigate();
+ const { cartProducts } = useAppSelector(state => state.cart);
+ const dispatch = useAppDispatch();
+ const [isLoading, setIsLoading] = useState(true);
+
+ useEffect(() => {
+ const timer = setTimeout(() => {
+ setIsLoading(false);
+ }, 2000);
+
+ return () => clearTimeout(timer);
+ }, []);
+
+ const totalPrice = cartProducts.reduce(
+ (total, product) => total + product.price * product.quantity,
+ 0,
+ );
+ const totalCount = cartProducts.reduce(
+ (count, product) => count + product.quantity,
+ 0,
+ );
+
+ const [isModalOpen, setIsModalOpen] = useState(false);
+ const handleCheckoutClick = () => {
+ setIsModalOpen(true);
+ };
+
+ const handleConfirm = () => {
+ dispatch(clearCart());
+ setIsModalOpen(false);
+ };
+
+ const handleCancel = () => {
+ setIsModalOpen(false);
+ };
+
+ const getItemForm = (count: number) => {
+ if (count === 1) {
+ return 'one';
+ }
+
+ if (count >= 2 && count <= 4) {
+ return 'few';
+ }
+
+ if (count > 4) {
+ return 'many';
+ }
+
+ return 'other';
+ };
+
+ useEffect(() => {
+ window.scrollTo({ top: 0 });
+ }, []);
+
+ return (
+
+
+ navigate(-1)}
+ />
+
+
+
{t('cartPage.title')}
+
+
+ {isLoading ? (
+
+
+
+ ) : cartProducts.length > 0 ? (
+ <>
+
+ {cartProducts.map(product => (
+
+
+
+ ))}
+
+
+
+
{`$${totalPrice}`}
+
+ {t('cartPage.totalFor', {
+ count: totalCount,
+ items: t(`cartPage.items.${getItemForm(totalCount)}`),
+ })}
+
+
+
+
+ {t('cartPage.checkout')}
+
+
+ >
+ ) : (
+
+
{t('cartPage.emptyCart')}
+
+
+ )}
+
+
+
+
+
+
{t('cartPage.movingText')} ❤️
+
+
+ );
+};
diff --git a/src/modules/CartPage/index.ts b/src/modules/CartPage/index.ts
new file mode 100644
index 0000000000..90c010237a
--- /dev/null
+++ b/src/modules/CartPage/index.ts
@@ -0,0 +1 @@
+export * from './CartPage';
diff --git a/src/modules/FavoritesPage/FavoritePage.module.scss b/src/modules/FavoritesPage/FavoritePage.module.scss
new file mode 100644
index 0000000000..01f9638abf
--- /dev/null
+++ b/src/modules/FavoritesPage/FavoritePage.module.scss
@@ -0,0 +1,74 @@
+@import '../../styles/main';
+
+.favoritesPage {
+ width: 100%;
+ height: 100%;
+
+ .container {
+ height: 100%;
+
+ @include padding-content-inline-responsive;
+ }
+
+ .title {
+ font-family: Mont, sans-serif;
+ font-size: 32px;
+ font-weight: 800;
+ line-height: 56px;
+ color: var(--primary);
+ margin-top: 24px;
+
+ @include on-tablet {
+ margin-top: 40px;
+ font-size: 48px;
+ }
+ }
+
+ .count {
+ margin-top: 8px;
+ font-family: Mont, sans-serif;
+ font-size: 14px;
+ font-weight: 600;
+ line-height: 21px;
+ color: var(--header-text-color);
+
+ }
+
+ .empty {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex-direction: column;
+ width: 100%;
+ height: 100%;
+
+ .img {
+ width: 100%;
+ height: auto;
+ max-width: 400px;
+ display: block;
+ }
+
+ .emptyTitle {
+ padding-top: 7px;
+ font-family: Mont, sans-serif;
+ font-weight: 700;
+ font-size: 21px;
+ line-height: 43px;
+ padding-bottom: 1rem;
+ color: var(--header-text-color);
+ text-align: center;
+ white-space: pre-line;
+
+ @include on-tablet {
+ font-size: 32px;;
+ }
+ }
+ }
+ }
+
+ .loader {
+ padding-top: 40px;
+
+ }
+
diff --git a/src/modules/FavoritesPage/FavoritesPage.tsx b/src/modules/FavoritesPage/FavoritesPage.tsx
new file mode 100644
index 0000000000..f515d99cf0
--- /dev/null
+++ b/src/modules/FavoritesPage/FavoritesPage.tsx
@@ -0,0 +1,67 @@
+import { useTranslation } from 'react-i18next';
+import { BreadCrumbs } from '../../components/BreadCrumbs';
+import { useAppSelector } from '../../hooks/hooks';
+import styles from './FavoritePage.module.scss';
+import { ProductGallery } from '../../components/ProductGallery';
+import { useEffect, useState } from 'react';
+import { Loader } from '../../components/Loader';
+import emptyFavoritesImg from '../../images/product-not-found.png';
+import { useNavigate } from 'react-router-dom';
+import { GoBackButton } from '../../components/GoBackButton/GoBackButton';
+
+export const FavoritesPage = () => {
+ const { favoriteProducts } = useAppSelector(state => state.favorites);
+ const { t } = useTranslation();
+ const navigate = useNavigate();
+ const [isLoading, setIsLoading] = useState(true);
+
+ useEffect(() => {
+ const timer = setTimeout(() => {
+ setIsLoading(false);
+ }, 2000);
+
+ return () => clearTimeout(timer);
+ }, []);
+
+ const handleGoBack = () => {
+ if (window.history.length > 1) {
+ navigate(-1);
+ } else {
+ navigate('/');
+ }
+ };
+
+ return (
+
+
+
+
{t('favoritesPage.title')}
+
+ {isLoading ? (
+
+
+
+ ) : (
+
+ {t('favoritesPage.count', { count: favoriteProducts.length })}
+
+ )}
+
+ {isLoading ? null : favoriteProducts.length > 0 ? (
+
+ ) : (
+
+
{t('favoritesPage.empty')}
+
+
+
+
+ )}
+
+
+ );
+};
diff --git a/src/modules/FavoritesPage/index.ts b/src/modules/FavoritesPage/index.ts
new file mode 100644
index 0000000000..b3a884b188
--- /dev/null
+++ b/src/modules/FavoritesPage/index.ts
@@ -0,0 +1 @@
+export * from './FavoritesPage';
diff --git a/src/modules/HomePage/HomePage.module.scss b/src/modules/HomePage/HomePage.module.scss
new file mode 100644
index 0000000000..6b02192155
--- /dev/null
+++ b/src/modules/HomePage/HomePage.module.scss
@@ -0,0 +1,66 @@
+@import '../../styles/main';
+
+.homePage {
+ position: relative;
+
+ &__title {
+ @include padding-content-inline-responsive;
+
+ &__text {
+ font-family: Mont, sans-serif;
+ font-size: 32px;
+ font-weight: 800;
+ line-height: 41px;
+ color: var(--primary);
+ padding-block: 24px;
+
+ @include on-tablet {
+ font-size: 48px;
+ font-weight: 800;
+ line-height: 56px;
+ padding-block: 32px;
+ }
+
+ @include on-desktop {
+ padding-block: 56px;
+ }
+ }
+ }
+
+ .content {
+ display: flex;
+ flex-direction: column;
+ row-gap: 56px;
+
+ @include on-tablet {
+ row-gap: 64px;
+ }
+
+ .picturesSlider {
+ padding-inline: 0;
+
+ @include on-tablet {
+ @include padding-content-inline-responsive;
+ }
+ }
+
+ .phonesSlider {
+ padding-left: 16px;
+
+ @include on-tablet {
+ padding-inline: 24px;
+ }
+
+ @include on-desktop {
+ padding-inline: 152px;
+ }
+ }
+ }
+
+}
+
+.loader {
+position: absolute;
+left: 45%;
+top: 25%;
+}
diff --git a/src/modules/HomePage/HomePage.tsx b/src/modules/HomePage/HomePage.tsx
new file mode 100644
index 0000000000..f963c9e084
--- /dev/null
+++ b/src/modules/HomePage/HomePage.tsx
@@ -0,0 +1,86 @@
+import { useTranslation } from 'react-i18next';
+import styles from '../../modules/HomePage/HomePage.module.scss';
+import PicturesSlider from '../../components/PicturesSlider/PicturesSlider';
+import { PhonesSlider } from '../../components/PhonesSlider/PhonesSlider';
+import { useEffect, useState } from 'react';
+import { getNewProducts } from '../../services/getNewProducts';
+import { useAppDispatch, useAppSelector } from '../../hooks/hooks';
+import { setProducts } from '../../features/productsSlice';
+import { Loader } from '../../components/Loader';
+import { CategoriesSection } from '../../components/CategoriesSection';
+
+export const HomePage = () => {
+ const { t } = useTranslation();
+ const dispatch = useAppDispatch();
+ const [isLoading, setIsLoading] = useState(false);
+ const { products } = useAppSelector(state => state.products);
+
+ useEffect(() => {
+ setIsLoading(true);
+
+ getNewProducts()
+ .then(resolve => {
+ const newProducts = resolve.map(item => ({ ...item, quantity: 1 }));
+
+ dispatch(setProducts(newProducts));
+ })
+ .catch(
+ () =>
+ // eslint-disable-next-line max-len
+ 'Oops! Something went wrong while loading data. Please try again later.',
+ )
+ .finally(() => {
+ setTimeout(() => {
+ setIsLoading(false);
+ }, 500);
+ });
+ }, [dispatch]);
+
+ const brandNewModels = [...products]
+ .filter(prod => prod.year === 2022)
+ .sort((prod1, prod2) => prod1.year - prod2.year);
+
+ const discountedModels = [...products]
+ .sort((a, b) => b.fullPrice - b.price - (a.fullPrice - a.price))
+ .filter(prod => prod.fullPrice - prod.price > 80);
+
+ if (isLoading) {
+ return (
+
+
+
+ );
+ }
+
+ return (
+
+
+
{t('homePage.title')}
+
+
+
+ );
+};
diff --git a/src/modules/HomePage/index.ts b/src/modules/HomePage/index.ts
new file mode 100644
index 0000000000..11e53da674
--- /dev/null
+++ b/src/modules/HomePage/index.ts
@@ -0,0 +1 @@
+export * from './HomePage';
diff --git a/src/modules/NotFoundPage/NotFoundPage.module.scss b/src/modules/NotFoundPage/NotFoundPage.module.scss
new file mode 100644
index 0000000000..27013e4a40
--- /dev/null
+++ b/src/modules/NotFoundPage/NotFoundPage.module.scss
@@ -0,0 +1,60 @@
+@import '../../styles/main';
+
+.notFoundPage {
+ width: 100%;
+ height: 100%;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ background-color: var(--c-background);
+ text-align: center;
+
+ @include padding-content-inline-responsive;
+}
+
+.title {
+ font-family: Mont, sans-serif;
+ font-weight: 800;
+ font-size: 32px;
+ padding-bottom: 1rem;
+ color: var(--primary);
+
+ @include on-tablet {
+ font-size: 48px;
+ }
+}
+
+.message {
+ font-family: Mont, sans-serif;
+ font-weight: 700;
+ font-size: 1.5rem;
+ padding-top: 60px;
+ color: var(--primary);
+}
+
+ .image {
+ max-width: 400px;
+ align-items: center;
+ }
+
+ .back {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+
+ .backIcon {
+ opacity: 0.5;
+ }
+
+ .link {
+ font-size: 1rem;
+ color: var(--header-text-color);
+ text-decoration: none;
+
+ &:hover {
+ color: var(--header-text-color-active);
+ }
+ }
+ }
+
diff --git a/src/modules/NotFoundPage/NotFoundPage.tsx b/src/modules/NotFoundPage/NotFoundPage.tsx
new file mode 100644
index 0000000000..18e07f13c6
--- /dev/null
+++ b/src/modules/NotFoundPage/NotFoundPage.tsx
@@ -0,0 +1,32 @@
+import styles from './NotFoundPage.module.scss';
+import NotFoundPageImg from '../../images/page-not-found.png';
+import { useTranslation } from 'react-i18next';
+import { GoBackButton } from '../../components/GoBackButton';
+import { useNavigate } from 'react-router-dom';
+
+export const NotFoundPage = () => {
+ const { t } = useTranslation();
+ const navigate = useNavigate();
+
+ const onGoBackHome = () => {
+ navigate('/');
+ };
+
+ return (
+
+
{t('notFoundPage.message')}
+
+
{t('notFoundPage.title')}
+
+
+
+ );
+};
diff --git a/src/modules/NotFoundPage/index.ts b/src/modules/NotFoundPage/index.ts
new file mode 100644
index 0000000000..6197aa75aa
--- /dev/null
+++ b/src/modules/NotFoundPage/index.ts
@@ -0,0 +1 @@
+export * from './NotFoundPage';
diff --git a/src/modules/PhonesPage/PhonesPage.module.scss b/src/modules/PhonesPage/PhonesPage.module.scss
new file mode 100644
index 0000000000..b66657e152
--- /dev/null
+++ b/src/modules/PhonesPage/PhonesPage.module.scss
@@ -0,0 +1,49 @@
+@import '../../styles/main';
+
+.phonesPage {
+ width: 100%;
+ height: 100%;
+
+ .container {
+ @include padding-content-inline-responsive;
+ }
+
+ .title {
+ font-family: Mont, sans-serif;
+ font-size: 48px;
+ font-weight: 800;
+ line-height: 56px;
+ color: var(--primary);
+ margin-top: 24px;
+
+ @include on-tablet {
+ margin-top: 40px;
+ }
+ }
+
+ .count {
+ margin-top: 8px;
+ font-family: Mont, sans-serif;
+ font-size: 14px;
+ font-weight: 600;
+ line-height: 21px;
+ color: var(--header-text-color);
+
+ }
+
+ .dropdownContainer {
+ width: 100%;
+ height: 100%;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ flex-direction: row;
+ gap: 16px;
+ margin-top: 32px;
+
+ @include on-tablet {
+ justify-content: left;
+ margin-top: 40px;
+ }
+ }
+}
diff --git a/src/modules/PhonesPage/PhonesPage.tsx b/src/modules/PhonesPage/PhonesPage.tsx
new file mode 100644
index 0000000000..0fc0b37ef1
--- /dev/null
+++ b/src/modules/PhonesPage/PhonesPage.tsx
@@ -0,0 +1,116 @@
+import { useTranslation } from 'react-i18next';
+import { BreadCrumbs } from '../../components/BreadCrumbs';
+import styles from './PhonesPage.module.scss';
+import { useAppSelector } from '../../hooks/hooks';
+import { useDispatch } from 'react-redux';
+import { useSearchParams } from 'react-router-dom';
+import { getNewProducts } from '../../services/getNewProducts';
+import { useEffect, useState } from 'react';
+import { setProducts } from '../../features/productsSlice';
+import { Categories } from '../../types/Categories';
+import { SortByDropdown } from '../../components/SortByDropdown';
+import { ItemsPerPageDropdown } from '../../components/ItemsPerPageDropdown';
+import { Product } from '../../types/Product';
+import { ProductGallery } from '../../components/ProductGallery';
+import { getItemsPerPage } from '../../services/getItemsPerPage';
+import { LoaderProductCard } from '../../components/LoaderProductCard';
+import { Pagination } from '../../components/Pagination/Pagination';
+import { ProductNotFoundPage } from '../ProductNotFoundPage';
+
+export const PhonesPage = () => {
+ const [isLoading, setIsLoading] = useState(true);
+ const { t } = useTranslation();
+ const { products } = useAppSelector(state => state.products);
+ const dispatch = useDispatch();
+ const [searchParams] = useSearchParams();
+ const sort = searchParams.get('sort');
+ const query = searchParams.get('search');
+
+ const type = Categories.Phones;
+
+ const filteredProducts = products.filter(product => {
+ if (query) {
+ return (
+ product.category === type &&
+ product.name.toLowerCase().includes(query.toLowerCase())
+ );
+ } else {
+ return product.category === type;
+ }
+ });
+
+ const sortProducts = (item: Product[], sortType: string) => {
+ switch (sortType) {
+ case `${t('sortByDropdown.option.newest')}`:
+ return item.sort((a, b) => b.year - a.year);
+
+ case `${t('sortByDropdown.option.alphabetically')}`:
+ return item.sort((a, b) => a.name.localeCompare(b.name));
+
+ case `${t('sortByDropdown.option.cheapest')}`:
+ return item.sort((a, b) => a.price - b.price);
+
+ default:
+ return item;
+ }
+ };
+
+ const sortedProducts = sortProducts(filteredProducts, sort as string);
+ const page = Number(searchParams.get('page')) || 1;
+ const perPage = searchParams.get('perPage');
+
+ const itemsPerPage = getItemsPerPage(perPage, page, filteredProducts);
+
+ useEffect(() => {
+ setIsLoading(true);
+
+ getNewProducts()
+ .then(resolve => {
+ const newProducts = resolve.map(item => ({ ...item, quantity: 1 }));
+
+ dispatch(setProducts(newProducts));
+ })
+ .catch(
+ () =>
+ // eslint-disable-next-line max-len
+ 'Oops! Something went wrong while loading data. Please try again later.',
+ )
+ .finally(() => {
+ setTimeout(() => {
+ setIsLoading(false);
+ }, 500);
+ });
+ }, [dispatch, searchParams]);
+
+ useEffect(() => {
+ window.scrollTo({ top: 0, behavior: 'smooth' });
+ }, [page]);
+
+ if (!itemsPerPage.length && !isLoading) {
+ return ;
+ }
+
+ return (
+
+
+
+
{t('phonesPage.title')}
+
+ {t('phonesPage.count', { count: filteredProducts.length })}
+
+
+
+
+
+
+ {isLoading ? (
+
+ ) : (
+
+ )}
+
+ {perPage &&
}
+
+
+ );
+};
diff --git a/src/modules/PhonesPage/index.ts b/src/modules/PhonesPage/index.ts
new file mode 100644
index 0000000000..380be65cc7
--- /dev/null
+++ b/src/modules/PhonesPage/index.ts
@@ -0,0 +1 @@
+export * from './PhonesPage';
diff --git a/src/modules/ProductDetailsPage/ProductDetailsPage.module.scss b/src/modules/ProductDetailsPage/ProductDetailsPage.module.scss
new file mode 100644
index 0000000000..174ee27b06
--- /dev/null
+++ b/src/modules/ProductDetailsPage/ProductDetailsPage.module.scss
@@ -0,0 +1,59 @@
+@import '../../styles/main';
+
+.productDetailsPage {
+ position: relative;
+
+ @include padding-content-inline-responsive;
+
+
+.goBackBtn {
+ display: flex;
+ align-items: flex-start;
+ padding-top: 24px;
+
+ @include on-tablet {
+ padding-top: 40px;
+ }
+
+ .customBackButton {
+ color: var(--back-btn-cart);
+ font-size: 12px;
+ transition: color 0.2s ease;
+
+ &:hover {
+ color: var(--back-btn-cart-hover);
+ }
+
+ .customIcon {
+ opacity: 1;
+ transition: filter 0.2s ease;
+
+ &:hover {
+ filter: invert(29%) sepia(57%) saturate(548%) hue-rotate(270deg) brightness(93%) contrast(92%);
+
+ }
+ }
+ }
+}
+
+ .productTitle {
+ font-family: Mont, sans-serif;
+ font-size: 22px;
+ font-weight: 800;
+ color: var(--header-text-color-active);
+ padding-top: 16px;
+ padding-bottom: 32px;
+
+ @include on-tablet {
+ font-size: 32px;
+ padding-bottom: 40px;
+}
+ }
+
+}
+
+.mainLoader {
+ position: absolute;
+left: 45%;
+top: 25%;
+}
diff --git a/src/modules/ProductDetailsPage/ProductDetailsPage.tsx b/src/modules/ProductDetailsPage/ProductDetailsPage.tsx
new file mode 100644
index 0000000000..d9059e78b4
--- /dev/null
+++ b/src/modules/ProductDetailsPage/ProductDetailsPage.tsx
@@ -0,0 +1,100 @@
+import { useEffect, useState } from 'react';
+import { BreadCrumbs } from '../../components/BreadCrumbs';
+import { PhonesSlider } from '../../components/PhonesSlider/PhonesSlider';
+import { getNewProducts } from '../../services/getNewProducts';
+import { useAppDispatch, useAppSelector } from '../../hooks/hooks';
+import { setProducts } from '../../features/productsSlice';
+import { getProductsDetails } from '../../services/getProductDetails';
+import { ProductDetails } from '../../types/ProductDetails';
+import { useLocation, useNavigate, useParams } from 'react-router-dom';
+import { ProductDetailsCard } from '../../components/ProductDetailsCard';
+import { Loader } from '../../components/Loader';
+import styles from './ProductDetailsPage.module.scss';
+import { useTranslation } from 'react-i18next';
+import { GoBackButton } from '../../components/GoBackButton';
+import { Product } from '../../types/Product';
+
+export const ProductDetailsPage = () => {
+ const [isLoading, setIsLoading] = useState(false);
+ const [details, setDetails] = useState([]);
+ const { products } = useAppSelector(state => state.products);
+ const { t } = useTranslation();
+ const navigate = useNavigate();
+ const dispatch = useAppDispatch();
+ const slash = true;
+ const { productId = '' } = useParams();
+ const { pathname } = useLocation();
+ const newId = productId.slice(1);
+
+ const selectedProduct: ProductDetails | undefined = details.find(
+ item => item.id === newId,
+ );
+
+ const path = pathname.slice(1);
+ const category = path.split('/').slice(0, 1).join();
+
+ useEffect(() => {
+ setIsLoading(true);
+
+ getNewProducts()
+ .then(response => {
+ dispatch(setProducts(response));
+ })
+ .catch(
+ () =>
+ // eslint-disable-next-line max-len
+ 'Oops! Something went wrong while loading data. Please try again later.',
+ )
+ .finally(() => {
+ setIsLoading(false);
+ });
+ }, [dispatch]);
+
+ useEffect(() => {
+ getProductsDetails(category).then(setDetails);
+ }, [category, newId]);
+
+ useEffect(() => {
+ window.scrollTo({ top: 0 });
+ }, [newId]);
+
+ const getRandomProducts = (productsRandom: Product[]) => {
+ const shuffled = [...productsRandom].sort(() => 0.5 - Math.random());
+
+ return shuffled;
+ };
+
+ const randomProducts = getRandomProducts(products);
+
+ if (isLoading) {
+ return (
+
+
+
+ );
+ }
+
+ return (
+
+
+
+ navigate(-1)}
+ />
+
+
{selectedProduct?.name}
+
+
+
+ );
+};
diff --git a/src/modules/ProductDetailsPage/index.ts b/src/modules/ProductDetailsPage/index.ts
new file mode 100644
index 0000000000..6615089e5e
--- /dev/null
+++ b/src/modules/ProductDetailsPage/index.ts
@@ -0,0 +1 @@
+export * from './ProductDetailsPage';
diff --git a/src/modules/ProductNotFoundPage/ProductNotFoundPage.module.scss b/src/modules/ProductNotFoundPage/ProductNotFoundPage.module.scss
new file mode 100644
index 0000000000..9e55a2939e
--- /dev/null
+++ b/src/modules/ProductNotFoundPage/ProductNotFoundPage.module.scss
@@ -0,0 +1,58 @@
+@import '../../styles/main';
+
+.productNotFoundPage {
+ width: 100%;
+ height: 100%;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ background-color: var(--c-background);
+ text-align: center;
+
+ @include padding-content-inline-responsive;
+}
+
+.title {
+ font-family: Mont, sans-serif;
+ font-weight: 800;
+ font-size: 32px;
+ padding-bottom: 1rem;
+ color: var(--primary);
+ white-space: pre-line;
+}
+
+.message {
+ font-family: Mont, sans-serif;
+ font-weight: 700;
+ font-size: 1.5rem;
+ padding-top: 60px;
+ color: var(--primary);
+}
+
+ .image {
+ max-width: 400px;
+ width: 100%;
+ height: 100%;
+ }
+
+ .back {
+ padding-top: 50px;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+
+ .backIcon {
+ opacity: 0.5;
+ }
+
+ .link {
+ font-size: 1rem;
+ color: var(--header-text-color);
+ text-decoration: none;
+
+ &:hover {
+ color: var(--header-text-color-active);
+ }
+ }
+ }
diff --git a/src/modules/ProductNotFoundPage/ProductNotFoundPage.tsx b/src/modules/ProductNotFoundPage/ProductNotFoundPage.tsx
new file mode 100644
index 0000000000..d704a8efda
--- /dev/null
+++ b/src/modules/ProductNotFoundPage/ProductNotFoundPage.tsx
@@ -0,0 +1,39 @@
+import styles from './ProductNotFoundPage.module.scss';
+import ProductNotFoundImg from '../../images/product-not-found.png';
+import React from 'react';
+import { useTranslation } from 'react-i18next';
+import { GoBackButton } from '../../components/GoBackButton';
+import { useNavigate } from 'react-router-dom';
+
+type Props = {
+ title?: string;
+};
+
+export const ProductNotFoundPage: React.FC = React.memo(({ title }) => {
+ const { t } = useTranslation();
+ const navigate = useNavigate();
+
+ const onGoBackHome = () => {
+ navigate('/');
+ };
+
+ return (
+
+
{t('notFoundPage.message')}
+
+
{title}
+
+
+
+ );
+});
+
+ProductNotFoundPage.displayName = 'ProductNotFoundPage';
diff --git a/src/modules/ProductNotFoundPage/index.ts b/src/modules/ProductNotFoundPage/index.ts
new file mode 100644
index 0000000000..d15aa85a32
--- /dev/null
+++ b/src/modules/ProductNotFoundPage/index.ts
@@ -0,0 +1 @@
+export * from './ProductNotFoundPage';
diff --git a/src/modules/TabletsPage/TabletsPage.module.scss b/src/modules/TabletsPage/TabletsPage.module.scss
new file mode 100644
index 0000000000..dea4997f96
--- /dev/null
+++ b/src/modules/TabletsPage/TabletsPage.module.scss
@@ -0,0 +1,48 @@
+@import '../../styles/main';
+
+.tabletsPage {
+ width: 100%;
+ height: 100%;
+
+ .container {
+ @include padding-content-inline-responsive;
+ }
+
+ .title {
+ font-family: Mont, sans-serif;
+ font-size: 48px;
+ font-weight: 800;
+ line-height: 56px;
+ color: var(--primary);
+ margin-top: 24px;
+
+ @include on-tablet {
+ margin-top: 40px;
+ }
+ }
+
+ .count {
+ margin-top: 8px;
+ font-family: Mont, sans-serif;
+ font-size: 14px;
+ font-weight: 600;
+ line-height: 21px;
+ color: var(--header-text-color);
+
+ }
+
+ .dropdownContainer {
+ width: 100%;
+ height: 100%;
+ display: flex;
+ justify-content: center;
+ flex-direction: row;
+ gap: 16px;
+ margin-top: 32px;
+
+ @include on-tablet {
+ justify-content: left;
+ margin-top: 40px;
+ }
+ }
+}
diff --git a/src/modules/TabletsPage/TabletsPage.tsx b/src/modules/TabletsPage/TabletsPage.tsx
new file mode 100644
index 0000000000..1b0bdd5ea9
--- /dev/null
+++ b/src/modules/TabletsPage/TabletsPage.tsx
@@ -0,0 +1,116 @@
+import { useTranslation } from 'react-i18next';
+import { BreadCrumbs } from '../../components/BreadCrumbs';
+import styles from './TabletsPage.module.scss';
+import { useAppSelector } from '../../hooks/hooks';
+import { useDispatch } from 'react-redux';
+import { useSearchParams } from 'react-router-dom';
+import { getNewProducts } from '../../services/getNewProducts';
+import { useEffect, useState } from 'react';
+import { setProducts } from '../../features/productsSlice';
+import { Categories } from '../../types/Categories';
+import { SortByDropdown } from '../../components/SortByDropdown';
+import { ItemsPerPageDropdown } from '../../components/ItemsPerPageDropdown';
+import { Product } from '../../types/Product';
+import { ProductGallery } from '../../components/ProductGallery';
+import { getItemsPerPage } from '../../services/getItemsPerPage';
+import { LoaderProductCard } from '../../components/LoaderProductCard';
+import { Pagination } from '../../components/Pagination/Pagination';
+import { ProductNotFoundPage } from '../ProductNotFoundPage';
+
+export const TabletsPage = () => {
+ const [isLoading, setIsLoading] = useState(true);
+ const { t } = useTranslation();
+ const { products } = useAppSelector(state => state.products);
+ const dispatch = useDispatch();
+ const [searchParams] = useSearchParams();
+ const sort = searchParams.get('sort');
+ const query = searchParams.get('search');
+
+ const type = Categories.Tablets;
+
+ const filteredProducts = products.filter(product => {
+ if (query) {
+ return (
+ product.category === type &&
+ product.name.toLowerCase().includes(query.toLowerCase())
+ );
+ } else {
+ return product.category === type;
+ }
+ });
+
+ const sortProducts = (item: Product[], sortType: string) => {
+ switch (sortType) {
+ case `${t('sortByDropdown.option.newest')}`:
+ return item.sort((a, b) => b.year - a.year);
+
+ case `${t('sortByDropdown.option.alphabetically')}`:
+ return item.sort((a, b) => a.name.localeCompare(b.name));
+
+ case `${t('sortByDropdown.option.cheapest')}`:
+ return item.sort((a, b) => a.price - b.price);
+
+ default:
+ return item;
+ }
+ };
+
+ const sortedProducts = sortProducts(filteredProducts, sort as string);
+ const page = searchParams.get('page') || 1;
+ const perPage = searchParams.get('perPage');
+
+ const itemsPerPage = getItemsPerPage(perPage, page, filteredProducts);
+
+ useEffect(() => {
+ setIsLoading(true);
+
+ getNewProducts()
+ .then(resolve => {
+ const newProducts = resolve.map(item => ({ ...item, quantity: 1 }));
+
+ dispatch(setProducts(newProducts));
+ })
+ .catch(
+ () =>
+ // eslint-disable-next-line max-len
+ 'Oops! Something went wrong while loading data. Please try again later.',
+ )
+ .finally(() => {
+ setTimeout(() => {
+ setIsLoading(false);
+ }, 500);
+ });
+ }, [dispatch, searchParams]);
+
+ useEffect(() => {
+ window.scrollTo({ top: 0, behavior: 'smooth' });
+ }, [page]);
+
+ if (!itemsPerPage.length && !isLoading) {
+ return ;
+ }
+
+ return (
+
+
+
+
{t('tabletsPage.title')}
+
+ {t('tabletsPage.count', { count: filteredProducts.length })}
+
+
+
+
+
+
+ {isLoading ? (
+
+ ) : (
+
+ )}
+
+ {perPage &&
}
+
+
+ );
+};
diff --git a/src/modules/TabletsPage/index.ts b/src/modules/TabletsPage/index.ts
new file mode 100644
index 0000000000..6988826db6
--- /dev/null
+++ b/src/modules/TabletsPage/index.ts
@@ -0,0 +1 @@
+export * from './TabletsPage';
diff --git a/src/services/getItemsPerPage.tsx b/src/services/getItemsPerPage.tsx
new file mode 100644
index 0000000000..e60627e996
--- /dev/null
+++ b/src/services/getItemsPerPage.tsx
@@ -0,0 +1,25 @@
+import { Product } from '../types/Product';
+
+export const getItemsPerPage = (
+ perPage: string | null,
+ currPage: number | string,
+ products: Product[],
+): Product[] => {
+ let paginatedProducts = [...products];
+
+ switch (perPage) {
+ case '4':
+ case '8':
+ case '16':
+ paginatedProducts = paginatedProducts.splice(
+ +perPage * (+currPage - 1),
+ +perPage,
+ );
+ break;
+
+ default:
+ break;
+ }
+
+ return paginatedProducts;
+};
diff --git a/src/services/getNewProducts.ts b/src/services/getNewProducts.ts
new file mode 100644
index 0000000000..e5533594e0
--- /dev/null
+++ b/src/services/getNewProducts.ts
@@ -0,0 +1,6 @@
+import { Product } from '../types/Product';
+import { getData } from '../helpers/httpClient';
+
+export function getNewProducts(): Promise {
+ return getData('/products.json');
+}
diff --git a/src/services/getProductDetails.ts b/src/services/getProductDetails.ts
new file mode 100644
index 0000000000..1654351f5c
--- /dev/null
+++ b/src/services/getProductDetails.ts
@@ -0,0 +1,8 @@
+import { getData } from '../helpers/httpClient';
+import { ProductDetails } from '../types/ProductDetails';
+
+export function getProductsDetails(
+ category: string,
+): Promise {
+ return getData(`/${category}.json`);
+}
diff --git a/src/styles/main.scss b/src/styles/main.scss
new file mode 100644
index 0000000000..9e79a343c8
--- /dev/null
+++ b/src/styles/main.scss
@@ -0,0 +1,6 @@
+@import '../styles/theme';
+@import '../utils/fonts';
+@import '../utils/reset';
+@import '../utils/variables';
+@import '../utils/mixins';
+
diff --git a/src/styles/theme.scss b/src/styles/theme.scss
new file mode 100644
index 0000000000..387a097cf5
--- /dev/null
+++ b/src/styles/theme.scss
@@ -0,0 +1,205 @@
+:root {
+ --c-background: #fff;
+ --header-text-color: #89939a;
+ --element-color: #e2e6e9;
+ --bg-hover-active: #e2e6e9;
+ --primary: #313237;
+ --footer-background: #fff;
+ --footer-border: #e2e6e9;
+ --footer-btn-bg-color: #fff;
+ --footer-border-btn: #b4bdc3;
+ --footer-text-color: #89939a;
+ --footer-link-hover: #313237;
+ --back-top: #89939a;
+ --footer-btn-hover: #313237;
+ --slider-btn-border: #b4bdc3;
+ --dropdown-bg: #fff;
+ --slider-bg: #fff;
+ --slider-border:#323542;
+ --bg-focus-list: #FAFBFC;
+ --card-bg-color: #fff;
+ --card-border: #E2E6E9;
+ --card-hover-border: #E2E6E9;
+ --box-shadow-hover: 0px 2px 16px 0px #0000001A;
+ --btn-add-cart-bg: #313237;
+ --fav-icon-border: #B4BDC3;
+ --fav-bg: #fff;
+ --btn-text-cart: #27AE60;
+ --btn-bg-hover: #313237;
+ --btn-pg-hover-border: #313237;
+ --btn-pg-border: #B4BDC3;
+ --pg-item: #000;
+ --pg-selected: #313237;
+ --pg-item-selected: #fff;
+ --fav-icon-count-border: #fff;
+ --fav-text-count: #fff;
+ --loader-color: #ffe135;
+ --back-btn-cart: #89939A;
+ --back-btn-cart-hover: #313237;
+ --checkout-btn-color: #fff;
+ --quantity-color: #000;
+ --border-cart-item: #E2E6E9;
+ --checkout-border: #E2E6E9;
+ --cart-btn-hover-border: #313237;
+ --overlay-bg-light: rgb(0 0 0 / 40%);
+ --overlay-bg-dark: rgb(255 255 255 / 10%);
+ --modal-bg-light: #fff;
+ --modal-bg-dark: #1e1e1e;
+ --modal-title-light: #333;
+ --modal-title-dark: #f0f0f0;
+ --modal-message-light: #666;
+ --modal-message-dark: #ccc;
+ --button-cancel-bg-light: #f0f0f0;
+ --button-cancel-bg-dark: #444;
+ --button-cancel-hover-light: #e0e0e0;
+ --button-cancel-hover-dark: #555;
+ --button-cancel-text-light: #555;
+ --button-cancel-text-dark: #eee;
+ --button-confirm-bg-light: #007bff;
+ --button-confirm-bg-dark: #4e8eff;
+ --button-confirm-hover-light: #0056b3;
+ --button-confirm-hover-dark: #3b6bcc;
+ --button-confirm-text-light: white;
+ --button-confirm-text-dark: white;
+ --modal-shadow-light: 0 4px 15px rgb(0 0 0 / 10%);
+ --modal-shadow-dark: 0 4px 15px rgb(0 0 0 / 30%);
+ --item-border: #C4C4C4;
+ --capacity-default-border: #B4BDC3;
+ --capacity-active-text: #FFF;
+
+};
+
+.light {
+ --c-background: #fff;
+ --header-text-color-active: #313237;
+ --header-text-color: #89939a;
+ --element-color: #e2e6e9;
+ --bg-hover-active: #e2e6e9;
+ --primary: #313237;
+ --footer-background: #fff;
+ --footer-border: #e2e6e9;
+ --footer-btn-bg-color: #fff;
+ --footer-border-btn: #b4bdc3;
+ --footer-text-color: #89939a;
+ --footer-link-hover: #313237;
+ --back-top: #89939a;
+ --slider-btn-border: #b4bdc3;
+ --slider-border: #b4bdc3;
+ --slider-bg: #fff;
+ --dropdown-bg: #fff;
+ --border-focus: #313237;
+ --bg-focus-list: #FAFBFC;
+ --card-bg-color: #fff;
+ --card-border: #E2E6E9;
+ --card-hover-border: #E2E6E9;
+ --box-shadow-hover: 0px 2px 16px 0px #0000001A;
+ --btn-add-cart-bg: #313237;
+ --fav-icon-border: #B4BDC3;
+ --fav-bg: #fff;
+ --fav-border-hover: #313237;
+ --cart-added: #ffff;
+ --cart-added-border: #E2E6E9;
+ --btn-text-cart: #27AE60;
+ --btn-bg-hover: #313237;
+ --btn-hover-shadow: 0px 3px 13px 0px #17203166;
+ --btn-pg-border: #B4BDC3;
+ --btn-pg-hover-border: #313237;
+ --pg-item: #000;
+ --pg-item-border: #E2E6E9;
+ --pg-selected: #313237;
+ --pg-item-selected: #fff;
+ --border-pg-selected: #313237;
+ --fav-icon-count-border: #fff;
+ --fav-text-count: #fff;
+ --loader-color: #f5c26b;
+ --back-btn-cart: #89939A;
+ --back-btn-cart-hover: #313237;
+ --checkout-btn-color: #fff;
+ --quantity-color: #000;
+ --border-cart-item: #E2E6E9;
+ --checkout-border: #E2E6E9;
+ --cart-btn-hover-border: #313237;
+ --overlay-bg: var(--overlay-bg-light);
+ --modal-bg: var(--modal-bg-light);
+ --modal-title: var(--modal-title-light);
+ --modal-message: var(--modal-message-light);
+ --button-cancel-bg: var(--button-cancel-bg-light);
+ --button-cancel-hover: var(--button-cancel-hover-light);
+ --button-cancel-text: var(--button-cancel-text-light);
+ --button-confirm-bg: #313237;
+ --button-confirm-hover: #414247;
+ --button-confirm-text: var(--button-confirm-text-light);
+ --modal-shadow: var(--modal-shadow-light);
+ --item-border: #C4C4C4;
+ --capacity-default-border: #B4BDC3;
+ --capacity-active-text: #FFF;
+};
+
+.dark {
+ --c-background: #0f1121;
+ --header-text-color-active: #f1f2f9;
+ --header-text-color: #75767f;
+ --element-color: #323542;
+ --bg-hover-active: #3b3e4a;
+ --primary: #f1f2f9;
+ --footer-background: #0f1121;
+ --footer-border: #3b3e4a;
+ --footer-btn-bg-color: #323542;
+ --footer-border-btn: #0f1121;
+ --footer-text-color: #f1f2f9;
+ --footer-link-hover: #89939a;
+ --back-top: #75767f;
+ --footer-button-bg-hover: #4a4d58;
+ --slider-btn-border: #323542;
+ --slider-bg: #323542;
+ --slider-border: #323542;
+ --dropdown-bg: #323542;
+ --border-focus: #905BFF;
+ --bg-focus-list: #323542;
+ --card-bg-color: #161827;
+ --card-border: #161827;
+ --card-hover-border: #323542;
+ --box-shadow-hover: 0px 2px 15px 0px #0000001A;
+ --btn-add-cart-bg: #905BFF;
+ --btn-bg-hover: #A378FF;
+ --fav-bg: #323542;
+ --fav-icon-border: #323542;
+ --fav-hover: #4A4D58;
+ --fav-active: #161827;
+ --btn-text-cart: #F1F2F9;
+ --btn-pg-border: #323542;
+ --btn-pg-bg-hover: #4A4D58;
+ --btn-pg-hover-border:#3B3E4A;
+ --pg-item: #F1F2F9;
+ --pg-bg-color: #161827;
+ --pg-bg-hover: #3B3E4A;
+ --pg-selected: #905BFF;
+ --pg-item-selected: #F1F2F9;
+ --fav-icon-count-border: #fff;
+ --fav-text-count: #F1F2F9;
+ --loader-color: #ff9800;
+ --back-btn-cart: #F1F2F9;
+ --back-btn-cart-hover: #905BFF;
+ --checkout-btn-color: #F1F2F9;
+ --quantity-color: #F1F2F9;
+ --border-cart-item: #161827;
+ --checkout-border: #3B3E4A;
+ --cart-btn-hover-border: #4A4D58;
+ --cart-added: #323542;
+ --overlay-bg: var(--overlay-bg-dark);
+ --modal-bg: var(--modal-bg-dark);
+ --modal-title: var(--modal-title-dark);
+ --modal-message: var(--modal-message-dark);
+ --button-cancel-bg: var(--button-cancel-bg-dark);
+ --button-cancel-hover: var(--button-cancel-hover-dark);
+ --button-cancel-text: var(--button-cancel-text-dark);
+ --button-confirm-bg: #905BFF;
+ --button-confirm-hover: #A378FF;
+ --button-confirm-text: var(--button-confirm-text-dark);
+ --modal-shadow: var(--modal-shadow-dark);
+ --item-border: #3B3E4A;
+ --capacity-default-border: #4A4D58;
+ --capacity-active-text: #0F1121;
+
+
+};
diff --git a/src/types/Categories.ts b/src/types/Categories.ts
new file mode 100644
index 0000000000..6e1033f878
--- /dev/null
+++ b/src/types/Categories.ts
@@ -0,0 +1,5 @@
+export enum Categories {
+ Phones = 'phones',
+ Tablets = 'tablets',
+ Accessories = 'accessories',
+}
diff --git a/src/types/Language.ts b/src/types/Language.ts
new file mode 100644
index 0000000000..5f9a1c729c
--- /dev/null
+++ b/src/types/Language.ts
@@ -0,0 +1,4 @@
+export enum LanguageType {
+ EN = 'en',
+ UK = 'uk',
+}
diff --git a/src/types/Product.ts b/src/types/Product.ts
new file mode 100644
index 0000000000..d14c6804cf
--- /dev/null
+++ b/src/types/Product.ts
@@ -0,0 +1,17 @@
+import { Categories } from './Categories';
+
+export interface Product {
+ id: number;
+ namespaceId: string;
+ category: Categories;
+ itemId: string;
+ name: string;
+ fullPrice: number;
+ price: number;
+ screen: string;
+ capacity: string;
+ color: string;
+ ram: string;
+ year: number;
+ image: string;
+}
diff --git a/src/types/ProductDetails.ts b/src/types/ProductDetails.ts
new file mode 100644
index 0000000000..e299f637b3
--- /dev/null
+++ b/src/types/ProductDetails.ts
@@ -0,0 +1,28 @@
+import { ProductColors } from '../constants/productColors';
+
+export type ProductDesription = {
+ title: string;
+ text: string[];
+};
+
+export interface ProductDetails {
+ id: string;
+ category: string;
+ namespaceId: string;
+ name: string;
+ capacityAvailable: string[];
+ capacity: string;
+ priceRegular: number;
+ priceDiscount: number;
+ colorsAvailable: (keyof ProductColors)[];
+ color: keyof ProductColors;
+ images: string[];
+ description: ProductDesription[];
+ screen: string;
+ resolution: string;
+ processor: string;
+ ram: string;
+ cell: string[];
+ camera: string;
+ zoom: string;
+}
diff --git a/src/types/UpdatedProduct.ts b/src/types/UpdatedProduct.ts
new file mode 100644
index 0000000000..c4bb3adae8
--- /dev/null
+++ b/src/types/UpdatedProduct.ts
@@ -0,0 +1,17 @@
+import { Categories } from './Categories';
+
+export type UpdatedProduct = {
+ id: number;
+ category: Categories;
+ itemId: string;
+ name: string;
+ fullPrice: number;
+ price: number;
+ screen: string;
+ capacity: string;
+ color: string;
+ ram: string;
+ year: number;
+ image: string;
+ quantity: number;
+};
diff --git a/src/utils/_fonts.scss b/src/utils/_fonts.scss
new file mode 100644
index 0000000000..9e98fd7c7b
--- /dev/null
+++ b/src/utils/_fonts.scss
@@ -0,0 +1,26 @@
+// fonts.scss
+
+@font-face {
+ font-family: Mont;
+ src: local('Mont Regular'), url('/fonts/Mont-Regular.otf') format('opentype');
+ font-weight: 500;
+ font-style: normal;
+}
+
+@font-face {
+ font-family: Mont;
+ src: local('Mont SemiBold'), url('/fonts/Mont-SemiBold.otf') format('opentype');
+ font-weight: 600;
+ font-style: normal;
+}
+
+@font-face {
+ font-family: Mont;
+ src: local('Mont Bold'), url('/fonts/Mont-Bold.otf') format('opentype');
+ font-weight: 800;
+ font-style: normal;
+}
+
+$mont-regular: mont, sans-serif;
+$mont-semiBold: mont, sans-serif;
+$mont-bold: mont, sans-serif;
diff --git a/src/utils/_reset.scss b/src/utils/_reset.scss
new file mode 100644
index 0000000000..8fdf79a3dd
--- /dev/null
+++ b/src/utils/_reset.scss
@@ -0,0 +1,31 @@
+iframe {
+ display: none;
+}
+
+* {
+ box-sizing: border-box;
+ margin: 0;
+ padding: 0;
+}
+
+ html {
+ width: 100%;
+ min-width: 320px;
+ min-height: 100vh;
+ scroll-behavior: smooth;
+}
+
+ul,
+li {
+ list-style: none;
+}
+
+a {
+ text-decoration: none;
+}
+
+button {
+ border-style: none;
+ cursor: pointer;
+}
+
diff --git a/src/utils/_variables.scss b/src/utils/_variables.scss
new file mode 100644
index 0000000000..dda89b0ff3
--- /dev/null
+++ b/src/utils/_variables.scss
@@ -0,0 +1,3 @@
+$black: #000;
+$white: #fff;
+$tomato-red: #EB5757;
diff --git a/src/utils/mixins.scss b/src/utils/mixins.scss
new file mode 100644
index 0000000000..7867508adf
--- /dev/null
+++ b/src/utils/mixins.scss
@@ -0,0 +1,50 @@
+@mixin on-tablet {
+ @media (width >= 640px) {
+ @content;
+ }
+}
+
+@mixin on-desktop {
+ @media (width >= 1200px) {
+ @content;
+ }
+}
+
+@mixin page-grid {
+ display: grid;
+ grid-template-columns: repeat(4, 1fr);
+ gap: 16px;
+
+ @include on-tablet {
+ grid-template-columns: repeat(12, 1fr);
+ }
+
+ @include on-desktop {
+ grid-template-columns: repeat(24, 1fr);
+ }
+}
+
+@mixin padding-content-inline-responsive() {
+ padding-inline: 16px;
+
+ @media (width >= 640px) {
+ padding-inline: 24px;
+ }
+
+ @media (width >= 1200px) {
+ padding-inline: 152px;
+ }
+}
+
+
+@mixin padding-inline-responsive() {
+ padding-inline: 32px;
+
+ @media (width >= 640px) {
+ padding-inline: 24px;
+ }
+
+ @media (width >= 1200px) {
+ padding-inline: 152px;
+ }
+}