diff --git a/declarations.d.ts b/declarations.d.ts
new file mode 100644
index 0000000000..a38a891d7d
--- /dev/null
+++ b/declarations.d.ts
@@ -0,0 +1,2 @@
+declare module '*.module.css';
+declare module '*.module.scss';
diff --git a/package-lock.json b/package-lock.json
index 98392dc382..43d59c209a 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -2421,9 +2421,9 @@
}
},
"@mate-academy/scripts": {
- "version": "1.7.9",
- "resolved": "https://registry.npmjs.org/@mate-academy/scripts/-/scripts-1.7.9.tgz",
- "integrity": "sha512-TDtSLf9BVwkaib4xpMB8r8VA18N6ABRpePGxpqk+aYOHcXq1DFwrzqCbOW9LyrOxWbqLVJBhP5exEgFXiaWhfw==",
+ "version": "1.9.4",
+ "resolved": "https://registry.npmjs.org/@mate-academy/scripts/-/scripts-1.9.4.tgz",
+ "integrity": "sha512-cFthaQW4MhphBbhLch1FmFA6xcdiLhRFNVgEVj5cUvjnw8VQ/I4Q/kKFT39y8yD9nNggAfNDrCTC1VB/wxpUPQ==",
"dev": true,
"requires": {
"@octokit/rest": "^17.11.2",
@@ -2777,10 +2777,28 @@
"source-map": "^0.7.3"
}
},
+ "@reduxjs/toolkit": {
+ "version": "2.2.7",
+ "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.2.7.tgz",
+ "integrity": "sha512-faI3cZbSdFb8yv9dhDTmGwclW0vk0z5o1cia+kf7gCbaCwHI5e+7tP57mJUv22pNcNbeA62GSrPpfrUfdXcQ6g==",
+ "requires": {
+ "immer": "^10.0.3",
+ "redux": "^5.0.1",
+ "redux-thunk": "^3.1.0",
+ "reselect": "^5.1.0"
+ },
+ "dependencies": {
+ "immer": {
+ "version": "10.1.1",
+ "resolved": "https://registry.npmjs.org/immer/-/immer-10.1.1.tgz",
+ "integrity": "sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw=="
+ }
+ }
+ },
"@remix-run/router": {
- "version": "1.15.3",
- "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.15.3.tgz",
- "integrity": "sha512-Oy8rmScVrVxWZVOpEF57ovlnhpZ8CCPlnIIumVcV9nFdiSIrus99+Lw78ekXyGvVDlIsFJbSfmSovJUhCWYV3w=="
+ "version": "1.19.1",
+ "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.19.1.tgz",
+ "integrity": "sha512-S45oynt/WH19bHbIXjtli6QmwNYvaz+vtnubvNpNDvUOoA/OWh6j1OikIP3G+v5GHdxyC6EXoChG3HgYGEUfcg=="
},
"@rollup/plugin-babel": {
"version": "5.3.1",
@@ -2868,9 +2886,9 @@
}
},
"@sinonjs/text-encoding": {
- "version": "0.7.2",
- "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.2.tgz",
- "integrity": "sha512-sXXKG+uL9IrKqViTtao2Ws6dy0znu9sOaP1di/jKGW1M6VssO8vlpXCQcpZ+jisQ1tTFAC5Jo/EOzFbggBagFQ==",
+ "version": "0.7.3",
+ "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.3.tgz",
+ "integrity": "sha512-DE427ROAphMQzU4ENbliGYrBSYPXF+TtLg9S8vzeA+OF4ZKzoDdzfL8sxuMUGS/lgRhM6j1URSk9ghf7Xo1tyA==",
"dev": true
},
"@surma/rollup-plugin-off-main-thread": {
@@ -3255,6 +3273,21 @@
"resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz",
"integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ=="
},
+ "@types/lodash": {
+ "version": "4.17.7",
+ "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.7.tgz",
+ "integrity": "sha512-8wTvZawATi/lsmNu10/j2hk1KEP0IvjubqPE3cu1Xz7xfXXt5oCq3SNUz4fMIP4XGF9Ky+Ue2tBA3hcS7LSBlA==",
+ "dev": true
+ },
+ "@types/lodash.debounce": {
+ "version": "4.0.9",
+ "resolved": "https://registry.npmjs.org/@types/lodash.debounce/-/lodash.debounce-4.0.9.tgz",
+ "integrity": "sha512-Ma5JcgTREwpLRwMM+XwBR7DaWe96nC38uCBDFKZWbNKD+osjVzdpnUSwBcqCptrp16sSOLBAUb50Car5I0TCsQ==",
+ "dev": true,
+ "requires": {
+ "@types/lodash": "*"
+ }
+ },
"@types/mime": {
"version": "1.3.5",
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz",
@@ -3473,6 +3506,11 @@
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="
},
+ "@types/use-sync-external-store": {
+ "version": "0.0.3",
+ "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz",
+ "integrity": "sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA=="
+ },
"@types/ws": {
"version": "8.5.10",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.10.tgz",
@@ -6653,13 +6691,13 @@
}
},
"eslint-plugin-prettier": {
- "version": "5.1.3",
- "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.1.3.tgz",
- "integrity": "sha512-C9GCVAs4Eq7ZC/XFQHITLiHJxQngdtraXaM+LoUFoFp/lHNl2Zn8f3WQbe9HvTBBQ9YnKFB0/2Ajdqwo5D1EAw==",
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.2.1.tgz",
+ "integrity": "sha512-gH3iR3g4JfF+yYPaJYkN7jEl9QbweL/YfkoRlNnuIEHEz1vHVlCmWOS+eGGiRuzHQXdJFCOTxRgvju9b8VUmrw==",
"dev": true,
"requires": {
"prettier-linter-helpers": "^1.0.0",
- "synckit": "^0.8.6"
+ "synckit": "^0.9.1"
}
},
"eslint-plugin-react": {
@@ -12998,26 +13036,35 @@
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
},
+ "react-redux": {
+ "version": "9.1.2",
+ "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.1.2.tgz",
+ "integrity": "sha512-0OA4dhM1W48l3uzmv6B7TXPCGmokUU4p1M44DGN2/D9a1FjVPukVjER1PcPX97jIg6aUeLq1XJo1IpfbgULn0w==",
+ "requires": {
+ "@types/use-sync-external-store": "^0.0.3",
+ "use-sync-external-store": "^1.0.0"
+ }
+ },
"react-refresh": {
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.11.0.tgz",
"integrity": "sha512-F27qZr8uUqwhWZboondsPx8tnC3Ct3SxZA3V5WyEvujRyyNv0VYPhoBg1gZ8/MV5tubQp76Trw8lTv9hzRBa+A=="
},
"react-router": {
- "version": "6.22.3",
- "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.22.3.tgz",
- "integrity": "sha512-dr2eb3Mj5zK2YISHK++foM9w4eBnO23eKnZEDs7c880P6oKbrjz/Svg9+nxqtHQK+oMW4OtjZca0RqPglXxguQ==",
+ "version": "6.26.1",
+ "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.26.1.tgz",
+ "integrity": "sha512-kIwJveZNwp7teQRI5QmwWo39A5bXRyqpH0COKKmPnyD2vBvDwgFXSqDUYtt1h+FEyfnE8eXr7oe0MxRzVwCcvQ==",
"requires": {
- "@remix-run/router": "1.15.3"
+ "@remix-run/router": "1.19.1"
}
},
"react-router-dom": {
- "version": "6.22.3",
- "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.22.3.tgz",
- "integrity": "sha512-7ZILI7HjcE+p31oQvwbokjk6OA/bnFxrhJ19n82Ex9Ph8fNAq+Hm/7KchpMGlTgWhUxRHMMCut+vEtNpWpowKw==",
+ "version": "6.26.1",
+ "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.26.1.tgz",
+ "integrity": "sha512-veut7m41S1fLql4pLhxeSW3jlqs+4MtjRLj0xvuCEXsxusJCbs6I8yn9BxzzDX2XDgafrccY6hwjmd/bL54tFw==",
"requires": {
- "@remix-run/router": "1.15.3",
- "react-router": "6.22.3"
+ "@remix-run/router": "1.19.1",
+ "react-router": "6.26.1"
}
},
"react-scripts": {
@@ -13226,6 +13273,11 @@
}
}
},
+ "react-swipeable": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/react-swipeable/-/react-swipeable-7.0.1.tgz",
+ "integrity": "sha512-RKB17JdQzvECfnVj9yDZsiYn3vH0eyva/ZbrCZXZR0qp66PBRhtg4F9yJcJTWYT5Adadi+x4NoG53BxKHwIYLQ=="
+ },
"react-transition-group": {
"version": "4.4.5",
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz",
@@ -13328,6 +13380,16 @@
}
}
},
+ "redux": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
+ "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w=="
+ },
+ "redux-thunk": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz",
+ "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw=="
+ },
"reflect.getprototypeof": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.6.tgz",
@@ -13572,6 +13634,11 @@
"resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
"integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ=="
},
+ "reselect": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz",
+ "integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w=="
+ },
"resolve": {
"version": "1.22.8",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz",
@@ -14911,9 +14978,9 @@
"integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw=="
},
"synckit": {
- "version": "0.8.8",
- "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.8.8.tgz",
- "integrity": "sha512-HwOKAP7Wc5aRGYdKH+dw0PRRpbO841v2DENBtjnR5HFWoiNByAl7vrx3p0G/rCyYXQsrxqtX48TImFtPcIHSpQ==",
+ "version": "0.9.1",
+ "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.9.1.tgz",
+ "integrity": "sha512-7gr8p9TQP6RAHusBOSLs46F4564ZrjV8xFmw5zCmgmhGUcw2hxsShhJ6CEiHQMgPDwAQ1fWHPM0ypc4RMAig4A==",
"dev": true,
"requires": {
"@pkgr/core": "^0.1.0",
@@ -14921,9 +14988,9 @@
},
"dependencies": {
"tslib": {
- "version": "2.6.2",
- "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz",
- "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==",
+ "version": "2.7.0",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz",
+ "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==",
"dev": true
}
}
@@ -15498,6 +15565,11 @@
"requires-port": "^1.0.0"
}
},
+ "use-sync-external-store": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.2.tgz",
+ "integrity": "sha512-PElTlVMwpblvbNqQ82d2n6RjStvdSoNe9FG28kNfz3WiXilJm4DdNkEzRhCZuIDwY8U08WVihhGR5iRqAwfDiw=="
+ },
"util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
diff --git a/package.json b/package.json
index 222c88a139..5975fa06b9 100644
--- a/package.json
+++ b/package.json
@@ -8,21 +8,27 @@
"dependencies": {
"@cypress/react18": "^2.0.0",
"@fortawesome/fontawesome-free": "^6.2.0",
+ "@reduxjs/toolkit": "^2.2.7",
"@types/react-transition-group": "^4.4.5",
"bulma": "^0.9.4",
"classnames": "^2.5.1",
+ "lodash.debounce": "^4.0.8",
"react": "^18.2.0",
"react-dom": "^18.2.0",
- "react-router-dom": "^6.22.3",
+ "react-redux": "^9.1.2",
+ "react-router-dom": "^6.26.1",
"react-scripts": "5.0.1",
- "react-transition-group": "^4.4.5"
+ "react-swipeable": "^7.0.1",
+ "react-transition-group": "^4.4.5",
+ "redux": "^5.0.1"
},
"devDependencies": {
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
"@mate-academy/eslint-config-react-typescript": "latest",
- "@mate-academy/scripts": "^1.7.9",
+ "@mate-academy/scripts": "^1.9.4",
"@mate-academy/students-ts-config": "latest",
"@mate-academy/stylelint-config": "latest",
+ "@types/lodash.debounce": "^4.0.9",
"@types/node": "^16.18.80",
"@types/react": "^18.2.55",
"@types/react-dom": "^18.2.19",
diff --git a/public/img/category-accessories.png b/public/img/category-accessories.png
index 67c5bfdb35..9ac6ee6318 100644
Binary files a/public/img/category-accessories.png and b/public/img/category-accessories.png differ
diff --git a/public/img/category-phones.png b/public/img/category-phones.png
index fd7616042f..0cb88ba4ee 100644
Binary files a/public/img/category-phones.png and b/public/img/category-phones.png differ
diff --git a/public/img/category-tablets.png b/public/img/category-tablets.png
index 57e33c5807..e5ccbf65a6 100644
Binary files a/public/img/category-tablets.png and b/public/img/category-tablets.png differ
diff --git a/public/img/favicon/favicon.png b/public/img/favicon/favicon.png
new file mode 100644
index 0000000000..5ef129f073
Binary files /dev/null and b/public/img/favicon/favicon.png differ
diff --git a/public/img/icons/CartIcon--DarkTheme.svg b/public/img/icons/CartIcon--DarkTheme.svg
new file mode 100644
index 0000000000..380aed0a0a
--- /dev/null
+++ b/public/img/icons/CartIcon--DarkTheme.svg
@@ -0,0 +1,5 @@
+
diff --git a/public/img/icons/CartIcon.svg b/public/img/icons/CartIcon.svg
new file mode 100644
index 0000000000..6deb3bf9b7
--- /dev/null
+++ b/public/img/icons/CartIcon.svg
@@ -0,0 +1,5 @@
+
diff --git a/public/img/icons/ChevronIcon--DarkTheme.svg b/public/img/icons/ChevronIcon--DarkTheme.svg
new file mode 100644
index 0000000000..9c6976541b
--- /dev/null
+++ b/public/img/icons/ChevronIcon--DarkTheme.svg
@@ -0,0 +1,3 @@
+
diff --git a/public/img/icons/ChevronIcon.svg b/public/img/icons/ChevronIcon.svg
new file mode 100644
index 0000000000..dfd84c3e92
--- /dev/null
+++ b/public/img/icons/ChevronIcon.svg
@@ -0,0 +1,3 @@
+
diff --git a/public/img/icons/CloseMenuIcon--DarkTheme.svg b/public/img/icons/CloseMenuIcon--DarkTheme.svg
new file mode 100644
index 0000000000..925e5fce49
--- /dev/null
+++ b/public/img/icons/CloseMenuIcon--DarkTheme.svg
@@ -0,0 +1,3 @@
+
diff --git a/public/img/icons/CloseMenuIcon.svg b/public/img/icons/CloseMenuIcon.svg
new file mode 100644
index 0000000000..78d418ab46
--- /dev/null
+++ b/public/img/icons/CloseMenuIcon.svg
@@ -0,0 +1,3 @@
+
diff --git a/public/img/icons/CrossIcon--DarkTheme.svg b/public/img/icons/CrossIcon--DarkTheme.svg
new file mode 100644
index 0000000000..aec415e35a
--- /dev/null
+++ b/public/img/icons/CrossIcon--DarkTheme.svg
@@ -0,0 +1,3 @@
+
diff --git a/public/img/icons/CrossIcon.svg b/public/img/icons/CrossIcon.svg
new file mode 100644
index 0000000000..61e7a40832
--- /dev/null
+++ b/public/img/icons/CrossIcon.svg
@@ -0,0 +1,3 @@
+
diff --git a/public/img/icons/FavoritesIcon--DarkTheme.svg b/public/img/icons/FavoritesIcon--DarkTheme.svg
new file mode 100644
index 0000000000..8fb5abef51
--- /dev/null
+++ b/public/img/icons/FavoritesIcon--DarkTheme.svg
@@ -0,0 +1,3 @@
+
diff --git a/public/img/icons/FavoritesIcon.svg b/public/img/icons/FavoritesIcon.svg
new file mode 100644
index 0000000000..ca57cfedd8
--- /dev/null
+++ b/public/img/icons/FavoritesIcon.svg
@@ -0,0 +1,3 @@
+
diff --git a/public/img/icons/HomeIcon--DarkTheme.svg b/public/img/icons/HomeIcon--DarkTheme.svg
new file mode 100644
index 0000000000..e16ca7d794
--- /dev/null
+++ b/public/img/icons/HomeIcon--DarkTheme.svg
@@ -0,0 +1,4 @@
+
diff --git a/public/img/icons/HomeIcon.svg b/public/img/icons/HomeIcon.svg
new file mode 100644
index 0000000000..474476cb02
--- /dev/null
+++ b/public/img/icons/HomeIcon.svg
@@ -0,0 +1,4 @@
+
diff --git a/public/img/icons/LogoIcon--DarkTheme.svg b/public/img/icons/LogoIcon--DarkTheme.svg
new file mode 100644
index 0000000000..d59f941639
--- /dev/null
+++ b/public/img/icons/LogoIcon--DarkTheme.svg
@@ -0,0 +1,25 @@
+
diff --git a/public/img/icons/LogoIcon.svg b/public/img/icons/LogoIcon.svg
new file mode 100644
index 0000000000..0a2d076bef
--- /dev/null
+++ b/public/img/icons/LogoIcon.svg
@@ -0,0 +1,25 @@
+
diff --git a/public/img/icons/MenuIcon--DarkTheme.svg b/public/img/icons/MenuIcon--DarkTheme.svg
new file mode 100644
index 0000000000..c8c52c08a9
--- /dev/null
+++ b/public/img/icons/MenuIcon--DarkTheme.svg
@@ -0,0 +1,5 @@
+
diff --git a/public/img/icons/MenuIcon.svg b/public/img/icons/MenuIcon.svg
new file mode 100644
index 0000000000..2c535f4586
--- /dev/null
+++ b/public/img/icons/MenuIcon.svg
@@ -0,0 +1,5 @@
+
diff --git a/public/img/icons/MinusIcon--DarkTheme.svg b/public/img/icons/MinusIcon--DarkTheme.svg
new file mode 100644
index 0000000000..7ca53e577a
--- /dev/null
+++ b/public/img/icons/MinusIcon--DarkTheme.svg
@@ -0,0 +1,3 @@
+
diff --git a/public/img/icons/MinusIcon.svg b/public/img/icons/MinusIcon.svg
new file mode 100644
index 0000000000..97c41038ac
--- /dev/null
+++ b/public/img/icons/MinusIcon.svg
@@ -0,0 +1,3 @@
+
diff --git a/public/img/icons/PlusIcon--DarkTheme.svg b/public/img/icons/PlusIcon--DarkTheme.svg
new file mode 100644
index 0000000000..aa791a47ad
--- /dev/null
+++ b/public/img/icons/PlusIcon--DarkTheme.svg
@@ -0,0 +1,3 @@
+
diff --git a/public/img/icons/PlusIcon.svg b/public/img/icons/PlusIcon.svg
new file mode 100644
index 0000000000..ab3c34061b
--- /dev/null
+++ b/public/img/icons/PlusIcon.svg
@@ -0,0 +1,3 @@
+
diff --git a/public/img/icons/SelectedToFaforitesIcon.svg b/public/img/icons/SelectedToFaforitesIcon.svg
new file mode 100644
index 0000000000..7138d7522b
--- /dev/null
+++ b/public/img/icons/SelectedToFaforitesIcon.svg
@@ -0,0 +1,3 @@
+
diff --git a/public/index.html b/public/index.html
index 4b622dad39..b75336a005 100644
--- a/public/index.html
+++ b/public/index.html
@@ -4,6 +4,7 @@
Phone catalog
+
diff --git a/src/App.module.scss b/src/App.module.scss
new file mode 100644
index 0000000000..c988ac9df2
--- /dev/null
+++ b/src/App.module.scss
@@ -0,0 +1,46 @@
+@import './styles/main';
+
+* {
+ box-sizing: border-box;
+ margin: 0;
+ padding: 0;
+}
+
+body {
+ overflow-y: scroll;
+ min-width: 320px;
+ background-color: var(--color-background);
+ color: var(--color-primary);
+}
+
+.app {
+ display: flex;
+ flex-direction: column;
+ min-height: 100vh;
+}
+
+.container {
+ flex: 1;
+ padding-top: 24px;
+ padding-bottom: 80px;
+
+ @include on-tablet {
+ padding-top: 32px;
+ }
+
+ @include on-desktop {
+ padding-top: 52px;
+ }
+}
+
+.containerHidden {
+ opacity: 0;
+}
+
+a img {
+ transition: transform $transition-duration ease;
+}
+
+a:hover img {
+ transform: scale(1.1);
+}
diff --git a/src/App.scss b/src/App.scss
deleted file mode 100644
index 71bc413aad..0000000000
--- a/src/App.scss
+++ /dev/null
@@ -1 +0,0 @@
-// not empty
diff --git a/src/App.tsx b/src/App.tsx
index 372e4b4206..1b846fac3a 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -1,7 +1,24 @@
-import './App.scss';
+import { Outlet } from 'react-router-dom';
+import React, { useState } from 'react';
+import classNames from 'classnames';
+import styles from './App.module.scss';
+import { Header } from './components/Header';
+import { Footer } from './components/Footer';
-export const App = () => (
-
-
Product Catalog
-
-);
+export const App: React.FC = () => {
+ const [isMenuOpen, setIsMenuOpen] = useState(false);
+
+ return (
+
+ );
+};
diff --git a/src/Root.tsx b/src/Root.tsx
new file mode 100644
index 0000000000..e0262748bd
--- /dev/null
+++ b/src/Root.tsx
@@ -0,0 +1,36 @@
+import { HashRouter as Router, Route, Routes } from 'react-router-dom';
+import { App } from './App';
+import { ThemeProvider } from './context/ThemeContext';
+import { AppProvider } from './context/AppContext';
+import { HomePage } from './modules/HomePage';
+import { PhonesPage } from './modules/PhonesPage';
+import { TabletsPage } from './modules/TabletsPage';
+import { AccessoriesPage } from './modules/AccessoriesPage';
+import { CartPage } from './modules/CartPage';
+import { FavoritesPage } from './modules/FavoritesPage';
+import { ProductDetailsPage } from './modules/ProductDetailsPage';
+import { NotFoundPage } from './modules/NotFoundPage';
+
+export const Root = () => (
+
+
+
+
+ }>
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ }
+ />
+ } />
+
+
+
+
+
+);
diff --git a/src/components/ActionButtons/ActionButtons.module.scss b/src/components/ActionButtons/ActionButtons.module.scss
new file mode 100644
index 0000000000..81a101e33a
--- /dev/null
+++ b/src/components/ActionButtons/ActionButtons.module.scss
@@ -0,0 +1,59 @@
+@import '../../styles/main';
+
+.buttons {
+ display: flex;
+ flex-direction: row;
+ justify-content: center;
+ align-items: center;
+ gap: 8px;
+}
+
+.buttonCard {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ cursor: pointer;
+ height: 40px;
+ width: 100%;
+ border: 0;
+ background-color: var(--color-button);
+ transition: transform $transition-duration ease;
+
+ &:hover {
+ transform: scale(1.05);
+ box-shadow: 0 3px 13px 0 #17203166;
+ background-color: var(--color-button-hover);
+ }
+}
+
+.buttonText {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ cursor: pointer;
+
+ @extend %body-text;
+
+ color: var(--color-white);
+}
+
+.buttonFavorite {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ width: 40px;
+ height: 40px;
+ flex-shrink: 0;
+ flex-grow: 0;
+
+ background-color: var(--color-icons);
+ border: 1px solid var(--color-surface);
+ box-sizing: border-box;
+
+ cursor: pointer;
+
+ &:hover {
+ border: 1px solid var(--color-primary);
+ background-color: var(--color-hover);
+ }
+}
diff --git a/src/components/ActionButtons/ActionButtons.tsx b/src/components/ActionButtons/ActionButtons.tsx
new file mode 100644
index 0000000000..e9cdf93efc
--- /dev/null
+++ b/src/components/ActionButtons/ActionButtons.tsx
@@ -0,0 +1,70 @@
+import React from 'react';
+import { useAppContext } from '../../context/AppContext';
+import { useTheme } from '../../context/ThemeContext';
+import { Product } from '../../types/Product';
+import { getFavoritesIconSrc } from '../../servises/iconSrc';
+import styles from './ActionButtons.module.scss';
+import { BASE_URL } from '../../utils/const';
+
+type Props = {
+ product: Product;
+};
+
+export const ActionButtons: React.FC = ({ product }) => {
+ const {
+ addToFavorites,
+ removeFromFavorites,
+ favorites,
+ cart,
+ addToCart,
+ removeFromCart,
+ } = useAppContext();
+ const { theme } = useTheme();
+
+ const isFavorite = favorites.some(favProduct => favProduct.id === product.id);
+
+ const handleFavoriteClick = () => {
+ if (isFavorite) {
+ removeFromFavorites(product.id);
+ } else {
+ addToFavorites(product);
+ }
+ };
+
+ const isProductInCart = cart.some(
+ cartItem => cartItem.product.id === product.id,
+ );
+
+ const handleCartClick = () => {
+ if (isProductInCart) {
+ removeFromCart(product.id);
+ } else {
+ addToCart(product);
+ }
+ };
+
+ const favoritesIconSrc = () => {
+ if (isFavorite) {
+ return `${BASE_URL}/img/icons/SelectedToFaforitesIcon.svg`;
+ }
+
+ return getFavoritesIconSrc(theme);
+ };
+
+ return (
+
+
+
+
+ );
+};
diff --git a/src/components/ActionButtons/index.ts b/src/components/ActionButtons/index.ts
new file mode 100644
index 0000000000..64917b7b7d
--- /dev/null
+++ b/src/components/ActionButtons/index.ts
@@ -0,0 +1 @@
+export * from './ActionButtons';
diff --git a/src/components/Breadcrumbs/Breadcrumbs.module.scss b/src/components/Breadcrumbs/Breadcrumbs.module.scss
new file mode 100644
index 0000000000..c984036041
--- /dev/null
+++ b/src/components/Breadcrumbs/Breadcrumbs.module.scss
@@ -0,0 +1,35 @@
+@import '../../styles/main';
+
+.breadcrumbs {
+ grid-column: 1 / -1;
+ display: flex;
+ overflow: hidden;
+ gap: 8px;
+ height: 16px;
+}
+
+.chevronSpan {
+ display: block;
+}
+
+.chevronIcon {
+ transform: rotate(180deg);
+ opacity: 0.5;
+}
+
+.label {
+ display: flex;
+ text-align: center;
+ padding-top: 2px;
+ height: 15px;
+ text-decoration: none;
+
+ @extend %small-text;
+
+ color: var(--color-secondary);
+}
+
+
+.active {
+ color: var(--color-primary);
+}
diff --git a/src/components/Breadcrumbs/Breadcrumbs.tsx b/src/components/Breadcrumbs/Breadcrumbs.tsx
new file mode 100644
index 0000000000..2db76b6b60
--- /dev/null
+++ b/src/components/Breadcrumbs/Breadcrumbs.tsx
@@ -0,0 +1,82 @@
+import React from 'react';
+import { Link, useLocation } from 'react-router-dom';
+import styles from './Breadcrumbs.module.scss';
+import { getChevronIconSrc, getHomeIconSrc } from '../../servises/iconSrc';
+import { useTheme } from '../../context/ThemeContext';
+import classNames from 'classnames';
+
+type BreadcrumbsProps = {
+ product?: {
+ category: string;
+ name: string;
+ };
+};
+
+export const Breadcrumbs: React.FC = ({ product }) => {
+ const { theme } = useTheme();
+ const { pathname } = useLocation();
+ const homeIconSrs = getHomeIconSrc(theme);
+ const chevronIconSrc = getChevronIconSrc(theme);
+
+ function capitalize(str: string) {
+ return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase();
+ }
+
+ const getBreadcrumbs = () => {
+ if (product) {
+ return [
+ { path: `/${product.category}`, label: capitalize(product.category) },
+ { label: product.name },
+ ];
+ } else {
+ const paths = pathname.split('/').filter(Boolean);
+
+ return paths.map((path, index, array) => ({
+ path: `/${array.slice(0, index + 1).join('/')}`,
+ label: capitalize(path),
+ }));
+ }
+ };
+
+ const breadcrumbs = getBreadcrumbs();
+
+ return (
+
+ );
+};
diff --git a/src/components/Breadcrumbs/index.tsx b/src/components/Breadcrumbs/index.tsx
new file mode 100644
index 0000000000..ce977548b1
--- /dev/null
+++ b/src/components/Breadcrumbs/index.tsx
@@ -0,0 +1 @@
+export * from './Breadcrumbs';
diff --git a/src/components/Dropdown/Dropdown.module.scss b/src/components/Dropdown/Dropdown.module.scss
new file mode 100644
index 0000000000..2209f170e1
--- /dev/null
+++ b/src/components/Dropdown/Dropdown.module.scss
@@ -0,0 +1,42 @@
+@import '../../styles/main';
+
+.label {
+ display: flex;
+ gap: 8px;
+ width: 100%;
+ flex-direction: column;
+ position: relative;
+ color: var(--color-secondary);
+
+ @extend %small-text;
+
+ &::before {
+ content: '';
+ position: absolute;
+ display: block;
+ width: 6px;
+ height: 6px;
+ border-right: 1px solid var(--color-secondary);
+ border-top: 1px solid var(--color-secondary);
+ transform: rotate(135deg);
+ bottom: 18px;
+ right: 12px;
+
+ }
+}
+
+.select {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ height: 40px;
+ width: 100%;
+ padding: 0 12px;
+ appearance: none;
+ outline: none;
+ border: 1px solid var(--color-elements);
+ background-color: var(--color-filter-bg);
+ color: var(--color-primary);
+
+ @extend %body-text;
+}
diff --git a/src/components/Dropdown/Dropdown.tsx b/src/components/Dropdown/Dropdown.tsx
new file mode 100644
index 0000000000..c91153e73e
--- /dev/null
+++ b/src/components/Dropdown/Dropdown.tsx
@@ -0,0 +1,38 @@
+import styles from './Dropdown.module.scss';
+
+type Option = {
+ value: string;
+ label: string;
+};
+
+type DropdownProps = {
+ label: string;
+ value: string | number;
+ options: Option[];
+ onChange: (event: React.ChangeEvent) => void;
+};
+
+export const Dropdown: React.FC = ({
+ label,
+ value,
+ options,
+ onChange,
+}) => {
+ return (
+
+ );
+};
diff --git a/src/components/Footer/Footer.module.scss b/src/components/Footer/Footer.module.scss
new file mode 100644
index 0000000000..266d38f8d4
--- /dev/null
+++ b/src/components/Footer/Footer.module.scss
@@ -0,0 +1,127 @@
+@import '../../styles/main';
+
+.footer {
+ display: flex;
+ width: 100%;
+ border-top: 1px solid var(--color-elements);
+ box-shadow: 0 -1px 0 0 var(--color-elements);
+
+ background-color: var(--color-background);
+
+ @include on-tablet {
+ height: 96px;
+ }
+}
+
+.wrapper {
+ display: flex;
+ width: 100%;
+ flex-direction: column;
+ padding: 32px 16px;
+ gap: 32px;
+
+ @include on-tablet {
+ @include section-grid;
+
+ align-items: center;
+ justify-content: center;
+ }
+
+
+}
+
+.logoLink {
+ display: flex;
+ width: 100%;
+ align-items: center;
+
+ @include on-tablet {
+ grid-column: span 4;
+ }
+
+ @include on-desktop {
+ grid-column: span 8;
+ }
+}
+
+.logo {
+ width: 89px;
+ height: 32px;
+}
+
+.nav {
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+ width: 100%;
+
+
+ @include on-tablet {
+ grid-column: span 4;
+ width: 100%;
+ flex-direction: row;
+ justify-content: space-between;
+ }
+
+ @include on-desktop {
+ grid-column: span 8;
+ }
+}
+
+.item {
+ @extend %uppercase-text;
+
+ color: var(--color-secondary);
+
+ cursor: pointer;
+
+ &:hover {
+ color: var(--color-primary);
+ }
+}
+
+.backToTop {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+
+ @include on-tablet {
+ justify-content: flex-end;
+ grid-column: span 4;
+ }
+
+ @include on-desktop {
+ grid-column: span 8;
+ }
+
+}
+
+.backToTopText {
+ @extend %small-text;
+
+ color: var(--color-secondary);
+ text-decoration: none;
+ margin-right: 16px;
+}
+
+.backToTopButton {
+ display: flex;
+ width: 32px;
+ height: 32px;
+ background-color: var(--color-icons);
+ border: 1px solid var(--color-surface);
+ border-radius: 5%;
+ cursor: pointer;
+ justify-content: center;
+ align-items: center;
+
+ &:hover {
+ border: 1px solid var(--color-primary);
+ background-color: var(--color-hover);
+ }
+}
+
+.backToTopIcon {
+ transform : rotate(90deg)
+}
+
diff --git a/src/components/Footer/Footer.tsx b/src/components/Footer/Footer.tsx
new file mode 100644
index 0000000000..284818f0d4
--- /dev/null
+++ b/src/components/Footer/Footer.tsx
@@ -0,0 +1,92 @@
+import React, { useEffect, useState } from 'react';
+import { Link, NavLink } from 'react-router-dom';
+import styles from './Footer.module.scss';
+import { useTheme } from '../../context/ThemeContext';
+import { getChevronIconSrc, getLogoIconSrc } from '../../servises/iconSrc';
+
+const Footer: React.FC = () => {
+ const { theme } = useTheme();
+
+ const [showBackToTop, setShowBackToTop] = useState(false);
+
+ useEffect(() => {
+ const observer = new MutationObserver(() => {
+ const contentHeight = document.documentElement.scrollHeight;
+ const viewportHeight = window.innerHeight;
+
+ setShowBackToTop(contentHeight > viewportHeight);
+ });
+
+ observer.observe(document.body, {
+ childList: true,
+ subtree: true,
+ attributes: true,
+ characterData: true,
+ });
+
+ return () => observer.disconnect();
+ }, []);
+
+ const scrollToTop = () => {
+ window.scrollTo({
+ top: 0,
+ behavior: 'smooth',
+ });
+ };
+
+ const logoImgSrc = getLogoIconSrc(theme);
+ const backToTopImgSrc = getChevronIconSrc(theme);
+
+ return (
+
+ );
+};
+
+export default Footer;
diff --git a/src/components/Footer/index.ts b/src/components/Footer/index.ts
new file mode 100644
index 0000000000..da94c29367
--- /dev/null
+++ b/src/components/Footer/index.ts
@@ -0,0 +1 @@
+export { default as Footer } from './Footer';
diff --git a/src/components/Header/Header.module.scss b/src/components/Header/Header.module.scss
new file mode 100644
index 0000000000..3be38b9cbf
--- /dev/null
+++ b/src/components/Header/Header.module.scss
@@ -0,0 +1,194 @@
+@import '../../styles/main';
+
+.header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ background-color: var(--color-background);
+ width: 100%;
+ height: $height-on-tablet;
+ gap: 16px;
+
+
+ position: sticky;
+ top: 0;
+ z-index: 1;
+
+ box-shadow: 0 1px 0 0 var(--color-elements);
+
+ @include on-desktop {
+ gap: 24px;
+ height: $height-on-desktop;
+ }
+}
+
+.logoLink {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ height: 100%;
+ padding-inline: 16px;
+
+ @include on-desktop {
+ padding-inline: 24px;
+ }
+}
+
+.logo {
+ width: 64px;
+ height: 22px;
+
+ @include on-desktop {
+ width: 80px;
+ height: 28px;
+ }
+}
+
+.container {
+ display: none;
+
+ @include on-tablet {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ width: 100%;
+ }
+}
+
+.nav {
+ display: flex;
+ align-items: center;
+}
+
+.navItem {
+ display: flex;
+ position: relative;
+ align-items: center;
+ margin-right: 32px;
+ height: $height-on-tablet;
+
+ color: var(--color-secondary);
+
+ @extend %uppercase-text;
+
+ cursor: pointer;
+
+ &:hover {
+ color: var(--color-primary);
+ }
+
+ &:last-child {
+ margin-right: 0;
+ }
+
+ @include on-desktop {
+ margin-right: 64px;
+ height: $height-on-desktop;
+ }
+}
+
+.actionsContainer {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 8px;
+
+ @include middle-screen {
+ gap: 16px;
+ }
+}
+
+.actions {
+ display: flex;
+ align-items: center;
+}
+
+.actionItem {
+ display: flex;
+ position: relative;
+ align-items: center;
+ justify-content: center;
+ cursor: pointer;
+
+ width: $with-icons-tablet;
+ height: $height-on-tablet;
+
+ flex-shrink: 0;
+ flex-grow: 0;
+
+ box-shadow: -1px 0 0 0 var(--color-elements);
+
+ @include on-desktop {
+ width: $with-icons-desktop;
+ height: $height-on-desktop;
+ }
+}
+
+.actionIcon {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ position: relative;
+ width: 28px;
+ height: 28px;
+
+
+}
+
+.count {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ position: absolute;
+ top: 0;
+ left: 14px;
+ width: 14px;
+ height: 14px;
+ border-radius: 50%;
+ border: 1px solid var(--color-background);
+ background-color: var(--color-red);
+}
+
+.countText {
+ color: #F1F2F9;
+ font-size: 9px;
+ font-weight: 700;
+ line-height: 11.5px;
+ text-align: center;
+}
+
+.mobile {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+
+ @include on-tablet {
+ display: none;
+ }
+}
+
+.mobileButton {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ height: $height-on-tablet;
+ background-color: transparent;
+ border: none;
+ width: $with-icons-tablet;
+ box-shadow: -1px 0 0 0 var(--color-elements);
+ cursor: pointer;
+}
+
+.isActive {
+ color: var(--color-primary);
+
+ &::after {
+ position: absolute;
+ content: "";
+ bottom: 0;
+ left: 0;
+ right: 0;
+ height: 3px;
+ background-color: var(--color-primary);
+ }
+}
diff --git a/src/components/Header/Header.tsx b/src/components/Header/Header.tsx
new file mode 100644
index 0000000000..5af6395e3f
--- /dev/null
+++ b/src/components/Header/Header.tsx
@@ -0,0 +1,140 @@
+/* eslint-disable import/no-extraneous-dependencies */
+import { NavLink, Link } from 'react-router-dom';
+import classNames from 'classnames';
+import { useTheme } from '../../context/ThemeContext';
+import { HeaderProps } from '../../types/Header';
+import styles from './Header.module.scss';
+import { OverlayMenu } from './components/OverlayMenu';
+import { ToggleTheme } from './components/ToggleTheme';
+import {
+ getMenuIconSrc,
+ getLogoIconSrc,
+ getCartIconSrc,
+ getFavoritesIconSrc,
+} from '../../servises/iconSrc';
+import { useAppContext } from '../../context/AppContext';
+import { Search } from '../Search/Search';
+
+const Header: React.FC = ({ isMenuOpen, setIsMenuOpen }) => {
+ const { theme } = useTheme();
+ const { cart, favorites } = useAppContext();
+
+ const toggleIsMenuOpen = () => {
+ setIsMenuOpen((prev: boolean) => !prev);
+ };
+
+ const menuIconSrc = getMenuIconSrc(isMenuOpen, theme);
+ const logoIconSrs = getLogoIconSrc(theme);
+ const cartIconSrc = getCartIconSrc(theme);
+ const favoritesIconSrc = getFavoritesIconSrc(theme);
+
+ return (
+
+ setIsMenuOpen(false)}
+ >
+
+
+
+
+
+
+
+
+
+
+
+ classNames(styles.actionItem, { [styles.isActive]: isActive })
+ }
+ >
+
+
+ {favorites.length > 0 && (
+
+ {favorites.length}
+
+ )}
+
+
+
+ classNames(styles.actionItem, { [styles.isActive]: isActive })
+ }
+ >
+
+
+ {cart.length > 0 && (
+
+
+ {cart.reduce((sum, item) => sum + item.quantity, 0)}
+
+
+ )}
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default Header;
diff --git a/src/components/Header/components/OverlayMenu/OverlayMenu.module.scss b/src/components/Header/components/OverlayMenu/OverlayMenu.module.scss
new file mode 100644
index 0000000000..a13e1354bc
--- /dev/null
+++ b/src/components/Header/components/OverlayMenu/OverlayMenu.module.scss
@@ -0,0 +1,104 @@
+@import '../../../../styles/main';
+
+.menuOverlay {
+ display: none;
+ position: fixed;
+ top: 72px;
+ left: 0;
+ right: 0;
+ height: 100vh;
+ z-index: 1;
+
+ background-color: var(--color-background);
+}
+
+.show {
+ display: flex;
+ justify-content: center;
+}
+
+.nav {
+ display: flex;
+ align-items: center;
+ flex-direction: column;
+ gap: 16px;
+}
+
+.item {
+ @extend %uppercase-text;
+
+ color: var(--color-secondary);
+ padding: 8px 0;
+}
+
+.actions {
+ display: flex;
+ width: 100%;
+ position: fixed;
+ bottom: 0;
+}
+
+.action {
+ display: flex;
+ flex: 1;
+ height: 64px;
+ align-items: center;
+ justify-content: center;
+ color: var(--color-secondary);
+ box-shadow: 0 -1px 0 0 var(--color-elements), 1px 0 0 0 var(--color-elements);
+}
+
+.actionIcon {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ position: relative;
+ width: 28px;
+ height: 28px;
+}
+
+.count {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ position: absolute;
+ top: 0;
+ left: 14px;
+ width: 14px;
+ height: 14px;
+ border-radius: 50%;
+ border: 1px solid var(--color-background);
+ background-color: var(--color-red);
+
+}
+
+.countText {
+ color: #F1F2F9;
+ font-size: 9px;
+ font-weight: 700;
+ line-height: 11.5px;
+ text-align: center;
+}
+
+.item,
+.action {
+ position: relative;
+
+ &:hover {
+ color: var(--color-primary);
+ }
+}
+
+.isActive {
+ color: var(--color-primary);
+
+ &::after {
+ position: absolute;
+ content: "";
+ bottom: 0;
+ left: 0;
+ right: 0;
+ height: 2px;
+ background-color: var(--color-primary);
+ }
+}
diff --git a/src/components/Header/components/OverlayMenu/OverlayMenu.tsx b/src/components/Header/components/OverlayMenu/OverlayMenu.tsx
new file mode 100644
index 0000000000..5729a22df5
--- /dev/null
+++ b/src/components/Header/components/OverlayMenu/OverlayMenu.tsx
@@ -0,0 +1,98 @@
+import React from 'react';
+import { NavLink } from 'react-router-dom';
+import classNames from 'classnames';
+import styles from './OverlayMenu.module.scss';
+import { OverlayMenuProps } from '../../../../types/OverlayMenu';
+
+export const OverlayMenu: React.FC = ({
+ isMenuOpen,
+ toggleIsMenuOpen,
+ cartIconSrc,
+ favoritesIconSrc,
+ favorites,
+ cart,
+}) => {
+ return (
+
+
+
+
+ classNames(styles.action, { [styles.isActive]: isActive })
+ }
+ >
+
+
+ {favorites.length > 0 && (
+
+ {favorites.length}
+
+ )}
+
+
+
+ classNames(styles.action, { [styles.isActive]: isActive })
+ }
+ >
+
+
+ {cart.length > 0 && (
+
+
+ {cart.reduce((sum, item) => sum + item.quantity, 0)}
+
+
+ )}
+
+
+
+
+ );
+};
diff --git a/src/components/Header/components/OverlayMenu/index.ts b/src/components/Header/components/OverlayMenu/index.ts
new file mode 100644
index 0000000000..7e36f742e8
--- /dev/null
+++ b/src/components/Header/components/OverlayMenu/index.ts
@@ -0,0 +1 @@
+export * from './OverlayMenu';
diff --git a/src/components/Header/components/ToggleTheme/ToggleTheme.module.scss b/src/components/Header/components/ToggleTheme/ToggleTheme.module.scss
new file mode 100644
index 0000000000..728922823e
--- /dev/null
+++ b/src/components/Header/components/ToggleTheme/ToggleTheme.module.scss
@@ -0,0 +1,33 @@
+@import '../../../../styles/main';
+
+.themeButton {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ box-shadow: -1px 0 0 0 var(--color-elements), 1px 0 0 0 var(--color-elements);
+
+ @extend %uppercase-text;
+
+ color: var(--color-secondary);
+
+ cursor: pointer;
+ background-color: transparent;
+ border: none;
+
+ padding: 0;
+
+ flex-shrink: 0;
+ flex-grow: 0;
+
+ width: $with-icons-tablet;
+ height: $height-on-tablet;
+
+ &:hover {
+ color: var(--color-primary);
+ }
+
+ @include on-desktop {
+ width: $with-icons-desktop;
+ height: $height-on-desktop;
+ }
+}
diff --git a/src/components/Header/components/ToggleTheme/ToggleTheme.tsx b/src/components/Header/components/ToggleTheme/ToggleTheme.tsx
new file mode 100644
index 0000000000..c18a366977
--- /dev/null
+++ b/src/components/Header/components/ToggleTheme/ToggleTheme.tsx
@@ -0,0 +1,18 @@
+import React from 'react';
+import { useTheme } from '../../../../context/ThemeContext';
+import styles from './ToggleTheme.module.scss';
+
+export const ToggleTheme: React.FC = () => {
+ const { theme, toggleTheme } = useTheme();
+
+ return (
+
+ );
+};
diff --git a/src/components/Header/components/ToggleTheme/index.tsx b/src/components/Header/components/ToggleTheme/index.tsx
new file mode 100644
index 0000000000..eae4e1cfdc
--- /dev/null
+++ b/src/components/Header/components/ToggleTheme/index.tsx
@@ -0,0 +1 @@
+export * from './ToggleTheme';
diff --git a/src/components/Header/index.tsx b/src/components/Header/index.tsx
new file mode 100644
index 0000000000..5653319dec
--- /dev/null
+++ b/src/components/Header/index.tsx
@@ -0,0 +1 @@
+export { default as Header } from './Header';
diff --git a/src/components/Loader/Loader.module.scss b/src/components/Loader/Loader.module.scss
new file mode 100644
index 0000000000..9ae4116303
--- /dev/null
+++ b/src/components/Loader/Loader.module.scss
@@ -0,0 +1,24 @@
+.loader {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+}
+
+.loaderContent {
+ border-radius: 50%;
+ width: 5em;
+ height: 5em;
+ margin: 1em auto;
+ border: 0.3em solid #ddd;
+ border-left-color: #000;
+ animation: load8 1.2s infinite linear;
+}
+
+@keyframes load8 {
+ 0% {
+ transform: rotate(0deg);
+ }
+ 100% {
+ transform: rotate(360deg);
+ }
+}
diff --git a/src/components/Loader/Loader.tsx b/src/components/Loader/Loader.tsx
new file mode 100644
index 0000000000..3af23d162d
--- /dev/null
+++ b/src/components/Loader/Loader.tsx
@@ -0,0 +1,8 @@
+import React from 'react';
+import styles from './Loader.module.scss';
+
+export const Loader: React.FC = () => (
+
+);
diff --git a/src/components/Loader/index.tsx b/src/components/Loader/index.tsx
new file mode 100644
index 0000000000..d5ce981151
--- /dev/null
+++ b/src/components/Loader/index.tsx
@@ -0,0 +1 @@
+export * from './Loader';
diff --git a/src/components/Pagination/Pagination.module.scss b/src/components/Pagination/Pagination.module.scss
new file mode 100644
index 0000000000..07b74717b6
--- /dev/null
+++ b/src/components/Pagination/Pagination.module.scss
@@ -0,0 +1,51 @@
+@import '../../styles/main';
+
+.pagination {
+ grid-column: 1 / -1;
+ display: flex;
+ gap: 8px;
+ align-items: center;
+ justify-content: center;
+}
+
+.button {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ width: 32px;
+ height: 32px;
+ flex-shrink: 0;
+ background-color: var(--color-icons);
+ border: 1px solid var(--color-surface);
+ color: var(--color-primary);
+ cursor: pointer;
+
+ &:hover {
+ border: 1px solid var(--color-primary);
+ background-color: var(--color-hover);
+ }
+}
+
+.active {
+ cursor: pointer;
+ border: 1px solid var(--color-primary);
+ background-color: var(--color-primary);
+ color: var(--color-background);
+
+ &:hover {
+ background-color: var(--color-primary);
+ }
+}
+
+.disabled {
+ pointer-events: none;
+ opacity: 0.5;
+
+ &:hover {
+ background-color: transparent;
+ }
+}
+
+.next {
+ transform: rotate(180deg);
+}
diff --git a/src/components/Pagination/Pagination.tsx b/src/components/Pagination/Pagination.tsx
new file mode 100644
index 0000000000..a8376f2602
--- /dev/null
+++ b/src/components/Pagination/Pagination.tsx
@@ -0,0 +1,99 @@
+import classNames from 'classnames';
+import styles from './Pagination.module.scss';
+import { getChevronIconSrc } from '../../servises/iconSrc';
+import { useTheme } from '../../context/ThemeContext';
+
+interface Props {
+ total: number;
+ perPage: number;
+ currentPage: number;
+ onPageChange: (page: number) => void;
+}
+
+export const Pagination: React.FC = ({
+ total,
+ perPage,
+ currentPage,
+ onPageChange,
+}) => {
+ const { theme } = useTheme();
+ const chevronIconSrc = getChevronIconSrc(theme);
+ const totalPages = Math.ceil(total / perPage);
+ const isFirstPage = currentPage === 1;
+ const isLastPage = currentPage === totalPages;
+ const isCurrentPage = (page: number) => currentPage === page;
+
+ const visiblePages = () => {
+ const pages = [];
+ const startPage = Math.max(1, Math.min(currentPage - 1, totalPages - 3));
+ const endPage = Math.min(totalPages, startPage + 3);
+
+ for (let page = startPage; page <= endPage; page++) {
+ pages.push(page);
+ }
+
+ return pages;
+ };
+
+ const goPreviousPage = () => {
+ if (!isFirstPage) {
+ onPageChange(currentPage - 1);
+ }
+ };
+
+ const selectPage = (page: number) => {
+ if (page !== currentPage) {
+ onPageChange(page);
+ }
+ };
+
+ const goNextPage = () => {
+ if (!isLastPage) {
+ onPageChange(currentPage + 1);
+ }
+ };
+
+ return (
+
+
+ {visiblePages().map(page => (
+
+ ))}
+
+
+ );
+};
diff --git a/src/components/Pagination/index.ts b/src/components/Pagination/index.ts
new file mode 100644
index 0000000000..e016c96b72
--- /dev/null
+++ b/src/components/Pagination/index.ts
@@ -0,0 +1 @@
+export * from './Pagination';
diff --git a/src/components/ProductCard/ProductCard.module.scss b/src/components/ProductCard/ProductCard.module.scss
new file mode 100644
index 0000000000..a9ead3e6e0
--- /dev/null
+++ b/src/components/ProductCard/ProductCard.module.scss
@@ -0,0 +1,119 @@
+@import '../../styles/main';
+
+.ProductCard {
+ display: flex;
+ flex-direction: column;
+ justify-content: space-between;
+ gap: 8px;
+ align-items: center;
+ height: 440px;
+ min-width: 212px;
+ width: 100%;
+ padding: 32px;
+ border: 1px solid var(--color-elements);
+ border-radius: 2px;
+
+ @include on-tablet {
+ height: 506px;
+ }
+
+ @include on-desktop {
+ width: 272px;
+ }
+}
+
+.ProductCard:hover {
+ border: 1px solid var(--color-primary);
+}
+
+.imageContainer {
+ display: flex;
+ align-items: flex-start;
+ cursor: pointer;
+
+ width: 148px;
+ min-height: 109px;
+
+ @include on-tablet {
+ min-width: 173px;
+ min-height: 181px;
+ }
+
+ @include on-desktop {
+ width: 208px;
+ height: 196px;
+ }
+}
+
+.image {
+ width: 100%;
+ height: 100%;
+ object-fit: contain;
+ cursor: pointer;
+}
+
+.wrapper {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ width: 100%;
+}
+
+.titleContainer {
+ display: flex;
+ width: 100%;
+}
+
+.title {
+ text-decoration: none;
+
+ @extend %body-text;
+
+ color: var(--color-primary);
+ padding-top: 16px;
+}
+
+
+.price {
+ display: flex;
+ gap: 8px;
+
+ @extend %h3-tablet;
+}
+
+.existPrice {
+ color: var(--color-primary);
+}
+
+.hotPrice {
+ display: flex;
+ color: var(--color-secondary);
+ text-decoration: line-through;
+}
+
+.divider {
+ border-top: 1px solid var(--color-elements);
+}
+
+.description {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ padding: 8px 0;
+}
+
+.existDescription {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
+
+.descriptionTitle {
+ @extend %small-text;
+}
+
+.descriptionText {
+ @extend %small-text;
+
+ color: var(--color-primary);
+}
diff --git a/src/components/ProductCard/ProductCard.tsx b/src/components/ProductCard/ProductCard.tsx
new file mode 100644
index 0000000000..81dc050992
--- /dev/null
+++ b/src/components/ProductCard/ProductCard.tsx
@@ -0,0 +1,59 @@
+import React from 'react';
+import styles from './ProductCard.module.scss';
+import { Product } from '../../types/Product';
+import { Link } from 'react-router-dom';
+import { ActionButtons } from '../ActionButtons';
+import { BASE_URL } from '../../utils/const';
+
+type ProductCardProps = {
+ product: Product;
+};
+
+export const ProductCard: React.FC = ({ product }) => {
+ const { image, name, fullPrice, price, screen, capacity, ram } = product;
+
+ return (
+
+
+
+
+
+
+
+ {name}
+
+
+
+
${fullPrice}
+
${price}
+
+
+
+
+
+
+
+
Capacity
+
{capacity}
+
+
+
+
+
+
+
+ );
+};
diff --git a/src/components/ProductCard/index.tsx b/src/components/ProductCard/index.tsx
new file mode 100644
index 0000000000..7ce031c382
--- /dev/null
+++ b/src/components/ProductCard/index.tsx
@@ -0,0 +1 @@
+export * from './ProductCard';
diff --git a/src/components/ProductsList/ProductsList.module.scss b/src/components/ProductsList/ProductsList.module.scss
new file mode 100644
index 0000000000..8d6655a160
--- /dev/null
+++ b/src/components/ProductsList/ProductsList.module.scss
@@ -0,0 +1,93 @@
+@import '../../styles/main';
+
+.ProductsPage {
+ @include section-grid;
+}
+
+.topContainer {
+ grid-column: 1 / -1;
+}
+
+.title {
+ margin-top: 40px;
+
+ @extend %h1-mobile;
+
+ @include on-tablet {
+ @include h1-tablet;
+ }
+}
+
+.count {
+ @extend %body-text;
+
+ color: var(--color-secondary);
+}
+
+.noResults {
+ grid-column: 1 / -1;
+
+ @extend %h3-tablet;
+
+ color: var(--color-secondary);
+}
+
+.filters {
+ margin-top: 16px;
+ grid-column: 1 / -1;
+
+ @include section-grid;
+
+ padding: 0;
+
+ @include on-tablet {
+ padding: 0;
+ }
+}
+
+.sortBy {
+ grid-column: span 2;
+
+ @include on-tablet {
+ grid-column: span 4;
+ }
+}
+
+.perPage {
+ grid-column: span 2;
+
+ @include on-tablet {
+ grid-column: span 3;
+ }
+}
+
+.container {
+ grid-column: 1 / -1;
+ margin-top: 20px;
+ margin-bottom: 40px;
+
+ @include section-grid;
+
+ padding: 0;
+
+ @include on-tablet {
+ padding: 0;
+ }
+}
+
+.product {
+ grid-column: span 4;
+
+ @include on-tablet {
+ grid-column: span 6;
+ }
+
+ @include middle-screen {
+ grid-column: span 4;
+ }
+
+ @include on-desktop {
+ grid-column: span 6;
+ }
+}
+
diff --git a/src/components/ProductsList/ProductsList.tsx b/src/components/ProductsList/ProductsList.tsx
new file mode 100644
index 0000000000..ec267cb6a7
--- /dev/null
+++ b/src/components/ProductsList/ProductsList.tsx
@@ -0,0 +1,168 @@
+import React, { useState, useEffect, useCallback } from 'react';
+import { getProductsByCategory } from '../../servises/Products';
+import { Product } from '../../types/Product';
+import { Breadcrumbs } from '../Breadcrumbs';
+import styles from './ProductsList.module.scss';
+import { ProductCard } from '../ProductCard';
+import { Loader } from '../Loader';
+import { Pagination } from '../Pagination';
+import { useSearchParams } from 'react-router-dom';
+import { FilterType, ItemsPerPage } from '../../types/Filter';
+import { sortProducts } from '../../utils/sortProducts';
+import { Dropdown } from '../Dropdown/Dropdown';
+
+type ProductsListProps = {
+ category: string;
+ title: string;
+};
+
+export const ProductsList: React.FC = ({
+ category,
+ title,
+}) => {
+ const [searchParams, setSearchParams] = useSearchParams();
+ const [products, setProducts] = useState([]);
+ const [loading, setLoading] = useState(true);
+
+ const page = parseInt(searchParams.get('page') || '1');
+ const perPage = searchParams.get('perPage') || ItemsPerPage.Four;
+ const sortType = searchParams.get('sort') || 'age';
+ const searchQuery = searchParams.get('query') || '';
+
+ const actualPerPage = perPage === 'All' ? products.length : parseInt(perPage);
+ const total = products.length;
+
+ const scrollToTop = () => {
+ window.scrollTo({ top: 0, behavior: 'smooth' });
+ };
+
+ useEffect(() => {
+ const fetchProducts = async () => {
+ setLoading(true);
+ try {
+ const fetchedProducts = await getProductsByCategory(category);
+
+ setProducts(fetchedProducts);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ fetchProducts();
+ }, [category]);
+
+ const handleSortChange = useCallback(
+ (event: React.ChangeEvent) => {
+ setSearchParams({
+ sort: event.target.value,
+ query: searchQuery,
+ page: '1',
+ perPage,
+ });
+ },
+ [setSearchParams, perPage, searchQuery],
+ );
+
+ const handlePerPageChange = useCallback(
+ (event: React.ChangeEvent) => {
+ setSearchParams({
+ perPage: event.target.value,
+ query: searchQuery,
+ page: '1',
+ sort: sortType,
+ });
+ },
+ [setSearchParams, sortType, searchQuery],
+ );
+
+ const handlePageChange = useCallback(
+ (newPage: number) => {
+ setSearchParams({ page: String(newPage), perPage, sort: sortType });
+ setTimeout(scrollToTop, 0);
+ },
+ [setSearchParams, perPage, sortType],
+ );
+
+ const startIndex = (page - 1) * actualPerPage;
+ const filteredProducts = products.filter(
+ product =>
+ product.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
+ product.screen.toLowerCase().includes(searchQuery.toLowerCase()) ||
+ product.capacity.toLowerCase().includes(searchQuery.toLowerCase()) ||
+ product.ram.toLowerCase().includes(searchQuery.toLowerCase()) ||
+ product.price.toString().includes(searchQuery),
+ );
+
+ const sortedProducts = sortProducts(filteredProducts, sortType);
+ const selectedProducts = sortedProducts.slice(
+ startIndex,
+ startIndex + actualPerPage,
+ );
+
+ if (loading) {
+ return ;
+ }
+
+ return (
+
+
+
+
{title}
+
+ {`${filteredProducts.length} item${products.length > 1 ? 's' : ''}`}
+
+
+
+ {filteredProducts.length === 0 ? (
+
+ There are no {category} matching your query.
+
+ ) : (
+ <>
+
+
+ ({
+ value: key,
+ label: value,
+ }))}
+ />
+
+
+
+ ({
+ value,
+ label: value,
+ }))}
+ />
+
+
+
+
+ {selectedProducts.map(product => (
+
+ ))}
+
+
+ {perPage !== 'All' && (
+
+ )}
+ >
+ )}
+
+ );
+};
diff --git a/src/components/ProductsList/index.ts b/src/components/ProductsList/index.ts
new file mode 100644
index 0000000000..09f9887f27
--- /dev/null
+++ b/src/components/ProductsList/index.ts
@@ -0,0 +1 @@
+export * from './ProductsList';
diff --git a/src/components/ProductsSlider/ProductsSlider.module.scss b/src/components/ProductsSlider/ProductsSlider.module.scss
new file mode 100644
index 0000000000..845175ccfc
--- /dev/null
+++ b/src/components/ProductsSlider/ProductsSlider.module.scss
@@ -0,0 +1,84 @@
+@import '../../styles/main';
+
+.productsSlider {
+ @include section-grid;
+
+ padding-right: 0;
+
+ @include on-tablet {
+ padding-right: 0;
+ }
+}
+
+.topWrapper {
+ grid-column: 1 / -1;
+ display: flex;
+ justify-content: space-between;
+ margin-bottom: 24px;
+ padding-right: 16px;
+
+ @include on-tablet {
+ padding-right: 16px;
+ }
+}
+
+.title {
+ @extend %h2-tablet;
+}
+
+.buttons {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 16px;
+}
+
+.button {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ cursor: pointer;
+ width: 32px;
+ height: 32px;
+ background-color: var(--color-icons);
+ border: 1px solid var(--color-surface);
+ border-radius: 5%;
+
+ &:hover {
+ border: 1px solid var(--color-primary);
+ background-color: var(--color-hover);
+ }
+}
+
+.isDisabled {
+ cursor: default;
+ opacity: 0.5;
+
+ &:hover {
+ border: 1px solid var(--color-surface);
+ }
+}
+
+.iconNext {
+ transform: rotate(180deg);
+}
+
+.container {
+ grid-column: 1 / -1;
+ display: flex;
+ overflow: hidden;
+ gap: 16px;
+}
+
+.mainContainer {
+ grid-column: 1 / -1;
+ display: flex;
+ overflow: hidden;
+ gap: 16px;
+}
+
+.sliderWrapper {
+ transition: transform 0.5s ease;
+ will-change: transform;
+}
+
diff --git a/src/components/ProductsSlider/ProductsSlider.tsx b/src/components/ProductsSlider/ProductsSlider.tsx
new file mode 100644
index 0000000000..12771988d8
--- /dev/null
+++ b/src/components/ProductsSlider/ProductsSlider.tsx
@@ -0,0 +1,113 @@
+import React, { useEffect, useState } from 'react';
+import styles from './ProductsSlider.module.scss';
+import { Product } from '../../types/Product';
+import { ProductCard } from '../ProductCard';
+import { getChevronIconSrc } from '../../servises/iconSrc';
+import { useTheme } from '../../context/ThemeContext';
+// eslint-disable-next-line import/no-extraneous-dependencies
+import { useSwipeable } from 'react-swipeable';
+import classNames from 'classnames';
+
+type Props = {
+ products: Product[];
+ title: string;
+ isHomePage?: boolean;
+};
+
+export const ProductsSlider: React.FC = ({ products, title }) => {
+ const [currentIndex, setCurrentIndex] = useState(0);
+ const [visibleCardsCount, setVisibleCardsCount] = useState(3);
+ const { theme } = useTheme();
+
+ const updateVisibleCardsCount = () => {
+ const width = window.innerWidth;
+
+ if (width < 640) {
+ setVisibleCardsCount(1);
+ } else if (width >= 640 && width < 1200) {
+ setVisibleCardsCount(2);
+ } else {
+ setVisibleCardsCount(4);
+ }
+ };
+
+ useEffect(() => {
+ updateVisibleCardsCount();
+ window.addEventListener('resize', updateVisibleCardsCount);
+
+ return () => {
+ window.removeEventListener('resize', updateVisibleCardsCount);
+ };
+ }, []);
+
+ const chevronIconSrc = getChevronIconSrc(theme);
+
+ const handlePrevClick = () => {
+ setCurrentIndex(prevIndex => Math.max(prevIndex - 1, 0));
+ };
+
+ const handleNextClick = () => {
+ setCurrentIndex(prevIndex =>
+ Math.min(prevIndex + 1, products.length - visibleCardsCount),
+ );
+ };
+
+ const isPrevDisabled = currentIndex === 0;
+ const isNextDisabled = currentIndex === products.length - visibleCardsCount;
+
+ const handlers = useSwipeable({
+ onSwipedLeft: () => handleNextClick(),
+ onSwipedRight: () => handlePrevClick(),
+ });
+
+ return (
+
+
+
{title}
+
+
+
+
+
+
+
+
+ {products.map(product => (
+
+ ))}
+
+
+ );
+};
diff --git a/src/components/ProductsSlider/index.ts b/src/components/ProductsSlider/index.ts
new file mode 100644
index 0000000000..68d3ea0012
--- /dev/null
+++ b/src/components/ProductsSlider/index.ts
@@ -0,0 +1 @@
+export * from './ProductsSlider';
diff --git a/src/components/Search/Search.module.scss b/src/components/Search/Search.module.scss
new file mode 100644
index 0000000000..70be92e7f9
--- /dev/null
+++ b/src/components/Search/Search.module.scss
@@ -0,0 +1,53 @@
+@import './../../styles/main';
+
+.search {
+ display: flex;
+ align-items: center;
+ box-shadow: -1px 0 0 0 var(--color-elements), 1px 0 0 0 var(--color-elements);
+ cursor: pointer;
+ background-color: transparent;
+ border: none;
+ overflow: hidden;
+ height: $height-on-tablet;
+ transition: width 0.5s ease;
+
+ @include on-desktop {
+ height: $height-on-desktop;
+ min-width: 320px;
+ }
+
+ &:hover {
+ color: var(--color-primary);
+
+ cursor: text;
+ }
+}
+
+.searchInput {
+ padding: 0 10px;
+ width: 100%;
+ height: 100%;
+ align-items: center;
+ padding-inline: 14px;
+ border: none;
+ outline: none;
+
+ background-color: transparent;
+ color: var(--color-primary);
+
+ @extend %body-text;
+
+ @include on-desktop {
+ padding-inline: 16px;
+ }
+
+ &::placeholder {
+ color: var(--color-secondary);
+
+ @extend %body-text;
+ }
+}
+
+.hidden {
+ display: none;
+}
diff --git a/src/components/Search/Search.tsx b/src/components/Search/Search.tsx
new file mode 100644
index 0000000000..41f6216051
--- /dev/null
+++ b/src/components/Search/Search.tsx
@@ -0,0 +1,62 @@
+import React, { useEffect, useState } from 'react';
+import { useLocation, useSearchParams } from 'react-router-dom';
+import styles from './Search.module.scss';
+import classNames from 'classnames';
+import debounce from 'lodash.debounce';
+import { useTheme } from '../../context/ThemeContext';
+
+export const Search: React.FC = () => {
+ const { pathname } = useLocation();
+ const { theme } = useTheme();
+
+ const [searchParams, setSearchParams] = useSearchParams();
+ const [query, setQuery] = useState(searchParams.get('query') || '');
+
+ useEffect(() => {
+ setQuery('');
+ setSearchParams({});
+ }, [pathname, theme]);
+
+ const updateSearchQuery = (value: string) => {
+ const trimmedValue = value.trim();
+ const newQuery = new URLSearchParams(searchParams.toString());
+
+ if (trimmedValue) {
+ newQuery.set('query', trimmedValue);
+ } else {
+ newQuery.delete('query');
+ }
+
+ setSearchParams(newQuery);
+ };
+
+ const debouncedUpdateSearchQuery = debounce(updateSearchQuery, 500);
+
+ const handleSearchChange = (event: React.ChangeEvent) => {
+ const inputValue = event.target.value;
+
+ debouncedUpdateSearchQuery(inputValue);
+ setQuery(inputValue);
+ };
+
+ const isHidden =
+ pathname === '/' ||
+ pathname === '/favorites' ||
+ pathname === '/cart' ||
+ pathname === '/products' ||
+ pathname.startsWith('/products/');
+
+ return (
+
+
+
+ );
+};
diff --git a/src/context/AppContext.tsx b/src/context/AppContext.tsx
new file mode 100644
index 0000000000..af49789a49
--- /dev/null
+++ b/src/context/AppContext.tsx
@@ -0,0 +1,121 @@
+import React, { createContext, useCallback, useContext } from 'react';
+import { Product } from '../types/Product';
+import { useLocalStorage } from '../hooks/useLocalStorage';
+import { CartItemProps } from '../types/CartItemProps';
+
+type Props = {
+ children: React.ReactNode;
+};
+
+type AppContextType = {
+ favorites: Product[];
+ addToFavorites: (product: Product) => void;
+ removeFromFavorites: (productId: string) => void;
+ cart: CartItemProps[];
+ addToCart: (product: Product) => void;
+ removeFromCart: (productId: string) => void;
+ updateCartQuantity: (productId: string, quantity: number) => void;
+ calculateTotalPrice: () => number;
+ clearCart: () => void;
+};
+
+const AppContext = createContext({
+ favorites: [],
+ addToFavorites: () => {},
+ removeFromFavorites: () => {},
+ cart: [],
+ addToCart: () => {},
+ removeFromCart: () => {},
+ updateCartQuantity: () => {},
+ calculateTotalPrice: () => 0,
+ clearCart: () => {},
+});
+
+export const useAppContext = () => {
+ return useContext(AppContext);
+};
+
+export const AppProvider: React.FC = ({ children }) => {
+ const [favorites, setFavorites] = useLocalStorage('favorites', []);
+ const [cart, setCart] = useLocalStorage('cart', []);
+
+ const addToFavorites = useCallback(
+ (product: Product) => {
+ setFavorites((prevFavorites: Product[]) => [...prevFavorites, product]);
+ },
+ [setFavorites],
+ );
+
+ const removeFromFavorites = useCallback(
+ (productId: string) => {
+ setFavorites((prevFavorites: Product[]) =>
+ prevFavorites.filter(product => product.id !== productId),
+ );
+ },
+ [setFavorites],
+ );
+
+ const addToCart = useCallback(
+ (product: Product) => {
+ setCart(prevCart => [...prevCart, { product, quantity: 1 }]);
+ },
+ [setCart],
+ );
+
+ const removeFromCart = useCallback(
+ (productId: string) => {
+ setCart((prevCart: CartItemProps[]) =>
+ prevCart.filter(item => item.product.id !== productId),
+ );
+ },
+ [setCart],
+ );
+
+ const updateCartQuantity = useCallback(
+ (productId: string, delta: number) => {
+ setCart(currentCart =>
+ currentCart.map(cartItem => {
+ if (cartItem.product.id === productId) {
+ const newQuantity = Math.max(cartItem.quantity + delta, 1);
+
+ return { ...cartItem, quantity: newQuantity };
+ }
+
+ return cartItem;
+ }),
+ );
+ },
+ [setCart],
+ );
+
+ const clearCart = useCallback(() => {
+ setCart([]);
+ }, [setCart]);
+
+ const calculateTotalPrice = useCallback(() => {
+ return Math.floor(
+ cart.reduce(
+ (total, { product, quantity }) => total + product.price * quantity,
+ 0,
+ ),
+ );
+ }, [cart]);
+
+ return (
+
+ {children}
+
+ );
+};
diff --git a/src/context/ThemeContext.tsx b/src/context/ThemeContext.tsx
new file mode 100644
index 0000000000..a486e82851
--- /dev/null
+++ b/src/context/ThemeContext.tsx
@@ -0,0 +1,44 @@
+import React, { createContext, useContext, useEffect, useState } from 'react';
+import { ThemeType } from '../types/ThemeType';
+
+interface ThemeContextType {
+ theme: ThemeType;
+ toggleTheme: () => void;
+}
+
+type Props = {
+ children: React.ReactNode;
+};
+export const ThemeContext = createContext(
+ undefined,
+);
+
+export const ThemeProvider: React.FC = ({ children }) => {
+ const [theme, setTheme] = useState(ThemeType.LIGHT);
+
+ useEffect(() => {
+ document.body.setAttribute('data-theme', theme);
+ }, [theme]);
+
+ const toggleTheme = () => {
+ setTheme(current =>
+ current === ThemeType.LIGHT ? ThemeType.DARK : ThemeType.LIGHT,
+ );
+ };
+
+ return (
+
+ {children}
+
+ );
+};
+
+export const useTheme = (): ThemeContextType => {
+ const context = useContext(ThemeContext);
+
+ if (context === undefined) {
+ throw new Error('useTheme must be used within a ThemeProvider');
+ }
+
+ return context;
+};
diff --git a/src/fonts/Mont-Bold.otf b/src/fonts/Mont-Bold.otf
new file mode 100755
index 0000000000..7f1598293a
Binary files /dev/null and b/src/fonts/Mont-Bold.otf differ
diff --git a/src/fonts/Mont-Regular.otf b/src/fonts/Mont-Regular.otf
new file mode 100755
index 0000000000..d5543feaf0
Binary files /dev/null and b/src/fonts/Mont-Regular.otf differ
diff --git a/src/fonts/Mont-SemiBold.otf b/src/fonts/Mont-SemiBold.otf
new file mode 100755
index 0000000000..a9fa16a9c5
Binary files /dev/null and b/src/fonts/Mont-SemiBold.otf differ
diff --git a/src/hooks/useLocalStorage.ts b/src/hooks/useLocalStorage.ts
new file mode 100644
index 0000000000..b64b85bbb9
--- /dev/null
+++ b/src/hooks/useLocalStorage.ts
@@ -0,0 +1,25 @@
+import { useState } from 'react';
+
+export function useLocalStorage(
+ key: string,
+ initialValue: T,
+): [T, (v: T | ((val: T) => T)) => void] {
+ const [storedValue, setStoredValue] = useState(() => {
+ try {
+ const item = window.localStorage.getItem(key);
+
+ return item ? JSON.parse(item) : initialValue;
+ } catch (error) {
+ return initialValue;
+ }
+ });
+
+ const setValue = (value: T | ((val: T) => T)) => {
+ const valueToStore = value instanceof Function ? value(storedValue) : value;
+
+ setStoredValue(valueToStore);
+ window.localStorage.setItem(key, JSON.stringify(valueToStore));
+ };
+
+ return [storedValue, setValue];
+}
diff --git a/src/index.tsx b/src/index.tsx
index 50470f1508..21951d42dd 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -1,4 +1,7 @@
import { createRoot } from 'react-dom/client';
-import { App } from './App';
-createRoot(document.getElementById('root') as HTMLElement).render();
+import { Root } from './Root';
+
+const container = document.getElementById('root') as HTMLElement;
+
+createRoot(container).render();
diff --git a/src/modules/AccessoriesPage/AccessoriesPage.scss b/src/modules/AccessoriesPage/AccessoriesPage.scss
new file mode 100644
index 0000000000..af05f80224
--- /dev/null
+++ b/src/modules/AccessoriesPage/AccessoriesPage.scss
@@ -0,0 +1,3 @@
+.accessories {
+ display: flex;
+}
diff --git a/src/modules/AccessoriesPage/AccessoriesPage.tsx b/src/modules/AccessoriesPage/AccessoriesPage.tsx
new file mode 100644
index 0000000000..05b55deaef
--- /dev/null
+++ b/src/modules/AccessoriesPage/AccessoriesPage.tsx
@@ -0,0 +1,11 @@
+import React from 'react';
+import { useLocation } from 'react-router-dom';
+import { ProductsList } from '../../components/ProductsList';
+
+export const AccessoriesPage: React.FC = () => {
+ const location = useLocation();
+
+ const category = location.pathname.split('/')[1];
+
+ return ;
+};
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/CartItem/CartItem.module.scss b/src/modules/CartPage/CartItem/CartItem.module.scss
new file mode 100644
index 0000000000..cea0d37446
--- /dev/null
+++ b/src/modules/CartPage/CartItem/CartItem.module.scss
@@ -0,0 +1,130 @@
+@import '../../../styles/main';
+
+.cartItem {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 16px;
+ padding: 16px;
+ border: 1px solid var(--color-elements);
+ width: 100%;
+
+ @include on-tablet {
+ gap: 24px;
+ flex-direction: row;
+ justify-content: space-between;
+ }
+}
+
+.mainContainer {
+ display: flex;
+ width: 100%;
+ align-items: center;
+ gap: 16px;
+
+}
+
+.deleteButton {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border: none;
+ background-color: transparent;
+ cursor: pointer;
+
+ &:hover img {
+ transform: scale(1.1);
+ }
+}
+
+.deleteButtonIcon {
+ width: 16px;
+ height: 16px;
+}
+
+.productImage {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 80px;
+ height: 80px;
+}
+
+.image {
+ width: 66px;
+ height: 66px;
+ object-fit: contain;
+}
+
+.quantityControl {
+ display: flex;
+ height: 32px;
+ width: 100%;
+ align-items: center;
+ justify-content: space-between;
+
+ @include on-tablet {
+ flex: 1;
+ gap: 24px;
+ }
+}
+
+.quantity {
+ display: flex;
+ align-items: center;
+ width: 96px;
+}
+
+.button {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 32px;
+ border: 1px solid var(--color-surface);
+ background-color: var(--color-icons);
+ height: 32px;
+ cursor: pointer;
+
+ transition: transform $transition-duration ease;
+
+ &:hover {
+ transform: scale(1.1);
+ border: 1px solid var(--color-primary);
+ background-color: var(--color-hover);
+ }
+}
+
+.disabled {
+ cursor: default;
+ background-color: var(--color-background);
+ opacity: 0.5;
+
+ &:hover {
+ transform: none;
+ border: 1px solid var(--color-surface);
+ background-color: transparent;
+ }
+}
+
+.controlButtonIcon {
+ width: 16px;
+ height: 16px;
+ border: none;
+ background: transparent;
+}
+
+.quantityValueContainer {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ height: 32px;
+ width: 32px;
+}
+
+.quantityValue {
+ @extend %body-text;
+}
+
+.price{
+ @extend %h3-tablet;
+}
diff --git a/src/modules/CartPage/CartItem/CartItem.tsx b/src/modules/CartPage/CartItem/CartItem.tsx
new file mode 100644
index 0000000000..de5d30752f
--- /dev/null
+++ b/src/modules/CartPage/CartItem/CartItem.tsx
@@ -0,0 +1,85 @@
+import React from 'react';
+import { useAppContext } from '../../../context/AppContext';
+import styles from './CartItem.module.scss';
+import { CartItemProps } from '../../../types/CartItemProps';
+import {
+ getDecreaseIconSrc,
+ getRemoveIconSrc,
+ getIncreaseIconSrc,
+} from '../../../servises/iconSrc';
+import { useTheme } from '../../../context/ThemeContext';
+import { Link } from 'react-router-dom';
+import classNames from 'classnames';
+import { BASE_URL } from '../../../utils/const';
+
+type Props = {
+ item: CartItemProps;
+};
+
+export const CartItem: React.FC = ({ item }) => {
+ const { updateCartQuantity, removeFromCart } = useAppContext();
+ const { theme } = useTheme();
+ const { product, quantity } = item;
+ const { image, name, id, price, itemId } = product;
+
+ const handleDeleteItem = () => {
+ removeFromCart(id);
+ };
+
+ const deleteImgSrc = getRemoveIconSrc(theme);
+ const degreaseImgSrc = getDecreaseIconSrc(theme);
+ const increaseImgSrc = getIncreaseIconSrc(theme);
+
+ return (
+
+
+
+
+
+
+
{name}
+
+
+
+
+
+
+
+
${price}
+
+
+ );
+};
diff --git a/src/modules/CartPage/CartItem/index.tsx b/src/modules/CartPage/CartItem/index.tsx
new file mode 100644
index 0000000000..37a0553540
--- /dev/null
+++ b/src/modules/CartPage/CartItem/index.tsx
@@ -0,0 +1 @@
+export * from './CartItem';
diff --git a/src/modules/CartPage/CartPage.module.scss b/src/modules/CartPage/CartPage.module.scss
new file mode 100644
index 0000000000..64c5655a30
--- /dev/null
+++ b/src/modules/CartPage/CartPage.module.scss
@@ -0,0 +1,136 @@
+@import '../../styles/main';
+
+.cartPage {
+ @include section-grid;
+}
+
+.topContainer {
+ grid-column: 1 / -1;
+ display: flex;
+ width: 100%;
+ flex-direction: column;
+ justify-content: center;
+}
+
+.goBackButton {
+ display: flex;
+ align-items: center;
+ overflow: hidden;
+ gap: 8px;
+ height: 16px;
+ width: 66px;
+ border: 0;
+ cursor: pointer;
+ background-color: transparent;
+}
+
+.goBackText {
+ display: block;
+ padding-top: 2px;
+
+ @extend %small-text;
+
+ color: var(--color-secondary);
+}
+
+.title {
+ margin-top: 40px;
+
+ @extend %h1-mobile;
+
+ @include on-tablet {
+ @include h1-tablet;
+ }
+}
+
+.emptyContainer {
+ grid-column: 1 / -1;
+ display: flex;
+ flex-direction: column;
+ color: var(--color-secondary);
+
+ @extend %h3-tablet;
+}
+
+.container {
+ grid-column: 1 / -1;
+
+ @include section-grid;
+
+ padding: 0;
+ margin-top: 24px;
+
+ @include on-tablet {
+ padding: 0;
+ }
+}
+
+.cartItems {
+ grid-column: 1 / -1;
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+ margin-bottom: 30px;
+
+ @include on-desktop {
+ grid-column: span 16;
+ }
+}
+
+.bottomContainer {
+ grid-column: 1 / -1;
+ display: flex;
+ width: 100%;
+ flex-direction: column;
+ align-items: center;
+ gap: 16px;
+
+ @include on-desktop {
+ grid-column: span 8;
+ }
+}
+
+.checkout {
+ display: flex;
+ width: 100%;
+ flex-direction: column;
+ align-items: center;
+ gap: 16px;
+ padding: 24px;
+ border: 1px solid var(--color-elements);
+}
+
+.totalPrice {
+ @extend %h2-tablet;
+}
+
+.totalItems {
+ @extend %body-text;
+
+ color: var(--color-secondary);
+}
+
+.divider {
+ width: 100%;
+ height: 1px;
+ background-color: var(--color-elements);
+}
+
+.checkoutButton {
+ width: 100%;
+ height: 48px;
+ background-color: var(--color-button);
+ cursor: pointer;
+ text-align: center;
+
+ @extend %body-text;
+
+ color: var(--color-white);
+ transition: transform $transition-duration ease;
+
+ &:hover {
+ transform: scale(1.05);
+ box-shadow: 0 3px 13px 0 #17203166;
+ background-color: var(--color-button-hover);
+ }
+}
diff --git a/src/modules/CartPage/CartPage.tsx b/src/modules/CartPage/CartPage.tsx
new file mode 100644
index 0000000000..0c7e799a2a
--- /dev/null
+++ b/src/modules/CartPage/CartPage.tsx
@@ -0,0 +1,79 @@
+import React from 'react';
+import styles from './CartPage.module.scss';
+import { useAppContext } from '../../context/AppContext';
+import { useNavigate } from 'react-router-dom';
+import { getChevronIconSrc } from '../../servises/iconSrc';
+import { useTheme } from '../../context/ThemeContext';
+import { CartItem } from './CartItem';
+
+const CartPage: React.FC = () => {
+ const { cart, calculateTotalPrice, clearCart } = useAppContext();
+ const totalItems = cart.reduce(
+ (total, cartItem) => total + cartItem.quantity,
+ 0,
+ );
+
+ const { theme } = useTheme();
+ const navigate = useNavigate();
+ const chevronIconSrc = getChevronIconSrc(theme);
+
+ const handleCheckout = () => {
+ const confirmClear = window.confirm(
+ 'Checkout is not implemented yet. Do you want to clear the Cart?',
+ );
+
+ if (confirmClear) {
+ clearCart();
+ }
+ };
+
+ return (
+
+
+
+
+
+
Cart
+
+ {cart.length === 0 ? (
+
+
Your cart is empty.
+
+ ) : (
+
+
+ {cart.map(cartItem => (
+
+ ))}
+
+
+
+
${calculateTotalPrice()}
+
{`Total for ${totalItems} item${totalItems > 1 ? 's' : ''}`}
+
+
+
+
+
+ )}
+
+ );
+};
+
+export default CartPage;
diff --git a/src/modules/CartPage/index.ts b/src/modules/CartPage/index.ts
new file mode 100644
index 0000000000..cba584bad9
--- /dev/null
+++ b/src/modules/CartPage/index.ts
@@ -0,0 +1 @@
+export { default as CartPage } from './CartPage';
diff --git a/src/modules/FavoritesPage/FavoritesPage.module.scss b/src/modules/FavoritesPage/FavoritesPage.module.scss
new file mode 100644
index 0000000000..bf0a0acb0d
--- /dev/null
+++ b/src/modules/FavoritesPage/FavoritesPage.module.scss
@@ -0,0 +1,54 @@
+@import '../../styles/main';
+
+.favoritesPage {
+ @include section-grid;
+}
+
+.topContainer {
+ grid-column: 1 / -1;
+ display: flex;
+ width: 100%;
+ flex-direction: column;
+ justify-content: center;
+}
+
+.title {
+ margin-top: 40px;
+
+ @extend %h1-mobile;
+
+ @include on-tablet {
+ @include h1-tablet;
+ }
+}
+
+.count {
+ @extend %body-text;
+
+ color: var(--color-secondary);
+}
+
+.container {
+ grid-column: 1 / -1;
+ margin-top: 40px;
+
+ @include section-grid;
+
+ padding: 0;
+}
+
+.product {
+ grid-column: span 4;
+
+ @include on-tablet {
+ grid-column: span 6;
+ }
+
+ @include middle-screen {
+ grid-column: span 4;
+ }
+
+ @include on-desktop {
+ grid-column: span 6;
+ }
+}
diff --git a/src/modules/FavoritesPage/FavoritesPage.tsx b/src/modules/FavoritesPage/FavoritesPage.tsx
new file mode 100644
index 0000000000..a38cdc5838
--- /dev/null
+++ b/src/modules/FavoritesPage/FavoritesPage.tsx
@@ -0,0 +1,30 @@
+import React from 'react';
+import styles from './FavoritesPage.module.scss';
+import { useAppContext } from '../../context/AppContext';
+import { ProductCard } from '../../components/ProductCard';
+import { Breadcrumbs } from '../../components/Breadcrumbs';
+
+const FavoritesPage: React.FC = () => {
+ const { favorites } = useAppContext();
+
+ return (
+
+
+
+
Favorites
+
+ {`${favorites.length} item${favorites.length > 1 ? 's' : ''}`}
+
+
+
+ {favorites.map(product => (
+
+ ))}
+
+
+ );
+};
+
+export default FavoritesPage;
diff --git a/src/modules/FavoritesPage/index.ts b/src/modules/FavoritesPage/index.ts
new file mode 100644
index 0000000000..cfb19c003a
--- /dev/null
+++ b/src/modules/FavoritesPage/index.ts
@@ -0,0 +1 @@
+export { default as FavoritesPage } from './FavoritesPage';
diff --git a/src/modules/HomePage/HomePage.module.scss b/src/modules/HomePage/HomePage.module.scss
new file mode 100644
index 0000000000..f1051c2766
--- /dev/null
+++ b/src/modules/HomePage/HomePage.module.scss
@@ -0,0 +1,20 @@
+@import '../../styles/main';
+
+.homePage {
+ display: flex;
+ flex-direction: column;
+ gap: 80px;
+}
+
+.visuallyHidden{
+ position: absolute;
+ width: 1px;
+ height: 1px;
+ margin: -1px;
+ border: 0;
+ padding: 0;
+ white-space: nowrap;
+ clip-path: inset(100%);
+ clip: rect(0 0 0 0);
+ overflow: hidden;
+}
diff --git a/src/modules/HomePage/HomePage.tsx b/src/modules/HomePage/HomePage.tsx
new file mode 100644
index 0000000000..6c042c085f
--- /dev/null
+++ b/src/modules/HomePage/HomePage.tsx
@@ -0,0 +1,52 @@
+import React, { useEffect, useState } from 'react';
+import styles from './HomePage.module.scss';
+import { PicturesSlider } from './components/PicturesSlider';
+import { Categories } from './components/Categories';
+import { getHotPriceProducts, getNewProducts } from '../../servises/Products';
+import { Product } from '../../types/Product';
+import { Loader } from '../../components/Loader';
+import { ProductsSlider } from '../../components/ProductsSlider';
+
+const HomePage: React.FC = () => {
+ const [newProducts, setNewProducts] = useState([]);
+ const [hotProducts, setHotProducts] = useState([]);
+ const [isLoading, setIsLoading] = useState(true);
+
+ useEffect(() => {
+ const fetchProducts = async () => {
+ try {
+ const [fetchedHotProducts, fetchedNewProducts] = await Promise.all([
+ getHotPriceProducts(),
+ getNewProducts(),
+ ]);
+
+ setHotProducts(fetchedHotProducts);
+ setNewProducts(fetchedNewProducts);
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ fetchProducts();
+ }, []);
+
+ return (
+
+
Product Catalog
+ {isLoading ? (
+
+ ) : (
+ <>
+
+
+
+
+
+
+ >
+ )}
+
+ );
+};
+
+export default HomePage;
diff --git a/src/modules/HomePage/components/Categories/Categories.module.scss b/src/modules/HomePage/components/Categories/Categories.module.scss
new file mode 100644
index 0000000000..24d8196b5a
--- /dev/null
+++ b/src/modules/HomePage/components/Categories/Categories.module.scss
@@ -0,0 +1,61 @@
+@import '../../../../styles/main';
+
+.categories {
+ @include section-grid;
+}
+
+.title {
+ grid-column: 1 / -1;
+
+ @extend %h2-mobile;
+
+ @include on-tablet {
+ font-family: Mont, sans-serif;
+ font-size: 32px;
+ line-height: 41px;
+ letter-spacing: 0.01em;
+ font-weight: bold;
+ color: var(--color-primary);
+ }
+}
+
+.category {
+ grid-column: 1 / -1;
+
+
+ @include on-tablet {
+ grid-column: span 4;
+ align-self: start;
+ }
+
+ @include on-desktop {
+ grid-column: span 8;
+ }
+}
+
+.link {
+ display: block;
+ text-decoration: none;
+ transition: transform $transition-duration ease;
+}
+
+.image {
+ object-fit: cover;
+ width: 100%;
+ margin-bottom: 10px;
+
+ &:hover {
+ transform: scale(1.05);
+ }
+}
+
+.category__title {
+ @extend %h4-tablet;
+}
+
+.category__subtitle {
+ @extend %body-text;
+
+ color: var(--color-secondary);
+}
+
diff --git a/src/modules/HomePage/components/Categories/Categories.tsx b/src/modules/HomePage/components/Categories/Categories.tsx
new file mode 100644
index 0000000000..8dcdf30019
--- /dev/null
+++ b/src/modules/HomePage/components/Categories/Categories.tsx
@@ -0,0 +1,56 @@
+import React, { useEffect, useState } from 'react';
+import styles from './Categories.module.scss';
+import { Link } from 'react-router-dom';
+import { ShopByCategoryMap } from '../Helpers/ShopByCategoryMap';
+import { getProductsByCategory } from '../../../../servises/Products';
+import { BASE_URL } from '../../../../utils/const';
+
+export const Categories: React.FC = () => {
+ const [phonesCount, setPhonesCount] = useState(0);
+ const [tabletsCount, setTabletsCount] = useState(0);
+ const [accessoriesCount, setAccessoriesCount] = useState(0);
+
+ useEffect(() => {
+ const fetchProductCounts = async () => {
+ const categories = ['phones', 'tablets', 'accessories'];
+
+ const productCounts = await Promise.all(
+ categories.map(category => getProductsByCategory(category)),
+ );
+
+ setPhonesCount(productCounts[0].length);
+ setTabletsCount(productCounts[1].length);
+ setAccessoriesCount(productCounts[2].length);
+ };
+
+ fetchProductCounts();
+ }, []);
+
+ return (
+
+
Shop by category
+
+ {ShopByCategoryMap.map(({ id, src, title, path }) => (
+
+
+
+
+
{title}
+
+
+
+ {(title === 'Mobile phones' && `${phonesCount} models`) ||
+ (title === 'Tablets' && `${tabletsCount} models`) ||
+ (title === 'Accessories' && `${accessoriesCount} models`)}
+
+
+
+
+ ))}
+
+ );
+};
diff --git a/src/modules/HomePage/components/Categories/index.tsx b/src/modules/HomePage/components/Categories/index.tsx
new file mode 100644
index 0000000000..79c7c7dcde
--- /dev/null
+++ b/src/modules/HomePage/components/Categories/index.tsx
@@ -0,0 +1 @@
+export * from './Categories';
diff --git a/src/modules/HomePage/components/Helpers/PicturesSliderMap.ts b/src/modules/HomePage/components/Helpers/PicturesSliderMap.ts
new file mode 100644
index 0000000000..c2d2e41846
--- /dev/null
+++ b/src/modules/HomePage/components/Helpers/PicturesSliderMap.ts
@@ -0,0 +1,17 @@
+export const PicturesSliderMap = [
+ {
+ id: 1,
+ src: './img/banner-phones.png',
+ title: 'phones',
+ },
+ {
+ id: 2,
+ src: './img/banner-tablets.png',
+ title: 'tablets',
+ },
+ {
+ id: 3,
+ src: './img/banner-accessories.png',
+ title: 'accessories',
+ },
+];
diff --git a/src/modules/HomePage/components/Helpers/ShopByCategoryMap.ts b/src/modules/HomePage/components/Helpers/ShopByCategoryMap.ts
new file mode 100644
index 0000000000..14d9428e00
--- /dev/null
+++ b/src/modules/HomePage/components/Helpers/ShopByCategoryMap.ts
@@ -0,0 +1,20 @@
+export const ShopByCategoryMap = [
+ {
+ id: 1,
+ src: './img/category-phones.png',
+ title: 'Mobile phones',
+ path: '/phones',
+ },
+ {
+ id: 2,
+ src: './img/category-tablets.png',
+ title: 'Tablets',
+ path: '/tablets',
+ },
+ {
+ id: 3,
+ src: './img/category-accessories.png',
+ title: 'Accessories',
+ path: '/accessories',
+ },
+];
diff --git a/src/modules/HomePage/components/PicturesSlider/PicturesSlider.module.scss b/src/modules/HomePage/components/PicturesSlider/PicturesSlider.module.scss
new file mode 100644
index 0000000000..4c0cc30042
--- /dev/null
+++ b/src/modules/HomePage/components/PicturesSlider/PicturesSlider.module.scss
@@ -0,0 +1,114 @@
+@import '../../../../styles/main';
+
+.slider {
+ @include section-grid;
+
+ padding-inline: 0;
+}
+
+.container {
+ grid-column: 1 / -1;
+ display: flex;
+ overflow: hidden;
+ width: 100%;
+ height: 320px;
+
+
+ @include on-tablet {
+ grid-column: 2 / -2;
+ height: 360px;
+ }
+
+ @include middle-screen {
+ height: 410px;
+ }
+
+ @include on-desktop {
+ height: 432px;
+ }
+}
+
+.link {
+ display: flex;
+ align-items: center;
+ min-width: 100%;
+ height: 100%;
+ cursor: pointer;
+}
+
+.image {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+
+ flex-shrink: 0;
+
+ transition: transform $transition-duration ease;
+}
+
+.button {
+ display: none;
+ width: 32px;
+ height: 100%;
+ background-color: var(--color-icons);
+ border: 1px solid var(--color-surface);
+ border-radius: 4px;
+ cursor: pointer;
+
+ @include on-tablet {
+ display: block;
+ grid-column: span 1;
+ }
+
+ &:hover {
+ border: 1px solid var(--color-primary);
+ background-color: var(--color-hover);
+ }
+}
+
+.next {
+ @include on-tablet {
+ justify-self: end;
+ }
+}
+
+.iconNext {
+ transform : rotate(180deg);
+}
+
+
+.dashes {
+ grid-column: 1 / -1;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ gap: 4px;
+ height: 24px;
+
+ @include on-tablet {
+ grid-column: 6 / 8;
+ }
+
+ @include on-desktop {
+ grid-column: 12 / 14;
+ }
+}
+
+.dashContainer {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ width: 24px;
+ height: 24px;
+ cursor: pointer;
+}
+
+.dash {
+ height: 4px;
+ width: 14px;
+ background-color: var(--color-surface);
+
+ &.active {
+ background-color: var(--color-primary);
+ }
+}
diff --git a/src/modules/HomePage/components/PicturesSlider/PicturesSlider.tsx b/src/modules/HomePage/components/PicturesSlider/PicturesSlider.tsx
new file mode 100644
index 0000000000..ff22c24707
--- /dev/null
+++ b/src/modules/HomePage/components/PicturesSlider/PicturesSlider.tsx
@@ -0,0 +1,98 @@
+import React, { useEffect, useState } from 'react';
+import classNames from 'classnames';
+import styles from './PicturesSlider.module.scss';
+import { PicturesSliderMap } from '../Helpers/PicturesSliderMap';
+import { useTheme } from '../../../../context/ThemeContext';
+// eslint-disable-next-line import/no-extraneous-dependencies
+import { useSwipeable } from 'react-swipeable';
+import { getChevronIconSrc } from '../../../../servises/iconSrc';
+import { Link } from 'react-router-dom';
+import { BASE_URL } from '../../../../utils/const';
+
+export const PicturesSlider: React.FC = () => {
+ const [currentIndex, setCurrentIndex] = useState(0);
+ const { theme } = useTheme();
+
+ const chevronIconSrc = getChevronIconSrc(theme);
+
+ useEffect(() => {
+ const intervalId = setInterval(() => {
+ setCurrentIndex(prevIndex => (prevIndex + 1) % PicturesSliderMap.length);
+ }, 5000);
+
+ return () => clearInterval(intervalId);
+ }, []);
+
+ const handlePrevClick = () => {
+ setCurrentIndex(prevIndex =>
+ prevIndex === 0 ? PicturesSliderMap.length - 1 : prevIndex - 1,
+ );
+ };
+
+ const handleNextClick = () => {
+ setCurrentIndex(prevIndex =>
+ prevIndex === PicturesSliderMap.length - 1 ? 0 : prevIndex + 1,
+ );
+ };
+
+ const handlers = useSwipeable({
+ onSwipedLeft: () => handleNextClick(),
+ onSwipedRight: () => handlePrevClick(),
+ });
+
+ return (
+
+
+
+ {PicturesSliderMap.map(({ id, src, title }) => (
+
+
+
+ ))}
+
+
+
+
+ {PicturesSliderMap.map(({ id }, index) => (
+
setCurrentIndex(index)}
+ className={styles.dashContainer}
+ onKeyDown={() => setCurrentIndex(index)}
+ aria-label={`Slide ${index + 1}`}
+ >
+
+
+ ))}
+
+
+ );
+};
diff --git a/src/modules/HomePage/components/PicturesSlider/index.tsx b/src/modules/HomePage/components/PicturesSlider/index.tsx
new file mode 100644
index 0000000000..81a373f3aa
--- /dev/null
+++ b/src/modules/HomePage/components/PicturesSlider/index.tsx
@@ -0,0 +1 @@
+export * from './PicturesSlider';
diff --git a/src/modules/HomePage/index.ts b/src/modules/HomePage/index.ts
new file mode 100644
index 0000000000..6ba9fc8ac9
--- /dev/null
+++ b/src/modules/HomePage/index.ts
@@ -0,0 +1 @@
+export { default as HomePage } from './HomePage';
diff --git a/src/modules/NotFoundPage/NotFoundPage.module.scss b/src/modules/NotFoundPage/NotFoundPage.module.scss
new file mode 100644
index 0000000000..8ec3e111a4
--- /dev/null
+++ b/src/modules/NotFoundPage/NotFoundPage.module.scss
@@ -0,0 +1,11 @@
+@import '../../styles/main';
+
+.notFoundPage {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+
+ @extend %h2-mobile;
+}
+
+
diff --git a/src/modules/NotFoundPage/NotFoundPage.tsx b/src/modules/NotFoundPage/NotFoundPage.tsx
new file mode 100644
index 0000000000..1bf958df67
--- /dev/null
+++ b/src/modules/NotFoundPage/NotFoundPage.tsx
@@ -0,0 +1,10 @@
+import React from 'react';
+import styles from './NotFoundPage.module.scss';
+
+export const NotFoundPage: React.FC = () => {
+ return (
+
+
Oops, page not found
+
+ );
+};
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..19b4146bf7
--- /dev/null
+++ b/src/modules/PhonesPage/PhonesPage.module.scss
@@ -0,0 +1 @@
+@import '../../styles/main';
diff --git a/src/modules/PhonesPage/PhonesPage.tsx b/src/modules/PhonesPage/PhonesPage.tsx
new file mode 100644
index 0000000000..0af2f5c0a3
--- /dev/null
+++ b/src/modules/PhonesPage/PhonesPage.tsx
@@ -0,0 +1,11 @@
+import React from 'react';
+import { useLocation } from 'react-router-dom';
+import { ProductsList } from '../../components/ProductsList';
+
+export const PhonesPage: React.FC = () => {
+ const location = useLocation();
+
+ const category = location.pathname.split('/')[1];
+
+ return ;
+};
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..18a872986d
--- /dev/null
+++ b/src/modules/ProductDetailsPage/ProductDetailsPage.module.scss
@@ -0,0 +1,50 @@
+@import '../../styles/main';
+
+.productDetailsPage {
+ display: flex;
+ flex-direction: column;
+ gap: 80px;
+}
+
+.container {
+ @include section-grid;
+}
+
+.breadcrumbs {
+ grid-column: 1 / -1;
+}
+
+.goBackButton {
+ margin-top: 16px;
+ grid-column: 1 / -1;
+ display: flex;
+ align-items: center;
+ overflow: hidden;
+ gap: 8px;
+ height: 16px;
+ width: 66px;
+ border: 0;
+ cursor: pointer;
+ background-color: transparent;
+}
+
+.goBackText {
+ display: block;
+ padding-top: 2px;
+
+ @extend %small-text;
+
+ color: var(--color-secondary);
+}
+
+.title {
+ grid-column: 1 / -1;
+ margin-bottom: 40px;
+
+ @extend %h2-mobile;
+
+ @include on-tablet {
+ @include h2-tablet;
+ }
+}
+
diff --git a/src/modules/ProductDetailsPage/ProductDetailsPage.tsx b/src/modules/ProductDetailsPage/ProductDetailsPage.tsx
new file mode 100644
index 0000000000..b73c7af76c
--- /dev/null
+++ b/src/modules/ProductDetailsPage/ProductDetailsPage.tsx
@@ -0,0 +1,102 @@
+import React, { useEffect, useState } from 'react';
+import { useParams, useNavigate } from 'react-router-dom';
+import { ProductDetails } from '../../types/ProductDetails';
+import { getProductDetails } from '../../servises/ProductsDetails';
+import styles from './ProductDetailsPage.module.scss';
+import { Breadcrumbs } from '../../components/Breadcrumbs/Breadcrumbs';
+import { getChevronIconSrc } from '../../servises/iconSrc';
+import { useTheme } from '../../context/ThemeContext';
+import { Product } from '../../types/Product';
+import { getAllProducts, getSuggestedProducts } from '../../servises/Products';
+import { ImageGallery } from './components/ImageGallery';
+import { TechSpecs } from './components/TechSpecs';
+import { Description } from './components/Description';
+import { MainControls } from './components/MainControls';
+import { Loader } from '../../components/Loader';
+import { ProductsSlider } from '../../components/ProductsSlider';
+
+const ProductDetailsPage: React.FC = () => {
+ const { productId } = useParams<{
+ productId: string;
+ }>();
+ const [productDetails, setProductDetails] = useState(
+ null,
+ );
+ const [product, setProduct] = useState();
+ const [isLoading, setIsLoading] = useState(true);
+ const [suggestedProducts, setSuggestedProducts] = useState([]);
+ const navigate = useNavigate();
+ const { theme } = useTheme();
+ const chevronIconSrc = getChevronIconSrc(theme);
+
+ useEffect(() => {
+ const fetchProductData = async () => {
+ setIsLoading(true);
+ try {
+ const allProducts: Product[] = await getAllProducts();
+ const productMatch = allProducts.find(p => p.itemId === productId);
+
+ setProduct(productMatch);
+
+ const cugettedProducts: Product[] = await getSuggestedProducts();
+
+ setSuggestedProducts(cugettedProducts);
+
+ if (productId && productMatch) {
+ const detailedProduct = await getProductDetails(
+ productId,
+ productMatch.category,
+ );
+
+ setProductDetails(detailedProduct);
+ } else {
+ setProductDetails(null);
+ }
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ if (productId) {
+ fetchProductData();
+ }
+ }, [productId]);
+
+ if (isLoading) {
+ return ;
+ }
+
+ if (!productDetails || !product) {
+ return No product found or failed to load product details.
;
+ }
+
+ return (
+
+
+
+
+
{productDetails.name}
+
+
+
+
+
+
+
+
+ );
+};
+
+export default ProductDetailsPage;
diff --git a/src/modules/ProductDetailsPage/components/Description/Description.module.scss b/src/modules/ProductDetailsPage/components/Description/Description.module.scss
new file mode 100644
index 0000000000..db5e9ad540
--- /dev/null
+++ b/src/modules/ProductDetailsPage/components/Description/Description.module.scss
@@ -0,0 +1,60 @@
+@import '../../../../styles/main';
+
+
+.section {
+ grid-column: 1 / -1;
+
+ @include on-desktop {
+ grid-column: 1 / 13;
+ }
+}
+
+.descriptionSection {
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+}
+
+.sectionTitle {
+ @extend %h3-mobile;
+
+ color: var(--color-primary);
+
+ @include on-tablet {
+ font-family: Mont, sans-serif;
+ font-size: 22px;
+ line-height: 31.8px;
+ letter-spacing: 0;
+ font-weight: 800;
+ color: var(--color-primary);
+ }
+}
+
+.divider {
+ width: 100%;
+ height: 1px;
+ background-color: var(--color-elements);
+}
+
+.descriptionTitle {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+
+ @extend %h4-mobile;
+
+ @include on-tablet {
+ font-family: Mont, sans-serif;
+ font-size: 20px;
+ line-height: 26px;
+ font-weight: 600;
+ letter-spacing: 0;
+ color: var(--color-primary);
+ }
+}
+
+.descriptionText {
+ @extend %body-text;
+
+ color: var(--color-secondary);
+}
diff --git a/src/modules/ProductDetailsPage/components/Description/Description.tsx b/src/modules/ProductDetailsPage/components/Description/Description.tsx
new file mode 100644
index 0000000000..80e2e2bc5b
--- /dev/null
+++ b/src/modules/ProductDetailsPage/components/Description/Description.tsx
@@ -0,0 +1,36 @@
+import React from 'react';
+import styles from './Description.module.scss';
+import classNames from 'classnames';
+
+type DescriptionProps = {
+ description: {
+ map(
+ arg0: (
+ desc: { title: string; text: string[] },
+ index: number,
+ ) => import('react/jsx-runtime').JSX.Element,
+ ): React.ReactNode;
+ };
+};
+
+export const Description: React.FC = ({ description }) => {
+ return (
+
+ About
+
+
+ {description.map(
+ (desc: { title: string; text: string[] }, index: number) => (
+
+ {desc.title}
+ {desc.text.map((paragraph, idx) => (
+
+ {paragraph}
+
+ ))}
+
+ ),
+ )}
+
+ );
+};
diff --git a/src/modules/ProductDetailsPage/components/Description/index.ts b/src/modules/ProductDetailsPage/components/Description/index.ts
new file mode 100644
index 0000000000..2b6c4564b9
--- /dev/null
+++ b/src/modules/ProductDetailsPage/components/Description/index.ts
@@ -0,0 +1 @@
+export * from './Description';
diff --git a/src/modules/ProductDetailsPage/components/ImageGallery/ImageGallery.module.scss b/src/modules/ProductDetailsPage/components/ImageGallery/ImageGallery.module.scss
new file mode 100644
index 0000000000..b519d4d375
--- /dev/null
+++ b/src/modules/ProductDetailsPage/components/ImageGallery/ImageGallery.module.scss
@@ -0,0 +1,109 @@
+@import '../../../../styles/main';
+
+.imageGallery {
+ grid-column: 1 / -1;
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+ margin-bottom: 24px;
+
+ @include on-tablet {
+ flex-direction: row;
+ grid-column: 1 / 8;
+ }
+
+ @include on-desktop {
+ grid-column: 1 / 13;
+ }
+}
+
+.thumbnailContainerTablet {
+ display: none;
+
+ @include on-tablet {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 8px;
+
+
+ @include on-desktop {
+ gap: 16px;
+ }
+
+ }
+}
+
+.mainImageContainer {
+ display: flex;
+ justify-content: center;
+ width: 100%;
+ position: relative;
+
+ cursor: pointer;
+ height: 288px;
+
+ @include middle-screen {
+ height: 360px;
+ }
+
+ @include on-desktop {
+ height: 464px;
+ }
+}
+
+.mainImage {
+ width: 100%;
+ height: 100%;
+ object-fit: contain;
+ position: absolute;
+ top: 0;
+ left: 0;
+ transition: opacity 0.5s ease;
+ opacity: 0;
+}
+
+.mainImageActive {
+ opacity: 1;
+}
+
+.thumbnailContainerMobile {
+ grid-column: 1 / -1;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ gap: 8px;
+ width: 100%;
+
+ @include on-tablet {
+ display: none;
+ }
+}
+
+.thumbnail {
+ width: 50px;
+ height: 50px;
+ object-fit: contain;
+ cursor: pointer;
+ border: 1px solid var(--color-elements);
+ transition: transform $transition-duration ease;
+
+ @include middle-screen {
+ width: 60px;
+ height: 60px;
+ }
+
+ @include on-desktop {
+ width: 80px;
+ height: 80px;
+ }
+
+ &:hover {
+ transform: scale(1.1);
+ }
+}
+
+.selectedThumbnail {
+ border-color: var(--color-primary);
+}
+
diff --git a/src/modules/ProductDetailsPage/components/ImageGallery/ImageGallery.tsx b/src/modules/ProductDetailsPage/components/ImageGallery/ImageGallery.tsx
new file mode 100644
index 0000000000..9c391a7cff
--- /dev/null
+++ b/src/modules/ProductDetailsPage/components/ImageGallery/ImageGallery.tsx
@@ -0,0 +1,69 @@
+import React, { useEffect, useState } from 'react';
+import styles from './ImageGallery.module.scss';
+import classNames from 'classnames';
+import { BASE_URL } from '../../../../utils/const';
+
+type ImageGalleryProps = {
+ images: string[];
+ productName: string;
+};
+
+export const ImageGallery: React.FC = ({
+ images,
+ productName,
+}) => {
+ const [selectedImage, setSelectedImage] = useState(images[0]);
+ const [activeIndex, setActiveIndex] = useState(0);
+
+ useEffect(() => {
+ const index = images.indexOf(selectedImage);
+
+ if (index !== activeIndex) {
+ setActiveIndex(index);
+ }
+ }, [selectedImage, images, activeIndex]);
+
+ return (
+
+
+ {images.map(image => (
+
setSelectedImage(image)}
+ />
+ ))}
+
+
+
+ {images.map((image, index) => (
+
+ ))}
+
+
+ {images.map(image => (
+
setSelectedImage(image)}
+ />
+ ))}
+
+
+ );
+};
diff --git a/src/modules/ProductDetailsPage/components/ImageGallery/index.ts b/src/modules/ProductDetailsPage/components/ImageGallery/index.ts
new file mode 100644
index 0000000000..9d76db4f79
--- /dev/null
+++ b/src/modules/ProductDetailsPage/components/ImageGallery/index.ts
@@ -0,0 +1 @@
+export * from './ImageGallery';
diff --git a/src/modules/ProductDetailsPage/components/MainControls/MainControls.module.scss b/src/modules/ProductDetailsPage/components/MainControls/MainControls.module.scss
new file mode 100644
index 0000000000..0b42683edf
--- /dev/null
+++ b/src/modules/ProductDetailsPage/components/MainControls/MainControls.module.scss
@@ -0,0 +1,121 @@
+@import '../../../../styles/main';
+
+.mainControls {
+ grid-column: 1 / -1;
+ display: flex;
+ flex-direction: column;
+ gap: 24px;
+
+ @include on-tablet {
+ grid-column: 8 / -1;
+ }
+
+ @include on-desktop {
+ grid-column: 14 / -5;
+
+ }
+}
+
+.divider {
+ width: 100%;
+ height: 1px;
+ background-color: var(--color-elements);
+}
+
+.selector {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+
+ @extend %small-text;
+
+ color: var(--color-secondary);
+}
+
+.buttons {
+ display: flex;
+ gap: 8px;
+}
+
+.colorButtonContainer {
+ width: 32px;
+ height: 32px;
+ border: 2px solid var(--color-surface);
+ border-radius: 50%;
+ padding: 2px;
+}
+
+.colorButton {
+ width: 100%;
+ height: 100%;
+ border-radius: 50%;
+ border: 0;
+ padding: 4px;
+ cursor: pointer;
+
+}
+
+.activeColorsAvailable {
+ border: 2px solid var(--color-primary);
+}
+
+.capacityButton {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding-top: 3px;
+ padding-inline: 4px;
+ height: 32px;
+ border: 1px solid var(--color-surface);
+ background-color: transparent;
+ color: var(--color-primary);
+ cursor: pointer;
+
+ @extend %body-text;
+
+}
+
+.active {
+ border: 1px solid var(--color-primary);
+ background-color: var(--color-primary);
+ color: var(--color-background);
+}
+
+.price {
+ display: flex;
+ gap: 8px;
+
+ @extend %h3-tablet;
+}
+
+.existPrice {
+ color: var(--color-primary);
+}
+
+.hotPrice {
+ display: flex;
+ color: var(--color-secondary);
+}
+
+.specsList {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+}
+
+.specs {
+ display: flex;
+ justify-content: space-between;
+}
+
+.specsKey {
+ @extend %body-text;
+
+ color: var(--color-secondary);
+}
+
+.specsValue {
+ @extend %body-text;
+
+ color: var(--color-primary);
+}
diff --git a/src/modules/ProductDetailsPage/components/MainControls/MainControls.tsx b/src/modules/ProductDetailsPage/components/MainControls/MainControls.tsx
new file mode 100644
index 0000000000..ed5b1913ea
--- /dev/null
+++ b/src/modules/ProductDetailsPage/components/MainControls/MainControls.tsx
@@ -0,0 +1,109 @@
+import React from 'react';
+import classNames from 'classnames';
+import styles from './MainControls.module.scss';
+import { ProductDetails } from '../../../../types/ProductDetails';
+import { ActionButtons } from '../../../../components/ActionButtons';
+import { Product } from '../../../../types/Product';
+// eslint-disable-next-line max-len
+import { fetchProductByColorAndCapacity } from '../../../../servises/ProductsDetails';
+import { useNavigate } from 'react-router-dom';
+
+type Props = {
+ productDetails: ProductDetails;
+ setProductDetails: (productDetails: ProductDetails) => void;
+ product: Product;
+};
+
+export const MainControls: React.FC = ({ productDetails, product }) => {
+ const navigate = useNavigate();
+
+ const handleAttributeChange = async (color: string, capacity: string) => {
+ const newProductId = await fetchProductByColorAndCapacity(
+ productDetails.category,
+ productDetails.namespaceId,
+ color,
+ capacity,
+ );
+
+ if (newProductId) {
+ navigate(`/products/${newProductId}`);
+ }
+ };
+
+ return (
+
+
+
Available colors
+
+
+ {productDetails.colorsAvailable.map(color => (
+
+
+ ))}
+
+
+
+
+
+
+
Select Capacity
+
+
+ {productDetails.capacityAvailable.map(capacity => (
+
+ ))}
+
+
+
+
+
+
+
${productDetails.priceRegular}
+
${productDetails.priceDiscount}
+
+
+ {product &&
}
+
+
+ -
+ Screen
+ {productDetails.screen}
+
+
+ -
+ Resolution
+ {productDetails.resolution}
+
+
+ -
+ Processor
+ {productDetails.processor}
+
+
+
+ );
+};
diff --git a/src/modules/ProductDetailsPage/components/MainControls/index.ts b/src/modules/ProductDetailsPage/components/MainControls/index.ts
new file mode 100644
index 0000000000..0444e1d193
--- /dev/null
+++ b/src/modules/ProductDetailsPage/components/MainControls/index.ts
@@ -0,0 +1 @@
+export * from './MainControls';
diff --git a/src/modules/ProductDetailsPage/components/TechSpecs/TechSpecs.module.scss b/src/modules/ProductDetailsPage/components/TechSpecs/TechSpecs.module.scss
new file mode 100644
index 0000000000..4e56cd8c2f
--- /dev/null
+++ b/src/modules/ProductDetailsPage/components/TechSpecs/TechSpecs.module.scss
@@ -0,0 +1,59 @@
+@import '../../../../styles/main';
+
+.section {
+ grid-column: 1 / -1;
+
+ @include on-desktop {
+ grid-column: 14 / -1;
+ }
+}
+
+.techSpecsSection {
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+}
+
+.sectionTitle {
+ @extend %h3-mobile;
+
+ color: var(--color-primary);
+
+ @include on-tablet {
+ font-family: Mont, sans-serif;
+ font-size: 22px;
+ line-height: 31.8px;
+ letter-spacing: 0;
+ font-weight: 800;
+ color: var(--color-primary);
+ }
+}
+
+.divider {
+ width: 100%;
+ height: 1px;
+ background-color: var(--color-elements);
+}
+
+.specsList {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+}
+
+.specs {
+ display: flex;
+ justify-content: space-between;
+}
+
+.specsKey {
+ @extend %body-text;
+
+ color: var(--color-secondary);
+}
+
+.specsValue {
+ @extend %body-text;
+
+ color: var(--color-primary);
+}
diff --git a/src/modules/ProductDetailsPage/components/TechSpecs/TechSpecs.tsx b/src/modules/ProductDetailsPage/components/TechSpecs/TechSpecs.tsx
new file mode 100644
index 0000000000..d55ae622bd
--- /dev/null
+++ b/src/modules/ProductDetailsPage/components/TechSpecs/TechSpecs.tsx
@@ -0,0 +1,65 @@
+import React from 'react';
+import styles from './TechSpecs.module.scss';
+import { ProductDetails } from '../../../../types/ProductDetails';
+import classNames from 'classnames';
+
+type TechSpecsProps = {
+ productDetails: ProductDetails;
+};
+
+export const TechSpecs: React.FC = ({ productDetails }) => {
+ return (
+
+ Tech specs
+
+
+
+ -
+ Screen
+ {productDetails.screen}
+
+
+ -
+ Resolution
+ {productDetails.resolution}
+
+
+ -
+ Processor
+ {productDetails.processor}
+
+
+ -
+ Ram
+ {productDetails.ram}
+
+
+ -
+ Built in memory
+ {productDetails.capacity}
+
+
+ {productDetails.camera && (
+ -
+ Camera
+ {productDetails.camera}
+
+ )}
+
+ {productDetails.zoom && (
+ -
+ Zoom
+ {productDetails.zoom}
+
+ )}
+
+ -
+ Cell
+
+ {productDetails.cell.join(', ')}
+
+
+
+
+ );
+};
diff --git a/src/modules/ProductDetailsPage/components/TechSpecs/index.ts b/src/modules/ProductDetailsPage/components/TechSpecs/index.ts
new file mode 100644
index 0000000000..eada3132a0
--- /dev/null
+++ b/src/modules/ProductDetailsPage/components/TechSpecs/index.ts
@@ -0,0 +1 @@
+export * from './TechSpecs';
diff --git a/src/modules/ProductDetailsPage/index.ts b/src/modules/ProductDetailsPage/index.ts
new file mode 100644
index 0000000000..55be825664
--- /dev/null
+++ b/src/modules/ProductDetailsPage/index.ts
@@ -0,0 +1 @@
+export { default as ProductDetailsPage } from './ProductDetailsPage';
diff --git a/src/modules/TabletsPage/TabletsPage.scss b/src/modules/TabletsPage/TabletsPage.scss
new file mode 100644
index 0000000000..c177aaf35d
--- /dev/null
+++ b/src/modules/TabletsPage/TabletsPage.scss
@@ -0,0 +1,5 @@
+.tablets-page {
+ display: flex;
+ align-items: center;
+ width: 100%;
+}
diff --git a/src/modules/TabletsPage/TabletsPage.tsx b/src/modules/TabletsPage/TabletsPage.tsx
new file mode 100644
index 0000000000..5347ae1e5b
--- /dev/null
+++ b/src/modules/TabletsPage/TabletsPage.tsx
@@ -0,0 +1,11 @@
+import React from 'react';
+import { useLocation } from 'react-router-dom';
+import { ProductsList } from '../../components/ProductsList';
+
+export const TabletsPage: React.FC = () => {
+ const location = useLocation();
+
+ const category = location.pathname.split('/')[1];
+
+ return ;
+};
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/servises/Products.ts b/src/servises/Products.ts
new file mode 100644
index 0000000000..5c54b373ff
--- /dev/null
+++ b/src/servises/Products.ts
@@ -0,0 +1,40 @@
+import { Product } from '../types/Product';
+import { ShuffleArray } from '../utils/ShaffleArray';
+import { getData } from '../utils/httpClient';
+
+export const getAllProducts = async (): Promise => {
+ return getData('/api/products.json');
+};
+
+export const getProductsByCategory = async (category: string) => {
+ const products = await getData('/api/products.json');
+
+ return products.filter((product: Product) => product.category === category);
+};
+
+export const getHotPriceProducts = async () => {
+ const response = await getData('/api/products.json');
+
+ return response.sort((a: Product, b: Product) => {
+ return b.fullPrice - b.price - (a.fullPrice - a.price);
+ });
+};
+
+export const getNewProducts = async () => {
+ const response = await getData('/api/products.json');
+ const latestYear = response.reduce(
+ (acc: number, product: Product) => Math.max(acc, product.year),
+ 0,
+ );
+
+ return response
+ .filter((product: Product) => product.year === latestYear)
+ .sort((a: Product, b: Product) => b.fullPrice - a.fullPrice);
+};
+
+export const getSuggestedProducts = async () => {
+ const products = await getData('/api/products.json');
+ const suggestedProducts = ShuffleArray(products);
+
+ return suggestedProducts;
+};
diff --git a/src/servises/ProductsDetails.ts b/src/servises/ProductsDetails.ts
new file mode 100644
index 0000000000..5b55022807
--- /dev/null
+++ b/src/servises/ProductsDetails.ts
@@ -0,0 +1,32 @@
+import { ProductDetails } from '../types/ProductDetails';
+import { getData } from '../utils/httpClient';
+
+export const getProductDetails = async (
+ itemId: string,
+ category = 'products',
+): Promise => {
+ const categoryProducts: ProductDetails[] = await getData(
+ `/api/${category}.json`,
+ );
+ const detailedProduct = categoryProducts.find(p => p.id === itemId);
+
+ return detailedProduct ?? null;
+};
+
+export const fetchProductByColorAndCapacity = async (
+ category: string,
+ namespaceId: string,
+ color: string,
+ capacity: string,
+): Promise => {
+ const allVariants = await getData(`/api/${category}.json`);
+
+ const matchedProduct = allVariants.find(
+ p =>
+ p.namespaceId === namespaceId &&
+ p.color === color &&
+ p.capacity === capacity,
+ );
+
+ return matchedProduct ? matchedProduct.id : undefined;
+};
diff --git a/src/servises/iconSrc.ts b/src/servises/iconSrc.ts
new file mode 100644
index 0000000000..fb2b4895fb
--- /dev/null
+++ b/src/servises/iconSrc.ts
@@ -0,0 +1,40 @@
+import { ThemeType } from '../types/ThemeType';
+import { BASE_URL } from '../utils/const';
+
+const ICON_BASE_PATH = `${BASE_URL}/img/icons`;
+
+const getIconSrc = (iconName: string, theme: ThemeType): string => {
+ const themePath = theme === 'dark' ? `--DarkTheme` : '';
+
+ return `${ICON_BASE_PATH}/${iconName}${themePath}.svg`;
+};
+
+export const getLogoIconSrc = (theme: ThemeType): string =>
+ getIconSrc('LogoIcon', theme);
+export const getMenuIconSrc = (
+ isMenuOpen: boolean,
+ theme: ThemeType,
+): string => {
+ return getIconSrc(isMenuOpen ? 'CloseMenuIcon' : 'MenuIcon', theme);
+};
+
+export const getFavoritesIconSrc = (theme: ThemeType): string =>
+ getIconSrc('FavoritesIcon', theme);
+
+export const getCartIconSrc = (theme: ThemeType): string =>
+ getIconSrc('CartIcon', theme);
+
+export const getChevronIconSrc = (theme: ThemeType): string =>
+ getIconSrc('ChevronIcon', theme);
+
+export const getHomeIconSrc = (theme: ThemeType): string =>
+ getIconSrc('HomeIcon', theme);
+
+export const getRemoveIconSrc = (theme: ThemeType): string =>
+ getIconSrc('CrossIcon', theme);
+
+export const getIncreaseIconSrc = (theme: ThemeType): string =>
+ getIconSrc('PlusIcon', theme);
+
+export const getDecreaseIconSrc = (theme: ThemeType): string =>
+ getIconSrc('MinusIcon', theme);
diff --git a/src/styles/main.scss b/src/styles/main.scss
new file mode 100644
index 0000000000..2ab6869999
--- /dev/null
+++ b/src/styles/main.scss
@@ -0,0 +1,4 @@
+@import './utils/typography';
+@import './utils/variables';
+@import './utils/mixins';
+@import './utils/fonts';
diff --git a/src/styles/utils/_fonts.scss b/src/styles/utils/_fonts.scss
new file mode 100644
index 0000000000..69131d8c22
--- /dev/null
+++ b/src/styles/utils/_fonts.scss
@@ -0,0 +1,14 @@
+@font-face {
+ font-family: Mont;
+ src: url('/fonts/Mont-Regular.otf') format('opentype');
+}
+
+@font-face {
+ font-family: Mont;
+ src: url('/fonts/Mont-SemiBold.otf') format('opentype');
+}
+
+@font-face {
+ font-family: Mont;
+ src: url('/fonts/Mont-Bold.otf') format('opentype');
+}
diff --git a/src/styles/utils/_mixins.scss b/src/styles/utils/_mixins.scss
new file mode 100644
index 0000000000..f9b9c8e812
--- /dev/null
+++ b/src/styles/utils/_mixins.scss
@@ -0,0 +1,48 @@
+@import './variables';
+
+@mixin on-tablet {
+ @media (min-width: $breakpoint-tablet) {
+ @content;
+ }
+}
+
+@mixin on-desktop {
+ @media (min-width: $breakpoint-desktop) {
+ @content;
+ }
+}
+
+@mixin middle-screen {
+ @media (min-width: $breakpoint-middle) {
+ @content;
+ }
+}
+
+@mixin padding-inline {
+ padding-inline:16px;
+
+ @include on-tablet {
+ padding-inline: 24px;
+ }
+
+ @include on-desktop {
+ padding-inline: 0;
+ }
+}
+
+@mixin section-grid {
+ display: grid;
+ grid-gap: 16px;
+ grid-template-columns:repeat(4, 1fr);
+ justify-content: center;
+
+ @include padding-inline;
+
+ @include on-tablet {
+ grid-template-columns:repeat(12, 1fr);
+ }
+
+ @include on-desktop {
+ grid-template-columns:repeat(24, 32px);
+ }
+}
diff --git a/src/styles/utils/_typography.scss b/src/styles/utils/_typography.scss
new file mode 100644
index 0000000000..467803e356
--- /dev/null
+++ b/src/styles/utils/_typography.scss
@@ -0,0 +1,125 @@
+@mixin h1-tablet {
+ font-family: Mont, sans-serif;
+ font-size: 48px;
+ line-height: 56px;
+ letter-spacing: -0.01em;
+ font-weight: bold;
+ color: var(--color-primary);
+}
+
+@mixin h2-tablet {
+ font-family: Mont, sans-serif;
+ font-size: 32px;
+ line-height: 41px;
+ letter-spacing: 0.01em;
+ font-weight: bold;
+ color: var(--color-primary);
+}
+
+%h1-tablet {
+ font-family: Mont, sans-serif;
+ font-size: 48px;
+ line-height: 56px;
+ letter-spacing: -0.01em;
+ font-weight: bold;
+ color: var(--color-primary);
+}
+
+%h2-tablet {
+ font-family: Mont, sans-serif;
+ font-size: 32px;
+ line-height: 41px;
+ letter-spacing: 0.01em;
+ font-weight: bold;
+ color: var(--color-primary);
+}
+
+%h3-tablet {
+ font-family: Mont, sans-serif;
+ font-size: 22px;
+ line-height: 31.8px;
+ letter-spacing: 0;
+ font-weight: 800;
+ color: var(--color-primary);
+}
+
+%h4-tablet {
+ font-family: Mont, sans-serif;
+ font-size: 20px;
+ line-height: 26px;
+ font-weight: 600;
+ letter-spacing: 0;
+ color: var(--color-primary);
+}
+
+%uppercase-text {
+ font-family: Mont, sans-serif;
+ font-weight: bold;
+ font-size: 12px;
+ line-height: 11px;
+ letter-spacing: 0.04em;
+ text-transform: uppercase;
+ text-decoration: none;
+}
+
+%buttons {
+ font-family: Mont, sans-serif;
+ font-weight: 600;
+ font-size: 12px;
+ line-height: 11px;
+ letter-spacing: 0;
+}
+
+%body-text {
+ font-family: Mont, sans-serif;
+ font-weight: 500;
+ font-size: 14px;
+ line-height: 21px;
+ letter-spacing: 0;
+}
+
+%small-text {
+ font-family: Mont, sans-serif;
+ font-weight: 700;
+ font-size: 12px;
+ line-height: 15.34px;
+ letter-spacing: 0;
+}
+
+
+
+%h1-mobile {
+ font-family: Mont, sans-serif;
+ font-size: 32px;
+ line-height: 41px;
+ letter-spacing: -0.01em;
+ font-weight: bold;
+ color: var(--color-primary);
+}
+
+%h2-mobile {
+ font-family: Mont, sans-serif;
+ font-size: 22px;
+ line-height: 31px;
+ letter-spacing: 0;
+ font-weight: bold;
+ color: var(--color-primary);
+}
+
+%h3-mobile {
+ font-family: Mont, sans-serif;
+ font-size: 20px;
+ line-height: 26px;
+ letter-spacing: 0;
+ font-weight: 600;
+ color: var(--color-primary);
+}
+
+%h4-mobile {
+ font-family: Mont, sans-serif;
+ font-size: 16px;
+ line-height: 20px;
+ letter-spacing: 0;
+ font-weight: 600;
+ color: var(--color-primary);
+}
diff --git a/src/styles/utils/_variables.scss b/src/styles/utils/_variables.scss
new file mode 100644
index 0000000000..d754427212
--- /dev/null
+++ b/src/styles/utils/_variables.scss
@@ -0,0 +1,40 @@
+$breakpoint-tablet: 640px;
+$breakpoint-middle: 768px;
+$breakpoint-desktop: 1200px;
+$with-icons-desktop: 64px;
+$with-icons-tablet: 48px;
+$height-on-desktop: 64px;
+$height-on-tablet: 48px;
+$transition-duration: 0.5s;
+
+:root {
+ --color-background: #fff;
+ --color-surface: #B4BDC3;
+ --color-primary: #313237;
+ --color-secondary: #89939A;
+ --color-icons: #fff;
+ --color-elements: #E2E6E9;
+ --color-hover: #FAFBFC;
+ --color-red: #EB5757;
+ --color-button: #313237;
+ --color-button-hover: #313237;
+ --color-white: #fff;
+ --color-black: #000;
+ --color-filter-bg:#fff
+}
+
+[data-theme="dark"] {
+ --color-background: #0F1121;
+ --color-surface: #323542;
+ --color-primary: #F1F2F9;
+ --color-secondary: #75767F;
+ --color-icons: #4A4D58;
+ --color-elements: #313237;
+ --color-hover: #89939A;
+ --color-button: #905BFF;
+ --color-button-hover: #A378FF;
+ --color-red: #EB5757;
+ --color-white: #fff;
+ --color-black: #000;
+ --color-filter-bg:#323542;
+}
diff --git a/src/types/CartItemProps.ts b/src/types/CartItemProps.ts
new file mode 100644
index 0000000000..dca696f8c2
--- /dev/null
+++ b/src/types/CartItemProps.ts
@@ -0,0 +1,6 @@
+import { Product } from './Product';
+
+export interface CartItemProps {
+ product: Product;
+ quantity: number;
+}
diff --git a/src/types/Filter.ts b/src/types/Filter.ts
new file mode 100644
index 0000000000..c392c7781b
--- /dev/null
+++ b/src/types/Filter.ts
@@ -0,0 +1,12 @@
+export enum FilterType {
+ age = 'Newest',
+ title = 'Alphabetically',
+ price = 'Cheapest',
+}
+
+export enum ItemsPerPage {
+ Four = '4',
+ Eight = '8',
+ Sixteen = '16',
+ All = 'All',
+}
diff --git a/src/types/Header.ts b/src/types/Header.ts
new file mode 100644
index 0000000000..0154760c02
--- /dev/null
+++ b/src/types/Header.ts
@@ -0,0 +1,4 @@
+export interface HeaderProps {
+ isMenuOpen: boolean;
+ setIsMenuOpen: React.Dispatch>;
+}
diff --git a/src/types/OverlayMenu.ts b/src/types/OverlayMenu.ts
new file mode 100644
index 0000000000..1a1deb49ed
--- /dev/null
+++ b/src/types/OverlayMenu.ts
@@ -0,0 +1,11 @@
+import { CartItemProps } from './CartItemProps';
+import { Product } from './Product';
+
+export interface OverlayMenuProps {
+ isMenuOpen: boolean;
+ toggleIsMenuOpen: () => void;
+ favoritesIconSrc: string;
+ favorites: Product[];
+ cartIconSrc: string;
+ cart: CartItemProps[];
+}
diff --git a/src/types/Product.ts b/src/types/Product.ts
new file mode 100644
index 0000000000..afb02cd58c
--- /dev/null
+++ b/src/types/Product.ts
@@ -0,0 +1,15 @@
+export interface Product {
+ id: string;
+ category: string;
+ phoneId: string;
+ 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..80067fcc72
--- /dev/null
+++ b/src/types/ProductDetails.ts
@@ -0,0 +1,32 @@
+interface ProductDescription {
+ map(
+ arg0: (
+ desc: { title: string; text: string[] },
+ index: number,
+ ) => import('react/jsx-runtime').JSX.Element,
+ ): import('react').ReactNode;
+ title: string;
+ text: string[];
+}
+
+export interface ProductDetails {
+ id: string;
+ category: string;
+ namespaceId: string;
+ name: string;
+ capacityAvailable: string[];
+ capacity: string;
+ priceRegular: number;
+ priceDiscount: number;
+ colorsAvailable: string[];
+ color: string;
+ images: string[];
+ description: ProductDescription;
+ screen: string;
+ resolution: string;
+ processor: string;
+ ram: string;
+ camera: string;
+ zoom: string;
+ cell: string[];
+}
diff --git a/src/types/ThemeType.ts b/src/types/ThemeType.ts
new file mode 100644
index 0000000000..41861f3924
--- /dev/null
+++ b/src/types/ThemeType.ts
@@ -0,0 +1,4 @@
+export enum ThemeType {
+ DARK = 'dark',
+ LIGHT = 'light',
+}
diff --git a/src/utils/ShaffleArray.ts b/src/utils/ShaffleArray.ts
new file mode 100644
index 0000000000..f21a7a7d1d
--- /dev/null
+++ b/src/utils/ShaffleArray.ts
@@ -0,0 +1,13 @@
+import { Product } from '../types/Product';
+
+export const ShuffleArray = (array: Product[]) => {
+ const shuffledArray = array.slice();
+
+ for (let i = shuffledArray.length - 1; i > 0; i--) {
+ const j = Math.floor(Math.random() * (i + 1));
+
+ [shuffledArray[i], shuffledArray[j]] = [shuffledArray[j], shuffledArray[i]];
+ }
+
+ return shuffledArray;
+};
diff --git a/src/utils/const.ts b/src/utils/const.ts
new file mode 100644
index 0000000000..8122617b6f
--- /dev/null
+++ b/src/utils/const.ts
@@ -0,0 +1 @@
+export const BASE_URL = `https://oleksiinesteruk.github.io/react_phone-catalog`;
diff --git a/src/utils/httpClient.ts b/src/utils/httpClient.ts
new file mode 100644
index 0000000000..52ba067d11
--- /dev/null
+++ b/src/utils/httpClient.ts
@@ -0,0 +1,7 @@
+import { BASE_URL } from './const';
+
+export const getData = async (url: string): Promise => {
+ const response = await fetch(BASE_URL + url);
+
+ return response.json();
+};
diff --git a/src/utils/sortProducts.ts b/src/utils/sortProducts.ts
new file mode 100644
index 0000000000..520bddcc99
--- /dev/null
+++ b/src/utils/sortProducts.ts
@@ -0,0 +1,14 @@
+import { Product } from '../types/Product';
+
+export const sortProducts = (products: Product[], sortKey: string) => {
+ switch (sortKey) {
+ case 'age':
+ return [...products].sort((a, b) => b.year - a.year);
+ case 'title':
+ return [...products].sort((a, b) => a.name.localeCompare(b.name));
+ case 'price':
+ return [...products].sort((a, b) => a.price - b.price);
+ default:
+ return products;
+ }
+};
diff --git a/tsconfig.json b/tsconfig.json
index cfb168bb26..da76cc58f9 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -1,7 +1,8 @@
{
"extends": "@mate-academy/students-ts-config",
"include": [
- "src"
+ "src",
+ "declarations.d.ts"
],
"compilerOptions": {
"sourceMap": false,