diff --git a/cypress.config.ts b/cypress.config.ts
index 6aa317d0101..3f52f1248e9 100644
--- a/cypress.config.ts
+++ b/cypress.config.ts
@@ -1,9 +1,10 @@
-const { defineConfig } = require('cypress');
+/* eslint-disable */
+import { defineConfig } from 'cypress';
-module.exports = defineConfig({
+export default defineConfig({
e2e: {
baseUrl: 'http://localhost:3000',
- specPattern: 'cypress/integration/**/*.spec.{js,ts,jsx,tsx}',
+ specPattern: 'cypress/integration/**/*.{cy,spec}.{js,ts,jsx,tsx}',
},
video: true,
viewportHeight: 1920,
diff --git a/cypress/tsconfig.json b/cypress/tsconfig.json
index 0211a30742d..c44713bd6b1 100644
--- a/cypress/tsconfig.json
+++ b/cypress/tsconfig.json
@@ -1,6 +1,7 @@
{
"extends": "@mate-academy/students-ts-config",
"compilerOptions": {
- "sourceMap": false
+ "sourceMap": false,
+ "baseUrl": "."
}
}
diff --git a/index.html b/index.html
index 095fb3a4537..0f5ac63bf4f 100644
--- a/index.html
+++ b/index.html
@@ -3,9 +3,10 @@
- Vite + React + TS
+ Welcome to Nice Gadgets store!
+
-
+
diff --git a/package-lock.json b/package-lock.json
index 836b9e63b46..b65cac4c40e 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -16,11 +16,12 @@
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-router-dom": "^6.25.1",
- "react-transition-group": "^4.4.5"
+ "react-transition-group": "^4.4.5",
+ "swiper": "^12.1.4"
},
"devDependencies": {
"@cypress/react18": "^2.0.1",
- "@mate-academy/scripts": "^1.8.5",
+ "@mate-academy/scripts": "^2.1.3",
"@mate-academy/students-ts-config": "*",
"@mate-academy/stylelint-config": "*",
"@types/node": "^20.14.10",
@@ -1184,10 +1185,11 @@
}
},
"node_modules/@mate-academy/scripts": {
- "version": "1.8.5",
- "resolved": "https://registry.npmjs.org/@mate-academy/scripts/-/scripts-1.8.5.tgz",
- "integrity": "sha512-mHRY2FkuoYCf5U0ahIukkaRo5LSZsxrTSgMJheFoyf3VXsTvfM9OfWcZIDIDB521kdPrScHHnRp+JRNjCfUO5A==",
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/@mate-academy/scripts/-/scripts-2.1.3.tgz",
+ "integrity": "sha512-a07wHTj/1QUK2Aac5zHad+sGw4rIvcNl5lJmJpAD7OxeSbnCdyI6RXUHwXhjF5MaVo9YHrJ0xVahyERS2IIyBQ==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"@octokit/rest": "^17.11.2",
"@types/get-port": "^4.2.0",
@@ -9930,6 +9932,25 @@
"integrity": "sha512-ovssysQTa+luh7A5Weu3Rta6FJlFBBbInjOh722LIt6klpU2/HtdUbszju/G4devcvk8PGt7FCLv5wftu3THUA==",
"dev": true
},
+ "node_modules/swiper": {
+ "version": "12.1.4",
+ "resolved": "https://registry.npmjs.org/swiper/-/swiper-12.1.4.tgz",
+ "integrity": "sha512-bihiwoKMOQwW8FfdUbo1DgkVH25E+4ZELIq0oopL1KTKBteLuaTMi/wwFjMxtlhTkk45k3XQ89D1Fvv0spSqBA==",
+ "funding": [
+ {
+ "type": "custom",
+ "url": "https://sponsors.nolimits4web.com"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/nolimits4web"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4.7.0"
+ }
+ },
"node_modules/synckit": {
"version": "0.8.8",
"resolved": "https://registry.npmjs.org/synckit/-/synckit-0.8.8.tgz",
diff --git a/package.json b/package.json
index ae251685c8b..48bb48bab1f 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "react_phone-catalog",
- "homepage": "react_phone-catalog",
+ "homepage": "https://u5135039754-dev.github.io/react_phone-catalog/",
"version": "0.1.0",
"keywords": [],
"author": "Mate Academy",
@@ -12,11 +12,12 @@
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-router-dom": "^6.25.1",
- "react-transition-group": "^4.4.5"
+ "react-transition-group": "^4.4.5",
+ "swiper": "^12.1.4"
},
"devDependencies": {
"@cypress/react18": "^2.0.1",
- "@mate-academy/scripts": "^1.8.5",
+ "@mate-academy/scripts": "^2.1.3",
"@mate-academy/students-ts-config": "*",
"@mate-academy/stylelint-config": "*",
"@types/node": "^20.14.10",
diff --git a/public/api/accessories.json b/public/api/accessories.json
index 6a36890cec7..eba8266c643 100644
--- a/public/api/accessories.json
+++ b/public/api/accessories.json
@@ -1428,3 +1428,5 @@
"cell": ["Wi-Fi", "Bluetooth", "LTE"]
}
]
+
+
diff --git a/public/api/phones.json b/public/api/phones.json
index e1535893222..fb045b2009c 100644
--- a/public/api/phones.json
+++ b/public/api/phones.json
@@ -5833,3 +5833,5 @@
"cell": ["GPRS", "EDGE", "WCDMA", "UMTS", "HSPA", "LTE", "5G"]
}
]
+
+
diff --git a/public/api/products.json b/public/api/products.json
index 41ecb5e5a6f..2d12cf45257 100644
--- a/public/api/products.json
+++ b/public/api/products.json
@@ -2715,4 +2715,5 @@
"year": 2022,
"image": "img/phones/apple-iphone-14-pro/gold/00.webp"
}
-]
\ No newline at end of file
+]
+
diff --git a/public/api/tablets.json b/public/api/tablets.json
index dc555396f72..3e7f68806f0 100644
--- a/public/api/tablets.json
+++ b/public/api/tablets.json
@@ -1700,3 +1700,5 @@
"cell": ["Not applicable"]
}
]
+
+
diff --git a/setup.md b/setup.md
index db99c0a748e..7a3835e07b0 100644
--- a/setup.md
+++ b/setup.md
@@ -155,7 +155,7 @@ jobs:
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
with:
- node-version: "12.x"
+ node-version: [20.x]
- run: npm ci
- run: npm run build
- name: Deploy
diff --git a/src/App.scss b/src/App.scss
index 71bc413aade..28d13f89768 100644
--- a/src/App.scss
+++ b/src/App.scss
@@ -1 +1,52 @@
-// not empty
+@import './Components/Home/Home';
+@import './Components/TopBar/top-bar';
+@import './Components/TopBar/nav';
+@import './utils/vars';
+@import './utils/mixins';
+@import './fonts/fonts';
+@import './Components/Header/header';
+@import './styles/section';
+@import './styles/WelcomeBlock';
+@import './styles/icon';
+@import './styles/categories';
+@import './styles/Page';
+@import './Components/Footer/Footer';
+@import './Components/ErrorNotification/ErrorNotification';
+@import './styles/newModels';
+@import './styles/Aside';
+
+
+html,
+body {
+ width: 100%;
+ min-height: 100%;
+ overflow-x: hidden;
+}
+
+html {
+ box-sizing: border-box;
+}
+
+*, *::before, *::after {
+ box-sizing: inherit;
+ min-width: 0;
+}
+
+img,
+svg,
+button,
+a {
+ max-width: 100%;
+}
+
+body {
+ margin: 0;
+ min-height: 100vh;
+ background-color: #0F1121;
+ color: #F1F2F9;
+ font-family: Mont, sans-serif;
+}
+
+.container {
+ @include content-padding-inline;
+}
diff --git a/src/App.tsx b/src/App.tsx
index 372e4b42066..979ae650c88 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -1,7 +1,10 @@
-import './App.scss';
+import { Outlet } from 'react-router-dom';
+import React from 'react';
-export const App = () => (
-
-
Product Catalog
-
-);
+export const App = () => {
+ return (
+
+
+
+ );
+};
diff --git a/src/Components/Accessories/Accessories.scss b/src/Components/Accessories/Accessories.scss
new file mode 100644
index 00000000000..08dae30b633
--- /dev/null
+++ b/src/Components/Accessories/Accessories.scss
@@ -0,0 +1,21 @@
+.accessories {
+ &__path {
+ display: flex;
+
+ gap: 8px;
+
+ align-items: center;
+
+ margin-top: 25px;
+
+
+ &-phones {
+ font-family: "Mont SemiBold", sans-serif;
+
+ color: #75767F;
+ font-weight: 700;
+ font-size: 12px;
+ line-height: 100%;
+ }
+ }
+}
diff --git a/src/Components/Accessories/Accessories.tsx b/src/Components/Accessories/Accessories.tsx
new file mode 100644
index 00000000000..c1bcb30a80b
--- /dev/null
+++ b/src/Components/Accessories/Accessories.tsx
@@ -0,0 +1,207 @@
+/* eslint-disable max-len */
+import { Link } from 'react-router-dom';
+import { Filter, FilterValue, ItemQuantity } from '../Filter/Filter';
+import { Header } from '../Header/header';
+import ArrowGray from '../../images/icons/ChevronGray.svg';
+import Home from '../../images/icons/Home.svg';
+import './Accessories.scss';
+import { getAccessories } from '../../api/api';
+import { useEffect, useState } from 'react';
+import { Accessorie } from '../../types/Accessories';
+import { Products } from '../../types/Products';
+import { useCart } from '../../Context/Context';
+import { useFav } from '../../Context/FavouritesContext';
+import React from 'react';
+import { ActiveQuantity16 } from '../ActiveQuantity/ActiveQuantityAcc/ActiveQuantity16';
+import { PageSliderAcc } from '../Page__Slider/PageSliderAcc';
+import { ActiveQuantity32 } from '../ActiveQuantity/ActiveQuantityAcc/ActiveQuantity32';
+import { ActiveQuantity64 } from '../ActiveQuantity/ActiveQuantityAcc/ActiveQuantity64';
+import { Footer } from '../Footer/Footer';
+import { Aside } from '../Aside/Aside';
+import { Loader } from '../Loader/Loader';
+
+export const Accessories = () => {
+ const [menuOpen, setMenuOpen] = useState(false);
+ const [accessories, setAccessories] = useState([]);
+ const [Loading, setLoading] = useState(true);
+ const [, setErrorMessage] = useState('');
+ const { totalQuantity } = useCart();
+ const { totalFavourites } = useFav();
+ const [activeQuantity, setActiveQuantity] = useState(16);
+ const [activeFilter, setActiveFilter] = useState('Newest');
+ const [activePage, setActivePage] = useState(0);
+ const mapAccessorieToProduct = (accessorie: Accessorie): Products => ({
+ id: String(accessorie.id),
+ itemId: String(accessorie.id),
+ name: accessorie.name,
+ category: accessorie.category,
+ fullPrice: Number(accessorie.priceRegular),
+ price: Number(accessorie.priceDiscount || accessorie.priceRegular),
+ screen: accessorie.screen,
+ capacity: accessorie.capacity,
+ color: accessorie.color || accessorie.colorsAvailable?.[0] || '—',
+ ram: accessorie.ram,
+ year: new Date().getFullYear(),
+ image:
+ typeof accessorie.images === 'string'
+ ? accessorie.images
+ : accessorie.images?.[0],
+ });
+
+ const getAccessoryOrder = (name: string) => {
+ if (name.includes('XS')) {
+ return 10.2;
+ }
+
+ if (name.includes('XR')) {
+ return 10.1;
+ }
+
+ if (name.includes('X')) {
+ return 10;
+ }
+
+ const match = name.match(/\d+/);
+
+ return match ? parseInt(match[0], 10) : 0;
+ };
+
+ const sortedAcc = [...accessories].sort((a, b) => {
+ return getAccessoryOrder(b.name) - getAccessoryOrder(a.name);
+ });
+
+ const sortedAccLowToHigh =
+ activeFilter === 'Price: Low to High'
+ ? [...accessories].sort((a, b) => a.priceRegular - b.priceRegular)
+ : accessories;
+
+ const sortedAccHighToLow =
+ activeFilter === 'Price: High to Low'
+ ? [...accessories].sort((a, b) => b.priceRegular - a.priceRegular)
+ : accessories;
+
+ useEffect(() => {
+ setLoading(true);
+ setErrorMessage('');
+
+ getAccessories()
+ .then(setAccessories)
+ .catch(() => setErrorMessage(`Couldn't load any accessories`))
+ .finally(() => setLoading(false));
+ }, []);
+
+ return (
+
+
+ {menuOpen && (
+
+ )}
+ {Loading ? (
+
+ ) : (
+
+
+
+
+
+
+
+
+
+ Accessories
+
+
+
+
Accessories
+
+ 100 models
+
+
+
+
+
+ {activeQuantity === 16 && (
+
+ )}
+ {activeQuantity === 32 && (
+
+ )}
+ {activeQuantity === 64 && (
+
+ )}
+
+
+
+
+ )}
+
+
+
+
+
+ );
+};
diff --git a/src/Components/ActiveQuantity/ActiveQuantity16.tsx b/src/Components/ActiveQuantity/ActiveQuantity16.tsx
new file mode 100644
index 00000000000..9f4df3521ed
--- /dev/null
+++ b/src/Components/ActiveQuantity/ActiveQuantity16.tsx
@@ -0,0 +1,163 @@
+import React from 'react';
+import { ProductCard } from '../ProductCard/ProductCard';
+import { Phone } from '../../types/Phone';
+import { Products } from '../../types/Products';
+import { Link } from 'react-router-dom';
+
+type ActiveQuantity16Props = {
+ activeQuantity: number;
+ activePage: number;
+ phones: Phone[];
+ mapPhoneToProduct: (p: Phone) => Products;
+};
+
+export const ActiveQuantity16: React.FC = ({
+ activePage,
+ phones,
+ mapPhoneToProduct,
+}) => {
+ return (
+
+ {activePage === 0 &&
+ phones.slice(0, 16).map(phone => (
+
+
+
+
+
+
{phone.name}
+
{phone.priceRegular}$
+
+
+
+ Screen{' '}
+ {phone.screen}
+
+
+ Capacity{' '}
+ {phone.capacity}
+
+
+ RAM {phone.ram}
+
+
+
+
+
+ ))}
+ {activePage === 1 &&
+ phones.slice(16, 32).map(phone => (
+
+
+
+
+
+
{phone.name}
+
{phone.priceRegular}$
+
+
+
+ Screen{' '}
+ {phone.screen}
+
+
+ Capacity{' '}
+ {phone.capacity}
+
+
+ RAM {phone.ram}
+
+
+
+
+
+ ))}
+ {activePage === 2 &&
+ phones.slice(32, 48).map(phone => (
+
+
+
+
+
+
{phone.name}
+
{phone.priceRegular}$
+
+
+
+ Screen{' '}
+ {phone.screen}
+
+
+ Capacity{' '}
+ {phone.capacity}
+
+
+ RAM {phone.ram}
+
+
+
+
+
+ ))}
+ {activePage === 3 &&
+ phones.slice(48, 64).map(phone => (
+
+
+
+
+
+
{phone.name}
+
{phone.priceRegular}$
+
+
+
+ Screen{' '}
+ {phone.screen}
+
+
+ Capacity{' '}
+ {phone.capacity}
+
+
+ RAM {phone.ram}
+
+
+
+
+
+ ))}
+
+ );
+};
diff --git a/src/Components/ActiveQuantity/ActiveQuantity32.tsx b/src/Components/ActiveQuantity/ActiveQuantity32.tsx
new file mode 100644
index 00000000000..e9110df169d
--- /dev/null
+++ b/src/Components/ActiveQuantity/ActiveQuantity32.tsx
@@ -0,0 +1,146 @@
+import React from 'react';
+import { ProductCard } from '../ProductCard/ProductCard';
+import { Phone } from '../../types/Phone';
+import { Products } from '../../types/Products';
+
+type ActiveQuantity32Props = {
+ activeQuantity: number;
+ activePage: number;
+ phones: Phone[];
+ mapPhoneToProduct: (p: Phone) => Products;
+};
+
+export const ActiveQuantity32: React.FC = ({
+ phones,
+ mapPhoneToProduct,
+ activePage,
+}: ActiveQuantity32Props) => {
+ return (
+
+ {activePage === 0 &&
+ phones.slice(0, 32).map(phone => (
+
+
+
+
+
+
{phone.name}
+
{phone.priceRegular}$
+
+
+
+ Screen{' '}
+ {phone.screen}
+
+
+ Capacity{' '}
+ {phone.capacity}
+
+
+ RAM {phone.ram}
+
+
+
+
+
+ ))}
+ {activePage === 1 &&
+ phones.slice(32, 64).map(phone => (
+
+
+
+
+
+
{phone.name}
+
{phone.priceRegular}$
+
+
+
+ Screen{' '}
+ {phone.screen}
+
+
+ Capacity{' '}
+ {phone.capacity}
+
+
+ RAM {phone.ram}
+
+
+
+
+
+ ))}
+ {activePage === 2 &&
+ phones.slice(64, 96).map(phone => (
+
+
+
+
+
+
{phone.name}
+
{phone.priceRegular}$
+
+
+
+ Screen{' '}
+ {phone.screen}
+
+
+ Capacity{' '}
+ {phone.capacity}
+
+
+ RAM {phone.ram}
+
+
+
+
+
+ ))}
+ {activePage === 3 &&
+ phones.slice(96, 128).map(phone => (
+
+
+
+
+
+
{phone.name}
+
{phone.priceRegular}$
+
+
+
+ Screen{' '}
+ {phone.screen}
+
+
+ Capacity{' '}
+ {phone.capacity}
+
+
+ RAM {phone.ram}
+
+
+
+
+
+ ))}
+
+ );
+};
diff --git a/src/Components/ActiveQuantity/ActiveQuantity64.tsx b/src/Components/ActiveQuantity/ActiveQuantity64.tsx
new file mode 100644
index 00000000000..4b3dedf9b53
--- /dev/null
+++ b/src/Components/ActiveQuantity/ActiveQuantity64.tsx
@@ -0,0 +1,146 @@
+import React from 'react';
+import { ProductCard } from '../ProductCard/ProductCard';
+import { Phone } from '../../types/Phone';
+import { Products } from '../../types/Products';
+
+type ActiveQuantity64Props = {
+ activeQuantity: number;
+ activePage: number;
+ phones: Phone[];
+ mapPhoneToProduct: (p: Phone) => Products;
+};
+
+export const ActiveQuantity64: React.FC = ({
+ activePage,
+ phones,
+ mapPhoneToProduct,
+}) => {
+ return (
+
+ {activePage === 0 &&
+ phones.slice(0, 64).map(phone => (
+
+
+
+
+
+
{phone.name}
+
{phone.priceRegular}$
+
+
+
+ Screen{' '}
+ {phone.screen}
+
+
+ Capacity{' '}
+ {phone.capacity}
+
+
+ RAM {phone.ram}
+
+
+
+
+
+ ))}
+ {activePage === 1 &&
+ phones.slice(64, 128).map(phone => (
+
+
+
+
+
+
{phone.name}
+
{phone.priceRegular}$
+
+
+
+ Screen{' '}
+ {phone.screen}
+
+
+ Capacity{' '}
+ {phone.capacity}
+
+
+ RAM {phone.ram}
+
+
+
+
+
+ ))}
+ {activePage === 2 &&
+ phones.slice(128, 192).map(phone => (
+
+
+
+
+
+
{phone.name}
+
{phone.priceRegular}$
+
+
+
+ Screen{' '}
+ {phone.screen}
+
+
+ Capacity{' '}
+ {phone.capacity}
+
+
+ RAM {phone.ram}
+
+
+
+
+
+ ))}
+ {activePage === 3 &&
+ phones.slice(192, 256).map(phone => (
+
+
+
+
+
+
{phone.name}
+
{phone.priceRegular}$
+
+
+
+ Screen{' '}
+ {phone.screen}
+
+
+ Capacity{' '}
+ {phone.capacity}
+
+
+ RAM {phone.ram}
+
+
+
+
+
+ ))}
+
+ );
+};
diff --git a/src/Components/ActiveQuantity/ActiveQuantityAcc/ActiveQuantity16.tsx b/src/Components/ActiveQuantity/ActiveQuantityAcc/ActiveQuantity16.tsx
new file mode 100644
index 00000000000..aac655192bd
--- /dev/null
+++ b/src/Components/ActiveQuantity/ActiveQuantityAcc/ActiveQuantity16.tsx
@@ -0,0 +1,163 @@
+import React from 'react';
+import { ProductCard } from '../../ProductCard/ProductCard';
+import { Products } from '../../../types/Products';
+import { Accessorie } from '../../../types/Accessories';
+import { Link } from 'react-router-dom';
+
+type ActiveQuantity16Props = {
+ activeQuantity: number;
+ activePage: number;
+ phones: Accessorie[];
+ mapAccessorieToProduct: (p: Accessorie) => Products;
+};
+
+export const ActiveQuantity16: React.FC = ({
+ activePage,
+ phones,
+ mapAccessorieToProduct,
+}) => {
+ return (
+
+ {activePage === 0 &&
+ phones.slice(0, 16).map(phone => (
+
+
+
+
+
+
{phone.name}
+
{phone.priceRegular}$
+
+
+
+ Screen{' '}
+ {phone.screen}
+
+
+ Capacity{' '}
+ {phone.capacity}
+
+
+ RAM {phone.ram}
+
+
+
+
+
+ ))}
+ {activePage === 1 &&
+ phones.slice(16, 32).map(phone => (
+
+
+
+
+
+
{phone.name}
+
{phone.priceRegular}$
+
+
+
+ Screen{' '}
+ {phone.screen}
+
+
+ Capacity{' '}
+ {phone.capacity}
+
+
+ RAM {phone.ram}
+
+
+
+
+
+ ))}
+ {activePage === 2 &&
+ phones.slice(32, 48).map(phone => (
+
+
+
+
+
+
{phone.name}
+
{phone.priceRegular}$
+
+
+
+ Screen{' '}
+ {phone.screen}
+
+
+ Capacity{' '}
+ {phone.capacity}
+
+
+ RAM {phone.ram}
+
+
+
+
+
+ ))}
+ {activePage === 3 &&
+ phones.slice(48, 64).map(phone => (
+
+
+
+
+
+
{phone.name}
+
{phone.priceRegular}$
+
+
+
+ Screen{' '}
+ {phone.screen}
+
+
+ Capacity{' '}
+ {phone.capacity}
+
+
+ RAM {phone.ram}
+
+
+
+
+
+ ))}
+
+ );
+};
diff --git a/src/Components/ActiveQuantity/ActiveQuantityAcc/ActiveQuantity32.tsx b/src/Components/ActiveQuantity/ActiveQuantityAcc/ActiveQuantity32.tsx
new file mode 100644
index 00000000000..c0c80d456f7
--- /dev/null
+++ b/src/Components/ActiveQuantity/ActiveQuantityAcc/ActiveQuantity32.tsx
@@ -0,0 +1,146 @@
+import React from 'react';
+import { ProductCard } from '../../ProductCard/ProductCard';
+import { Products } from '../../../types/Products';
+import { Accessorie } from '../../../types/Accessories';
+
+type ActiveQuantity32Props = {
+ activeQuantity: number;
+ activePage: number;
+ phones: Accessorie[];
+ mapAccessorieToProduct: (p: Accessorie) => Products;
+};
+
+export const ActiveQuantity32: React.FC = ({
+ phones,
+ mapAccessorieToProduct,
+ activePage,
+}: ActiveQuantity32Props) => {
+ return (
+
+ {activePage === 0 &&
+ phones.slice(0, 32).map(phone => (
+
+
+
+
+
+
{phone.name}
+
{phone.priceRegular}$
+
+
+
+ Screen{' '}
+ {phone.screen}
+
+
+ Capacity{' '}
+ {phone.capacity}
+
+
+ RAM {phone.ram}
+
+
+
+
+
+ ))}
+ {activePage === 1 &&
+ phones.slice(32, 64).map(phone => (
+
+
+
+
+
+
{phone.name}
+
{phone.priceRegular}$
+
+
+
+ Screen{' '}
+ {phone.screen}
+
+
+ Capacity{' '}
+ {phone.capacity}
+
+
+ RAM {phone.ram}
+
+
+
+
+
+ ))}
+ {activePage === 2 &&
+ phones.slice(64, 96).map(phone => (
+
+
+
+
+
+
{phone.name}
+
{phone.priceRegular}$
+
+
+
+ Screen{' '}
+ {phone.screen}
+
+
+ Capacity{' '}
+ {phone.capacity}
+
+
+ RAM {phone.ram}
+
+
+
+
+
+ ))}
+ {activePage === 3 &&
+ phones.slice(96, 128).map(phone => (
+
+
+
+
+
+
{phone.name}
+
{phone.priceRegular}$
+
+
+
+ Screen{' '}
+ {phone.screen}
+
+
+ Capacity{' '}
+ {phone.capacity}
+
+
+ RAM {phone.ram}
+
+
+
+
+
+ ))}
+
+ );
+};
diff --git a/src/Components/ActiveQuantity/ActiveQuantityAcc/ActiveQuantity64.tsx b/src/Components/ActiveQuantity/ActiveQuantityAcc/ActiveQuantity64.tsx
new file mode 100644
index 00000000000..cacc0c175dc
--- /dev/null
+++ b/src/Components/ActiveQuantity/ActiveQuantityAcc/ActiveQuantity64.tsx
@@ -0,0 +1,146 @@
+import React from 'react';
+import { ProductCard } from '../../ProductCard/ProductCard';
+import { Products } from '../../../types/Products';
+import { Accessorie } from '../../../types/Accessories';
+
+type ActiveQuantity64Props = {
+ activeQuantity: number;
+ activePage: number;
+ phones: Accessorie[];
+ mapAccessorieToProduct: (p: Accessorie) => Products;
+};
+
+export const ActiveQuantity64: React.FC = ({
+ activePage,
+ phones,
+ mapAccessorieToProduct,
+}) => {
+ return (
+
+ {activePage === 0 &&
+ phones.slice(0, 64).map(phone => (
+
+
+
+
+
+
{phone.name}
+
{phone.priceRegular}$
+
+
+
+ Screen{' '}
+ {phone.screen}
+
+
+ Capacity{' '}
+ {phone.capacity}
+
+
+ RAM {phone.ram}
+
+
+
+
+
+ ))}
+ {activePage === 1 &&
+ phones.slice(64, 128).map(phone => (
+
+
+
+
+
+
{phone.name}
+
{phone.priceRegular}$
+
+
+
+ Screen{' '}
+ {phone.screen}
+
+
+ Capacity{' '}
+ {phone.capacity}
+
+
+ RAM {phone.ram}
+
+
+
+
+
+ ))}
+ {activePage === 2 &&
+ phones.slice(128, 192).map(phone => (
+
+
+
+
+
+
{phone.name}
+
{phone.priceRegular}$
+
+
+
+ Screen{' '}
+ {phone.screen}
+
+
+ Capacity{' '}
+ {phone.capacity}
+
+
+ RAM {phone.ram}
+
+
+
+
+
+ ))}
+ {activePage === 3 &&
+ phones.slice(192, 256).map(phone => (
+
+
+
+
+
+
{phone.name}
+
{phone.priceRegular}$
+
+
+
+ Screen{' '}
+ {phone.screen}
+
+
+ Capacity{' '}
+ {phone.capacity}
+
+
+ RAM {phone.ram}
+
+
+
+
+
+ ))}
+
+ );
+};
diff --git a/src/Components/ActiveQuantity/ActiveQuantityTablets/ActiveQuantity16.tsx b/src/Components/ActiveQuantity/ActiveQuantityTablets/ActiveQuantity16.tsx
new file mode 100644
index 00000000000..40437da858d
--- /dev/null
+++ b/src/Components/ActiveQuantity/ActiveQuantityTablets/ActiveQuantity16.tsx
@@ -0,0 +1,163 @@
+import React from 'react';
+import { Products } from '../../../types/Products';
+import { Tablet } from '../../../types/Tablets';
+import { ProductCard } from '../../ProductCard/ProductCard';
+import { Link } from 'react-router-dom';
+
+type ActiveQuantity16Props = {
+ activeQuantity: number;
+ activePage: number;
+ phones: Tablet[];
+ mapTabletToProduct: (p: Tablet) => Products;
+};
+
+export const ActiveQuantity16: React.FC = ({
+ activePage,
+ phones,
+ mapTabletToProduct,
+}) => {
+ return (
+
+ {activePage === 0 &&
+ phones.slice(0, 16).map(phone => (
+
+
+
+
+
+
{phone.name}
+
{phone.priceRegular}$
+
+
+
+ Screen{' '}
+ {phone.screen}
+
+
+ Capacity{' '}
+ {phone.capacity}
+
+
+ RAM {phone.ram}
+
+
+
+
+
+ ))}
+ {activePage === 1 &&
+ phones.slice(16, 32).map(phone => (
+
+
+
+
+
+
{phone.name}
+
{phone.priceRegular}$
+
+
+
+ Screen{' '}
+ {phone.screen}
+
+
+ Capacity{' '}
+ {phone.capacity}
+
+
+ RAM {phone.ram}
+
+
+
+
+
+ ))}
+ {activePage === 2 &&
+ phones.slice(32, 48).map(phone => (
+
+
+
+
+
+
{phone.name}
+
{phone.priceRegular}$
+
+
+
+ Screen{' '}
+ {phone.screen}
+
+
+ Capacity{' '}
+ {phone.capacity}
+
+
+ RAM {phone.ram}
+
+
+
+
+
+ ))}
+ {activePage === 3 &&
+ phones.slice(48, 64).map(phone => (
+
+
+
+
+
+
{phone.name}
+
{phone.priceRegular}$
+
+
+
+ Screen{' '}
+ {phone.screen}
+
+
+ Capacity{' '}
+ {phone.capacity}
+
+
+ RAM {phone.ram}
+
+
+
+
+
+ ))}
+
+ );
+};
diff --git a/src/Components/ActiveQuantity/ActiveQuantityTablets/ActiveQuantity32.tsx b/src/Components/ActiveQuantity/ActiveQuantityTablets/ActiveQuantity32.tsx
new file mode 100644
index 00000000000..f826cde9f62
--- /dev/null
+++ b/src/Components/ActiveQuantity/ActiveQuantityTablets/ActiveQuantity32.tsx
@@ -0,0 +1,163 @@
+import React from 'react';
+import { Products } from '../../../types/Products';
+import { Tablet } from '../../../types/Tablets';
+import { ProductCard } from '../../ProductCard/ProductCard';
+import { Link } from 'react-router-dom';
+
+type ActiveQuantity32Props = {
+ activeQuantity: number;
+ activePage: number;
+ phones: Tablet[];
+ mapTabletToProduct: (p: Tablet) => Products;
+};
+
+export const ActiveQuantity32: React.FC = ({
+ phones,
+ mapTabletToProduct,
+ activePage,
+}: ActiveQuantity32Props) => {
+ return (
+
+ {activePage === 0 &&
+ phones.slice(0, 32).map(phone => (
+
+
+
+
+
+
{phone.name}
+
{phone.priceRegular}$
+
+
+
+ Screen{' '}
+ {phone.screen}
+
+
+ Capacity{' '}
+ {phone.capacity}
+
+
+ RAM {phone.ram}
+
+
+
+
+
+ ))}
+ {activePage === 1 &&
+ phones.slice(32, 64).map(phone => (
+
+
+
+
+
+
{phone.name}
+
{phone.priceRegular}$
+
+
+
+ Screen{' '}
+ {phone.screen}
+
+
+ Capacity{' '}
+ {phone.capacity}
+
+
+ RAM {phone.ram}
+
+
+
+
+
+ ))}
+ {activePage === 2 &&
+ phones.slice(64, 96).map(phone => (
+
+
+
+
+
+
{phone.name}
+
{phone.priceRegular}$
+
+
+
+ Screen{' '}
+ {phone.screen}
+
+
+ Capacity{' '}
+ {phone.capacity}
+
+
+ RAM {phone.ram}
+
+
+
+
+
+ ))}
+ {activePage === 3 &&
+ phones.slice(96, 128).map(phone => (
+
+
+
+
+
+
{phone.name}
+
{phone.priceRegular}$
+
+
+
+ Screen{' '}
+ {phone.screen}
+
+
+ Capacity{' '}
+ {phone.capacity}
+
+
+ RAM {phone.ram}
+
+
+
+
+
+ ))}
+
+ );
+};
diff --git a/src/Components/ActiveQuantity/ActiveQuantityTablets/ActiveQuantity64.tsx b/src/Components/ActiveQuantity/ActiveQuantityTablets/ActiveQuantity64.tsx
new file mode 100644
index 00000000000..7a07ed5912f
--- /dev/null
+++ b/src/Components/ActiveQuantity/ActiveQuantityTablets/ActiveQuantity64.tsx
@@ -0,0 +1,162 @@
+import React from 'react';
+import { Tablet } from '../../../types/Tablets';
+import { Products } from '../../../types/Products';
+import { ProductCard } from '../../ProductCard/ProductCard';
+import { Link } from 'react-router-dom';
+type ActiveQuantity64Props = {
+ activeQuantity: number;
+ activePage: number;
+ phones: Tablet[];
+ mapTabletToProduct: (p: Tablet) => Products;
+};
+
+export const ActiveQuantity64: React.FC = ({
+ activePage,
+ phones,
+ mapTabletToProduct,
+}) => {
+ return (
+
+ {activePage === 0 &&
+ phones.slice(0, 64).map(phone => (
+
+
+
+
+
+
{phone.name}
+
{phone.priceRegular}$
+
+
+
+ Screen{' '}
+ {phone.screen}
+
+
+ Capacity{' '}
+ {phone.capacity}
+
+
+ RAM {phone.ram}
+
+
+
+
+
+ ))}
+ {activePage === 1 &&
+ phones.slice(64, 128).map(phone => (
+
+
+
+
+
+
{phone.name}
+
{phone.priceRegular}$
+
+
+
+ Screen{' '}
+ {phone.screen}
+
+
+ Capacity{' '}
+ {phone.capacity}
+
+
+ RAM {phone.ram}
+
+
+
+
+
+ ))}
+ {activePage === 2 &&
+ phones.slice(128, 192).map(phone => (
+
+
+
+
+
+
{phone.name}
+
{phone.priceRegular}$
+
+
+
+ Screen{' '}
+ {phone.screen}
+
+
+ Capacity{' '}
+ {phone.capacity}
+
+
+ RAM {phone.ram}
+
+
+
+
+
+ ))}
+ {activePage === 3 &&
+ phones.slice(192, 256).map(phone => (
+
+
+
+
+
+
{phone.name}
+
{phone.priceRegular}$
+
+
+
+ Screen{' '}
+ {phone.screen}
+
+
+ Capacity{' '}
+ {phone.capacity}
+
+
+ RAM {phone.ram}
+
+
+
+
+
+ ))}
+
+ );
+};
diff --git a/src/Components/Aside/Aside.scss b/src/Components/Aside/Aside.scss
new file mode 100644
index 00000000000..6f08e3b8336
--- /dev/null
+++ b/src/Components/Aside/Aside.scss
@@ -0,0 +1,168 @@
+@import "../../utils/mixins";
+
+.aside {
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background-color: #0F1121;
+ align-items: stretch;
+ z-index: 1000;
+
+ &__header {
+ display: flex;
+ height: 48px;
+ width: 100%;
+ padding-left: 16px;
+ box-shadow: 0 1px #323542;
+ align-items: center;
+ justify-content: space-between;
+ }
+
+ &__close {
+ height: 48px;
+ width: 48px;
+ border: none;
+
+ box-shadow: -1px 0 #323542;
+
+ background-color: transparent;
+ }
+
+ &__nav {
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+
+ padding: 32px 16px;
+
+ &-list {
+ list-style: none;
+ padding: 0;
+ margin: 0;
+ display: flex;
+ flex-direction: column;
+ gap: 32px;
+ width: 100%;
+ }
+
+ &-item {
+ display: flex;
+ justify-content: center;
+ width: 100%;
+ }
+
+ &-button {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ height: 48px;
+ border: none;
+
+ width: 100%;
+
+ box-shadow: 1px 0 #323542;
+
+ background-color: transparent;
+
+ &_selected {
+ width: 100%;
+
+ &::after {
+ content: "";
+ display: block;
+ position: absolute;
+ left: 0;
+ top: 61px;
+ bottom: 0;
+ width: 100%;
+ height: 3px;
+ background: #F1F2F9;
+ }
+ }
+ }
+
+ &-link {
+ display: flex;
+ position: relative;
+ justify-content: center;
+ color: #75767F;
+ text-decoration: none;
+
+ width: fit-content;
+
+ font-family: Mont, sans-serif;
+ font-weight: 800;
+ font-size: 12px;
+ line-height: 11px;
+ letter-spacing: 4%;
+ text-transform: uppercase;
+
+ &_active {
+ color: #F1F2F9;
+
+ &::after {
+ content: "";
+ display: block;
+ position: absolute;
+ left: 0;
+ top: 17px;
+ bottom: 0;
+ width: 100%;
+ height: 3px;
+ background: #F1F2F9;
+ }
+ }
+
+ }
+ }
+
+ &__logo {
+ width: 64px;
+ }
+
+ &__footer {
+ position: fixed;
+ width: 100%;
+ bottom: 0;
+ margin-top: auto;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+
+ &-link {
+ width: 100%;
+ }
+
+ &-button {
+ position: relative;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 100%;
+ height: 64px;
+ border: none;
+
+ box-shadow: 1px -1px #323542;
+
+ background-color: transparent;
+ }
+ }
+
+ &__count {
+ position: absolute;
+ top: 18px;
+ right: 42%;
+
+ display: flex;
+ justify-content: center;
+ align-items: center;
+
+ width: 16px;
+ height: 16px;
+
+ background-color: #F1F2F9;
+ border-radius: 50%;
+ }
+}
diff --git a/src/Components/Aside/Aside.tsx b/src/Components/Aside/Aside.tsx
new file mode 100644
index 00000000000..008af13b792
--- /dev/null
+++ b/src/Components/Aside/Aside.tsx
@@ -0,0 +1,89 @@
+import React from 'react';
+import './Aside.scss';
+import Close from '../../images/icons/Close.svg';
+import logo from '../../images/Logo.png';
+import { NavLink } from 'react-router-dom';
+import favourites from '../../images/fav/Icons/Favourites (Heart Like).svg';
+import cart from '../../images/fav/Shopping bag (Cart).svg';
+import classNames from 'classnames';
+
+type Props = {
+ setMenuOpen: React.Dispatch>;
+ totalFavourites: number;
+ totalQuantity: number;
+};
+
+export const Aside: React.FC = ({
+ setMenuOpen,
+ totalQuantity,
+ totalFavourites,
+}) => {
+ // className for main navigation links (no "selected" decoration)
+ const getLink = ({ isActive }: { isActive: boolean }) =>
+ classNames('aside__nav-link', {
+ 'aside__nav-link_active': isActive,
+ });
+
+ // className for footer links (apply extra "selected" style when active)
+ const getFooterLink = ({ isActive }: { isActive: boolean }) =>
+ classNames('aside__nav-button', {
+ 'aside__nav-button_selected': isActive,
+ });
+
+ return (
+
+
+
+
setMenuOpen(false)}>
+
+
+
+
+
+
+
+ Home
+
+
+
+
+ Phones
+
+
+
+
+ Tablets
+
+
+
+
+ Accessories
+
+
+
+
+
+
+
+
+ {(totalFavourites ?? 0) > 0 && (
+
+ {totalFavourites}
+
+ )}
+
+
+
+
+
+ {(totalQuantity ?? 0) > 0 && (
+
+ {totalQuantity}
+
+ )}
+
+
+
+
+ );
+};
diff --git a/src/Components/Cart/Cart.scss b/src/Components/Cart/Cart.scss
new file mode 100644
index 00000000000..b7722b7429b
--- /dev/null
+++ b/src/Components/Cart/Cart.scss
@@ -0,0 +1,409 @@
+@import '../../utils/mixins';
+
+.cart {
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ place-items: center;
+ min-height: 100vh;
+ min-width: 100%;
+
+ &__items {
+ width: 100%;
+ flex: 2 1 0;
+ min-width: 0;
+
+ &-empty {
+ font-family: Mont, sans-serif;
+ font-weight: 800;
+ font-size: 32px;
+ line-height: 41px;
+ letter-spacing: -1%;
+
+ @include on-tablet {
+ font-size: 48px;
+ line-height: 56px;
+ }
+ }
+ }
+
+ &__header {
+ width: 100%;
+ }
+
+ &__content {
+ @include content-padding-inline;
+
+ width: 100%;
+
+ margin-bottom: 56px;
+
+ margin-inline: 16px;
+
+ @include on-tablet {
+ margin-inline: 24px;
+ margin-bottom: 64px;
+ }
+
+ @include on-desktop {
+ margin-bottom: 0;
+ margin-inline: 32px;
+ }
+ }
+
+ &__path {
+ margin-top: 25px;
+ margin-bottom: 40px;
+
+ display: flex;
+ gap: 8px;
+ text-align: center;
+ align-items: center;
+
+ &-name {
+ color: #75767F;
+ font-family: Mont, sans-serif;
+ font-weight: 700;
+ font-size: 12px;
+ line-height: 100%;
+ }
+ }
+
+ &__sub {
+ margin-top: 8px;
+ font-family: 'Mont Regular', sans-serif;
+ font-weight: 600;
+ font-size: 14px;
+ line-height: 21px;
+ color: #75767F;
+
+ margin-bottom: 32px;
+ }
+
+ &__footer {
+ position: relative;
+ width: 100%;
+ margin-top: auto;
+ padding-top: 40px;
+ }
+
+ &__grid {
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+
+ width: 100%;
+
+ margin-bottom: 80px;
+ }
+
+ &__item {
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+ padding: 16px;
+
+ width: 100%;
+ background-color: #161827;
+ justify-content: space-between;
+
+ border: #161827 solid 1px;
+
+ // padding-left: 25px;
+ // padding-right: 25px;
+
+ max-width: none;
+
+ text-decoration: none;
+ color: #F1F2F9;
+
+ transition: 0.3s;
+
+ @include on-tablet {
+ flex-direction: row;
+ gap: 24px;
+ align-items: center;
+ }
+
+ &:hover {
+ border: #323542 solid 1px;
+ transition: 0.3s;
+ }
+ }
+
+ &__right {
+ flex: 0 0 auto;
+
+ display: flex;
+ justify-content: space-between;
+
+ align-items: center;
+
+ @include on-tablet {
+ justify-content: end;
+ }
+ }
+
+ &__left {
+ gap: 16px;
+ display: flex;
+ align-items: center;
+ flex: 1 1 auto;
+ min-width: 0;
+
+ @include on-tablet {
+ gap: 24px;
+ }
+ }
+
+ &__img {
+ flex: 0 0 auto;
+ }
+
+ &__image {
+ height: 80px;
+ width: auto;
+ display: block;
+ }
+
+ &__title {
+ margin: 0;
+
+ font-family: 'Mont Regular', sans-serif;
+ font-weight: 600;
+ font-size: 14px;
+ line-height: 21px;
+ letter-spacing: 0%;
+
+ flex: 1 1 auto;
+ }
+
+ &__quantity {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ align-items: center;
+ justify-content: center;
+ min-width: 48px;
+
+ @include on-tablet {
+ flex-direction: row;
+ gap: 14px;
+ min-width: 0;
+ }
+
+ &-button {
+ cursor: pointer;
+
+ display: flex;
+ height: 32px;
+ width: 32px;
+ background-color: #323542;
+ align-items: center;
+ justify-content: center;
+
+ @include on-tablet {
+ width: auto;
+ min-width: 32px;
+ padding: 0 8px;
+ }
+
+ border: #161827 solid 1px;
+
+ &--disabled {
+ cursor: not-allowed;
+ background-color: transparent;
+ border: #3B3E4A solid 1px;
+
+ opacity: 0.5;
+ }
+
+ &:hover:not(.cart__quantity-button--disabled) {
+ background-color: #4A4D58;
+ transition: 0.3s;
+ }
+ }
+ }
+
+ &__price {
+ margin-inline: 0;
+
+ font-family: Mont, sans-serif;
+ font-weight: 800;
+ font-size: 22px;
+ line-height: 140%;
+ text-align: right;
+
+ @include on-tablet {
+ margin-inline: 24px;
+ }
+ }
+
+ &__delete {
+ padding: 0;
+
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+
+ background-color: transparent;
+ border: none;
+
+ width: 32px !important;
+ height: 32px;
+
+ transition: 0.3s;
+
+ &-icon {
+ display: flex;
+ width: 16px;
+ height: 16px;
+ color: #F1F2F9;
+ }
+ }
+
+ &__container {
+ @include on-desktop {
+ display: flex;
+ align-items: top;
+ gap: 16px;
+ }
+
+ }
+
+ &__total {
+ flex: 1 1 0;
+ min-width: 0;
+ height: 206px;
+ padding-inline: 24px;
+ width: auto;
+
+ border: #3B3E4A solid 1px;
+
+ justify-content: center;
+ align-items: center;
+
+ display: flex;
+ flex-direction: column;
+ font-family: Mont, sans-serif;
+ font-weight: 800;
+ font-size: 32px;
+ line-height: 41px;
+
+ &-subtotal {
+ color: #75767F;
+ font-family: 'Mont Regular', sans-serif;
+ font-weight: 600;
+ font-size: 14px;
+ line-height: 21px;
+ text-align: center;
+
+ margin-bottom: 25px;
+ }
+
+ &-none {
+ display: none;
+ }
+ }
+
+ &__line {
+ width: 100%;
+ height: 1px;
+ background-color: #3B3E4A;
+
+ margin-bottom: 25px;
+ }
+
+ &__checkout {
+ color: #F1F2F9;
+
+ width: 100%;
+ height: 48px;
+ background-color: #905BFF;
+
+ font-family: 'Mont Regular', sans-serif;
+ font-weight: 700;
+ font-size: 14px;
+ line-height: 21px;
+ text-align: center;
+
+ border: none;
+ cursor: pointer;
+ transition: 0.3s;
+
+ &:hover {
+ transition: 0.3s;
+ background-color: #A378FF;
+ }
+ }
+
+ &__check {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+
+ width: 100%;
+
+ &-container {
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+ align-items: center;
+ justify-content: center;
+
+ padding-inline: 24px;
+
+ width: 400px;
+ height: 230px;
+
+ background-color: transparent;
+ border: #3B3E4A solid 1px;
+ }
+
+ &-title {
+ font-family: Mont, sans-serif;
+ font-weight: 800;
+ font-size: 32px;
+ line-height: 41px;
+
+ margin-bottom: 10px;
+ }
+
+ &-close {
+ width: 120px;
+ height: 48px;
+
+ background-color: #905BFF;
+
+ font-family: 'Mont Regular', sans-serif;
+ color: #F1F2F9;
+ font-weight: 700;
+ font-size: 14px;
+ line-height: 21px;
+ text-align: center;
+
+ border: none;
+ cursor: pointer;
+ transition: 0.3s;
+
+ &:hover {
+ transition: 0.3s;
+ background-color: #A378FF;
+ }
+ }
+ }
+
+ &__title-cart {
+ font-family: Mont, sans-serif;
+ font-weight: 800;
+ font-size: 32px;
+ line-height: 41px;
+ letter-spacing: -1%;
+
+ @include on-tablet {
+ font-size: 48px;
+ line-height: 56px;
+ }
+ }
+}
diff --git a/src/Components/Cart/Cart.tsx b/src/Components/Cart/Cart.tsx
new file mode 100644
index 00000000000..8c8f04af9f3
--- /dev/null
+++ b/src/Components/Cart/Cart.tsx
@@ -0,0 +1,165 @@
+/* eslint-disable max-len */
+import React, { useState } from 'react';
+import { Header } from '../Header/header';
+import { useCart } from '../../Context/Context';
+import { useFav } from '../../Context/FavouritesContext';
+import { Footer } from '../Footer/Footer';
+import ArrowGray from '../../images/icons/ChevronGray.svg';
+import Home from '../../images/icons/Home.svg';
+import { Link } from 'react-router-dom';
+import './Cart.scss';
+import Delete from '../../images/icons/Close.svg';
+import Minus from '../../images/icons/Minus.svg';
+import Plus from '../../images/icons/Plus.svg';
+import { Aside } from '../Aside/Aside';
+
+export const Cart = () => {
+ const [menuOpen, setMenuOpen] = useState(false);
+ const [checkOut, setCheckOut] = useState(false);
+ const { totalQuantity, items, removeFromCart, changeQuantity, clearCart } =
+ useCart();
+ const { totalFavourites } = useFav();
+ const totalPrice = items.reduce(
+ (total, item) => total + item.product.fullPrice * item.quantity,
+ 0,
+ );
+
+ return (
+
+
+
+ {menuOpen && (
+
+ )}
+
+
+
+
+
+
+
+
Cart
+
+
Cart
+
{totalQuantity} items
+
+
+ {totalQuantity === 0 && !checkOut && (
+
Your cart is empty
+ )}
+
+ {items.map(item => (
+
+
+
{
+ e.preventDefault();
+ removeFromCart(item.id);
+ }}
+ >
+
+
+
+
+
+
{item.product.name}
+
+
+
+
{
+ e.preventDefault();
+ if (item.quantity > 1) {
+ changeQuantity(item.id, item.quantity - 1);
+ }
+ }}
+ disabled={item.quantity < 2}
+ // eslint-disable-next-line max-len
+ className={`icon cart__quantity-minus cart__quantity-button ${item.quantity < 2 ? 'cart__quantity-button--disabled' : ''}`}
+ >
+
+
+
+ {item.quantity}
+
+
{
+ e.preventDefault();
+ changeQuantity(item.id, item.quantity + 1);
+ }}
+ className="icon cart__quantity-plus cart__quantity-button"
+ >
+
+
+
+
+ ${item.product.fullPrice * item.quantity}
+
+
+
+ ))}
+
+
+
+
${totalPrice}
+
+ Total for {totalQuantity} items
+
+
+
{
+ setCheckOut(true);
+ clearCart();
+ }}
+ className="cart__checkout"
+ disabled={totalQuantity === 0}
+ >
+ Checkout
+
+
+ {checkOut && (
+
+
+ <>
+
+ Thank you for your purchase!
+
+
+
setCheckOut(false)}
+ className="cart__check-close"
+ >
+ Close
+
+ >
+
+
+ )}
+
+
+
+
+
+
+ );
+};
diff --git a/src/Components/DevSpec/AccSpec.tsx b/src/Components/DevSpec/AccSpec.tsx
new file mode 100644
index 00000000000..284009d5658
--- /dev/null
+++ b/src/Components/DevSpec/AccSpec.tsx
@@ -0,0 +1,496 @@
+/* eslint-disable react/no-unescaped-entities */
+/* eslint-disable react-hooks/exhaustive-deps */
+/* eslint-disable no-console */
+/* eslint-disable max-len */
+/* eslint-disable react/jsx-no-comment-textnodes */
+import { Link, useParams, useLocation, useNavigate } from 'react-router-dom';
+import { Header } from '../Header/header';
+import { useEffect, useState } from 'react';
+import { getAccessorieById } from '../../api/api';
+import home from '../../images/icons/Home.svg';
+import arr from '../../images/icons/Chevron (Arrow Right) grey.png';
+import backArr from '../../images/icons/Chevron (Arrow Left).svg';
+import './PhoneSpec.scss';
+import fav from '../../images/fav/Icons/Favourites (Heart Like).svg';
+import activeFav from '../../images/icons/ActiveFav.svg';
+import { useCart } from '../../Context/Context';
+import { useFav } from '../../Context/FavouritesContext';
+import { PhoneLike } from '../PhoneLike/PhoneLike';
+import { Footer } from '../Footer/Footer';
+import { Products } from '../../types/Products';
+import React from 'react';
+import { Accessorie } from '../../types/Accessories';
+import { Aside } from '../Aside/Aside';
+import { Loader } from '../Loader/Loader';
+
+// type Props = {
+// favouritesCount: number;
+// };
+
+enum Image {
+ first = 'first',
+ second = 'second',
+ third = 'third',
+ fourth = 'fourth',
+ fifth = 'fifth',
+}
+
+export const AccSpec: React.FC = () => {
+ const { productId } = useParams<{ productId: string }>();
+ const { pathname } = useLocation();
+ const [accessorie, setAccessorie] = useState();
+ const { totalQuantity, addToCart, removeFromCart, isInCart } = useCart();
+ const { addToFav, removeFromFav, isInFav, totalFavourites } = useFav();
+ const [menuOpen, setMenuOpen] = useState(false);
+ const [loader, setLoader] = useState(false);
+
+ useEffect(() => {
+ window.scrollTo(0, 0);
+ }, [pathname]);
+
+ const mapAccessorieToProduct = (a: Accessorie): Products => ({
+ id: String(a.id),
+ itemId: String(a.id),
+ name: a.name ?? 'Unknown',
+ category: 'accessories',
+ fullPrice: Number(a.priceRegular ?? 0),
+ price: Number(a.priceDiscount ?? a.priceRegular ?? 0),
+ screen: a.screen ?? '—',
+ capacity: a.capacity ?? '—',
+ color: Array.isArray(a.color)
+ ? (a.color[0] ?? '—')
+ : (a.color ?? a.colorsAvailable?.[0] ?? '—'),
+ ram: a.ram ?? '—',
+ year: new Date().getFullYear(),
+ image: Array.isArray(a.images) ? (a.images[0] ?? '') : (a.images ?? ''),
+ });
+ const navigate = useNavigate();
+ const [, setErrorMessage] = useState(false);
+ const [image, setImage] = useState(Image.first);
+ const [color, setColor] = useState(null);
+ const images = accessorie?.images || [];
+ const path = accessorie?.id;
+
+ const normalizePathValue = (value: string) =>
+ value.toLowerCase().replace(/\s+/g, '-');
+
+ const getDefaultColor = (acc?: Accessorie) => {
+ if (!acc) {
+ return null;
+ }
+
+ const rawColor = acc.color as unknown;
+
+ if (typeof rawColor === 'string' && rawColor.trim()) {
+ return rawColor;
+ }
+
+ if (Array.isArray(rawColor) && rawColor.length) {
+ return rawColor[0];
+ }
+
+ return acc.colorsAvailable?.[0] ?? null;
+ };
+
+ // const capacitiesRaw = accessorie?.capacity ?? [];
+ const capacities: string[] = Array.isArray(accessorie?.capacity)
+ ? accessorie.capacity
+ : accessorie?.capacity
+ ? [accessorie.capacity]
+ : [];
+
+ const [selectedCapacity, setSelectedCapacity] = useState(null);
+
+ useEffect(() => {
+ if (capacities.length) {
+ setSelectedCapacity(capacities[0]);
+ }
+ }, [capacities]);
+
+ useEffect(() => {
+ if (!accessorie) {
+ return;
+ }
+
+ const defaultColor = getDefaultColor(accessorie);
+
+ if (defaultColor) {
+ setColor(defaultColor);
+ }
+ }, [accessorie]);
+ const imageKeys: Image[] = [
+ Image.first,
+ Image.second,
+ Image.third,
+ Image.fourth,
+ Image.fifth,
+ ];
+
+ const mainIndex =
+ imageKeys.indexOf(image) === -1 ? 0 : imageKeys.indexOf(image);
+ const imageByIndex = (i: number): Image => imageKeys[i];
+
+ useEffect(() => {
+ if (images.length <= 1) {
+ return;
+ }
+
+ const enums = [
+ Image.first,
+ Image.second,
+ Image.third,
+ Image.fourth,
+ Image.fifth,
+ ];
+ const id = setInterval(() => {
+ setImage(prev => {
+ const idx = enums.indexOf(prev);
+ const safeIdx = idx === -1 ? 0 : idx;
+ const next = enums[(safeIdx + 1) % images.length];
+
+ return next;
+ });
+ }, 5000);
+
+ return () => clearInterval(id);
+ }, [images.length]);
+
+ useEffect(() => {
+ if (accessorie?.capacity) {
+ const caps = Array.isArray(accessorie.capacity)
+ ? accessorie.capacity
+ : [accessorie.capacity];
+
+ setSelectedCapacity(caps[0]);
+ }
+ }, [accessorie?.capacity]);
+
+ useEffect(() => {
+ if (!productId) {
+ return;
+ }
+
+ setLoader(true);
+ getAccessorieById(productId)
+ .then(product => {
+ if (product) {
+ setAccessorie(product);
+ console.log(product);
+ }
+ })
+ .catch(() => setErrorMessage(true))
+ .finally(() => setLoader(false));
+ }, [productId]);
+ console.log('productId from params:', productId);
+
+ return (
+
+
+ {menuOpen && (
+
+ )}
+ {loader &&
}
+ {!loader && (
+
+
+
+
+
+
+
+
+
+ Accessories
+
+
+
+
+
{accessorie?.name}
+
+
+
+
Back
+
+
{accessorie?.name}
+
+
+
+ {images[mainIndex] && (
+
+ )}
+
+
+ {images.map((src, i) => (
+
+
setImage(imageByIndex(i))}
+ />
+
+ ))}
+
+
+ {images[mainIndex] && (
+
+ )}
+
+
+
+
Available colors
+
+ {accessorie?.colorsAvailable.map(c => {
+ const updatedPath = path?.replace(
+ path?.includes('space') ? /-[^-]+-[^-]+$/ : /-[^-]+$/,
+ `-${normalizePathValue(c)}`,
+ );
+
+ return (
+
+
{
+ setColor(c);
+ navigate(`../accessories/${updatedPath}`);
+ }}
+ className={`phone__color-color phone__color-${normalizePathValue(c)}`}
+ >
+
+ );
+ })}
+
+
+
+
Select capacity
+
+ {accessorie?.capacityAvailable.map(cap => {
+ const activeColor =
+ color || getDefaultColor(accessorie) || '';
+ const updatedPathCap = path?.replace(
+ /-[^-]+-[^-]+$/,
+ `-${normalizePathValue(cap)}-${normalizePathValue(activeColor)}`,
+ );
+
+ return (
+
{
+ setSelectedCapacity(cap);
+ navigate(`../accessories/${updatedPathCap}`);
+ }}
+ onKeyDown={e =>
+ (e.key === 'Enter' || e.key === ' ') &&
+ setSelectedCapacity(cap)
+ }
+ aria-pressed={cap === selectedCapacity}
+ >
+ {cap}
+
+ );
+ })}
+
+
+
+
+
+
+
+ {accessorie?.priceRegular}$
+
+
+ {accessorie?.priceDiscount}$
+
+
+
+
+
{
+ if (accessorie) {
+ const product = mapAccessorieToProduct(accessorie);
+
+ if (isInCart(String(product.id))) {
+ removeFromCart(String(product.id));
+ } else {
+ addToCart(product);
+ }
+ }
+ }}
+ >
+ {accessorie && isInCart(String(accessorie.id))
+ ? 'Added'
+ : 'Add to cart'}
+
+
{
+ if (accessorie) {
+ const product = mapAccessorieToProduct(accessorie);
+
+ if (isInFav(String(product.id))) {
+ removeFromFav(String(product.id));
+ } else {
+ addToFav(product);
+ }
+ }
+ }}
+ >
+
+
+
+
+
+ Screen{' '}
+
+ {accessorie?.screen}
+
+
+
+ Capacity{' '}
+
+ {accessorie?.capacity}
+
+
+
+ Processor{' '}
+
+ {accessorie?.processor}
+
+
+
+ RAM{' '}
+
+ {accessorie?.ram}
+
+
+
+
+
+
+
+
About
+
+
+
+ Premium Accessories
+
+
+
+ Discover our premium collection of accessories designed to
+ enhance your mobile experience.
+
+ {''}
+
+ From stylish cases to powerful chargers, our accessories
+ combine functionality with elegance. Each item is crafted
+ with precision and tested for quality.
+
+
+
+
+
+ Quality & Design
+
+
+ Our accessories are designed with the same attention to detail
+ as our premium devices. Whether you're looking for protection,
+ convenience, or style, you'll find the perfect accessory to
+ complement your lifestyle.
+
+
+
+
+ Compatibility & Performance
+
+
+ Every accessory is engineered for optimal performance and
+ compatibility. Experience seamless integration with your
+ devices, ensuring you get the most out of your technology
+ investment.
+
+
+
+
+
Tech specs
+
+
+ Screen{' '}
+ {accessorie?.screen}
+
+
+ Resolution{' '}
+
+ {accessorie?.resolution}
+
+
+
+ Processor{' '}
+
+ {accessorie?.processor}
+
+
+
+ RAM {accessorie?.ram}
+
+
+ Built in memory{' '}
+ {accessorie?.capacity}
+
+
+ Color{' '}
+ {accessorie?.color}
+
+
+ Material{' '}
+ {accessorie?.material}
+
+
+ Compatibility{' '}
+
+ {accessorie?.compatibility}
+
+
+
+
+
+
+ )}
+
+
+
+
+ );
+};
diff --git a/src/Components/DevSpec/PhoneSpec.scss b/src/Components/DevSpec/PhoneSpec.scss
new file mode 100644
index 00000000000..a9fcce43d36
--- /dev/null
+++ b/src/Components/DevSpec/PhoneSpec.scss
@@ -0,0 +1,658 @@
+@import '../../utils/mixins';
+
+.phone {
+ &__container {
+ margin-bottom: 80px;
+ }
+
+ &__path {
+ display: flex;
+ align-items: center;
+ margin-top: 25px;
+ margin-bottom: 41px;
+
+ gap: 8px;
+
+ &-phones {
+ margin: 0;
+
+ color: #F1F2F9;
+
+ font-family: 'Mont regular', sans-serif;
+ font-weight: 700;
+ font-size: 12px;
+
+ text-decoration: none;
+ }
+
+ &-name {
+ margin: 0;
+
+ color: #75767F;
+
+ font-family: 'Mont regular', sans-serif;
+ font-weight: 700;
+ font-size: 12px;
+ }
+ }
+
+ &__back {
+ display: flex;
+ align-items: center;
+
+ gap: 4px;
+
+ text-decoration: none;
+ margin-bottom: 16px;
+
+ &-back {
+ margin: 0;
+
+ color: #F1F2F9;
+
+ font-family: 'Mont regular', sans-serif;
+ font-weight: 700;
+ font-size: 12px;
+ }
+
+ }
+
+ &__title {
+ margin: 0;
+
+ color: #F1F2F9;
+
+ font-family: Mont, sans-serif;
+ font-weight: 800;
+ font-size: 32px;
+ line-height: 41px;
+ margin-bottom: 32px;
+
+ @include on-tablet {
+ margin-bottom: 40px;
+ }
+ }
+
+ &__images {
+ display: flex;
+ gap: 8px;
+ justify-content: space-between;
+ flex-direction: row;
+ flex: 0 0 auto;
+
+ margin-bottom: 40px;
+
+ @include on-tablet {
+ gap: 16px;
+ flex-direction: column;
+ margin-bottom: 0;
+ }
+ }
+
+ &__main-image {
+ display: block;
+ width: 100%;
+ height: 100%;
+ max-width: 100%;
+ max-height: 100%;
+ object-fit: contain;
+ aspect-ratio: 1 / 1;
+
+ cursor: pointer;
+ }
+
+ &__image {
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+ align-items: flex-start;
+
+ &-main {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ width: 100%;
+ max-width: 464px;
+ min-width: 0;
+ aspect-ratio: 1 / 1;
+ overflow: hidden;
+
+ margin-bottom: 16px;
+
+ @include on-tablet {
+ margin-bottom: 0;
+ }
+
+ &__first {
+ @include on-tablet {
+ display: none;
+ }
+ }
+
+ &__second {
+ display: none;
+
+ @include on-tablet {
+ width: 100%;
+ min-width: 287px;
+
+ display: flex;
+ }
+
+ @include on-desktop {
+ width: 100%;
+ min-width: 464px;
+
+ display: flex;
+ }
+ }
+ }
+
+ @include on-tablet {
+ min-width: 0;
+ flex-direction: row;
+ gap: 8px;
+ }
+ }
+
+ &__imgs {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+
+ cursor: pointer;
+
+ border: 1px solid #3B3E4A;
+
+ height: 100%;
+ width: 100%;
+ min-width: 50px;
+ aspect-ratio: 1 / 1;
+ transition: 0.3s;
+
+ &:hover {
+ border: 1px solid #75767F;
+ transition: 0.3s;
+ }
+
+ &-active {
+ border: 1px solid #F1F2F9;
+ transition: 0.3s;
+ }
+
+ @include on-tablet {
+ height: 80px;
+ width: 80px;
+ }
+ }
+
+ &__img {
+ height: 100%;
+ padding: 5px;
+
+ @include on-tablet {
+ height: 66px;
+ }
+ }
+
+ &__buttons {
+ display: flex;
+
+ gap: 8px;
+
+ margin-bottom: 32px;
+ }
+
+ &__specs {
+ width: 100%;
+ max-width: 320px;
+
+ &-capacities {
+ display: flex;
+ gap: 8px;
+ }
+
+ &-border {
+ width: 100%;
+ height: 1px;
+ background-color: #3B3E4A;
+
+ margin-bottom: 25px;
+
+ // border: 1px solid #3B3E4A;
+ }
+
+ &-cap {
+ color: #75767F;
+
+ font-family: 'Mont regular', sans-serif;
+ font-weight: 700;
+ font-size: 12px;
+ line-height: 100%;
+
+ &-text {
+ margin: 0;
+ margin-bottom: 8px;
+ }
+ }
+
+ &-capacity {
+ margin-bottom: 25px;
+
+ cursor: pointer;
+
+ color: #F1F2F9;
+
+ font-family: 'Mont regular', sans-serif;
+ font-weight: 600;
+ font-size: 14px;
+ line-height: 21px;
+ text-align: center;
+
+
+ width: 63px;
+ height: 32px;
+
+ border: 1px solid #4A4D58;
+
+ background-color: inherit;
+
+ &-active {
+ background-color: #fff;
+ color: #0F1121;
+ }
+ }
+ }
+ &__main {
+ display: flex;
+ flex-direction: column;
+ margin-bottom: 80px;
+
+ @include on-tablet {
+ flex-direction: row;
+ gap: 64px;
+ }
+ }
+
+ &__colors {
+ display: flex;
+ gap: 8px;
+
+ margin-bottom: 25px;
+
+ &-text {
+ margin: 0;
+
+ margin-bottom: 8px;
+
+ color: #75767F;
+
+ font-family: 'Mont regular', sans-serif;
+ font-weight: 700;
+ font-size: 12px;
+ line-height: 100%;
+ }
+ }
+
+ &__color {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+
+ cursor: pointer;
+
+ height: 32px;
+ width: 32px;
+ border: 1px solid #3B3E4A;
+
+ border-radius: 50%;
+
+ transition: 0.3s;
+
+ &-active {
+ border: 1px solid #F1F2F9;
+ }
+
+ &-color {
+ height: 26px;
+ width: 26px;
+
+ border-radius: 50%;
+ }
+
+ &-beige {
+ background-color: #FCDBC1;
+ }
+
+ &-green {
+ background-color: #5F7170;
+ }
+
+ &-gray {
+ background-color: #4C4C4C;
+ }
+
+ &-white {
+ background-color: #F0F0F0;
+ }
+
+ &-purple {
+ background-color: #905BFF;
+ }
+
+ &-red {
+ background-color: #EB5757;
+ }
+
+ &-yellow {
+ background-color: #F2C94C;
+ }
+
+ &-black {
+ background-color: #000;
+ }
+
+ &-rosegold {
+ background-color: #B76E79;
+ }
+
+ &-gold {
+ background-color: #F2C94C;
+ }
+
+ &-silver {
+ background-color: #C0C0C0;
+ }
+
+ &-spacegray {
+ background-color: #808080;
+ }
+
+ &-coral {
+ background-color: #FF6F61;
+ }
+
+ &-midnight {
+ background-color: #1A1A1A;
+ }
+
+ &-graphite {
+ background-color: #4B4B4B;
+ }
+
+ &-sierrablue {
+ background-color: #5F9EA0;
+ }
+
+ &-pink {
+ background-color: #FF69B4;
+ }
+
+ &-blue {
+ background-color: #4169E1;
+ }
+
+ &:hover {
+ border: 1px solid #75767F;
+ transition: 0.3s;
+ }
+ }
+
+ &__price {
+ display: flex;
+ gap: 8px;
+
+ margin-bottom: 16px;
+
+ align-items: center;
+
+ &-full {
+ margin: 0;
+
+ color: #F1F2F9;
+
+ font-family: Mont, sans-serif;
+ font-weight: 800;
+ font-size: 32px;
+ line-height: 41px;
+ letter-spacing: -1%;
+ }
+
+ &-discount {
+ margin: 0;
+
+ color: #75767F;
+
+ font-family: 'Mont semibold', sans-serif;
+ font-weight: 600;
+ font-size: 22px;
+ line-height: 100%;
+ text-decoration: line-through;
+ }
+ }
+
+
+ &__add {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+
+ border: none;
+
+ cursor: pointer;
+
+ width: 100%;
+ max-width: 263px;
+ height: 48px;
+
+ background-color: #905BFF;
+
+ color: #F1F2F9;
+
+ font-family: 'Mont regular', sans-serif;
+ font-weight: 700;
+ font-size: 14px;
+ line-height: 21px;
+ text-align: center;
+ transition: 0.3s;
+
+ &-link {
+ text-decoration: none;
+
+ width: 100%;
+ height: 48px;
+ }
+
+ &-added {
+ background-color: #323542;
+ }
+
+ &:hover {
+ transition: 0.3s;
+ background-color: #A378FF;
+ }
+ }
+
+ &__fav {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+
+ height: 48px;
+ width: 48px;
+
+ background-color: #323542;
+ border: 1px solid #323542;
+
+ cursor: pointer;
+ transition: 0.3s;
+
+ &-added {
+ background-color: transparent;
+ border: 1px solid #3B3E4A;
+
+ &:hover {
+ background-color: none;
+ }
+ }
+
+ &:hover {
+ transition: 0.3s;
+ background-color: #4A4D58;
+ }
+ }
+
+ &__specifications {
+ &-text {
+ margin: 0;
+
+ display: flex;
+ justify-content: space-between;
+
+ margin-bottom: 8px;
+
+ color: #75767F;
+
+ font-family: 'Mont regular', sans-serif;
+ font-weight: 700;
+ font-size: 12px;
+ line-height: 100%;
+ }
+
+ &-span {
+ color: #F1F2F9;
+
+ font-family: 'Mont regular', sans-serif;
+ font-weight: 700;
+ font-size: 12px;
+ }
+ }
+
+ &__about {
+ width: 100%;
+ max-width: 560px;
+
+ &-border {
+ height: 1px;
+ width: 100%;
+
+ margin-bottom: 32px;
+
+ background-color: #3B3E4A;
+ }
+
+ &-title {
+ margin: 0;
+
+ color: #F1F2F9;
+ font-family: Mont, sans-serif;
+ font-weight: 800;
+ font-size: 22px;
+ line-height: 140%;
+
+ margin-bottom: 16px;
+ }
+
+ &-article-title {
+ color: #F1F2F9;
+
+ font-family: 'Mont regular', sans-serif;
+ font-weight: 700;
+ font-size: 20px;
+ line-height: 100%;
+
+ margin-bottom: 16px;
+ }
+
+ &-article-subtitle {
+ color: #75767F;
+
+ font-family: 'Mont regular', sans-serif;
+ font-weight: 600;
+ font-size: 14px;
+ line-height: 21px;
+ }
+
+ &-article {
+ margin-bottom: 32px;
+ }
+ }
+
+ &__tech {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ width: 100%;
+ max-width: 560px;
+
+ &-title {
+ color: #F1F2F9;
+
+ font-family: Mont, sans-serif;
+ font-weight: 800;
+ font-size: 22px;
+ line-height: 140%;
+
+ margin-bottom: 16px;
+ }
+
+ &-border {
+ width: 100%;
+ height: 1px;
+
+ margin-bottom: 25px;
+
+ background-color: #3B3E4A;
+ }
+
+ &-text {
+ display: flex;
+
+ margin: 0;
+
+ justify-content: space-between;
+
+ color: #75767F;
+
+ font-family: 'Mont regular', sans-serif;
+ font-weight: 700;
+ font-size: 12px;
+ line-height: 100%;
+
+ margin-bottom: 8px;
+ }
+
+ &-span {
+ color: #F1F2F9;
+
+ font-family: 'Mont regular', sans-serif;
+ font-weight: 700;
+ font-size: 12px;
+ }
+ }
+
+ &__info {
+ display: flex;
+ flex-direction: column;
+ gap: 64px;
+
+ margin-bottom: 80px;
+
+ @include on-tablet {
+ flex-direction: row;
+ gap: 64px;
+ }
+ }
+
+ &__footer {
+ &-fixed {
+ position: fixed;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ z-index: 1000;
+
+ background-color: #0F1121;
+ }
+ }
+}
diff --git a/src/Components/DevSpec/PhoneSpec.tsx b/src/Components/DevSpec/PhoneSpec.tsx
new file mode 100644
index 00000000000..8a0f91ab805
--- /dev/null
+++ b/src/Components/DevSpec/PhoneSpec.tsx
@@ -0,0 +1,491 @@
+/* eslint-disable react-hooks/exhaustive-deps */
+/* eslint-disable no-console */
+/* eslint-disable max-len */
+/* eslint-disable react/jsx-no-comment-textnodes */
+import { Link, useParams, useLocation, useNavigate } from 'react-router-dom';
+import { Header } from '../Header/header';
+import { useEffect, useState } from 'react';
+import { getProductById } from '../../api/api';
+import home from '../../images/icons/Home.svg';
+import arr from '../../images/icons/Chevron (Arrow Right) grey.png';
+import backArr from '../../images/icons/Chevron (Arrow Left).svg';
+import './PhoneSpec.scss';
+import fav from '../../images/fav/Icons/Favourites (Heart Like).svg';
+import activeFav from '../../images/icons/ActiveFav.svg';
+import { useCart } from '../../Context/Context';
+import { useFav } from '../../Context/FavouritesContext';
+import { PhoneLike } from '../PhoneLike/PhoneLike';
+import { Footer } from '../Footer/Footer';
+import { Phone } from '../../types/Phone';
+import { Products } from '../../types/Products';
+import React from 'react';
+import { Aside } from '../Aside/Aside';
+import { Loader } from '../Loader/Loader';
+
+// type Props = {
+// favouritesCount: number;
+// };
+
+enum Image {
+ first = 'first',
+ second = 'second',
+ third = 'third',
+ fourth = 'fourth',
+ fifth = 'fifth',
+}
+
+export const PhoneSpec: React.FC = () => {
+ const [menuOpen, setMenuOpen] = useState(false);
+ const navigate = useNavigate();
+ const { productId } = useParams<{ productId: string }>();
+ const { pathname } = useLocation();
+ const [phone, setPhone] = useState();
+ const { totalQuantity, addToCart, removeFromCart, isInCart } = useCart();
+ const { addToFav, removeFromFav, isInFav } = useFav();
+ const { totalFavourites } = useFav();
+ const [loader, setLoader] = useState(false);
+
+ useEffect(() => {
+ window.scrollTo(0, 0);
+ }, [pathname]);
+
+ const mapPhoneToProduct = (p: Phone): Products => ({
+ id: String(p.id),
+ itemId: String(p.id),
+ name: p.name ?? 'Unknown',
+ category: 'phones',
+ fullPrice: Number(p.priceRegular ?? 0),
+ price: Number(p.priceDiscount ?? p.priceRegular ?? 0),
+ screen: p.screen ?? '—',
+ capacity: p.capacity ?? '—',
+ color: p.color ?? p.colorsAvailable?.[0] ?? '—',
+ ram: p.ram ?? '—',
+ year: new Date().getFullYear(),
+ image: typeof p.images?.[0] === 'string' ? p.images[0] : '',
+ });
+ const [, setErrorMessage] = useState(false);
+ const [image, setImage] = useState(Image.first);
+ const [color, setColor] = useState(null);
+ const images = phone?.images ?? [];
+
+ const normalizePathValue = (value: string) =>
+ value.toLowerCase().replace(/\s+/g, '-');
+
+ const getDefaultColor = (item?: Phone) => {
+ if (!item) {
+ return null;
+ }
+
+ const rawColor = item.color as unknown;
+
+ if (typeof rawColor === 'string' && rawColor.trim()) {
+ return rawColor;
+ }
+
+ if (Array.isArray(rawColor) && rawColor.length) {
+ return rawColor[0];
+ }
+
+ return item.colorsAvailable?.[0] ?? null;
+ };
+
+ // const capacitiesRaw = phone?.capacity ?? [];
+ const capacities: string[] = Array.isArray(phone?.capacityAvailable)
+ ? phone.capacityAvailable
+ : phone?.capacityAvailable
+ ? [phone.capacityAvailable]
+ : [];
+
+ const [selectedCapacity, setSelectedCapacity] = useState(null);
+
+ const path = phone?.id;
+
+ useEffect(() => {
+ if (!phone) {
+ return;
+ }
+
+ const defaultColor = getDefaultColor(phone);
+
+ if (defaultColor) {
+ setColor(defaultColor);
+ }
+ }, [phone]);
+
+ useEffect(() => {
+ if (capacities.length) {
+ setSelectedCapacity(capacities[0]);
+ }
+ }, [capacities]);
+ const imageKeys: Image[] = [
+ Image.first,
+ Image.second,
+ Image.third,
+ Image.fourth,
+ Image.fifth,
+ ];
+
+ const mainIndex =
+ imageKeys.indexOf(image) === -1 ? 0 : imageKeys.indexOf(image);
+ const imageByIndex = (i: number): Image => imageKeys[i];
+
+ useEffect(() => {
+ if (images.length <= 1) {
+ return;
+ }
+
+ const enums = [
+ Image.first,
+ Image.second,
+ Image.third,
+ Image.fourth,
+ Image.fifth,
+ ];
+ const id = setInterval(() => {
+ setImage(prev => {
+ const idx = enums.indexOf(prev);
+ const safeIdx = idx === -1 ? 0 : idx;
+ const next = enums[(safeIdx + 1) % images.length];
+
+ return next;
+ });
+ }, 5000);
+
+ return () => clearInterval(id);
+ }, [images.length]);
+
+ useEffect(() => {
+ if (phone?.capacity) {
+ const caps = Array.isArray(phone.capacity)
+ ? phone.capacity
+ : [phone.capacity];
+
+ setSelectedCapacity(caps[0]);
+ }
+ }, [phone?.capacity]);
+
+ useEffect(() => {
+ if (!productId) {
+ return;
+ }
+
+ setLoader(true);
+ getProductById(productId)
+ .then(product => {
+ if (product) {
+ setPhone(product);
+ console.log(product);
+ }
+ })
+ .catch(() => setErrorMessage(true))
+ .finally(() => setLoader(false));
+ }, [productId]);
+ console.log('productId from params:', productId);
+
+ return (
+
+
+ {menuOpen && (
+
+ )}
+ {loader &&
}
+ {!loader && (
+
+
+
+
+
+
+
+
+
+ Phones
+
+
+
+
+
{phone?.name}
+
+
+
+
Back
+
+
{phone?.name}
+
+
+
+ {images[mainIndex] && (
+
+ )}
+
+
+ {images.map((src, i) => (
+
+
setImage(imageByIndex(i))}
+ />
+
+ ))}
+
+
+ {images[mainIndex] && (
+
+ )}
+
+
+
+
Available colors
+
+ {phone?.colorsAvailable.map(c => {
+ const updatedPath = path?.replace(
+ /-[^-]+$/,
+ `-${normalizePathValue(c)}`,
+ );
+
+ return (
+
+
{
+ setColor(c);
+ navigate(`../phones/${updatedPath}`);
+ }}
+ className={`phone__color-color phone__color-${normalizePathValue(c)}`}
+ >
+
+ );
+ })}
+
+
+
+
Select capacity
+
+ {phone?.capacityAvailable.map(cap => {
+ const activeColor = color || getDefaultColor(phone) || '';
+ const updatedPathCap = path?.replace(
+ /-[^-]+-[^-]+$/,
+ `-${normalizePathValue(cap)}-${normalizePathValue(activeColor)}`,
+ );
+
+ return (
+
{
+ setSelectedCapacity(cap);
+ navigate(`../phones/${updatedPathCap}`);
+ }}
+ onKeyDown={e =>
+ (e.key === 'Enter' || e.key === ' ') &&
+ setSelectedCapacity(cap)
+ }
+ aria-pressed={cap === selectedCapacity}
+ >
+ {cap}
+
+ );
+ })}
+
+
+
+
+
+
+
+ {phone?.priceRegular}$
+
+
+ {phone?.priceDiscount}$
+
+
+
+
+
{
+ if (phone) {
+ const product = mapPhoneToProduct(phone);
+
+ if (isInCart(String(product.id))) {
+ removeFromCart(String(product.id));
+ } else {
+ addToCart(product);
+ }
+ }
+ }}
+ >
+ {phone && isInCart(String(phone.id))
+ ? 'Added'
+ : 'Add to cart'}
+
+
{
+ if (phone) {
+ const product = mapPhoneToProduct(phone);
+
+ if (isInFav(String(product.id))) {
+ removeFromFav(String(product.id));
+ } else {
+ addToFav(product);
+ }
+ }
+ }}
+ >
+
+
+
+
+
+ Screen{' '}
+
+ {phone?.screen}
+
+
+
+ Capacity{' '}
+
+ {phone?.capacityAvailable.join(', ')}
+
+
+
+ Processor{' '}
+
+ {phone?.processor}
+
+
+
+ RAM{' '}
+
+ {phone?.ram}
+
+
+
+
+
+
+
+
About
+
+
+
+ And then there was Pro
+
+
+
+ A transformative triple‑camera system that adds tons of
+ capability without complexity.
+
+ {''}
+
+ An unprecedented leap in battery life. And a mind‑blowing
+ chip that doubles down on machine learning and pushes the
+ boundaries boundaries of what a smartphone can do. Welcome
+ to the first powerful enough to be called Pro.
+
+
+
+
+
Camera
+
+ Meet the first triple‑camera system to combine cutting‑edge
+ technology with the legendary simplicity of iPhone. Capture up
+ to four times more scene. Get beautiful images in drastically
+ lower light. Shoot the highest‑quality video in a smartphone —
+ then edit with the same tools you love for photos. You’ve
+ never shot with anything like it.
+
+
+
+
+ Shoot it. Flip it. Zoom it. Crop it. Cut it. Light it. Tweak
+ it. Love it.
+
+
+ A transformative triple‑camera system that adds tons of
+ capability without complexity. iPhone 11 Pro lets you capture
+ videos that are beautifully true to life, with greater detail
+ and smoother motion. Epic processing power means it can shoot
+ 4K video with extended dynamic range and cinematic video
+ stabilization — all at 60 fps. You get more creative control,
+ too, with four times more scene and powerful new editing tools
+ to play with.
+
+
+
+
+
Tech specs
+
+
+ Screen {phone?.screen}
+
+
+ Resolution{' '}
+ {phone?.resolution}
+
+
+ Processor{' '}
+ {phone?.processor}
+
+
+ RAM {phone?.ram}
+
+
+ Built in memory{' '}
+
+ {phone?.capacityAvailable.join(', ')}
+
+
+
+ Camera {phone?.camera}
+
+
+ Zoom {phone?.zoom}
+
+
+ Cell{' '}
+
+ {phone?.cell.join(', ')}
+
+
+
+
+
+
+ )}
+
+
+
+
+ );
+};
diff --git a/src/Components/DevSpec/TabletSpec.tsx b/src/Components/DevSpec/TabletSpec.tsx
new file mode 100644
index 00000000000..9df077da74e
--- /dev/null
+++ b/src/Components/DevSpec/TabletSpec.tsx
@@ -0,0 +1,486 @@
+/* eslint-disable react-hooks/exhaustive-deps */
+/* eslint-disable no-console */
+/* eslint-disable max-len */
+/* eslint-disable react/jsx-no-comment-textnodes */
+import { Link, useParams, useLocation, useNavigate } from 'react-router-dom';
+import { Header } from '../Header/header';
+import { useEffect, useState } from 'react';
+import { getTabletById } from '../../api/api';
+import home from '../../images/icons/Home.svg';
+import arr from '../../images/icons/Chevron (Arrow Right) grey.png';
+import backArr from '../../images/icons/Chevron (Arrow Left).svg';
+import './PhoneSpec.scss';
+import fav from '../../images/fav/Icons/Favourites (Heart Like).svg';
+import activeFav from '../../images/icons/ActiveFav.svg';
+import { useCart } from '../../Context/Context';
+import { useFav } from '../../Context/FavouritesContext';
+import { PhoneLike } from '../PhoneLike/PhoneLike';
+import { Footer } from '../Footer/Footer';
+import { Products } from '../../types/Products';
+import React from 'react';
+import { Tablet } from '../../types/Tablets';
+import { Aside } from '../Aside/Aside';
+import { Loader } from '../Loader/Loader';
+
+// type Props = {
+// favouritesCount: number;
+// };
+
+enum Image {
+ first = 'first',
+ second = 'second',
+ third = 'third',
+ fourth = 'fourth',
+ fifth = 'fifth',
+}
+
+export const TabletSpec: React.FC = () => {
+ const { productId } = useParams<{ productId: string }>();
+ const { pathname } = useLocation();
+ const [tablet, setTablet] = useState();
+ const { totalQuantity, addToCart, removeFromCart, isInCart } = useCart();
+ const { addToFav, removeFromFav, isInFav, totalFavourites } = useFav();
+ const [menuOpen, setMenuOpen] = useState(false);
+
+ useEffect(() => {
+ window.scrollTo(0, 0);
+ }, [pathname]);
+
+ const mapTabletToProduct = (t: Tablet): Products => ({
+ id: String(t.id),
+ itemId: String(t.id),
+ name: t.name ?? 'Unknown',
+ category: 'tablets',
+ fullPrice: Number(t.priceRegular ?? 0),
+ price: Number(t.priceDiscount ?? t.priceRegular ?? 0),
+ screen: t.screen ?? '—',
+ capacity: t.capacity ?? '—',
+ color: t.color ?? t.colorsAvailable ?? '—',
+ ram: t.ram ?? '—',
+ year: new Date().getFullYear(),
+ image: Array.isArray(t.images) ? (t.images[0] ?? '') : (t.images ?? ''),
+ });
+ const [, setErrorMessage] = useState(false);
+ const [loader, setLoader] = useState(false);
+ const [image, setImage] = useState(Image.first);
+ const [color, setColor] = useState(null);
+ const images = tablet?.images || [];
+ const path = tablet?.id;
+
+ const normalizePathValue = (value: string) =>
+ value.toLowerCase().replace(/\s+/g, '-');
+
+ const getDefaultColor = (item?: Tablet) => {
+ if (!item) {
+ return null;
+ }
+
+ const rawColor = item.color as unknown;
+
+ if (typeof rawColor === 'string' && rawColor.trim()) {
+ return rawColor;
+ }
+
+ if (Array.isArray(rawColor) && rawColor.length) {
+ return rawColor[0];
+ }
+
+ return item.colorsAvailable?.[0] ?? null;
+ };
+
+ // const capacitiesRaw = tablet?.capacity ?? [];
+ const capacities: string[] = Array.isArray(tablet?.capacity)
+ ? tablet.capacity
+ : tablet?.capacity
+ ? [tablet.capacity]
+ : [];
+
+ const [selectedCapacity, setSelectedCapacity] = useState(null);
+ const navigate = useNavigate();
+
+ useEffect(() => {
+ if (!tablet) {
+ return;
+ }
+
+ const defaultColor = getDefaultColor(tablet);
+
+ if (defaultColor) {
+ setColor(defaultColor);
+ }
+ }, [tablet]);
+
+ useEffect(() => {
+ if (capacities.length) {
+ setSelectedCapacity(capacities[0]);
+ }
+ }, [capacities]);
+ const imageKeys: Image[] = [
+ Image.first,
+ Image.second,
+ Image.third,
+ Image.fourth,
+ Image.fifth,
+ ];
+
+ const mainIndex =
+ imageKeys.indexOf(image) === -1 ? 0 : imageKeys.indexOf(image);
+ const imageByIndex = (i: number): Image => imageKeys[i];
+
+ useEffect(() => {
+ if (images.length <= 1) {
+ return;
+ }
+
+ const enums = [
+ Image.first,
+ Image.second,
+ Image.third,
+ Image.fourth,
+ Image.fifth,
+ ];
+ const id = setInterval(() => {
+ setImage(prev => {
+ const idx = enums.indexOf(prev);
+ const safeIdx = idx === -1 ? 0 : idx;
+ const next = enums[(safeIdx + 1) % images.length];
+
+ return next;
+ });
+ }, 5000);
+
+ return () => clearInterval(id);
+ }, [images.length]);
+
+ useEffect(() => {
+ if (tablet?.capacity) {
+ const caps = Array.isArray(tablet.capacity)
+ ? tablet.capacity
+ : [tablet.capacity];
+
+ setSelectedCapacity(caps[0]);
+ }
+ }, [tablet?.capacity]);
+
+ useEffect(() => {
+ if (!productId) {
+ return;
+ }
+
+ setLoader(true);
+ getTabletById(productId)
+ .then(product => {
+ if (product) {
+ setTablet(product);
+ console.log(product);
+ }
+ })
+ .catch(() => setErrorMessage(true))
+ .finally(() => setLoader(false));
+ }, [productId]);
+ console.log('productId from params:', productId);
+
+ return (
+
+
+ {menuOpen && (
+
+ )}
+ {loader &&
}
+ {!loader && (
+
+
+
+
+
+
+
+
+
+ Tablets
+
+
+
+
+
{tablet?.name}
+
+
+
+
Back
+
+
{tablet?.name}
+
+
+
+ {images[mainIndex] && (
+
+ )}
+
+
+ {images.map((src, i) => (
+
+
setImage(imageByIndex(i))}
+ />
+
+ ))}
+
+
+ {images[mainIndex] && (
+
+ )}
+
+
+
+
Available colors
+
+ {tablet?.colorsAvailable.map(c => {
+ const updatedPath = path?.replace(
+ /-[^-]+$/,
+ `-${normalizePathValue(c)}`,
+ );
+
+ return (
+
+
{
+ setColor(c);
+ navigate(`../tablets/${updatedPath}`);
+ }}
+ className={`phone__color-color phone__color-${normalizePathValue(c)}`}
+ >
+
+ );
+ })}
+
+
+
+
Select capacity
+
+ {tablet?.capacityAvailable.map(cap => {
+ const activeColor = color || getDefaultColor(tablet) || '';
+ const updatedPathCap = path?.replace(
+ /-[^-]+-[^-]+$/,
+ `-${normalizePathValue(cap)}-${normalizePathValue(activeColor)}`,
+ );
+
+ return (
+
{
+ setSelectedCapacity(cap);
+ navigate(`../tablets/${updatedPathCap}`);
+ }}
+ onKeyDown={e =>
+ (e.key === 'Enter' || e.key === ' ') &&
+ setSelectedCapacity(cap)
+ }
+ aria-pressed={cap === selectedCapacity}
+ >
+ {cap}
+
+ );
+ })}
+
+
+
+
+
+
+
+ {tablet?.priceRegular}$
+
+
+ {tablet?.priceDiscount}$
+
+
+
+
+
{
+ if (tablet) {
+ const product = mapTabletToProduct(tablet);
+
+ if (isInCart(String(product.id))) {
+ removeFromCart(String(product.id));
+ } else {
+ addToCart(product);
+ }
+ }
+ }}
+ >
+ {tablet && isInCart(String(tablet.id))
+ ? 'Added'
+ : 'Add to cart'}
+
+
{
+ if (tablet) {
+ const product = mapTabletToProduct(tablet);
+
+ if (isInFav(String(product.id))) {
+ removeFromFav(String(product.id));
+ } else {
+ addToFav(product);
+ }
+ }
+ }}
+ >
+
+
+
+
+
+ Screen{' '}
+
+ {tablet?.screen}
+
+
+
+ Capacity{' '}
+
+ {tablet?.capacity}
+
+
+
+ Processor{' '}
+
+ {tablet?.processor}
+
+
+
+ RAM{' '}
+
+ {tablet?.ram}
+
+
+
+
+
+
+
+
About
+
+
+
+ And then there was Pro
+
+
+
+ A transformative triple‑camera system that adds tons of
+ capability without complexity.
+
+ {''}
+
+ An unprecedented leap in battery life. And a mind‑blowing
+ chip that doubles down on machine learning and pushes the
+ boundaries boundaries of what a smartphone can do. Welcome
+ to the first powerful enough to be called Pro.
+
+
+
+
+
Camera
+
+ Meet the first triple‑camera system to combine cutting‑edge
+ technology with the legendary simplicity of iPhone. Capture up
+ to four times more scene. Get beautiful images in drastically
+ lower light. Shoot the highest‑quality video in a smartphone —
+ then edit with the same tools you love for photos. You’ve
+ never shot with anything like it.
+
+
+
+
+ Shoot it. Flip it. Zoom it. Crop it. Cut it. Light it. Tweak
+ it. Love it.
+
+
+ A transformative triple‑camera system that adds tons of
+ capability without complexity. iPhone 11 Pro lets you capture
+ videos that are beautifully true to life, with greater detail
+ and smoother motion. Epic processing power means it can shoot
+ 4K video with extended dynamic range and cinematic video
+ stabilization — all at 60 fps. You get more creative control,
+ too, with four times more scene and powerful new editing tools
+ to play with.
+
+
+
+
+
Tech specs
+
+
+ Screen{' '}
+ {tablet?.screen}
+
+
+ Resolution{' '}
+ {tablet?.resolution}
+
+
+ Processor{' '}
+ {tablet?.processor}
+
+
+ RAM {tablet?.ram}
+
+
+ Built in memory{' '}
+ {tablet?.capacity}
+
+
+ Camera{' '}
+ {tablet?.camera}
+
+
+ Zoom {tablet?.zoom}
+
+
+ Cell {tablet?.cell}
+
+
+
+
+
+ )}
+
+
+
+
+ );
+};
diff --git a/src/Components/Discouned/Discounted.tsx b/src/Components/Discouned/Discounted.tsx
new file mode 100644
index 00000000000..726ceae0c6d
--- /dev/null
+++ b/src/Components/Discouned/Discounted.tsx
@@ -0,0 +1,88 @@
+import { Discounted } from '../../types/Discounted';
+import { ProductCard } from '../ProductCard/ProductCard';
+import React, { useEffect, useRef, useState } from 'react';
+import { Link } from 'react-router-dom';
+
+type Props = {
+ DiscountedProducts: Discounted[];
+ currentSlide: number;
+ visibleCount: number;
+};
+
+export const Discount: React.FC = ({
+ DiscountedProducts,
+ currentSlide,
+ visibleCount,
+}) => {
+ const trackRef = useRef(null);
+ const [cardStep, setCardStep] = useState(0);
+ const gap = 24;
+
+ useEffect(() => {
+ const updateCardStep = () => {
+ const track = trackRef.current;
+ const card = track?.querySelector('.products-phone');
+
+ if (!card) {
+ setCardStep(0);
+
+ return;
+ }
+
+ setCardStep(card.offsetWidth + gap);
+ };
+
+ updateCardStep();
+ window.addEventListener('resize', updateCardStep);
+
+ return () => window.removeEventListener('resize', updateCardStep);
+ }, [DiscountedProducts.length, visibleCount]);
+
+ return (
+
+
+ {DiscountedProducts.map(product => (
+
+
+
+
+
+
+
{product.name}
+
${product.fullPrice}
+
+
+
+
+
+ Screen{' '}
+ {product.screen}
+
+
+ Capacity{' '}
+ {product.capacity}
+
+
+ RAM {product.ram}
+
+
+
+
+
+
+ ))}
+
+
+ );
+};
diff --git a/src/Components/ErrorNotification/ErrorNotification.scss b/src/Components/ErrorNotification/ErrorNotification.scss
new file mode 100644
index 00000000000..47889d7dcb4
--- /dev/null
+++ b/src/Components/ErrorNotification/ErrorNotification.scss
@@ -0,0 +1,13 @@
+
+@import '../../utils/mixins';
+
+.error-notification {
+ @include content-padding-inline;
+
+ &__title {
+ font-family: Mont, sans-serif;
+ font-weight: 800;
+ font-size: 48px;
+ line-height: 56px;
+ }
+}
diff --git a/src/Components/ErrorNotification/ErrorNotification.tsx b/src/Components/ErrorNotification/ErrorNotification.tsx
new file mode 100644
index 00000000000..544d88a6fbd
--- /dev/null
+++ b/src/Components/ErrorNotification/ErrorNotification.tsx
@@ -0,0 +1,16 @@
+import React from 'react';
+import { Header } from '../Header/header';
+import { Footer } from '../Footer/Footer';
+
+export const ErrorNotification = () => {
+ return (
+ <>
+ {}} />
+
+
Page Not Found
+
+
+
+ >
+ );
+};
diff --git a/src/Components/Favourites/Favourites.scss b/src/Components/Favourites/Favourites.scss
new file mode 100644
index 00000000000..bb2e585891b
--- /dev/null
+++ b/src/Components/Favourites/Favourites.scss
@@ -0,0 +1,98 @@
+@import '../../utils/mixins';
+
+.favourites {
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ place-items: center;
+ min-height: 100vh;
+
+
+ &__content {
+ @include content-padding-inline;
+
+ width: 100%;
+
+ margin-inline: 16px;
+
+ @include on-tablet {
+ margin-inline: 24px;
+ }
+
+ @include on-desktop {
+ margin-inline: 32px;
+ }
+ }
+
+ &__header {
+ width: 100%;
+ }
+
+ &__path {
+ margin-top: 25px;
+ margin-bottom: 40px;
+
+ display: flex;
+ gap: 8px;
+ text-align: center;
+ align-items: center;
+
+ &-name {
+ color: #75767F;
+ font-family: Mont, sans-serif;
+ font-weight: 700;
+ font-size: 12px;
+ line-height: 100%;
+ }
+ }
+
+ &__items {
+ width: 100%;
+ flex: 2 1 0;
+ min-width: 0;
+
+ &-empty {
+ font-family: Mont, sans-serif;
+ font-weight: 800;
+ font-size: 32px;
+ line-height: 41px;
+ letter-spacing: -1%;
+
+ @include on-tablet {
+ font-size: 48px;
+ line-height: 56px;
+ }
+ }
+ }
+
+ &__sub {
+ margin-top: 8px;
+ font-family: 'Mont Regular', sans-serif;
+ font-weight: 600;
+ font-size: 14px;
+ line-height: 21px;
+ color: #75767F;
+
+ margin-bottom: 40px;
+ }
+
+ &__footer {
+ position: relative;
+ width: 100%;
+ margin-top: auto;
+ padding-top: 40px;
+ }
+
+ &__title {
+ font-family: Mont, sans-serif;
+ font-weight: 800;
+ font-size: 32px;
+ line-height: 41px;
+ letter-spacing: -1%;
+
+ @include on-tablet {
+ font-size: 48px;
+ line-height: 56px;
+ }
+ }
+}
diff --git a/src/Components/Favourites/Favourites.tsx b/src/Components/Favourites/Favourites.tsx
new file mode 100644
index 00000000000..082c64de79d
--- /dev/null
+++ b/src/Components/Favourites/Favourites.tsx
@@ -0,0 +1,102 @@
+import React, { useState } from 'react';
+import { Header } from '../Header/header';
+import { useCart } from '../../Context/Context';
+import { useFav } from '../../Context/FavouritesContext';
+import { Footer } from '../Footer/Footer';
+import ArrowGray from '../../images/icons/ChevronGray.svg';
+import Home from '../../images/icons/Home.svg';
+import './Favourites.scss';
+import { Link } from 'react-router-dom';
+import { ProductCard } from '../ProductCard/ProductCard';
+import { Aside } from '../Aside/Aside';
+
+export const Favourites = () => {
+ const [menuOpen, setMenuOpen] = useState(false);
+ const { totalQuantity } = useCart();
+ const { totalFavourites, items = [] } = useFav();
+
+ return (
+
+
+
+ {menuOpen && (
+
+ )}
+
+
+
+
+
+
+
+
Favourites
+
+
Favourites
+
{totalFavourites} items
+
+ {totalFavourites === 0 && (
+
+ Your favourites list is empty
+
+ )}
+
+ {items.map(item => (
+
+
+
+
+
+
{item.product.name}
+
+ {item.product.fullPrice}$
+
+
+
+
+ Screen{' '}
+
+ {item.product.screen}
+
+
+
+ Capacity{' '}
+
+ {item.product.capacity}
+
+
+
+ RAM{' '}
+
+ {item.product.ram}
+
+
+
+
+
+
+ ))}
+
+
+
+
+
+
+
+ );
+};
diff --git a/src/Components/Filter/Filter.scss b/src/Components/Filter/Filter.scss
new file mode 100644
index 00000000000..a7cb6e6b036
--- /dev/null
+++ b/src/Components/Filter/Filter.scss
@@ -0,0 +1,157 @@
+.filter {
+ display: flex;
+ gap: 16px;
+
+ margin-bottom: 24px;
+
+ &__number {
+ position: absolute;
+ z-index: 10;
+
+ display: flex;
+ flex-direction: column;
+
+ border: #323542 1px solid;
+
+ &-option {
+ cursor: pointer;
+
+ height: 40px;
+ width: 128px;
+
+ color: #75767F;
+ font-family: 'Mont Regular', sans-serif;
+ text-align: left;
+
+ border: none;
+ padding-left: 12px;
+
+ background-color: #0F1121;
+
+ transition: 0.3s;
+
+ &:hover {
+ color: #F1F2F9;
+ background-color: #323542;
+ transition: 0.3s;
+ }
+ }
+ }
+
+ &__options {
+ opacity: 0;
+ z-index: -10;
+ transform: translateX(-200px);
+
+ border: 1px solid #323542;
+
+ display: flex;
+ flex-direction: column;
+
+ position: absolute;
+
+ background-color: #0F1121;
+ transition: 0.5s;
+
+ &-active {
+ transition: 0.5s;
+ opacity: 1;
+ z-index: 10;
+ transform: translateX(0);
+ }
+ }
+
+ &__option {
+ padding-left: 12px;
+
+ cursor: pointer;
+
+ background-color: #0F1121;
+
+ height: 32px;
+ width: 174px;
+
+ border: none;
+
+ color: #75767F;
+ font-family: "Mont Regular", sans-serif;
+ text-align: left;
+ font-weight: 600;
+ font-size: 14px;
+ line-height: 21px;
+ transition: 0.3s;
+
+ &:hover {
+ color: #F1F2F9;
+ background-color: #323542;
+ transition: 0.3s;
+ }
+
+ &-first {
+ padding-top: 8px;
+ }
+
+ &-last {
+ padding-bottom: 8px;
+ }
+ }
+
+ &__newest {
+ &-title {
+ color: #75767F;
+ margin-bottom: 4px;
+
+ font-family: "Mont Regular", sans-serif;
+ font-weight: 700;
+ font-size: 12px;
+ line-height: 100%;
+ }
+
+ &-checkbox {
+ z-index: 15;
+
+ height: 40px;
+ width: 176px;
+
+ cursor: pointer;
+
+ color: #F1F2F9;
+
+ font-family: "Mont Regular", sans-serif;
+ font-weight: 700;
+ font-size: 14px;
+ line-height: 21px;
+
+ padding-left: 12px;
+
+ background-color: #323542;
+ border: 1px solid #323542;
+
+
+ margin-bottom: 4px;
+ transition: 0.3s;
+
+ &-number {
+ width: 128px;
+ }
+
+ &-active {
+ transition: 0.3s;
+ border: 1px solid #905BFF;
+ }
+ }
+
+ &-option {
+ display: flex;
+ justify-content: space-between;
+
+ &-arrow {
+ transition: 0.3s;
+ &-active {
+ transition: 0.3s;
+ transform: rotate(180deg);
+ }
+ }
+ }
+ }
+}
diff --git a/src/Components/Filter/Filter.tsx b/src/Components/Filter/Filter.tsx
new file mode 100644
index 00000000000..023ffbbd015
--- /dev/null
+++ b/src/Components/Filter/Filter.tsx
@@ -0,0 +1,176 @@
+import './Filter.scss';
+import Arrow from '../../images/icons/arrow-down.svg';
+import React from 'react';
+import { useSearchParams } from 'react-router-dom';
+
+export type FilterValue =
+ | 'Newest'
+ | 'Popular'
+ | 'Price: Low to High'
+ | 'Price: High to Low';
+
+export type ItemQuantity = 16 | 32 | 64;
+
+type FilterProps = {
+ activeQuantity: ItemQuantity;
+ setActiveQuantity: (quantity: ItemQuantity) => void;
+ activeFilter: FilterValue;
+ setActiveFilter: (filter: FilterValue) => void;
+};
+
+export const Filter = ({
+ activeFilter,
+ setActiveFilter,
+ activeQuantity,
+ setActiveQuantity,
+}: FilterProps) => {
+ const [isOptionsActive, setIsOptionsActive] = React.useState(false);
+ const [isNumberOptionsActive, setIsNumberOptionsActive] =
+ React.useState(false);
+ const [searchParams, setSearchParams] = useSearchParams();
+
+ const handleQuantityChange = (value: string) => {
+ const params = new URLSearchParams(searchParams);
+
+ params.set('Quantity', value);
+
+ setSearchParams(params);
+
+ setIsNumberOptionsActive(false);
+ };
+
+ const handleFilterChange = (value: string) => {
+ const params = new URLSearchParams(searchParams);
+
+ params.set('Filter', value);
+
+ setSearchParams(params);
+
+ setIsOptionsActive(false);
+ };
+
+ return (
+
+
+
Sort by
+
setIsOptionsActive(!isOptionsActive)}
+ type="button"
+ className={`filter__newest-checkbox ${isOptionsActive ? 'filter__newest-checkbox-active' : ''}`}
+ >
+
+
{activeFilter}
+
+
+
+
+ {
+ setActiveFilter('Newest');
+ setIsOptionsActive(!isOptionsActive);
+ handleFilterChange('Newest');
+ }}
+ className="filter__option filter__option-first"
+ >
+ Newest
+
+ {
+ setActiveFilter('Popular');
+ setIsOptionsActive(!isOptionsActive);
+ handleFilterChange('Popular');
+ }}
+ className="filter__option"
+ >
+ Popular
+
+ {
+ setActiveFilter('Price: Low to High');
+ setIsOptionsActive(!isOptionsActive);
+ handleFilterChange('Price: Low to High');
+ }}
+ className="filter__option filter__option-last"
+ >
+ Price: Low to High
+
+ {
+ setActiveFilter('Price: High to Low');
+ setIsOptionsActive(!isOptionsActive);
+ handleFilterChange('Price: High to Low');
+ }}
+ type="button"
+ className="filter__option filter__option-last"
+ >
+ Price: High to Low
+
+
+
+
+
+
Items on page
+
setIsNumberOptionsActive(!isNumberOptionsActive)}
+ type="button"
+ className={`filter__newest-checkbox filter__newest-checkbox-number ${isNumberOptionsActive ? 'filter__newest-checkbox-active' : ''}`}
+ >
+
+
{activeQuantity}
+
+
+
+
+ {
+ setActiveQuantity(16);
+ setIsNumberOptionsActive(!isNumberOptionsActive);
+ handleQuantityChange('16');
+ }}
+ >
+ 16
+
+ {
+ setActiveQuantity(32);
+ setIsNumberOptionsActive(!isNumberOptionsActive);
+ handleQuantityChange('32');
+ }}
+ >
+ 32
+
+ {
+ setActiveQuantity(64);
+ setIsNumberOptionsActive(!isNumberOptionsActive);
+ handleQuantityChange('64');
+ }}
+ >
+ 64
+
+
+
+
+ );
+};
diff --git a/src/Components/Footer/Footer.scss b/src/Components/Footer/Footer.scss
new file mode 100644
index 00000000000..846c6c58905
--- /dev/null
+++ b/src/Components/Footer/Footer.scss
@@ -0,0 +1,157 @@
+
+@import '../../utils/mixins';
+
+.footer {
+ width: 100%;
+ min-height: auto;
+
+ @include on-tablet {
+ height: 96px;
+ }
+
+ &__navs {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ width: 100%;
+
+ @include on-tablet {
+ display: flex;
+ width: auto;
+ flex-direction: row;
+ gap: 32px;
+ }
+
+ @include on-desktop {
+ gap: 106px;
+ }
+ }
+
+ &__line {
+ margin: 0;
+ border: 0.5px solid #323542;
+ }
+
+ &__container {
+ @include content-padding-inline;
+ }
+
+ &__nav {
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+
+ margin-top: 16px;
+ margin-bottom: 16px;
+ align-items: center;
+ justify-content: center;
+
+ @include on-tablet {
+ flex-direction: row;
+ display: flex;
+ gap: 32px;
+ margin-top: 30px;
+ height: 100%;
+ justify-content: space-between;
+ }
+
+ &-link {
+ font-family: 'Mont semibold', sans-serif;
+ color: #F1F2F9;
+ font-weight: 800;
+ font-size: 10px;
+ line-height: 11px;
+ letter-spacing: 4%;
+ text-transform: uppercase;
+ text-decoration: none;
+ transition: 0.3s;
+
+ &:hover {
+ color: #A378FF;
+ transition: 0.3s;
+ }
+
+ @include on-tablet {
+ font-size: 12px;
+ }
+ }
+
+ &-list {
+ list-style: none;
+ display: flex;
+ }
+ }
+
+ &__logo {
+ display: flex;
+ align-items: center;
+ justify-content: left;
+ width: 100%;
+
+ @include on-tablet {
+ width: auto;
+ justify-content: left;
+ }
+
+ &-img {
+ height: 24px;
+
+ @include on-tablet {
+ height: 32px;
+ }
+ }
+ }
+
+ &__back {
+ display: flex;
+ gap: 8px;
+ align-items: center;
+ margin-bottom: 0;
+ flex-direction: column-reverse;
+ width: 100%;
+
+ @include on-tablet {
+ margin-bottom: 0;
+ flex-direction: row;
+ gap: 16px;
+ justify-content: flex-end;
+ width: auto;
+ }
+ &-text {
+ color: #75767F;
+ font-family: 'Mont regular', sans-serif;
+ font-weight: 700;
+ font-size: 10px;
+ line-height: 100%;
+ letter-spacing: 0%;
+ text-align: center;
+
+ @include on-tablet {
+ font-size: 12px;
+ text-align: right;
+ }
+ }
+
+ &-button {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+
+ cursor: pointer;
+ border: 0;
+ height: 32px;
+ width: 32px;
+ background-color: #323542;
+ transition: 0.3s;
+
+ &-img {
+ height: 16px;
+ }
+
+ &:hover {
+ background-color: #4A4D58;
+ transition: 0.3s;
+ }
+ }
+ }
+}
diff --git a/src/Components/Footer/Footer.tsx b/src/Components/Footer/Footer.tsx
new file mode 100644
index 00000000000..f4e29c0d671
--- /dev/null
+++ b/src/Components/Footer/Footer.tsx
@@ -0,0 +1,57 @@
+import React from 'react';
+import { Link, NavLink } from 'react-router-dom';
+
+export const Footer = () => {
+ const logo = new URL('../../images/Logo.png', import.meta.url).href;
+ const arrowUp = new URL(
+ '../../images/icons/Chevron Arrow up.svg',
+ import.meta.url,
+ ).href;
+
+ const handleScrollToTop = () => {
+ window.scrollTo({ top: 0, behavior: 'smooth' });
+ };
+
+ const navLink = [
+ { id: 'https://github.com/u5135039754-dev/', title: 'Github' },
+ { id: 'https://github.com/u5135039754-dev/', title: 'Contacts' },
+ { id: 'https://github.com/u5135039754-dev/', title: 'rights' },
+ ];
+
+ return (
+
+
+
+
+
+
+
+
+
+
+ {navLink.map(link => (
+
+
+
+ {link.title}
+
+
+
+ ))}
+
+
+
+
Back to top
+
+
+
+
+
+
+
+ );
+};
diff --git a/src/Components/Header/header.scss b/src/Components/Header/header.scss
new file mode 100644
index 00000000000..58f060f3fd7
--- /dev/null
+++ b/src/Components/Header/header.scss
@@ -0,0 +1,213 @@
+
+@import '../../utils/mixins';
+
+.header {
+ display: grid;
+ align-items: center;
+ min-height: 48px;
+ box-shadow: 0 1px #323542;
+ padding-left: 16px;
+
+ align-content: center;
+
+ @include on-tablet {
+ padding-left: 24px;
+ }
+
+ @include on-desktop {
+ height: 64px;
+ padding-left: 24px;
+ }
+
+ &__menu {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+
+
+ @include on-tablet {
+ display: none;
+ }
+
+ &-img {
+ height: 16px;
+ }
+ }
+
+ &__container {
+ height: 100%;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ }
+
+ &__vectors {
+ display: none;
+ min-height: 48px;
+ min-width: 48px;
+
+ @include on-tablet {
+ display: flex;
+ align-items: center;
+ justify-content: right;
+ }
+
+ @include on-desktop {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ min-height: 64px;
+ min-width: 64px;
+ }
+
+ &-selected {
+ &::after {
+ content: '';
+ display: block;
+
+ width: 100%;
+ height: 3px;
+ background: #F1F2F9;
+
+ animation: slide-in-right 0.5s ease-out;
+
+ @include on-tablet {
+ position: relative;
+ top: 0;
+ }
+
+ @include on-desktop {
+ position: static;
+ left: 0;
+ top: 200px;
+ bottom: 0;
+ }
+ }
+ }
+ }
+
+ &__fav {
+ &-img {
+ height: 16px;
+ }
+ }
+
+ &__button {
+ display: none;
+ height: 48px;
+ width: 48px;
+
+ @include on-tablet {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ }
+
+ @include on-desktop {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ height: 64px;
+ width: 64px;
+ }
+
+ cursor: pointer;
+ background-color: #0F1121;
+
+ border: none;
+
+ align-content: center;
+ box-shadow: -1px 0 #323542;
+
+ &-menu {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ }
+ }
+
+ &__count {
+ display: flex;
+ justify-content: center;
+ position: absolute;
+ align-items: center;
+
+ border: 1px #0F1121 solid;
+ text-align: center;
+
+ border-radius: 50%;
+
+ width: 14px;
+ height: 14px;
+ background-color: #EB5757;
+
+ @include on-tablet {
+ top: 10px;
+ right: 10px;
+ }
+
+ @include on-desktop {
+ top: 18px;
+ right: 18px;
+ }
+
+ &-fav {
+ right: 81px;
+ }
+
+ &-number {
+ color: #F1F2F9;
+
+ font-family: 'Mont regular', sans-serif;
+ font-weight: 700;
+ font-size: 9px;
+ line-height: 100%;
+ text-align: center;
+ }
+
+ &-left {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+
+ border: 1px #0F1121 solid;
+
+ position: absolute;
+
+ @include on-tablet {
+ top: -1px;
+ right: 48px;
+ }
+
+ @include on-desktop {
+ top: -1px;
+ right: 63px;
+ }
+
+ width: 14px;
+ height: 14px;
+
+ background-color: #EB5757;
+ border-radius: 50%;
+
+ }
+ }
+
+ &__basket {
+ &-img {
+ height: 16px;
+ }
+
+ }
+}
+
+@keyframes slide-in-right {
+ from {
+ transform:scaleX(0);
+ opacity: 0;
+ }
+ to {
+ transform: scaleX(100%);
+ opacity: 1;
+ }
+}
diff --git a/src/Components/Header/header.tsx b/src/Components/Header/header.tsx
new file mode 100644
index 00000000000..df5871fcf6c
--- /dev/null
+++ b/src/Components/Header/header.tsx
@@ -0,0 +1,30 @@
+import React from 'react';
+import { TopBar } from '../TopBar/TopBar';
+
+type Props = {
+ cartItemsCount: number;
+ favouritesCount: number;
+ setMenuOpen: React.Dispatch>;
+};
+
+export const Header: React.FC = ({
+ cartItemsCount,
+ favouritesCount,
+ setMenuOpen,
+}) => {
+ // debug: verify props received by Header
+ // eslint-disable-next-line no-console
+ console.debug('Header props:', { cartItemsCount, favouritesCount });
+
+ return (
+
+ );
+};
diff --git a/src/Components/Home/Home.scss b/src/Components/Home/Home.scss
new file mode 100644
index 00000000000..22c187d105e
--- /dev/null
+++ b/src/Components/Home/Home.scss
@@ -0,0 +1,3 @@
+.page__body {
+ background-color: #0F1121;
+}
diff --git a/src/Components/Home/Home.tsx b/src/Components/Home/Home.tsx
new file mode 100644
index 00000000000..a6f0480e586
--- /dev/null
+++ b/src/Components/Home/Home.tsx
@@ -0,0 +1,39 @@
+import React, { useEffect, useState } from 'react';
+import { useCart } from '../../Context/Context';
+import { Footer } from '../Footer/Footer';
+import { Header } from '../Header/header';
+import { Main } from '../Main/Main';
+import './Home.scss';
+import { useFav } from '../../Context/FavouritesContext';
+import { Aside } from '../Aside/Aside';
+
+export const Home = () => {
+ const { totalQuantity } = useCart();
+ const { totalFavourites } = useFav();
+ const [menuOpen, setMenuOpen] = useState(false);
+
+ useEffect(() => {
+ document.documentElement.style.overflow = menuOpen ? 'hidden' : 'auto';
+ }, [menuOpen]);
+
+ return (
+
+
+ {menuOpen && (
+
+ )}
+
+
+
+
+
+ );
+};
diff --git a/src/Components/Loader/Loader.scss b/src/Components/Loader/Loader.scss
new file mode 100644
index 00000000000..396b70c5222
--- /dev/null
+++ b/src/Components/Loader/Loader.scss
@@ -0,0 +1,32 @@
+.Loader {
+ position: absolute;
+
+ display: flex;
+ width: 100%;
+ height: 100%;
+ opacity: 0.3;
+ background-color: #000;
+
+ 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 00000000000..380d97e46a5
--- /dev/null
+++ b/src/Components/Loader/Loader.tsx
@@ -0,0 +1,10 @@
+import React from 'react';
+import './Loader.scss';
+
+export const Loader: React.FC = () => {
+ return (
+
+ );
+};
diff --git a/src/Components/Main/Main.tsx b/src/Components/Main/Main.tsx
new file mode 100644
index 00000000000..dbb83b336ea
--- /dev/null
+++ b/src/Components/Main/Main.tsx
@@ -0,0 +1,425 @@
+/* eslint-disable react-hooks/exhaustive-deps */
+/* eslint-disable @typescript-eslint/no-unused-vars */
+/* eslint-disable max-len */
+import { useCallback, useEffect, useState } from 'react';
+import { Link } from 'react-router-dom';
+import { Products } from '../../types/Products';
+import { getDiscountedProducts, getProducts } from '../../api/api';
+import { Product } from '../Products/Products';
+import React from 'react';
+import { Discount } from '../Discouned/Discounted';
+import { Discounted } from '../../types/Discounted';
+import favi from '../../images/icons/favicon.svg';
+import welcomeImagePhone from '../../images/banner-phone.png';
+import welcomeImageAcc from '../../images/BannerAppleWatch.jpg';
+import welcomeImageTablet from '../../images/BannerTablet.jpg';
+import welcomeImagePhonePhone from '../../images/bannerphonephone.jpg';
+import welcomeImageTabletPhone from '../../images/bannerphonetablet.jpg';
+import welcomeImageAccPhone from '../../images/bannerphoneacc.jpg';
+
+enum Rectangles {
+ first = 'first',
+ second = 'second',
+ third = 'third',
+}
+
+export const Main: React.FC = () => {
+ const [activeRec, setActiveRec] = useState(Rectangles.first);
+ const pics = [Rectangles.first, Rectangles.second, Rectangles.third];
+ const [picIndex, setPicIndex] = useState(0);
+
+ const [products, setProducts] = useState([]);
+ const [discountedProducts, setDiscountedProducts] = useState(
+ [],
+ );
+ const [, setLoading] = useState(true);
+ const [, setErrorMessage] = useState('');
+
+ useEffect(() => {
+ setLoading(true);
+ setErrorMessage('');
+
+ getProducts()
+ .then(setProducts)
+ .catch(() => setErrorMessage(`Couldn't load any tablets`))
+ .finally(() => setLoading(false));
+
+ getDiscountedProducts()
+ .then(setDiscountedProducts)
+ .catch(() => setErrorMessage(`Couldn't load any discounted products`))
+ .finally(() => setLoading(false));
+ }, []);
+
+ const categories = [
+ {
+ id: '/phones',
+ img: './img/category-phones.webp',
+ title: 'Mobile phones',
+ subTitle: '95 models',
+ categoryClass: 'category-pink',
+ categoryClassImg: 'category-pink-img',
+ },
+ {
+ id: '/tablets',
+ img: './img/category-tablets.webp',
+ title: 'Tablets',
+ subTitle: '24 models',
+ categoryClass: 'category-grey',
+ categoryClassImg: 'category-grey-img',
+ },
+ {
+ id: '/accessories',
+ img: './img/category-accessories.webp',
+ title: 'Accessories',
+ subTitle: '100 models',
+ categoryClass: 'category-purple',
+ categoryClassImg: 'category-purple-img',
+ },
+ ];
+
+ const [activeIndex, setActiveIndex] = useState(0);
+ const [productsPerSlide, setProductsPerSlide] = useState(4);
+ const activeRect = activeRec;
+ const widt = window.innerWidth;
+
+ const [discountedActiveIndex, setDiscountedActiveIndex] = useState(0);
+
+ const getProductsPerSlide = (width: number) =>
+ width >= 1200 ? 4 : width >= 768 ? 3 : width >= 480 ? 2 : 1;
+
+ useEffect(() => {
+ const handleResize = () => {
+ setProductsPerSlide(getProductsPerSlide(window.innerWidth));
+ };
+
+ handleResize();
+ window.addEventListener('resize', handleResize);
+
+ return () => window.removeEventListener('resize', handleResize);
+ }, []);
+
+ useEffect(() => {
+ setActiveIndex(prev => {
+ const maxStart = Math.max(0, products.length - productsPerSlide);
+
+ return prev > maxStart ? maxStart : prev;
+ });
+ }, [products.length, productsPerSlide]);
+
+ useEffect(() => {
+ setDiscountedActiveIndex(prev => {
+ const maxStart = Math.max(
+ 0,
+ discountedProducts.length - productsPerSlide,
+ );
+
+ return prev > maxStart ? maxStart : prev;
+ });
+ }, [discountedProducts.length, productsPerSlide]);
+
+ const handlePicChange = useCallback(
+ (direction: 'next' | 'prev') => {
+ const nextIndex =
+ direction === 'next'
+ ? (picIndex + 1) % pics.length
+ : (picIndex - 1 + pics.length) % pics.length;
+
+ setPicIndex(nextIndex);
+ setActiveRec(pics[nextIndex]);
+ },
+ [picIndex, pics],
+ );
+
+ const handleProductChange = useCallback(
+ (direction: 'next' | 'prev') => {
+ setActiveIndex(prev => {
+ const lastIndex = Math.max(0, products.length - productsPerSlide);
+
+ if (direction === 'next') {
+ return Math.min(prev + 1, lastIndex);
+ }
+
+ return Math.max(0, prev - 1);
+ });
+ },
+ [products.length, productsPerSlide],
+ );
+
+ const handleDiscountedChange = useCallback(
+ (direction: 'next' | 'prev') => {
+ setDiscountedActiveIndex(prev => {
+ const lastIndex = Math.max(
+ 0,
+ discountedProducts.length - productsPerSlide,
+ );
+
+ if (direction === 'next') {
+ return prev < lastIndex ? prev + 1 : 0;
+ }
+
+ return prev > 0 ? prev - 1 : lastIndex;
+ });
+ },
+ [discountedProducts.length, productsPerSlide],
+ );
+
+ const arrowLeft = new URL(
+ '../../images/icons/Chevron (Arrow Left).svg',
+ import.meta.url,
+ ).href;
+
+ const arrowRight = new URL(
+ '../../images/icons/Chevron (Arrow Right).svg',
+ import.meta.url,
+ ).href;
+
+ const welcomeImage = new URL('../../images/Banner.png', import.meta.url).href;
+ const welcomeSlides = [welcomeImage, welcomeImageTablet, welcomeImageAcc];
+ const welcomeSlidesPhone = [
+ welcomeImagePhonePhone,
+ welcomeImageTabletPhone,
+ welcomeImageAccPhone,
+ ];
+
+ useEffect(() => {
+ const id = window.setTimeout(() => {
+ setPicIndex(p => (p + 1) % pics.length);
+ }, 5000);
+
+ return () => clearTimeout(id);
+ }, [picIndex, pics.length]);
+
+ return (
+
+
+
+
+
+
+ Welcome to Nice Gadgets store!
+
+
+
+
+
handlePicChange('prev')}
+ >
+
+
+ {/*
*/}
+
+
+ {welcomeSlides.map((slide, index) => (
+
+ {widt >= 768 ? (
+
+
+
+
+ Now available in our store!{' '}
+
+
+
+ Be the first!
+
+
+
+
+ Order Now
+
+
+
+
+
+
+
+ ) : (
+
+ )}
+
+ {/*
+
+
+
+ */}
+
+ ))}
+
+
+ {/* */}
+
handlePicChange('next')}
+ >
+
+
+
+
+ {activeRect === Rectangles.first ? (
+ // eslint-disable-next-line max-len
+
+
+
+ ) : (
+
setActiveRec(Rectangles.first)}
+ className="welcome__block-rectangle"
+ >
+
+
+ )}
+ {activeRect === Rectangles.second ? (
+ // eslint-disable-next-line max-len
+
+
+
+ ) : (
+
setActiveRec(Rectangles.second)}
+ className="welcome__block-rectangle"
+ >
+
+
+ )}
+ {activeRect === Rectangles.third ? (
+ // eslint-disable-next-line max-len
+
+
+
+ ) : (
+
setActiveRec(Rectangles.third)}
+ className="welcome__block-rectangle "
+ >
+
+
+ )}
+
+
+
+
+
+
+ Brand new models
+
+
+
+
handleProductChange('prev')}
+ disabled={activeIndex === 0}
+ >
+
+
+
+
+
= products.length ? 'new__models-arrow-disabled' : ''}`}
+ onClick={() => handleProductChange('next')}
+ disabled={activeIndex + productsPerSlide >= products.length}
+ >
+
+
+
+
+
+
+
+
+
+
+ Shop by category
+
+
+ {categories.map(category => (
+
+
+
+
+
+
+
{category.title}
+
{category.subTitle}
+
+ ))}
+
+
+
+
+
+
Hot prices
+
+
+
handleDiscountedChange('prev')}
+ disabled={discountedActiveIndex === 0}
+ >
+
+
+
+
+
= discountedProducts.length ? 'new__models-arrow-disabled' : ''}`}
+ onClick={() => handleDiscountedChange('next')}
+ disabled={
+ discountedActiveIndex + productsPerSlide >=
+ discountedProducts.length
+ }
+ >
+
+
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/src/Components/Page__Slider/PageSlider.tsx b/src/Components/Page__Slider/PageSlider.tsx
new file mode 100644
index 00000000000..837b6e3d045
--- /dev/null
+++ b/src/Components/Page__Slider/PageSlider.tsx
@@ -0,0 +1,106 @@
+import arrRight from '../../images/icons/Chevron (Arrow Right).svg';
+import arrLeft from '../../images/icons/Chevron (Arrow Left).svg';
+import React from 'react';
+
+// eslint-disable-next-line max-len
+import arrRightDisabled from '../../images/icons/Chevron (Arrow Right) grey.png';
+import { useSearchParams } from 'react-router-dom';
+
+type Props = {
+ activePage: number;
+ setActivePage: React.Dispatch>;
+ activeQuantity: number;
+};
+
+export const PageSlider: React.FC = ({
+ activePage,
+ setActivePage,
+ activeQuantity,
+}: Props) => {
+ const [searchParams, setSearchParams] = useSearchParams();
+
+ const handleScrollToTop = () => {
+ window.scrollTo({ top: 0, behavior: 'smooth' });
+ };
+
+ const handlePageChange = (value: string) => {
+ const params = new URLSearchParams(searchParams);
+
+ params.set('Page', value);
+
+ setSearchParams(params);
+ };
+
+ return (
+
+
{
+ setActivePage(activePage - 1);
+ handlePageChange((activePage - 1 + 1).toString());
+ handleScrollToTop();
+ }}
+ disabled={activePage === 0}
+ className={`page__models-button page__models-arr ${activePage === 0 ? 'page__models-arr-disabled' : ''}`}
+ >
+
+
+
{
+ setActivePage(0);
+ handleScrollToTop();
+ handlePageChange('1');
+ }}
+ className={`page__models-button ${activePage === 0 ? 'page__models-button_active' : ''}`}
+ >
+ 1
+
+
{
+ setActivePage(1);
+ handleScrollToTop();
+ handlePageChange('2');
+ }}
+ className={`page__models-button ${activePage === 1 ? 'page__models-button_active' : ''}`}
+ >
+ 2
+
+
{
+ setActivePage(2);
+ handleScrollToTop();
+ handlePageChange('3');
+ }}
+ className={`page__models-button ${activePage === 2 ? 'page__models-button_active' : ''} ${activeQuantity === 64 ? 'page__models-disabled' : ''}`}
+ >
+ 3
+
+
{
+ setActivePage(3);
+ handleScrollToTop();
+ handlePageChange('4');
+ }}
+ className={`page__models-button ${activePage === 3 ? 'page__models-button_active' : ''} ${activeQuantity === 64 ? 'page__models-disabled' : ''}`}
+ >
+ 4
+
+
{
+ setActivePage(activePage + 1);
+ handlePageChange((activePage + 1 + 1).toString());
+ handleScrollToTop();
+ }}
+ className={`page__models-button page__models-arr ${activePage === 3 ? 'page__models-arr-disabled' : ''} ${activePage === 1 && activeQuantity === 64 ? 'page__models-arr-disabled' : ''}`}
+ disabled={
+ activePage === 3 || (activePage === 1 && activeQuantity === 64)
+ }
+ >
+
+
+
+ );
+};
diff --git a/src/Components/Page__Slider/PageSliderAcc.tsx b/src/Components/Page__Slider/PageSliderAcc.tsx
new file mode 100644
index 00000000000..0fc0b0a443b
--- /dev/null
+++ b/src/Components/Page__Slider/PageSliderAcc.tsx
@@ -0,0 +1,119 @@
+import arrRight from '../../images/icons/Chevron (Arrow Right).svg';
+import arrLeft from '../../images/icons/Chevron (Arrow Left).svg';
+import React from 'react';
+
+// eslint-disable-next-line max-len
+import arrRightDisabled from '../../images/icons/Chevron (Arrow Right) grey.png';
+import { useSearchParams } from 'react-router-dom';
+
+type Props = {
+ activePage: number;
+ setActivePage: React.Dispatch>;
+ activeQuantity: number;
+};
+
+export const PageSliderAcc: React.FC = ({
+ activePage,
+ setActivePage,
+ activeQuantity,
+}: Props) => {
+ const handleScrollToTop = () => {
+ window.scrollTo({ top: 0, behavior: 'smooth' });
+ };
+
+ const [searchParams, setSearchParams] = useSearchParams();
+
+ const handlePageChange = (value: string) => {
+ const params = new URLSearchParams(searchParams);
+
+ params.set('Page', value);
+
+ setSearchParams(params);
+ };
+
+ return (
+
+
{
+ setActivePage(activePage - 1);
+ handlePageChange((activePage - 1 + 1).toString());
+ handleScrollToTop();
+ }}
+ disabled={activePage === 0}
+ className={`page__models-button page__models-arr ${activePage === 0 ? 'page__models-arr-disabled' : ''}`}
+ >
+
+
+
{
+ setActivePage(0);
+ handleScrollToTop();
+ handlePageChange('1');
+ }}
+ className={`page__models-button ${activePage === 0 ? 'page__models-button_active' : ''}`}
+ >
+ 1
+
+
{
+ setActivePage(1);
+ handleScrollToTop();
+ handlePageChange('2');
+ }}
+ className={`page__models-button ${activePage === 1 ? 'page__models-button_active' : ''} ${activeQuantity === 64 ? 'page__models-disabled' : ''}`}
+ >
+ 2
+
+
{
+ setActivePage(2);
+ handleScrollToTop();
+ handlePageChange('3');
+ }}
+ className={`page__models-button ${activePage === 2 ? 'page__models-button_active' : ''} ${activeQuantity === 64 ? 'page__models-disabled' : ''} ${activeQuantity === 32 ? 'page__models-disabled' : ''}`}
+ >
+ 3
+
+
{
+ setActivePage(3);
+ handleScrollToTop();
+ handlePageChange('4');
+ }}
+ className={`page__models-button ${activePage === 3 ? 'page__models-button_active' : ''} ${activeQuantity === 64 ? 'page__models-disabled' : ''} ${activeQuantity === 32 ? 'page__models-disabled' : ''} ${activeQuantity === 16 ? 'page__models-disabled' : ''}`}
+ >
+ 4
+
+
{
+ setActivePage(activePage + 1);
+ handlePageChange((activePage + 1 + 1).toString());
+ handleScrollToTop();
+ }}
+ className={`page__models-button page__models-arr ${activePage === 3 ? 'page__models-arr-disabled' : ''} ${activePage === 0 && activeQuantity === 64 ? 'page__models-arr-disabled' : ''} ${activePage === 1 && activeQuantity === 32 ? 'page__models-arr-disabled' : ''} ${activePage === 2 && activeQuantity === 16 ? 'page__models-arr-disabled' : ''} `}
+ disabled={
+ activePage === 3 ||
+ (activePage === 0 && activeQuantity === 64) ||
+ (activePage === 1 && activeQuantity === 32) ||
+ (activePage === 2 && activeQuantity === 16)
+ }
+ >
+
+
+
+ );
+};
diff --git a/src/Components/Page__Slider/PageSliderTablet.tsx b/src/Components/Page__Slider/PageSliderTablet.tsx
new file mode 100644
index 00000000000..7a881f4924c
--- /dev/null
+++ b/src/Components/Page__Slider/PageSliderTablet.tsx
@@ -0,0 +1,119 @@
+import arrRight from '../../images/icons/Chevron (Arrow Right).svg';
+import arrLeft from '../../images/icons/Chevron (Arrow Left).svg';
+import React from 'react';
+
+// eslint-disable-next-line max-len
+import arrRightDisabled from '../../images/icons/Chevron (Arrow Right) grey.png';
+import { useSearchParams } from 'react-router-dom';
+
+type Props = {
+ activePage: number;
+ setActivePage: React.Dispatch>;
+ activeQuantity: number;
+};
+
+export const PageSliderTablet: React.FC = ({
+ activePage,
+ setActivePage,
+ activeQuantity,
+}: Props) => {
+ const handleScrollToTop = () => {
+ window.scrollTo({ top: 0, behavior: 'smooth' });
+ };
+
+ const [searchParams, setSearchParams] = useSearchParams();
+
+ const handlePageChange = (value: string) => {
+ const params = new URLSearchParams(searchParams);
+
+ params.set('Page', value);
+
+ setSearchParams(params);
+ };
+
+ return (
+
+
{
+ setActivePage(activePage - 1);
+ handlePageChange((activePage - 1 + 1).toString());
+ handleScrollToTop();
+ }}
+ disabled={activePage === 0}
+ className={`page__models-button page__models-arr ${activePage === 0 ? 'page__models-arr-disabled' : ''}`}
+ >
+
+
+
{
+ setActivePage(0);
+ handleScrollToTop();
+ handlePageChange('1');
+ }}
+ className={`page__models-button ${activePage === 0 ? 'page__models-button_active' : ''}`}
+ >
+ 1
+
+
{
+ setActivePage(1);
+ handleScrollToTop();
+ handlePageChange('2');
+ }}
+ className={`page__models-button ${activePage === 1 ? 'page__models-button_active' : ''} ${activeQuantity === 64 ? 'page__models-disabled' : ''}`}
+ >
+ 2
+
+
{
+ setActivePage(2);
+ handleScrollToTop();
+ handlePageChange('3');
+ }}
+ className={`page__models-button ${activePage === 2 ? 'page__models-button_active' : ''} ${activeQuantity === 64 ? 'page__models-disabled' : ''} ${activeQuantity === 32 ? 'page__models-disabled' : ''}`}
+ >
+ 3
+
+
{
+ setActivePage(3);
+ handleScrollToTop();
+ handlePageChange('4');
+ }}
+ className={`page__models-button ${activePage === 3 ? 'page__models-button_active' : ''} ${activeQuantity === 64 ? 'page__models-disabled' : ''} ${activeQuantity === 32 ? 'page__models-disabled' : ''} ${activeQuantity === 16 ? 'page__models-disabled' : ''}`}
+ >
+ 4
+
+
{
+ setActivePage(activePage + 1);
+ handlePageChange((activePage + 1 + 1).toString());
+ handleScrollToTop();
+ }}
+ className={`page__models-button page__models-arr ${activePage === 3 ? 'page__models-arr-disabled' : ''} ${activePage === 0 && activeQuantity === 64 ? 'page__models-arr-disabled' : ''} ${activePage === 1 && activeQuantity === 32 ? 'page__models-arr-disabled' : ''} ${activePage === 2 && activeQuantity === 16 ? 'page__models-arr-disabled' : ''} `}
+ disabled={
+ activePage === 3 ||
+ (activePage === 0 && activeQuantity === 64) ||
+ (activePage === 1 && activeQuantity === 32) ||
+ (activePage === 2 && activeQuantity === 16)
+ }
+ >
+
+
+
+ );
+};
diff --git a/src/Components/PhoneLike/PhoneLike.scss b/src/Components/PhoneLike/PhoneLike.scss
new file mode 100644
index 00000000000..34b7c6aac1d
--- /dev/null
+++ b/src/Components/PhoneLike/PhoneLike.scss
@@ -0,0 +1,36 @@
+.phone__like {
+ &-container {
+ display: flex;
+
+ justify-content: space-between;
+
+ margin-bottom: 25px;
+ }
+
+ &-buttons {
+ display: flex;
+ gap: 8px;
+
+ padding-bottom: 32px;
+ }
+
+ &-arr {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ }
+
+ &-arrows {
+ display: flex;
+ align-items: center;
+ gap: 16px;
+ }
+
+ &-arrow {
+ cursor: pointer;
+ border: 0;
+ height: 32px;
+ width: 32px;
+ background-color: #323542;
+ }
+}
diff --git a/src/Components/PhoneLike/PhoneLike.tsx b/src/Components/PhoneLike/PhoneLike.tsx
new file mode 100644
index 00000000000..b5d830cb831
--- /dev/null
+++ b/src/Components/PhoneLike/PhoneLike.tsx
@@ -0,0 +1,96 @@
+import { Link } from 'react-router-dom';
+import arrowLeft from '../../images/icons/Chevron (Arrow Left).svg';
+import arrowRight from '../../images/icons/Chevron (Arrow Right).svg';
+import React, { useCallback, useEffect, useState } from 'react';
+import { Products } from '../../types/Products';
+import './PhoneLike.scss';
+import { getProducts } from '../../api/api';
+import { ProductSpecs } from '../ProductsSpec/ProductsSpec';
+
+export const PhoneLike = () => {
+ const [products, setProducts] = useState([]);
+ const [activeIndex, setActiveIndex] = useState(0);
+ const [productsPerSlide, setProductsPerSlide] = useState(4);
+ const [, setLoading] = useState(true);
+ const [, setErrorMessage] = useState('');
+
+ const getProductsPerSlide = (width: number) =>
+ width >= 1200 ? 4 : width >= 768 ? 3 : width >= 480 ? 2 : 1;
+
+ useEffect(() => {
+ const handleResize = () => {
+ setProductsPerSlide(getProductsPerSlide(window.innerWidth));
+ };
+
+ handleResize();
+ window.addEventListener('resize', handleResize);
+
+ return () => window.removeEventListener('resize', handleResize);
+ }, []);
+
+ useEffect(() => {
+ setActiveIndex(prev => {
+ const maxStart = Math.max(0, products.length - productsPerSlide);
+
+ return prev > maxStart ? maxStart : prev;
+ });
+ }, [products.length, productsPerSlide]);
+
+ const handleProductChange = useCallback(
+ (direction: 'next' | 'prev') => {
+ setActiveIndex(prev => {
+ const lastIndex = Math.max(0, products.length - productsPerSlide);
+
+ if (direction === 'next') {
+ return prev < lastIndex ? prev + 1 : 0;
+ }
+
+ return prev > 0 ? prev - 1 : lastIndex;
+ });
+ },
+ [products.length, productsPerSlide],
+ );
+
+ useEffect(() => {
+ setLoading(true);
+ setErrorMessage('');
+
+ getProducts()
+ .then(setProducts)
+ .catch(() => setErrorMessage(`Couldn't load any tablets`))
+ .finally(() => setLoading(false));
+ }, []);
+
+ return (
+ <>
+
+
+
You may also like
+
+
+
handleProductChange('prev')}
+ >
+
+
+
+
+
handleProductChange('next')}
+ >
+
+
+
+
+
+
+
+ >
+ );
+};
diff --git a/src/Components/Phones/Phones.scss b/src/Components/Phones/Phones.scss
new file mode 100644
index 00000000000..d05bcc3eb7d
--- /dev/null
+++ b/src/Components/Phones/Phones.scss
@@ -0,0 +1,40 @@
+@import '../../utils/mixins';
+
+.phones {
+ &__grid {
+ display: grid;
+
+ @include on-tablet {
+ --columns: 2;
+
+ column-gap: 16px;
+ grid-template-columns: repeat(var(--columns), 1fr);
+ }
+
+ @include on-desktop {
+ --columns: 4;
+
+ grid-template-columns: repeat(var(--columns), 1fr);
+ }
+ }
+
+ &__path {
+ display: flex;
+
+ gap: 8px;
+
+ align-items: center;
+
+ margin-top: 25px;
+
+
+ &-phones {
+ font-family: "Mont SemiBold", sans-serif;
+
+ color: #75767F;
+ font-weight: 700;
+ font-size: 12px;
+ line-height: 100%;
+ }
+ }
+}
diff --git a/src/Components/Phones/Phones.tsx b/src/Components/Phones/Phones.tsx
new file mode 100644
index 00000000000..d23503b8b63
--- /dev/null
+++ b/src/Components/Phones/Phones.tsx
@@ -0,0 +1,214 @@
+/* eslint-disable max-len */
+import { Link, useParams } from 'react-router-dom';
+import { Header } from '../Header/header';
+import './Phones.scss';
+import ArrowGray from '../../images/icons/ChevronGray.svg';
+import Home from '../../images/icons/Home.svg';
+import { Filter, FilterValue, ItemQuantity } from '../Filter/Filter';
+
+import { getPhones } from '../../api/api';
+
+import { useEffect, useState } from 'react';
+import { useCart } from '../../Context/Context';
+import { Phone } from '../../types/Phone';
+import { Products } from '../../types/Products';
+import React from 'react';
+import { useFav } from '../../Context/FavouritesContext';
+
+import { PageSlider } from '../Page__Slider/PageSlider';
+import { ActiveQuantity16 } from '../ActiveQuantity/ActiveQuantity16';
+import { ActiveQuantity32 } from '../ActiveQuantity/ActiveQuantity32';
+import { ActiveQuantity64 } from '../ActiveQuantity/ActiveQuantity64';
+import { Footer } from '../Footer/Footer';
+import { Aside } from '../Aside/Aside';
+import { Loader } from '../Loader/Loader';
+
+export const Phones = () => {
+ const [menuOpen, setMenuOpen] = useState(false);
+ const [phones, setPhones] = useState([]);
+ const [activeQuantity, setActiveQuantity] = useState(16);
+ const [activeFilter, setActiveFilter] = useState('Newest');
+ const [activePage, setActivePage] = useState(0);
+ const { productId } = useParams();
+ const [Loading, setLoading] = useState(true);
+ const [, setErrorMessage] = useState('');
+
+ const mapPhoneToProduct = (p: Phone): Products => ({
+ id: String(p.id),
+ itemId: String(p.id),
+ name: p.name ?? 'Unknown',
+ category: 'phones',
+ fullPrice: Number(p.priceRegular ?? 0),
+ price: Number(p.priceDiscount ?? p.priceRegular ?? 0),
+ screen: p.screen ?? '—',
+ capacity: p.capacity ?? '—',
+ color: p.color ?? p.colorsAvailable?.[0] ?? '—',
+ ram: p.ram ?? '—',
+ year: new Date().getFullYear(),
+ image: p.images?.[0] ?? 'img/placeholder.png',
+ });
+
+ const getIphoneOrder = (name: string) => {
+ if (name.includes('XS')) {
+ return 10.2;
+ }
+
+ if (name.includes('XR')) {
+ return 10.1;
+ }
+
+ if (name.includes('X')) {
+ return 10;
+ }
+
+ const match = name.match(/\d+/);
+
+ return match ? parseInt(match[0], 10) : 0;
+ };
+
+ const sortedPhones = [...phones].sort((a, b) => {
+ return getIphoneOrder(b.name) - getIphoneOrder(a.name);
+ });
+
+ const sortedPhonesLowToHigh = [...phones].sort(
+ (a, b) => a.priceRegular - b.priceRegular,
+ );
+
+ const sortedPhonesHighToLow = [...phones].sort(
+ (a, b) => b.priceRegular - a.priceRegular,
+ );
+
+ const filteredPhones =
+ activeFilter === 'Newest'
+ ? sortedPhones
+ : activeFilter === 'Price: Low to High'
+ ? sortedPhonesLowToHigh
+ : activeFilter === 'Price: High to Low'
+ ? sortedPhonesHighToLow
+ : phones;
+
+ const { totalQuantity } = useCart();
+ const { totalFavourites } = useFav();
+
+ useEffect(() => {
+ setLoading(true);
+ setErrorMessage('');
+
+ getPhones()
+ .then(setPhones)
+ .catch(() => setErrorMessage(`Couldn't load any phones`))
+ .finally(() => setLoading(false));
+ }, []);
+
+ const getSuggestedProducts = (
+ all: Phone[],
+ currentId?: string | number,
+ count = 4,
+ ) => {
+ const filtered = all.filter(p => String(p.id) !== String(currentId));
+
+ // simple Fisher–Yates shuffle
+ for (let i = filtered.length - 1; i > 0; i--) {
+ const j = Math.floor(Math.random() * (i + 1));
+
+ [filtered[i], filtered[j]] = [filtered[j], filtered[i]];
+ }
+
+ return filtered.slice(0, count);
+ };
+
+ const [, setSuggested] = useState([]);
+
+ useEffect(() => {
+ getPhones().then(data =>
+ setSuggested(getSuggestedProducts(data, productId, 4)),
+ );
+ }, [productId]);
+
+ return (
+
+
+ {menuOpen && (
+
+ )}
+ {Loading ? (
+
+ ) : (
+
+
+
+
+
+
+
+
+
Phones
+
+
+
Mobile phones
+ 95 models
+
+
+
+
+ {activeQuantity === 16 && (
+
+ )}
+ {activeQuantity === 32 && (
+
+ )}
+ {activeQuantity === 64 && (
+
+ )}
+
+
+
+
+ )}
+
+
+
+
+
+ );
+};
diff --git a/src/Components/ProductCard/ProductCard.tsx b/src/Components/ProductCard/ProductCard.tsx
new file mode 100644
index 00000000000..7e69fbf757d
--- /dev/null
+++ b/src/Components/ProductCard/ProductCard.tsx
@@ -0,0 +1,65 @@
+import fav from '../../images/fav/Icons/Favourites (Heart Like).svg';
+import { useCart } from '../../Context/Context';
+import { Products } from '../../types/Products';
+import classNames from 'classnames';
+import { useFav } from '../../Context/FavouritesContext';
+import activeFav from '../../images/icons/ActiveFav.svg';
+import React from 'react';
+
+type Props = {
+ product: Products;
+};
+
+export const ProductCard: React.FC = ({ product }) => {
+ const { addToFav, removeFromFav, isInFav } = useFav();
+ const addedFav = isInFav(String(product.id));
+
+ const { addToCart, isInCart, removeFromCart } = useCart();
+ const added = isInCart(String(product.id));
+ const buttonClass = classNames('products-cart', {
+ 'products-cart-active': added,
+ });
+
+ return (
+
+
{
+ e.preventDefault();
+ if (added) {
+ removeFromCart(String(product.id));
+ } else {
+ addToCart(product);
+ }
+ }}
+ >
+
+
+ {added ? 'Added' : 'Add to cart'}
+
+
+
+
+
{
+ e.preventDefault();
+ if (addedFav) {
+ removeFromFav(String(product.id));
+ } else {
+ addToFav(product);
+ }
+ }}
+ >
+ {addedFav ? (
+
+ ) : (
+
+ )}
+
+
+ );
+};
diff --git a/src/Components/Products/Products.scss b/src/Components/Products/Products.scss
new file mode 100644
index 00000000000..1c2356ea2d5
--- /dev/null
+++ b/src/Components/Products/Products.scss
@@ -0,0 +1,272 @@
+@import '../../utils/mixins';
+
+.products {
+ &-phones {
+ position: relative;
+ overflow: hidden;
+ width: 100%;
+ }
+
+ &-track {
+ display: flex;
+ gap: 24px;
+ transition: transform 0.5s ease-in-out;
+ will-change: transform;
+ }
+
+ &__bottom {
+ position: relative;
+ bottom: 0;
+ }
+
+ &-container {
+ padding-top: 32px;
+ padding-inline: 32px;
+
+ display: flex;
+ flex-direction: column;
+ justify-content: space-between;
+ gap: 100%;
+ }
+
+ &-phone {
+ cursor: pointer;
+ flex: 0 0 auto;
+
+ color: #F1F2F9;
+ text-decoration: none;
+
+ background-color: #161827;
+ border: 1px solid #161827;
+ align-items: center;
+ justify-content: center;
+
+ transition: 0.5s;
+
+ &:hover {
+ border: 1px solid #323542;
+ transition: 0.7s;
+ }
+ }
+
+ &-img {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ height: 200px;
+
+ margin-bottom: 16px;
+ }
+
+ &-image {
+ object-fit: contain;
+ width: 100%;
+ height: 100%;
+ transition: 0.3s;
+
+ &:hover {
+ transform: scale(1.1);
+ }
+ }
+
+ &-title {
+ margin: 0;
+ margin-bottom: 8px;
+
+ font-family: 'Mont Regular', sans-serif;
+ font-weight: 600;
+ font-size: 14px;
+ line-height: 21px;
+ width: 180px;
+ }
+
+ &-price {
+ display: flex;
+
+ align-items: center;
+
+ font-family: 'Mont SemiBold', sans-serif;
+ font-weight: 800;
+ font-size: 22px;
+ line-height: 140%;
+
+ gap: 8px;
+
+ &-span {
+ font-weight: 600;
+ font-size: 22px;
+ line-height: 100%;
+ text-decoration: line-through;
+ color: #89939A;
+ }
+ }
+
+ &-string {
+ margin-top: 10px;
+ width: 100%;
+ height: 1px;
+ background-color: #3B3E4A;
+ }
+
+ &-text {
+ display: flex;
+ justify-content: space-between;
+ margin-top: 8px;
+ color: #75767F;
+ font-family: 'Mont Regular', sans-serif;
+ font-weight: 700;
+ font-size: 12px;
+ line-height: 100%;
+
+ &__first {
+ margin-top: 16px;
+ }
+ }
+
+ &-info {
+ margin-bottom: 16px;
+ }
+
+ &-span {
+ color: #F1F2F9;
+ }
+
+ &-buttons {
+ display: flex;
+ gap: 8px;
+
+ padding-bottom: 32px;
+ }
+
+ &__link {
+ display: flex;
+
+ align-items: center;
+ justify-content: center;
+
+ text-decoration: none;
+
+ width: 100%;
+ height: 100%;
+ }
+
+ &-fav {
+ cursor: pointer;
+ display: flex;
+ height: 40px;
+ width: 54px;
+
+ background-color: #323542;
+ border: 1px solid #3B3E4A;
+
+ justify-content: center;
+
+ text-decoration: none;
+ align-items: center;
+
+ transition: 0.3s;
+
+ &:hover {
+ background-color: #4A4D58;
+ transition: 0.3s;
+ }
+
+ &-active {
+ background-color: transparent;
+
+ &:hover {
+ background-color: transparent;
+ }
+ }
+
+ &__img {
+ height: 16px;
+ }
+
+ &__button {
+ border: none;
+
+ background-color: inherit;
+ }
+ }
+
+ &-cart {
+ border: none;
+ height: 40px;
+ width: 100%;
+ background-color: #905BFF;
+
+ cursor: pointer;
+
+ transition: 0.3s;
+
+ &:hover {
+ background-color: #A378FF;
+ transition: 0.3s;
+ }
+
+ &__text {
+ margin: 0;
+ font-family: Mont, sans-serif;
+ font-weight: 700;
+ font-size: 14px;
+ line-height: 21px;
+ text-align: center;
+ color: #F1F2F9;
+ }
+
+ &-active {
+ background-color: #323542;
+
+ &:hover {
+ background-color: #323542;
+ }
+ }
+
+ &__link {
+ display: flex;
+
+ align-items: center;
+ justify-content: center;
+
+ text-decoration: none;
+
+ width: 100%;
+ height: 40px;
+ }
+ }
+
+ &-arr {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ }
+
+ &-arrows {
+ display: flex;
+ gap: 16px;
+ }
+
+ &-arrow {
+ cursor: pointer;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ height: 32px;
+ width: 32px;
+ background-color: #323542;
+ border: 1px solid #3B3E4A;
+ transition: 0.3s;
+
+ &-disabled {
+ background-color: transparent;
+ cursor: not-allowed;
+ opacity: 0.5;
+ }
+
+ &:hover {
+ background-color: #4A4D58;
+ transition: 0.3s;
+ }
+ }
+}
diff --git a/src/Components/Products/Products.tsx b/src/Components/Products/Products.tsx
new file mode 100644
index 00000000000..3f49aeb62d7
--- /dev/null
+++ b/src/Components/Products/Products.tsx
@@ -0,0 +1,96 @@
+import { Products } from '../../types/Products';
+import './Products.scss';
+import { ProductCard } from '../ProductCard/ProductCard';
+import React from 'react';
+import { Link } from 'react-router-dom';
+
+type Props = {
+ products: Products[];
+ currentSlide: number;
+ visibleCount: number;
+};
+
+export const Product: React.FC = ({
+ products,
+ currentSlide,
+ visibleCount,
+}) => {
+ const trackRef = React.useRef(null);
+ const [cardStep, setCardStep] = React.useState(0);
+
+ React.useEffect(() => {
+ const updateCardStep = () => {
+ const track = trackRef.current;
+ const card = track?.querySelector('.products-phone');
+
+ if (!card) {
+ setCardStep(0);
+
+ return;
+ }
+
+ const gap = 24;
+
+ setCardStep(card.offsetWidth + gap);
+ };
+
+ updateCardStep();
+ window.addEventListener('resize', updateCardStep);
+
+ return () => window.removeEventListener('resize', updateCardStep);
+ }, [products.length, visibleCount]);
+
+ return (
+
+
+ {products.map(product => {
+ return (
+
+
+
+
+
+
+
{product.name}
+
${product.fullPrice}
+
+
+
+
+
+ Screen{' '}
+ {product.screen}
+
+
+ Capacity{' '}
+ {product.capacity}
+
+
+ RAM {product.ram}
+
+
+
+
+
+
+ );
+ })}
+
+
+ );
+};
diff --git a/src/Components/ProductsSpec/ProductsSpec.tsx b/src/Components/ProductsSpec/ProductsSpec.tsx
new file mode 100644
index 00000000000..be26e60b9e2
--- /dev/null
+++ b/src/Components/ProductsSpec/ProductsSpec.tsx
@@ -0,0 +1,89 @@
+import { Products } from '../../types/Products';
+import '../DevSpec/PhoneSpec.scss';
+import { ProductCard } from '../ProductCard/ProductCard';
+import React, { useEffect, useRef, useState } from 'react';
+import { Link } from 'react-router-dom';
+
+type Props = {
+ products: Products[];
+ currentSlide: number;
+ visibleCount: number;
+};
+
+export const ProductSpecs: React.FC = ({
+ products,
+ currentSlide,
+ visibleCount,
+}) => {
+ const trackRef = useRef(null);
+ const [cardStep, setCardStep] = useState(0);
+ const gap = 24;
+
+ useEffect(() => {
+ const updateCardStep = () => {
+ const track = trackRef.current;
+ const card = track?.querySelector('.products-phone');
+
+ if (!card) {
+ setCardStep(0);
+
+ return;
+ }
+
+ setCardStep(card.offsetWidth + gap);
+ };
+
+ updateCardStep();
+ window.addEventListener('resize', updateCardStep);
+
+ return () => window.removeEventListener('resize', updateCardStep);
+ }, [products.length, visibleCount]);
+
+ return (
+
+
+ {products.map(product => (
+
+
+
+
+
+
+
{product.name}
+
${product.fullPrice}
+
+
+
+
+
+ Screen{' '}
+ {product.screen}
+
+
+ Capacity{' '}
+ {product.capacity}
+
+
+ RAM {product.ram}
+
+
+
+
+
+
+ ))}
+
+
+ );
+};
diff --git a/src/Components/Tablets/Tablets.scss b/src/Components/Tablets/Tablets.scss
new file mode 100644
index 00000000000..eabf02943d3
--- /dev/null
+++ b/src/Components/Tablets/Tablets.scss
@@ -0,0 +1,25 @@
+.tablets {
+ &__path {
+ display: flex;
+
+ gap: 8px;
+
+ align-items: center;
+
+ margin-top: 25px;
+
+
+ &-phones {
+ font-family: "Mont SemiBold", sans-serif;
+
+ color: #75767F;
+ font-weight: 700;
+ font-size: 12px;
+ line-height: 100%;
+ }
+ }
+
+ &__articles {
+ gap: 40px;
+ }
+}
diff --git a/src/Components/Tablets/Tablets.tsx b/src/Components/Tablets/Tablets.tsx
new file mode 100644
index 00000000000..b8a3fa39db0
--- /dev/null
+++ b/src/Components/Tablets/Tablets.tsx
@@ -0,0 +1,187 @@
+/* eslint-disable max-len */
+import { Link } from 'react-router-dom';
+import { Filter, FilterValue, ItemQuantity } from '../Filter/Filter';
+import { Header } from '../Header/header';
+import ArrowGray from '../../images/icons/ChevronGray.svg';
+import Home from '../../images/icons/Home.svg';
+import './Tablets.scss';
+import { useEffect, useState } from 'react';
+import { getTablets } from '../../api/api';
+import { Tablet } from '../../types/Tablets';
+import { Products } from '../../types/Products';
+import { useCart } from '../../Context/Context';
+import { useFav } from '../../Context/FavouritesContext';
+import React from 'react';
+import { ActiveQuantity16 } from '../ActiveQuantity/ActiveQuantityTablets/ActiveQuantity16';
+import { ActiveQuantity32 } from '../ActiveQuantity/ActiveQuantityTablets/ActiveQuantity32';
+import { ActiveQuantity64 } from '../ActiveQuantity/ActiveQuantityTablets/ActiveQuantity64';
+import { PageSliderTablet } from '../Page__Slider/PageSliderTablet';
+import { Footer } from '../Footer/Footer';
+import { Aside } from '../Aside/Aside';
+import { Loader } from '../Loader/Loader';
+
+export const Tablets = () => {
+ const [menuOpen, setMenuOpen] = useState(false);
+ const [Loading, setLoading] = useState(true);
+ const [tablets, setTablets] = useState([]);
+ const [, setErrorMessage] = useState('');
+ const { totalQuantity } = useCart();
+ const { totalFavourites } = useFav();
+
+ const getIphoneOrder = (name: string) => {
+ if (name.includes('XS')) {
+ return 10.2;
+ }
+
+ if (name.includes('XR')) {
+ return 10.1;
+ }
+
+ if (name.includes('X')) {
+ return 10;
+ }
+
+ const match = name.match(/\d+/);
+
+ return match ? parseInt(match[0], 10) : 0;
+ };
+
+ const sortedTablets = [...tablets].sort((a, b) => {
+ return getIphoneOrder(b.name) - getIphoneOrder(a.name);
+ });
+
+ const [activeQuantity, setActiveQuantity] = useState(16);
+ const [activeFilter, setActiveFilter] = useState('Newest');
+
+ const sortedTabletsLowToHigh = [...tablets].sort(
+ (a, b) => a.priceRegular - b.priceRegular,
+ );
+
+ const sortedTabletsHighToLow = [...tablets].sort(
+ (a, b) => b.priceRegular - a.priceRegular,
+ );
+
+ const filteredTablets =
+ activeFilter === 'Newest'
+ ? sortedTablets
+ : activeFilter === 'Price: Low to High'
+ ? sortedTabletsLowToHigh
+ : activeFilter === 'Price: High to Low'
+ ? sortedTabletsHighToLow
+ : tablets;
+
+ const [activePage, setActivePage] = useState(0);
+
+ const mapTabletToProduct = (tablet: Tablet): Products => ({
+ id: String(tablet.id),
+ itemId: String(tablet.id),
+ name: tablet.name,
+ category: tablet.category,
+ fullPrice: Number(tablet.priceRegular),
+ price: Number(tablet.priceDiscount || tablet.priceRegular),
+ screen: tablet.screen,
+ capacity: tablet.capacity,
+ color: tablet.color || tablet.colorsAvailable?.[0] || '—',
+ ram: tablet.ram,
+ year: new Date().getFullYear(),
+ image:
+ typeof tablet.images === 'string' ? tablet.images : tablet.images?.[0],
+ });
+
+ useEffect(() => {
+ setLoading(true);
+ setErrorMessage('');
+
+ getTablets()
+ .then(setTablets)
+ .catch(() => setErrorMessage(`Couldn't load any tablets`))
+ .finally(() => setLoading(false));
+ }, []);
+
+ return (
+
+
+ {menuOpen && (
+
+ )}
+ {Loading ? (
+
+ ) : (
+
+
+
+
+
+
+
+
+
Tablets
+
+
+
Tablets
+ 24 models
+
+
+
+
+ {activeQuantity === 16 && (
+
+ )}
+ {activeQuantity === 32 && (
+
+ )}
+ {activeQuantity === 64 && (
+
+ )}
+
+
+
+
+ )}
+
+
+
+
+
+ );
+};
diff --git a/src/Components/TopBar/TopBar.tsx b/src/Components/TopBar/TopBar.tsx
new file mode 100644
index 00000000000..4ef7d1cdf1b
--- /dev/null
+++ b/src/Components/TopBar/TopBar.tsx
@@ -0,0 +1,98 @@
+import { Link, NavLink } from 'react-router-dom';
+import './top-bar.scss';
+import './nav.scss';
+import classNames from 'classnames';
+import React, { useEffect } from 'react';
+import menu from '../../images/icons/Menu.svg';
+const logo = new URL('../../images/Logo.png', import.meta.url).href;
+const fav = new URL(
+ '../../images/fav/Icons/Favourites (Heart Like).svg',
+ import.meta.url,
+).href;
+const basket = new URL(
+ '../../images/fav/Shopping bag (Cart).svg',
+ import.meta.url,
+).href;
+
+type Props = {
+ cartItemsCount: number;
+ favouritesCount: number;
+ setMenuOpen: React.Dispatch>;
+};
+
+export const TopBar: React.FC = ({
+ cartItemsCount,
+ favouritesCount,
+ setMenuOpen,
+}) => {
+ useEffect(() => {
+ // debug: verify props received by TopBar
+ // eslint-disable-next-line no-console
+ console.debug('TopBar props:', { cartItemsCount, favouritesCount });
+ }, [cartItemsCount, favouritesCount]);
+ const navLink = [
+ { id: '/', title: 'home' },
+ { id: '/phones', title: 'phones' },
+ { id: '/tablets', title: 'tablets' },
+ { id: '/accessories', title: 'accessories' },
+ ];
+
+ const getLinkClass = ({ isActive }: { isActive: boolean }) =>
+ classNames('nav__link', {
+ 'nav__link-selected': isActive,
+ });
+
+ const getLink = ({ isActive }: { isActive: boolean }) =>
+ classNames('', {
+ 'header__vectors-selected': isActive,
+ });
+
+ return (
+
+ );
+};
diff --git a/src/Components/TopBar/nav.scss b/src/Components/TopBar/nav.scss
new file mode 100644
index 00000000000..f9b248d6e19
--- /dev/null
+++ b/src/Components/TopBar/nav.scss
@@ -0,0 +1,86 @@
+
+@import '../../utils/mixins';
+
+.nav {
+ display: flex;
+ align-items: center;
+
+ @include on-tablet {
+ // display: flex;
+ gap: 34px;
+ }
+
+ @include on-desktop {
+ gap: 64px;
+ }
+
+ &__list {
+ display: none;
+
+ list-style: none;
+ margin: 0;
+ padding: 0;
+ flex-direction: column;
+
+ @include on-tablet {
+ display: flex;
+ }
+ }
+ &__link {
+ font-weight: 800;
+ font-size: 12px;
+ line-height: 11px;
+ text-decoration: none;
+ text-transform: uppercase;
+ color: #75767F;
+ transition: 0.3s;
+
+ &:hover {
+ color: #F1F2F9;
+ transition: 0.3s;
+ }
+ }
+
+ &__link-selected {
+ font-weight: 800;
+ font-size: 12px;
+ line-height: 11px;
+ text-decoration: none;
+ text-transform: uppercase;
+ color: #F1F2F9;
+ transition: 0.3s;
+
+ &::after {
+ content: '';
+ display: block;
+ position: relative;
+ left: 0;
+ bottom: 0;
+ width: 100%;
+ height: 3px;
+ background: #F1F2F9;
+
+ animation: slide-in-right 0.5s ease-out;
+
+ @include on-tablet {
+ top: 12px;
+ }
+
+ @include on-desktop {
+ top: 18px;
+ }
+ }
+ }
+}
+
+
+@keyframes slide-in-right {
+ from {
+ transform:scaleX(0);
+ opacity: 0;
+ }
+ to {
+ transform: scaleX(100%);
+ opacity: 1;
+ }
+}
diff --git a/src/Components/TopBar/top-bar.scss b/src/Components/TopBar/top-bar.scss
new file mode 100644
index 00000000000..4576782f565
--- /dev/null
+++ b/src/Components/TopBar/top-bar.scss
@@ -0,0 +1,19 @@
+@import '../../utils/mixins';
+
+.top-bar {
+ display: flex;
+ align-items: center;
+
+ justify-content: space-between;
+ width: 100%;
+
+ gap: 50px;
+
+ &__logo {
+ width: 64px;
+
+ @include on-desktop {
+ width: 80px;
+ }
+ }
+}
diff --git a/src/Context/Context.tsx b/src/Context/Context.tsx
new file mode 100644
index 00000000000..7212c112fc0
--- /dev/null
+++ b/src/Context/Context.tsx
@@ -0,0 +1,178 @@
+/* eslint-disable @typescript-eslint/indent */
+/* eslint-disable @typescript-eslint/no-explicit-any */
+
+import {
+ CartState,
+ CartAction,
+ CartItem,
+ CartContextType,
+} from '../types/Context';
+
+import React, {
+ createContext,
+ useContext,
+ useReducer,
+ useEffect,
+ useCallback,
+} from 'react';
+import { Products } from '../types/Products';
+
+type Props = React.PropsWithChildren<{ someFlag?: boolean }>;
+
+const STORAGE_KEY = 'cart';
+
+const normalizeImagePath = (src: string) => src.replace(/rimg\//g, 'img/');
+
+const normalizeProduct = (product: Products): Products =>
+ product.image
+ ? { ...product, image: normalizeImagePath(product.image) }
+ : product;
+
+function readCart() {
+ try {
+ const raw = localStorage.getItem(STORAGE_KEY);
+
+ if (!raw) {
+ return { items: [] };
+ }
+
+ const parsed = JSON.parse(raw);
+
+ return {
+ items: Array.isArray(parsed.items)
+ ? parsed.items.map((item: CartItem) => ({
+ ...item,
+ id: String(item.id),
+ product: normalizeProduct(item.product),
+ }))
+ : [],
+ };
+ } catch {
+ return { items: [] };
+ }
+}
+
+function cartReducer(state: CartState, action: CartAction): CartState {
+ switch (action.type) {
+ case 'ADD': {
+ const payloadId = String(action.payload.id);
+ const normalizedPayload = {
+ ...action.payload,
+ id: payloadId,
+ };
+ const exists = state.items.find(i => i.id === payloadId);
+
+ if (exists) {
+ return state;
+ } // do nothing if already in cart
+
+ return {
+ ...state,
+ items: [
+ ...state.items,
+ {
+ id: payloadId,
+ product: normalizedPayload,
+ quantity: 1,
+ },
+ ],
+ };
+ }
+
+ case 'REMOVE':
+ return {
+ ...state,
+ items: state.items.filter(i => i.id !== action.payload),
+ };
+ case 'CHANGE_QTY':
+ return {
+ ...state,
+ items: state.items.map(i =>
+ i.id === action.payload.id
+ ? { ...i, quantity: Math.max(1, action.payload.quantity) }
+ : i,
+ ),
+ };
+ case 'CLEAR':
+ return { items: [] };
+ default:
+ return state;
+ }
+}
+
+const CartContext = createContext(null);
+
+export const CartProvider = ({ children }: Props) => {
+ const [state, dispatch] = useReducer(cartReducer, readCart());
+
+ useEffect(() => {
+ try {
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
+ } catch (e) {
+ /* ignore storage errors */
+ }
+ }, [state]);
+ const addToCart = useCallback(
+ (product: Products) =>
+ dispatch({
+ type: 'ADD',
+ payload: product,
+ }),
+ [dispatch],
+ );
+
+ const removeFromCart = useCallback(
+ (id: string) => dispatch({ type: 'REMOVE', payload: id }),
+ [dispatch],
+ );
+ const changeQuantity = useCallback(
+ (id: string, quantity: number) =>
+ dispatch({
+ type: 'CHANGE_QTY',
+ payload: { id, quantity },
+ }),
+ [dispatch],
+ );
+
+ const isInCart = useCallback(
+ (id: string | number) => {
+ const nid = typeof id === 'number' ? String(id) : id;
+
+ return state.items.some(i => i.id === nid);
+ },
+ [state.items],
+ );
+
+ const clearCart = useCallback(() => dispatch({ type: 'CLEAR' }), [dispatch]);
+
+ const totalQuantity = state.items.reduce(
+ (s: number, it: CartItem) => s + it.quantity,
+ 0,
+ );
+ const totalPrice = state.items.reduce(
+ (s: number, it: CartItem) => s + it.quantity * (it.product.price || 0),
+ 0,
+ );
+ const value = {
+ items: state.items,
+ addToCart,
+ removeFromCart,
+ changeQuantity,
+ clearCart,
+ isInCart,
+ totalQuantity,
+ totalPrice,
+ };
+
+ return {children} ;
+};
+
+export function useCart() {
+ const ctx = useContext(CartContext);
+
+ if (!ctx) {
+ throw new Error('useCart must be used inside CartProvider');
+ }
+
+ return ctx;
+}
diff --git a/src/Context/FavouritesContext.tsx b/src/Context/FavouritesContext.tsx
new file mode 100644
index 00000000000..ad63111840f
--- /dev/null
+++ b/src/Context/FavouritesContext.tsx
@@ -0,0 +1,152 @@
+/* eslint-disable @typescript-eslint/no-unused-vars */
+import React, {
+ createContext,
+ useCallback,
+ useContext,
+ useEffect,
+ useReducer,
+} from 'react';
+import {
+ FavState,
+ FavAction,
+ FavContextType,
+ FavItem,
+} from '../types/ContextFavourites';
+import { Products } from '../types/Products';
+
+type Props = React.PropsWithChildren<{ someFlag?: boolean }>;
+
+const STORAGE_KEY = 'favourites';
+
+const normalizeImagePath = (src: string) => src.replace(/rimg\//g, 'img/');
+
+const normalizeProduct = (product: Products): Products =>
+ product.image
+ ? { ...product, image: normalizeImagePath(product.image) }
+ : product;
+
+function readFav(): FavState {
+ try {
+ const raw = localStorage.getItem(STORAGE_KEY);
+
+ if (!raw) {
+ return { items: [] };
+ }
+
+ const parsed = JSON.parse(raw);
+
+ if (!parsed || !Array.isArray(parsed.items)) {
+ return { items: [] };
+ }
+
+ return {
+ items: parsed.items.map((item: FavItem) => ({
+ ...item,
+ product: normalizeProduct(item.product),
+ })),
+ };
+ } catch {
+ return { items: [] };
+ }
+}
+
+function FavReducer(state: FavState, action: FavAction): FavState {
+ switch (action.type) {
+ case 'ADD': {
+ const payload = action.payload as Products;
+ const id = String(payload.id);
+ const exists = state.items.find(i => i.id === id);
+
+ if (exists) {
+ return state;
+ }
+
+ return {
+ ...state,
+ items: [...state.items, { id, product: payload, quantity: 1 }],
+ };
+ }
+
+ case 'REMOVE':
+ return {
+ ...state,
+ items: state.items.filter(i => i.id !== action.payload),
+ };
+
+ case 'CLEAR':
+ return { items: [] };
+
+ case 'TOGGLE': {
+ if (state.items.some(i => i.id === action.payload)) {
+ return { items: state.items.filter(i => i.id !== action.payload) };
+ }
+
+ return state;
+ }
+
+ default:
+ return state;
+ }
+}
+
+const FavContext = createContext(null);
+
+export const FavProvider = ({ children }: Props) => {
+ const [state, dispatch] = useReducer(FavReducer, readFav());
+
+ useEffect(() => {
+ try {
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
+ } catch {
+ /* ignore */
+ }
+ }, [state]);
+ const addToFav = useCallback(
+ (product: Products) => {
+ dispatch({ type: 'ADD', payload: product });
+ },
+ [dispatch],
+ );
+ const removeFromFav = useCallback(
+ (id: string) => {
+ dispatch({ type: 'REMOVE', payload: id });
+ },
+ [dispatch],
+ );
+ const isInFav = useCallback(
+ (id: string | number) => {
+ const nid = typeof id === 'number' ? String(id) : id;
+
+ return state.items.some(i => i.id === nid);
+ },
+ [state.items],
+ );
+ const clearFav = useCallback(() => {
+ dispatch({ type: 'CLEAR' });
+ }, [dispatch]);
+ const totalFavourites = state.items.reduce(
+ (s: number, it: FavItem) => s + it.quantity,
+ 0,
+ );
+
+ const value: FavContextType = {
+ items: state.items,
+ addToFav,
+ removeFromFav,
+ clearFav,
+ isInFav,
+ totalFavourites,
+ };
+
+ return {children} ;
+};
+
+export function useFav() {
+ const ctx = useContext(FavContext);
+
+ if (!ctx) {
+ throw new Error('useFav must be used inside FavProvider');
+ }
+
+ return ctx;
+}
diff --git a/src/Root.tsx b/src/Root.tsx
new file mode 100644
index 00000000000..6857e36176d
--- /dev/null
+++ b/src/Root.tsx
@@ -0,0 +1,37 @@
+import { HashRouter as Router, Routes, Route } from 'react-router-dom';
+import { Home } from './Components/Home/Home';
+import './App.scss';
+// eslint-disable-next-line max-len
+import { ErrorNotification } from './Components/ErrorNotification/ErrorNotification';
+import { Phones } from './Components/Phones/Phones';
+import { Tablets } from './Components/Tablets/Tablets';
+import { Accessories } from './Components/Accessories/Accessories';
+import { PhoneSpec } from './Components/DevSpec/PhoneSpec';
+import { CartProvider } from './Context/Context';
+import { FavProvider } from './Context/FavouritesContext';
+import React from 'react';
+import { TabletSpec } from './Components/DevSpec/TabletSpec';
+import { AccSpec } from './Components/DevSpec/AccSpec';
+import { Cart } from './Components/Cart/Cart';
+import { Favourites } from './Components/Favourites/Favourites';
+
+export const Root = () => (
+
+
+
+
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+
+
+
+
+);
diff --git a/src/api/api.ts b/src/api/api.ts
new file mode 100644
index 00000000000..b915dcce438
--- /dev/null
+++ b/src/api/api.ts
@@ -0,0 +1,167 @@
+/* eslint-disable no-console */
+import { Accessorie } from '../types/Accessories';
+import { Discounted } from '../types/Discounted';
+import { Phone } from '../types/Phone';
+import { Products } from '../types/Products';
+import { Tablet } from '../types/Tablets';
+
+const BASE_URL = import.meta.env.BASE_URL;
+
+const API_URL_PHONES = `${BASE_URL}/api/phones.json`;
+const API_URL_TABLETS = `${BASE_URL}/api/tablets.json`;
+const API_URL_PRODUCTS = `${BASE_URL}/api/products.json`;
+const API_URL_DISCOUNTED_PRODUCTS = `${BASE_URL}/api/products.json`;
+const API_URL_ACCESSORIES = `${BASE_URL}/api/accessories.json`;
+
+function wait(delay: number) {
+ return new Promise(resolve => setTimeout(resolve, delay));
+}
+
+export async function getPhones(): Promise {
+ await wait(500);
+ const res = await fetch(API_URL_PHONES);
+
+ if (!res.ok) {
+ throw new Error(`Failed to load phones: ${res.status}`);
+ }
+
+ const data = await res.json();
+
+ return data as Phone[];
+}
+
+export async function getTablets(): Promise {
+ await wait(500);
+ const res = await fetch(API_URL_TABLETS);
+
+ if (!res.ok) {
+ throw new Error(`Failed to load phones: ${res.status}`);
+ }
+
+ const data = await res.json();
+
+ return data as Tablet[];
+}
+
+export async function getTabletById(id: string): Promise {
+ if (!id) {
+ throw new Error('id is required');
+ }
+
+ await wait(500);
+ const res = await fetch(API_URL_TABLETS);
+
+ if (!res.ok) {
+ throw new Error(`Failed to load tablet: ${res.status}`);
+ }
+
+ const tablets: Tablet[] = await res.json();
+
+ return (
+ tablets.find(item => String(item.id) === id || item.namespaceId === id) ??
+ null
+ );
+}
+
+export async function getProducts(): Promise {
+ await wait(500);
+ const res = await fetch(API_URL_PRODUCTS);
+
+ if (!res.ok) {
+ throw new Error(`Failed to load phones: ${res.status}`);
+ }
+
+ const data = await res.json();
+
+ return data as Products[];
+}
+
+export async function getDiscountedProducts(): Promise {
+ await wait(500);
+ const res = await fetch(API_URL_DISCOUNTED_PRODUCTS);
+
+ if (!res.ok) {
+ throw new Error(`Failed to load discounted products: ${res.status}`);
+ }
+
+ const data = await res.json();
+
+ return data as Discounted[];
+}
+
+export async function getAccessories(): Promise {
+ await wait(500);
+ const res = await fetch(API_URL_ACCESSORIES);
+
+ if (!res.ok) {
+ throw new Error(`Failed to load phones: ${res.status}`);
+ }
+
+ const data = await res.json();
+
+ return data as Accessorie[];
+}
+
+export async function getAccessorieById(
+ id: string,
+): Promise {
+ if (!id) {
+ throw new Error('id is required');
+ }
+
+ await wait(500);
+ const res = await fetch(API_URL_ACCESSORIES);
+
+ if (!res.ok) {
+ throw new Error(`Failed to load accessorie: ${res.status}`);
+ }
+
+ const accessories: Accessorie[] = await res.json();
+
+ return (
+ accessories.find(
+ item => String(item.id) === id || item.namespaceId === id,
+ ) ?? null
+ );
+}
+
+export async function getProductById(id: string): Promise {
+ if (!id) {
+ throw new Error('id is required');
+ }
+
+ await wait(500);
+
+ const res = await fetch(API_URL_PRODUCTS);
+
+ if (!res.ok) {
+ throw new Error(`Failed to load product: ${res.status}`);
+ }
+
+ const products: Products[] = await res.json();
+ const product = products.find(
+ item => String(item.id) === id || item.itemId === id,
+ );
+
+ if (!product) {
+ const phonesRes = await fetch(API_URL_PHONES);
+
+ if (!phonesRes.ok) {
+ throw new Error(`Failed to load phones: ${phonesRes.status}`);
+ }
+
+ const phones: Phone[] = await phonesRes.json();
+
+ return phones.find(phone => String(phone.id) === id) ?? null;
+ }
+
+ const phonesRes = await fetch(API_URL_PHONES);
+
+ if (!phonesRes.ok) {
+ throw new Error(`Failed to load phones: ${phonesRes.status}`);
+ }
+
+ const phones: Phone[] = await phonesRes.json();
+
+ return phones.find(phone => phone.id === product.itemId) ?? null;
+}
diff --git a/src/fonts/Fontfabric - Mont ExtraLight DEMO.otf b/src/fonts/Fontfabric - Mont ExtraLight DEMO.otf
new file mode 100644
index 00000000000..13d1950f093
Binary files /dev/null and b/src/fonts/Fontfabric - Mont ExtraLight DEMO.otf differ
diff --git a/src/fonts/Fontfabric - Mont Heavy DEMO.otf b/src/fonts/Fontfabric - Mont Heavy DEMO.otf
new file mode 100644
index 00000000000..7429cee45fa
Binary files /dev/null and b/src/fonts/Fontfabric - Mont Heavy DEMO.otf differ
diff --git a/src/fonts/fonts.scss b/src/fonts/fonts.scss
new file mode 100644
index 00000000000..16379e9433c
--- /dev/null
+++ b/src/fonts/fonts.scss
@@ -0,0 +1,20 @@
+@font-face {
+ font-family: Mont;
+ src: url('/fonts/Mont-Bold.otf') format('opentype');
+ font-weight: normal;
+ font-style: normal;
+}
+
+@font-face {
+ font-family: 'Mont SemiBold';
+ src: url('/fonts/Mont-SemiBold.otf') format('opentype');
+ font-weight: normal;
+ font-style: normal;
+}
+
+@font-face {
+ font-family: 'Mont Regular';
+ src: url('/fonts/Mont-Regular.otf') format('opentype');
+ font-weight: normal;
+ font-style: normal;
+}
diff --git a/src/fonts/mont-font-family.zip b/src/fonts/mont-font-family.zip
new file mode 100644
index 00000000000..956716fca1a
Binary files /dev/null and b/src/fonts/mont-font-family.zip differ
diff --git a/src/fonts/mont.zip b/src/fonts/mont.zip
new file mode 100644
index 00000000000..7990d69688a
Binary files /dev/null and b/src/fonts/mont.zip differ
diff --git a/src/fonts/mont/EULA Free Font License Ver. 2.0.pdf b/src/fonts/mont/EULA Free Font License Ver. 2.0.pdf
new file mode 100644
index 00000000000..0211d9fd9df
Binary files /dev/null and b/src/fonts/mont/EULA Free Font License Ver. 2.0.pdf differ
diff --git a/src/fonts/mont/Mont-ExtraLightDEMO.otf b/src/fonts/mont/Mont-ExtraLightDEMO.otf
new file mode 100644
index 00000000000..13d1950f093
Binary files /dev/null and b/src/fonts/mont/Mont-ExtraLightDEMO.otf differ
diff --git a/src/fonts/mont/Mont-HeavyDEMO.otf b/src/fonts/mont/Mont-HeavyDEMO.otf
new file mode 100644
index 00000000000..7429cee45fa
Binary files /dev/null and b/src/fonts/mont/Mont-HeavyDEMO.otf differ
diff --git a/src/fonts/mont/Mont_Specimen.pdf b/src/fonts/mont/Mont_Specimen.pdf
new file mode 100644
index 00000000000..cdb899b82d5
Binary files /dev/null and b/src/fonts/mont/Mont_Specimen.pdf differ
diff --git a/src/images/Banner-tablet.png b/src/images/Banner-tablet.png
new file mode 100644
index 00000000000..16fa4acee40
Binary files /dev/null and b/src/images/Banner-tablet.png differ
diff --git a/src/images/Banner.png b/src/images/Banner.png
new file mode 100644
index 00000000000..d58f0a6a5f0
Binary files /dev/null and b/src/images/Banner.png differ
diff --git a/src/images/BannerAppleWatch.jpg b/src/images/BannerAppleWatch.jpg
new file mode 100644
index 00000000000..6cae7bb1381
Binary files /dev/null and b/src/images/BannerAppleWatch.jpg differ
diff --git a/src/images/BannerTablet.jpg b/src/images/BannerTablet.jpg
new file mode 100644
index 00000000000..6be60458060
Binary files /dev/null and b/src/images/BannerTablet.jpg differ
diff --git a/src/images/Logo.png b/src/images/Logo.png
new file mode 100644
index 00000000000..115dab4942e
Binary files /dev/null and b/src/images/Logo.png differ
diff --git a/src/images/New Models/14 plus red.png b/src/images/New Models/14 plus red.png
new file mode 100644
index 00000000000..3c4e48f8894
Binary files /dev/null and b/src/images/New Models/14 plus red.png differ
diff --git a/src/images/New Models/14 pro gold.png b/src/images/New Models/14 pro gold.png
new file mode 100644
index 00000000000..35c3d2e6158
Binary files /dev/null and b/src/images/New Models/14 pro gold.png differ
diff --git a/src/images/New Models/14 pro purple.png b/src/images/New Models/14 pro purple.png
new file mode 100644
index 00000000000..f0e47e7da5f
Binary files /dev/null and b/src/images/New Models/14 pro purple.png differ
diff --git a/src/images/New Models/14 pro silver.png b/src/images/New Models/14 pro silver.png
new file mode 100644
index 00000000000..c90eacf8629
Binary files /dev/null and b/src/images/New Models/14 pro silver.png differ
diff --git a/src/images/New Models/category-phone.png b/src/images/New Models/category-phone.png
new file mode 100644
index 00000000000..ebb10d86a1c
Binary files /dev/null and b/src/images/New Models/category-phone.png differ
diff --git a/src/images/New Models/category-tablet.png b/src/images/New Models/category-tablet.png
new file mode 100644
index 00000000000..335f50b2624
Binary files /dev/null and b/src/images/New Models/category-tablet.png differ
diff --git a/src/images/New Models/gold.svg b/src/images/New Models/gold.svg
new file mode 100644
index 00000000000..06bd6976ed1
--- /dev/null
+++ b/src/images/New Models/gold.svg
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/src/images/banner-phone.png b/src/images/banner-phone.png
new file mode 100644
index 00000000000..402cc61117e
Binary files /dev/null and b/src/images/banner-phone.png differ
diff --git a/src/images/bannerphoneacc.jpg b/src/images/bannerphoneacc.jpg
new file mode 100644
index 00000000000..119b2dee09f
Binary files /dev/null and b/src/images/bannerphoneacc.jpg differ
diff --git a/src/images/bannerphonephone.jpg b/src/images/bannerphonephone.jpg
new file mode 100644
index 00000000000..06e89fc209b
Binary files /dev/null and b/src/images/bannerphonephone.jpg differ
diff --git a/src/images/bannerphonetablet.jpg b/src/images/bannerphonetablet.jpg
new file mode 100644
index 00000000000..40018b73bf2
Binary files /dev/null and b/src/images/bannerphonetablet.jpg differ
diff --git a/src/images/fav/Icons/Favourites (Heart Like).svg b/src/images/fav/Icons/Favourites (Heart Like).svg
new file mode 100644
index 00000000000..8caddd8d94d
--- /dev/null
+++ b/src/images/fav/Icons/Favourites (Heart Like).svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/src/images/fav/Shopping bag (Cart).svg b/src/images/fav/Shopping bag (Cart).svg
new file mode 100644
index 00000000000..380aed0a0a0
--- /dev/null
+++ b/src/images/fav/Shopping bag (Cart).svg
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/src/images/icons/ActiveFav.svg b/src/images/icons/ActiveFav.svg
new file mode 100644
index 00000000000..0a33fa8d98e
--- /dev/null
+++ b/src/images/icons/ActiveFav.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/src/images/icons/Chevron (Arrow Left).svg b/src/images/icons/Chevron (Arrow Left).svg
new file mode 100644
index 00000000000..e2016da355b
--- /dev/null
+++ b/src/images/icons/Chevron (Arrow Left).svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/src/images/icons/Chevron (Arrow Right) grey.png b/src/images/icons/Chevron (Arrow Right) grey.png
new file mode 100644
index 00000000000..477bab50840
Binary files /dev/null and b/src/images/icons/Chevron (Arrow Right) grey.png differ
diff --git a/src/images/icons/Chevron (Arrow Right).svg b/src/images/icons/Chevron (Arrow Right).svg
new file mode 100644
index 00000000000..3609b14bd62
--- /dev/null
+++ b/src/images/icons/Chevron (Arrow Right).svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/src/images/icons/Chevron Arrow up.svg b/src/images/icons/Chevron Arrow up.svg
new file mode 100644
index 00000000000..2c6c2331122
--- /dev/null
+++ b/src/images/icons/Chevron Arrow up.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/src/images/icons/ChevronGray.svg b/src/images/icons/ChevronGray.svg
new file mode 100644
index 00000000000..6e938ce2452
--- /dev/null
+++ b/src/images/icons/ChevronGray.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/src/images/icons/Close.png b/src/images/icons/Close.png
new file mode 100644
index 00000000000..5bcd4f0d03e
Binary files /dev/null and b/src/images/icons/Close.png differ
diff --git a/src/images/icons/Close.svg b/src/images/icons/Close.svg
new file mode 100644
index 00000000000..aec415e35ad
--- /dev/null
+++ b/src/images/icons/Close.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/src/images/icons/Home.svg b/src/images/icons/Home.svg
new file mode 100644
index 00000000000..e16ca7d7943
--- /dev/null
+++ b/src/images/icons/Home.svg
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/src/images/icons/Icons/Rectangle 37.svg b/src/images/icons/Icons/Rectangle 37.svg
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/src/images/icons/Icons/favicon.png b/src/images/icons/Icons/favicon.png
new file mode 100644
index 00000000000..9caf48e6483
Binary files /dev/null and b/src/images/icons/Icons/favicon.png differ
diff --git a/src/images/icons/Logo.svg b/src/images/icons/Logo.svg
new file mode 100644
index 00000000000..d3f5d02da14
--- /dev/null
+++ b/src/images/icons/Logo.svg
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/images/icons/Menu.svg b/src/images/icons/Menu.svg
new file mode 100644
index 00000000000..c8c52c08a95
--- /dev/null
+++ b/src/images/icons/Menu.svg
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/src/images/icons/Minus.svg b/src/images/icons/Minus.svg
new file mode 100644
index 00000000000..7ca53e577aa
--- /dev/null
+++ b/src/images/icons/Minus.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/src/images/icons/Nice Gadgets.svg b/src/images/icons/Nice Gadgets.svg
new file mode 100644
index 00000000000..07cfcaa431f
--- /dev/null
+++ b/src/images/icons/Nice Gadgets.svg
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/images/icons/Phone catalog (V2) Original Dark (1).zip b/src/images/icons/Phone catalog (V2) Original Dark (1).zip
new file mode 100644
index 00000000000..8da676adb9d
Binary files /dev/null and b/src/images/icons/Phone catalog (V2) Original Dark (1).zip differ
diff --git a/src/images/icons/Phone catalog (V2) Original Dark (2).zip b/src/images/icons/Phone catalog (V2) Original Dark (2).zip
new file mode 100644
index 00000000000..63a6dfba53c
Binary files /dev/null and b/src/images/icons/Phone catalog (V2) Original Dark (2).zip differ
diff --git a/src/images/icons/Phone catalog (V2) Original Dark (3).zip b/src/images/icons/Phone catalog (V2) Original Dark (3).zip
new file mode 100644
index 00000000000..3e989ac2f2d
Binary files /dev/null and b/src/images/icons/Phone catalog (V2) Original Dark (3).zip differ
diff --git a/src/images/icons/Phone catalog (V2) Original Dark (4).zip b/src/images/icons/Phone catalog (V2) Original Dark (4).zip
new file mode 100644
index 00000000000..fadd504e481
Binary files /dev/null and b/src/images/icons/Phone catalog (V2) Original Dark (4).zip differ
diff --git a/src/images/icons/Phone catalog (V2) Original Dark (5).zip b/src/images/icons/Phone catalog (V2) Original Dark (5).zip
new file mode 100644
index 00000000000..c44db5a66e4
Binary files /dev/null and b/src/images/icons/Phone catalog (V2) Original Dark (5).zip differ
diff --git a/src/images/icons/Phone catalog (V2) Original Dark (6).zip b/src/images/icons/Phone catalog (V2) Original Dark (6).zip
new file mode 100644
index 00000000000..b59677f9e6a
Binary files /dev/null and b/src/images/icons/Phone catalog (V2) Original Dark (6).zip differ
diff --git a/src/images/icons/Phone catalog (V2) Original Dark (7).zip b/src/images/icons/Phone catalog (V2) Original Dark (7).zip
new file mode 100644
index 00000000000..f2299bf36f6
Binary files /dev/null and b/src/images/icons/Phone catalog (V2) Original Dark (7).zip differ
diff --git a/src/images/icons/Phone catalog (V2) Original Dark (8).zip b/src/images/icons/Phone catalog (V2) Original Dark (8).zip
new file mode 100644
index 00000000000..dd471a8c01c
Binary files /dev/null and b/src/images/icons/Phone catalog (V2) Original Dark (8).zip differ
diff --git a/src/images/icons/Phone catalog (V2) Original Dark.zip b/src/images/icons/Phone catalog (V2) Original Dark.zip
new file mode 100644
index 00000000000..0a32034c6d7
Binary files /dev/null and b/src/images/icons/Phone catalog (V2) Original Dark.zip differ
diff --git a/src/images/icons/Plus.svg b/src/images/icons/Plus.svg
new file mode 100644
index 00000000000..aa791a47ad5
--- /dev/null
+++ b/src/images/icons/Plus.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/src/images/icons/Rectangle 37.svg b/src/images/icons/Rectangle 37.svg
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/src/images/icons/arrow-down.svg b/src/images/icons/arrow-down.svg
new file mode 100644
index 00000000000..d888975fcb7
--- /dev/null
+++ b/src/images/icons/arrow-down.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/src/images/icons/favicon.svg b/src/images/icons/favicon.svg
new file mode 100644
index 00000000000..fd9ab53141d
--- /dev/null
+++ b/src/images/icons/favicon.svg
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/src/images/icons/minus b/src/images/icons/minus
new file mode 100644
index 00000000000..db49ffc766b
Binary files /dev/null and b/src/images/icons/minus differ
diff --git a/src/images/icons/minuss.zip b/src/images/icons/minuss.zip
new file mode 100644
index 00000000000..e83bc180d43
Binary files /dev/null and b/src/images/icons/minuss.zip differ
diff --git a/src/images/icons/plus.zip b/src/images/icons/plus.zip
new file mode 100644
index 00000000000..ca3fc61ac3e
Binary files /dev/null and b/src/images/icons/plus.zip differ
diff --git a/src/index.tsx b/src/index.tsx
index 50470f1508d..b0620ab4c64 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -1,4 +1,4 @@
import { createRoot } from 'react-dom/client';
-import { App } from './App';
+import { Root } from './Root';
-createRoot(document.getElementById('root') as HTMLElement).render( );
+createRoot(document.getElementById('root') as HTMLElement).render( );
diff --git a/src/styles/Aside.scss b/src/styles/Aside.scss
new file mode 100644
index 00000000000..86a09d1225d
--- /dev/null
+++ b/src/styles/Aside.scss
@@ -0,0 +1,15 @@
+.aside {
+ &__title {
+ font-family: Mont, sans-serif;
+ font-weight: 800;
+ font-style: Bold;
+ font-size: 48px;
+ line-height: 56px;
+ letter-spacing: -1%;
+ }
+}
+
+html.no-scroll {
+ overflow: hidden;
+ scroll-behavior: none;
+}
diff --git a/src/styles/Page.scss b/src/styles/Page.scss
new file mode 100644
index 00000000000..de9ad59ccd9
--- /dev/null
+++ b/src/styles/Page.scss
@@ -0,0 +1,368 @@
+.page {
+ display: flex;
+ flex-direction: column;
+ min-height: 100vh;
+ overflow: hidden;
+ scroll-behavior: smooth;
+
+ &__grid {
+ display: grid;
+
+ --columns: 4;
+
+ column-gap: 20px;
+ grid-template-columns: repeat(var(--columns), 1fr);
+ }
+
+ &__favourites {
+ display: none;
+ z-index: 0;
+
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+
+ opacity: 0;
+ transition: all 0.3s;
+ transform: translateX(0);
+
+ &:target {
+ opacity: 0;
+ transform: translateX(0);
+ }
+ }
+
+ &__footer {
+ margin-top: auto;
+ width: 100%;
+ }
+
+ &__icon {
+ &-link {
+ display: flex;
+
+ align-items: center;
+
+ gap: 8px;
+ }
+ }
+
+ &__path {
+ display: flex;
+
+ gap: 8px;
+
+ align-items: center;
+
+ margin-top: 25px;
+
+
+ &-phones {
+ font-family: "Mont SemiBold", sans-serif;
+
+ color: #75767F;
+ font-weight: 700;
+ font-size: 12px;
+ line-height: 100%;
+ }
+ }
+
+ &__icon-home {
+ height: 16px;
+ width: 16px;
+ }
+
+ &__text {
+ margin-top: 40px;
+ margin-bottom: 40px;
+ }
+
+ &__title {
+ margin: 0;
+
+ font-weight: 800;
+ font-size: 48px;
+ line-height: 56px;
+
+ margin-bottom: 8px;
+ }
+
+ &__subtitle {
+ font-family: "Mont Regular", sans-serif;
+ color: #75767F;
+ font-weight: 600;
+ font-size: 14px;
+ line-height: 21px;
+ }
+
+ &__paths {
+ color: #75767F;
+ font-family: 'Mont regular', sans-serif;
+ font-weight: 700;
+ font-size: 12px;
+ line-height: 100%;
+ }
+
+ &__models {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+
+ &-phones {
+ display: flex;
+ gap: 16px;
+ }
+
+ &-disabled {
+ display: none;
+ }
+
+ &-page {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ }
+
+ &-button {
+ color: #F1F2F9;
+
+ height: 32px;
+ width: 32px;
+ border: none;
+ background-color: #161827;
+
+ cursor: pointer;
+
+ &_active {
+ background-color: #905BFF;
+ }
+ }
+
+ &-arr {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background-color: #323542;
+ border: #323542 1px solid;
+ margin-inline: 8px;
+ transition: 0.3s;
+
+ &-disabled {
+ background-color: transparent;
+ border: #3B3E4A 1px solid;
+ cursor: not-allowed;
+ }
+
+ &-left {
+ transform: rotate(180deg);
+ }
+
+ &:hover {
+ transition: 0.3s;
+ background-color: #4A4D58;
+ }
+ }
+
+ &-container {
+ color: #F1F2F9;
+
+ padding-top: 32px;
+ padding-inline: 32px;
+ max-width: 100%;
+
+ transition: 0.5s;
+ }
+
+
+ &-phone {
+ cursor: pointer;
+ text-decoration: none;
+
+ background-color: #161827;
+ align-items: center;
+ justify-content: center;
+ border: #161827 1px solid;
+
+ margin-bottom: 40px;
+
+ transition: 0.3s;
+
+ &:hover {
+ transition: 0.3s;
+ border: #323542 1px solid;
+ }
+ }
+
+ &-image {
+ object-fit: contain;
+ width: 100%;
+ height: 100%;
+
+ transition: 0.3s;
+
+ &:hover {
+ transform: scale(1.1);
+ }
+ }
+
+ &-img {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ height: 200px;
+
+ margin-bottom: 8px;
+ }
+
+ &-title {
+ font-family: 'Mont Regular', sans-serif;
+ font-weight: 600;
+ font-size: 14px;
+ line-height: 21px;
+ }
+
+ &-price {
+ display: flex;
+
+ align-items: center;
+
+ font-family: 'Mont SemiBold', sans-serif;
+ font-weight: 800;
+ font-size: 22px;
+ line-height: 140%;
+
+ gap: 8px;
+
+ &-span {
+ font-weight: 600;
+ font-size: 22px;
+ line-height: 100%;
+ text-decoration: line-through;
+ color: #89939A;
+ }
+ }
+
+ &-link {
+ text-decoration: none;
+ }
+
+ &-string {
+ margin-top: 10px;
+ width: 100%;
+ height: 1px;
+ background-color: #3B3E4A;
+ }
+
+ &-text {
+ display: flex;
+ justify-content: space-between;
+ margin-top: 8px;
+ color: #75767F;
+ font-family: 'Mont Regular', sans-serif;
+ font-weight: 700;
+ font-size: 12px;
+ line-height: 100%;
+
+ &__first {
+ margin-top: 16px;
+ }
+ }
+
+ &-info {
+ margin-bottom: 16px;
+ }
+
+ &-span {
+ color: #F1F2F9;
+
+ text-align: right;
+ }
+
+ &-buttons {
+ display: flex;
+ gap: 8px;
+
+ padding-bottom: 32px;
+ }
+
+ &__link {
+ display: flex;
+
+ align-items: center;
+ justify-content: center;
+
+ text-decoration: none;
+
+ width: 100%;
+ height: 100%;
+ }
+
+ &-fav {
+ display: flex;
+ border: none;
+ height: 40px;
+ width: 54px;
+
+ background-color: #323542;
+
+ justify-content: center;
+
+ text-decoration: none;
+ align-items: center;
+
+ &__img {
+ height: 16px;
+ }
+
+ &__button {
+ border: none;
+
+ background-color: inherit;
+ }
+ }
+
+ &-cart {
+ border: none;
+ height: 40px;
+ width: 100%;
+ background-color: #905BFF;
+
+ &__text {
+ margin: 0;
+ font-family: Mont, sans-serif;
+ font-weight: 700;
+ font-size: 14px;
+ line-height: 21px;
+ text-align: center;
+ color: #F1F2F9;
+ }
+
+ &__link {
+ display: flex;
+
+ align-items: center;
+ justify-content: center;
+
+ text-decoration: none;
+
+ width: 100%;
+ height: 40px;
+ }
+ }
+
+ &-arrows {
+ display: flex;
+ align-items: center;
+ gap: 16px;
+ }
+
+ &-arrow {
+ cursor: pointer;
+ border: 0;
+ height: 32px;
+ width: 32px;
+ background-color: #323542;
+ }
+ }
+}
diff --git a/src/styles/WelcomeBlock.scss b/src/styles/WelcomeBlock.scss
new file mode 100644
index 00000000000..fa492f8b6d0
--- /dev/null
+++ b/src/styles/WelcomeBlock.scss
@@ -0,0 +1,197 @@
+@import '../utils/mixins';
+
+.welcome__block {
+ margin-bottom: 80px;
+
+ &-title {
+ font-family: Mont, sans-serif;
+ font-weight: 800;
+ font-size: 32px;
+ line-height: 56px;
+
+ @include on-tablet {
+ margin-top: 32px;
+ margin-bottom: 32px;
+ font-size: 48px;
+ line-height: 56px;
+ }
+
+ @include on-desktop {
+ margin-top: 56px;
+ margin-bottom: 56px;
+ }
+ }
+
+ &-sliders {
+ gap: 16px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ margin-bottom: 24px;
+ }
+
+ &-image {
+ width: 100%;
+ height: 100%;
+ transition: opacity 0.4s ease, transform 0.4s ease;
+ }
+
+ &-img {
+ position: relative;
+ overflow: hidden;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ align-self: stretch;
+ }
+
+ &-track {
+ display: flex;
+ width: 300%;
+ transition: transform 0.6s ease;
+ }
+
+ &-slide {
+ width: 100%;
+ flex-shrink: 0;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ }
+
+ &-slider {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ align-self: stretch;
+ min-width: 32px !important;
+ min-height: 100%;
+ background-color: #323542;
+ border: none;
+
+ cursor: pointer;
+ padding: 0;
+ transition: 0.3s;
+
+ &:hover {
+ background-color: #4A4D58;
+ transition: 0.3s;
+ }
+ }
+
+ &-rectangles {
+ justify-content: center;
+ display: flex;
+ gap: 14px;
+ }
+
+ &-rectangle {
+ width: 14px;
+ height: 4px;
+ background-color: #3B3E4A;
+ cursor: pointer;
+
+ &-active {
+ background-color: #F1F2F9;
+ width: 14px;
+ height: 4px;
+ }
+ }
+
+ &-order {
+ display: flex;
+ flex-direction: column;
+ justify-content: space-between;
+
+ padding: 50px;
+
+ background-color: #222;
+ border: 1px solid #222;
+
+ margin: 12px;
+ border-radius: 30px;
+ transition: 0.3s;
+
+ &:hover {
+ border: 1px solid #A378FF;
+ transition: 0.3s;
+ }
+
+ &-title {
+ margin: 0;
+ font-size: 40px;
+ line-height: 48px;
+ background: linear-gradient(45deg, #6F44D3, #775AF8);
+ -webkit-background-clip: text;
+ -webkit-text-fill-color: transparent;
+ background-clip: text;
+
+ margin-bottom: 10px;
+ }
+
+ &-description {
+ margin: 0;
+ font-family: 'Mont Regular', sans-serif;
+ font-weight: 500;
+ font-size: 14px;
+ line-height: 24px;
+ color: #F1F2F9;
+ }
+
+ &-btn {
+ cursor: pointer;
+ border-radius: 50px;
+ border: 1px solid #3B3E4A;
+ background-color: transparent;
+ color: #F1F2F9;
+ font-family: 'Mont Regular', sans-serif;
+ text-transform: uppercase;
+
+ height: 60px;
+ width: 190px;
+
+ transition: 0.3s;
+
+ &:hover {
+ border: 1px solid #75767F;
+ transition: 0.3s;
+ }
+ }
+ }
+
+ &-banner {
+ z-index: 2;
+ display: flex;
+ justify-content: center;
+ gap: 100px;
+ padding-right: 24px;
+
+ background-color: #000;
+ }
+
+ &-favi {
+ width: 32px;
+ height: 32px;
+ margin-left: 8px;
+ }
+
+ &-ima {
+ transform: translateY(10px);
+ width: auto;
+ max-width: 100%;
+ object-fit: contain;
+ transition: 0.3s;
+
+ cursor: pointer;
+
+ &:hover {
+ // transform: translateY(-5px);
+ transform: scale(1.05);
+ transition: 0.3s;
+ }
+
+ @include on-tablet {
+ height: 400px;
+ }
+ }
+}
diff --git a/src/styles/categories.scss b/src/styles/categories.scss
new file mode 100644
index 00000000000..eb98e4c5b60
--- /dev/null
+++ b/src/styles/categories.scss
@@ -0,0 +1,99 @@
+
+@import '../utils/mixins';
+
+.categories {
+ @include on-desktop {
+ margin-bottom: 0;
+ }
+
+ &__blocks {
+ display: flex;
+ flex-direction: column;
+ gap: 32px;
+
+ @include on-tablet {
+ flex-direction: row;
+ display: flex;
+ gap: 16px;
+ }
+
+ @include on-desktop {
+ display: flex;
+ gap: 16px;
+ }
+ }
+ &__title {
+ margin-bottom: 25px;
+ }
+}
+
+.category {
+ flex: 0 0 calc(33.33% - 11px);
+ transition: transform 0.3s ease;
+
+ &:hover {
+ transition: transform 0.3s ease;
+ transform: scale(1.01);
+ }
+
+ &__title {
+ margin: 0;
+ font-family: 'Mont regular', sans-serif;
+ font-weight: 700;
+ font-size: 20px;
+ line-height: 100%;
+ margin-bottom: 4px;
+ }
+
+ &__sub-title {
+ margin: 0;
+ color: #75767F;
+ font-family: 'Mont regular', sans-serif;
+ font-weight: 600;
+ font-size: 14px;
+ line-height: 21px;
+ }
+
+ &__image {
+ aspect-ratio: 1 / 1;
+ height: 100%;
+
+ overflow: hidden;
+ position: relative;
+ display: flex;
+ align-items: left;
+ justify-content: right;
+ margin-bottom: 24px;
+
+ @include on-tablet {
+ aspect-ratio: 0;
+ width: 100%;
+ height: 61.5%;
+ }
+ }
+
+ &__img {
+ margin-left: 100px;
+ margin-top: 60px;
+
+ max-width: 100%;
+ max-height: 100%;
+ object-fit: scale-down; // or 'cover' if you want to fill the block
+ transition: transform 0.3s ease;
+
+ &:hover {
+ transition: transform 0.3s ease;
+ transform: scale(1.05);
+ }
+ }
+
+ &-pink {
+ background-color: #6D6474;
+ }
+ &-grey {
+ background-color: #8D8D92;
+ }
+ &-purple {
+ background-color: #973D5F;
+ }
+}
diff --git a/src/styles/icon.scss b/src/styles/icon.scss
new file mode 100644
index 00000000000..0b43357f390
--- /dev/null
+++ b/src/styles/icon.scss
@@ -0,0 +1,11 @@
+.icon {
+ height: 16px;
+
+ &-slider {
+ height: 16px;
+ }
+
+ &-arrow {
+ height: 16px;
+ }
+}
diff --git a/src/styles/newModels.scss b/src/styles/newModels.scss
new file mode 100644
index 00000000000..a0d1ea16f6e
--- /dev/null
+++ b/src/styles/newModels.scss
@@ -0,0 +1,45 @@
+.new__models {
+ &-arr {
+ display: flex;
+ justify-content: space-between;
+ margin-bottom: 24px;
+ }
+
+ &-title {
+ width: 136px;
+
+ @include on-tablet {
+ width: 100%;
+ }
+ }
+
+ &-arrows {
+ display: flex;
+ gap: 16px;
+ }
+
+ &-arrow {
+ cursor: pointer;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ height: 32px;
+ width: 32px;
+ background-color: #323542;
+ border: 1px solid #3B3E4A;
+ transition: 0.3s;
+
+ &-disabled {
+ background-color: transparent;
+ cursor: not-allowed;
+ opacity: 0.5;
+ border-color: #3B3E4A;
+ }
+
+ &:hover {
+ background-color: #4A4D58;
+ border-color: #525761;
+ transition: 0.3s;
+ }
+ }
+}
diff --git a/src/styles/section.scss b/src/styles/section.scss
new file mode 100644
index 00000000000..01912f209a0
--- /dev/null
+++ b/src/styles/section.scss
@@ -0,0 +1,19 @@
+@import '../utils/mixins';
+
+.section {
+ margin-bottom: 80px;
+
+ &-title {
+ margin: 0;
+ font-family: Mont, sans-serif;
+ font-weight: 800;
+ font-size: 22px;
+ line-height: 140%;
+ letter-spacing: -1%;
+
+ @include on-tablet {
+ font-size: 32px;
+ line-height: 41px;
+ }
+ }
+}
diff --git a/src/types/Accessories.ts b/src/types/Accessories.ts
new file mode 100644
index 00000000000..bea3db52742
--- /dev/null
+++ b/src/types/Accessories.ts
@@ -0,0 +1,23 @@
+export interface Accessorie {
+ id: string;
+ category: string;
+ namespaceId: string;
+ capacityAvailable: string[];
+ colorsAvailable: string[];
+ description: [title: string, text: string];
+ images: string[];
+ name: string;
+ priceRegular: number;
+ priceDiscount: number;
+ screen: string;
+ capacity: string;
+ resolution: string;
+ processor: string;
+ ram: string;
+ camera: string;
+ zoom: string;
+ cell: string;
+ color: string[];
+ compatibility: string;
+ material: string;
+}
diff --git a/src/types/Context.ts b/src/types/Context.ts
new file mode 100644
index 00000000000..82270681d46
--- /dev/null
+++ b/src/types/Context.ts
@@ -0,0 +1,20 @@
+import { Products } from './Products';
+
+/* eslint-disable @typescript-eslint/no-unused-vars */
+export type CartItem = { id: string; quantity: number; product: Products };
+export type CartState = { items: CartItem[] };
+export type CartAction =
+ | { type: 'ADD'; payload: Products }
+ | { type: 'REMOVE'; payload: string }
+ | { type: 'CHANGE_QTY'; payload: { id: string; quantity: number } }
+ | { type: 'CLEAR' };
+export type CartContextType = {
+ items: CartItem[];
+ addToCart: (p: Products) => void;
+ removeFromCart: (id: string) => void;
+ changeQuantity: (id: string, qty: number) => void;
+ clearCart: () => void;
+ isInCart: (id: string) => boolean;
+ totalQuantity: number;
+ totalPrice: number;
+};
diff --git a/src/types/ContextFavourites.ts b/src/types/ContextFavourites.ts
new file mode 100644
index 00000000000..a7c89d032b4
--- /dev/null
+++ b/src/types/ContextFavourites.ts
@@ -0,0 +1,17 @@
+import { Products } from './Products';
+
+export type FavItem = { id: string; quantity: number; product: Products };
+export type FavState = { items: FavItem[] }; // store ids
+export type FavAction =
+ | { type: 'ADD'; payload: Products }
+ | { type: 'REMOVE'; payload: string }
+ | { type: 'TOGGLE'; payload: string }
+ | { type: 'CLEAR' };
+export type FavContextType = {
+ items: FavItem[];
+ addToFav: (p: Products) => void;
+ removeFromFav: (id: string) => void;
+ clearFav: () => void;
+ isInFav: (id: string) => boolean;
+ totalFavourites: number;
+};
diff --git a/src/types/Discounted.ts b/src/types/Discounted.ts
new file mode 100644
index 00000000000..f22e84d6931
--- /dev/null
+++ b/src/types/Discounted.ts
@@ -0,0 +1,14 @@
+export interface Discounted {
+ id: string;
+ category: string;
+ itemId: string;
+ name: string;
+ fullPrice: number;
+ price: number;
+ screen: string;
+ capacity: string;
+ color: string;
+ ram: string;
+ year: number;
+ image: string;
+}
diff --git a/src/types/Phone.ts b/src/types/Phone.ts
new file mode 100644
index 00000000000..c3e3724d871
--- /dev/null
+++ b/src/types/Phone.ts
@@ -0,0 +1,25 @@
+export interface Phone {
+ id: string;
+ category: string;
+ namespaceId: string;
+ name: string;
+ capacityAvailable: string[];
+ capacity: string;
+ priceRegular: number;
+ priceDiscount: number;
+ colorsAvailable: string[];
+ color?: string;
+ images: string[];
+ description: Array<{
+ title: string;
+ text: string[];
+ }>;
+ screen: string;
+ resolution: string;
+ processor: string;
+ ram: string;
+ camera: string;
+ zoom: string;
+ cell: string[];
+ year: number;
+}
diff --git a/src/types/Phones.ts b/src/types/Phones.ts
new file mode 100644
index 00000000000..fb624ba8a90
--- /dev/null
+++ b/src/types/Phones.ts
@@ -0,0 +1,22 @@
+export interface Phones {
+ id: number;
+ category: string;
+ namespaceId: string;
+ capacityAvailable: string;
+ colorsAvailable: string;
+ description: [title: string, text: string];
+ images: string[];
+ name: string;
+ priceRegular: string;
+ priceDiscount: string;
+ screen: string;
+ capacity: string;
+ resolution: string;
+ processor: string;
+ year: number;
+ ram: string;
+ camera: string;
+ zoom: string;
+ cell: string;
+ color: string;
+}
diff --git a/src/types/Products.ts b/src/types/Products.ts
new file mode 100644
index 00000000000..1ae49680456
--- /dev/null
+++ b/src/types/Products.ts
@@ -0,0 +1,14 @@
+export interface Products {
+ id: number | string;
+ category: string;
+ itemId: string;
+ name: string;
+ fullPrice: number;
+ price: number;
+ screen: string;
+ capacity: string;
+ color: string;
+ ram: string;
+ year: number;
+ image: string;
+}
diff --git a/src/types/Tablets.ts b/src/types/Tablets.ts
new file mode 100644
index 00000000000..f0af204acaa
--- /dev/null
+++ b/src/types/Tablets.ts
@@ -0,0 +1,21 @@
+export interface Tablet {
+ id: string;
+ category: string;
+ namespaceId: string;
+ capacityAvailable: string[];
+ colorsAvailable: string[];
+ description: [title: string, text: string];
+ images: string[];
+ name: string;
+ priceRegular: number;
+ priceDiscount: number;
+ screen: string;
+ capacity: string;
+ resolution: string;
+ processor: string;
+ ram: string;
+ camera: string;
+ zoom: string;
+ cell: string;
+ color: string;
+}
diff --git a/src/utils/_vars.scss b/src/utils/_vars.scss
new file mode 100644
index 00000000000..0bf6cf65735
--- /dev/null
+++ b/src/utils/_vars.scss
@@ -0,0 +1,3 @@
+$tablet-min-width: 640px;
+$desktop-min-width: 1200px;
+$main-text-color: #F1F2F9;
diff --git a/src/utils/mixins.scss b/src/utils/mixins.scss
new file mode 100644
index 00000000000..fffc88f2fc8
--- /dev/null
+++ b/src/utils/mixins.scss
@@ -0,0 +1,61 @@
+$tablet-min-width: 640px;
+$desktop-min-width: 1200px;
+$main-text-color: #F1F2F9;
+
+@mixin on-tablet {
+ @media (min-width: $tablet-min-width) {
+ @content;
+ }
+}
+
+@mixin on-desktop {
+ @media (min-width: $desktop-min-width) {
+ @content;
+ }
+}
+
+@mixin content-padding-inline() {
+ padding-inline: 16px;
+
+ @include on-tablet {
+ padding-inline: 24px;
+ }
+
+ @include on-desktop {
+ margin-inline: auto;
+ padding-inline: 24px;
+ max-width: 1136px;
+ }
+}
+
+.container {
+ @include content-padding-inline;
+}
+
+@mixin page-grid {
+ display: grid;
+
+ --columns: 4;
+
+ column-gap: 20px;
+ grid-template-columns: repeat(var(--columns), 1fr);
+
+ @include on-tablet {
+ --columns: 8;
+
+ column-gap: 30px;
+ }
+
+ @include on-desktop {
+ --columns: 12;
+
+ column-gap: 16px;
+ }
+}
+
+@mixin hover($_property, $_toValue) {
+ transition: #{$_property} 0.3s;
+ &:hover {
+ #{$_property}: $_toValue;
+ }
+}
diff --git a/src/utils/paths.ts b/src/utils/paths.ts
new file mode 100644
index 00000000000..f0ed19662ef
--- /dev/null
+++ b/src/utils/paths.ts
@@ -0,0 +1,20 @@
+// const BASE_URL = import.meta.env.BASE_URL;
+
+// export const getImagePath = (src?: string): string => {
+// if (!src) {
+// return `${BASE_URL}img/placeholder.png`;
+// }
+
+// // If it already starts with http or https, return as is
+// if (src.startsWith('http://') || src.startsWith('https://')) {
+// return src;
+// }
+
+// // If it starts with /, remove it and prepend BASE_URL
+// if (src.startsWith('/')) {
+// return `${BASE_URL}${src.slice(1)}`;
+// }
+
+// // Otherwise, prepend BASE_URL with /
+// return `${BASE_URL}${src}`;
+// };
diff --git a/tsconfig.json b/tsconfig.json
index cfb168bb26c..1e0fe981faf 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -4,7 +4,9 @@
"src"
],
"compilerOptions": {
+ "jsx": "react",
"sourceMap": false,
+ "baseUrl": ".",
"types": ["node", "cypress"]
}
}
diff --git a/vite.config.ts b/vite.config.ts
index 5a33944a9b4..69e1cbd21b8 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -2,6 +2,7 @@ import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
// https://vitejs.dev/config/
-export default defineConfig({
+export default defineConfig(({ command }) => ({
plugins: [react()],
-})
+ base: command === 'build' ? '/react-portfolio/' : '/',
+}))