diff --git a/src/App.scss b/src/App.scss
index 71bc413aad..5294541bf7 100644
--- a/src/App.scss
+++ b/src/App.scss
@@ -1 +1,29 @@
-// not empty
+html {
+ scroll-behavior: smooth;
+}
+
+p,
+h1,
+h2,
+h3,
+h4,
+ul,
+button,
+span,
+body {
+ margin: 0;
+ padding: 0;
+}
+
+body {
+ box-sizing: border-box;
+ font-family: "Mont", arial, helvetica, sans-serif;
+ font-weight: 500;
+ font-size: 14px;
+ line-height: 21px;
+}
+
+.container {
+ max-width: 1136px;
+ margin: 0 auto;
+}
diff --git a/src/App.tsx b/src/App.tsx
index a1715e52b3..3f503cf72d 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -1,8 +1,19 @@
+import { Outlet } from 'react-router-dom';
import './App.scss';
+import { Footer } from './components/Footer/Footer';
+import { Header } from './components/Header/Header';
const App = () => (
-
React Phone Catalog
+
+
+
+
+
+
+
+
+
);
diff --git a/src/api/CategoriesList.json b/src/api/CategoriesList.json
new file mode 100644
index 0000000000..9b3f0f1c09
--- /dev/null
+++ b/src/api/CategoriesList.json
@@ -0,0 +1,23 @@
+[
+ {
+ "title": "Mobile phones",
+ "image": "img/categories/phones.jpg",
+ "type": "phones",
+ "itemType": "phone",
+ "url": "/phones"
+ },
+ {
+ "title": "Tablets",
+ "image": "img/categories/tablets.jpg",
+ "type": "tablets",
+ "itemType": "tablet",
+ "url": "/tablets"
+ },
+ {
+ "title": "Accessories",
+ "image": "img/categories/accessories.jpg",
+ "type": "accessories",
+ "itemType": "accessory",
+ "url": "/accessories"
+ }
+]
diff --git a/src/api/Products.ts b/src/api/Products.ts
new file mode 100644
index 0000000000..3f64583c3c
--- /dev/null
+++ b/src/api/Products.ts
@@ -0,0 +1,11 @@
+import { Product } from '../types/Product';
+import { ProductDetails } from '../types/ProductDetails';
+import { client } from '../utils/fetchClient';
+
+export const getProducts = () => {
+ return client.get('/products.json');
+};
+
+export const getProductDetails = (productId: string) => {
+ return client.get(`/products/${productId}.json`);
+};
diff --git a/src/api/navFooter.json b/src/api/navFooter.json
new file mode 100644
index 0000000000..7c0deaf4ea
--- /dev/null
+++ b/src/api/navFooter.json
@@ -0,0 +1,14 @@
+[
+ {
+ "title": "Github",
+ "path": "https://github.com/NikaNika12"
+ },
+ {
+ "title": "Contacts",
+ "path": "https://github.com/NikaNika12"
+ },
+ {
+ "title": "Rights",
+ "path": "https://github.com/NikaNika12"
+ }
+]
diff --git a/src/api/navHeader.json b/src/api/navHeader.json
new file mode 100644
index 0000000000..a1f5e17190
--- /dev/null
+++ b/src/api/navHeader.json
@@ -0,0 +1,18 @@
+[
+ {
+ "title": "Home",
+ "path": "/"
+ },
+ {
+ "title": "Phones",
+ "path": "/phones"
+ },
+ {
+ "title": "Tablets",
+ "path": "/tablets"
+ },
+ {
+ "title": "Accessories",
+ "path": "/accessories"
+ }
+]
diff --git a/src/components/AddToCartButton/AddToCartButton.scss b/src/components/AddToCartButton/AddToCartButton.scss
new file mode 100644
index 0000000000..0131884428
--- /dev/null
+++ b/src/components/AddToCartButton/AddToCartButton.scss
@@ -0,0 +1,38 @@
+@import "../../main.scss";
+
+.button {
+ width: 176px;
+ height: 40px;
+
+ @extend %buttons-text;
+
+ color: $color-white;
+ background-color: $color-primary;
+ border: none;
+ outline: none;
+
+ transition: box-shadow 0.5s;
+ cursor: pointer;
+
+ &--small {
+ width: 176px;
+ height: 40px;
+ }
+
+ &--big {
+ width: 100%;
+ height: 48px;
+ }
+
+ &--selected {
+ color: $color-green;
+ background: none;
+ border: 1px solid $color-elements;
+ }
+
+ &:hover {
+ color: $color-white;
+ background-color: $color-primary;
+ box-shadow: 0 3px 13px rgba(23, 32, 49, 0.26);
+ }
+}
diff --git a/src/components/AddToCartButton/AddToCartButton.tsx b/src/components/AddToCartButton/AddToCartButton.tsx
new file mode 100644
index 0000000000..47e6437e03
--- /dev/null
+++ b/src/components/AddToCartButton/AddToCartButton.tsx
@@ -0,0 +1,34 @@
+import classNames from 'classnames';
+import { useContext } from 'react';
+import { Product } from '../../types/Product';
+import { Context } from '../../context/Context';
+import './AddToCartButton.scss';
+import { ButtonType } from '../../types/ButtonType';
+
+type Props = {
+ product: Product;
+ size: ButtonType;
+};
+
+export const AddToCartButton: React.FC = ({ product, size }) => {
+ const { changeCart, cart } = useContext(Context);
+
+ const isInCart = cart.length > 0
+ ? cart.find(item => item.id === product?.id) : false;
+
+ return (
+
+ );
+};
diff --git a/src/components/BackButton/BackButton.scss b/src/components/BackButton/BackButton.scss
new file mode 100644
index 0000000000..979d2e66dd
--- /dev/null
+++ b/src/components/BackButton/BackButton.scss
@@ -0,0 +1,21 @@
+@import "../../main.scss";
+
+.backButton {
+ display: flex;
+ align-items: center;
+ margin-bottom: 24px;
+ padding: 10px 20px;
+ width: 50px;
+ height: 30px;
+ background-color: $color-white;
+ background-image: url("../../images/ArrowLeft.svg");
+ background-position: left;
+ background-repeat: no-repeat;
+ border: 0;
+ color: $color-secondary;
+ cursor: pointer;
+
+ &:hover {
+ color: $color-primary;
+ }
+}
diff --git a/src/components/BackButton/BackButton.tsx b/src/components/BackButton/BackButton.tsx
new file mode 100644
index 0000000000..b26fef7bb3
--- /dev/null
+++ b/src/components/BackButton/BackButton.tsx
@@ -0,0 +1,21 @@
+import { useNavigate } from 'react-router-dom';
+import './BackButton.scss';
+
+export const BackButton = () => {
+ const navigate = useNavigate();
+
+ return (
+
+ );
+};
diff --git a/src/components/BannerSlider/BannerSlider.scss b/src/components/BannerSlider/BannerSlider.scss
new file mode 100644
index 0000000000..531643ca78
--- /dev/null
+++ b/src/components/BannerSlider/BannerSlider.scss
@@ -0,0 +1,70 @@
+@import "../../main.scss";
+
+.banner {
+ &__container {
+ display: flex;
+ gap: 16px;
+ margin-bottom: 8px;
+ }
+
+ &__images {
+ display: flex;
+ height: 400px;
+ order: 2;
+ }
+
+ &__image {
+ opacity: 0;
+ height: 0;
+ width: 0;
+ transition: opacity 0.3s;
+
+ &--active {
+ opacity: 1;
+ height: 100%;
+ width: 100%;
+ }
+ }
+
+ &__button {
+ flex: 0 0 32px;
+ border: 1px solid $color-icons;
+ background-color: $color-white;
+ transition: all 0.3s ease;
+
+ &:hover {
+ border: 1px solid $color-primary;
+ }
+
+ &--prev {
+ order: 1;
+ }
+
+ &--next {
+ order: 3;
+ }
+ }
+
+ &__pagination-container {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ gap: 15px;
+ height: 24px;
+ margin-bottom: 72px;
+ }
+
+ &__button-pg {
+ border: 1px solid transparent;
+ transition: all 0.3s;
+ background-color: $color-elements;
+
+ &:hover {
+ border: 1px solid $color-primary;
+ }
+
+ &--active {
+ background-color: $color-primary;
+ }
+ }
+}
diff --git a/src/components/BannerSlider/BannerSlider.tsx b/src/components/BannerSlider/BannerSlider.tsx
new file mode 100644
index 0000000000..236420515c
--- /dev/null
+++ b/src/components/BannerSlider/BannerSlider.tsx
@@ -0,0 +1,78 @@
+import { useEffect, useState } from 'react';
+import classNames from 'classnames';
+import './BannerSlider.scss';
+
+const BannerImages = [
+ 'img/banner1.jpg',
+ 'img/banner2.jpg',
+ 'img/banner3.jpg',
+];
+
+export const BannerSlider: React.FC = () => {
+ const [currentIndex, setCurrentIndex] = useState(0);
+
+ useEffect(() => {
+ const interval = setInterval(
+ () => setCurrentIndex((prev) => prev + 1),
+ 5000,
+ );
+
+ return () => clearInterval(interval);
+ }, [currentIndex]);
+
+ return (
+
+
+
+
+ {BannerImages.map((image, index) => {
+ if (currentIndex > BannerImages.length - 1) {
+ setCurrentIndex(0);
+ }
+
+ if (currentIndex < 0) {
+ setCurrentIndex(BannerImages.length - 1);
+ }
+
+ return (
+
+ );
+ })}
+
+
+
+
+ {BannerImages.map((image, index) => (
+
+
+ );
+};
diff --git a/src/components/CartItem/CartItem.scss b/src/components/CartItem/CartItem.scss
new file mode 100644
index 0000000000..ddc37926ec
--- /dev/null
+++ b/src/components/CartItem/CartItem.scss
@@ -0,0 +1,121 @@
+@import "../../main.scss";
+
+.cart {
+ &__empty-title {
+ display: flex;
+ justify-content: center;
+ margin-bottom: 40px;
+ padding-top: 16px;
+ color: $color-primary;
+ }
+
+ &__list {
+ display: flex;
+ flex-direction: column;
+ }
+
+ &__item {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 16px;
+ border: 1px solid $color-elements;
+ transition: all 0.5s;
+ padding: 24px;
+
+ &:last-child {
+ margin-bottom: 0;
+ }
+
+ &:hover {
+ box-shadow: 0 3px 13px rgba(23, 32, 49, 0.26);
+ }
+
+ &__icon {
+ background-color: $color-white;
+ border: none;
+ transition: all 0.5s;
+ cursor: pointer;
+
+ &--close {
+ display: block;
+ background-position: center;
+ background-repeat: no-repeat;
+ @include squareSize(16px);
+ border: 1px solid $color-icons;
+ background-image: url(../../images/Close.svg);
+ }
+ }
+ }
+
+ &__image {
+ height: 100%;
+ &--container {
+ @include squareSize(80px);
+ padding: 7px;
+ }
+ }
+
+ &__item-title {
+ width: 295px;
+ }
+
+ &__buttons {
+ display: flex;
+ align-items: center;
+
+ &__icon {
+ border: 1px solid $color-icons;
+ transition: all 0.5s;
+ background-color: $color-white;
+ cursor: pointer;
+
+ &:hover {
+ border-color: $color-icons;
+ }
+
+ &:hover:disabled {
+ border-color: $color-elements;
+ }
+
+ &--substract {
+ display: block;
+ background-position: center;
+ background-repeat: no-repeat;
+ @include squareSize(32px);
+ border: 1px solid $color-icons;
+ background-image: url(../../images/Minus.svg);
+ }
+
+ &--add {
+ display: block;
+ background-position: center;
+ background-repeat: no-repeat;
+ @include squareSize(32px);
+ border: 1px solid $color-icons;
+ background-image: url(../../images/Plus.svg);
+ }
+ }
+ }
+
+ &__quantity {
+ min-width: 32px;
+ width: 100%;
+ text-align: center;
+ color: #000;
+ }
+
+ &__price {
+ min-width: 60px;
+ @extend %h2-text;
+ color: $color-primary;
+ }
+
+ &__empty-phrase {
+ width: 100%;
+ margin-top: 30px;
+ @extend %h1-text;
+ text-align: center;
+ color: $color-primary;
+ }
+}
diff --git a/src/components/CartItem/CartItem.tsx b/src/components/CartItem/CartItem.tsx
new file mode 100644
index 0000000000..d068a8a96a
--- /dev/null
+++ b/src/components/CartItem/CartItem.tsx
@@ -0,0 +1,90 @@
+import { useContext } from 'react';
+import { Context } from '../../context/Context';
+import { Product } from '../../types/Product';
+import './CartItem.scss';
+
+export const CartItem: React.FC = (product) => {
+ const {
+ imageUrl,
+ name,
+ price,
+ quantity,
+ discount,
+ } = product;
+
+ const priceAfterDiscount = price * ((100 - discount) / 100);
+
+ const { changeCart } = useContext(Context);
+
+ const addItem = () => {
+ changeCart({
+ ...product,
+ quantity: quantity && quantity + 1,
+ });
+ };
+
+ const substractItem = () => {
+ changeCart({
+ ...product,
+ quantity: quantity && quantity - 1,
+ });
+ };
+
+ return (
+
+
+
+
+
+
+
+
+ {name}
+
+
+
+
+
+
+ {quantity}
+
+
+
+
+
+
+ {`$${priceAfterDiscount * (quantity || 1)}`}
+
+
+ );
+};
diff --git a/src/components/Dropdown/Dropdown.scss b/src/components/Dropdown/Dropdown.scss
new file mode 100644
index 0000000000..e628ca3723
--- /dev/null
+++ b/src/components/Dropdown/Dropdown.scss
@@ -0,0 +1,82 @@
+@import "../../main.scss";
+
+.dropdown {
+ position: relative;
+
+ &__label {
+ @extend %small-text;
+ color: $color-secondary;
+ margin-bottom: 4px;
+ position: relative;
+ }
+
+ &__select {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ @extend %buttons-text;
+ padding: 10px;
+ height: 40px;
+ width: 100%;
+ border: 1px solid $color-icons;
+ outline: none;
+ color: $color-primary;
+ background-color: $color-white;
+ text-transform: capitalize;
+
+ &:hover {
+ border: 1px solid $color-primary;
+ }
+ }
+
+ &__arrow {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ transition: transform 0.3s;
+
+ &--active {
+ transform: rotate(180deg);
+ }
+ }
+
+ &__options {
+ position: absolute;
+ top: 110%;
+ width: 100%;
+ border: 1px solid $color-elements;
+ list-style: none;
+ padding: 8px 0;
+ background-color: $color-white;
+ }
+
+ &__option {
+ cursor: pointer;
+ height: 32px;
+ padding: 0 12px;
+ transition: all 0.3s;
+
+ &:hover {
+ background-color: $color-background;
+ }
+ }
+
+ &__option a {
+ display: flex;
+ align-items: center;
+ color: $color-secondary;
+ width: 100%;
+ height: 100%;
+ text-decoration: none;
+ text-transform: capitalize;
+ transition: all 0.3s;
+
+ &:last-child {
+ margin-bottom: 0;
+ }
+
+ &:hover {
+ color: $color-primary;
+ }
+ }
+}
diff --git a/src/components/Dropdown/Dropdown.tsx b/src/components/Dropdown/Dropdown.tsx
new file mode 100644
index 0000000000..321942691f
--- /dev/null
+++ b/src/components/Dropdown/Dropdown.tsx
@@ -0,0 +1,96 @@
+import classNames from 'classnames';
+import { useRef, useState } from 'react';
+import { Link, useSearchParams } from 'react-router-dom';
+import { getSearchWith } from '../../utils/searchHelper';
+import './Dropdown.scss';
+
+type Props = {
+ dropdownList: string[];
+ defaultValue: string;
+ label: string;
+ searchParamsKey: string;
+};
+
+export const Dropdown: React.FC = ({
+ dropdownList,
+ defaultValue,
+ label,
+ searchParamsKey,
+}) => {
+ const [isOpen, setIsOpen] = useState(false);
+ const [searchParams] = useSearchParams();
+ const [value, setValue] = useState(defaultValue);
+ const buttonRef = useRef(null);
+
+ const getSearchParams = (params: string) => {
+ if (searchParamsKey === 'perPage') {
+ return getSearchWith(searchParams, {
+ page: '1',
+ [searchParamsKey]: params,
+ });
+ }
+
+ return getSearchWith(searchParams, {
+ [searchParamsKey]: params,
+ });
+ };
+
+ document.addEventListener('click', (e) => {
+ if (e.target !== buttonRef.current) {
+ setIsOpen(false);
+ }
+ });
+
+ document.addEventListener('keyup', (e) => {
+ if (e.key === 'Tab' || e.key === 'Escape') {
+ setIsOpen(false);
+ }
+ });
+
+ return (
+
+
{label}
+
+
+ {isOpen && (
+
+ {dropdownList.map(dropdownItem => (
+ - {
+ setValue(dropdownItem);
+ setIsOpen(false);
+ }}
+ >
+
+ {dropdownItem}
+
+
+ ))}
+
+ )}
+
+ );
+};
diff --git a/src/components/Footer/Footer.scss b/src/components/Footer/Footer.scss
new file mode 100644
index 0000000000..433529ae6a
--- /dev/null
+++ b/src/components/Footer/Footer.scss
@@ -0,0 +1,55 @@
+@import "../../main.scss";
+
+.footer {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ height: 96px;
+ box-sizing: border-box;
+ border-top: 1px solid $color-elements;
+
+ &__container {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ max-width: 1136px;
+ width: 100%;
+ margin: 0 auto;
+ }
+
+ &__nav {
+ display: flex;
+ gap: 64px;
+ }
+
+ &__link {
+ @extend %uppercase-text;
+ color: $color-secondary;
+ text-decoration: none;
+ }
+}
+
+.button-top {
+ display: flex;
+ align-items: center;
+ gap: 16px;
+ cursor: pointer;
+ font-family: "Mont", arial, helvetica, sans-serif;
+ font-weight: 600;
+ font-size: 12px;
+ line-height: 15px;
+ text-decoration: none;
+
+ &__text {
+ color: $color-secondary;
+ }
+
+ &__icon {
+ display: block;
+ background-position: center;
+ background-repeat: no-repeat;
+ @include squareSize(32px);
+ border: 1px solid $color-icons;
+ background-image: url(../../images/ArrowUp.svg);
+ }
+}
diff --git a/src/components/Footer/Footer.tsx b/src/components/Footer/Footer.tsx
new file mode 100644
index 0000000000..6b73cdac88
--- /dev/null
+++ b/src/components/Footer/Footer.tsx
@@ -0,0 +1,50 @@
+import { Link, useLocation } from 'react-router-dom';
+import { Logo } from '../Logo/Logo';
+import footerList from '../../api/navFooter.json';
+import './Footer.scss';
+
+export const Footer: React.FC = () => {
+ const location = useLocation();
+
+ const handleScrollToTop = () => {
+ window.scrollTo({
+ top: 0,
+ left: 0,
+ behavior: 'smooth',
+ });
+ };
+
+ return (
+
+ );
+};
diff --git a/src/components/Header/Header.scss b/src/components/Header/Header.scss
new file mode 100644
index 0000000000..a0363f68df
--- /dev/null
+++ b/src/components/Header/Header.scss
@@ -0,0 +1,33 @@
+@import "../../main.scss";
+
+.header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ height: 64px;
+ margin: 0 0 40px;
+ border-bottom: 1px solid $color-elements;
+
+ &__container {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ width: 100%;
+ max-width: 1440px;
+ margin: 0 auto;
+ padding-left: 40px;
+ }
+
+ &__left {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ gap: 64px;
+ }
+
+ &__right {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ }
+}
diff --git a/src/components/Header/Header.tsx b/src/components/Header/Header.tsx
new file mode 100644
index 0000000000..18a5d6db6e
--- /dev/null
+++ b/src/components/Header/Header.tsx
@@ -0,0 +1,43 @@
+import { useLocation, useParams } from 'react-router-dom';
+import { Logo } from '../Logo/Logo';
+import { Nav } from '../Nav/Nav';
+import { SearchBar } from '../SearchBar/SearchBar';
+import { HeaderButtons } from '../HeaderButtons/HeaderButtons';
+import CategoriesList from '../../api/CategoriesList.json';
+import './Header.scss';
+
+export const Header: React.FC = () => {
+ const location = useLocation();
+ const { productId = '' } = useParams();
+
+ const cartCondition = location.pathname.includes('cart');
+ const favoriteCondition = location.pathname.includes('favorite');
+
+ const productCategoriesList = CategoriesList.map(item => item.type).some(
+ item => location.pathname.includes(item),
+ ) && !productId;
+
+ return (
+
+
+
+
+
+
+ {!cartCondition &&
}
+
+
+ {(favoriteCondition || productCategoriesList) && }
+ {!cartCondition && (
+
+ )}
+
+
+
+
+ );
+};
diff --git a/src/components/Header/index.tsx b/src/components/Header/index.tsx
new file mode 100644
index 0000000000..266dec8a1b
--- /dev/null
+++ b/src/components/Header/index.tsx
@@ -0,0 +1 @@
+export * from './Header';
diff --git a/src/components/HeaderButtons/HeaderButtons.scss b/src/components/HeaderButtons/HeaderButtons.scss
new file mode 100644
index 0000000000..52660893c2
--- /dev/null
+++ b/src/components/HeaderButtons/HeaderButtons.scss
@@ -0,0 +1,55 @@
+@import "../../main.scss";
+
+.top-item {
+ position: relative;
+ display: flex;
+ align-items: center;
+ border-left: 1px solid $color-elements;
+
+ &-count {
+ position: absolute;
+ top: 14px;
+ right: 17px;
+
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ @include squareSize(14px);
+
+ font-family: inherit;
+ font-weight: 600;
+ font-size: 9px;
+ line-height: 12px;
+ color: $color-white;
+ background-color: $color-red;
+ border: 1px solid $color-white;
+ border-radius: 50%;
+ }
+
+ &__link {
+ display: flex;
+ padding: 22px 24px;
+ color: $color-primary;
+ transition: border 0.5s;
+
+ &:hover {
+ border-bottom: 3px solid $color-primary;
+ cursor: pointer;
+ }
+ }
+
+ &__icon {
+ display: block;
+ background-position: center;
+ background-repeat: no-repeat;
+ padding: 8px;
+
+ &--favorite {
+ background-image: url(../../images/Favorite.svg);
+ }
+
+ &--cart {
+ background-image: url(../../images/Cart.svg);
+ }
+ }
+}
diff --git a/src/components/HeaderButtons/HeaderButtons.tsx b/src/components/HeaderButtons/HeaderButtons.tsx
new file mode 100644
index 0000000000..d3b3389a9b
--- /dev/null
+++ b/src/components/HeaderButtons/HeaderButtons.tsx
@@ -0,0 +1,55 @@
+import classNames from 'classnames';
+import { useContext } from 'react';
+import { Link } from 'react-router-dom';
+import { Context } from '../../context/Context';
+import './HeaderButtons.scss';
+
+type ButtonType = 'favorite' | 'cart';
+
+type Props = {
+ type: ButtonType,
+};
+
+export const HeaderButtons: React.FC = ({ type }) => {
+ const { cart, favorite } = useContext(Context);
+
+ const productsAmount = (type === 'cart')
+ ? cart.length
+ : favorite.length;
+
+ return (
+
+
+ {type === 'favorite'
+ && (
+
+ )}
+
+ {type === 'cart'
+ && (
+
+ )}
+
+ {productsAmount > 0
+ && (
+
+ {productsAmount}
+
+ )}
+
+
+ );
+};
diff --git a/src/components/LikeButton/LikeButton.scss b/src/components/LikeButton/LikeButton.scss
new file mode 100644
index 0000000000..841abfc86b
--- /dev/null
+++ b/src/components/LikeButton/LikeButton.scss
@@ -0,0 +1,43 @@
+@import "../../main.scss";
+
+.button-like {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ padding: 0;
+
+ background-color: $color-white;
+ border: 1px solid $color-icons;
+ transition: all 0.5s;
+ cursor: pointer;
+
+ &--small {
+ @include squareSize(40px);
+ }
+
+ &--big {
+ @include squareSize(48px);
+ }
+
+ &--icon {
+ display: block;
+ background-position: center;
+ background-repeat: no-repeat;
+ background-image: url(../../images/FavouritesHeartLike.svg);
+
+ &--selected {
+ display: block;
+ background-position: center;
+ background-repeat: no-repeat;
+ background-image: url(../../images/FavouritesFilledHeartLike.svg);
+ }
+ }
+
+ &--selected {
+ border-color: $color-elements;
+ }
+
+ &:hover {
+ border-color: $color-primary;
+ }
+}
diff --git a/src/components/LikeButton/LikeButton.tsx b/src/components/LikeButton/LikeButton.tsx
new file mode 100644
index 0000000000..2f4d6b0cb6
--- /dev/null
+++ b/src/components/LikeButton/LikeButton.tsx
@@ -0,0 +1,36 @@
+import { useContext } from 'react';
+import classNames from 'classnames';
+import { Product } from '../../types/Product';
+import { Context } from '../../context/Context';
+import './LikeButton.scss';
+import { ButtonType } from '../../types/ButtonType';
+
+type Props = {
+ product: Product;
+ size: ButtonType;
+};
+
+export const LikeButton: React.FC = ({ product, size }) => {
+ const { changeFavorite, favorite } = useContext(Context);
+
+ const isInFavs = favorite.length > 0
+ ? favorite.find(item => item.id === product?.id) : false;
+
+ return (
+
+ );
+};
diff --git a/src/components/Loader/Loader.scss b/src/components/Loader/Loader.scss
new file mode 100644
index 0000000000..36624981d2
--- /dev/null
+++ b/src/components/Loader/Loader.scss
@@ -0,0 +1,25 @@
+.loader {
+ display: flex;
+ width: 100%;
+ justify-content: center;
+ align-items: center;
+
+ &__content {
+ border-radius: 50%;
+ width: 2em;
+ height: 2em;
+ margin: 1em auto;
+ border: 0.3em solid #ddd;
+ border-left-color: #000;
+ animation: load8 1.2s infinite linear;
+ }
+}
+
+@keyframes load8 {
+ 0% {
+ transform: rotate(0deg);
+ }
+ 100% {
+ transform: rotate(360deg);
+ }
+}
diff --git a/src/components/Loader/Loader.tsx b/src/components/Loader/Loader.tsx
new file mode 100644
index 0000000000..36d7ba0c3c
--- /dev/null
+++ b/src/components/Loader/Loader.tsx
@@ -0,0 +1,5 @@
+export const Loader = () => (
+
+);
diff --git a/src/components/Logo/Logo.scss b/src/components/Logo/Logo.scss
new file mode 100644
index 0000000000..32a1301507
--- /dev/null
+++ b/src/components/Logo/Logo.scss
@@ -0,0 +1,7 @@
+.logo {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ height: 24px;
+ width: 40px;
+}
diff --git a/src/components/Logo/Logo.tsx b/src/components/Logo/Logo.tsx
new file mode 100644
index 0000000000..265922305d
--- /dev/null
+++ b/src/components/Logo/Logo.tsx
@@ -0,0 +1,7 @@
+import { Link } from 'react-router-dom';
+
+export const Logo: React.FC = () => (
+
+
+
+);
diff --git a/src/components/Nav/Nav.scss b/src/components/Nav/Nav.scss
new file mode 100644
index 0000000000..62ea7f3238
--- /dev/null
+++ b/src/components/Nav/Nav.scss
@@ -0,0 +1,6 @@
+.navbar {
+ display: flex;
+ gap: 64px;
+ align-items: center;
+ justify-content: space-between;
+}
diff --git a/src/components/Nav/Nav.tsx b/src/components/Nav/Nav.tsx
new file mode 100644
index 0000000000..1953037634
--- /dev/null
+++ b/src/components/Nav/Nav.tsx
@@ -0,0 +1,17 @@
+import pageNavList from '../../api/navHeader.json';
+import { PageNavLink } from '../PageNavLink/PageNavLink';
+import './Nav.scss';
+
+export const Nav = () => {
+ return (
+
+ {pageNavList.map(link => (
+
+ ))}
+
+ );
+};
diff --git a/src/components/NavLinkByCategory/NavLinkByCategory.tsx b/src/components/NavLinkByCategory/NavLinkByCategory.tsx
new file mode 100644
index 0000000000..ff297cd46e
--- /dev/null
+++ b/src/components/NavLinkByCategory/NavLinkByCategory.tsx
@@ -0,0 +1,37 @@
+import { NavLink } from 'react-router-dom';
+
+interface Props {
+ path: string,
+ type: string,
+ text: string,
+ amount: number,
+}
+
+export const NavLinkByCategory:React.FC = ({
+ path,
+ type,
+ text,
+ amount,
+}) => {
+ return (
+
+
+ {}
+
+
+ {text}
+
+
+ {amount}
+ {' '}
+ models
+
+
+ );
+};
diff --git a/src/components/NoResults/NoResults.scss b/src/components/NoResults/NoResults.scss
new file mode 100644
index 0000000000..b2ca5165a2
--- /dev/null
+++ b/src/components/NoResults/NoResults.scss
@@ -0,0 +1,37 @@
+@import "../../main.scss";
+
+.noresults {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ flex-direction: column;
+
+ &__title {
+ @extend %h2-text;
+ text-transform: uppercase;
+ color: $color-secondary;
+ margin-bottom: 40px;
+ }
+
+ &__button {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ text-align: center;
+
+ @extend %buttons-text;
+
+ width: 176px;
+ height: 40px;
+ background-color: $color-primary;
+ color: $color-white;
+ border: none;
+ outline: none;
+
+ &:hover {
+ color: $color-white;
+ background-color: $color-primary;
+ box-shadow: 0 3px 13px rgba(23, 32, 49, 0.26);
+ }
+ }
+}
diff --git a/src/components/NoResults/NoResults.tsx b/src/components/NoResults/NoResults.tsx
new file mode 100644
index 0000000000..52a9374ad4
--- /dev/null
+++ b/src/components/NoResults/NoResults.tsx
@@ -0,0 +1,19 @@
+import { useNavigate } from 'react-router-dom';
+import './NoResults.scss';
+
+export const NoResults = () => {
+ const navigate = useNavigate();
+
+ return (
+
+
Products can not be found
+
+
+ );
+};
diff --git a/src/components/PageNavLink/PageNavLink.scss b/src/components/PageNavLink/PageNavLink.scss
new file mode 100644
index 0000000000..7b4cb09cde
--- /dev/null
+++ b/src/components/PageNavLink/PageNavLink.scss
@@ -0,0 +1,25 @@
+@import "../../main.scss";
+
+.nav {
+ &__link {
+ display: flex;
+ align-items: center;
+ color: $color-secondary;
+
+ @extend %uppercase-text;
+ text-decoration: none;
+ transition: border 0.5s, color 0.5s;
+
+ &--active {
+ color: $color-primary;
+ padding: 25px 0;
+ border-bottom: 3px solid $color-primary;
+ }
+
+ &:hover {
+ color: $color-primary;
+ padding: 25px 0;
+ border-bottom: 3px solid $color-primary;
+ }
+ }
+}
diff --git a/src/components/PageNavLink/PageNavLink.tsx b/src/components/PageNavLink/PageNavLink.tsx
new file mode 100644
index 0000000000..6dff39ef79
--- /dev/null
+++ b/src/components/PageNavLink/PageNavLink.tsx
@@ -0,0 +1,28 @@
+import { NavLink } from 'react-router-dom';
+import classNames from 'classnames';
+import './PageNavLink.scss';
+
+type Props = {
+ name: string,
+ to: string,
+};
+
+export const PageNavLink:React.FC = ({
+ name,
+ to,
+}) => {
+ return (
+ classNames(
+ 'nav__link',
+ { 'nav__link--active': isActive },
+ )}
+ >
+ {name}
+
+ );
+};
diff --git a/src/components/Pagination/Pagination.scss b/src/components/Pagination/Pagination.scss
new file mode 100644
index 0000000000..71a2167cbc
--- /dev/null
+++ b/src/components/Pagination/Pagination.scss
@@ -0,0 +1,42 @@
+@import "../../main.scss";
+
+.pagination {
+ display: flex;
+ &__list {
+ display: flex;
+ gap: 8px;
+ margin: 0 8px;
+ }
+
+ &__item {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ @include squareSize(32px);
+ border: 1px solid $color-icons;
+
+ &--disabled {
+ pointer-events: none;
+ border: 1px solid $color-elements;
+ }
+
+ &:hover {
+ border: 1px solid $color-primary;
+ }
+ }
+
+ &__link {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ width: 100%;
+ height: 100%;
+ color: $color-primary;
+ text-decoration: none;
+
+ &--active {
+ background-color: $color-primary;
+ color: $color-white;
+ }
+ }
+}
diff --git a/src/components/Pagination/Pagination.tsx b/src/components/Pagination/Pagination.tsx
new file mode 100644
index 0000000000..710fd3ec7b
--- /dev/null
+++ b/src/components/Pagination/Pagination.tsx
@@ -0,0 +1,87 @@
+import { Link, useSearchParams } from 'react-router-dom';
+import classNames from 'classnames';
+import { getSearchWith } from '../../utils/searchHelper';
+import './Pagination.scss';
+
+type Props = {
+ productsAmount: number;
+ perPage: number;
+ currentPage: number;
+};
+
+export const Pagination: React.FC = ({
+ productsAmount, perPage, currentPage,
+}) => {
+ const [searchParams] = useSearchParams('');
+ const pagesAmount = () => (perPage ? Math.ceil(productsAmount / perPage) : 0);
+
+ const pagesList = (pagesAmount() > 1)
+ ? (Array.from(
+ { length: pagesAmount() },
+ (_, i) => i + 1,
+ ))
+ : [1];
+
+ return (
+
+
+
+
+
+
+ {pagesList.map((n) => {
+ const numberPage = n.toString();
+
+ return (
+ -
+
+ {numberPage}
+
+
+ );
+ })}
+
+
+
+
+
+
+ );
+};
diff --git a/src/components/ProductCard/ProductCard.scss b/src/components/ProductCard/ProductCard.scss
new file mode 100644
index 0000000000..faf14819ba
--- /dev/null
+++ b/src/components/ProductCard/ProductCard.scss
@@ -0,0 +1,150 @@
+@import "../../main.scss";
+
+.product-card {
+ &__container {
+ display: flex;
+ flex-direction: column;
+ padding: 24px;
+ width: 222px;
+ max-width: 272px;
+ border: 1px solid $color-elements;
+
+ &:hover {
+ border: 1px solid $color-primary;
+ }
+ }
+
+ &__link {
+ display: flex;
+ flex-direction: column;
+ text-decoration: none;
+ margin-bottom: 8px;
+ }
+
+ &__image {
+ display: flex;
+ height: 208px;
+ flex-basis: 208px;
+ object-fit: contain;
+ margin-bottom: 24px;
+ }
+
+ &__title {
+ @extend %buttons-text;
+ flex: 1 0 42px;
+ color: $color-primary;
+ }
+
+ &__prices {
+ border-bottom: 1px solid $color-elements;
+ padding-bottom: 4px;
+ margin-bottom: 16px;
+ }
+
+ &__price {
+ @extend %h2-text;
+ color: $color-primary;
+ margin-right: 8px;
+ }
+
+ &__discount {
+ @extend %h2-text;
+ color: $color-secondary;
+ text-decoration: line-through;
+ }
+
+ &__features {
+ display: flex;
+ justify-content: space-between;
+ text-transform: capitalize;
+ width: 100%;
+ margin-bottom: 16px;
+ }
+
+ &__list {
+ @extend %small-text;
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ list-style: none;
+ }
+
+ &__key {
+ color: $color-secondary;
+ }
+
+ &__value {
+ color: $color-primary;
+ text-align: right;
+ }
+
+ &__buttons {
+ display: flex;
+ gap: 8px;
+
+ &__to-cart {
+ padding: 0;
+ width: 176px;
+ height: 40px;
+
+ @extend %buttons-text;
+
+ color: $color-white;
+ background-color: $color-primary;
+ border: none;
+ outline: none;
+
+ transition: box-shadow 0.5s;
+ cursor: pointer;
+
+ &--selected {
+ color: $color-green;
+ background: none;
+ border: 1px solid $color-elements;
+ }
+
+ &:hover {
+ color: $color-white;
+ background-color: $color-primary;
+ box-shadow: 0 3px 13px rgba(23, 32, 49, 0.26);
+ }
+ }
+
+ &__to-favs {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ @include squareSize(40px);
+ padding: 0;
+
+ background-color: $color-white;
+ border: 1px solid $color-icons;
+ transition: all 0.5s;
+ cursor: pointer;
+
+ &--icon {
+ @include squareSize(16px);
+ display: block;
+ background-position: center;
+ background-repeat: no-repeat;
+ background-image: url(../../images/FavouritesHeartLike.svg);
+
+ &--selected {
+ @include squareSize(16px);
+ display: block;
+ background-position: center;
+ background-repeat: no-repeat;
+ background-image: url(../../images/FavouritesFilledHeartLike.svg);
+ }
+ }
+
+ &--selected {
+ border-color: $color-elements;
+ }
+
+ &:hover {
+ border-color: $color-primary;
+ }
+ }
+ }
+}
diff --git a/src/components/ProductCard/ProductCard.tsx b/src/components/ProductCard/ProductCard.tsx
new file mode 100644
index 0000000000..1f7bccfa5d
--- /dev/null
+++ b/src/components/ProductCard/ProductCard.tsx
@@ -0,0 +1,157 @@
+import { useContext } from 'react';
+import { Link } from 'react-router-dom';
+import classNames from 'classnames';
+import { Product } from '../../types/Product';
+import { Context } from '../../context/Context';
+import { getProductDetails } from '../../api/Products';
+import { Error } from '../../types/ErrorType';
+import './ProductCard.scss';
+
+type Props = {
+ product: Product,
+};
+
+export const ProductCard: React.FC = ({ product }) => {
+ const {
+ products,
+ setSelectedProduct,
+ setIsError,
+ setIsLoading,
+ } = useContext(Context);
+
+ const {
+ id,
+ type,
+ imageUrl,
+ name,
+ price,
+ discount,
+ screen,
+ capacity,
+ ram,
+ } = product;
+
+ const {
+ cart, changeCart, favorite, changeFavorite,
+ } = useContext(Context);
+
+ const isInCart = cart.length > 0
+ ? cart.find(item => item.id === id) : false;
+
+ const isInFavs = favorite.length > 0
+ ? favorite.find(item => item.id === id) : false;
+
+ const getProductLink = (searchProduct: Product) => {
+ switch (searchProduct.type) {
+ case 'phone':
+ return `../phones/${searchProduct.id}`;
+
+ case 'tablet':
+ return `../tablets/${searchProduct.id}`;
+
+ case 'accessories':
+ return `../accessories/${searchProduct.id}`;
+
+ default:
+ return '';
+ }
+ };
+
+ const currentProduct = products.find(
+ productCard => productCard.id === id,
+ );
+
+ const discountPrice = price - (price / 100) * discount;
+
+ const getCardDetails = async (productId: string) => {
+ setIsError(null);
+ setIsLoading(true);
+
+ try {
+ const currentProductDetails = await getProductDetails(productId);
+
+ setSelectedProduct(currentProductDetails);
+ } catch {
+ setIsError(Error.GET_PRODUCT_DETAILS);
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ const features = {
+ keys: ['screen', 'capacity', 'RAM'],
+ values: [`${screen}`, `${capacity}`, `${ram}`],
+ };
+
+ return (
+
+
+
currentProduct && getCardDetails(currentProduct.id)}
+ >
+
+
{name}
+
+
+
+ {`$${price}`}
+ {discount > 0 && {`$${discountPrice}`}}
+
+
+
+
+ {features.keys.map((key) => (
+ -
+ {key}
+
+ ))}
+
+
+
+ {features.values.map((value) => (
+ -
+ {value}
+
+ ))}
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/src/components/ProductsList/ProductList.scss b/src/components/ProductsList/ProductList.scss
new file mode 100644
index 0000000000..f84d393275
--- /dev/null
+++ b/src/components/ProductsList/ProductList.scss
@@ -0,0 +1,33 @@
+@import "../../main.scss";
+
+.product-list {
+ &__products {
+ display: grid;
+ grid-template-columns: repeat(4, 1fr);
+ row-gap: 40px;
+ column-gap: 16px;
+ margin-bottom: 40px;
+ }
+
+ &__title {
+ margin-bottom: 8px;
+ }
+
+ &__count {
+ margin-bottom: 40px;
+ color: $color-secondary;
+ }
+
+ &__dropdowns {
+ display: grid;
+ grid-template-columns: 176px 128px;
+ column-gap: 16px;
+ margin-bottom: 24px;
+ }
+
+ &__pagination {
+ display: flex;
+ justify-content: center;
+ margin-bottom: 80px;
+ }
+}
diff --git a/src/components/ProductsList/ProductsList.tsx b/src/components/ProductsList/ProductsList.tsx
new file mode 100644
index 0000000000..e0a4d6feca
--- /dev/null
+++ b/src/components/ProductsList/ProductsList.tsx
@@ -0,0 +1,115 @@
+import { useSearchParams } from 'react-router-dom';
+import { NoResults } from '../NoResults/NoResults';
+import { Product } from '../../types/Product';
+import { ProductCard } from '../ProductCard/ProductCard';
+import { Dropdown } from '../Dropdown/Dropdown';
+import { Pagination } from '../Pagination/Pagination';
+import './ProductList.scss';
+
+type Props = {
+ title: string
+ products: Product[]
+};
+
+const optionsSort = ['newest', 'alphabetically', 'price', 'all'];
+const optionsPage = ['4', '8', '16', 'all'];
+
+export const ProductsList: React.FC = ({ title, products }) => {
+ const [searchParams] = useSearchParams();
+
+ const productsAmount = products.length;
+
+ const currentPage = Number(searchParams.get('page')) || 1;
+ const sort = searchParams.get('sort') || '';
+ const perPage = Number(searchParams.get('perPage')) || productsAmount;
+ const query = searchParams.get('query') || '';
+
+ const getSortedProducts = (productsList: Product[]) => {
+ let sortedProducts = [...productsList];
+
+ if (sort === 'age') {
+ sortedProducts = sortedProducts.sort(
+ (product1, product2) => product1.age - product2.age,
+ );
+ }
+
+ if (sort === 'name') {
+ sortedProducts = sortedProducts.sort(
+ (product1, product2) => product1.name.localeCompare(product2.name),
+ );
+ }
+
+ if (sort === 'price') {
+ sortedProducts = sortedProducts.sort(
+ (product1, product2) => product1.price - product2.price,
+ );
+ }
+
+ if (query) {
+ const queryFilter = (param?: string | null) => {
+ return param
+ ? param.toLowerCase().includes(query.toLowerCase())
+ : null;
+ };
+
+ sortedProducts = sortedProducts.filter(
+ product => queryFilter(product.name),
+ );
+ }
+
+ return sortedProducts || null;
+ };
+
+ const lastPage = Math.ceil(productsAmount / +perPage);
+
+ const start = currentPage * perPage - perPage;
+ const end = currentPage * perPage <= productsAmount
+ ? currentPage * perPage
+ : productsAmount;
+
+ const sortedProducts = getSortedProducts(products).slice(start, end);
+
+ return (
+
+
+
{title}
+
{`${productsAmount} models`}
+ {productsAmount === 0 ? (
+
+ ) : (
+ <>
+
+
+
+
+
+
+ {sortedProducts.map((product) => (
+
+ ))}
+
+ >
+ )}
+
+ {perPage !== productsAmount && lastPage > 1 && (
+
+ )}
+
+
+
+ );
+};
diff --git a/src/components/ProductsNav/ProductsNav.scss b/src/components/ProductsNav/ProductsNav.scss
new file mode 100644
index 0000000000..0685c31376
--- /dev/null
+++ b/src/components/ProductsNav/ProductsNav.scss
@@ -0,0 +1,42 @@
+@import "../../main.scss";
+
+.productsNav {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ margin-bottom: 40px;
+
+ &__home {
+ @include squareSize(16px);
+ background-image: url("../../images/Home.svg");
+ background-position: center;
+ background-repeat: no-repeat;
+ }
+
+ &__link {
+ @extend %small-text;
+ color: $color-primary;
+ display: flex;
+ gap: 8px;
+ text-decoration: none;
+
+ &::before {
+ content: "";
+ display: block;
+ @include squareSize(16px);
+ background-image: url("../../images/ArrowRight.svg");
+ background-position: center;
+ background-repeat: no-repeat;
+ color: $color-icons;
+ }
+
+ &--disabled {
+ pointer-events: none;
+ color: $color-secondary;
+ }
+
+ &--title {
+ color: $color-secondary;
+ }
+ }
+}
diff --git a/src/components/ProductsNav/ProductsNav.tsx b/src/components/ProductsNav/ProductsNav.tsx
new file mode 100644
index 0000000000..f8ef9db460
--- /dev/null
+++ b/src/components/ProductsNav/ProductsNav.tsx
@@ -0,0 +1,66 @@
+import {
+ useContext, useEffect, useMemo, useState,
+} from 'react';
+import { Link, useLocation, useParams } from 'react-router-dom';
+import './ProductsNav.scss';
+import { Context } from '../../context/Context';
+import { getProductDetails } from '../../api/Products';
+
+export const ProductsNav: React.FC = () => {
+ const { selectedProduct, setSelectedProduct } = useContext(Context);
+ const location = useLocation();
+ const { productId = '' } = useParams();
+ const [isProduct, setIsProduct] = useState(false);
+
+ const pageLink = location.pathname.split('/').slice(1)[0];
+ const pageTitle = pageLink.charAt(0).toUpperCase() + pageLink.slice(1);
+
+ const productTitle = useMemo(() => {
+ if (!selectedProduct && productId) {
+ getProductDetails(productId)
+ .then((productItem) => {
+ setSelectedProduct(productItem);
+ });
+ }
+
+ return selectedProduct?.name;
+ }, [selectedProduct, productId]);
+
+ useEffect(() => {
+ if (productId.length > 0) {
+ setIsProduct(true);
+ } else {
+ setIsProduct(false);
+ }
+ }, []);
+
+ return (
+ <>
+
+
+
+ {isProduct ? (
+ <>
+
+ {pageTitle}
+
+
+
+ {productTitle}
+
+ >
+ ) : (
+
+ {pageTitle}
+
+ )}
+
+ >
+ );
+};
diff --git a/src/components/ProductsSlider/ProductsSlider.scss b/src/components/ProductsSlider/ProductsSlider.scss
new file mode 100644
index 0000000000..c0998c3bb2
--- /dev/null
+++ b/src/components/ProductsSlider/ProductsSlider.scss
@@ -0,0 +1,60 @@
+@import "../../main.scss";
+
+.products-slider {
+ &__container {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ margin-bottom: 33px;
+ }
+
+ &__header {
+ @extend %h1-text;
+ }
+
+ &__buttons {
+ display: flex;
+ gap: 16px;
+ }
+
+ &__button {
+ @include squareSize(32px);
+ background-color: $color-white;
+ border: 1px solid $color-icons;
+ cursor: pointer;
+
+ &:hover {
+ border-color: $color-primary;
+ }
+
+ &--disabled {
+ border: 1px solid $color-elements;
+ pointer-events: none;
+ opacity: 0.5;
+ }
+ }
+
+ &__products {
+ display: flex;
+ justify-content: space-between;
+ margin-bottom: 72px;
+ gap: 16px;
+ }
+
+ &__icon {
+ display: block;
+ margin: 0 auto;
+ background-position: center;
+ background-repeat: no-repeat;
+ @include squareSize(16px);
+ cursor: pointer;
+ &--right {
+ background-image: url(../../images/ArrowRight.svg);
+ }
+
+ &--left {
+ background-image: url(../../images/ArrowLeft.svg);
+ }
+
+ }
+}
diff --git a/src/components/ProductsSlider/ProductsSlider.tsx b/src/components/ProductsSlider/ProductsSlider.tsx
new file mode 100644
index 0000000000..db877dac25
--- /dev/null
+++ b/src/components/ProductsSlider/ProductsSlider.tsx
@@ -0,0 +1,59 @@
+import { memo, useState } from 'react';
+import classNames from 'classnames';
+import { Product } from '../../types/Product';
+import { ProductCard } from '../ProductCard/ProductCard';
+import './ProductsSlider.scss';
+
+type Props = {
+ title: string;
+ products: Product[];
+};
+
+export const ProductsSlider: React.FC = memo(({ title, products }) => {
+ const [start, setStart] = useState(0);
+ const end = start + 4;
+
+ const disablePrevButton = start <= 0;
+ const disableNextButton = end > products.length - 1;
+
+ return (
+
+
+
{title}
+
+
+
+
+
+
+ {products.slice(start, end).map((product: Product) => (
+
+ ))}
+
+
+ );
+});
diff --git a/src/components/SearchBar/SearchBar.scss b/src/components/SearchBar/SearchBar.scss
new file mode 100644
index 0000000000..7a2e3aacc4
--- /dev/null
+++ b/src/components/SearchBar/SearchBar.scss
@@ -0,0 +1,42 @@
+@import "../../main.scss";
+
+.searchbar {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ min-width: 60px;
+
+ padding: 15px 24px;
+ border-left: 1px solid $color-elements;
+
+ &__input {
+ width: 90%;
+
+ @extend %buttons-text;
+ line-height: 18px;
+ color: $color-icons;
+ border: none;
+ outline: none;
+
+ &::placeholder {
+ cursor: text;
+ color: $color-icons;
+ }
+ }
+
+ &__icon {
+ display: block;
+ background-position: center;
+ background-repeat: no-repeat;
+ @include squareSize(16px);
+ padding: 8px;
+
+ &--search {
+ background-image: url(../../images/Search.svg);
+ }
+
+ &--close {
+ background-image: url(../../images/Close.svg);
+ }
+ }
+}
diff --git a/src/components/SearchBar/SearchBar.tsx b/src/components/SearchBar/SearchBar.tsx
new file mode 100644
index 0000000000..f914cc780c
--- /dev/null
+++ b/src/components/SearchBar/SearchBar.tsx
@@ -0,0 +1,68 @@
+import { debounce } from 'lodash';
+import { useMemo, useState } from 'react';
+import { useLocation, useSearchParams } from 'react-router-dom';
+import { getSearchWith } from '../../utils/searchHelper';
+import './SearchBar.scss';
+
+export const SearchBar: React.FC = () => {
+ const [searchParams, setSearchParams] = useSearchParams();
+ const query = searchParams.get('query') || '';
+ const [inputValue, setInputValue] = useState(query);
+ const location = useLocation();
+ const currentLocation = location.pathname.slice(1);
+
+ useMemo(() => {
+ setInputValue(query);
+ }, [query]);
+
+ const handleDebounceChange = debounce(
+ (event: React.ChangeEvent) => {
+ setSearchParams(
+ getSearchWith(searchParams,
+ { query: event.target.value.trim() || null }),
+ );
+ }, 1000,
+ );
+
+ const handleInputChange = (event: React.ChangeEvent) => {
+ setInputValue(event.target.value);
+ handleDebounceChange(event);
+ };
+
+ const handleDeleteChange = () => {
+ setInputValue('');
+ setSearchParams(getSearchWith(searchParams, { query: null }));
+ };
+
+ return (
+
+
+
+ {!inputValue && (
+
+ )}
+
+ { inputValue.length > 0 && (
+
handleDeleteChange()}
+ onClick={() => handleDeleteChange()}
+ >
+
+
+ )}
+
+ );
+};
diff --git a/src/components/ShopByCategory/ShopByCategory.scss b/src/components/ShopByCategory/ShopByCategory.scss
new file mode 100644
index 0000000000..5bfd2e6cdb
--- /dev/null
+++ b/src/components/ShopByCategory/ShopByCategory.scss
@@ -0,0 +1,49 @@
+@import "../../main.scss";
+
+.categories {
+ margin-bottom: 80px;
+
+ &__title {
+ @extend %h1-text;
+ margin-bottom: 24px;
+ }
+
+ &__container {
+ display: flex;
+ justify-content: space-between;
+ }
+
+ &__image {
+ @include squareSize(368px);
+ margin-bottom: 14px;
+ object-fit: scale-down;
+ background-position: bottom right;
+ }
+
+ &__item {
+ text-decoration: none;
+ }
+
+ &__name {
+ @extend %h3-text;
+ margin-bottom: 4px;
+ color: $color-primary;
+ }
+
+ &__count {
+ color: $color-secondary;
+ }
+
+ &__item-container {
+ transition: all 0.5s;
+ cursor: pointer;
+
+ &:hover {
+ transform: scale(0.95);
+ }
+
+ &:hover .categories__image {
+ box-shadow: 0 3px 13px rgba(23, 32, 49, 0.26);
+ }
+ }
+}
diff --git a/src/components/ShopByCategory/ShopByCategory.tsx b/src/components/ShopByCategory/ShopByCategory.tsx
new file mode 100644
index 0000000000..c79a663f5e
--- /dev/null
+++ b/src/components/ShopByCategory/ShopByCategory.tsx
@@ -0,0 +1,74 @@
+import { useContext, useMemo } from 'react';
+import { Link } from 'react-router-dom';
+import CategoriesList from '../../api/CategoriesList.json';
+import { Context } from '../../context/Context';
+import './ShopByCategory.scss';
+
+export const ShopByCategory: React.FC = () => {
+ const { products } = useContext(Context);
+
+ const phones = useMemo(() => (
+ products.filter(product => product.type === 'phone')
+ ), [products]);
+
+ const tablets = useMemo(() => (
+ products.filter(product => product.type === 'tablet')
+ ), [products]);
+
+ const accessories = useMemo(() => (
+ products.filter(product => product.type === 'accessory')
+ ), [products]);
+
+ const categoryCount = (type: string) => {
+ switch (type) {
+ case 'phones':
+ return phones.length;
+
+ case 'tablets':
+ return tablets.length;
+
+ case 'accessory':
+ return accessories.length;
+
+ default:
+ return 0;
+ }
+ };
+
+ return (
+
+
+ Shop by category
+
+
+ {CategoriesList.map(type => (
+
+
+
+
+
+ {type.title}
+
+
+
+ {`${categoryCount(type.type)} models`}
+
+
+
+ ))}
+
+
+ );
+};
diff --git a/src/context/Context.tsx b/src/context/Context.tsx
new file mode 100644
index 0000000000..1a40f5de58
--- /dev/null
+++ b/src/context/Context.tsx
@@ -0,0 +1,111 @@
+import React, {
+ Dispatch, SetStateAction, useEffect, useState,
+} from 'react';
+import { Product } from '../types/Product';
+import { ProductDetails } from '../types/ProductDetails';
+import { Error } from '../types/ErrorType';
+import { getProducts } from '../api/Products';
+import { useLocalStorage } from '../utils/useLocaleStorage';
+import { getProductToSave } from '../utils/getProductToSave';
+
+type ContextValue = {
+ products: Product[],
+ selectedProduct: ProductDetails,
+ cart: Product[],
+ favorite: Product[],
+ isError: Error | null,
+ isLoading: boolean;
+ setProducts: Dispatch>,
+ setSelectedProduct: Dispatch>,
+ setCart: Dispatch>,
+ setFavorite: Dispatch>,
+ setIsError: Dispatch>,
+ setIsLoading: Dispatch>,
+ changeCart: (newProduct: Product) => void,
+ changeFavorite: (newProduct: Product) => void,
+};
+
+export const Context = React.createContext({
+ products: [],
+ selectedProduct: JSON.parse(`${localStorage.getItem('selectedProduct')}`),
+ cart: [],
+ favorite: [],
+ isError: null,
+ isLoading: false,
+ setProducts: () => {},
+ setSelectedProduct: () => {},
+ setCart: () => {},
+ setFavorite: () => {},
+ setIsError: () => {},
+ setIsLoading: () => {},
+ changeCart: () => {},
+ changeFavorite: () => {},
+});
+
+type Props = {
+ children: React.ReactNode;
+};
+
+export const ContextProvider: React.FC = ({ children }) => {
+ const selectedProductStorage = JSON.parse(`${localStorage.getItem('selectedProduct')}`);
+
+ const [products, setProducts] = useState([]);
+ const [
+ selectedProduct, setSelectedProduct,
+ ] = useState(selectedProductStorage);
+ const [cart, setCart] = useLocalStorage('cart', []);
+ const [favorite, setFavorite] = useLocalStorage('favorite', []);
+ const [isError, setIsError] = useState(null);
+ const [isLoading, setIsLoading] = useState(false);
+
+ const changeCart = getProductToSave(
+ 'cart',
+ cart,
+ setCart,
+ );
+ const changeFavorite = getProductToSave(
+ 'favorite',
+ favorite,
+ setFavorite,
+ );
+
+ const contextValue: ContextValue = {
+ products,
+ selectedProduct,
+ cart,
+ favorite,
+ isError,
+ isLoading,
+ setProducts,
+ setSelectedProduct,
+ setCart,
+ setFavorite,
+ setIsError,
+ setIsLoading,
+ changeCart,
+ changeFavorite,
+ };
+
+ useEffect(() => {
+ const loadProducts = async () => {
+ try {
+ setIsLoading(true);
+ const loadedProducts = await getProducts();
+
+ setProducts(loadedProducts);
+ } catch (error) {
+ setIsError(Error.GET_PRODUCTS);
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ loadProducts();
+ }, []);
+
+ return (
+
+ {children}
+
+ );
+};
diff --git a/src/fonts/FontsFree-Net-Mont-Bold.ttf b/src/fonts/FontsFree-Net-Mont-Bold.ttf
new file mode 100644
index 0000000000..d359b09ba6
Binary files /dev/null and b/src/fonts/FontsFree-Net-Mont-Bold.ttf differ
diff --git a/src/fonts/FontsFree-regular.ttf b/src/fonts/FontsFree-regular.ttf
new file mode 100644
index 0000000000..27a0f98c04
Binary files /dev/null and b/src/fonts/FontsFree-regular.ttf differ
diff --git a/src/fonts/FontsFree-semibold.ttf b/src/fonts/FontsFree-semibold.ttf
new file mode 100644
index 0000000000..f3d6cef520
Binary files /dev/null and b/src/fonts/FontsFree-semibold.ttf differ
diff --git a/src/fonts/_fonts.scss b/src/fonts/_fonts.scss
new file mode 100644
index 0000000000..9eb66e0626
--- /dev/null
+++ b/src/fonts/_fonts.scss
@@ -0,0 +1,17 @@
+@font-face {
+ font-family: Mont;
+ font-weight: 500;
+ src: url(./FontsFree-regular.ttf);
+}
+
+@font-face {
+ font-family: Mont;
+ font-weight: 600;
+ src: url(./FontsFree-semibold.ttf);
+}
+
+@font-face {
+ font-family: Mont;
+ font-weight: 700;
+ src: url(./FontsFree-Net-Mont-Bold.ttf);
+}
diff --git a/src/images/ArrowDown.svg b/src/images/ArrowDown.svg
new file mode 100644
index 0000000000..e51b6915b6
--- /dev/null
+++ b/src/images/ArrowDown.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/images/ArrowLeft.svg b/src/images/ArrowLeft.svg
new file mode 100644
index 0000000000..5e3a0a6280
--- /dev/null
+++ b/src/images/ArrowLeft.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/images/ArrowRight.svg b/src/images/ArrowRight.svg
new file mode 100644
index 0000000000..d996658665
--- /dev/null
+++ b/src/images/ArrowRight.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/images/ArrowUp.svg b/src/images/ArrowUp.svg
new file mode 100644
index 0000000000..46cc1e3b34
--- /dev/null
+++ b/src/images/ArrowUp.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/images/Cart.svg b/src/images/Cart.svg
new file mode 100644
index 0000000000..6030970f2e
--- /dev/null
+++ b/src/images/Cart.svg
@@ -0,0 +1,5 @@
+
diff --git a/src/images/Close.svg b/src/images/Close.svg
new file mode 100644
index 0000000000..78d418ab46
--- /dev/null
+++ b/src/images/Close.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/images/Favorite.svg b/src/images/Favorite.svg
new file mode 100644
index 0000000000..802e30c79e
--- /dev/null
+++ b/src/images/Favorite.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/images/FavouritesFilledHeartLike.svg b/src/images/FavouritesFilledHeartLike.svg
new file mode 100644
index 0000000000..7138d7522b
--- /dev/null
+++ b/src/images/FavouritesFilledHeartLike.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/images/FavouritesHeartLike.svg b/src/images/FavouritesHeartLike.svg
new file mode 100644
index 0000000000..ec808b48e6
--- /dev/null
+++ b/src/images/FavouritesHeartLike.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/images/Home.svg b/src/images/Home.svg
new file mode 100644
index 0000000000..474476cb02
--- /dev/null
+++ b/src/images/Home.svg
@@ -0,0 +1,4 @@
+
diff --git a/src/images/Minus.svg b/src/images/Minus.svg
new file mode 100644
index 0000000000..97c41038ac
--- /dev/null
+++ b/src/images/Minus.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/images/Plus.svg b/src/images/Plus.svg
new file mode 100644
index 0000000000..ab3c34061b
--- /dev/null
+++ b/src/images/Plus.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/images/Search.svg b/src/images/Search.svg
new file mode 100644
index 0000000000..3dc966918a
--- /dev/null
+++ b/src/images/Search.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/index.tsx b/src/index.tsx
index 5f7410fd92..5f33fab9f7 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -1,8 +1,54 @@
import ReactDOM from 'react-dom';
+import {
+ HashRouter as Router,
+ Navigate,
+ Route,
+ Routes,
+} from 'react-router-dom';
import App from './App';
+import { PageNotFound } from './pages/PageNotFound/PageNotFound';
+import { HomePage } from './pages/HomePage/HomePage';
+import { PhonesPage } from './pages/PhonesPage/PhonesPage';
+import {
+ ProductDetailsPage,
+} from './pages/ProductDetailsPage/ProductDetailsPage';
+import { TabletsPage } from './pages/TabletsPage/TabletsPage';
+import { AccessoriesPage } from './pages/AccessoriesPage/AccessoriesPage';
+import { FavoritesPage } from './pages/FavoritesPage/FavoritesPage';
+import { CartPage } from './pages/CartPage/CartPage';
+import { ContextProvider } from './context/Context';
ReactDOM.render(
- ,
+
+
+
+ }>
+ } />
+
+ } />
+ } />
+
+
+ } />
+ } />
+
+
+
+ } />
+ } />
+
+
+
+ } />
+ } />
+
+
+ } />
+ } />
+
+
+
+ ,
document.getElementById('root'),
);
diff --git a/src/main.scss b/src/main.scss
new file mode 100644
index 0000000000..c405e1b5fa
--- /dev/null
+++ b/src/main.scss
@@ -0,0 +1,4 @@
+@import "./utils/variables";
+@import "./utils/mixins";
+@import "./utils/extends";
+@import "./fonts/fonts";
diff --git a/src/pages/AccessoriesPage/AccessoriesPage.tsx b/src/pages/AccessoriesPage/AccessoriesPage.tsx
new file mode 100644
index 0000000000..e76cacf44c
--- /dev/null
+++ b/src/pages/AccessoriesPage/AccessoriesPage.tsx
@@ -0,0 +1,52 @@
+import { useContext, useEffect, useState } from 'react';
+import { Product } from '../../types/Product';
+import { Context } from '../../context/Context';
+import { getProducts } from '../../api/Products';
+import { Error } from '../../types/ErrorType';
+import { Loader } from '../../components/Loader/Loader';
+import { ProductsNav } from '../../components/ProductsNav/ProductsNav';
+import { ProductsList } from '../../components/ProductsList/ProductsList';
+
+export const AccessoriesPage: React.FC = () => {
+ const [accessories, setAccessories] = useState([]);
+ const { isLoading, setIsLoading, setIsError } = useContext(Context);
+
+ const getAccessories = (products: Product[], type: string) => {
+ const accessoriesFromApi = products.filter(
+ product => product.type === type,
+ );
+
+ setAccessories(accessoriesFromApi);
+ };
+
+ useEffect(() => {
+ const loadPhones = async () => {
+ try {
+ setIsLoading(true);
+ const products = await getProducts();
+
+ getAccessories(products, 'accessory');
+ } catch (error) {
+ setIsError(Error.GET_PRODUCTS);
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ loadPhones();
+ }, []);
+
+ return (
+
+ {isLoading ? (
+
+ ) : (
+ <>
+
+
+
+ >
+ )}
+
+ );
+};
diff --git a/src/pages/CartPage/CartPage.scss b/src/pages/CartPage/CartPage.scss
new file mode 100644
index 0000000000..41d87f81f2
--- /dev/null
+++ b/src/pages/CartPage/CartPage.scss
@@ -0,0 +1,102 @@
+@import "../../main.scss";
+
+.cart {
+ margin-bottom: 80px;
+
+ &__navigate {
+ margin: 40px 0 16px;
+ }
+
+ &__empty {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ @extend %h2-text;
+ color: $color-secondary;
+ margin-bottom: 40px;
+ }
+
+ &__title {
+ margin-bottom: 24px;
+ }
+
+ &__content {
+ display: grid;
+ grid-template-columns: 1fr 368px;
+ gap: 16px;
+ }
+
+ &__products {
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+ }
+
+ &__total-box {
+ align-self: start;
+ padding: 24px;
+ border: 1px solid $color-elements;
+ position: relative;
+ }
+
+ &__info {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ border-bottom: 1px solid $color-elements;
+ margin-bottom: 24px;
+ }
+
+ &__total-price {
+ @extend %h2-text;
+ color: $color-primary;
+ }
+
+ &__total-count {
+ @extend %buttons-text;
+ color: $color-secondary;
+ margin-bottom: 24px;
+ }
+}
+
+.modal {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ display: flex;
+ flex-direction: column;
+ justify-content: space-around;
+ border: 1px solid $color-elements;
+ background-color: $color-red;
+
+ &__content {
+ padding: 10px 24px;
+ }
+
+ &__title {
+ display: flex;
+ flex-direction: column;
+ text-align: center;
+ color: $color-white;
+ margin-bottom: 10px;
+ }
+}
+
+.button {
+ width: 100%;
+ height: 48px;
+
+ @extend %buttons-text;
+ text-align: center;
+ color: $color-white;
+ background-color: $color-primary;
+ border: none;
+ transition: all 0.5s;
+ cursor: pointer;
+
+ &:hover {
+ box-shadow: 0 3px 13px rgba(23, 32, 49, 0.26);
+ }
+}
diff --git a/src/pages/CartPage/CartPage.tsx b/src/pages/CartPage/CartPage.tsx
new file mode 100644
index 0000000000..a55fae3560
--- /dev/null
+++ b/src/pages/CartPage/CartPage.tsx
@@ -0,0 +1,124 @@
+import { useContext, useState } from 'react';
+import { Context } from '../../context/Context';
+import { Loader } from '../../components/Loader/Loader';
+import { BackButton } from '../../components/BackButton/BackButton';
+import './CartPage.scss';
+import { CartItem } from '../../components/CartItem/CartItem';
+
+export const CartPage: React.FC = () => {
+ const { isLoading, cart } = useContext(Context);
+ const [isOpenModal, setIsOpenModal] = useState(false);
+ /* const [cartList, setCartList] = useState([]);
+
+ function getUniqueItems(arr: Product[], getId: (item: Product) => string) {
+ const visibleItems: Record = {};
+
+ return arr.filter((item) => {
+ const key = getId(item);
+
+ if (visibleItems[key]) {
+ return false;
+ }
+
+ visibleItems[key] = true;
+
+ return true;
+ });
+ }
+
+ const cartItems = () => {
+ const list = getUniqueItems(cart, (item: Product) => item.id);
+
+ setCartList(list);
+ };
+
+ useEffect(() => {
+ cartItems();
+ }, [cart.length]); */
+
+ /* const totalPrice = () => {
+ const currentItems = JSON.parse(localStorage.getItem('cart') || '[]');
+
+ return currentItems.reduce(
+ (sum: number, current: Product) => sum + current.price, 0,
+ );
+ }; */
+
+ const totalPrice = cart.length === 0 ? 0 : cart.map((item) => {
+ const priceAfterDiscount = item.price * ((100 - item.discount) / 100);
+ const quantity = item.quantity || 1;
+
+ return priceAfterDiscount * quantity;
+ }).reduce((a, b) => a + b, 0);
+
+ let totalItems = 0;
+
+ cart.forEach(item => {
+ const quantity = item.quantity || 1;
+
+ totalItems += quantity;
+ });
+
+ return (
+ <>
+ {isLoading
+ ?
+ : (
+
+
+
+
+
+
Cart
+ {cart.length === 0 ? (
+
Your cart is empty
+ ) : (
+ <>
+
+
+ {cart.map((product) => (
+
+ ))}
+
+
+
+
{`$${totalPrice}`}
+
{`Total for ${totalItems} items`}
+
+
+
+
+ {isOpenModal && (
+
+
+
+ We are sorry,
+ but this feature is not implemented yet
+
+
+
+
+ )}
+
+
+ >
+ )}
+
+ )}
+ >
+ );
+};
diff --git a/src/pages/FavoritesPage/FavoritesPage.scss b/src/pages/FavoritesPage/FavoritesPage.scss
new file mode 100644
index 0000000000..5189c252ca
--- /dev/null
+++ b/src/pages/FavoritesPage/FavoritesPage.scss
@@ -0,0 +1,33 @@
+@import "../../main.scss";
+
+.favourites {
+ margin-bottom: 80px;
+ &__list {
+ display: grid;
+ grid-template-columns: repeat(4, 1fr);
+ row-gap: 40px;
+ column-gap: 16px;
+ }
+
+ &__top {
+ margin: 25px 0 40px;
+ }
+
+ &__title {
+ margin-bottom: 8px;
+ }
+
+ &__empty-title {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ @extend %h2-text;
+ color: $color-secondary;
+ margin-bottom: 40px;
+ }
+
+ &__count {
+ margin-bottom: 40px;
+ color: $color-secondary;
+ }
+}
diff --git a/src/pages/FavoritesPage/FavoritesPage.tsx b/src/pages/FavoritesPage/FavoritesPage.tsx
new file mode 100644
index 0000000000..11f3f4df9a
--- /dev/null
+++ b/src/pages/FavoritesPage/FavoritesPage.tsx
@@ -0,0 +1,32 @@
+import { useContext } from 'react';
+import { Context } from '../../context/Context';
+import { ProductsNav } from '../../components/ProductsNav/ProductsNav';
+import { ProductCard } from '../../components/ProductCard/ProductCard';
+import './FavoritesPage.scss';
+
+export const FavoritesPage: React.FC = () => {
+ const { favorite } = useContext(Context);
+
+ const favoriteCount = favorite.length;
+
+ return (
+
+
+
Favourites
+
{`${favoriteCount} items`}
+ {favoriteCount === 0 ? (
+
+ There no favorite products found
+
+ ) : (
+
+ {favorite.map((product) => (
+
+ ))}
+
+ )}
+
+ );
+};
diff --git a/src/pages/HomePage/HomePage.tsx b/src/pages/HomePage/HomePage.tsx
new file mode 100644
index 0000000000..d0a8e16c51
--- /dev/null
+++ b/src/pages/HomePage/HomePage.tsx
@@ -0,0 +1,43 @@
+import { useContext } from 'react';
+import { BannerSlider } from '../../components/BannerSlider/BannerSlider';
+import { ProductsSlider } from '../../components/ProductsSlider/ProductsSlider';
+import { Product } from '../../types/Product';
+import { Context } from '../../context/Context';
+import { ShopByCategory } from '../../components/ShopByCategory/ShopByCategory';
+
+export const HomePage: React.FC = () => {
+ const { products } = useContext(Context);
+
+ const getHotPriceProducts = (items: Product[]) => {
+ return items
+ .filter(item => item.discount > 0)
+ .sort((product1, product2) => (
+ (product2.price * ((100 - product2.discount) / 100))
+ - (product1.price * ((100 - product1.discount) / 100))
+ ));
+ };
+
+ const getBrandNewProducts = (items: Product[]) => {
+ return items
+ .filter(item => item.discount === 0)
+ .sort((product1, product2) => product2.price - product1.price);
+ };
+
+ return (
+ <>
+
+
+
+
+
+
+
+ >
+ );
+};
diff --git a/src/pages/PageNotFound/PageNotFound.scss b/src/pages/PageNotFound/PageNotFound.scss
new file mode 100644
index 0000000000..c26b301b3c
--- /dev/null
+++ b/src/pages/PageNotFound/PageNotFound.scss
@@ -0,0 +1,10 @@
+@import "../../main.scss";
+
+.not-found {
+ margin-bottom: 40px;
+
+ &__title {
+ @extend %h1-text;
+ color: $color-primary;
+ }
+}
diff --git a/src/pages/PageNotFound/PageNotFound.tsx b/src/pages/PageNotFound/PageNotFound.tsx
new file mode 100644
index 0000000000..358db75c3b
--- /dev/null
+++ b/src/pages/PageNotFound/PageNotFound.tsx
@@ -0,0 +1,12 @@
+import { BackButton } from '../../components/BackButton/BackButton';
+import './PageNotFound.scss';
+
+export const PageNotFound: React.FC = () => (
+
+
+
+
+ Page not found!
+
+
+);
diff --git a/src/pages/PhonesPage/PhonesPage.tsx b/src/pages/PhonesPage/PhonesPage.tsx
new file mode 100644
index 0000000000..fd2da5b581
--- /dev/null
+++ b/src/pages/PhonesPage/PhonesPage.tsx
@@ -0,0 +1,52 @@
+import { useContext, useEffect, useState } from 'react';
+import { Context } from '../../context/Context';
+import { Loader } from '../../components/Loader/Loader';
+import { ProductsNav } from '../../components/ProductsNav/ProductsNav';
+import { ProductsList } from '../../components/ProductsList/ProductsList';
+import { Product } from '../../types/Product';
+import { getProducts } from '../../api/Products';
+import { Error } from '../../types/ErrorType';
+
+export const PhonesPage: React.FC = () => {
+ const [phones, setPhones] = useState([]);
+ const { isLoading, setIsLoading, setIsError } = useContext(Context);
+
+ const getPhones = (products: Product[], type: string) => {
+ const phonesFromApi = products.filter(
+ product => product.type === type,
+ );
+
+ setPhones(phonesFromApi);
+ };
+
+ useEffect(() => {
+ const loadPhones = async () => {
+ try {
+ setIsLoading(true);
+ const products = await getProducts();
+
+ getPhones(products, 'phone');
+ } catch (error) {
+ setIsError(Error.GET_PRODUCTS);
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ loadPhones();
+ }, []);
+
+ return (
+
+ {isLoading ? (
+
+ ) : (
+ <>
+
+
+
+ >
+ )}
+
+ );
+};
diff --git a/src/pages/ProductDetailsPage/ProductDetailsPage.scss b/src/pages/ProductDetailsPage/ProductDetailsPage.scss
new file mode 100644
index 0000000000..d2ea814826
--- /dev/null
+++ b/src/pages/ProductDetailsPage/ProductDetailsPage.scss
@@ -0,0 +1,259 @@
+@import "../../main.scss";
+
+.product-details {
+ margin-bottom: 80px;
+
+ &__title {
+ @extend %h1-text;
+ margin-bottom: 40px;
+ color: $color-primary;
+ }
+
+ &__content {
+ margin-bottom: 80px;
+ }
+
+ &__top {
+ display: grid;
+ grid-template-columns: repeat(24, 1fr);
+ column-gap: 16px;
+ margin-bottom: 80px;
+ }
+
+ &__slider {
+ grid-column: 1 / 13;
+ }
+
+ &__action {
+ grid-column: 14 / 21;
+ }
+
+ &__colors {
+ margin-bottom: 48px;
+ }
+
+ &__small-text {
+ @extend %small-text;
+ color: $color-secondary;
+ margin-bottom: 8px;
+ }
+
+ &__select-buttons {
+ display: flex;
+ gap: 8px;
+ }
+
+ &__select {
+ border-bottom: 1px solid $color-elements;
+ padding-bottom: 24px;
+ &:first-child {
+ margin-bottom: 24px;
+ }
+
+ margin-bottom: 32px;
+ }
+
+ &__prices {
+ border-bottom: 1px solid $color-elements;
+ padding-bottom: 4px;
+ margin-bottom: 16px;
+ }
+
+ &__price {
+ @extend %h2-text;
+ color: $color-primary;
+ margin-right: 8px;
+ }
+
+ &__discount {
+ @extend %h2-text;
+ color: $color-secondary;
+ text-decoration: line-through;
+ }
+
+ &__actions-button {
+ display: flex;
+ gap: 8px;
+ margin-bottom: 32px;
+ }
+
+ &__features {
+ display: flex;
+ justify-content: space-between;
+ text-transform: capitalize;
+ width: 100%;
+ }
+
+ &__list {
+ @extend %small-text;
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ list-style: none;
+ }
+
+ &__key {
+ color: $color-secondary;
+ }
+
+ &__value {
+ color: $color-primary;
+ text-align: right;
+ }
+
+ &__botom {
+ display: grid;
+ grid-template-columns: repeat(24, 1fr);
+ column-gap: 16px;
+ }
+
+ &__subtitle {
+ @extend %h2-text;
+ padding-bottom: 16px;
+ border-bottom: 1px solid $color-elements;
+ margin-bottom: 32px;
+ }
+
+ &__text {
+ display: flex;
+ flex-direction: column;
+ gap: 32px;
+ }
+
+ &__info-title {
+ @extend %h3-text;
+ padding-bottom: 16px;
+ color: $color-primary;
+ }
+
+ &__info-text {
+ color: $color-secondary;
+ }
+
+ &__description {
+ grid-column: 1 / 13;
+ }
+
+ &__tech-specs {
+ grid-column: 14 / 25;
+ }
+}
+
+.slider {
+ display: flex;
+ width: 100%;
+ height: 464px;
+ gap: 16px;
+
+ &__col {
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+ }
+
+ &__button {
+ border: 1px solid $color-icons;
+ height: 80px;
+ width: 80px;
+ background-size: contain;
+ transition: all 0.3s;
+
+ &:hover {
+ border: 1px solid $color-primary;
+ }
+
+ &--active {
+ border: 1px solid $color-primary;
+ }
+ }
+
+ &__main {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ flex: 1 1 auto;
+ background-size: contain;
+ background-repeat: no-repeat;
+ background-position: center;
+ transition: 0.2s ease;
+ }
+}
+
+.colors {
+ list-style: none;
+
+ &__item-circle {
+ width: 32px;
+ height: 32px;
+ border: 1px solid $color-elements;
+ border-radius: 36px;
+ cursor: pointer;
+ transition: all 0.3s;
+
+ &--is-active {
+ border-color: $color-primary;
+ }
+
+ &:hover {
+ border-color: $color-primary;
+ }
+ }
+
+ &__item {
+ width: 28px;
+ height: 28px;
+ border: 2px solid $color-white;
+ border-radius: 36px;
+
+ &--pink {
+ background-color: #fcdbc1;
+ }
+
+ &--grey {
+ background-color: #5f7170;
+ }
+
+ &--black {
+ background-color: #4c4c4c;
+ }
+
+ &--white {
+ background-color: #f0f0f0;
+ }
+
+ }
+}
+
+.capacity {
+ &__list {
+ display: flex;
+ gap: 8px;
+ margin-bottom: 32px;
+ padding-bottom: 24px;
+
+ border-bottom: 1px solid $color-elements;
+ list-style-type: none;
+ }
+
+ &__item {
+ padding: 8px;
+ font-weight: 500;
+ font-size: 14px;
+ line-height: 21px;
+ color: $color-primary;
+ background-color: $color-white;
+ border: 1px solid $color-icons;
+
+ transition: all 0.3s;
+ cursor: pointer;
+
+ &:hover {
+ border-color: $color-primary;
+ }
+
+ &--is-active {
+ color: $color-white;
+ background-color: $color-primary;
+ }
+ }
+}
diff --git a/src/pages/ProductDetailsPage/ProductDetailsPage.tsx b/src/pages/ProductDetailsPage/ProductDetailsPage.tsx
new file mode 100644
index 0000000000..bf82dbf6e2
--- /dev/null
+++ b/src/pages/ProductDetailsPage/ProductDetailsPage.tsx
@@ -0,0 +1,293 @@
+import {
+ useContext, useEffect, useMemo, useState,
+} from 'react';
+import { useLocation, useParams } from 'react-router-dom';
+import classNames from 'classnames';
+import { Loader } from '../../components/Loader/Loader';
+import { ProductsNav } from '../../components/ProductsNav/ProductsNav';
+import { Context } from '../../context/Context';
+import { ProductsSlider } from '../../components/ProductsSlider/ProductsSlider';
+import CategoriesList from '../../api/CategoriesList.json';
+import { Product } from '../../types/Product';
+import { BackButton } from '../../components/BackButton/BackButton';
+import {
+ AddToCartButton,
+} from '../../components/AddToCartButton/AddToCartButton';
+import { LikeButton } from '../../components/LikeButton/LikeButton';
+import { getProductDetails } from '../../api/Products';
+import { Error } from '../../types/ErrorType';
+import './ProductDetailsPage.scss';
+
+const colorsList = ['pink', 'grey', 'black', 'white'];
+const capacityList = [16, 256, 512];
+
+export const ProductDetailsPage: React.FC = () => {
+ const {
+ isLoading, products, setIsError, setIsLoading,
+ selectedProduct, setSelectedProduct,
+ } = useContext(Context);
+ const { productId = '' } = useParams();
+ const location = useLocation();
+ // const [productInfo, setProductInfo] = useState();
+ const [selectedImage, setSelectedImage] = useState(
+ selectedProduct?.images[0],
+ );
+ const [selectedColor, setSelectedColor] = useState(colorsList[0]);
+ const [selectedCapacity, setSelectedCapacity] = useState(capacityList[0]);
+
+ const priceWithDiscount = (product: Product) => {
+ return product.price - (product.discount * product.price) / 100;
+ };
+
+ const product = products.find(
+ item => item.id === selectedProduct?.id,
+ );
+
+ const newPrice = product ? priceWithDiscount(product) : 0;
+
+ const featuresShort = {
+ keys: ['screen', 'resolution', 'processor', 'RAM'],
+ values: [
+ `${selectedProduct?.display.screenSize}`,
+ `${selectedProduct?.display.screenResolution}`,
+ `${selectedProduct?.hardware.cpu}`,
+ `${selectedProduct?.storage.ram}`],
+ };
+
+ const featuresLong = {
+ keys: ['screen', 'resolution', 'processor', 'RAM', 'built in memory',
+ 'android', 'bluetooth', 'battery'],
+ values: [
+ `${selectedProduct?.display.screenSize}`,
+ `${selectedProduct?.display.screenResolution}`,
+ `${selectedProduct?.hardware.cpu}`,
+ `${selectedProduct?.storage.ram}`,
+ `${selectedProduct?.storage.flash}`,
+ `${selectedProduct?.android.os}`,
+ `${selectedProduct?.connectivity.bluetooth}`,
+ `${selectedProduct?.battery.standbyTime}`,
+ ],
+ };
+
+ const currentCategory = location.pathname.split('/').slice(1)[0];
+
+ const productCategory = useMemo(() => {
+ const list = CategoriesList.filter(
+ item => item.type === currentCategory,
+ )[0];
+
+ return list.itemType;
+ }, [location]);
+
+ const getRecommendations = (
+ items: Product[], category: string, currentId: string,
+ ) => {
+ return items
+ .filter(item => (item.type === category)
+ && (currentId !== item.id))
+ .sort((product1, product2) => (
+ product2.price - product1.price
+ ));
+ };
+
+ const getProductInfo = async (id: string) => {
+ setIsError(null);
+
+ try {
+ const item = await getProductDetails(id);
+
+ setSelectedProduct(item);
+ } catch {
+ setIsError(Error.GET_PRODUCT_DETAILS);
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ useEffect(() => {
+ if (!selectedProduct && productId.length > 0) {
+ getProductInfo(productId);
+ }
+ }, [productId]);
+
+ return (
+
+ {isLoading ? (
+
+ ) : (
+
+
+
+
{selectedProduct?.name}
+
+
+
+
+
+ {selectedProduct?.images.map((image) => (
+
+
+
+
+
+
+
+ Available colors
+
+
+
+
+
+
+ Select capacity
+
+
+ {capacityList.map(el => (
+
+ ))}
+
+
+
+
+ {`$${product?.price}`}
+ {product?.discount !== 0
+ && (
+
+ {`$${newPrice}`}
+
+
+ )}
+
+
+ {product
+ && (
+
+ )}
+
+
+
+ {featuresShort.keys.map((key) => (
+ -
+ {key}
+
+ ))}
+
+
+
+ {featuresShort.values.map((value) => (
+ -
+ {value}
+
+ ))}
+
+
+
+
+
+
+ {selectedProduct && (
+
+
About
+
+
+ {selectedProduct.description}
+
+
+ )}
+
+
+
Tech specs
+
+
+ {featuresLong.keys.map((key) => (
+ -
+ {key}
+
+ ))}
+
+
+
+ {featuresLong.values.map((value) => (
+ -
+ {value}
+
+ ))}
+
+
+
+
+
+
+
+
+ )}
+
+ );
+};
diff --git a/src/pages/TabletsPage/TabletsPage.tsx b/src/pages/TabletsPage/TabletsPage.tsx
new file mode 100644
index 0000000000..6080356350
--- /dev/null
+++ b/src/pages/TabletsPage/TabletsPage.tsx
@@ -0,0 +1,52 @@
+import { useContext, useEffect, useState } from 'react';
+import { Context } from '../../context/Context';
+import { Loader } from '../../components/Loader/Loader';
+import { ProductsNav } from '../../components/ProductsNav/ProductsNav';
+import { ProductsList } from '../../components/ProductsList/ProductsList';
+import { Product } from '../../types/Product';
+import { getProducts } from '../../api/Products';
+import { Error } from '../../types/ErrorType';
+
+export const TabletsPage: React.FC = () => {
+ const [tablets, setTablets] = useState([]);
+ const { isLoading, setIsLoading, setIsError } = useContext(Context);
+
+ const getTablets = (products: Product[], type: string) => {
+ const tabletsFromApi = products.filter(
+ product => product.type === type,
+ );
+
+ setTablets(tabletsFromApi);
+ };
+
+ useEffect(() => {
+ const loadPhones = async () => {
+ try {
+ setIsLoading(true);
+ const products = await getProducts();
+
+ getTablets(products, 'tablet');
+ } catch (error) {
+ setIsError(Error.GET_PRODUCTS);
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ loadPhones();
+ }, []);
+
+ return (
+
+ {isLoading ? (
+
+ ) : (
+ <>
+
+
+
+ >
+ )}
+
+ );
+};
diff --git a/src/types/ButtonType.ts b/src/types/ButtonType.ts
new file mode 100644
index 0000000000..064c1e4137
--- /dev/null
+++ b/src/types/ButtonType.ts
@@ -0,0 +1 @@
+export type ButtonType = 'big' | 'small';
diff --git a/src/types/ErrorType.ts b/src/types/ErrorType.ts
new file mode 100644
index 0000000000..eb9728510e
--- /dev/null
+++ b/src/types/ErrorType.ts
@@ -0,0 +1,5 @@
+export enum Error {
+ PAGE_NOT_FOUND = 'Page not found',
+ GET_PRODUCTS = 'Unable to get the products',
+ GET_PRODUCT_DETAILS = 'Unable to get the product\'s details',
+}
diff --git a/src/types/IconType.ts b/src/types/IconType.ts
new file mode 100644
index 0000000000..df08d2ea00
--- /dev/null
+++ b/src/types/IconType.ts
@@ -0,0 +1,22 @@
+export enum IconType {
+ ARROW_DOWN = 'icon__arrow-down',
+ ARROW_DOWN_DISABLED = 'icon__arrow-down--disabled',
+ ARROW_LEFT = 'icon__arrow-left',
+ ARROW_LEFT_DISABLED = 'icon__arrow-left--disabled',
+ ARROW_RIGHT = 'icon__arrow-right',
+ ARROW_RIGHT_DISABLED = 'icon__arrow-right--disabled',
+ ARROW_UP = 'icon__arrow-up',
+ ARROW_UP_DISABLED = 'icon__arrow-up--disabled',
+ CART = 'icon__cart',
+ CLOSE = 'icon__close',
+ CLOSE_DISABLED = 'icon__close--disabled',
+ CROSS = 'icon__cross',
+ FAVORITE = 'icon__favorite',
+ FAVORITE_FILLED = 'icon__favorite--active',
+ HOME = 'icon__home',
+ MENU_BURGER = 'icon__menu-burger',
+ MINUS = 'icon__minus',
+ MINUS_DISABLED = 'icon__minus--disabled',
+ PLUS = 'icon__plus',
+ SEARCH = 'icon__search',
+}
diff --git a/src/types/Product.ts b/src/types/Product.ts
new file mode 100644
index 0000000000..6659cc75a9
--- /dev/null
+++ b/src/types/Product.ts
@@ -0,0 +1,14 @@
+export interface Product {
+ age: number,
+ id: string,
+ type: string,
+ imageUrl: string,
+ name: string,
+ snippet: string,
+ price: number,
+ discount: number,
+ screen: string,
+ capacity: string,
+ ram: string,
+ quantity?: number,
+}
diff --git a/src/types/ProductDetails.ts b/src/types/ProductDetails.ts
new file mode 100644
index 0000000000..7a133b202d
--- /dev/null
+++ b/src/types/ProductDetails.ts
@@ -0,0 +1,49 @@
+export interface ProductDetails {
+ additionalFeatures: string,
+ android: {
+ os: string,
+ ui: string,
+ },
+ availability: string[],
+ battery: {
+ standbyTime: string,
+ talkTime: string,
+ type: string
+ },
+ camera: {
+ features: string[],
+ primary?: string
+ },
+ connectivity: {
+ bluetooth: string,
+ cell: string,
+ gps: boolean,
+ infrared: boolean,
+ wifi: string
+ },
+ description: string,
+ display: {
+ screenResolution: string,
+ screenSize: string,
+ touchScreen: boolean
+ },
+ hardware: {
+ accelerometer: boolean,
+ audioJack: string,
+ cpu: string,
+ fmRadio: boolean,
+ physicalKeyboard: boolean,
+ usb: string
+ },
+ id: string,
+ images: string[],
+ name: string,
+ sizeAndWeight: {
+ dimensions: string[],
+ weight: string
+ },
+ storage: {
+ flash: string,
+ ram: string
+ }
+}
diff --git a/src/types/SortType.ts b/src/types/SortType.ts
new file mode 100644
index 0000000000..8ca98468ef
--- /dev/null
+++ b/src/types/SortType.ts
@@ -0,0 +1,8 @@
+export enum SortType {
+ Newest = 'newest',
+ Alphabetically = 'alphabetically',
+ Price = 'price',
+ Year = 'year',
+ MaxDiscount = 'maxDiscount',
+ Random = 'random',
+}
diff --git a/src/utils/_extends.scss b/src/utils/_extends.scss
new file mode 100644
index 0000000000..7f9ad33918
--- /dev/null
+++ b/src/utils/_extends.scss
@@ -0,0 +1,48 @@
+%h1-text {
+ font-family: inherit;
+ font-weight: 700;
+ font-size: 32px;
+ line-height: 41px;
+ letter-spacing: -0.01em;
+}
+
+%h2-text {
+ font-family: inherit;
+ font-weight: 700;
+ font-size: 22px;
+ line-height: 31px;
+ letter-spacing: 0;
+}
+
+%h3-text {
+ font-family: inherit;
+ font-weight: 600;
+ font-size: 20px;
+ line-height: 26px;
+ letter-spacing: 0;
+}
+
+%uppercase-text {
+ font-family: inherit;
+ font-weight: 700;
+ font-size: 12px;
+ line-height: 11px;
+ letter-spacing: 0.04em;
+ text-transform: uppercase;
+}
+
+%buttons-text {
+ font-family: inherit;
+ font-weight: 600;
+ font-size: 14px;
+ line-height: 21px;
+ letter-spacing: 0;
+}
+
+%small-text {
+ font-family: inherit;
+ font-weight: 600;
+ font-size: 12px;
+ line-height: 15px;
+ letter-spacing: 0;
+}
diff --git a/src/utils/_mixins.scss b/src/utils/_mixins.scss
new file mode 100644
index 0000000000..b9032dca38
--- /dev/null
+++ b/src/utils/_mixins.scss
@@ -0,0 +1,4 @@
+@mixin squareSize($size) {
+ width: $size;
+ height: $size;
+}
diff --git a/src/utils/_variables.scss b/src/utils/_variables.scss
new file mode 100644
index 0000000000..8647d4386e
--- /dev/null
+++ b/src/utils/_variables.scss
@@ -0,0 +1,8 @@
+$color-primary: #313237;
+$color-secondary: #89939a;
+$color-icons: #b4bdc3;
+$color-elements: #e2e6e9;
+$color-background: #fafbfc;
+$color-white: #fff;
+$color-green: #27ae60;
+$color-red: #eb5757;
diff --git a/src/utils/fetchClient.ts b/src/utils/fetchClient.ts
new file mode 100644
index 0000000000..fb828e82ff
--- /dev/null
+++ b/src/utils/fetchClient.ts
@@ -0,0 +1,41 @@
+const BASE_URL = 'https://mate-academy.github.io/react_phone-catalog/api';
+
+function wait(delay: number) {
+ return new Promise(resolve => {
+ setTimeout(resolve, delay);
+ });
+}
+
+type RequestMethod = 'GET' | 'POST' | 'PATCH' | 'DELETE';
+
+function request(
+ url: string,
+ method: RequestMethod = 'GET',
+ data: unknown = null,
+): Promise {
+ const options: RequestInit = { method };
+
+ if (data) {
+ options.body = JSON.stringify(data);
+ options.headers = {
+ 'Content-Type': 'application/json; charset=UTF-8',
+ };
+ }
+
+ return wait(300)
+ .then(() => fetch(BASE_URL + url, options))
+ .then(response => {
+ if (!response.ok) {
+ throw new Error();
+ }
+
+ return response.json();
+ });
+}
+
+export const client = {
+ get: (url: string) => request(url),
+ post: (url: string, data: unknown) => request(url, 'POST', data),
+ patch: (url: string, data: unknown) => request(url, 'PATCH', data),
+ delete: (url: string) => request(url, 'DELETE'),
+};
diff --git a/src/utils/getProductToSave.ts b/src/utils/getProductToSave.ts
new file mode 100644
index 0000000000..93f0bcafcb
--- /dev/null
+++ b/src/utils/getProductToSave.ts
@@ -0,0 +1,49 @@
+import { Product } from '../types/Product';
+
+export const getProductToSave = (
+ key: string,
+ savedProducts: Product[],
+ saveToState: React.Dispatch>,
+) => {
+ return (newProduct: Product) => {
+ let newProducts: Product[];
+ const copyOfProducts = [...savedProducts];
+ const savedProduct = savedProducts.find(
+ item => item.id === newProduct.id,
+ );
+ const savedProductIndex = savedProduct
+ ? savedProducts.indexOf(savedProduct)
+ : -1;
+ const isChangingQuantity = (
+ savedProduct?.quantity !== newProduct.quantity
+ ) && newProduct?.quantity;
+
+ if (isChangingQuantity && savedProductIndex >= 0) {
+ copyOfProducts[savedProductIndex].quantity = newProduct.quantity;
+ }
+
+ if (savedProduct) {
+ newProducts = copyOfProducts.filter(
+ (product) => {
+ if (isChangingQuantity) {
+ return true;
+ }
+
+ return product.id !== newProduct.id;
+ },
+ );
+ } else {
+ const useProduct = key === 'cart'
+ ? { ...newProduct, quantity: 1 }
+ : { ...newProduct };
+
+ newProducts = [
+ ...savedProducts,
+ useProduct,
+ ];
+ }
+
+ saveToState(newProducts);
+ localStorage.setItem(key, JSON.stringify(newProducts));
+ };
+};
diff --git a/src/utils/searchHelper.ts b/src/utils/searchHelper.ts
new file mode 100644
index 0000000000..42d8db56d5
--- /dev/null
+++ b/src/utils/searchHelper.ts
@@ -0,0 +1,47 @@
+export type SearchParams = {
+ [key: string]: string | string[] | null,
+};
+
+/**
+ * This function prepares a correct search string
+ * from a given currentParams and paramsToUpdate.
+ */
+export function getSearchWith(
+ currentParams: URLSearchParams,
+ paramsToUpdate: SearchParams, // it's our custom type
+): string {
+ // copy currentParams by creating new object from a string
+ const newParams = new URLSearchParams(
+ currentParams.toString(),
+ );
+
+ // Here is the example of paramsToUpdate
+ // {
+ // sex: 'm', ['sex', 'm']
+ // order: null, ['order', null]
+ // centuries: ['16', '19'], ['centuries', ['16', '19']]
+ // }
+ //
+ // - params with the `null` value are deleted;
+ // - string value is set to given param key;
+ // - array of strings adds several params with the same key;
+
+ Object.entries(paramsToUpdate)
+ .forEach(([key, value]) => {
+ if (value === null) {
+ newParams.delete(key);
+ } else if (Array.isArray(value)) {
+ // we delete the key to remove old values
+ newParams.delete(key);
+
+ value.forEach(part => {
+ newParams.append(key, part);
+ });
+ } else {
+ newParams.set(key, value);
+ }
+ });
+
+ // we return a string to use it inside links
+ return newParams.toString();
+}
diff --git a/src/utils/useLocaleStorage.ts b/src/utils/useLocaleStorage.ts
new file mode 100644
index 0000000000..4fd098576c
--- /dev/null
+++ b/src/utils/useLocaleStorage.ts
@@ -0,0 +1,31 @@
+import { useState } from 'react';
+import { Product } from '../types/Product';
+
+export const useLocalStorage = (key: string, initialValue: Product[]) => {
+ const valueFromStorage = localStorage.getItem(key) || '[]';
+
+ const [value, setValue] = useState(
+ valueFromStorage ? JSON.parse(valueFromStorage) : initialValue,
+ );
+
+ return [value, setValue];
+};
+
+/* export const useLocalStorage = (key: string, initialValue: Product[]) => {
+ const [value, setValue] = useState(() => {
+ try {
+ const data = localStorage.getItem(key);
+
+ return data ? JSON.parse(data) : initialValue;
+ } catch {
+ return initialValue;
+ }
+ });
+
+ const save = (currentValue: Product) => {
+ setValue(currentValue);
+ localStorage.setItem(key, JSON.stringify(currentValue));
+ };
+
+ return [value, save];
+}; */