diff --git a/package-lock.json b/package-lock.json
index 98392dc382..6c5d476953 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1566,6 +1566,24 @@
}
}
},
+ "@emotion/is-prop-valid": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.2.2.tgz",
+ "integrity": "sha512-uNsoYd37AFmaCdXlg6EYD1KaPOaRWRByMCYzbKUX4+hhMfrxdVSelShywL4JVaAeM/eHUOSprYBQls+/neX3pw==",
+ "requires": {
+ "@emotion/memoize": "^0.8.1"
+ }
+ },
+ "@emotion/memoize": {
+ "version": "0.8.1",
+ "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.1.tgz",
+ "integrity": "sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA=="
+ },
+ "@emotion/unitless": {
+ "version": "0.8.1",
+ "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.1.tgz",
+ "integrity": "sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ=="
+ },
"@eslint-community/eslint-utils": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz",
@@ -2421,9 +2439,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.8.1",
+ "resolved": "https://registry.npmjs.org/@mate-academy/scripts/-/scripts-1.8.1.tgz",
+ "integrity": "sha512-zRhcI3uiflwWXeODuyELMqh1j4s438nTRiXXQ+v6Cs95kWMrfUDuzmg6ZqqJuIPZtmpg0KTLghGBf38vKalW/g==",
"dev": true,
"requires": {
"@octokit/rest": "^17.11.2",
@@ -3468,6 +3486,11 @@
"resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz",
"integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw=="
},
+ "@types/stylis": {
+ "version": "4.2.5",
+ "resolved": "https://registry.npmjs.org/@types/stylis/-/stylis-4.2.5.tgz",
+ "integrity": "sha512-1Xve+NMN7FWjY14vLoY5tL3BVEQ/n42YLwaqJIPYhotZ9uBHt87VceMwWQpzmdEt2TNXIorIFG+YeCUUW7RInw=="
+ },
"@types/trusted-types": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
@@ -4719,6 +4742,11 @@
}
}
},
+ "camelize": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.1.tgz",
+ "integrity": "sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ=="
+ },
"caniuse-api": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/caniuse-api/-/caniuse-api-3.0.0.tgz",
@@ -5100,6 +5128,11 @@
"postcss-selector-parser": "^6.0.9"
}
},
+ "css-color-keywords": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz",
+ "integrity": "sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg=="
+ },
"css-declaration-sorter": {
"version": "6.4.1",
"resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-6.4.1.tgz",
@@ -5210,6 +5243,16 @@
"resolved": "https://registry.npmjs.org/css-select-base-adapter/-/css-select-base-adapter-0.1.1.tgz",
"integrity": "sha512-jQVeeRG70QI08vSTwf1jHxp74JoZsr2XSgETae8/xC8ovSnL2WF87GTLO86Sbwdt2lK4Umg4HnnwMO4YF3Ce7w=="
},
+ "css-to-react-native": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/css-to-react-native/-/css-to-react-native-3.2.0.tgz",
+ "integrity": "sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ==",
+ "requires": {
+ "camelize": "^1.0.0",
+ "css-color-keywords": "^1.0.0",
+ "postcss-value-parser": "^4.0.2"
+ }
+ },
"css-tree": {
"version": "1.0.0-alpha.37",
"resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.0.0-alpha.37.tgz",
@@ -12998,6 +13041,22 @@
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
},
+ "react-loader-spinner": {
+ "version": "6.1.6",
+ "resolved": "https://registry.npmjs.org/react-loader-spinner/-/react-loader-spinner-6.1.6.tgz",
+ "integrity": "sha512-x5h1Jcit7Qn03MuKlrWcMG9o12cp9SNDVHVJTNRi9TgtGPKcjKiXkou4NRfLAtXaFB3+Z8yZsVzONmPzhv2ErA==",
+ "requires": {
+ "react-is": "^18.2.0",
+ "styled-components": "^6.1.2"
+ },
+ "dependencies": {
+ "react-is": {
+ "version": "18.3.1",
+ "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz",
+ "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="
+ }
+ }
+ },
"react-refresh": {
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.11.0.tgz",
@@ -14044,6 +14103,11 @@
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="
},
+ "shallowequal": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz",
+ "integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ=="
+ },
"shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
@@ -14569,6 +14633,44 @@
"integrity": "sha512-Dj1Okke1C3uKKwQcetra4jSuk0DqbzbYtXipzFlFMZtowbF1x7BKJwB9AayVMyFARvU8EDrZdcax4At/452cAg==",
"dev": true
},
+ "styled-components": {
+ "version": "6.1.13",
+ "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-6.1.13.tgz",
+ "integrity": "sha512-M0+N2xSnAtwcVAQeFEsGWFFxXDftHUD7XrKla06QbpUMmbmtFBMMTcKWvFXtWxuD5qQkB8iU5gk6QASlx2ZRMw==",
+ "requires": {
+ "@emotion/is-prop-valid": "1.2.2",
+ "@emotion/unitless": "0.8.1",
+ "@types/stylis": "4.2.5",
+ "css-to-react-native": "3.2.0",
+ "csstype": "3.1.3",
+ "postcss": "8.4.38",
+ "shallowequal": "1.1.0",
+ "stylis": "4.3.2",
+ "tslib": "2.6.2"
+ },
+ "dependencies": {
+ "postcss": {
+ "version": "8.4.38",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz",
+ "integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==",
+ "requires": {
+ "nanoid": "^3.3.7",
+ "picocolors": "^1.0.0",
+ "source-map-js": "^1.2.0"
+ }
+ },
+ "source-map-js": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="
+ },
+ "tslib": {
+ "version": "2.6.2",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz",
+ "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q=="
+ }
+ }
+ },
"stylehacks": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-5.1.1.tgz",
@@ -14788,6 +14890,11 @@
"postcss-value-parser": "^4.2.0"
}
},
+ "stylis": {
+ "version": "4.3.2",
+ "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.2.tgz",
+ "integrity": "sha512-bhtUjWd/z6ltJiQwg0dUfxEJ+W+jdqQd8TbWLWyeIJHlnsqmGLRFFd8e5mA0AZi/zx90smXRlN66YMTcaSFifg=="
+ },
"sucrase": {
"version": "3.35.0",
"resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz",
@@ -14921,9 +15028,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.6.3",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz",
+ "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==",
"dev": true
}
}
diff --git a/package.json b/package.json
index 222c88a139..ea45891c8c 100644
--- a/package.json
+++ b/package.json
@@ -13,6 +13,7 @@
"classnames": "^2.5.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
+ "react-loader-spinner": "^6.1.6",
"react-router-dom": "^6.22.3",
"react-scripts": "5.0.1",
"react-transition-group": "^4.4.5"
@@ -20,7 +21,7 @@
"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.8.1",
"@mate-academy/students-ts-config": "latest",
"@mate-academy/stylelint-config": "latest",
"@types/node": "^16.18.80",
diff --git a/public/favicon.ico b/public/favicon.ico
new file mode 100644
index 0000000000..8f1a49d8f0
Binary files /dev/null and b/public/favicon.ico differ
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/favicons/apple.png b/public/img/favicons/apple.png
new file mode 100644
index 0000000000..db0136d8ea
Binary files /dev/null and b/public/img/favicons/apple.png differ
diff --git a/public/img/favicons/favicon.svg b/public/img/favicons/favicon.svg
new file mode 100644
index 0000000000..fa86701a2a
--- /dev/null
+++ b/public/img/favicons/favicon.svg
@@ -0,0 +1,5 @@
+
\ No newline at end of file
diff --git a/public/img/picthree.bdd2e0fc.png b/public/img/picthree.bdd2e0fc.png
deleted file mode 100644
index 28b5c4c99a..0000000000
Binary files a/public/img/picthree.bdd2e0fc.png and /dev/null differ
diff --git a/public/index.html b/public/index.html
index 4b622dad39..f2f174329a 100644
--- a/public/index.html
+++ b/public/index.html
@@ -3,6 +3,9 @@
+
+
+
Phone catalog
diff --git a/src/App.module.scss b/src/App.module.scss
new file mode 100644
index 0000000000..f7ae09493d
--- /dev/null
+++ b/src/App.module.scss
@@ -0,0 +1,15 @@
+@import './styles/utils';
+
+.app {
+ min-height: 100vh;
+ display: grid;
+ grid-template-rows: min-content 1fr min-content;
+ font-family: Mont, Arial, sans-serif;
+ color: var(--primary);
+
+ @include body-text;
+
+ &__main {
+ background-color: var(--hover-bg);
+ }
+}
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..e87426cfef 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -1,7 +1,26 @@
-import './App.scss';
+import { Outlet, useLocation } from 'react-router-dom';
+import { Footer } from './modules/Footer';
+import { Header } from './modules/Header';
-export const App = () => (
-
-
Product Catalog
-
-);
+import './styles/globals.scss';
+import styles from './App.module.scss';
+import { useEffect } from 'react';
+import { scrollToTop } from './utils/utility';
+
+export const App = () => {
+ const location = useLocation();
+
+ useEffect(() => {
+ scrollToTop();
+ }, [location.pathname]);
+
+ return (
+
+
+
+
+
+
+
+ );
+};
diff --git a/src/Root.tsx b/src/Root.tsx
new file mode 100644
index 0000000000..3ecd205112
--- /dev/null
+++ b/src/Root.tsx
@@ -0,0 +1,47 @@
+import { Route, Routes } from 'react-router-dom';
+
+import { App } from './App';
+import { HomePage } from './modules/HomePage';
+import { PhonesPage } from './modules/PhonesPage';
+import { MainNavigation } from './utils/constants';
+import { TabletsPage } from './modules/TabletsPage';
+import { AccessoriesPage } from './modules/AccessoriesPage';
+import { ProductPage } from './modules/ProductPage';
+import { FavouritesPage } from './modules/FavouritesPage';
+import { CartPage } from './modules/CartPage';
+import { NotFoundPage } from './modules/NotFoundPage';
+
+export const Root = () => {
+ return (
+
+ }>
+ } />
+
+ } />
+ }
+ />
+
+ } />
+ }
+ />
+
+ }
+ />
+ }
+ />
+
+ } />
+ } />
+ } />
+
+
+ );
+};
diff --git a/public/fonts/Mont-Bold.otf b/src/assets/fonts/Mont-Bold.otf
old mode 100755
new mode 100644
similarity index 100%
rename from public/fonts/Mont-Bold.otf
rename to src/assets/fonts/Mont-Bold.otf
diff --git a/public/fonts/Mont-Regular.otf b/src/assets/fonts/Mont-Regular.otf
old mode 100755
new mode 100644
similarity index 100%
rename from public/fonts/Mont-Regular.otf
rename to src/assets/fonts/Mont-Regular.otf
diff --git a/public/fonts/Mont-SemiBold.otf b/src/assets/fonts/Mont-SemiBold.otf
old mode 100755
new mode 100644
similarity index 100%
rename from public/fonts/Mont-SemiBold.otf
rename to src/assets/fonts/Mont-SemiBold.otf
diff --git a/src/assets/images/icons/arrow.svg b/src/assets/images/icons/arrow.svg
new file mode 100644
index 0000000000..02568eec7f
--- /dev/null
+++ b/src/assets/images/icons/arrow.svg
@@ -0,0 +1,5 @@
+
diff --git a/src/assets/images/icons/cart.svg b/src/assets/images/icons/cart.svg
new file mode 100644
index 0000000000..d76e7eb922
--- /dev/null
+++ b/src/assets/images/icons/cart.svg
@@ -0,0 +1,11 @@
+
diff --git a/src/assets/images/icons/close.svg b/src/assets/images/icons/close.svg
new file mode 100644
index 0000000000..44fe5e9c28
--- /dev/null
+++ b/src/assets/images/icons/close.svg
@@ -0,0 +1,5 @@
+
diff --git a/src/assets/images/icons/heart-like.svg b/src/assets/images/icons/heart-like.svg
new file mode 100644
index 0000000000..d6f5122dee
--- /dev/null
+++ b/src/assets/images/icons/heart-like.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/assets/images/icons/heart.svg b/src/assets/images/icons/heart.svg
new file mode 100644
index 0000000000..35afa4bb10
--- /dev/null
+++ b/src/assets/images/icons/heart.svg
@@ -0,0 +1,5 @@
+
diff --git a/src/assets/images/icons/home.svg b/src/assets/images/icons/home.svg
new file mode 100644
index 0000000000..5737c25522
--- /dev/null
+++ b/src/assets/images/icons/home.svg
@@ -0,0 +1,8 @@
+
diff --git a/src/assets/images/icons/menu.svg b/src/assets/images/icons/menu.svg
new file mode 100644
index 0000000000..d729715935
--- /dev/null
+++ b/src/assets/images/icons/menu.svg
@@ -0,0 +1,11 @@
+
diff --git a/src/assets/images/icons/moon.svg b/src/assets/images/icons/moon.svg
new file mode 100644
index 0000000000..74ff5d6b89
--- /dev/null
+++ b/src/assets/images/icons/moon.svg
@@ -0,0 +1,5 @@
+
\ No newline at end of file
diff --git a/src/assets/images/icons/search-dark.svg b/src/assets/images/icons/search-dark.svg
new file mode 100644
index 0000000000..1760357193
--- /dev/null
+++ b/src/assets/images/icons/search-dark.svg
@@ -0,0 +1,12 @@
+
diff --git a/src/assets/images/icons/search-light.svg b/src/assets/images/icons/search-light.svg
new file mode 100644
index 0000000000..929556143b
--- /dev/null
+++ b/src/assets/images/icons/search-light.svg
@@ -0,0 +1,12 @@
+
diff --git a/src/assets/images/icons/sun.svg b/src/assets/images/icons/sun.svg
new file mode 100644
index 0000000000..aa68592a72
--- /dev/null
+++ b/src/assets/images/icons/sun.svg
@@ -0,0 +1,3 @@
+
\ No newline at end of file
diff --git a/src/assets/images/logo-dark.svg b/src/assets/images/logo-dark.svg
new file mode 100644
index 0000000000..26676b7233
--- /dev/null
+++ b/src/assets/images/logo-dark.svg
@@ -0,0 +1,25 @@
+
diff --git a/src/assets/images/logo-light.svg b/src/assets/images/logo-light.svg
new file mode 100644
index 0000000000..b38b893097
--- /dev/null
+++ b/src/assets/images/logo-light.svg
@@ -0,0 +1,25 @@
+
diff --git a/src/components/Actions/Actions.module.scss b/src/components/Actions/Actions.module.scss
new file mode 100644
index 0000000000..b82a0e2a68
--- /dev/null
+++ b/src/components/Actions/Actions.module.scss
@@ -0,0 +1,56 @@
+@import '../../styles/utils';
+
+.actions {
+ display: flex;
+ justify-content: space-between;
+ gap: 8px;
+ width: 100%;
+ height: 40px;
+
+ &__btn-cart {
+ border: none;
+ flex: 1 1;
+ background-color: var(--accent);
+ color: var(--white);
+ border-radius: 8px;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ padding: 9px;
+
+ &--active {
+ background-color: inherit;
+ color: var(--accent);
+ box-shadow: 0 0 0 1px var(--elements);
+ }
+ }
+
+ &__btn-favourites {
+ height: 100%;
+ aspect-ratio: 1 / 1;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ border: 1px var(--icon-secondary) solid;
+ border-radius: 50%;
+ background-color: inherit;
+
+ @include transition;
+
+ svg path {
+ fill: var(--icon-primary);
+ }
+
+ &:hover {
+ border-color: var(--icon-primary);
+ }
+
+ &--active {
+ border-color: var(--elements);
+
+ svg path {
+ fill: var(--secondary-accent);
+ }
+ }
+ }
+}
diff --git a/src/components/Actions/Actions.tsx b/src/components/Actions/Actions.tsx
new file mode 100644
index 0000000000..372dd9e7cd
--- /dev/null
+++ b/src/components/Actions/Actions.tsx
@@ -0,0 +1,65 @@
+import cn from 'classnames';
+import styles from './Actions.module.scss';
+import { useContext } from 'react';
+import { SvgIcon } from '../SvgIcon';
+import { DispatchContext, StateContext } from '../../contex/State';
+import { Product } from '../../types';
+
+interface Props {
+ product: Product;
+ className?: string;
+}
+
+export const Actions: React.FC = ({ className, product }) => {
+ const { favourites, cart } = useContext(StateContext);
+ const dispatch = useContext(DispatchContext);
+
+ const { itemId } = product;
+
+ const isFavourite = favourites.find(p => p.itemId === itemId);
+ const isInCart = cart.find(p => p.id === itemId);
+
+ const changeFavouriteStatus = () => {
+ if (isFavourite) {
+ dispatch({ type: 'removeFavourites', payload: itemId });
+ } else {
+ dispatch({ type: 'addFavourites', payload: product });
+ }
+ };
+
+ const addToCart = () => {
+ if (!isInCart) {
+ dispatch({
+ type: 'addCart',
+ payload: { product: product, quantity: 1, id: product.itemId },
+ });
+ }
+ };
+
+ return (
+
+
+
+
+
+ );
+};
diff --git a/src/components/Actions/index.ts b/src/components/Actions/index.ts
new file mode 100644
index 0000000000..ac0eab8d42
--- /dev/null
+++ b/src/components/Actions/index.ts
@@ -0,0 +1 @@
+export { Actions } from './Actions';
diff --git a/src/components/BackLink/BackLink.module.scss b/src/components/BackLink/BackLink.module.scss
new file mode 100644
index 0000000000..b4067103c0
--- /dev/null
+++ b/src/components/BackLink/BackLink.module.scss
@@ -0,0 +1,24 @@
+@import '../../styles/utils';
+
+.back-link {
+ width: 65px;
+ display: flex;
+ gap: 4px;
+ align-items: center;
+ height: 16px;
+
+ &__icon {
+ transform: rotate(-90deg);
+ }
+
+ &__text {
+ @include small-text;
+
+ color: var(--secondary);
+ line-height: 15px;
+ }
+
+ svg {
+ fill: var(--icon-primary);
+ }
+}
diff --git a/src/components/BackLink/BackLink.tsx b/src/components/BackLink/BackLink.tsx
new file mode 100644
index 0000000000..b3d59320b3
--- /dev/null
+++ b/src/components/BackLink/BackLink.tsx
@@ -0,0 +1,31 @@
+import styles from './BackLink.module.scss';
+import { Link, useNavigate } from 'react-router-dom';
+import { SvgIcon } from '../SvgIcon';
+import React from 'react';
+import cn from 'classnames';
+
+interface Props {
+ className?: string;
+}
+
+export const BackLink: React.FC = ({ className }) => {
+ const navigate = useNavigate();
+
+ const onClickHandler = (
+ evt: React.MouseEvent,
+ ) => {
+ evt.preventDefault();
+ navigate(-1);
+ };
+
+ return (
+
+
+ Back
+
+ );
+};
diff --git a/src/components/BackLink/index.ts b/src/components/BackLink/index.ts
new file mode 100644
index 0000000000..907b5bb3c8
--- /dev/null
+++ b/src/components/BackLink/index.ts
@@ -0,0 +1 @@
+export { BackLink } from './BackLink';
diff --git a/src/components/Grid/Grid.module.scss b/src/components/Grid/Grid.module.scss
new file mode 100644
index 0000000000..24caadc076
--- /dev/null
+++ b/src/components/Grid/Grid.module.scss
@@ -0,0 +1,29 @@
+@import '../../styles/utils';
+
+.grid {
+ display: flex;
+ flex-direction: column;
+ gap: 40px;
+
+ @include on-tablet {
+ display: grid;
+ grid-template-columns: repeat(2, 1fr);
+ gap: 40px 16px;
+ }
+
+ @media (min-width: 768px) {
+ grid-template-columns: repeat(3, 1fr);
+ }
+
+ @include on-desktop {
+ grid-template-columns: repeat(4, 1fr);
+ }
+
+ &__item {
+ height: 440px;
+
+ @include on-tablet {
+ height: 506px;
+ }
+ }
+}
diff --git a/src/components/Grid/Grid.tsx b/src/components/Grid/Grid.tsx
new file mode 100644
index 0000000000..5fb2e8f32d
--- /dev/null
+++ b/src/components/Grid/Grid.tsx
@@ -0,0 +1,21 @@
+import styles from './Grid.module.scss';
+import { Product } from '../../types/Product';
+import { ProductCard } from '../ProductCard';
+
+interface Props {
+ products: Product[];
+}
+
+export const Grid: React.FC = ({ products }) => {
+ return (
+
+ {products.map(product => {
+ return (
+ -
+
+
+ );
+ })}
+
+ );
+};
diff --git a/src/components/Grid/index.ts b/src/components/Grid/index.ts
new file mode 100644
index 0000000000..66c39561da
--- /dev/null
+++ b/src/components/Grid/index.ts
@@ -0,0 +1 @@
+export { Grid } from './Grid';
diff --git a/src/components/Loader/Loader.module.scss b/src/components/Loader/Loader.module.scss
new file mode 100644
index 0000000000..4fa829ae76
--- /dev/null
+++ b/src/components/Loader/Loader.module.scss
@@ -0,0 +1,14 @@
+@import '../../styles/utils';
+
+.loader {
+ margin: 32px auto;
+ justify-content: center;
+
+ svg {
+ stroke: var(--accent);
+ }
+
+ svg circle {
+ stroke: rgba($color: var(--primary), $alpha: 0.5);
+ }
+}
diff --git a/src/components/Loader/Loader.tsx b/src/components/Loader/Loader.tsx
new file mode 100644
index 0000000000..a0b186d485
--- /dev/null
+++ b/src/components/Loader/Loader.tsx
@@ -0,0 +1,20 @@
+// eslint-disable-next-line import/no-extraneous-dependencies
+import { Oval } from 'react-loader-spinner';
+
+import styles from './Loader.module.scss';
+import React from 'react';
+import classNames from 'classnames';
+
+interface Props {
+ className?: string;
+}
+
+export const Loader: React.FC = ({ className }) => {
+ return (
+
+ );
+};
diff --git a/src/components/Loader/index.ts b/src/components/Loader/index.ts
new file mode 100644
index 0000000000..d702788525
--- /dev/null
+++ b/src/components/Loader/index.ts
@@ -0,0 +1 @@
+export { Loader } from './Loader';
diff --git a/src/components/Logo/Logo.styles.scss b/src/components/Logo/Logo.styles.scss
new file mode 100644
index 0000000000..6838d347d5
--- /dev/null
+++ b/src/components/Logo/Logo.styles.scss
@@ -0,0 +1,3 @@
+.logo {
+ display: block;
+}
diff --git a/src/components/Logo/Logo.tsx b/src/components/Logo/Logo.tsx
new file mode 100644
index 0000000000..f5b82bbb0f
--- /dev/null
+++ b/src/components/Logo/Logo.tsx
@@ -0,0 +1,20 @@
+import cn from 'classnames';
+import logoLight from '../../assets/images/logo-light.svg';
+import logoDark from '../../assets/images/logo-dark.svg';
+import { useContext } from 'react';
+import { ThemeContext } from '../../contex/Theme';
+import { Theme } from '../../utils/constants';
+
+import styles from './Logo.styles.scss';
+
+interface Props {
+ className?: string;
+}
+
+export const Logo: React.FC = ({ className }) => {
+ const { theme } = useContext(ThemeContext);
+
+ const logo = theme === Theme.LIGTH ? logoLight : logoDark;
+
+ return ;
+};
diff --git a/src/components/Logo/index.ts b/src/components/Logo/index.ts
new file mode 100644
index 0000000000..33af505338
--- /dev/null
+++ b/src/components/Logo/index.ts
@@ -0,0 +1 @@
+export { Logo } from './Logo';
diff --git a/src/components/ProductCard/ProductCard.module.scss b/src/components/ProductCard/ProductCard.module.scss
new file mode 100644
index 0000000000..1c2409995c
--- /dev/null
+++ b/src/components/ProductCard/ProductCard.module.scss
@@ -0,0 +1,118 @@
+@import '../../styles/utils';
+
+.product-card {
+ width: 100%;
+ height: 100%;
+ padding: 32px;
+ border-radius: 8px;
+ box-shadow: inset 0 0 0 1px var(--elements);
+ display: flex;
+ gap: 24px;
+ flex-direction: column;
+ justify-content: space-between;
+ background-color: var(--bg-secondary);
+
+ @include transition;
+
+ &:hover {
+ box-shadow: inset 0 0 0 1px var(--active-link);
+ }
+
+ &__photo-link {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ flex-shrink: 1;
+ flex-grow: 1;
+ min-height: 80px;
+
+ @include transition;
+
+ &:hover {
+ transform: scale(1.1);
+ }
+ }
+
+ &__photo {
+ display: block;
+ object-fit: contain;
+ height: 100%;
+ width: 100%;
+ }
+
+ &__wrapper {
+ flex-shrink: 0;
+ }
+
+ &__link {
+ &:hover {
+ color: var(--link);
+
+ @include transition;
+ }
+ }
+
+ &__title {
+ margin: 0;
+ margin-bottom: 8px;
+ font-size: inherit;
+ font-weight: 600;
+ }
+
+ &__price-wrapper {
+ position: relative;
+ display: flex;
+ flex-wrap: wrap;
+ gap: 8px;
+ padding-bottom: 8px;
+ line-height: 31px;
+ margin-bottom: 16px;
+
+ @include h3-tablet-desktop;
+
+ &::after {
+ position: absolute;
+ display: block;
+ content: '';
+ width: 100%;
+ height: 1px;
+ background-color: var(--elements);
+ bottom: 0;
+ }
+ }
+
+ &__full-price {
+ color: var(--secondary);
+ text-decoration: line-through;
+ font-weight: 600;
+ }
+
+ &__properties-list {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ margin-bottom: 16px;
+ }
+
+ &__specs-list {
+ display: flex;
+ flex-direction: column;
+ }
+
+ &__definition-list {
+ margin: 0;
+ display: flex;
+ gap: 8px;
+ justify-content: space-between;
+
+ @include small-text;
+ }
+
+ &__definition-term {
+ color: var(--secondary);
+ }
+
+ &__definition-desc {
+ margin: 0;
+ }
+}
diff --git a/src/components/ProductCard/ProductCard.tsx b/src/components/ProductCard/ProductCard.tsx
new file mode 100644
index 0000000000..b39254b5c3
--- /dev/null
+++ b/src/components/ProductCard/ProductCard.tsx
@@ -0,0 +1,80 @@
+import styles from './ProductCard.module.scss';
+import cn from 'classnames';
+import { Product } from '../../types/Product';
+import { Link } from 'react-router-dom';
+import { Actions } from '../Actions';
+import { scrollToTop } from '../../utils/utility';
+
+interface Props {
+ product: Product;
+}
+
+export const ProductCard: React.FC = ({ product }) => {
+ const {
+ name,
+ image,
+ price,
+ fullPrice,
+ screen,
+ capacity,
+ ram,
+ category,
+ itemId,
+ } = product;
+
+ const properties = [
+ { term: 'Screen', desc: screen },
+ { term: 'Capacity', desc: capacity },
+ { term: 'RAM', desc: ram },
+ ];
+
+ const pathLink = `/${category}/${itemId}`;
+
+ return (
+
+
+
+
+
+
+
+
{name}
+
+
+
+
${price}
+
${fullPrice}
+
+
+
+ {properties.map(property => (
+ -
+
+ -
+ {property.term}
+
+ -
+ {property.desc}
+
+
+
+ ))}
+
+
+
+
+
+ );
+};
diff --git a/src/components/ProductCard/index.ts b/src/components/ProductCard/index.ts
new file mode 100644
index 0000000000..c4f2778191
--- /dev/null
+++ b/src/components/ProductCard/index.ts
@@ -0,0 +1 @@
+export { ProductCard } from './ProductCard';
diff --git a/src/components/ProductsSlider/ProductsSlider.module.scss b/src/components/ProductsSlider/ProductsSlider.module.scss
new file mode 100644
index 0000000000..619905991c
--- /dev/null
+++ b/src/components/ProductsSlider/ProductsSlider.module.scss
@@ -0,0 +1,87 @@
+@import '../../styles/utils';
+
+.products-slider {
+ display: grid;
+ grid-template-columns: repeat(12, 1fr);
+
+ &__header {
+ grid-column: 1 / -1;
+ display: flex;
+ gap: 10px;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 24px;
+
+ @include page-padding-inline;
+ }
+
+ &__title {
+ margin: 0;
+
+ @include h2-mobile;
+
+ @include on-tablet {
+ @include h2-tablet-desktop;
+ }
+ }
+
+ &__controls {
+ display: flex;
+ gap: 16px;
+ }
+
+ &__btn {
+ width: 32px;
+ height: 32px;
+
+ &--prev {
+ transform: rotate(-90deg);
+ }
+ &--next {
+ transform: rotate(90deg);
+ }
+
+ &:disabled {
+ svg path {
+ fill: var(--elements);
+ }
+ }
+ }
+
+ &__wrapper {
+ grid-column: 1 / -1;
+ padding-left: $mobile-padding-inline;
+
+ @include on-tablet {
+ padding-left: $tablet-padding-inline;
+ }
+
+ @include on-desktop {
+ padding-inline: $desktop-padding-inline;
+ }
+ }
+
+ &__list {
+ display: flex;
+ overflow: hidden;
+ height: auto;
+ gap: 16px;
+ }
+
+ &__slide {
+ flex: 0 0 212px;
+ height: 440px;
+
+ transition: transform 0.5s ease-in-out;
+
+ @include on-tablet {
+ flex: 0 0 237px;
+ height: 512px;
+ }
+
+ @include on-desktop {
+ flex: 0 0 272px;
+ height: 506px;
+ }
+ }
+}
diff --git a/src/components/ProductsSlider/ProductsSlider.tsx b/src/components/ProductsSlider/ProductsSlider.tsx
new file mode 100644
index 0000000000..a5782c22c1
--- /dev/null
+++ b/src/components/ProductsSlider/ProductsSlider.tsx
@@ -0,0 +1,103 @@
+import styles from './ProductsSlider.module.scss';
+import cn from 'classnames';
+import { useEffect, useState } from 'react';
+import { Product } from '../../types';
+import { RoundButton } from '../RoundButton';
+import { SvgIcon } from '../SvgIcon';
+import { ProductCard } from '../ProductCard';
+
+interface Props {
+ title: string;
+ products: Product[];
+}
+
+export const ProductsSlider: React.FC = ({ title, products }) => {
+ const [currentSlideIndex, setCurrentSlideIndex] = useState(0);
+ const [visibleCardsCount, setVisibleCardsCount] = useState(3);
+
+ const updateVisibleCardsCount = () => {
+ const width = window.innerWidth;
+
+ switch (true) {
+ case width < 440:
+ setVisibleCardsCount(1);
+ break;
+ case width < 767:
+ setVisibleCardsCount(2);
+ break;
+ case width < 1200:
+ setVisibleCardsCount(3);
+ break;
+ default:
+ setVisibleCardsCount(4);
+ break;
+ }
+ };
+
+ useEffect(() => {
+ updateVisibleCardsCount();
+ window.addEventListener('resize', updateVisibleCardsCount);
+
+ return () => {
+ window.removeEventListener('resize', updateVisibleCardsCount);
+ };
+ }, []);
+
+ const previousSlide = () => {
+ setCurrentSlideIndex(index => Math.max(index - 1, 0));
+ };
+
+ const nextSlide = () => {
+ setCurrentSlideIndex(index =>
+ Math.min(index + 1, products.length - visibleCardsCount),
+ );
+ };
+
+ return (
+
+
+
{title}
+
+
+
+
+
+
+
+
+
+
+
+
+ {products.map(product => {
+ return (
+ -
+
+
+ );
+ })}
+
+
+
+ );
+};
diff --git a/src/components/ProductsSlider/index.ts b/src/components/ProductsSlider/index.ts
new file mode 100644
index 0000000000..0a5bb98662
--- /dev/null
+++ b/src/components/ProductsSlider/index.ts
@@ -0,0 +1 @@
+export { ProductsSlider } from './ProductsSlider';
diff --git a/src/components/RoundButton/RoundButton.module.scss b/src/components/RoundButton/RoundButton.module.scss
new file mode 100644
index 0000000000..9e40be423f
--- /dev/null
+++ b/src/components/RoundButton/RoundButton.module.scss
@@ -0,0 +1,30 @@
+@import '../../styles/utils';
+
+.round-button {
+ width: 100%;
+ background-color: inherit;
+ border-radius: 50%;
+ border: 1px solid var(--icon-secondary);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ cursor: pointer;
+ aspect-ratio: 1 / 1;
+ color: inherit;
+
+ @include transition;
+
+ &:hover {
+ border-color: var(--round-btn-hover-border);
+ background-color: var(--round-btn-hover);
+ }
+
+ &:disabled {
+ border-color: var(--elements);
+ background-color: inherit;
+ }
+
+ svg path {
+ fill: var(--icon-primary);
+ }
+}
diff --git a/src/components/RoundButton/RoundButton.tsx b/src/components/RoundButton/RoundButton.tsx
new file mode 100644
index 0000000000..79678d5626
--- /dev/null
+++ b/src/components/RoundButton/RoundButton.tsx
@@ -0,0 +1,20 @@
+import cn from 'classnames';
+import React from 'react';
+
+import styles from './RoundButton.module.scss';
+
+interface Props extends React.ButtonHTMLAttributes {
+ children: React.ReactNode;
+}
+
+export const RoundButton: React.FC = ({
+ className,
+ children,
+ ...rest
+}) => {
+ return (
+
+ );
+};
diff --git a/src/components/RoundButton/index.ts b/src/components/RoundButton/index.ts
new file mode 100644
index 0000000000..6d06eb8935
--- /dev/null
+++ b/src/components/RoundButton/index.ts
@@ -0,0 +1 @@
+export { RoundButton } from './RoundButton';
diff --git a/src/components/SvgIcon/SvgIcon.tsx b/src/components/SvgIcon/SvgIcon.tsx
new file mode 100644
index 0000000000..93a4da5103
--- /dev/null
+++ b/src/components/SvgIcon/SvgIcon.tsx
@@ -0,0 +1,190 @@
+/* eslint-disable max-len */
+import React from 'react';
+
+type Type =
+ | 'arrow'
+ | 'heart'
+ | 'heart-like'
+ | 'cart'
+ | 'menu'
+ | 'close'
+ | 'search'
+ | 'home';
+
+interface Props {
+ className?: string;
+ type: Type;
+}
+
+export const SvgIcon: React.FC = ({ className, type }) => {
+ switch (type) {
+ case 'arrow':
+ return (
+
+ );
+ case 'heart':
+ return (
+
+ );
+ case 'heart-like':
+ return (
+
+ );
+ case 'cart':
+ return (
+
+ );
+
+ case 'menu':
+ return (
+
+ );
+ case 'close':
+ return (
+
+ );
+ case 'search':
+ return (
+
+ );
+ case 'home':
+ return (
+
+ );
+ default:
+ return null;
+ }
+};
diff --git a/src/components/SvgIcon/index.ts b/src/components/SvgIcon/index.ts
new file mode 100644
index 0000000000..bd502a01b6
--- /dev/null
+++ b/src/components/SvgIcon/index.ts
@@ -0,0 +1 @@
+export { SvgIcon } from './SvgIcon';
diff --git a/src/components/breadcrumbs/breadcrumbs.module.scss b/src/components/breadcrumbs/breadcrumbs.module.scss
new file mode 100644
index 0000000000..1d7d9346a9
--- /dev/null
+++ b/src/components/breadcrumbs/breadcrumbs.module.scss
@@ -0,0 +1,54 @@
+@import '../../styles/utils';
+
+.breadcrumbs {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+
+ &__icon {
+ display: block;
+ }
+
+ &__item {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ font-size: 12px;
+ font-weight: 700;
+ line-height: 15px;
+ color: var(--primary);
+
+ &:not(:last-child)::after {
+ display: block;
+ content: '';
+ width: 16px;
+ height: 16px;
+ background-image: url('../../assets/images/icons/arrow.svg');
+ background-size: cover;
+ transform: rotate(90deg);
+ }
+
+ &:last-child {
+ flex: 1;
+ font-weight: 600;
+ color: var(--secondary);
+ width: 50px;
+ }
+ }
+
+ &__link {
+ display: flex;
+ align-items: center;
+
+ svg path{
+ fill: var(--icon-primary);
+ }
+ }
+
+ &__last-text {
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ width: 100%;
+ }
+}
diff --git a/src/components/breadcrumbs/breadcrumbs.tsx b/src/components/breadcrumbs/breadcrumbs.tsx
new file mode 100644
index 0000000000..56dc4b8258
--- /dev/null
+++ b/src/components/breadcrumbs/breadcrumbs.tsx
@@ -0,0 +1,48 @@
+import { Link, useLocation } from 'react-router-dom';
+import styles from './Breadcrumbs.module.scss';
+import cn from 'classnames';
+import { firstLetterCap } from '../../utils/utility';
+import { SvgIcon } from '../SvgIcon';
+
+interface Props {
+ className?: string;
+}
+
+export const Breadcrumbs: React.FC = ({ className }) => {
+ const location = useLocation();
+
+ const crumbs = location.pathname
+ .replaceAll('#', '')
+ .split('/')
+ .filter(crumb => !!crumb.length);
+
+ let currentLink = '';
+
+ return (
+
+ -
+
+
+
+
+
+ {crumbs.map((crumb, i, arr) => {
+ currentLink += `/${crumb}`;
+ const title = firstLetterCap(crumb).replaceAll('-', ' ');
+ const isLast = i === arr.length - 1;
+
+ return (
+ -
+ {isLast ? (
+
{title}
+ ) : (
+
+ {title}
+
+ )}
+
+ );
+ })}
+
+ );
+};
diff --git a/src/components/breadcrumbs/index.ts b/src/components/breadcrumbs/index.ts
new file mode 100644
index 0000000000..28140a257f
--- /dev/null
+++ b/src/components/breadcrumbs/index.ts
@@ -0,0 +1 @@
+export { Breadcrumbs } from './Breadcrumbs';
diff --git a/src/contex/State.tsx b/src/contex/State.tsx
new file mode 100644
index 0000000000..b6ff3bdd63
--- /dev/null
+++ b/src/contex/State.tsx
@@ -0,0 +1,110 @@
+import React, { useEffect, useReducer } from 'react';
+import { Product } from '../types';
+import { CartProduct } from '../types/Product';
+
+type Action =
+ | { type: 'addFavourites'; payload: Product }
+ | { type: 'removeFavourites'; payload: string }
+ | { type: 'addCart'; payload: CartProduct }
+ | { type: 'removeCart'; payload: string }
+ | { type: 'clearCart' }
+ | { type: 'increaseCountInCart'; payload: string }
+ | { type: 'decreaseCountInCart'; payload: string };
+
+interface State {
+ favourites: Product[];
+ cart: CartProduct[];
+}
+
+const initialState: State = {
+ favourites: [],
+ cart: [],
+};
+
+const reducer = (state: State, action: Action): State => {
+ switch (action.type) {
+ case 'addCart':
+ return { ...state, cart: [...state.cart, action.payload] };
+
+ case 'removeCart':
+ const filteredCart = state.cart.filter(
+ product => product.id !== action.payload,
+ );
+
+ return { ...state, cart: filteredCart };
+
+ case 'clearCart':
+ return { ...state, cart: [] };
+
+ case 'addFavourites':
+ return { ...state, favourites: [...state.favourites, action.payload] };
+
+ case 'removeFavourites':
+ const filteredFavousrites = state.favourites.filter(
+ product => product.itemId !== action.payload,
+ );
+
+ return { ...state, favourites: filteredFavousrites };
+
+ case 'increaseCountInCart':
+ return {
+ ...state,
+ cart: state.cart.map(p =>
+ p.id === action.payload ? { ...p, quantity: p.quantity + 1 } : p,
+ ),
+ };
+
+ case 'decreaseCountInCart':
+ return {
+ ...state,
+ cart: state.cart.map(p =>
+ p.id === action.payload ? { ...p, quantity: p.quantity - 1 } : p,
+ ),
+ };
+
+ default:
+ return { ...state };
+ }
+};
+
+export const StateContext = React.createContext(initialState);
+// eslint-disable-next-line @typescript-eslint/no-unused-vars
+export const DispatchContext = React.createContext((_action: Action) => {});
+
+interface Props {
+ children: React.ReactNode;
+}
+
+export const GlobalStateProvider: React.FC = ({ children }) => {
+ const getInitialState = (): State => {
+ try {
+ const savedFavourites = JSON.parse(
+ localStorage.getItem('favourites') || '[]',
+ );
+ const savedCart = JSON.parse(localStorage.getItem('cart') || '[]');
+
+ return {
+ favourites: savedFavourites,
+ cart: savedCart,
+ };
+ } catch (_) {
+ return {
+ favourites: [],
+ cart: [],
+ };
+ }
+ };
+
+ const [state, dispatch] = useReducer(reducer, getInitialState());
+
+ useEffect(() => {
+ localStorage.setItem('favourites', JSON.stringify(state.favourites));
+ localStorage.setItem('cart', JSON.stringify(state.cart));
+ }, [state]);
+
+ return (
+
+ {children}
+
+ );
+};
diff --git a/src/contex/Theme.tsx b/src/contex/Theme.tsx
new file mode 100644
index 0000000000..15f0ddb500
--- /dev/null
+++ b/src/contex/Theme.tsx
@@ -0,0 +1,31 @@
+import { createContext, useEffect, useState } from 'react';
+import { Theme } from '../utils/constants';
+
+const initialState = {
+ theme: Theme.LIGTH,
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ setTheme: (_theme: Theme) => {},
+};
+
+export const ThemeContext = createContext(initialState);
+
+interface Props {
+ children: React.ReactNode;
+}
+
+export const ThemeProvider: React.FC = ({ children }) => {
+ const initialTheme = (localStorage.getItem('theme') as Theme) || Theme.LIGTH;
+
+ const [theme, setTheme] = useState(initialTheme);
+
+ useEffect(() => {
+ document.body.className = theme;
+ localStorage.setItem('theme', theme);
+ }, [theme]);
+
+ return (
+
+ {children}
+
+ );
+};
diff --git a/src/declaration.d.ts b/src/declaration.d.ts
new file mode 100644
index 0000000000..7b2a58cfcc
--- /dev/null
+++ b/src/declaration.d.ts
@@ -0,0 +1,3 @@
+declare module '*.scss';
+declare module '*.svg';
+declare module '*.png';
diff --git a/src/helpers/colorMap.ts b/src/helpers/colorMap.ts
new file mode 100644
index 0000000000..7e76de6335
--- /dev/null
+++ b/src/helpers/colorMap.ts
@@ -0,0 +1,26 @@
+interface ColorMap {
+ [key: string]: string;
+}
+
+export const colorMap: ColorMap = {
+ black: '#000',
+ gold: '#FFD700',
+ yellow: '#FFFF00',
+ green: '#008000',
+ midnightgreen: '#004953',
+ silver: '#c0c0c0',
+ spacegray: '#717378',
+ red: '#ff0000',
+ white: '#FFF',
+ purple: '#800080',
+ coral: '#ff7f50',
+ rosegold: '#B76E79',
+ midnight: '#191970',
+ spaceblack: '#505150',
+ blue: '#0000ff',
+ pink: '#ffc0cb',
+ sierrablue: '#BFDAF7',
+ graphite: '#41424C',
+ skyblue: '#87CEEB',
+ starlight: '#F8F9EC',
+};
diff --git a/src/helpers/index.ts b/src/helpers/index.ts
new file mode 100644
index 0000000000..2ee4854e13
--- /dev/null
+++ b/src/helpers/index.ts
@@ -0,0 +1 @@
+export { colorMap } from './colorMap';
diff --git a/src/hooks/index.ts b/src/hooks/index.ts
new file mode 100644
index 0000000000..4be657d117
--- /dev/null
+++ b/src/hooks/index.ts
@@ -0,0 +1 @@
+export { useUpdateSearchParams } from './useUpdateSearchParams';
diff --git a/src/hooks/useUpdateSearchParams.tsx b/src/hooks/useUpdateSearchParams.tsx
new file mode 100644
index 0000000000..334871cf44
--- /dev/null
+++ b/src/hooks/useUpdateSearchParams.tsx
@@ -0,0 +1,28 @@
+import { useCallback } from 'react';
+import { useSearchParams } from 'react-router-dom';
+
+export const useUpdateSearchParams = () => {
+ const [searchParams, setSearchParams] = useSearchParams();
+
+ const updateSearchParams = useCallback(
+ (key: string, value: string | number) => {
+ const params = new URLSearchParams(searchParams);
+
+ params.set(key, value.toString());
+ setSearchParams(params);
+ },
+ [searchParams, setSearchParams],
+ );
+
+ const deleteSearchParam = useCallback(
+ (key: string) => {
+ const newSearchParams = new URLSearchParams(searchParams);
+
+ newSearchParams.delete(key);
+ setSearchParams(newSearchParams);
+ },
+ [searchParams, setSearchParams],
+ );
+
+ return { searchParams, updateSearchParams, deleteSearchParam };
+};
diff --git a/src/index.tsx b/src/index.tsx
index 50470f1508..ee4a60d280 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -1,4 +1,15 @@
import { createRoot } from 'react-dom/client';
-import { App } from './App';
+import { HashRouter as Router } from 'react-router-dom';
+import { Root } from './Root';
+import { GlobalStateProvider } from './contex/State';
+import { ThemeProvider } from './contex/Theme';
-createRoot(document.getElementById('root') as HTMLElement).render();
+createRoot(document.getElementById('root') as HTMLElement).render(
+
+
+
+
+
+
+ ,
+);
diff --git a/src/modules/AccessoriesPage/AccessoriesPage.tsx b/src/modules/AccessoriesPage/AccessoriesPage.tsx
new file mode 100644
index 0000000000..a4d0de7c2f
--- /dev/null
+++ b/src/modules/AccessoriesPage/AccessoriesPage.tsx
@@ -0,0 +1,27 @@
+import { useEffect, useState } from 'react';
+import { Product } from '../../types/Product';
+import { getProductsByCategory } from '../../servises/products';
+import { ProductsCatalog } from '../ProductsCatalog';
+
+export const AccessoriesPage = () => {
+ const [accessories, setAccessories] = useState([]);
+ const [loading, setLoading] = useState(false);
+ const [error, setError] = useState('');
+
+ useEffect(() => {
+ setLoading(true);
+ getProductsByCategory('accessories')
+ .then(setAccessories)
+ .catch(() => setError('Something went wrong'))
+ .finally(() => setLoading(false));
+ }, []);
+
+ return (
+
+ );
+};
diff --git a/src/modules/AccessoriesPage/index.ts b/src/modules/AccessoriesPage/index.ts
new file mode 100644
index 0000000000..83dcf696d1
--- /dev/null
+++ b/src/modules/AccessoriesPage/index.ts
@@ -0,0 +1 @@
+export { AccessoriesPage } from './AccessoriesPage';
diff --git a/src/modules/CartPage/CartPage.module.scss b/src/modules/CartPage/CartPage.module.scss
new file mode 100644
index 0000000000..c446278c12
--- /dev/null
+++ b/src/modules/CartPage/CartPage.module.scss
@@ -0,0 +1,109 @@
+@import '../../styles/utils';
+
+.cart-page {
+ max-width: $desktop-width;
+ margin: 0 auto;
+ padding-top: 24px;
+ padding-bottom: 56px;
+
+ @include page-padding-inline;
+
+ @include on-tablet {
+ padding-bottom: 64px;
+ }
+
+ @include on-desktop {
+ padding-bottom: 80px;
+ }
+
+ &__back {
+ margin-bottom: 24px;
+
+ @include on-tablet {
+ margin-bottom: 40px;
+ }
+ }
+
+ &__title {
+ margin: 0;
+ margin-bottom: 32px;
+
+ @include h1-mobile;
+
+ @include on-tablet {
+ @include h1-tablet-desktop;
+ }
+ }
+
+ &__container {
+ @include on-desktop {
+ display: grid;
+ grid-template-columns: 752px 368px;
+ gap: 16px;
+ }
+ }
+
+ &__text {
+ color: var(--secondary);
+ margin-bottom: 32px;
+
+ @include on-tablet {
+ margin-bottom: 40px;
+ }
+ }
+
+ &__list {
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+ margin-bottom: 32px;
+
+ @include on-desktop {
+ margin: 0;
+ }
+ }
+
+ &__total {
+ text-align: center;
+ padding: 24px;
+ border: 1px solid var(--elements);
+ border-radius: 16px;
+
+ @include on-desktop {
+ align-self: start;
+ }
+ }
+
+ &__total-price {
+ @include h2-tablet-desktop;
+ }
+
+ &__total-desc {
+ display: flex;
+ flex-direction: column;
+ gap: 15px;
+ color: var(--secondary);
+ margin-bottom: 16px;
+
+ &::after {
+ display: block;
+ content: '';
+ width: 100%;
+ height: 1px;
+ background-color: var(--elements);
+ }
+ }
+
+ &__checkout {
+ height: 48px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 100%;
+ padding: 13px;
+ border: none;
+ background-color: var(--accent);
+ color: var(--white);
+ border-radius: 8px;
+ }
+}
diff --git a/src/modules/CartPage/CartPage.tsx b/src/modules/CartPage/CartPage.tsx
new file mode 100644
index 0000000000..de65a3386e
--- /dev/null
+++ b/src/modules/CartPage/CartPage.tsx
@@ -0,0 +1,61 @@
+import { useContext, useState } from 'react';
+import { StateContext } from '../../contex/State';
+import styles from './CartPage.module.scss';
+import { BackLink } from '../../components/BackLink';
+import { CartItem } from './components/CartItem';
+import { EmptyCart } from './components/EmptyCart';
+import { Modal } from './components/Modal';
+
+export const CartPage = () => {
+ const [isModalOpen, setIsModalOpen] = useState(false);
+ const { cart } = useContext(StateContext);
+
+ const totalPrice = cart.reduce(
+ (total, p) => total + p.product.price * p.quantity,
+ 0,
+ );
+
+ const totalQuantity = cart.reduce((count, p) => count + p.quantity, 0);
+
+ return (
+
+
+ Cart
+ {cart.length ? (
+
+
+ {cart.map(product => {
+ return (
+ -
+
+
+ );
+ })}
+
+
+
${totalPrice}
+
+ Total for {totalQuantity} item{cart.length > 1 ? 's' : ''}
+
+
+
+
+ ) : (
+
+ )}
+
+ {isModalOpen && (
+ {
+ setIsModalOpen(false);
+ }}
+ />
+ )}
+
+ );
+};
diff --git a/src/modules/CartPage/components/CartItem/CartItem.module.scss b/src/modules/CartPage/components/CartItem/CartItem.module.scss
new file mode 100644
index 0000000000..b206423cfd
--- /dev/null
+++ b/src/modules/CartPage/components/CartItem/CartItem.module.scss
@@ -0,0 +1,138 @@
+@import '../../../../styles/utils';
+
+.cart-item {
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+ padding: 16px;
+ border: 1px solid var(--elements);
+ border-radius: 16px;
+ background-color: var(--bg-secondary);
+
+ @include on-tablet {
+ flex-direction: row;
+ justify-content: space-between;
+ gap: 24px;
+ padding: 24px;
+ }
+
+ &__container {
+ display: flex;
+ gap: 16px;
+ align-items: center;
+
+ @include on-tablet {
+ gap: 24px;
+ }
+ }
+
+ &__del {
+ flex-shrink: 0;
+ position: relative;
+ width: 16px;
+ height: 16px;
+ align-self: center;
+ border: none;
+ background-color: inherit;
+ padding: 0;
+
+ &::before,
+ &::after {
+ position: absolute;
+ content: '';
+ width: 12px;
+ height: 1px;
+ background-color: var(--icon-secondary);
+ border-radius: 1px;
+ top: 50%;
+ left: 50%;
+ }
+
+ @include transition;
+
+ &::before {
+ transform: translate(-50%, -50%) rotate(45deg);
+ }
+
+ &::after {
+ transform: translate(-50%, -50%) rotate(-45deg);
+ }
+
+ &:hover {
+ scale: 1.2;
+ }
+ }
+
+ &__img {
+ width: 80px;
+ object-fit: contain;
+ aspect-ratio: 1 / 1;
+ }
+
+ &__link {
+ @include transition;
+
+ &:hover {
+ color: var(--icon-secondary);
+ }
+ }
+
+ &__count {
+ display: flex;
+ align-items: center;
+ gap: 14px;
+ }
+
+ &__count-btn {
+ position: relative;
+ border-radius: 50%;
+ width: 30px;
+ height: 30px;
+ border: 1px solid var(--icon-secondary);
+ background-color: inherit;
+
+ &::before,
+ &::after {
+ position: absolute;
+ content: '';
+ width: 11px;
+ height: 1px;
+ background-color: var(--primary);
+ border-radius: 1px;
+ top: 50%;
+ left: 50%;
+ }
+
+ &--minus::before,
+ &--minus::after {
+ transform: translate(-50%);
+ }
+
+ &--plus::after {
+ transform: translate(-50%);
+ }
+ &--plus::before {
+ transform: translate(-50%) rotate(90deg);
+ }
+
+ &:disabled {
+ border-color: var(--elements);
+
+ &::before,
+ &::after {
+ background-color: var(--icon-secondary);
+ }
+ }
+ }
+
+ &__price {
+ margin-left: auto;
+
+ @include h3-tablet-desktop;
+
+ @include on-tablet {
+ width: 80px;
+ text-align: end;
+ }
+ }
+}
diff --git a/src/modules/CartPage/components/CartItem/CartItem.tsx b/src/modules/CartPage/components/CartItem/CartItem.tsx
new file mode 100644
index 0000000000..a9f250be1f
--- /dev/null
+++ b/src/modules/CartPage/components/CartItem/CartItem.tsx
@@ -0,0 +1,70 @@
+import classNames from 'classnames';
+import { CartProduct } from '../../../../types';
+import styles from './CartItem.module.scss';
+import { useContext } from 'react';
+import { DispatchContext } from '../../../../contex/State';
+import { Link } from 'react-router-dom';
+
+interface Props {
+ product: CartProduct;
+}
+
+export const CartItem: React.FC = ({ product }) => {
+ const { name, image, price, itemId, category } = product.product;
+ const { quantity } = product;
+ const dispatch = useContext(DispatchContext);
+
+ const deleteGood = () => {
+ dispatch({ type: 'removeCart', payload: itemId });
+ };
+
+ const increaseQuantity = () => {
+ dispatch({ type: 'increaseCountInCart', payload: itemId });
+ };
+
+ const decreaseQuantity = () => {
+ dispatch({ type: 'decreaseCountInCart', payload: itemId });
+ };
+
+ const path = `/${category}/${itemId}`;
+
+ return (
+
+
+
+
+
+ {name}
+
+
+
+
+
+
${price * quantity}
+
+
+ );
+};
diff --git a/src/modules/CartPage/components/CartItem/index.ts b/src/modules/CartPage/components/CartItem/index.ts
new file mode 100644
index 0000000000..186b364ebd
--- /dev/null
+++ b/src/modules/CartPage/components/CartItem/index.ts
@@ -0,0 +1 @@
+export { CartItem } from './CartItem';
diff --git a/src/modules/CartPage/components/EmptyCart/EmptyCart.module.scss b/src/modules/CartPage/components/EmptyCart/EmptyCart.module.scss
new file mode 100644
index 0000000000..6cc850e7eb
--- /dev/null
+++ b/src/modules/CartPage/components/EmptyCart/EmptyCart.module.scss
@@ -0,0 +1,26 @@
+@import '../../../../styles/utils';
+
+.empty-cart {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 24px;
+
+ &__text {
+ @include h2-mobile;
+
+ @include on-tablet {
+ @include h2-tablet-desktop;
+ }
+ }
+
+ &__img {
+ width: 100%;
+ max-width: 450px;
+ background: radial-gradient(circle, var(--accent) 0, var(--hover-bg) 70%);
+ padding: 60px;
+ object-fit: contain;
+ aspect-ratio: 1 / 1;
+ }
+
+}
diff --git a/src/modules/CartPage/components/EmptyCart/EmptyCart.tsx b/src/modules/CartPage/components/EmptyCart/EmptyCart.tsx
new file mode 100644
index 0000000000..b1b01a9555
--- /dev/null
+++ b/src/modules/CartPage/components/EmptyCart/EmptyCart.tsx
@@ -0,0 +1,14 @@
+import styles from './EmptyCart.module.scss';
+
+export const EmptyCart: React.FC = () => {
+ return (
+
+
Your cart is empty
+
+
+ );
+};
diff --git a/src/modules/CartPage/components/EmptyCart/index.ts b/src/modules/CartPage/components/EmptyCart/index.ts
new file mode 100644
index 0000000000..a0a65c39fc
--- /dev/null
+++ b/src/modules/CartPage/components/EmptyCart/index.ts
@@ -0,0 +1 @@
+export { EmptyCart } from './EmptyCart';
diff --git a/src/modules/CartPage/components/Modal/Modal.module.scss b/src/modules/CartPage/components/Modal/Modal.module.scss
new file mode 100644
index 0000000000..b77d8c2127
--- /dev/null
+++ b/src/modules/CartPage/components/Modal/Modal.module.scss
@@ -0,0 +1,67 @@
+@import '../../../../styles/utils';
+
+.modal {
+ position: fixed;
+ z-index: 3;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ margin: 0;
+ padding: 40px;
+ border: 2px solid var(--accent);
+ border-radius: 16px;
+ width: 80%;
+ max-width: 600px;
+ background-color: var(--bg-secondary);
+
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+
+ &__close {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 0;
+ position: absolute;
+ width: 24px;
+ height: 24px;
+ top: 10px;
+ right: 10px;
+ border: none;
+ background-color: inherit;
+
+ @include transition;
+
+ &:hover {
+ transform: scale(1.2);
+ opacity: 0.6;
+ }
+
+ svg path {
+ fill: var(--icon-primary);
+ }
+ }
+
+ &__text {
+ color: var(--primary);
+ text-align: center;
+
+ @include h4-mobile;
+
+ @include on-tablet {
+ @include h4-tablet-desktop;
+ }
+ }
+
+ &__confirm {
+ padding: 12px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background-color: var(--accent);
+ border: none;
+ color: var(--white);
+ border-radius: 8px;
+ }
+}
diff --git a/src/modules/CartPage/components/Modal/Modal.tsx b/src/modules/CartPage/components/Modal/Modal.tsx
new file mode 100644
index 0000000000..c010151675
--- /dev/null
+++ b/src/modules/CartPage/components/Modal/Modal.tsx
@@ -0,0 +1,32 @@
+import { useContext } from 'react';
+import styles from './Modal.module.scss';
+import { DispatchContext } from '../../../../contex/State';
+import { SvgIcon } from '../../../../components/SvgIcon';
+
+interface Props {
+ close: () => void;
+}
+
+export const Modal: React.FC = ({ close }) => {
+ const dispatch = useContext(DispatchContext);
+
+ const onConfirmClick = () => {
+ dispatch({ type: 'clearCart' });
+ close();
+ };
+
+ return (
+
+ );
+};
diff --git a/src/modules/CartPage/components/Modal/index.ts b/src/modules/CartPage/components/Modal/index.ts
new file mode 100644
index 0000000000..8deb0a3dff
--- /dev/null
+++ b/src/modules/CartPage/components/Modal/index.ts
@@ -0,0 +1 @@
+export { Modal } from './Modal';
diff --git a/src/modules/CartPage/index.ts b/src/modules/CartPage/index.ts
new file mode 100644
index 0000000000..203fb0ea4b
--- /dev/null
+++ b/src/modules/CartPage/index.ts
@@ -0,0 +1 @@
+export { CartPage } from './CartPage';
diff --git a/src/modules/FavouritesPage/FavouritesPage.module.scss b/src/modules/FavouritesPage/FavouritesPage.module.scss
new file mode 100644
index 0000000000..be6ad1b612
--- /dev/null
+++ b/src/modules/FavouritesPage/FavouritesPage.module.scss
@@ -0,0 +1,45 @@
+@import '../../styles/utils';
+
+.favourites-page {
+ max-width: $desktop-width;
+ margin: 0 auto;
+ padding-top: 24px;
+ padding-bottom: 56px;
+
+ @include page-padding-inline;
+
+ @include on-tablet {
+ padding-bottom: 64px;
+ }
+
+ @include on-desktop {
+ padding-bottom: 80px;
+ }
+
+ &__breadcrumbs {
+ margin-bottom: 24px;
+
+ @include on-tablet {
+ margin-bottom: 40px;
+ }
+ }
+
+ &__title {
+ margin-bottom: 8px;
+
+ @include h1-mobile;
+
+ @include on-tablet {
+ @include h1-tablet-desktop;
+ }
+ }
+
+ &__text {
+ color: var(--secondary);
+ margin-bottom: 32px;
+
+ @include on-tablet {
+ margin-bottom: 40px;
+ }
+ }
+}
diff --git a/src/modules/FavouritesPage/FavouritesPage.tsx b/src/modules/FavouritesPage/FavouritesPage.tsx
new file mode 100644
index 0000000000..027148ea3b
--- /dev/null
+++ b/src/modules/FavouritesPage/FavouritesPage.tsx
@@ -0,0 +1,26 @@
+import { useContext } from 'react';
+import { Breadcrumbs } from '../../components/Breadcrumbs';
+import { StateContext } from '../../contex/State';
+import styles from './FavouritesPage.module.scss';
+import { Grid } from '../../components/Grid';
+
+export const FavouritesPage = () => {
+ const { favourites } = useContext(StateContext);
+
+ return (
+
+
+ Favourites
+ {favourites.length ? (
+ <>
+
+ {favourites.length} item{favourites.length === 1 ? '' : 's'}
+
+
+ >
+ ) : (
+ No items
+ )}
+
+ );
+};
diff --git a/src/modules/FavouritesPage/index.ts b/src/modules/FavouritesPage/index.ts
new file mode 100644
index 0000000000..0a16512c05
--- /dev/null
+++ b/src/modules/FavouritesPage/index.ts
@@ -0,0 +1 @@
+export { FavouritesPage } from './FavouritesPage';
diff --git a/src/modules/Footer/Footer.module.scss b/src/modules/Footer/Footer.module.scss
new file mode 100644
index 0000000000..c0e4f33851
--- /dev/null
+++ b/src/modules/Footer/Footer.module.scss
@@ -0,0 +1,62 @@
+@import '../../styles/utils';
+
+.footer {
+ display: flex;
+ flex-direction: column;
+ gap: 32px;
+ padding: 32px;
+ color: var(--text-footer);
+ box-shadow: 0 -1px 0 0 var(--elements);
+ background-color: var(--bg-header-footer);
+
+ @include page-padding-inline;
+
+ @include on-tablet {
+ flex-direction: row;
+ gap: 15px;
+ justify-content: space-between;
+ }
+
+ &__logo {
+ width: 89px;
+ height: 32px;
+ }
+
+ &__list {
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+
+ @include uppercase;
+
+ @include on-tablet {
+ flex-flow: row wrap;
+ gap: 13.5px;
+ }
+ }
+
+ &__item {
+ display: flex;
+ align-items: center;
+ }
+
+ &__link {
+ color: var(--active-link);
+
+ @include transition;
+
+ &:hover {
+ color: var(--link);
+ }
+ }
+
+ &__container {
+ color: var(--link);
+ display: grid;
+ grid-template-columns: 1fr 32px;
+ align-items: center;
+ gap: 16px;
+
+ @include small-text;
+ }
+}
diff --git a/src/modules/Footer/Footer.tsx b/src/modules/Footer/Footer.tsx
new file mode 100644
index 0000000000..ea40f0eb2e
--- /dev/null
+++ b/src/modules/Footer/Footer.tsx
@@ -0,0 +1,38 @@
+import { Link } from 'react-router-dom';
+import { SvgIcon } from '../../components/SvgIcon';
+
+import styles from './Footer.module.scss';
+import { RoundButton } from '../../components/RoundButton';
+import { scrollToTop } from '../../utils/utility';
+import { Logo } from '../../components/Logo';
+
+export const Footer = () => {
+ const links = [
+ { title: 'Github', path: '#' },
+ { title: 'Contacts', path: '#' },
+ { title: 'Rights', path: '#' },
+ ];
+
+ return (
+
+ );
+};
diff --git a/src/modules/Footer/index.ts b/src/modules/Footer/index.ts
new file mode 100644
index 0000000000..65e2506faf
--- /dev/null
+++ b/src/modules/Footer/index.ts
@@ -0,0 +1 @@
+export { Footer } from './Footer';
diff --git a/src/modules/Header/Header.module.scss b/src/modules/Header/Header.module.scss
new file mode 100644
index 0000000000..9b03edfe8d
--- /dev/null
+++ b/src/modules/Header/Header.module.scss
@@ -0,0 +1,116 @@
+@import '../../styles/utils';
+
+.header {
+ border-bottom: 1px solid var(--elements);
+ position: sticky;
+ top: 0;
+ z-index: 2;
+ background-color: var(--bg-header-footer);
+ display: flex;
+ gap: 16px;
+ justify-content: space-between;
+ height: 48px;
+
+ @include on-tablet {
+ display: grid;
+ grid-template-columns: min-content 1fr;
+ }
+
+ @include on-desktop {
+ height: 64px;
+ }
+
+ &__link-logo {
+ display: flex;
+ width: min-content;
+ align-items: center;
+ justify-content: center;
+ padding: 13px 16px;
+
+ @include transition;
+
+ &:hover {
+ scale: 1.1;
+ }
+ }
+
+ &__logo {
+ display: block;
+ width: 64px;
+ height: 22px;
+
+ @include on-desktop {
+ width: 80px;
+ height: 28px;
+ }
+ }
+
+ &__menu {
+ position: fixed;
+ z-index: 1;
+ inset: 0;
+ background-color: var(--bg-header-footer);
+ padding-top: 72px;
+
+ @include on-tablet {
+ display: none;
+ }
+ }
+
+ &__wrapper {
+ display: flex;
+ gap: 8px;
+
+ &--mobile {
+ @include on-tablet {
+ display: none;
+ }
+ }
+ }
+
+ &__theme-switcher {
+ width: 48px;
+ height: 48px;
+
+ @include on-desktop {
+ width: 64px;
+ height: 64px;
+ }
+ }
+
+ &__menu-btn {
+ height: 100%;
+ width: 48px;
+ border: none;
+ border-left: 1px solid var(--elements);
+ background-color: inherit;
+
+ display: flex;
+ align-items: center;
+ justify-content: center;
+
+ svg path {
+ fill: var(--icon-primary);
+ }
+ }
+
+ &__nav-bar {
+ display: none;
+
+ @include on-tablet {
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: space-between;
+ align-items: center;
+ }
+ }
+
+ &__actions {
+ display: flex;
+ height: 48px;
+
+ @include on-desktop {
+ height: 64px;
+ }
+ }
+}
diff --git a/src/modules/Header/Header.tsx b/src/modules/Header/Header.tsx
new file mode 100644
index 0000000000..6404fe3d84
--- /dev/null
+++ b/src/modules/Header/Header.tsx
@@ -0,0 +1,68 @@
+import { NavLink, useLocation } from 'react-router-dom';
+import styles from './Header.module.scss';
+import { useEffect, useState } from 'react';
+import { Menu } from './components/Menu';
+import { Navigation } from './components/Navigation';
+import { Actions } from './components/Actions';
+import { MainNavigation } from '../../utils/constants';
+import { disableScroll, enableScroll } from '../../utils/utility';
+import { ThemeSwitcher } from './components/ThemeSwitcher';
+import { Logo } from '../../components/Logo';
+import { SvgIcon } from '../../components/SvgIcon';
+import classNames from 'classnames';
+
+export const Header = () => {
+ const [isMenuOpen, setIsMenuOpen] = useState(false);
+ const location = useLocation();
+
+ useEffect(() => {
+ setIsMenuOpen(false);
+ }, [location]);
+
+ useEffect(() => {
+ if (isMenuOpen) {
+ disableScroll();
+ } else {
+ enableScroll();
+ }
+ }, [isMenuOpen]);
+
+ return (
+ <>
+
+
+ {isMenuOpen && }
+ >
+ );
+};
diff --git a/src/modules/Header/components/Actions/Actions.module.scss b/src/modules/Header/components/Actions/Actions.module.scss
new file mode 100644
index 0000000000..631d62829a
--- /dev/null
+++ b/src/modules/Header/components/Actions/Actions.module.scss
@@ -0,0 +1,72 @@
+@import '../../../../styles/utils';
+
+.actions {
+ &__item {
+ position: relative;
+ width: 100%;
+ height: 100%;
+ box-shadow: -1px 0 0 0 var(--elements);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+
+ @include on-tablet {
+ width: 48px;
+ }
+
+ @include on-desktop {
+ width: 64px;
+ }
+
+ &::after {
+ position: absolute;
+ content: '';
+ width: 100%;
+ height: 2px;
+ background-color: var(--active-link);
+ bottom: 0;
+ left: 0;
+
+ transition: transform 0.3s;
+ transform: scale(0);
+ transform-origin: left;
+ }
+
+ &:hover {
+ &::after {
+ transform: scale(1);
+ }
+ }
+
+ &--active {
+ cursor: auto;
+
+ &::after {
+ transform: scale(1);
+ }
+ }
+
+ svg path {
+ fill: var(--icon-primary);
+ }
+ }
+
+ &__count {
+ position: absolute;
+ border-radius: 50%;
+ background-color: var(--secondary-accent);
+ color: var(--white);
+ width: 14px;
+ height: 14px;
+
+ font-size: 9px;
+ font-weight: 700;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ bottom: 50%;
+ left: 50%;
+
+ border: 1px solid var(--bg-header-footer);
+ }
+}
diff --git a/src/modules/Header/components/Actions/Actions.tsx b/src/modules/Header/components/Actions/Actions.tsx
new file mode 100644
index 0000000000..b93319c4dd
--- /dev/null
+++ b/src/modules/Header/components/Actions/Actions.tsx
@@ -0,0 +1,53 @@
+import { NavLink } from 'react-router-dom';
+import cn from 'classnames';
+import styles from './Actions.module.scss';
+import { useContext } from 'react';
+import { StateContext } from '../../../../contex/State';
+import { SvgIcon } from '../../../../components/SvgIcon';
+
+interface Props {
+ className?: string;
+}
+
+interface Action {
+ name: string;
+ count: number;
+ icon: 'heart' | 'cart';
+}
+
+export const Actions: React.FC = ({ className }) => {
+ const { favourites, cart } = useContext(StateContext);
+
+ const actions: Action[] = [
+ { name: 'favourites', count: favourites.length, icon: 'heart' },
+ {
+ name: 'cart',
+ count: cart.reduce((count, p) => count + p.quantity, 0),
+ icon: 'cart',
+ },
+ ];
+
+ return (
+
+ {actions.map(action => {
+ const { name, count, icon } = action;
+
+ return (
+
+ cn(styles.actions__item, {
+ [styles['actions__item--active']]: isActive,
+ })
+ }
+ to={`/${name}`}
+ >
+
+ {!!count && {count}
}
+ {name}
+
+ );
+ })}
+
+ );
+};
diff --git a/src/modules/Header/components/Actions/index.ts b/src/modules/Header/components/Actions/index.ts
new file mode 100644
index 0000000000..ac0eab8d42
--- /dev/null
+++ b/src/modules/Header/components/Actions/index.ts
@@ -0,0 +1 @@
+export { Actions } from './Actions';
diff --git a/src/modules/Header/components/Menu/Menu.module.scss b/src/modules/Header/components/Menu/Menu.module.scss
new file mode 100644
index 0000000000..7eb522e6d5
--- /dev/null
+++ b/src/modules/Header/components/Menu/Menu.module.scss
@@ -0,0 +1,22 @@
+@import '../../../../styles/utils';
+
+.menu {
+ display: flex;
+ flex-direction: column;
+ justify-content: space-between;
+
+ &__navigation {
+ @include page-padding-inline;
+ }
+
+ &__actions {
+ display: grid;
+ grid-template-columns: repeat(2, 1fr);
+ border-top: 1px solid var(--elements);
+ height: 64px;
+ }
+
+ @include on-tablet {
+ display: none;
+ }
+}
diff --git a/src/modules/Header/components/Menu/Menu.tsx b/src/modules/Header/components/Menu/Menu.tsx
new file mode 100644
index 0000000000..df046b823f
--- /dev/null
+++ b/src/modules/Header/components/Menu/Menu.tsx
@@ -0,0 +1,19 @@
+import { Navigation } from '../Navigation';
+
+import styles from './Menu.module.scss';
+import React from 'react';
+import { Actions } from '../Actions';
+
+interface Props {
+ className?: string;
+}
+
+export const Menu: React.FC = ({ className }) => {
+ return (
+
+ );
+};
diff --git a/src/modules/Header/components/Menu/index.ts b/src/modules/Header/components/Menu/index.ts
new file mode 100644
index 0000000000..81b93712f1
--- /dev/null
+++ b/src/modules/Header/components/Menu/index.ts
@@ -0,0 +1 @@
+export { Menu } from './Menu';
diff --git a/src/modules/Header/components/Navigation/Navigation.module.scss b/src/modules/Header/components/Navigation/Navigation.module.scss
new file mode 100644
index 0000000000..2b08f074d6
--- /dev/null
+++ b/src/modules/Header/components/Navigation/Navigation.module.scss
@@ -0,0 +1,65 @@
+@import '../../../../styles/utils';
+
+.navigation {
+ &__list {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 16px;
+
+ @include on-tablet {
+ flex-direction: row;
+ }
+ }
+
+ &__link {
+ position: relative;
+ display: block;
+ text-transform: uppercase;
+ font-size: 12px;
+ font-weight: 800;
+ line-height: 27px;
+ letter-spacing: 0.04em;
+ color: var(--link);
+
+ @include transition;
+
+ @include on-tablet {
+ line-height: 48px;
+ }
+
+ @include on-desktop {
+ line-height: 64px;
+ }
+
+ &::after {
+ position: absolute;
+ content: '';
+ width: 100%;
+ height: 2px;
+ background-color: var(--active-link);
+ bottom: 0;
+ left: 0;
+
+ transition: transform 0.3s;
+ transform: scale(0);
+ transform-origin: left;
+ }
+
+ &:hover {
+ color: var(--active-link);
+
+ &::after {
+ transform: scale(1);
+ }
+ }
+
+ &--active {
+ color: var(--active-link);
+
+ &::after {
+ transform: scale(1);
+ }
+ }
+ }
+}
diff --git a/src/modules/Header/components/Navigation/Navigation.tsx b/src/modules/Header/components/Navigation/Navigation.tsx
new file mode 100644
index 0000000000..0c4fafa6af
--- /dev/null
+++ b/src/modules/Header/components/Navigation/Navigation.tsx
@@ -0,0 +1,42 @@
+import { NavLink } from 'react-router-dom';
+
+import styles from './Navigation.module.scss';
+import React from 'react';
+import cn from 'classnames';
+import { MainNavigation } from '../../../../utils/constants';
+
+const mainNav = [
+ { title: 'Home', path: MainNavigation.HOME },
+ { title: 'Phones', path: MainNavigation.PHONES },
+ { title: 'Tablets', path: MainNavigation.TABLETS },
+ { title: 'Accessories', path: MainNavigation.ACCESSORIES },
+];
+
+interface Props {
+ className?: string;
+}
+
+export const Navigation: React.FC = ({ className }) => {
+ return (
+
+ );
+};
diff --git a/src/modules/Header/components/Navigation/index.ts b/src/modules/Header/components/Navigation/index.ts
new file mode 100644
index 0000000000..61f39d15c9
--- /dev/null
+++ b/src/modules/Header/components/Navigation/index.ts
@@ -0,0 +1 @@
+export { Navigation } from './Navigation';
diff --git a/src/modules/Header/components/ThemeSwitcher/ThemeSwitcher.module.scss b/src/modules/Header/components/ThemeSwitcher/ThemeSwitcher.module.scss
new file mode 100644
index 0000000000..21ddbaa114
--- /dev/null
+++ b/src/modules/Header/components/ThemeSwitcher/ThemeSwitcher.module.scss
@@ -0,0 +1,22 @@
+@import '../../../../styles/utils';
+
+.theme-switcher {
+ &__btn {
+ padding: 0;
+ border: none;
+ background-color: inherit;
+ height: 100%;
+ width: 100%;
+ background-size: 24px;
+ background-position: center;
+ background-repeat: no-repeat;
+
+ &--light {
+ background-image: url('../../../../assets/images/icons/sun.svg');
+ }
+
+ &--dark {
+ background-image: url('../../../../assets/images/icons/moon.svg');
+ }
+ }
+}
diff --git a/src/modules/Header/components/ThemeSwitcher/ThemeSwitcher.tsx b/src/modules/Header/components/ThemeSwitcher/ThemeSwitcher.tsx
new file mode 100644
index 0000000000..32fbd27546
--- /dev/null
+++ b/src/modules/Header/components/ThemeSwitcher/ThemeSwitcher.tsx
@@ -0,0 +1,38 @@
+import { useContext } from 'react';
+import { ThemeContext } from '../../../../contex/Theme';
+import { Theme } from '../../../../utils/constants';
+
+import styles from './ThemeSwitcher.module.scss';
+import cn from 'classnames';
+
+interface Props {
+ className?: string;
+}
+
+export const ThemeSwitcher: React.FC = ({ className }) => {
+ const { theme, setTheme } = useContext(ThemeContext);
+
+ return (
+
+ {theme === Theme.LIGTH ? (
+
+ );
+};
diff --git a/src/modules/Header/components/ThemeSwitcher/index.ts b/src/modules/Header/components/ThemeSwitcher/index.ts
new file mode 100644
index 0000000000..5c112587e9
--- /dev/null
+++ b/src/modules/Header/components/ThemeSwitcher/index.ts
@@ -0,0 +1 @@
+export { ThemeSwitcher } from './ThemeSwitcher';
diff --git a/src/modules/Header/index.ts b/src/modules/Header/index.ts
new file mode 100644
index 0000000000..29429dc97e
--- /dev/null
+++ b/src/modules/Header/index.ts
@@ -0,0 +1 @@
+export { Header } from './Header';
diff --git a/src/modules/HomePage/HomePage.module.scss b/src/modules/HomePage/HomePage.module.scss
new file mode 100644
index 0000000000..fdfef7c6e2
--- /dev/null
+++ b/src/modules/HomePage/HomePage.module.scss
@@ -0,0 +1,52 @@
+@import '../../styles/utils';
+
+.home-page {
+ max-width: $desktop-width;
+ margin: 24px auto 64px;
+
+ display: flex;
+ flex-direction: column;
+ gap: 56px;
+
+ @include on-tablet {
+ gap: 64px;
+ margin-top: 32px;
+ }
+
+ @include on-desktop {
+ gap: 80px;
+ margin-top: 56px;
+ margin-bottom: 80px;
+ }
+
+ &__wrapper {
+ display: flex;
+ flex-direction: column;
+ gap: 24px;
+
+ @include on-tablet {
+ gap: 32px;
+ }
+
+ @include on-desktop {
+ gap: 56px;
+ }
+ }
+
+ &__title {
+ margin: 0;
+
+ @include h1-mobile;
+ @include page-padding-inline;
+
+ @include on-tablet {
+ @include h1-tablet-desktop;
+ }
+ }
+
+ &__title-break {
+ @include on-desktop {
+ display: none;
+ }
+ }
+}
diff --git a/src/modules/HomePage/HomePage.tsx b/src/modules/HomePage/HomePage.tsx
new file mode 100644
index 0000000000..cdd0047a14
--- /dev/null
+++ b/src/modules/HomePage/HomePage.tsx
@@ -0,0 +1,36 @@
+import { useEffect, useState } from 'react';
+import styles from './HomePage.module.scss';
+import { Categories } from './components/Categories';
+import { MainSlider } from './components/MainSlider';
+import { getHotPriceProducts, getNewProducts } from '../../servises/products';
+import { Product } from '../../types/Product';
+import { ProductsSlider } from '../../components/ProductsSlider';
+
+export const HomePage = () => {
+ const [newModels, setNewModels] = useState([]);
+ const [hotPrices, setHotPrices] = useState([]);
+
+ useEffect(() => {
+ getHotPriceProducts().then(setHotPrices);
+ getNewProducts().then(setNewModels);
+ }, []);
+
+ return (
+
+
+
Product Catalog
+
+ Welcome to Nice
+ Gadgets store!
+
+
+
+
+
+
+
+
+
+
+ );
+};
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..7849eb6ae1
--- /dev/null
+++ b/src/modules/HomePage/components/Categories/Categories.module.scss
@@ -0,0 +1,63 @@
+@import '../../../../styles/utils';
+
+.categories {
+ @include page-padding-inline;
+
+ &__title {
+ margin-bottom: 24px;
+
+ @include h2-mobile;
+
+ @include on-tablet {
+ @include h2-tablet-desktop;
+ }
+ }
+
+ &__list {
+ display: flex;
+ flex-direction: column;
+ gap: 32px;
+
+ @include on-tablet {
+ display: grid;
+ grid-template-columns: repeat(3, 1fr);
+ gap: 16px;
+ }
+ }
+
+ &__item {
+ display: flex;
+ flex-direction: column;
+ }
+
+ &__item-title {
+ margin: 0;
+ margin-bottom: 4px;
+
+ @include h4-tablet-desktop;
+ }
+
+ &__text {
+ color: var(--secondary);
+
+ @include body-text;
+ }
+
+ &__img {
+ display: block;
+ width: 100%;
+ aspect-ratio: 1 / 1;
+ object-fit: contain;
+ border-radius: 8px;
+ margin-bottom: 24px;
+ overflow: hidden;
+
+ @include transition;
+ }
+
+ &__link {
+ &:hover .categories__img {
+ transform: scale(1.05);
+ }
+ }
+}
diff --git a/src/modules/HomePage/components/Categories/Categories.tsx b/src/modules/HomePage/components/Categories/Categories.tsx
new file mode 100644
index 0000000000..29f408f78d
--- /dev/null
+++ b/src/modules/HomePage/components/Categories/Categories.tsx
@@ -0,0 +1,79 @@
+import { Link } from 'react-router-dom';
+import styles from './Categories.module.scss';
+import { MainNavigation } from '../../../../utils/constants';
+import { useEffect, useState } from 'react';
+import { getAllProducts } from '../../../../servises/products';
+import { Category } from '../../../../types';
+
+export const Categories = () => {
+ const initialCategories = [
+ {
+ title: 'Mobile phones',
+ count: 0,
+ category: 'phones',
+ img: 'img/category-phones.png',
+ path: MainNavigation.PHONES,
+ },
+ {
+ title: 'Tablets',
+ count: 0,
+ category: 'tablets',
+ img: 'img/category-tablets.png',
+ path: MainNavigation.TABLETS,
+ },
+ {
+ title: 'Accessories',
+ count: 0,
+ category: 'accessories',
+ img: 'img/category-accessories.png',
+ path: MainNavigation.ACCESSORIES,
+ },
+ ];
+ const [categories, setCategories] = useState(initialCategories);
+
+ useEffect(() => {
+ getAllProducts.then(products => {
+ const counts: Record = {
+ phones: 0,
+ tablets: 0,
+ accessories: 0,
+ };
+
+ products.forEach(p => {
+ counts[p.category] += 1;
+ });
+
+ setCategories(current =>
+ current.map(category => ({
+ ...category,
+ count: counts[category.category as Category],
+ })),
+ );
+ });
+ }, []);
+
+ return (
+
+ Shop by category
+
+ {categories.map(category => {
+ const { path, img, count, title } = category;
+
+ return (
+ -
+
+
+
{title}
+ {count} models
+
+
+ );
+ })}
+
+
+ );
+};
diff --git a/src/modules/HomePage/components/Categories/index.ts b/src/modules/HomePage/components/Categories/index.ts
new file mode 100644
index 0000000000..8ae5932a87
--- /dev/null
+++ b/src/modules/HomePage/components/Categories/index.ts
@@ -0,0 +1 @@
+export { Categories } from './Categories';
diff --git a/src/modules/HomePage/components/MainSlider/MainSlider.module.scss b/src/modules/HomePage/components/MainSlider/MainSlider.module.scss
new file mode 100644
index 0000000000..d957eab49b
--- /dev/null
+++ b/src/modules/HomePage/components/MainSlider/MainSlider.module.scss
@@ -0,0 +1,120 @@
+@import '../../../../styles/utils';
+
+.main-slider {
+ @include on-tablet {
+ padding-inline: $tablet-padding-inline;
+ }
+
+ @include on-desktop {
+ padding-inline: desktop-padding-inline;
+ }
+
+ &__container {
+ display: flex;
+ width: 100%;
+ height: 320px;
+ gap: 16px;
+
+ @include on-tablet {
+ height: 360px;
+ }
+
+ @include on-desktop {
+ height: 400px;
+ }
+ }
+
+ &__controller {
+ display: none;
+
+ &:hover {
+ animation: squish 200ms ease-in-out;
+ }
+
+ @include on-tablet {
+ display: block;
+ width: 32px;
+ border-radius: 48px;
+ border: 1px solid var(--icon-secondary);
+ background-color: inherit;
+ padding: 8px;
+
+ svg {
+ path {
+ fill: var(--icon-primary);
+ }
+ }
+ }
+
+ &--next {
+ svg {
+ transform: rotate(90deg);
+ }
+ }
+
+ &--prev {
+ svg {
+ transform: rotate(-90deg);
+ }
+ }
+ }
+
+ &__list {
+ display: flex;
+ overflow: hidden;
+ width: 100%;
+ height: 100%;
+
+ @include on-tablet {
+ border-radius: 8px;
+ }
+ }
+
+ &__slide {
+ width: 100%;
+ height: 100%;
+ flex: 0 0 auto;
+ transition: transform 0.5s ease-in-out;
+ background: var(--bg-secondary);
+ }
+
+ &__img {
+ display: block;
+ object-fit: cover;
+ width: 100%;
+ height: 100%;
+ }
+
+ &__dots {
+ display: flex;
+ gap: 4px;
+ justify-content: center;
+ }
+
+ &__dot {
+ width: 24px;
+ height: 24px;
+ border: none;
+ background-color: inherit;
+
+ &::after {
+ content: '';
+ display: block;
+ width: 14px;
+ height: 4px;
+ background-color: var(--elements);
+ }
+
+ &--active {
+ &::after {
+ background-color: var(--primary);
+ }
+ }
+ }
+}
+
+@keyframes squish {
+ 50% {
+ scale: 1.2 0.9;
+ }
+}
diff --git a/src/modules/HomePage/components/MainSlider/MainSlider.tsx b/src/modules/HomePage/components/MainSlider/MainSlider.tsx
new file mode 100644
index 0000000000..b6b8db408f
--- /dev/null
+++ b/src/modules/HomePage/components/MainSlider/MainSlider.tsx
@@ -0,0 +1,93 @@
+import cn from 'classnames';
+import styles from './MainSlider.module.scss';
+import { SvgIcon } from '../../../../components/SvgIcon';
+import { useEffect, useState } from 'react';
+
+const slides = [
+ 'img/banner-accessories.png',
+ 'img/banner-phones.png',
+ 'img/banner-tablets.png',
+];
+
+interface Props {
+ className: string;
+}
+
+export const MainSlider: React.FC = ({ className = '' }) => {
+ const [currentSlide, setCurrentSlide] = useState(0);
+
+ const handleNext = () => {
+ setCurrentSlide(prev => (prev + 1) % slides.length);
+ };
+
+ const handlePrev = () => {
+ setCurrentSlide(prev => (prev - 1 + slides.length) % slides.length);
+ };
+
+ useEffect(() => {
+ const intervalId = setInterval(handleNext, 5000);
+
+ return () => clearInterval(intervalId);
+ }, []);
+
+ return (
+
+
+
+
+
+
+
+ {slides.map(slide => {
+ return (
+ -
+
+
+ );
+ })}
+
+
+
+
+
+
+
+ {slides.map((_, index) => {
+ const isActive = index === currentSlide;
+
+ return (
+ setCurrentSlide(index)}
+ className={cn(styles['main-slider__dot'], {
+ [styles['main-slider__dot--active']]: isActive,
+ })}
+ />
+ );
+ })}
+
+
+ );
+};
diff --git a/src/modules/HomePage/components/MainSlider/index.ts b/src/modules/HomePage/components/MainSlider/index.ts
new file mode 100644
index 0000000000..c1dbaddb82
--- /dev/null
+++ b/src/modules/HomePage/components/MainSlider/index.ts
@@ -0,0 +1 @@
+export { MainSlider } from './MainSlider';
diff --git a/src/modules/HomePage/index.ts b/src/modules/HomePage/index.ts
new file mode 100644
index 0000000000..0799f479a2
--- /dev/null
+++ b/src/modules/HomePage/index.ts
@@ -0,0 +1 @@
+export { 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..a81d3b497a
--- /dev/null
+++ b/src/modules/NotFoundPage/NotFoundPage.module.scss
@@ -0,0 +1,31 @@
+@import '../../styles/utils';
+
+.not-found-page {
+ max-width: $desktop-width;
+ margin: 0 auto;
+ padding-top: 24px;
+ padding-bottom: 32px;
+ display: flex;
+ flex-direction: column;
+ gap: 24px;
+
+ @include page-padding-inline;
+
+ &__title {
+ margin: 0;
+
+ @include h1-mobile;
+
+ @include on-tablet {
+ @include h1-tablet-desktop;
+ }
+ }
+
+ &__img {
+ width: 100%;
+ height: 100%;
+ object-fit: contain;
+ max-width: 600px;
+ align-self: center;
+ }
+}
diff --git a/src/modules/NotFoundPage/NotFoundPage.tsx b/src/modules/NotFoundPage/NotFoundPage.tsx
new file mode 100644
index 0000000000..745124c09f
--- /dev/null
+++ b/src/modules/NotFoundPage/NotFoundPage.tsx
@@ -0,0 +1,16 @@
+import { BackLink } from '../../components/BackLink';
+import styles from './NotFoundPage.module.scss';
+
+export const NotFoundPage = () => {
+ return (
+
+
+
Page not found
+
+
+ );
+};
diff --git a/src/modules/NotFoundPage/index.ts b/src/modules/NotFoundPage/index.ts
new file mode 100644
index 0000000000..642c600088
--- /dev/null
+++ b/src/modules/NotFoundPage/index.ts
@@ -0,0 +1 @@
+export { NotFoundPage } from './NotFoundPage';
diff --git a/src/modules/PhonesPage/PhonesPage.tsx b/src/modules/PhonesPage/PhonesPage.tsx
new file mode 100644
index 0000000000..c5982134e1
--- /dev/null
+++ b/src/modules/PhonesPage/PhonesPage.tsx
@@ -0,0 +1,27 @@
+import { useEffect, useState } from 'react';
+import { Product } from '../../types/Product';
+import { getProductsByCategory } from '../../servises/products';
+import { ProductsCatalog } from '../ProductsCatalog';
+
+export const PhonesPage = () => {
+ const [phones, setPhones] = useState([]);
+ const [loading, setLoading] = useState(false);
+ const [error, setError] = useState('');
+
+ useEffect(() => {
+ setLoading(true);
+ getProductsByCategory('phones')
+ .then(setPhones)
+ .catch(() => setError('Something went wrong'))
+ .finally(() => setLoading(false));
+ }, []);
+
+ return (
+
+ );
+};
diff --git a/src/modules/PhonesPage/index.ts b/src/modules/PhonesPage/index.ts
new file mode 100644
index 0000000000..6054067fc6
--- /dev/null
+++ b/src/modules/PhonesPage/index.ts
@@ -0,0 +1 @@
+export { PhonesPage } from './PhonesPage';
diff --git a/src/modules/ProductPage/ProductPage.module.scss b/src/modules/ProductPage/ProductPage.module.scss
new file mode 100644
index 0000000000..2944ca5ec1
--- /dev/null
+++ b/src/modules/ProductPage/ProductPage.module.scss
@@ -0,0 +1,59 @@
+@import '../../styles/utils';
+
+.product-page {
+ max-width: $desktop-width;
+ margin: 0 auto;
+ padding-top: 24px;
+ padding-bottom: 56px;
+
+ @include on-tablet {
+ padding-bottom: 64px;
+ }
+
+ @include on-desktop {
+ padding-bottom: 80px;
+ }
+
+ &__nav-wrapper {
+ display: flex;
+ flex-direction: column;
+ gap: 24px;
+ margin-bottom: 16px;
+
+ @include page-padding-inline;
+
+ @include on-tablet {
+ gap: 40px;
+ }
+ }
+
+ &__gadget {
+ margin-bottom: 64px;
+
+ @include on-tablet {
+ margin-bottom: 80px;
+ }
+ }
+
+ &__not-found {
+ @include page-padding-inline;
+ }
+
+ &__not-found-text {
+ margin-bottom: 24px;
+
+ @include h1-mobile;
+
+ @include on-tablet {
+ @include h1-tablet-desktop;
+ }
+ }
+
+ &__not-found-img {
+ display: block;
+ width: 100%;
+ height: 100%;
+ max-width: 600px;
+ margin: 0 auto;
+ }
+}
diff --git a/src/modules/ProductPage/ProductPage.tsx b/src/modules/ProductPage/ProductPage.tsx
new file mode 100644
index 0000000000..dea676c762
--- /dev/null
+++ b/src/modules/ProductPage/ProductPage.tsx
@@ -0,0 +1,82 @@
+import styles from './ProductPage.module.scss';
+import { BackLink } from '../../components/BackLink';
+import { useLocation, useParams } from 'react-router-dom';
+import { useEffect, useState } from 'react';
+import { getGadgetById } from '../../servises/gadgets';
+import { Category, Product, GadgetType } from '../../types';
+
+import { Breadcrumbs } from '../../components/Breadcrumbs';
+import { ProductsSlider } from '../../components/ProductsSlider';
+import { getProductById, getSuggestedProducts } from '../../servises/products';
+import { Gadget } from './components/Gadget';
+import { Loader } from '../../components/Loader';
+
+export const ProductPage = () => {
+ const [currentGadget, setCurrentGadget] = useState(null);
+ const [currentProduct, setCurrentProduct] = useState(null);
+ const [suggestedProducts, setSuggestedProducts] = useState([]);
+ const [isLoading, setIsLoading] = useState(false);
+
+ const params = useParams();
+ const location = useLocation();
+
+ const category = location.pathname.split('/')[1] as Category;
+
+ const id = params.id as string;
+
+ useEffect(() => {
+ const fetchData = async () => {
+ try {
+ setIsLoading(true);
+ const gadget = await getGadgetById(category, id);
+ const product = await getProductById(id);
+ const suggested = await getSuggestedProducts();
+
+ setCurrentGadget(gadget);
+ setCurrentProduct(product);
+ setSuggestedProducts(suggested);
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ fetchData();
+ }, [category, id]);
+
+ if (isLoading) {
+ return ;
+ }
+
+ return (
+
+
+
+
+
+ {!currentGadget || !currentProduct ? (
+
+
+ Product was not found
+
+
+
+ ) : (
+ <>
+
+
+ >
+ )}
+
+ );
+};
diff --git a/src/modules/ProductPage/components/About/About.module.scss b/src/modules/ProductPage/components/About/About.module.scss
new file mode 100644
index 0000000000..7112775d3d
--- /dev/null
+++ b/src/modules/ProductPage/components/About/About.module.scss
@@ -0,0 +1,47 @@
+@import '../../../../styles/utils';
+
+.about {
+ &__title {
+ margin: 0;
+ margin-bottom: 32px;
+
+ @include h3-mobile;
+
+ @include on-tablet {
+ @include h3-tablet-desktop;
+ }
+
+ &::after {
+ display: block;
+ content: '';
+ height: 1px;
+ background-color: var(--elements);
+ margin-top: 16px;
+ }
+ }
+
+ &__list {
+ display: flex;
+ flex-direction: column;
+ gap: 32px;
+ }
+
+ &__subtitle {
+ margin: 0;
+ margin-bottom: 16px;
+
+ @include h4-mobile;
+
+ @include on-tablet {
+ @include h4-tablet-desktop;
+ }
+ }
+
+ &__text {
+ color: var(--secondary);
+
+ &:not(:last-child) {
+ margin-bottom: 21px;
+ }
+ }
+}
diff --git a/src/modules/ProductPage/components/About/About.tsx b/src/modules/ProductPage/components/About/About.tsx
new file mode 100644
index 0000000000..5829a63e1f
--- /dev/null
+++ b/src/modules/ProductPage/components/About/About.tsx
@@ -0,0 +1,36 @@
+import cn from 'classnames';
+import { Description } from '../../../../types/Gadget';
+
+import styles from './About.module.scss';
+
+interface Props {
+ description: Description[];
+ className?: string;
+}
+
+export const About: React.FC = ({ description, className }) => {
+ return (
+
+
About
+
+
+ {description.map(desc => {
+ const { title, text } = desc;
+
+ return (
+ -
+
{title}
+ {text.map(t => {
+ return (
+
+ {t}
+
+ );
+ })}
+
+ );
+ })}
+
+
+ );
+};
diff --git a/src/modules/ProductPage/components/About/index.ts b/src/modules/ProductPage/components/About/index.ts
new file mode 100644
index 0000000000..b749bad6d5
--- /dev/null
+++ b/src/modules/ProductPage/components/About/index.ts
@@ -0,0 +1 @@
+export { About } from './About';
diff --git a/src/modules/ProductPage/components/AvailableCapacity/AvailableCapacity.module.scss b/src/modules/ProductPage/components/AvailableCapacity/AvailableCapacity.module.scss
new file mode 100644
index 0000000000..769bce95ed
--- /dev/null
+++ b/src/modules/ProductPage/components/AvailableCapacity/AvailableCapacity.module.scss
@@ -0,0 +1,50 @@
+@import '../../../../styles/utils';
+
+.available-capacity {
+ padding-bottom: 24px;
+ position: relative;
+
+ &::after {
+ display: block;
+ position: absolute;
+ content: '';
+ width: 100%;
+ height: 1px;
+ background-color: var(--elements);
+ bottom: 0;
+ }
+
+ &__title {
+ @include small-text;
+
+ color: var(--secondary);
+ margin-bottom: 8px;
+ font-weight: 600;
+ }
+
+ &__list {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 8px;
+ }
+
+ &__link {
+ display: block;
+ border: 1px solid var(--icon-secondary);
+ padding: 5px 8px 4px;
+ border-radius: 4px;
+
+ @include transition;
+
+ &--active {
+ background-color: var(--primary);
+ border-color: var(--primary);
+ color: var(--text-active);
+ cursor: auto;
+ }
+
+ &:not(&--active):hover {
+ border-color: var(--primary);
+ }
+ }
+}
diff --git a/src/modules/ProductPage/components/AvailableCapacity/AvailableCapacity.tsx b/src/modules/ProductPage/components/AvailableCapacity/AvailableCapacity.tsx
new file mode 100644
index 0000000000..2fd219d290
--- /dev/null
+++ b/src/modules/ProductPage/components/AvailableCapacity/AvailableCapacity.tsx
@@ -0,0 +1,61 @@
+import { Link, useLocation } from 'react-router-dom';
+import styles from './AvailableCapacity.module.scss';
+import cn from 'classnames';
+
+interface Props {
+ capacity: string[];
+ activeCapacity: string;
+ className?: string;
+}
+
+export const AvailableCapacity: React.FC = ({
+ capacity,
+ activeCapacity,
+ className,
+}) => {
+ const location = useLocation();
+
+ const normalizeCapacity = (cap: string) => {
+ let res = '';
+
+ for (let i = 0; i < cap.length; i++) {
+ if (cap[i].toLowerCase() === cap[i].toUpperCase()) {
+ res += cap[i];
+ } else {
+ res += ' ' + cap.slice(i);
+ break;
+ }
+ }
+
+ return res;
+ };
+
+ const path = location.pathname.toLowerCase();
+
+ return (
+
+
Select capacity
+
+ {capacity.map(cap => {
+ const isActive = activeCapacity === cap;
+ const currentPath = path
+ .split(activeCapacity.toLowerCase())
+ .join(cap.toLowerCase());
+
+ return (
+ -
+
+ {normalizeCapacity(cap)}
+
+
+ );
+ })}
+
+
+ );
+};
diff --git a/src/modules/ProductPage/components/AvailableCapacity/index.ts b/src/modules/ProductPage/components/AvailableCapacity/index.ts
new file mode 100644
index 0000000000..738402b710
--- /dev/null
+++ b/src/modules/ProductPage/components/AvailableCapacity/index.ts
@@ -0,0 +1 @@
+export { AvailableCapacity } from './AvailableCapacity';
diff --git a/src/modules/ProductPage/components/AvailableColors/AvailableColors.module.scss b/src/modules/ProductPage/components/AvailableColors/AvailableColors.module.scss
new file mode 100644
index 0000000000..abf325ac39
--- /dev/null
+++ b/src/modules/ProductPage/components/AvailableColors/AvailableColors.module.scss
@@ -0,0 +1,52 @@
+@import '../../../../styles/utils';
+
+.available-colors {
+ padding-bottom: 24px;
+ position: relative;
+
+ &::after {
+ display: block;
+ position: absolute;
+ content: '';
+ width: 100%;
+ height: 1px;
+ background-color: var(--elements);
+ bottom: 0;
+ }
+
+ &__title {
+ @include small-text;
+
+ color: var(--secondary);
+ margin-bottom: 8px;
+ font-weight: 600;
+ }
+
+ &__list {
+ display: flex;
+ gap: 8px;
+ }
+
+ &__item {
+ border-radius: 50%;
+ width: 32px;
+ height: 32px;
+ border: 1px solid var(--elements);
+ padding: 2px;
+
+ &:not(&--active):hover {
+ border-color: var(--icon-secondary);
+ }
+
+ &--active {
+ border-color: var(--primary);
+ }
+ }
+
+ &__link {
+ display: block;
+ width: 100%;
+ height: 100%;
+ border-radius: 50%;
+ }
+}
diff --git a/src/modules/ProductPage/components/AvailableColors/AvailableColors.tsx b/src/modules/ProductPage/components/AvailableColors/AvailableColors.tsx
new file mode 100644
index 0000000000..ff679baaf2
--- /dev/null
+++ b/src/modules/ProductPage/components/AvailableColors/AvailableColors.tsx
@@ -0,0 +1,51 @@
+import { Link, useLocation } from 'react-router-dom';
+import styles from './AvailableColors.module.scss';
+import cn from 'classnames';
+import { colorMap } from '../../../../helpers';
+
+interface Props {
+ colors: string[];
+ currentColor: string;
+ className?: string;
+}
+
+export const AvailableColors: React.FC = ({
+ colors,
+ currentColor,
+ className,
+}) => {
+ const location = useLocation();
+
+ const path = location.pathname.toLowerCase();
+
+ return (
+
+
Available colors
+
+ {colors.map(c => {
+ const color = c.split(' ').join('-');
+
+ const isActive = c === currentColor;
+ const currentPath = path
+ .split(currentColor.split(' ').join('-'))
+ .join(color);
+
+ return (
+ -
+
+
+ );
+ })}
+
+
+ );
+};
diff --git a/src/modules/ProductPage/components/AvailableColors/index.ts b/src/modules/ProductPage/components/AvailableColors/index.ts
new file mode 100644
index 0000000000..ec9b7b3461
--- /dev/null
+++ b/src/modules/ProductPage/components/AvailableColors/index.ts
@@ -0,0 +1 @@
+export { AvailableColors } from './AvailableColors';
diff --git a/src/modules/ProductPage/components/Gadget/Gadget.module.scss b/src/modules/ProductPage/components/Gadget/Gadget.module.scss
new file mode 100644
index 0000000000..1ad368ff6d
--- /dev/null
+++ b/src/modules/ProductPage/components/Gadget/Gadget.module.scss
@@ -0,0 +1,44 @@
+@import '../../../../styles/utils';
+
+.gadget {
+ @include page-padding-inline;
+
+ &__title {
+ margin: 0;
+ margin-bottom: 32px;
+
+ @include h2-mobile;
+
+ @include on-tablet {
+ margin-bottom: 40px;
+
+ @include h2-tablet-desktop;
+ }
+ }
+
+ &__details {
+ display: flex;
+ flex-direction: column;
+ gap: 40px;
+
+ @include on-tablet {
+ display: grid;
+ grid-template-columns: 113fr 79fr;
+ gap: 16px;
+ }
+
+ @include on-desktop {
+ grid-template-columns: 35fr 32fr;
+ gap: 64px;
+ }
+ }
+
+ &__about,
+ &__tech-specs {
+ grid-column: 1 / -1;
+
+ @include on-desktop {
+ grid-column: auto;
+ }
+ }
+}
diff --git a/src/modules/ProductPage/components/Gadget/Gadget.tsx b/src/modules/ProductPage/components/Gadget/Gadget.tsx
new file mode 100644
index 0000000000..4832882a4e
--- /dev/null
+++ b/src/modules/ProductPage/components/Gadget/Gadget.tsx
@@ -0,0 +1,78 @@
+import cn from 'classnames';
+import { About } from '../About';
+import { ProductPhotos } from '../ProductPhotos';
+import { TechSpecs } from '../TechSpecs';
+import styles from './Gadget.module.scss';
+import { MainControls } from '../MainControls';
+import { Product, GadgetType } from '../../../../types';
+
+interface Props {
+ gadget: GadgetType;
+ className?: string;
+ product: Product;
+}
+
+export const Gadget: React.FC = ({ gadget, className, product }) => {
+ const {
+ name,
+ images,
+ description,
+ screen,
+ resolution,
+ processor,
+ ram,
+ capacity,
+ cell,
+ } = gadget;
+
+ const techSpecs = [
+ {
+ title: 'Screen',
+ value: screen,
+ },
+ {
+ title: 'Resolution',
+ value: resolution,
+ },
+ {
+ title: 'Processor',
+ value: processor,
+ },
+ {
+ title: 'RAM',
+ value: ram,
+ },
+ {
+ title: 'Built in memory',
+ value: capacity,
+ },
+ 'camera' in gadget && {
+ title: 'Camera',
+ value: gadget.camera,
+ },
+ 'zoom' in gadget && {
+ title: 'Zoom',
+ value: gadget.zoom,
+ },
+ {
+ title: 'Cell',
+ value: cell.join(', '),
+ },
+ ].filter(Boolean) as { title: string; value: string }[];
+
+ return (
+
+ );
+};
diff --git a/src/modules/ProductPage/components/Gadget/index.ts b/src/modules/ProductPage/components/Gadget/index.ts
new file mode 100644
index 0000000000..ea503cfb32
--- /dev/null
+++ b/src/modules/ProductPage/components/Gadget/index.ts
@@ -0,0 +1 @@
+export { Gadget } from './Gadget';
diff --git a/src/modules/ProductPage/components/MainControls/MainControls.module.scss b/src/modules/ProductPage/components/MainControls/MainControls.module.scss
new file mode 100644
index 0000000000..166468e258
--- /dev/null
+++ b/src/modules/ProductPage/components/MainControls/MainControls.module.scss
@@ -0,0 +1,82 @@
+@import '../../../../styles/utils';
+
+.main-controls {
+ position: relative;
+
+ @include on-desktop {
+ display: grid;
+ grid-template-columns: 320px 1fr;
+ gap: 16px;
+ }
+
+ &__colors {
+ margin-bottom: 24px;
+ }
+
+ &__capacity {
+ margin-bottom: 32px;
+ }
+
+ &__price {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ margin-bottom: 16px;
+ }
+
+ &__price-discount {
+ font-size: 32px;
+ font-weight: 800;
+ line-height: 41px;
+ }
+
+ &__price-full {
+ font-size: 22px;
+ font-weight: 500;
+ line-height: 28px;
+ text-decoration: line-through;
+ color: var(--secondary);
+ }
+
+ &__actions {
+ height: 48px;
+ margin-bottom: 32px;
+ }
+
+ &__prop-list {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ }
+
+ &__prop-item {
+ display: flex;
+ justify-content: space-between;
+ gap: 8px;
+
+ @include small-text;
+ }
+
+ &__term {
+ color: var(--secondary);
+ font-weight: 600;
+ }
+
+ &__desc {
+ margin: 0;
+ }
+
+ &__id {
+ @include small-text;
+
+ color: var(--icon-secondary);
+ position: absolute;
+ top: 0;
+ right: 0;
+
+ @include on-desktop {
+ position: static;
+ text-align: right;
+ }
+ }
+}
diff --git a/src/modules/ProductPage/components/MainControls/MainControls.tsx b/src/modules/ProductPage/components/MainControls/MainControls.tsx
new file mode 100644
index 0000000000..8e3d17f3bb
--- /dev/null
+++ b/src/modules/ProductPage/components/MainControls/MainControls.tsx
@@ -0,0 +1,78 @@
+import cn from 'classnames';
+
+import styles from './MainControls.module.scss';
+import { Actions } from '../../../../components/Actions';
+import { AvailableColors } from '../AvailableColors';
+import { AvailableCapacity } from '../AvailableCapacity';
+import { Product, GadgetType } from '../../../../types';
+
+interface Props {
+ gadget: GadgetType;
+ className?: string;
+ product: Product;
+}
+
+export const MainControls: React.FC = ({
+ className,
+ gadget,
+ product,
+}) => {
+ const {
+ colorsAvailable,
+ color,
+ priceDiscount,
+ priceRegular,
+ screen,
+ resolution,
+ processor,
+ ram,
+ capacityAvailable,
+ capacity,
+ } = gadget;
+
+ const properties = [
+ { term: 'Screen', desc: screen },
+ { term: 'Resolution', desc: resolution },
+ { term: 'Processor', desc: processor },
+ { term: 'RAM', desc: ram },
+ ];
+
+ return (
+
+
+
+
+
+
+ {priceDiscount}$
+
+
{priceRegular}$
+
+
+
+ {properties.map(property => (
+ -
+
- {property.term}
+ - {property.desc}
+
+ ))}
+
+
+
ID: {product.id}
+
+ );
+};
diff --git a/src/modules/ProductPage/components/MainControls/index.ts b/src/modules/ProductPage/components/MainControls/index.ts
new file mode 100644
index 0000000000..48877cc344
--- /dev/null
+++ b/src/modules/ProductPage/components/MainControls/index.ts
@@ -0,0 +1 @@
+export { MainControls } from './MainControls';
diff --git a/src/modules/ProductPage/components/ProductPhotos/ProductPhotos.module.scss b/src/modules/ProductPage/components/ProductPhotos/ProductPhotos.module.scss
new file mode 100644
index 0000000000..8de61560ea
--- /dev/null
+++ b/src/modules/ProductPage/components/ProductPhotos/ProductPhotos.module.scss
@@ -0,0 +1,72 @@
+@import '../../../../styles/utils';
+
+.product-photos {
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+ align-items: center;
+
+ @include on-tablet {
+ display: grid;
+ grid-template-columns: repeat(7, 1fr);
+ align-items: start;
+ }
+
+ @include on-desktop {
+ grid-template-columns: 5fr 29fr;
+ }
+
+ &__current {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 100%;
+ aspect-ratio: 1 / 1;
+ overflow: hidden;
+
+ @include on-tablet {
+ grid-column: 2 / -1;
+ }
+ }
+
+ &__list {
+ display: flex;
+ gap: 8px;
+ max-width: 100%;
+
+ @include on-tablet {
+ order: -1;
+ display: flex;
+ flex-direction: column;
+ }
+ }
+
+ &__item {
+ border: 1px solid var(--elements);
+ border-radius: 8px;
+ padding: 3px;
+ width: 51px;
+ height: 49px;
+
+ @include on-tablet {
+ width: 100%;
+ height: 100%;
+ aspect-ratio: 1 / 1;
+ }
+ }
+
+ &__btn {
+ border: none;
+ background-color: inherit;
+ width: 100%;
+ height: 100%;
+ padding: 0;
+ }
+
+ &__img {
+ display: block;
+ object-fit: contain;
+ width: 100%;
+ height: 100%;
+ }
+}
diff --git a/src/modules/ProductPage/components/ProductPhotos/ProductPhotos.tsx b/src/modules/ProductPage/components/ProductPhotos/ProductPhotos.tsx
new file mode 100644
index 0000000000..57b105c93d
--- /dev/null
+++ b/src/modules/ProductPage/components/ProductPhotos/ProductPhotos.tsx
@@ -0,0 +1,47 @@
+import { useEffect, useState } from 'react';
+import styles from './ProductPhotos.module.scss';
+import cn from 'classnames';
+
+interface Props {
+ photos: string[];
+ className?: string;
+}
+
+export const ProductPhotos: React.FC = ({ photos, className }) => {
+ const [currentImage, setCurrentImage] = useState(photos[0]);
+
+ useEffect(() => {
+ setCurrentImage(photos[0]);
+ }, [photos]);
+
+ return (
+
+
+
+
+
+ {photos.map(img => {
+ return (
+ - setCurrentImage(img)}
+ >
+
+
+
+
+ );
+ })}
+
+
+ );
+};
diff --git a/src/modules/ProductPage/components/ProductPhotos/index.ts b/src/modules/ProductPage/components/ProductPhotos/index.ts
new file mode 100644
index 0000000000..c200dd5eb1
--- /dev/null
+++ b/src/modules/ProductPage/components/ProductPhotos/index.ts
@@ -0,0 +1 @@
+export { ProductPhotos } from './ProductPhotos';
diff --git a/src/modules/ProductPage/components/TechSpecs/TechSpecs.module.scss b/src/modules/ProductPage/components/TechSpecs/TechSpecs.module.scss
new file mode 100644
index 0000000000..d4dab62cb4
--- /dev/null
+++ b/src/modules/ProductPage/components/TechSpecs/TechSpecs.module.scss
@@ -0,0 +1,45 @@
+@import '../../../../styles/utils';
+
+.tech-specs {
+ &__title {
+ margin: 0;
+ margin-bottom: 32px;
+
+ @include h3-mobile;
+
+ @include on-tablet {
+ @include h3-tablet-desktop;
+ }
+
+ &::after {
+ display: block;
+ content: '';
+ height: 1px;
+ background-color: var(--elements);
+ margin-top: 16px;
+ }
+ }
+
+ &__list {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ }
+
+ &__item {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ gap: 10px;
+ }
+
+ &__term {
+ font-weight: 500;
+ color: var(--secondary);
+ }
+
+ &__desc {
+ margin: 0;
+ font-weight: 600;
+ }
+}
diff --git a/src/modules/ProductPage/components/TechSpecs/TechSpecs.tsx b/src/modules/ProductPage/components/TechSpecs/TechSpecs.tsx
new file mode 100644
index 0000000000..b21dbdee14
--- /dev/null
+++ b/src/modules/ProductPage/components/TechSpecs/TechSpecs.tsx
@@ -0,0 +1,31 @@
+import cn from 'classnames';
+import styles from './TechSpecs.module.scss';
+
+interface Props {
+ className?: string;
+ techSpecs: {
+ title: string;
+ value: string;
+ }[];
+}
+
+export const TechSpecs: React.FC = ({ techSpecs, className }) => {
+ return (
+
+
Tech specs
+
+
+ {techSpecs.map(spec => {
+ const { title, value } = spec;
+
+ return (
+ -
+
- {title}
+ - {value}
+
+ );
+ })}
+
+
+ );
+};
diff --git a/src/modules/ProductPage/components/TechSpecs/index.ts b/src/modules/ProductPage/components/TechSpecs/index.ts
new file mode 100644
index 0000000000..fced6b5881
--- /dev/null
+++ b/src/modules/ProductPage/components/TechSpecs/index.ts
@@ -0,0 +1 @@
+export { TechSpecs } from './TechSpecs';
diff --git a/src/modules/ProductPage/index.ts b/src/modules/ProductPage/index.ts
new file mode 100644
index 0000000000..93f6975c68
--- /dev/null
+++ b/src/modules/ProductPage/index.ts
@@ -0,0 +1 @@
+export { ProductPage } from './ProductPage';
diff --git a/src/modules/ProductsCatalog/ProductsCatalog.module.scss b/src/modules/ProductsCatalog/ProductsCatalog.module.scss
new file mode 100644
index 0000000000..a276d20c98
--- /dev/null
+++ b/src/modules/ProductsCatalog/ProductsCatalog.module.scss
@@ -0,0 +1,73 @@
+@import '../../styles/utils';
+
+.products-catalog {
+ max-width: $desktop-width;
+ padding-top: 24px;
+ margin: 0 auto;
+ display: flex;
+ flex-direction: column;
+ gap: 24px;
+ padding-bottom: 64px;
+
+ @include page-padding-inline;
+
+ @include on-tablet {
+ gap: 40px;
+ }
+
+ &__title {
+ margin: 0;
+ margin-bottom: 8px;
+
+ @include h1-mobile;
+
+ @include on-tablet {
+ @include h1-tablet-desktop;
+ }
+ }
+
+ &__text {
+ color: var(--secondary);
+ margin-bottom: 8px;
+
+ @include on-tablet {
+ margin: 0;
+ }
+ }
+
+ &__controllers {
+ margin-bottom: 24px;
+
+ @include on-tablet {
+ display: flex;
+ gap: 16px;
+ }
+ }
+
+ &__selectors-wrapper {
+ display: grid;
+ grid-template-columns: repeat(2, 1fr);
+ gap: 16px;
+ margin-bottom: 16px;
+
+ @include on-tablet {
+ grid-template-columns: 187px 136px;
+ }
+
+ @include on-desktop {
+ grid-template-columns: 176px 128px;
+ }
+ }
+
+ &__no-found {
+ @include h2-mobile;
+
+ @include on-tablet {
+ @include h2-tablet-desktop;
+ }
+ }
+
+ &__pagination {
+ margin: 0 auto;
+ }
+}
diff --git a/src/modules/ProductsCatalog/ProductsCatalog.tsx b/src/modules/ProductsCatalog/ProductsCatalog.tsx
new file mode 100644
index 0000000000..15fa4de782
--- /dev/null
+++ b/src/modules/ProductsCatalog/ProductsCatalog.tsx
@@ -0,0 +1,143 @@
+import { Product } from '../../types/Product';
+import { Breadcrumbs } from '../../components/Breadcrumbs';
+import { Pagination } from './components/Pagination';
+import styles from './ProductsCatalog.module.scss';
+import { Selector } from './components/Selector';
+import { Grid } from '../../components/Grid';
+import { useUpdateSearchParams } from '../../hooks';
+import { Loader } from '../../components/Loader';
+import { Search } from './components/Search';
+
+interface Props {
+ title: string;
+ products: Product[];
+ loading: boolean;
+ error: string;
+}
+
+export const ProductsCatalog: React.FC = ({
+ title,
+ products,
+ loading,
+ error,
+}) => {
+ const { searchParams, updateSearchParams } = useUpdateSearchParams();
+
+ const SORT_OPTIONS = [
+ { name: 'newest', value: 'age' },
+ { name: 'alphabetically', value: 'title' },
+ { name: 'cheapest', value: 'price' },
+ ];
+
+ const PER_PAGE_OPTIONS = [
+ { name: '4', value: '4' },
+ { name: '8', value: '8' },
+ { name: '16', value: '16' },
+ { name: 'all', value: 'all' },
+ ];
+
+ const perPage =
+ PER_PAGE_OPTIONS.find(item => item.value === searchParams.get('perPage'))
+ ?.value || PER_PAGE_OPTIONS[0].value;
+
+ const sort = searchParams.get('sort') || SORT_OPTIONS[0].value;
+ const query = searchParams.get('query');
+
+ const refineProducts = (p: Product[]) => {
+ let currentProducts = p.slice();
+
+ if (query) {
+ currentProducts = currentProducts.filter(a =>
+ a.name.toLowerCase().includes(query.toLowerCase()),
+ );
+ }
+
+ return currentProducts.sort((a, b) => {
+ switch (sort) {
+ case 'age':
+ return b.year - a.year;
+ case 'title':
+ return a.name.localeCompare(b.name);
+ case 'price':
+ return b.price - a.price;
+ default:
+ return 0;
+ }
+ });
+ };
+
+ const refinedProducts = refineProducts(products);
+
+ const getCurrentPage = () => {
+ const page = Number(searchParams.get('page')) || 1;
+ const maxPages = Math.ceil(refinedProducts.length / +perPage);
+
+ return page > maxPages ? maxPages : page;
+ };
+
+ const currentPage = getCurrentPage();
+
+ const getVisibleProducts = (p: Product[]) => {
+ if (perPage === 'all') {
+ return p;
+ }
+
+ const startIndex = (currentPage - 1) * +perPage;
+ const endIndex =
+ currentPage * +perPage > p.length ? p.length : currentPage * +perPage;
+
+ return p.slice(startIndex, endIndex);
+ };
+
+ const visibleProducts = getVisibleProducts(refinedProducts);
+
+ if (loading) {
+ return ;
+ }
+
+ return (
+
+
+
+ {error ? (
+ {error}
+ ) : (
+ <>
+
+
{title}
+
+ {refinedProducts.length} models
+
+
+
+
+ {refinedProducts.length ? (
+
+ ) : (
+
+ No matches found
+
+ )}
+
+ updateSearchParams('page', page)}
+ className={styles['products-catalog__pagination']}
+ />
+ >
+ )}
+
+ );
+};
diff --git a/src/modules/ProductsCatalog/components/Pagination/Pagination.module.scss b/src/modules/ProductsCatalog/components/Pagination/Pagination.module.scss
new file mode 100644
index 0000000000..6bf3f0f334
--- /dev/null
+++ b/src/modules/ProductsCatalog/components/Pagination/Pagination.module.scss
@@ -0,0 +1,27 @@
+@import '../../../../styles/utils';
+
+.pagination {
+ display: flex;
+ gap: 16px;
+
+ &__container {
+ display: flex;
+ gap: 8px;
+ }
+
+ &__item {
+ width: 32px;
+ height: 32px;
+
+ &--next {
+ transform: rotate(90deg);
+ }
+ &--prev {
+ transform: rotate(-90deg);
+ }
+ }
+
+ &__dots {
+ align-self: end;
+ }
+}
diff --git a/src/modules/ProductsCatalog/components/Pagination/Pagination.tsx b/src/modules/ProductsCatalog/components/Pagination/Pagination.tsx
new file mode 100644
index 0000000000..ac3ab4e9fb
--- /dev/null
+++ b/src/modules/ProductsCatalog/components/Pagination/Pagination.tsx
@@ -0,0 +1,139 @@
+import React, { useCallback, useMemo } from 'react';
+import cn from 'classnames';
+import styles from './Pagination.module.scss';
+import { RoundButton } from '../../../../components/RoundButton';
+import { SvgIcon } from '../../../../components/SvgIcon';
+import { scrollToTop } from '../../../../utils/utility';
+import { PaginationItem } from '../PaginationItem';
+
+interface Props {
+ perPage: string;
+ currentPage: number;
+ total: number;
+ onPageChange: (page: number) => void;
+ className?: string;
+}
+
+export const Pagination: React.FC = ({
+ perPage,
+ currentPage,
+ total,
+ onPageChange,
+ className,
+}) => {
+ const perPageNumber = perPage === 'all' ? total : Number(perPage);
+
+ const pages = useMemo(
+ () => Array.from({ length: Math.ceil(total / +perPage) }, (_, i) => i + 1),
+ [perPage, total],
+ );
+
+ const getMiddlePages = useCallback(() => {
+ const pagesCount = pages.length;
+
+ if (currentPage <= 2) {
+ return [2, 3, 4];
+ }
+
+ if (currentPage >= pagesCount - 2) {
+ return [pagesCount - 3, pagesCount - 2, pagesCount - 1];
+ }
+
+ return [currentPage - 1, currentPage, currentPage + 1];
+ }, [currentPage, pages.length]);
+
+ const isFirstPage = currentPage === 1;
+ const isLastPage = pages.length === currentPage;
+
+ const onClickPrevPage = () => {
+ onPageChange(Math.max(currentPage - 1, 1));
+ scrollToTop();
+ };
+
+ const onClickNextPage = () => {
+ onPageChange(Math.max(currentPage + 1, 1));
+ scrollToTop();
+ };
+
+ if (total <= perPageNumber) {
+ return null;
+ }
+
+ return (
+
+
+
+
+
+
+
+
+ {pages.length <= 5 ? (
+ pages.map(page => (
+
+ ))
+ ) : (
+ <>
+
+ {currentPage > 3 && (
+
+ ...
+
+ )}
+
+ {getMiddlePages().map(page => {
+ return (
+
+ );
+ })}
+
+ {currentPage < pages.length - 3 && (
+
+ ...
+
+ )}
+
+ >
+ )}
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/src/modules/ProductsCatalog/components/Pagination/index.ts b/src/modules/ProductsCatalog/components/Pagination/index.ts
new file mode 100644
index 0000000000..0a1fd4dad6
--- /dev/null
+++ b/src/modules/ProductsCatalog/components/Pagination/index.ts
@@ -0,0 +1 @@
+export { Pagination } from './Pagination';
diff --git a/src/modules/ProductsCatalog/components/PaginationItem/PaginationItem.module.scss b/src/modules/ProductsCatalog/components/PaginationItem/PaginationItem.module.scss
new file mode 100644
index 0000000000..cc01518b8e
--- /dev/null
+++ b/src/modules/ProductsCatalog/components/PaginationItem/PaginationItem.module.scss
@@ -0,0 +1,15 @@
+@import '../../../../styles/utils';
+
+.pagination-item {
+ display: block;
+
+ &__btn {
+ &--active,
+ &--active:hover {
+ cursor: auto;
+ background-color: var(--primary);
+ color: var(--text-active);
+ border-color: var(--primary);
+ }
+ }
+}
diff --git a/src/modules/ProductsCatalog/components/PaginationItem/PaginationItem.tsx b/src/modules/ProductsCatalog/components/PaginationItem/PaginationItem.tsx
new file mode 100644
index 0000000000..dab37cde9c
--- /dev/null
+++ b/src/modules/ProductsCatalog/components/PaginationItem/PaginationItem.tsx
@@ -0,0 +1,38 @@
+import cn from 'classnames';
+import { RoundButton } from '../../../../components/RoundButton';
+import { scrollToTop } from '../../../../utils/utility';
+import styles from './PaginationItem.module.scss';
+
+interface Props {
+ page: number;
+ onPageChange: (page: number) => void;
+ isCurrent: boolean;
+ className?: string;
+}
+
+export const PaginationItem: React.FC = ({
+ page,
+ onPageChange,
+ isCurrent,
+ className,
+}) => {
+ const onBtnClickHandler = () => {
+ if (!isCurrent) {
+ onPageChange(page);
+ scrollToTop();
+ }
+ };
+
+ return (
+
+
+ {page}
+
+
+ );
+};
diff --git a/src/modules/ProductsCatalog/components/PaginationItem/index.ts b/src/modules/ProductsCatalog/components/PaginationItem/index.ts
new file mode 100644
index 0000000000..c070a870df
--- /dev/null
+++ b/src/modules/ProductsCatalog/components/PaginationItem/index.ts
@@ -0,0 +1 @@
+export { PaginationItem } from './PaginationItem';
diff --git a/src/modules/ProductsCatalog/components/Search/Search.module.scss b/src/modules/ProductsCatalog/components/Search/Search.module.scss
new file mode 100644
index 0000000000..4e0f330036
--- /dev/null
+++ b/src/modules/ProductsCatalog/components/Search/Search.module.scss
@@ -0,0 +1,62 @@
+@import '../../../../styles/utils';
+
+.search {
+ width: 100%;
+
+ &__title {
+ display: block;
+ color: var(--secondary);
+ margin-bottom: 4px;
+
+ @include small-text;
+ }
+
+ &__input-wrapper {
+ position: relative;
+ }
+
+ &__input {
+ width: 100%;
+ height: 40px;
+ border-radius: 8px;
+ border: 1px solid var(--icon-secondary);
+ background-color: var(--bg-secondary);
+ color: var(--primary);
+ padding: 0 30px;
+ background-image: var(--search-img);
+ background-repeat: no-repeat;
+ background-position: 8px center;
+
+ @include transition;
+
+ &:focus {
+ border-color: var(--primary);
+ outline: none;
+ }
+ }
+
+ &__clear {
+ position: absolute;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 0;
+ top: 50%;
+ right: 4px;
+ transform: translateY(-50%);
+ width: 24px;
+ height: 24px;
+ background-color: inherit;
+ border: none;
+
+ @include transition;
+
+ &:hover {
+ opacity: 0.5;
+ }
+
+ svg path {
+ fill: var(--icon-primary);
+ }
+ }
+}
diff --git a/src/modules/ProductsCatalog/components/Search/Search.tsx b/src/modules/ProductsCatalog/components/Search/Search.tsx
new file mode 100644
index 0000000000..49d965ab3a
--- /dev/null
+++ b/src/modules/ProductsCatalog/components/Search/Search.tsx
@@ -0,0 +1,59 @@
+import cn from 'classnames';
+import styles from './Search.module.scss';
+import { useUpdateSearchParams } from '../../../../hooks';
+import { useEffect, useState } from 'react';
+import { SvgIcon } from '../../../../components/SvgIcon';
+
+interface Props {
+ className?: string;
+}
+
+export const Search: React.FC = ({ className }) => {
+ const { updateSearchParams, deleteSearchParam, searchParams } =
+ useUpdateSearchParams();
+ const [value, setValue] = useState(searchParams.get('query') || '');
+
+ useEffect(() => {
+ const timerId = setTimeout(() => {
+ if (value) {
+ updateSearchParams('query', value);
+ } else {
+ deleteSearchParam('query');
+ }
+ }, 500);
+
+ return () => {
+ clearTimeout(timerId);
+ };
+ }, [deleteSearchParam, updateSearchParams, value]);
+
+ const onInputChange = (evt: React.ChangeEvent) => {
+ setValue(evt.target.value);
+ };
+
+ const clear = () => {
+ setValue('');
+ };
+
+ return (
+
+
+
+
+ {value && (
+
+
+
+ )}
+
+
+ );
+};
diff --git a/src/modules/ProductsCatalog/components/Search/index.ts b/src/modules/ProductsCatalog/components/Search/index.ts
new file mode 100644
index 0000000000..6860ea7e14
--- /dev/null
+++ b/src/modules/ProductsCatalog/components/Search/index.ts
@@ -0,0 +1 @@
+export { Search } from './Search';
diff --git a/src/modules/ProductsCatalog/components/Selector/Selector.module.scss b/src/modules/ProductsCatalog/components/Selector/Selector.module.scss
new file mode 100644
index 0000000000..444de27d49
--- /dev/null
+++ b/src/modules/ProductsCatalog/components/Selector/Selector.module.scss
@@ -0,0 +1,95 @@
+@import '../../../../styles/utils';
+
+.selector {
+ &__title {
+ color: var(--secondary);
+ margin-bottom: 4px;
+
+ @include small-text;
+ }
+
+ &__dropdown {
+ position: relative;
+ width: 100%;
+ }
+
+ &__btn {
+ width: 100%;
+ height: 40px;
+ border-radius: 8px;
+ border: 1px solid var(--icon-secondary);
+ padding-inline: 12px;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 10px;
+ background-color: inherit;
+
+ &:hover {
+ border-color: var(--secondary);
+ }
+
+ &:focus {
+ border-color: var(--primary);
+ outline: none;
+ }
+ }
+
+ &__text {
+ font-weight: 700;
+ color: var(--primary);
+ }
+
+ &__icon {
+ width: 16px;
+ height: 16px;
+ transform: rotate(180deg);
+
+ svg {
+ fill: var(--icon-secondary);
+ }
+ }
+
+ &__menu {
+ width: 100%;
+ display: none;
+ position: absolute;
+ z-index: 2;
+ padding-top: 4px;
+
+ &--active {
+ display: block;
+ }
+ }
+
+ &__content {
+ display: flex;
+ flex-direction: column;
+ border-radius: 8px;
+ border: 1px solid var(--elements);
+ background-color: var(--bg-secondary);
+ }
+
+ &__item {
+ text-align: start;
+ line-height: 32px;
+ cursor: pointer;
+ font-weight: 600;
+ color: var(--secondary);
+ padding-inline: 12px;
+ background-color: inherit;
+ border: none;
+
+ &:hover,
+ &:focus,
+ &--active {
+ background-color: var(--hover-bg);
+ border-radius: 8px;
+ color: var(--primary);
+ }
+
+ &--active {
+ cursor: default;
+ }
+ }
+}
diff --git a/src/modules/ProductsCatalog/components/Selector/Selector.tsx b/src/modules/ProductsCatalog/components/Selector/Selector.tsx
new file mode 100644
index 0000000000..cc6d84f9a8
--- /dev/null
+++ b/src/modules/ProductsCatalog/components/Selector/Selector.tsx
@@ -0,0 +1,110 @@
+import { useEffect, useRef, useState } from 'react';
+import cn from 'classnames';
+
+import styles from './Selector.module.scss';
+import { useSearchParams } from 'react-router-dom';
+import { firstLetterCap } from '../../../../utils/utility';
+import { SvgIcon } from '../../../../components/SvgIcon';
+
+interface Props {
+ title: string;
+ type: string;
+ items: { name: string; value: string }[];
+ className?: string;
+}
+
+export const Selector: React.FC = ({
+ title,
+ type,
+ items,
+ className,
+}) => {
+ const [expanded, setExpanded] = useState(false);
+ const [searchParams, setSearchParams] = useSearchParams();
+
+ const dropdownRef = useRef(null);
+
+ const selectedValue =
+ items.find(item => item.value === searchParams.get(type)) || items[0];
+
+ const onChangeValue = (value: string) => {
+ const params = new URLSearchParams(searchParams);
+
+ params.set(type, value);
+ params.set('page', '1');
+ setSearchParams(params);
+ };
+
+ const handleDocumentClick = (evt: MouseEvent) => {
+ if (
+ dropdownRef.current &&
+ !dropdownRef.current.contains(evt.target as Node)
+ ) {
+ setExpanded(false);
+ }
+ };
+
+ useEffect(() => {
+ if (expanded) {
+ document.addEventListener('click', handleDocumentClick);
+ } else {
+ document.removeEventListener('click', handleDocumentClick);
+ }
+
+ return () => {
+ document.removeEventListener('click', handleDocumentClick);
+ };
+ }, [expanded]);
+
+ return (
+
+
{title}
+
+
+ {
+ e.stopPropagation();
+ setExpanded(current => !current);
+ }}
+ >
+
+ {firstLetterCap(selectedValue.name)}
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/src/modules/ProductsCatalog/components/Selector/index.ts b/src/modules/ProductsCatalog/components/Selector/index.ts
new file mode 100644
index 0000000000..90d25f2bf2
--- /dev/null
+++ b/src/modules/ProductsCatalog/components/Selector/index.ts
@@ -0,0 +1 @@
+export { Selector } from './Selector';
diff --git a/src/modules/ProductsCatalog/index.ts b/src/modules/ProductsCatalog/index.ts
new file mode 100644
index 0000000000..be4f0c9e98
--- /dev/null
+++ b/src/modules/ProductsCatalog/index.ts
@@ -0,0 +1 @@
+export { ProductsCatalog } from './ProductsCatalog';
diff --git a/src/modules/TabletsPage/TabletsPage.tsx b/src/modules/TabletsPage/TabletsPage.tsx
new file mode 100644
index 0000000000..d71997f5cd
--- /dev/null
+++ b/src/modules/TabletsPage/TabletsPage.tsx
@@ -0,0 +1,27 @@
+import { useEffect, useState } from 'react';
+import { Product } from '../../types/Product';
+import { getProductsByCategory } from '../../servises/products';
+import { ProductsCatalog } from '../ProductsCatalog';
+
+export const TabletsPage = () => {
+ const [tablets, setTablets] = useState([]);
+ const [loading, setLoading] = useState(false);
+ const [error, setError] = useState('');
+
+ useEffect(() => {
+ setLoading(true);
+ getProductsByCategory('tablets')
+ .then(setTablets)
+ .catch(() => setError('Something went wrong'))
+ .finally(() => setLoading(false));
+ }, []);
+
+ return (
+
+ );
+};
diff --git a/src/modules/TabletsPage/index.ts b/src/modules/TabletsPage/index.ts
new file mode 100644
index 0000000000..5f5d7eb9d6
--- /dev/null
+++ b/src/modules/TabletsPage/index.ts
@@ -0,0 +1 @@
+export { TabletsPage } from './TabletsPage';
diff --git a/src/servises/gadgets.ts b/src/servises/gadgets.ts
new file mode 100644
index 0000000000..acc714573c
--- /dev/null
+++ b/src/servises/gadgets.ts
@@ -0,0 +1,27 @@
+import { Category, Accessory, Phone, Tablet } from '../types';
+import { getData } from '../utils/httpClient';
+
+export const getPhones = getData('/api/phones.json');
+export const getAccessories = getData('/api/accessories.json');
+export const getTablets = getData('/api/tablets.json');
+
+export const getGadgetById = async (category: Category, id: string) => {
+ switch (category) {
+ case 'phones':
+ const phones = await getPhones;
+
+ return phones.find(phone => phone.id === id.toLowerCase()) || null;
+ case 'tablets':
+ const tablets = await getTablets;
+
+ return tablets.find(tablet => tablet.id === id.toLowerCase()) || null;
+ case 'accessories':
+ const accessories = await getAccessories;
+
+ return (
+ accessories.find(accessory => accessory.id === id.toLowerCase()) || null
+ );
+ default:
+ return null;
+ }
+};
diff --git a/src/servises/products.ts b/src/servises/products.ts
new file mode 100644
index 0000000000..e6c1bbfac3
--- /dev/null
+++ b/src/servises/products.ts
@@ -0,0 +1,43 @@
+import { Product } from '../types/Product';
+import { getData } from '../utils/httpClient';
+
+export const getAllProducts = getData('/api/products.json');
+
+export const getProductsByCategory = async (category: string) => {
+ const products = await getAllProducts;
+
+ return products.filter(product => product.category === category);
+};
+
+export const getHotPriceProducts = async () => {
+ const products = await getAllProducts;
+
+ return products.sort(
+ (a, b) => b.fullPrice - b.price - (a.fullPrice - a.price),
+ );
+};
+
+export const getNewProducts = async () => {
+ const products = await getAllProducts;
+ const latestYear = Math.max(...products.map(product => product.year));
+
+ return products
+ .filter(p => p.year === latestYear)
+ .sort((a, b) => b.fullPrice - a.fullPrice);
+};
+
+export const getProductById = async (id: string) => {
+ const products = await getAllProducts;
+
+ return (
+ products.find(p => {
+ return p.itemId.toLowerCase() === id.toLowerCase();
+ }) || null
+ );
+};
+
+export const getSuggestedProducts = async () => {
+ const products = await getAllProducts;
+
+ return products.filter(() => Math.random() > 0.5);
+};
diff --git a/src/styles/_fonts.scss b/src/styles/_fonts.scss
new file mode 100644
index 0000000000..eb01589020
--- /dev/null
+++ b/src/styles/_fonts.scss
@@ -0,0 +1,17 @@
+@font-face {
+ font-family: Mont;
+ src: url('../assets/fonts/Mont-Regular.otf') format('opentype');
+ font-weight: normal;
+}
+
+@font-face {
+ font-family: Mont;
+ src: url('../assets/fonts/Mont-SemiBold.otf') format('opentype');
+ font-weight: 600;
+}
+
+@font-face {
+ font-family: Mont;
+ src: url('../assets/fonts/Mont-Bold.otf') format('opentype');
+ font-weight: bold;
+}
diff --git a/src/styles/_normalize.scss b/src/styles/_normalize.scss
new file mode 100644
index 0000000000..2d553f3cfd
--- /dev/null
+++ b/src/styles/_normalize.scss
@@ -0,0 +1,516 @@
+// =============================================================================
+// Normalize.scss based on Nicolas Gallagher and Jonathan Neal's
+// normalize.css v2.1.3 | MIT License | git.io/normalize
+// =============================================================================
+
+// =============================================================================
+// Normalize.scss settings
+// =============================================================================
+
+// Set to true if you want to add support for IE6 and IE7
+// Notice: setting to true might render some elements
+// slightly differently than when set to false
+$legacy_support_for_ie: false !default; // Used also in Compass
+
+// Set the default font family here so you don't have to override it later
+$normalized_font_family: sans-serif !default;
+$normalize_headings: true !default;
+$h1_font_size: 2em !default;
+$h2_font_size: 1.5em !default;
+$h3_font_size: 1.17em !default;
+$h4_font_size: 1em !default;
+$h5_font_size: 0.83em !default;
+$h6_font_size: 0.75em !default;
+$h1_margin: 0.67em 0 !default;
+$h2_margin: 0.83em 0 !default;
+$h3_margin: 1em 0 !default;
+$h4_margin: 1.33em 0 !default;
+$h5_margin: 1.67em 0 !default;
+$h6_margin: 2.33em 0 !default;
+$background: #fff !default;
+$color: #000 !default;
+
+// =============================================================================
+// HTML5 display definitions
+// =============================================================================
+
+// Corrects block display not defined in IE6/7/8/9 & FF3
+
+article,
+aside,
+details,
+figcaption,
+figure,
+footer,
+header,
+hgroup,
+nav,
+section,
+summary {
+ display: block;
+}
+
+// Corrects inline-block display not defined in IE6/7/8/9 & FF3
+
+audio,
+canvas,
+video {
+ display: inline-block;
+
+ @if $legacy_support_for_ie {
+ *display: inline;
+ *zoom: 1;
+ }
+}
+
+// 1. Prevents modern browsers from displaying 'audio' without controls
+// 2. Remove excess height in iOS5 devices
+
+audio:not([controls]) {
+ display: none; // 1
+ height: 0; // 2
+}
+
+// Address `[hidden]` styling not present in IE 8/9.
+// Hide the `template` element in IE, Safari, and Firefox < 22.
+
+[hidden],
+template {
+ display: none;
+}
+
+// =============================================================================
+// Base
+// =============================================================================
+
+// 1. Corrects text resizing oddly in IE6/7 when body font-size is set using em units
+// http://clagnut.com/blog/348/#c790
+// 2. Prevents iOS text size adjust after orientation change, without disabling user zoom
+// www.456bereastreet.com/archive/201012/controlling_text_size_in_safari_for_ios_without_disabling_user_zoom/
+
+html {
+ @if $legacy_support_for_ie {
+ font-size: 100%; // 1
+ }
+
+ background: $background;
+ color: $color;
+ -webkit-text-size-adjust: 100%; // 2
+ -ms-text-size-adjust: 100%; // 2
+}
+
+// Addresses font-family inconsistency between 'textarea' and other form elements.
+
+html,
+button,
+input,
+select,
+textarea {
+ font-family: $normalized_font_family;
+}
+
+// Addresses margins handled incorrectly in IE6/7
+
+body {
+ margin: 0;
+}
+
+// =============================================================================
+// Links
+// =============================================================================
+
+// 1. Remove the gray background color from active links in IE 10.
+// 2. Addresses outline displayed oddly in Chrome
+// 3. Improves readability when focused and also mouse hovered in all browsers
+// people.opera.com/patrickl/experiments/keyboard/test
+
+a {
+ // 1
+
+ background: transparent;
+
+ // 2
+
+ &:focus {
+ outline: thin dotted;
+ }
+
+ // 3
+
+ &:hover,
+ &:active {
+ outline: 0;
+ }
+}
+
+// =============================================================================
+// Typography
+// =============================================================================
+
+// Addresses font sizes and margins set differently in IE6/7
+// Addresses font sizes within 'section' and 'article' in FF4+, Chrome, S5
+
+@if $normalize_headings == true {
+ h1 {
+ font-size: $h1_font_size;
+ margin: $h1_margin;
+ }
+
+ h2 {
+ font-size: $h2_font_size;
+ margin: $h2_margin;
+ }
+
+ h3 {
+ font-size: $h3_font_size;
+ margin: $h3_margin;
+ }
+
+ h4 {
+ font-size: $h4_font_size;
+ margin: $h4_margin;
+ }
+
+ h5 {
+ font-size: $h5_font_size;
+ margin: $h5_margin;
+ }
+
+ h6 {
+ font-size: $h6_font_size;
+ margin: $h6_margin;
+ }
+}
+
+// Addresses styling not present in IE 8/9, S5, Chrome
+
+abbr[title] {
+ border-bottom: 1px dotted;
+}
+
+// Addresses style set to 'bolder' in FF3+, S4/5, Chrome
+
+b,
+strong {
+ font-weight: bold;
+}
+
+@if $legacy_support_for_ie {
+ blockquote {
+ margin: 1em 40px;
+ }
+}
+
+// Addresses styling not present in S5, Chrome
+
+dfn {
+ font-style: italic;
+}
+
+// Addresses styling not present in IE6/7/8/9
+
+mark {
+ background: #ff0;
+ color: #000;
+}
+
+// Addresses margins set differently in IE6/7
+@if $legacy_support_for_ie {
+ p,
+ pre {
+ margin: 1em 0;
+ }
+}
+
+// Corrects font family set oddly in IE6, S4/5, Chrome
+// en.wikipedia.org/wiki/User:Davidgothberg/Test59
+
+code,
+kbd,
+pre,
+samp {
+ font-family: monospace, serif;
+
+ @if $legacy_support_for_ie {
+ _font-family: 'courier new', monospace;
+ }
+
+ font-size: 1em;
+}
+
+// Improves readability of pre-formatted text in all browsers
+pre {
+ white-space: pre;
+ white-space: pre-wrap;
+ word-wrap: break-word;
+}
+
+// Set consistent quote types.
+
+q {
+ quotes: '\201C' '\201D' '\2018' '\2019';
+
+ &::before,
+ &::after {
+ content: '';
+ content: none;
+ }
+}
+
+// 1. Addresses CSS quotes not supported in IE6/7
+@if $legacy_support_for_ie {
+ q {
+ quotes: none;
+ }
+}
+
+// Address inconsistent and variable font size in all browsers.
+
+small {
+ font-size: 80%;
+}
+
+// Prevents sub and sup affecting line-height in all browsers
+// gist.github.com/413930
+
+sub,
+sup {
+ font-size: 75%;
+ line-height: 0;
+ position: relative;
+ vertical-align: baseline;
+}
+
+sup {
+ top: -0.5em;
+}
+
+sub {
+ bottom: -0.25em;
+}
+
+// =============================================================================
+// Lists
+// =============================================================================
+
+// Addresses margins set differently in IE6/7
+@if $legacy_support_for_ie {
+ dl,
+ menu,
+ ol,
+ ul {
+ margin: 1em 0;
+ }
+}
+
+@if $legacy_support_for_ie {
+ dd {
+ margin: 0 0 0 40px;
+ }
+}
+
+// Addresses paddings set differently in IE6/7
+@if $legacy_support_for_ie {
+ menu,
+ ol,
+ ul {
+ padding: 0 0 0 40px;
+ }
+}
+
+// Corrects list images handled incorrectly in IE7
+
+nav {
+ ul,
+ ol {
+ @if $legacy_support_for_ie {
+ list-style-image: none;
+ }
+ }
+}
+
+// =============================================================================
+// Embedded content
+// =============================================================================
+
+// 1. Removes border when inside 'a' element in IE6/7/8/9, FF3
+// 2. Improves image quality when scaled in IE7
+// code.flickr.com/blog/2008/11/12/on-ui-quality-the-little-things-client-side-image-resizing/
+
+img {
+ border: 0; // 1
+
+ @if $legacy_support_for_ie {
+ -ms-interpolation-mode: bicubic; // 2
+ }
+}
+
+// Corrects overflow displayed oddly in IE9
+
+svg:not(:root) {
+ overflow: hidden;
+}
+
+// =============================================================================
+// Figures
+// =============================================================================
+
+// Addresses margin not present in IE6/7/8/9, S5, O11
+
+figure {
+ margin: 0;
+}
+
+// =============================================================================
+// Forms
+// =============================================================================
+
+// Corrects margin displayed oddly in IE6/7
+@if $legacy_support_for_ie {
+ form {
+ margin: 0;
+ }
+}
+
+// Define consistent border, margin, and padding
+
+fieldset {
+ border: 1px solid #c0c0c0;
+ margin: 0 2px;
+ padding: 0.35em 0.63em 0.75em;
+}
+
+// 1. Corrects color not being inherited in IE6/7/8/9
+// 2. Remove padding so people aren't caught out if they zero out fieldsets.
+// 3. Corrects text not wrapping in FF3
+// 4. Corrects alignment displayed oddly in IE6/7
+
+legend {
+ border: 0; // 1
+ padding: 0; // 2
+ white-space: normal; // 3
+
+ @if $legacy_support_for_ie {
+ *margin-left: -7px; // 4
+ }
+}
+
+// 1. Correct font family not being inherited in all browsers.
+// 2. Corrects font size not being inherited in all browsers
+// 3. Addresses margins set differently in IE6/7, FF3+, S5, Chrome
+// 4. Improves appearance and consistency in all browsers
+
+button,
+input,
+select,
+textarea {
+ font-family: inherit; // 1
+ font-size: 100%; // 2
+ margin: 0; // 3
+ vertical-align: baseline; // 4
+
+ @if $legacy_support_for_ie {
+ *vertical-align: middle; // 4
+ }
+}
+
+// Addresses FF3/4 setting line-height on 'input' using !important in the UA stylesheet
+
+button,
+input {
+ line-height: normal;
+
+ &::-moz-focus-inner {
+ border: 0;
+ padding: 0;
+ }
+}
+
+// Address inconsistent `text-transform` inheritance for `button` and `select`.
+// All other form control elements do not inherit `text-transform` values.
+// Correct `button` style inheritance in Chrome, Safari 5+, and IE 8+.
+// Correct `select` style inheritance in Firefox 4+ and Opera.
+
+button,
+select {
+ text-transform: none;
+}
+
+// 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio`
+// and `video` controls
+// 2. Corrects inability to style clickable 'input' types in iOS
+// 3. Improves usability and consistency of cursor style between image-type
+// 'input' and others
+// 4. Removes inner spacing in IE7 without affecting normal text inputs
+// Known issue: inner spacing remains in IE6
+
+button,
+html input[type="button"], // 1
+input[type="reset"],
+input[type="submit"] {
+ -webkit-appearance: button; // 2
+ cursor: pointer; // 3
+
+ @if $legacy_support_for_ie {
+ *overflow: visible; // 4
+ }
+}
+
+// Re-set default cursor for disabled elements
+
+button[disabled],
+input[disabled] {
+ cursor: default;
+}
+
+// 1. Removes default vertical scrollbar in IE6/7/8/9
+// 2. Improves readability and alignment in all browsers
+
+textarea {
+ overflow: auto; // 1
+ vertical-align: top; // 2
+}
+
+// =============================================================================
+// Tables
+// =============================================================================
+
+// Remove most spacing between table cells
+
+table {
+ border-collapse: collapse;
+ border-spacing: 0;
+}
+
+input {
+ // 1. Addresses appearance set to searchfield in S5, Chrome
+ // 2. Addresses box-sizing set to border-box in S5, Chrome (include -moz to future-proof)
+ &[type='search'] {
+ -webkit-appearance: textfield; // 1
+ -moz-box-sizing: content-box;
+ -webkit-box-sizing: content-box; // 2
+ box-sizing: content-box;
+
+ // Remove inner padding and search cancel button in Safari 5 and Chrome
+ // on OS X.
+ &::-webkit-search-cancel-button,
+ &::-webkit-search-decoration {
+ -webkit-appearance: none;
+ }
+ }
+
+ // 1. Address box sizing set to `content-box` in IE 8/9/10.
+ // 2. Remove excess padding in IE 8/9/10.
+ // 3. Removes excess padding in IE7
+ // Known issue: excess padding remains in IE6
+ &[type='checkbox'],
+ &[type='radio'] {
+ box-sizing: border-box; // 1
+ padding: 0; // 2
+
+ @if $legacy_support_for_ie {
+ *height: 13px; // 3
+ *width: 13px; // 3
+ }
+ }
+}
diff --git a/src/styles/_typography.scss b/src/styles/_typography.scss
new file mode 100644
index 0000000000..115fc310dd
--- /dev/null
+++ b/src/styles/_typography.scss
@@ -0,0 +1,75 @@
+@mixin small-text {
+ font-size: 12px;
+ font-weight: 700;
+ line-height: 15.34px;
+}
+
+@mixin body-text {
+ font-size: 14px;
+ font-weight: 600;
+ line-height: 21px;
+}
+
+@mixin buttons {
+ font-size: 14px;
+ font-weight: 700;
+ line-height: 21px;
+}
+
+@mixin uppercase {
+ font-size: 12px;
+ font-weight: 800;
+ line-height: 11px;
+ letter-spacing: 0.04em;
+}
+
+@mixin h1-mobile {
+ font-size: 32px;
+ font-weight: 800;
+ line-height: 41px;
+ letter-spacing: -0.01em;
+}
+
+@mixin h2-mobile {
+ font-size: 22px;
+ font-weight: 800;
+ line-height: 30.8px;
+}
+
+@mixin h3-mobile {
+ font-size: 20px;
+ font-weight: 700;
+ line-height: 25.56px;
+}
+
+@mixin h4-mobile {
+ font-size: 16px;
+ font-weight: 700;
+ line-height: 20.45px;
+}
+
+@mixin h1-tablet-desktop {
+ font-size: 48px;
+ font-weight: 800;
+ line-height: 56px;
+ letter-spacing: -0.01em;
+}
+
+@mixin h2-tablet-desktop {
+ font-size: 32px;
+ font-weight: 800;
+ line-height: 41px;
+ letter-spacing: -0.01em;
+}
+
+@mixin h3-tablet-desktop {
+ font-size: 22px;
+ font-weight: 800;
+ line-height: 30.8px;
+}
+
+@mixin h4-tablet-desktop {
+ font-size: 20px;
+ font-weight: 700;
+ line-height: 25.56px;
+}
diff --git a/src/styles/_utility.scss b/src/styles/_utility.scss
new file mode 100644
index 0000000000..a2715cdbc2
--- /dev/null
+++ b/src/styles/_utility.scss
@@ -0,0 +1,13 @@
+.visually-hidden {
+ 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/styles/globals.scss b/src/styles/globals.scss
new file mode 100644
index 0000000000..f5bddf30fc
--- /dev/null
+++ b/src/styles/globals.scss
@@ -0,0 +1,30 @@
+@import './theme/dark';
+@import './theme/light';
+@import './normalize';
+@import './fonts';
+@import './utility';
+
+*,
+*::before,
+*::after {
+ box-sizing: border-box;
+}
+
+body {
+ margin: 0;
+}
+
+ul {
+ margin: 0;
+ padding: 0;
+ list-style: none;
+}
+
+p {
+ margin: 0;
+}
+
+a {
+ text-decoration: none;
+ color: inherit;
+}
diff --git a/src/styles/theme/dark.scss b/src/styles/theme/dark.scss
new file mode 100644
index 0000000000..ca725c40e1
--- /dev/null
+++ b/src/styles/theme/dark.scss
@@ -0,0 +1,24 @@
+@import '../utils';
+
+body.dark-theme {
+ --green: #{$green};
+ --red: #{$red};
+ --primary: #{$dark-white};
+ --secondary: #{$dark-secondary};
+ --white: #{$dark-white};
+ --elements: #{$dark-elements};
+ --hover-bg: #{$dark-black};
+ --accent: #{$dark-accent};
+ --secondary-accent: #{$red};
+ --text-footer: #{$dark-white};
+ --bg-header-footer: #{$dark-black};
+ --link: #{$dark-secondary};
+ --active-link: #{$dark-white};
+ --icon-primary: #{$dark-white};
+ --icon-secondary: #{$dark-icons};
+ --round-btn-hover: #{$dark-icons};
+ --round-btn-hover-border: #{$dark-icons};
+ --bg-secondary: #{$dark-surface-1};
+ --text-active: #{$dark-black};
+ --search-img: url('../../assets/images/icons/search-dark.svg');
+}
diff --git a/src/styles/theme/light.scss b/src/styles/theme/light.scss
new file mode 100644
index 0000000000..e5e8396118
--- /dev/null
+++ b/src/styles/theme/light.scss
@@ -0,0 +1,24 @@
+@import '../utils';
+
+body.light-theme {
+ --green: #{$green};
+ --red: #{$red};
+ --primary: #{$light-primary};
+ --secondary: #{$light-secondary};
+ --white: #{$light-white};
+ --elements: #{$light-elements};
+ --hover-bg: #{$light-hover-bg};
+ --accent: #{$light-accent};
+ --secondary-accent: #{$light-secondary-accent};
+ --text-footer: #{$light-secondary};
+ --bg-header-footer: #{$light-white};
+ --link: #{$light-secondary};
+ --active-link: #{$light-primary};
+ --icon-primary: #{$light-primary};
+ --icon-secondary: #{$light-icons};
+ --round-btn-hover: #{$light-hover-bg};
+ --round-btn-hover-border: #{$light-primary};
+ --bg-secondary: #{$light-white};
+ --text-active: #{$light-white};
+ --search-img: url('../../assets/images/icons/search-light.svg');
+}
diff --git a/src/styles/utils.scss b/src/styles/utils.scss
new file mode 100644
index 0000000000..bb90f70234
--- /dev/null
+++ b/src/styles/utils.scss
@@ -0,0 +1,3 @@
+@import './utils/variables';
+@import './utils/mixins';
+@import './typography';
diff --git a/src/styles/utils/_mixins.scss b/src/styles/utils/_mixins.scss
new file mode 100644
index 0000000000..d705b7fd6a
--- /dev/null
+++ b/src/styles/utils/_mixins.scss
@@ -0,0 +1,33 @@
+@mixin on-tablet {
+ @media (min-width: $tablet-width) {
+ @content;
+ }
+}
+
+@mixin on-desktop {
+ @media (min-width: $desktop-width) {
+ @content;
+ }
+}
+
+@mixin only-mobile {
+ @media (max-width: calc($tablet-width - 1px)) {
+ @content;
+ }
+}
+
+@mixin page-padding-inline {
+ padding-inline: $mobile-padding-inline;
+
+ @include on-tablet {
+ padding-inline: $tablet-padding-inline;
+ }
+
+ @include on-desktop {
+ padding-inline: $desktop-padding-inline;
+ }
+}
+
+@mixin transition {
+ transition: 0.4s;
+}
diff --git a/src/styles/utils/_variables.scss b/src/styles/utils/_variables.scss
new file mode 100644
index 0000000000..002ce6f880
--- /dev/null
+++ b/src/styles/utils/_variables.scss
@@ -0,0 +1,34 @@
+// viewports
+$mobile-width: 320px;
+$tablet-width: 640px;
+$desktop-width: 1200px;
+
+// inline paddings
+$mobile-padding-inline: 16px;
+$tablet-padding-inline: 24px;
+$desktop-padding-inline: 32px;
+
+// colors
+$green: #27ae60;
+$red: #eb5757;
+
+// colors light theme
+$light-accent: #f86800;
+$light-secondary-accent: #476df4;
+$light-primary: #0f0f11;
+$light-secondary: #89939a;
+$light-icons: #89939a;
+$light-elements: #e2e6e9;
+$light-hover-bg: #fafbfc;
+$light-white: #fff;
+
+// colors dark theme
+$dark-secondary: #75767f;
+$dark-icons: #75767f;
+$dark-elements: #3b3e4a;
+$dark-surface-1: #161827;
+$dark-surface-2: #323542;
+$dark-black: #0f1121;
+$dark-white: #f1f2f9;
+$dark-accent: #905bff;
+
diff --git a/src/types/Category.ts b/src/types/Category.ts
new file mode 100644
index 0000000000..45f85d6f95
--- /dev/null
+++ b/src/types/Category.ts
@@ -0,0 +1 @@
+export type Category = 'phones' | 'tablets' | 'accessories';
diff --git a/src/types/Gadget.ts b/src/types/Gadget.ts
new file mode 100644
index 0000000000..5363de5d3c
--- /dev/null
+++ b/src/types/Gadget.ts
@@ -0,0 +1,35 @@
+export interface Description {
+ title: string;
+ text: string[];
+}
+
+export interface BaseGadget {
+ id: string;
+ category: string;
+ namespaceId: string;
+ name: string;
+ capacityAvailable: string[];
+ capacity: string;
+ priceRegular: number;
+ priceDiscount: number;
+ colorsAvailable: string[];
+ color: string;
+ images: string[];
+ description: Description[];
+ screen: string;
+ resolution: string;
+ processor: string;
+ ram: string;
+ cell: string[];
+}
+
+export type Accessory = BaseGadget;
+
+export interface Phone extends BaseGadget {
+ camera: string;
+ zoom: string;
+}
+
+export type Tablet = Phone;
+
+export type GadgetType = Phone | Tablet | Accessory;
diff --git a/src/types/Product.ts b/src/types/Product.ts
new file mode 100644
index 0000000000..26f15f406f
--- /dev/null
+++ b/src/types/Product.ts
@@ -0,0 +1,22 @@
+import { Category } from './Category';
+
+export interface Product {
+ id: number;
+ category: Category;
+ itemId: string;
+ name: string;
+ capacity: string;
+ fullPrice: number;
+ price: number;
+ color: string;
+ image: string;
+ screen: string;
+ ram: string;
+ year: number;
+}
+
+export interface CartProduct {
+ quantity: number;
+ id: string;
+ product: Product;
+}
diff --git a/src/types/index.ts b/src/types/index.ts
new file mode 100644
index 0000000000..dc2efb6276
--- /dev/null
+++ b/src/types/index.ts
@@ -0,0 +1,3 @@
+export type { Category } from './Category';
+export type { Product, CartProduct } from './Product';
+export type { Phone, Tablet, Accessory, GadgetType } from './Gadget';
diff --git a/src/utils/constants.ts b/src/utils/constants.ts
new file mode 100644
index 0000000000..04f39ab821
--- /dev/null
+++ b/src/utils/constants.ts
@@ -0,0 +1,14 @@
+export const MainNavigation = {
+ HOME: '/',
+ PHONES: '/phones',
+ TABLETS: '/tablets',
+ ACCESSORIES: '/accessories',
+ FAVOURITES: '/favourites',
+ CART: '/cart',
+ NOT_FOUND: '*',
+};
+
+export enum Theme {
+ LIGTH = 'light-theme',
+ DARK = 'dark-theme',
+}
diff --git a/src/utils/httpClient.ts b/src/utils/httpClient.ts
new file mode 100644
index 0000000000..3ef361e785
--- /dev/null
+++ b/src/utils/httpClient.ts
@@ -0,0 +1,7 @@
+const BASE_URL = `https://andriy-kostiuk.github.io/react_phone-catalog/`;
+
+export const getData = async (url: string): Promise => {
+ const responce = await fetch(BASE_URL + url);
+
+ return responce.json();
+};
diff --git a/src/utils/utility.ts b/src/utils/utility.ts
new file mode 100644
index 0000000000..6755faafbb
--- /dev/null
+++ b/src/utils/utility.ts
@@ -0,0 +1,18 @@
+export const firstLetterCap = (word: string) => {
+ return word[0].toUpperCase() + word.slice(1);
+};
+
+export const scrollToTop = () => {
+ window.scrollTo({
+ top: 0,
+ behavior: 'smooth',
+ });
+};
+
+export const disableScroll = () => {
+ document.body.style.overflow = 'hidden';
+};
+
+export const enableScroll = () => {
+ document.body.style.overflow = '';
+};