diff --git a/.eslintrc.cjs b/.eslintrc.cjs
index b51149cf57..4f84730cb1 100644
--- a/.eslintrc.cjs
+++ b/.eslintrc.cjs
@@ -5,7 +5,7 @@ module.exports = {
},
extends: [
'plugin:react/recommended',
- "plugin:react-hooks/recommended",
+ 'plugin:react-hooks/recommended',
'airbnb-typescript',
'plugin:@typescript-eslint/eslint-recommended',
'plugin:@typescript-eslint/recommended',
@@ -14,11 +14,11 @@ module.exports = {
],
overrides: [
{
- 'files': ['**/*.spec.jsx'],
- 'rules': {
+ files: ['**/*.spec.jsx'],
+ rules: {
'react/jsx-filename-extension': ['off'],
- }
- }
+ },
+ },
],
parser: '@typescript-eslint/parser',
parserOptions: {
@@ -34,18 +34,21 @@ module.exports = {
'import',
'react-hooks',
'@typescript-eslint',
- 'prettier'
+ 'prettier',
],
rules: {
// JS
- 'semi': 'off',
+ semi: 'off',
'@typescript-eslint/semi': ['error', 'always'],
'prefer-const': 2,
curly: [2, 'all'],
- 'max-len': ['error', {
- ignoreTemplateLiterals: true,
- ignoreComments: true,
- }],
+ 'max-len': [
+ 'error',
+ {
+ ignoreTemplateLiterals: true,
+ ignoreComments: true,
+ },
+ ],
'no-redeclare': [2, { builtinGlobals: true }],
'no-console': 2,
'operator-linebreak': 0,
@@ -57,7 +60,11 @@ module.exports = {
2,
{ blankLine: 'always', prev: '*', next: 'return' },
{ blankLine: 'always', prev: ['const', 'let', 'var'], next: '*' },
- { blankLine: 'any', prev: ['const', 'let', 'var'], next: ['const', 'let', 'var'] },
+ {
+ blankLine: 'any',
+ prev: ['const', 'let', 'var'],
+ next: ['const', 'let', 'var'],
+ },
{ blankLine: 'always', prev: 'directive', next: '*' },
{ blankLine: 'always', prev: 'block-like', next: '*' },
],
@@ -73,16 +80,22 @@ module.exports = {
'react/jsx-props-no-spreading': 0,
'react/state-in-constructor': [2, 'never'],
'react-hooks/rules-of-hooks': 2,
- 'jsx-a11y/label-has-associated-control': ["error", {
- assert: "either",
- }],
- 'jsx-a11y/label-has-for': [2, {
- components: ['Label'],
- required: {
- some: ['id', 'nesting'],
+ 'jsx-a11y/label-has-associated-control': [
+ 'error',
+ {
+ assert: 'either',
},
- allowChildren: true,
- }],
+ ],
+ 'jsx-a11y/label-has-for': [
+ 2,
+ {
+ components: ['Label'],
+ required: {
+ some: ['id', 'nesting'],
+ },
+ allowChildren: true,
+ },
+ ],
'react/jsx-uses-react': 'off',
'react/react-in-jsx-scope': 'off',
@@ -91,7 +104,9 @@ module.exports = {
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-unused-vars': ['error'],
'@typescript-eslint/indent': ['error', 2],
- '@typescript-eslint/ban-types': ['error', {
+ '@typescript-eslint/ban-types': [
+ 'error',
+ {
extendDefaults: true,
types: {
'{}': false,
@@ -99,7 +114,13 @@ module.exports = {
},
],
},
- ignorePatterns: ['dist', '.eslintrc.cjs', 'vite.config.ts', 'src/vite-env.d.ts', 'cypress'],
+ ignorePatterns: [
+ 'dist',
+ '.eslintrc.cjs',
+ 'vite.config.ts',
+ 'src/vite-env.d.ts',
+ 'cypress',
+ ],
settings: {
react: {
version: 'detect',
diff --git a/package-lock.json b/package-lock.json
index 836b9e63b4..c04af0e507 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -11,16 +11,20 @@
"license": "GPL-3.0",
"dependencies": {
"@fortawesome/fontawesome-free": "^6.5.2",
+ "bootstrap": "^5.3.3",
"bulma": "^1.0.1",
"classnames": "^2.5.1",
"react": "^18.3.1",
+ "react-bootstrap": "^2.10.5",
"react-dom": "^18.3.1",
+ "react-modal": "^3.16.1",
"react-router-dom": "^6.25.1",
- "react-transition-group": "^4.4.5"
+ "react-transition-group": "^4.4.5",
+ "swiper": "^11.1.14"
},
"devDependencies": {
"@cypress/react18": "^2.0.1",
- "@mate-academy/scripts": "^1.8.5",
+ "@mate-academy/scripts": "^1.9.12",
"@mate-academy/students-ts-config": "*",
"@mate-academy/stylelint-config": "*",
"@types/node": "^20.14.10",
@@ -1184,10 +1188,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": "1.9.12",
+ "resolved": "https://registry.npmjs.org/@mate-academy/scripts/-/scripts-1.9.12.tgz",
+ "integrity": "sha512-/OcmxMa34lYLFlGx7Ig926W1U1qjrnXbjFJ2TzUcDaLmED+A5se652NcWwGOidXRuMAOYLPU2jNYBEkKyXrFJA==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"@octokit/rest": "^17.11.2",
"@types/get-port": "^4.2.0",
@@ -1875,6 +1880,31 @@
"url": "https://opencollective.com/unts"
}
},
+ "node_modules/@popperjs/core": {
+ "version": "2.11.8",
+ "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
+ "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==",
+ "license": "MIT",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/popperjs"
+ }
+ },
+ "node_modules/@react-aria/ssr": {
+ "version": "3.9.6",
+ "resolved": "https://registry.npmjs.org/@react-aria/ssr/-/ssr-3.9.6.tgz",
+ "integrity": "sha512-iLo82l82ilMiVGy342SELjshuWottlb5+VefO3jOQqQRNYnJBFpUSadswDPbRimSgJUZuFwIEYs6AabkP038fA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@swc/helpers": "^0.5.0"
+ },
+ "engines": {
+ "node": ">= 12"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0"
+ }
+ },
"node_modules/@remix-run/router": {
"version": "1.18.0",
"resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.18.0.tgz",
@@ -1883,6 +1913,48 @@
"node": ">=14.0.0"
}
},
+ "node_modules/@restart/hooks": {
+ "version": "0.4.16",
+ "resolved": "https://registry.npmjs.org/@restart/hooks/-/hooks-0.4.16.tgz",
+ "integrity": "sha512-f7aCv7c+nU/3mF7NWLtVVr0Ra80RqsO89hO72r+Y/nvQr5+q0UFGkocElTH6MJApvReVh6JHUFYn2cw1WdHF3w==",
+ "license": "MIT",
+ "dependencies": {
+ "dequal": "^2.0.3"
+ },
+ "peerDependencies": {
+ "react": ">=16.8.0"
+ }
+ },
+ "node_modules/@restart/ui": {
+ "version": "1.8.0",
+ "resolved": "https://registry.npmjs.org/@restart/ui/-/ui-1.8.0.tgz",
+ "integrity": "sha512-xJEOXUOTmT4FngTmhdjKFRrVVF0hwCLNPdatLCHkyS4dkiSK12cEu1Y0fjxktjJrdst9jJIc5J6ihMJCoWEN/g==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.21.0",
+ "@popperjs/core": "^2.11.6",
+ "@react-aria/ssr": "^3.5.0",
+ "@restart/hooks": "^0.4.9",
+ "@types/warning": "^3.0.0",
+ "dequal": "^2.0.3",
+ "dom-helpers": "^5.2.0",
+ "uncontrollable": "^8.0.1",
+ "warning": "^4.0.3"
+ },
+ "peerDependencies": {
+ "react": ">=16.14.0",
+ "react-dom": ">=16.14.0"
+ }
+ },
+ "node_modules/@restart/ui/node_modules/uncontrollable": {
+ "version": "8.0.4",
+ "resolved": "https://registry.npmjs.org/uncontrollable/-/uncontrollable-8.0.4.tgz",
+ "integrity": "sha512-ulRWYWHvscPFc0QQXvyJjY6LIXU56f0h8pQFvhxiKk5V1fcI8gp9Ht9leVAhrVjzqMw0BgjspBINx9r6oyJUvQ==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": ">=16.14.0"
+ }
+ },
"node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.18.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.18.1.tgz",
@@ -2126,6 +2198,15 @@
"integrity": "sha512-sXXKG+uL9IrKqViTtao2Ws6dy0znu9sOaP1di/jKGW1M6VssO8vlpXCQcpZ+jisQ1tTFAC5Jo/EOzFbggBagFQ==",
"dev": true
},
+ "node_modules/@swc/helpers": {
+ "version": "0.5.13",
+ "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.13.tgz",
+ "integrity": "sha512-UoKGxQ3r5kYI9dALKJapMmuK+1zWM/H17Z1+iwnNmzcJRnfFuevZs375TA5rW31pu4BS4NoSy1fRsexDXfWn5w==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
"node_modules/@types/babel__core": {
"version": "7.20.5",
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
@@ -2215,14 +2296,12 @@
"node_modules/@types/prop-types": {
"version": "15.7.12",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.12.tgz",
- "integrity": "sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==",
- "dev": true
+ "integrity": "sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q=="
},
"node_modules/@types/react": {
"version": "18.3.3",
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.3.tgz",
"integrity": "sha512-hti/R0pS0q1/xx+TsI73XIqk26eBsISZ2R0wUijXIngRK9R/e7Xw/cXVxQK7R5JjW+SV4zGcn5hXjudkN/pLIw==",
- "dev": true,
"dependencies": {
"@types/prop-types": "*",
"csstype": "^3.0.2"
@@ -2241,7 +2320,6 @@
"version": "4.4.10",
"resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.10.tgz",
"integrity": "sha512-hT/+s0VQs2ojCX823m60m5f0sL5idt9SO6Tj6Dg+rdphGPIeJbJ6CxvBYkgkGKrYeDjvIpKTR38UzmtHJOGW3Q==",
- "dev": true,
"dependencies": {
"@types/react": "*"
}
@@ -2258,6 +2336,12 @@
"integrity": "sha512-0vWLNK2D5MT9dg0iOo8GlKguPAU02QjmZitPEsXRuJXU/OGIOt9vT9Fc26wtYuavLxtO45v9PGleoL9Z0k1LHg==",
"dev": true
},
+ "node_modules/@types/warning": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/@types/warning/-/warning-3.0.3.tgz",
+ "integrity": "sha512-D1XC7WK8K+zZEveUPY+cf4+kgauk8N4eHr/XIHXGlGYkHLud6hK9lYfZk1ry1TNh798cZUCgb6MqGEG8DkJt6Q==",
+ "license": "MIT"
+ },
"node_modules/@types/yauzl": {
"version": "2.10.3",
"resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz",
@@ -2982,6 +3066,25 @@
"integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==",
"dev": true
},
+ "node_modules/bootstrap": {
+ "version": "5.3.3",
+ "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.3.tgz",
+ "integrity": "sha512-8HLCdWgyoMguSO9o+aH+iuZ+aht+mzW0u3HIMzVu7Srrpv7EBBxTnrFlSCskwdY1+EOFQSm7uMJhNQHkdPcmjg==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/twbs"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/bootstrap"
+ }
+ ],
+ "license": "MIT",
+ "peerDependencies": {
+ "@popperjs/core": "^2.11.8"
+ }
+ },
"node_modules/brace-expansion": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
@@ -3904,6 +4007,15 @@
"integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==",
"dev": true
},
+ "node_modules/dequal": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
+ "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/diff": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz",
@@ -4986,6 +5098,12 @@
"node": ">=4"
}
},
+ "node_modules/exenv": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/exenv/-/exenv-1.2.2.tgz",
+ "integrity": "sha512-Z+ktTxTwv9ILfgKCk32OX3n/doe+OcLTRtqK9pcL+JsP3J1/VW8Uvl4ZjLlKqeW4rzK4oesDOGMEMRIZqtP4Iw==",
+ "license": "BSD-3-Clause"
+ },
"node_modules/extend": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
@@ -6026,6 +6144,15 @@
"node": ">= 0.4"
}
},
+ "node_modules/invariant": {
+ "version": "2.2.4",
+ "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz",
+ "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==",
+ "license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.0.0"
+ }
+ },
"node_modules/is-arguments": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz",
@@ -8611,6 +8738,19 @@
"react-is": "^16.13.1"
}
},
+ "node_modules/prop-types-extra": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/prop-types-extra/-/prop-types-extra-1.1.1.tgz",
+ "integrity": "sha512-59+AHNnHYCdiC+vMwY52WmvP5dM3QLeoumYuEyceQDi9aEhtwN9zIQ2ZNo25sMyXnbh32h+P1ezDsUpUH3JAew==",
+ "license": "MIT",
+ "dependencies": {
+ "react-is": "^16.3.2",
+ "warning": "^4.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=0.14.0"
+ }
+ },
"node_modules/proxy-from-env": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.0.0.tgz",
@@ -8717,6 +8857,36 @@
"node": ">=0.10.0"
}
},
+ "node_modules/react-bootstrap": {
+ "version": "2.10.5",
+ "resolved": "https://registry.npmjs.org/react-bootstrap/-/react-bootstrap-2.10.5.tgz",
+ "integrity": "sha512-XueAOEn64RRkZ0s6yzUTdpFtdUXs5L5491QU//8ZcODKJNDLt/r01tNyriZccjgRImH1REynUc9pqjiRMpDLWQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.24.7",
+ "@restart/hooks": "^0.4.9",
+ "@restart/ui": "^1.6.9",
+ "@types/react-transition-group": "^4.4.6",
+ "classnames": "^2.3.2",
+ "dom-helpers": "^5.2.1",
+ "invariant": "^2.2.4",
+ "prop-types": "^15.8.1",
+ "prop-types-extra": "^1.1.0",
+ "react-transition-group": "^4.4.5",
+ "uncontrollable": "^7.2.1",
+ "warning": "^4.0.3"
+ },
+ "peerDependencies": {
+ "@types/react": ">=16.14.8",
+ "react": ">=16.14.0",
+ "react-dom": ">=16.14.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
"node_modules/react-dom": {
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
@@ -8734,6 +8904,31 @@
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
},
+ "node_modules/react-lifecycles-compat": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz",
+ "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==",
+ "license": "MIT"
+ },
+ "node_modules/react-modal": {
+ "version": "3.16.1",
+ "resolved": "https://registry.npmjs.org/react-modal/-/react-modal-3.16.1.tgz",
+ "integrity": "sha512-VStHgI3BVcGo7OXczvnJN7yT2TWHJPDXZWyI/a0ssFNhGZWsPmB8cF0z33ewDXq4VfYMO1vXgiv/g8Nj9NDyWg==",
+ "license": "MIT",
+ "dependencies": {
+ "exenv": "^1.2.0",
+ "prop-types": "^15.7.2",
+ "react-lifecycles-compat": "^3.0.0",
+ "warning": "^4.0.3"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "peerDependencies": {
+ "react": "^0.14.0 || ^15.0.0 || ^16 || ^17 || ^18",
+ "react-dom": "^0.14.0 || ^15.0.0 || ^16 || ^17 || ^18"
+ }
+ },
"node_modules/react-refresh": {
"version": "0.14.2",
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz",
@@ -9930,6 +10125,25 @@
"integrity": "sha512-ovssysQTa+luh7A5Weu3Rta6FJlFBBbInjOh722LIt6klpU2/HtdUbszju/G4devcvk8PGt7FCLv5wftu3THUA==",
"dev": true
},
+ "node_modules/swiper": {
+ "version": "11.1.14",
+ "resolved": "https://registry.npmjs.org/swiper/-/swiper-11.1.14.tgz",
+ "integrity": "sha512-VbQLQXC04io6AoAjIUWuZwW4MSYozkcP9KjLdrsG/00Q/yiwvhz9RQyt0nHXV10hi9NVnDNy1/wv7Dzq1lkOCQ==",
+ "funding": [
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/swiperjs"
+ },
+ {
+ "type": "open_collective",
+ "url": "http://opencollective.com/swiper"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4.7.0"
+ }
+ },
"node_modules/synckit": {
"version": "0.8.8",
"resolved": "https://registry.npmjs.org/synckit/-/synckit-0.8.8.tgz",
@@ -10203,8 +10417,7 @@
"node_modules/tslib": {
"version": "2.6.3",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz",
- "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==",
- "dev": true
+ "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ=="
},
"node_modules/tunnel-agent": {
"version": "0.6.0",
@@ -10358,6 +10571,21 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/uncontrollable": {
+ "version": "7.2.1",
+ "resolved": "https://registry.npmjs.org/uncontrollable/-/uncontrollable-7.2.1.tgz",
+ "integrity": "sha512-svtcfoTADIB0nT9nltgjujTi7BzVmwjZClOmskKu/E8FW9BXzg9os8OLr4f8Dlnk0rYWJIWr4wv9eKUXiQvQwQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.6.3",
+ "@types/react": ">=16.9.11",
+ "invariant": "^2.2.4",
+ "react-lifecycles-compat": "^3.0.4"
+ },
+ "peerDependencies": {
+ "react": ">=15.0.0"
+ }
+ },
"node_modules/undici-types": {
"version": "5.26.5",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
@@ -10542,6 +10770,15 @@
}
}
},
+ "node_modules/warning": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz",
+ "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==",
+ "license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.0.0"
+ }
+ },
"node_modules/webidl-conversions": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
diff --git a/package.json b/package.json
index ae251685c8..eb26248b7e 100644
--- a/package.json
+++ b/package.json
@@ -7,16 +7,20 @@
"license": "GPL-3.0",
"dependencies": {
"@fortawesome/fontawesome-free": "^6.5.2",
+ "bootstrap": "^5.3.3",
"bulma": "^1.0.1",
"classnames": "^2.5.1",
"react": "^18.3.1",
+ "react-bootstrap": "^2.10.5",
"react-dom": "^18.3.1",
+ "react-modal": "^3.16.1",
"react-router-dom": "^6.25.1",
- "react-transition-group": "^4.4.5"
+ "react-transition-group": "^4.4.5",
+ "swiper": "^11.1.14"
},
"devDependencies": {
"@cypress/react18": "^2.0.1",
- "@mate-academy/scripts": "^1.8.5",
+ "@mate-academy/scripts": "^1.9.12",
"@mate-academy/students-ts-config": "*",
"@mate-academy/stylelint-config": "*",
"@types/node": "^20.14.10",
@@ -57,7 +61,8 @@
"update": "mate-scripts update",
"postinstall": "npm run update && cypress verify",
"predeploy": "npm run build",
- "deploy": "mate-scripts deploy"
+ "deploy": "mate-scripts deploy",
+ "dev": "vite"
},
"browserslist": {
"production": [
diff --git a/public/img/banner-phones.png b/public/img/banners/image1.png
similarity index 100%
rename from public/img/banner-phones.png
rename to public/img/banners/image1.png
diff --git a/public/img/banner-tablets.png b/public/img/banners/image2.png
similarity index 100%
rename from public/img/banner-tablets.png
rename to public/img/banners/image2.png
diff --git a/public/img/banner-accessories.png b/public/img/banners/image3.png
similarity index 100%
rename from public/img/banner-accessories.png
rename to public/img/banners/image3.png
diff --git a/public/img/category-accessories.png b/public/img/category-accessories.png
deleted file mode 100644
index 67c5bfdb35..0000000000
Binary files a/public/img/category-accessories.png and /dev/null differ
diff --git a/public/img/category-phones.webp b/public/img/category-phones.webp
deleted file mode 100644
index 5ba1b294b1..0000000000
Binary files a/public/img/category-phones.webp and /dev/null differ
diff --git a/public/img/category-tablets.png b/public/img/category-tablets.png
deleted file mode 100644
index 57e33c5807..0000000000
Binary files a/public/img/category-tablets.png and /dev/null differ
diff --git a/public/img/shopByCategory/accessories.png b/public/img/shopByCategory/accessories.png
new file mode 100644
index 0000000000..1847eb7a89
Binary files /dev/null and b/public/img/shopByCategory/accessories.png differ
diff --git a/public/img/shopByCategory/phones.png b/public/img/shopByCategory/phones.png
new file mode 100644
index 0000000000..01cb631053
Binary files /dev/null and b/public/img/shopByCategory/phones.png differ
diff --git a/public/img/shopByCategory/tablets.png b/public/img/shopByCategory/tablets.png
new file mode 100644
index 0000000000..68084fd25a
Binary files /dev/null and b/public/img/shopByCategory/tablets.png differ
diff --git a/public/index.html b/public/index.html
new file mode 100644
index 0000000000..cffa35ba1d
--- /dev/null
+++ b/public/index.html
@@ -0,0 +1,11 @@
+
+
+
+
+
+ Phone catalog
+
+
+
+
+
diff --git a/src/App.module.scss b/src/App.module.scss
new file mode 100644
index 0000000000..ca3f0f607d
--- /dev/null
+++ b/src/App.module.scss
@@ -0,0 +1,94 @@
+@import './normilize.css';
+@import './utils/mixins';
+@import './utils/variables';
+
+@font-face {
+ font-family: Mont;
+ src: url(../public/fonts/Mont-Regular.otf);
+ font-weight: 400;
+}
+
+@font-face {
+ font-family: Mont;
+ src: url(../public/fonts/Mont-Bold.otf);
+ font-weight: 700;
+}
+
+@font-face {
+ font-family: Mont;
+ src: url(../public/fonts/Mont-SemiBold.otf);
+ font-weight: 600;
+}
+
+html {
+ scroll-behavior: smooth;
+ &:has(.app__menu:target) {
+ overflow: hidden;
+ }
+}
+
+body {
+ font-family: Mont, sans-serif;
+ font-size: 12px;
+}
+
+.app {
+ min-height: 100vh;
+ background-color: var(--color-page-background);
+
+ &__header {
+ position: sticky;
+ z-index: 2;
+ top: 0;
+ left: 0;
+ right: 0;
+ }
+
+ &__title {
+ display: none;
+ position: absolute !important;
+ width: 1px !important;
+ height: 1px !important;
+ margin: -1px !important;
+ border: 0 !important;
+ padding: 0 !important;
+ overflow: hidden !important;
+ clip: rect(0, 0, 0, 0) !important;
+ white-space: nowrap !important;
+ }
+
+ &__container {
+ max-width: 1136px;
+ box-sizing: content-box;
+ min-height: calc(100vh - $header-height-mobile - $footer-height-mobile);
+
+ margin: 0 auto;
+ padding-inline: 16px;
+
+ @include on-tablet {
+ min-height: calc(100vh - $header-height-tablet - $footer-height-tablet);
+ padding-inline: 24px;
+ }
+
+ @include on-desktop {
+ min-height: calc(100vh - $header-height-desktop - $footer-height-desktop);
+ padding-inline: 32px;
+ }
+ }
+
+ &__menu {
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ opacity: 0;
+ pointer-events: none;
+ transition: opacity 0.3s ease;
+ z-index: 2;
+
+ &--open {
+ opacity: 1;
+ pointer-events: all;
+ }
+ }
+}
diff --git a/src/App.scss b/src/App.scss
deleted file mode 100644
index 71bc413aad..0000000000
--- a/src/App.scss
+++ /dev/null
@@ -1 +0,0 @@
-// not empty
diff --git a/src/App.tsx b/src/App.tsx
index 372e4b4206..72a29becfb 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -1,7 +1,78 @@
-import './App.scss';
+import { Outlet } from 'react-router-dom';
-export const App = () => (
-
-
Product Catalog
-
-);
+import styles from './App.module.scss';
+
+import { Menu } from './components/Menu';
+import { Header } from './components/Header';
+import { Footer } from './components/Footer';
+import { CartProvider } from './context/CartContext';
+import React, { useEffect, useState } from 'react';
+import { FavoritesProvider } from './context/FavoritesContext';
+import { ThemeProvider, useTheme } from './context/ThemeContext';
+type Props = {
+ isMenuOpen: boolean;
+ openMenu: () => void;
+ closeMenu: () => void;
+};
+
+const AppContent: React.FC = ({ isMenuOpen, openMenu, closeMenu }) => {
+ const { isDarkTheme } = useTheme();
+
+ useEffect(() => {
+ if (isDarkTheme) {
+ document.body.classList.add('dark-theme');
+ } else {
+ document.body.classList.remove('dark-theme');
+ }
+ }, [isDarkTheme]);
+
+ return (
+
+
Product Catalog
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export const App = () => {
+ const [isMenuOpen, setIsMenuOpen] = useState(false);
+
+ const openMenu = () => {
+ setIsMenuOpen(true);
+ document.body.style.overflow = 'hidden';
+ };
+
+ const closeMenu = () => {
+ setIsMenuOpen(false);
+ document.body.style.overflow = '';
+ };
+
+ return (
+
+
+
+
+
+
+
+ );
+};
diff --git a/src/Root.tsx b/src/Root.tsx
new file mode 100644
index 0000000000..9f7b979f98
--- /dev/null
+++ b/src/Root.tsx
@@ -0,0 +1,29 @@
+import { Routes, Route, BrowserRouter } from 'react-router-dom';
+import { App } from './App';
+import { HomePage } from './pages/HomePage';
+import { Catalog } from './pages/Catalog';
+import { NotFoundPage } from './pages/NotFoundPage';
+import { ProductDetailsPage } from './pages/ProductDetailsPage';
+import { CartPage } from './pages/CartPage';
+import { Favorites } from './pages/Favorites';
+import { BASE_URL } from './utils/constants';
+
+export const Root = () => (
+
+
+ }>
+ } />
+
+ } />
+ } />
+
+
+ } />
+ } />
+
+
+ } />
+
+
+
+);
diff --git a/src/components/AboutProduct/AboutProduct.module.scss b/src/components/AboutProduct/AboutProduct.module.scss
new file mode 100644
index 0000000000..282300dc0a
--- /dev/null
+++ b/src/components/AboutProduct/AboutProduct.module.scss
@@ -0,0 +1,25 @@
+@import '../../utils/mixins';
+@import '../../utils/variables';
+
+.aboutProduct {
+ display: flex;
+ flex-direction: column;
+ gap: 32px;
+
+ &__title {
+ padding-bottom: 16px;
+ border-bottom: 1px solid var(--color-grey-second);
+ }
+
+ &__description {
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+ }
+
+ &__text {
+ @include body-text;
+
+ color: var(--color-grey);
+ }
+}
diff --git a/src/components/AboutProduct/AboutProduct.tsx b/src/components/AboutProduct/AboutProduct.tsx
new file mode 100644
index 0000000000..9157df2e42
--- /dev/null
+++ b/src/components/AboutProduct/AboutProduct.tsx
@@ -0,0 +1,28 @@
+import styles from './AboutProduct.module.scss';
+import { Title } from '../Title';
+import React from 'react';
+import { Product, ProductDescription } from '../../utils/types';
+
+type Props = {
+ product: Product;
+};
+
+export const AboutProduct: React.FC = ({ product }) => {
+ return (
+
+
+
About
+
+ {product.description.map((desc: ProductDescription, index) => {
+ return (
+
+
+
{desc.title}
+
+ {desc.text}
+
+ );
+ })}
+
+ );
+};
diff --git a/src/components/AboutProduct/index.js b/src/components/AboutProduct/index.js
new file mode 100644
index 0000000000..046edc0496
--- /dev/null
+++ b/src/components/AboutProduct/index.js
@@ -0,0 +1 @@
+export * from './AboutProduct';
diff --git a/src/components/AddToFavourites/AddToFavourites.module.scss b/src/components/AddToFavourites/AddToFavourites.module.scss
new file mode 100644
index 0000000000..aa92152060
--- /dev/null
+++ b/src/components/AddToFavourites/AddToFavourites.module.scss
@@ -0,0 +1,26 @@
+@import '../../utils/variables';
+
+.addToFavourites {
+ background-image: url(../../img/icons/heart.png);
+ background-size: 16px 16px;
+ background-repeat: no-repeat;
+ background-position: center;
+ background-color: var(--color-button);
+ border: 1px solid var(--color-button-border);
+ border-radius: 48px;
+
+ &--dark {
+ background-image: url(../../img/icons/night_theme_heart.png);
+ }
+
+ &--added {
+ background-image: url(../../img/icons/heart_yellow.png);
+ background-color: var(--color-button-disabled);
+ border: 1px solid var(--color-button-border-disabled);
+ }
+
+ &:hover {
+ background-color: var(--color-button-hover);
+ border: 1px solid var(--color-button-border-hover);
+ }
+}
diff --git a/src/components/AddToFavourites/AddToFavourites.tsx b/src/components/AddToFavourites/AddToFavourites.tsx
new file mode 100644
index 0000000000..b87f975495
--- /dev/null
+++ b/src/components/AddToFavourites/AddToFavourites.tsx
@@ -0,0 +1,47 @@
+import React, { useContext } from 'react';
+import styles from './AddToFavourites.module.scss';
+import classNames from 'classnames';
+import { Products } from '../../utils/types';
+import { FavoritesContext } from '../../context/FavoritesContext';
+import { useTheme } from '../../context/ThemeContext';
+
+type Props = {
+ size: 's' | 'm';
+ product: Products;
+};
+export const AddToFavourites: React.FC = ({ size, product }) => {
+ const { isDarkTheme } = useTheme();
+ const { favorites, setFavorites } = useContext(FavoritesContext);
+
+ const productExistinFavorites = favorites.find(
+ favItem => favItem.itemId === product.itemId,
+ );
+
+ const handleFavoritesAdd = () => {
+ setFavorites(prevFavorites => [...prevFavorites, product]);
+ };
+
+ const handleFavoritesRemove = () => {
+ setFavorites(previousFavorites =>
+ previousFavorites.filter(prevItem => prevItem.itemId !== product.itemId),
+ );
+ };
+
+ const elementSize =
+ size === 's'
+ ? { width: '40px', height: '40px' }
+ : { width: '48px', height: '48px' };
+
+ return (
+
+ );
+};
diff --git a/src/components/AddToFavourites/index.js b/src/components/AddToFavourites/index.js
new file mode 100644
index 0000000000..96d230a952
--- /dev/null
+++ b/src/components/AddToFavourites/index.js
@@ -0,0 +1 @@
+export * from './AddToFavourites';
diff --git a/src/components/ArrowButton/ArrowButton.module.scss b/src/components/ArrowButton/ArrowButton.module.scss
new file mode 100644
index 0000000000..9def218f4d
--- /dev/null
+++ b/src/components/ArrowButton/ArrowButton.module.scss
@@ -0,0 +1,83 @@
+@import '../../utils/mixins';
+@import '../../utils/variables';
+
+.arrowButton {
+ display: block;
+ width: 32px;
+ height: 32px;
+
+ border: 1px solid var(--color-button-border);
+ background-color: var(--color-button);
+ border-radius: 48px;
+
+ background-size: 16px 16px;
+ background-repeat: no-repeat;
+ background-position: center;
+
+ &:hover {
+ border-color: var(--color-button-border-hover);
+ background-color: var(--color-button-hover);
+ }
+
+ &--right {
+ background-image: url(../../img/icons/arrow_right_black.png);
+ }
+
+ &--right-dark {
+ background-image: url(../../img/icons/arrow_right_white.png);
+ }
+
+ &--left {
+ background-image: url(../../img/icons/arrow_left_black.png);
+ }
+
+ &--left-dark {
+ background-image: url(../../img/icons/arrow_left_white.png);
+ }
+
+ &--up {
+ background-image: url(../../img/icons/arrow_up_black.png);
+ }
+
+ &--up-dark {
+ background-image: url(../../img/icons/arrow_up_white.png);
+ }
+
+ &--down {
+ background-image: url(../../img/icons/arrow_down_grey.png);
+ }
+
+ &--wide {
+ height: 189px;
+
+ @include on-desktop {
+ height: 400px;
+ }
+ }
+
+ &--right.arrowButton--disabled,
+ &--right-dark.arrowButton--disabled {
+ background-color: var(--color-button-disabled);
+ border: 1px solid var(--color-button-border-disabled);
+ background-image: url(../../img/icons/arrow_right_grey.png);
+ }
+
+ &--left.arrowButton--disabled,
+ &--left-dark.arrowButton--disabled {
+ background-color: var(--color-button-disabled);
+ border: 1px solid var(--color-button-border-disabled);
+ background-image: url(../../img/icons/arrow_left_grey.png);
+ }
+
+ &--up.arrowButton--disabled {
+ background-color: var(--color-button-disabled);
+ border: 1px solid var(--color-button-border-disabled);
+ background-image: url(../../img/icons/arrow_up_grey.png);
+ }
+
+ &--down.arrowButton--disabled {
+ background-color: var(--color-button-disabled);
+ border: 1px solid var(--color-button-border-disabled);
+ background-image: url(../../img/icons/arrow_down_grey.png);
+ }
+}
diff --git a/src/components/ArrowButton/ArrowButton.tsx b/src/components/ArrowButton/ArrowButton.tsx
new file mode 100644
index 0000000000..a13edd229b
--- /dev/null
+++ b/src/components/ArrowButton/ArrowButton.tsx
@@ -0,0 +1,36 @@
+import React from 'react';
+import styles from './ArrowButton.module.scss';
+import classNames from 'classnames';
+import { ArrowType } from '../../utils/types';
+import { useTheme } from '../../context/ThemeContext';
+
+type Props = {
+ type: ArrowType;
+ size?: 'default' | 'wide';
+ disabled?: boolean;
+ onClick?: () => void;
+};
+
+export const ArrowButton: React.FC = ({
+ type,
+ size = 'default',
+ disabled = false,
+ onClick = () => {},
+}) => {
+ const { isDarkTheme } = useTheme();
+
+ const buttonClass = classNames(styles.arrowButton, {
+ [styles[`arrowButton--${type}`]]: !isDarkTheme,
+ [styles[`arrowButton--${type}-dark`]]: isDarkTheme,
+ [styles['arrowButton--wide']]: size === 'wide',
+ [styles['arrowButton--disabled']]: disabled,
+ });
+
+ return (
+
+ );
+};
diff --git a/src/components/ArrowButton/index.js b/src/components/ArrowButton/index.js
new file mode 100644
index 0000000000..ce91b1996e
--- /dev/null
+++ b/src/components/ArrowButton/index.js
@@ -0,0 +1 @@
+export * from './ArrowButton';
diff --git a/src/components/ArrowGrey/ArrowGrey.module.scss b/src/components/ArrowGrey/ArrowGrey.module.scss
new file mode 100644
index 0000000000..fc2434ef8b
--- /dev/null
+++ b/src/components/ArrowGrey/ArrowGrey.module.scss
@@ -0,0 +1,7 @@
+.arrow {
+ width: 16px;
+ height: 16px;
+ background-image: url(../../img/icons/arrow_right_grey.png);
+ background-repeat: no-repeat;
+ background-position: center;
+}
diff --git a/src/components/ArrowGrey/ArrowGrey.tsx b/src/components/ArrowGrey/ArrowGrey.tsx
new file mode 100644
index 0000000000..9654e32655
--- /dev/null
+++ b/src/components/ArrowGrey/ArrowGrey.tsx
@@ -0,0 +1,4 @@
+import styles from './ArrowGrey.module.scss';
+export const ArrowGrey = () => {
+ return ;
+};
diff --git a/src/components/ArrowGrey/index.js b/src/components/ArrowGrey/index.js
new file mode 100644
index 0000000000..274ab4fa31
--- /dev/null
+++ b/src/components/ArrowGrey/index.js
@@ -0,0 +1 @@
+export * from './ArrowGrey';
diff --git a/src/components/Breadcrumbs/Breadcrumbs.module.scss b/src/components/Breadcrumbs/Breadcrumbs.module.scss
new file mode 100644
index 0000000000..9562f9e23f
--- /dev/null
+++ b/src/components/Breadcrumbs/Breadcrumbs.module.scss
@@ -0,0 +1,33 @@
+@import '../../utils/mixins';
+@import '../../utils/variables';
+
+.breadcrumbs {
+ padding-top: 24px;
+ padding-bottom: 24px;
+ display: flex;
+ gap: 8px;
+
+ @include on-tablet {
+ padding-bottom: 40px;
+ }
+
+ &__prev-page {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ text-decoration: none;
+ font-weight: 600;
+ color: var(--color-black);
+ line-height: 15px;
+ text-transform: capitalize;
+ }
+
+ &__current-page {
+ line-height: 15px;
+ font-weight: 600;
+ color: var(--color-grey);
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+}
diff --git a/src/components/Breadcrumbs/Breadcrumbs.tsx b/src/components/Breadcrumbs/Breadcrumbs.tsx
new file mode 100644
index 0000000000..c9fafefde0
--- /dev/null
+++ b/src/components/Breadcrumbs/Breadcrumbs.tsx
@@ -0,0 +1,34 @@
+import { NavLink, useParams } from 'react-router-dom';
+import styles from './Breadcrumbs.module.scss';
+import React from 'react';
+import { useTheme } from '../../context/ThemeContext';
+import homeIcon from '../../img/icons/home.png';
+import homeIconDark from '../../img/icons/night_theme_home.png';
+import { ArrowGrey } from '../ArrowGrey';
+import { BASE_URL } from '../../utils/constants';
+
+type Props = {
+ name: string | undefined;
+};
+export const Breadcrumbs: React.FC = ({ name }) => {
+ const { category } = useParams();
+ const { isDarkTheme } = useTheme();
+
+ return (
+
+
+
+
+
+
+ {category}
+
+
+
{name}
+
+ );
+};
diff --git a/src/components/Breadcrumbs/index.js b/src/components/Breadcrumbs/index.js
new file mode 100644
index 0000000000..ce977548b1
--- /dev/null
+++ b/src/components/Breadcrumbs/index.js
@@ -0,0 +1 @@
+export * from './Breadcrumbs';
diff --git a/src/components/CartIsEmpty/CartIsEmpty.module.scss b/src/components/CartIsEmpty/CartIsEmpty.module.scss
new file mode 100644
index 0000000000..29eee9db52
--- /dev/null
+++ b/src/components/CartIsEmpty/CartIsEmpty.module.scss
@@ -0,0 +1,20 @@
+@import '../../utils/mixins';
+
+.cartIsEmpty {
+ &__container {
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+ gap: 20px;
+ }
+ &__image {
+ width: 100px;
+ height: 100px;
+
+ @include on-tablet {
+ width: 200px;
+ height: 200px;
+ }
+ }
+}
diff --git a/src/components/CartIsEmpty/CartIsEmpty.tsx b/src/components/CartIsEmpty/CartIsEmpty.tsx
new file mode 100644
index 0000000000..e60028af11
--- /dev/null
+++ b/src/components/CartIsEmpty/CartIsEmpty.tsx
@@ -0,0 +1,18 @@
+import { BASE_URL } from '../../utils/constants';
+import { Title } from '../Title';
+import styles from './CartIsEmpty.module.scss';
+
+export const CartIsEmpty = () => {
+ return (
+
+
+
Your cart is empty
+
+
+
+ );
+};
diff --git a/src/components/CartIsEmpty/index.js b/src/components/CartIsEmpty/index.js
new file mode 100644
index 0000000000..4955d89fda
--- /dev/null
+++ b/src/components/CartIsEmpty/index.js
@@ -0,0 +1 @@
+export * from './CartIsEmpty';
diff --git a/src/components/CartProduct/CartProduct.module.scss b/src/components/CartProduct/CartProduct.module.scss
new file mode 100644
index 0000000000..ecc3b2d9b2
--- /dev/null
+++ b/src/components/CartProduct/CartProduct.module.scss
@@ -0,0 +1,92 @@
+@import '../../utils/mixins';
+@import '../../utils/variables';
+
+.cartProduct {
+ width: 100%;
+ height: 160px;
+ padding: 16px;
+ border: 1px solid var(--color-grey-third);
+ background-color: var(--color-card-background);
+ border-radius: 16px;
+
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+
+ &:hover {
+ box-shadow: 0 2px 16px 0 #0000001a;
+ }
+
+ @include on-tablet {
+ padding: 24px;
+ flex-direction: row;
+ justify-content: space-between;
+ gap: 24px;
+ }
+
+ &__top {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+
+ @include on-tablet {
+ gap: 24px;
+ }
+ }
+
+ &__bottom {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+
+ @include on-tablet {
+ gap: 50px;
+ }
+ }
+
+ &__buttons {
+ display: flex;
+ gap: 14px;
+ align-items: center;
+ }
+
+ &__buttonClose {
+ background-color: var(--color-card-background);
+ border: none;
+ }
+
+ &__deleteIcon {
+ width: 16px;
+ height: 16px;
+ background-image: url(../../img/icons/close-grey.png);
+ background-repeat: no-repeat;
+ background-position: center;
+ }
+
+ &__image {
+ width: 80px;
+ height: 80px;
+ object-fit: contain;
+ }
+
+ &__name {
+ color: var(--color-black);
+ width: 128px;
+
+ @include body-text;
+
+ @include on-tablet {
+ width: 136px;
+ }
+
+ @include on-desktop {
+ width: 100%;
+ }
+ }
+
+ &__quantity {
+ color: var(--color-black);
+
+ @include body-text;
+ }
+}
diff --git a/src/components/CartProduct/CartProduct.tsx b/src/components/CartProduct/CartProduct.tsx
new file mode 100644
index 0000000000..cec379863e
--- /dev/null
+++ b/src/components/CartProduct/CartProduct.tsx
@@ -0,0 +1,90 @@
+import React, { useContext } from 'react';
+import styles from './CartProduct.module.scss';
+import { CartItem } from '../../utils/types';
+import { QuantityButton } from '../QuantityButton';
+import { Title } from '../Title';
+import { CartContext } from '../../context/CartContext';
+import { BASE_URL } from '../../utils/constants';
+
+type Props = {
+ cartProduct: CartItem;
+};
+export const CartProduct: React.FC = ({ cartProduct }) => {
+ const product = cartProduct.product;
+ const { setCart } = useContext(CartContext);
+
+ // #region functions
+
+ function handleProductDelete(id: number) {
+ setCart(prevCart => prevCart.filter(item => item.id !== id));
+ }
+
+ function handleQantityDecrease(id: number) {
+ setCart(prevCart =>
+ prevCart.map((cartItem: CartItem) => {
+ if (cartItem.id === id) {
+ return { ...cartItem, quantity: cartItem.quantity - 1 };
+ } else {
+ return cartItem;
+ }
+ }),
+ );
+ }
+
+ function handleQantityIncrease(id: number) {
+ setCart(prevCart =>
+ prevCart.map((cartItem: CartItem) => {
+ if (cartItem.id === id) {
+ return { ...cartItem, quantity: cartItem.quantity + 1 };
+ } else {
+ return cartItem;
+ }
+ }),
+ );
+ }
+
+ // #endregion
+
+ return (
+
+
+
+
+
+
{product.name}
+
+
+
+
{
+ handleQantityDecrease(cartProduct.id);
+ }}
+ />
+ {cartProduct.quantity}
+ {
+ handleQantityIncrease(cartProduct.id);
+ }}
+ />
+
+
+
{`$${product.price}`}
+
+
+
+ );
+};
diff --git a/src/components/CartProduct/index.js b/src/components/CartProduct/index.js
new file mode 100644
index 0000000000..2e149fe78e
--- /dev/null
+++ b/src/components/CartProduct/index.js
@@ -0,0 +1 @@
+export * from './CartProduct';
diff --git a/src/components/CheckoutButton/CheckoutButton.module.scss b/src/components/CheckoutButton/CheckoutButton.module.scss
new file mode 100644
index 0000000000..5a688a6083
--- /dev/null
+++ b/src/components/CheckoutButton/CheckoutButton.module.scss
@@ -0,0 +1,18 @@
+@import '../../utils/mixins';
+@import '../../utils/variables';
+
+.checkoutButton {
+ width: 100%;
+ height: 48px;
+ background-color: var(--color-accent);
+ border: 1px solid var(--color-accent-button-border);
+ border: none;
+ border-radius: 48px;
+
+ @include buttons-text;
+
+ &:hover {
+ background-color: var(--color-accent-button-hover);
+ box-shadow: 0 3px 13px 0 #17203166;
+ }
+}
diff --git a/src/components/CheckoutButton/CheckoutButton.tsx b/src/components/CheckoutButton/CheckoutButton.tsx
new file mode 100644
index 0000000000..ad6fe2cd62
--- /dev/null
+++ b/src/components/CheckoutButton/CheckoutButton.tsx
@@ -0,0 +1,49 @@
+import { useContext } from 'react';
+import styles from './CheckoutButton.module.scss';
+import { useState } from 'react';
+import Button from 'react-bootstrap/Button';
+import Modal from 'react-bootstrap/Modal';
+import { CartContext } from '../../context/CartContext';
+import './modal.scss';
+
+export const CheckoutButton = () => {
+ const { setCart } = useContext(CartContext);
+ const [show, setShow] = useState(false);
+
+ const handleClose = () => setShow(false);
+ const handleShow = () => setShow(true);
+ const handleClear = () => {
+ setCart([]);
+ handleClose();
+ };
+
+ return (
+ <>
+
+
+
+
+
+ Checkout is not implemented yet. Do you want to clear the Cart?
+
+
+
+
+
+
+
+ >
+ );
+};
diff --git a/src/components/CheckoutButton/index.js b/src/components/CheckoutButton/index.js
new file mode 100644
index 0000000000..1e42f0b9ae
--- /dev/null
+++ b/src/components/CheckoutButton/index.js
@@ -0,0 +1 @@
+export * from './CheckoutButton';
diff --git a/src/components/CheckoutButton/modal.scss b/src/components/CheckoutButton/modal.scss
new file mode 100644
index 0000000000..940e2b6aef
--- /dev/null
+++ b/src/components/CheckoutButton/modal.scss
@@ -0,0 +1,28 @@
+@import 'bootstrap/scss/functions';
+@import 'bootstrap/scss/variables';
+@import 'bootstrap/scss/mixins';
+@import 'bootstrap/scss/transitions';
+@import 'bootstrap/scss/bootstrap-utilities';
+@import 'bootstrap/scss/modal';
+@import 'bootstrap/scss/buttons';
+@import '../../utils/variables';
+
+.custom-modal {
+ .modal-content {
+ font-size: 18px;
+ }
+ .modal-footer {
+ .btn {
+ border-radius: 48px;
+ }
+ .btn-secondary {
+ background-color: var(--color-grey);
+ }
+ .btn-primary {
+ width: 84px;
+ height: 38px;
+ border: none;
+ background-color: var(--color-accent);
+ }
+ }
+}
diff --git a/src/components/DarkThemeButton/DarkThemeButton.module.scss b/src/components/DarkThemeButton/DarkThemeButton.module.scss
new file mode 100644
index 0000000000..2f08ced706
--- /dev/null
+++ b/src/components/DarkThemeButton/DarkThemeButton.module.scss
@@ -0,0 +1,31 @@
+@import '../../utils/mixins';
+@import '../../utils/variables';
+
+.darkThemeButton {
+ width: 48px;
+ height: 48px;
+ box-shadow: -1px 0 0 0 var(--color-grey-third);
+ border-bottom: 1px solid var(--color-grey-third);
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+
+ @include on-desktop {
+ width: 64px;
+ height: 64px;
+ }
+
+ &__btn {
+ width: 20px;
+ height: 20px;
+ outline: none;
+ border: none;
+
+ background-color: transparent;
+ background-image: url(../../img/icons/night_theme_btn.png);
+ background-size: cover;
+ background-repeat: no-repeat;
+ background-position: center;
+ }
+}
diff --git a/src/components/DarkThemeButton/DarkThemeButton.tsx b/src/components/DarkThemeButton/DarkThemeButton.tsx
new file mode 100644
index 0000000000..a401bb641c
--- /dev/null
+++ b/src/components/DarkThemeButton/DarkThemeButton.tsx
@@ -0,0 +1,15 @@
+import { useTheme } from '../../context/ThemeContext';
+import styles from './DarkThemeButton.module.scss';
+
+export const DarkThemeButton = () => {
+ const { isDarkTheme, toggleTheme } = useTheme();
+
+ return (
+
+
+ {isDarkTheme ? 'OFF' : 'ON'}
+
+
+
+ );
+};
diff --git a/src/components/DarkThemeButton/index.js b/src/components/DarkThemeButton/index.js
new file mode 100644
index 0000000000..80839bb086
--- /dev/null
+++ b/src/components/DarkThemeButton/index.js
@@ -0,0 +1 @@
+export * from './DarkThemeButton';
diff --git a/src/components/Footer/Footer.module.scss b/src/components/Footer/Footer.module.scss
new file mode 100644
index 0000000000..15321b34f3
--- /dev/null
+++ b/src/components/Footer/Footer.module.scss
@@ -0,0 +1,67 @@
+@import '../../utils/mixins';
+@import '../../utils/variables';
+
+.footer {
+ height: $footer-height-mobile;
+ display: flex;
+ flex-direction: column;
+ justify-content: space-between;
+ padding: 32px 16px;
+ border-top: 1px solid var(--color-grey-third);
+ background-color: var(--color-header-footer);
+
+ @include on-tablet {
+ height: $footer-height-tablet;
+ padding: 32px;
+ flex-direction: row;
+ align-items: center;
+ }
+
+ &__logo {
+ display: block;
+ width: 89px;
+ height: 32px;
+ }
+
+ &__contacts {
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+
+ @include on-tablet {
+ flex-direction: row;
+ gap: 14px;
+ }
+
+ @include on-desktop {
+ gap: 106px;
+ }
+ }
+
+ &__contact {
+ display: block;
+ color: var(--color-footer-links);
+ text-decoration: none;
+ text-transform: uppercase;
+ font-weight: 800;
+ line-height: 11px;
+ letter-spacing: 4%;
+
+ &:hover {
+ color: var(--color-grey);
+ }
+ }
+
+ &__back {
+ align-self: center;
+ display: flex;
+ align-items: center;
+ gap: 16px;
+ }
+
+ &__back-title {
+ color: var(--color-grey);
+
+ @include small-text;
+ }
+}
diff --git a/src/components/Footer/Footer.tsx b/src/components/Footer/Footer.tsx
new file mode 100644
index 0000000000..e755386d1d
--- /dev/null
+++ b/src/components/Footer/Footer.tsx
@@ -0,0 +1,52 @@
+import { Link } from 'react-router-dom';
+import logo from '../../img/logo.png';
+import logoDarkTheme from '../../img/night_theme_logo.png';
+import styles from './Footer.module.scss';
+import { ArrowButton } from '../ArrowButton';
+import { ArrowType } from '../../utils/types';
+import { useTheme } from '../../context/ThemeContext';
+
+export const Footer = () => {
+ const { isDarkTheme } = useTheme();
+
+ return (
+
+ );
+};
diff --git a/src/components/Footer/index.js b/src/components/Footer/index.js
new file mode 100644
index 0000000000..ddcc5a9cd1
--- /dev/null
+++ b/src/components/Footer/index.js
@@ -0,0 +1 @@
+export * from './Footer';
diff --git a/src/components/GoBack/GoBack.module.scss b/src/components/GoBack/GoBack.module.scss
new file mode 100644
index 0000000000..d0ab7c8778
--- /dev/null
+++ b/src/components/GoBack/GoBack.module.scss
@@ -0,0 +1,15 @@
+@import '../../utils/mixins';
+@import '../../utils/variables';
+
+.goBack {
+ @include small-text;
+
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ color: var(--color-grey);
+
+ &:hover {
+ color: var(--color-black);
+ }
+}
diff --git a/src/components/GoBack/GoBack.tsx b/src/components/GoBack/GoBack.tsx
new file mode 100644
index 0000000000..0af209567b
--- /dev/null
+++ b/src/components/GoBack/GoBack.tsx
@@ -0,0 +1,25 @@
+import { useNavigate } from 'react-router-dom';
+import styles from './GoBack.module.scss';
+import React from 'react';
+import { useTheme } from '../../context/ThemeContext';
+
+type Props = {
+ children: React.ReactNode;
+};
+export const GoBack: React.FC = ({ children }) => {
+ const navigate = useNavigate();
+ const { isDarkTheme } = useTheme();
+
+ return (
+ navigate(-1)}
+ style={{
+ cursor: 'pointer',
+ color: isDarkTheme ? '#F1F2F9' : '#89939A',
+ }}
+ >
+ {children}
+
+ );
+};
diff --git a/src/components/GoBack/index.js b/src/components/GoBack/index.js
new file mode 100644
index 0000000000..7a534e1e6b
--- /dev/null
+++ b/src/components/GoBack/index.js
@@ -0,0 +1 @@
+export * from './GoBack';
diff --git a/src/components/Header/Header.module.scss b/src/components/Header/Header.module.scss
new file mode 100644
index 0000000000..4cac5124ef
--- /dev/null
+++ b/src/components/Header/Header.module.scss
@@ -0,0 +1,109 @@
+@import '../../utils/mixins';
+@import '../../utils/variables';
+
+.header {
+ height: $header-height-mobile;
+ display: none;
+ background-color: var(--color-header-footer);
+ border-bottom: 1px solid var(--color-grey-third);
+
+ @include on-tablet {
+ display: flex;
+ justify-content: space-between;
+ }
+
+ @include on-desktop {
+ height: $header-height-desktop;
+ }
+
+ &--mobile {
+ display: flex;
+ justify-content: space-between;
+
+ @include on-tablet {
+ display: none;
+ }
+ }
+
+ &__menuButton {
+ display: block;
+ width: 48px;
+ height: 48px;
+ background-size: 16px 16px;
+ background-repeat: no-repeat;
+ background-position: center;
+ box-shadow: -1px 0 0 0 var(--color-grey-third);
+ border: none;
+ border-bottom: 1px solid var(--color-grey-third);
+ background-color: #fff;
+ background-image: url(../../img/icons/menu.png);
+
+ &--dark {
+ background-color: #0f1121;
+ background-image: url(../../img/icons/menu-white.png);
+ }
+
+ @include on-desktop {
+ width: 64px;
+ height: 64px;
+ }
+ }
+
+ &__left {
+ display: flex;
+ gap: 16px;
+
+ @include on-tablet {
+ gap: 24px;
+ }
+ }
+
+ &__right {
+ display: flex;
+ }
+
+ &__logo {
+ display: flex;
+ margin: 13px 16px;
+ width: 64px;
+ height: 22px;
+
+ @include on-desktop {
+ margin: 18px 24px;
+ width: 80px;
+ height: 28px;
+ }
+ }
+
+ &__nav {
+ height: 100%;
+ display: flex;
+ align-items: center;
+
+ &_list {
+ height: 100%;
+ display: flex;
+ align-items: center;
+ list-style: none;
+ gap: 32px;
+ }
+
+ &_link {
+ @include nav-link-styles;
+
+ display: flex;
+ align-items: center;
+ height: 100%;
+
+ &:hover {
+ color: var(--color-black);
+ }
+ }
+
+ &_item {
+ height: 100%;
+ display: flex;
+ align-items: center;
+ }
+ }
+}
diff --git a/src/components/Header/Header.tsx b/src/components/Header/Header.tsx
new file mode 100644
index 0000000000..eefbbde5ae
--- /dev/null
+++ b/src/components/Header/Header.tsx
@@ -0,0 +1,130 @@
+import { NavLink, Link } from 'react-router-dom';
+import { IconType } from '../../utils/types';
+import { HeaderIcon } from '../HeaderIcon';
+
+import logo from '../../img/logo.png';
+import logoDarkTheme from '../../img/night_theme_logo.png';
+import styles from './Header.module.scss';
+import React, { useContext } from 'react';
+import { CartContext } from '../../context/CartContext';
+import { FavoritesContext } from '../../context/FavoritesContext';
+import { useTheme } from '../../context/ThemeContext';
+import { DarkThemeButton } from '../DarkThemeButton';
+import classNames from 'classnames';
+import { BASE_URL } from '../../utils/constants';
+
+type Props = {
+ onMenuClick: () => void;
+ closeMenu: () => void;
+};
+
+export const Header: React.FC = ({ onMenuClick, closeMenu }) => {
+ const { cart } = useContext(CartContext);
+ const { favorites } = useContext(FavoritesContext);
+ const { isDarkTheme } = useTheme();
+ const cartQuantity = cart ? cart.length : null;
+ const favoritesQuantity = favorites ? favorites.length : null;
+ const getLinkStyle = ({ isActive }: { isActive: boolean }) => {
+ let color;
+ let borderBottom;
+
+ if (isActive && isDarkTheme) {
+ color = '#F1F2F9';
+ borderBottom = '3px solid #F1F2F9';
+ } else if (isActive && !isDarkTheme) {
+ color = '#0F0F11';
+ borderBottom = '3px solid #0F0F11';
+ } else {
+ color = '';
+ borderBottom = '';
+ }
+
+ return {
+ color,
+ borderBottom,
+ };
+ };
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+ );
+};
diff --git a/src/components/Header/index.js b/src/components/Header/index.js
new file mode 100644
index 0000000000..266dec8a1b
--- /dev/null
+++ b/src/components/Header/index.js
@@ -0,0 +1 @@
+export * from './Header';
diff --git a/src/components/HeaderIcon/HeaderIcon.module.scss b/src/components/HeaderIcon/HeaderIcon.module.scss
new file mode 100644
index 0000000000..68570b1c7c
--- /dev/null
+++ b/src/components/HeaderIcon/HeaderIcon.module.scss
@@ -0,0 +1,54 @@
+@import '../../utils/mixins';
+@import '../../utils/variables';
+
+.icon {
+ position: relative;
+ width: 48px;
+ height: 48px;
+
+ @include on-desktop {
+ width: 64px;
+ height: 64px;
+ }
+
+ &--wide {
+ width: 50%;
+ height: 64px;
+
+ &:hover {
+ border-bottom: 2px solid var(--color-black);
+ }
+ }
+
+ &__image {
+ display: block;
+ width: 100%;
+ height: 100%;
+ background-size: 16px 16px;
+ background-repeat: no-repeat;
+ background-position: center;
+ box-shadow: -1px 0 0 0 var(--color-grey-third);
+ }
+
+ &__image--favourites {
+ background-image: url(../../img/icons/favourites.png);
+ }
+
+ &__image--favourites-dark {
+ background-image: url(../../img/icons/night_theme_heart.png);
+ }
+
+ &__image--cart {
+ background-image: url(../../img/icons/cart.png);
+ }
+
+ &__image--cart-dark {
+ background-image: url(../../img/icons/night_theme_cart.png);
+ }
+
+ &__quantity {
+ position: absolute;
+ left: 50%;
+ bottom: 50%;
+ }
+}
diff --git a/src/components/HeaderIcon/HeaderIcon.tsx b/src/components/HeaderIcon/HeaderIcon.tsx
new file mode 100644
index 0000000000..dee172338d
--- /dev/null
+++ b/src/components/HeaderIcon/HeaderIcon.tsx
@@ -0,0 +1,44 @@
+import { NavLink } from 'react-router-dom';
+import styles from './HeaderIcon.module.scss';
+import { QuantityIndicator } from '../QuantityIndicator';
+import { useTheme } from '../../context/ThemeContext';
+import { BASE_URL } from '../../utils/constants';
+
+type Props = {
+ type: string;
+ href: string;
+ size?: 'default' | 'wide';
+ onClick: () => void;
+ number: number | null;
+};
+export const HeaderIcon: React.FC = ({
+ type,
+ size = 'default',
+ href,
+ number,
+ onClick,
+}) => {
+ const { isDarkTheme } = useTheme();
+ const sizeClass = size === 'wide' ? styles['icon--wide'] : '';
+ const getLinkStyle = ({ isActive }: { isActive: boolean }) => {
+ return {
+ borderBottom: isActive ? '3px solid var(--color-black)' : '',
+ };
+ };
+
+ return (
+
+
+ {number !== null && (
+
+
+
+ )}
+
+ );
+};
diff --git a/src/components/HeaderIcon/index.js b/src/components/HeaderIcon/index.js
new file mode 100644
index 0000000000..3aac0d9d1f
--- /dev/null
+++ b/src/components/HeaderIcon/index.js
@@ -0,0 +1 @@
+export * from './HeaderIcon';
diff --git a/src/components/Loader/Loader.module.scss b/src/components/Loader/Loader.module.scss
new file mode 100644
index 0000000000..36624981d2
--- /dev/null
+++ b/src/components/Loader/Loader.module.scss
@@ -0,0 +1,25 @@
+.loader {
+ display: flex;
+ width: 100%;
+ justify-content: center;
+ align-items: center;
+
+ &__content {
+ border-radius: 50%;
+ width: 2em;
+ height: 2em;
+ margin: 1em auto;
+ border: 0.3em solid #ddd;
+ border-left-color: #000;
+ animation: load8 1.2s infinite linear;
+ }
+}
+
+@keyframes load8 {
+ 0% {
+ transform: rotate(0deg);
+ }
+ 100% {
+ transform: rotate(360deg);
+ }
+}
diff --git a/src/components/Loader/Loader.tsx b/src/components/Loader/Loader.tsx
new file mode 100644
index 0000000000..887221a135
--- /dev/null
+++ b/src/components/Loader/Loader.tsx
@@ -0,0 +1,7 @@
+import styles from './Loader.module.scss';
+
+export const Loader = () => (
+
+);
diff --git a/src/components/Loader/index.js b/src/components/Loader/index.js
new file mode 100644
index 0000000000..d5ce981151
--- /dev/null
+++ b/src/components/Loader/index.js
@@ -0,0 +1 @@
+export * from './Loader';
diff --git a/src/components/Menu/Menu.module.scss b/src/components/Menu/Menu.module.scss
new file mode 100644
index 0000000000..1507f96ded
--- /dev/null
+++ b/src/components/Menu/Menu.module.scss
@@ -0,0 +1,84 @@
+@import '../../utils/variables';
+@import '../../utils/mixins';
+
+.menu {
+ height: 100vh;
+ display: flex;
+ flex-direction: column;
+ justify-content: space-between;
+ background-color: var(--color-page-background);
+
+ @include on-tablet {
+ display: none;
+ }
+
+ &__closeButton {
+ display: block;
+ width: 48px;
+ height: 48px;
+ background-size: 16px 16px;
+ background-repeat: no-repeat;
+ background-position: center;
+ box-shadow: -1px 0 0 0 var(--color-grey-third);
+ border: none;
+ background-color: var(--color-page-background);
+ background-image: url(../../img/icons/close.png);
+
+ &--dark {
+ background-image: url(../../img/icons/close-white.png);
+ }
+
+ @include on-desktop {
+ width: 64px;
+ height: 64px;
+ }
+ }
+
+ &__top-bar {
+ display: flex;
+ margin-bottom: 24px;
+ justify-content: space-between;
+ box-shadow: 0 1px 0 0 var(--color-grey-third);
+ background-color: var(--color-page-background);
+ }
+
+ &__bottom {
+ display: flex;
+ border: 1px solid var(--color-grey-second);
+ }
+
+ &__logo {
+ display: flex;
+ margin: 13px 16px;
+ width: 64px;
+ height: 22px;
+ }
+
+ &__nav_list {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 16px;
+ list-style: none;
+ }
+
+ &__nav_link {
+ @include nav-link-styles;
+
+ height: 100%;
+ display: flex;
+ align-items: center;
+
+ &:hover {
+ color: var(--color-black);
+ }
+ }
+
+ &__nav_item {
+ height: 27px;
+
+ &:hover {
+ border-bottom: 2px solid var(--color-black);
+ }
+ }
+}
diff --git a/src/components/Menu/Menu.tsx b/src/components/Menu/Menu.tsx
new file mode 100644
index 0000000000..e74bea0cf4
--- /dev/null
+++ b/src/components/Menu/Menu.tsx
@@ -0,0 +1,95 @@
+import React, { useContext } from 'react';
+import { IconType } from '../../utils/types';
+import { HeaderIcon } from '../HeaderIcon';
+import styles from './Menu.module.scss';
+import { Link } from 'react-router-dom';
+import { CartContext } from '../../context/CartContext';
+import { FavoritesContext } from '../../context/FavoritesContext';
+import { useTheme } from '../../context/ThemeContext';
+import logo from '../../img/logo.png';
+import logoDarkTheme from '../../img/night_theme_logo.png';
+import { BASE_URL } from '../../utils/constants';
+
+type Props = {
+ closeMenu: () => void;
+};
+
+export const Menu: React.FC = ({ closeMenu }) => {
+ const { cart } = useContext(CartContext);
+ const { favorites } = useContext(FavoritesContext);
+ const { isDarkTheme } = useTheme();
+ const cartQuantity = cart ? cart.length : null;
+ const favoritesQuantity = favorites ? favorites.length : null;
+
+ return (
+
+ );
+};
diff --git a/src/components/Menu/index.js b/src/components/Menu/index.js
new file mode 100644
index 0000000000..629d3d0aa1
--- /dev/null
+++ b/src/components/Menu/index.js
@@ -0,0 +1 @@
+export * from './Menu';
diff --git a/src/components/Pagination/Pagination.module.scss b/src/components/Pagination/Pagination.module.scss
new file mode 100644
index 0000000000..8cc345dfc7
--- /dev/null
+++ b/src/components/Pagination/Pagination.module.scss
@@ -0,0 +1,52 @@
+@import '../../utils/mixins';
+@import '../../utils/variables';
+
+.pagination {
+ display: flex;
+ justify-content: center;
+ padding-bottom: 64px;
+
+ @include on-desktop {
+ padding-bottom: 80px;
+ }
+
+ &__buttons {
+ // max-width: 248px;
+ display: flex;
+ gap: 16px;
+ }
+
+ &__numbers {
+ display: flex;
+ gap: 8px;
+ flex-wrap: wrap;
+ }
+
+ &__button {
+ display: block;
+ width: 32px;
+ height: 32px;
+ border: 1px solid var(--color-pagination-border);
+ background-color: var(--color-pagination-background);
+ color: var(--color-black);
+ border-radius: 48px;
+ font-family: inherit;
+ font-size: 14px;
+ line-height: 21px;
+
+ &.pagination__button--active {
+ background-color: var(--color-pagination-background-selected);
+ border-color: var(--color-pagination-border-selected);
+ color: #fff;
+
+ &:hover {
+ background-color: var(--color-pagination-background-selected);
+ }
+ }
+
+ &:hover {
+ background-color: var(--color-pagination-background-hover);
+ border-color: var(--color-pagination-border-hover);
+ }
+ }
+}
diff --git a/src/components/Pagination/Pagination.tsx b/src/components/Pagination/Pagination.tsx
new file mode 100644
index 0000000000..71bf0953d4
--- /dev/null
+++ b/src/components/Pagination/Pagination.tsx
@@ -0,0 +1,155 @@
+import React from 'react';
+import styles from './Pagination.module.scss';
+import { ArrowButton } from '../ArrowButton';
+import { ArrowType } from '../../utils/types';
+import classNames from 'classnames';
+
+type Props = {
+ pagesAmount: number;
+ searchParams: URLSearchParams;
+ setSearchParams: (value: URLSearchParams) => void;
+ activePage: string;
+};
+
+export const Pagination: React.FC = ({
+ pagesAmount,
+ searchParams,
+ setSearchParams,
+ activePage,
+}) => {
+ const pagesAmountArray = Array.from(
+ { length: pagesAmount },
+ (_, index) => index + 1,
+ );
+ // #region functions
+ const handlePageSelect = (number: number) => {
+ const params = new URLSearchParams(searchParams);
+
+ params.set('page', String(number));
+ setSearchParams(params);
+ };
+
+ const handleNextClick = () => {
+ const params = new URLSearchParams(searchParams);
+
+ params.set('page', String(+activePage + 1));
+ setSearchParams(params);
+ };
+
+ const handlePrevClick = () => {
+ const params = new URLSearchParams(searchParams);
+
+ params.set('page', String(+activePage - 1));
+ setSearchParams(params);
+ };
+
+ const pagesAmountSlicedArray = (
+ pageIsActive: number,
+ pagesToSlice: number,
+ ) => {
+ const activeIndex = pagesAmountArray.indexOf(pageIsActive);
+ const sliced = pagesAmountArray.slice(
+ activeIndex,
+ activeIndex + pagesToSlice + 1,
+ );
+
+ return sliced;
+ };
+ // #endregion
+
+ // #region vars
+
+ const slicedArray = pagesAmountSlicedArray(+activePage, 2);
+
+ // #endregion
+
+ return (
+
+
+
+
+ {
+ pagesAmount <= 5 ? (
+ pagesAmountArray.map((pageNumber: number) => (
+
+ ))
+ ) : (
+ /* eslint-disable @typescript-eslint/indent */
+ <>
+ {slicedArray[slicedArray.length - 1] === pagesAmount ||
+ slicedArray[slicedArray.length - 1] + 1 === pagesAmount ? (
+ pagesAmountArray.slice(-5).map((pageNumber: number) => (
+
+ ))
+ ) : (
+ <>
+ {slicedArray.map((pageNumber: number) => (
+
+ ))}
+
+
+
+
+ >
+ )}
+ >
+ )
+ /* eslint-enable @typescript-eslint/indent */
+ }
+
+
+
+
+
+ );
+};
diff --git a/src/components/Pagination/index.js b/src/components/Pagination/index.js
new file mode 100644
index 0000000000..e016c96b72
--- /dev/null
+++ b/src/components/Pagination/index.js
@@ -0,0 +1 @@
+export * from './Pagination';
diff --git a/src/components/ProductCard/ProductCard.module.scss b/src/components/ProductCard/ProductCard.module.scss
new file mode 100644
index 0000000000..d284030b46
--- /dev/null
+++ b/src/components/ProductCard/ProductCard.module.scss
@@ -0,0 +1,113 @@
+@import '../../utils/variables';
+@import '../../utils/mixins';
+
+.productCard {
+ height: 440px;
+ padding: 32px;
+ display: flex;
+ gap: 8px;
+ flex-direction: column;
+ background-color: var(--color-card-background);
+ border: 1px solid var(--color-card-border);
+ border-radius: 8px;
+
+ &:hover {
+ cursor: pointer;
+ box-shadow: 0 2px 16px 0 #0000001a;
+ }
+
+ @include on-tablet {
+ height: 506px;
+ }
+
+ &__imagewrapper {
+ display: flex;
+ width: 100%;
+ min-height: 109px;
+
+ @include on-tablet {
+ min-height: 181px;
+ }
+
+ @include on-desktop {
+ height: 196px;
+ }
+ }
+
+ &__image {
+ display: block;
+ width: 100%;
+ height: 100%;
+ object-fit: contain;
+ transition: transform 0.3s ease-in-out;
+
+ &:hover {
+ transform: scale(1.1);
+ }
+ }
+
+ &__title {
+ @include body-text;
+
+ padding-top: 16px;
+ text-decoration: none;
+ color: var(--color-black);
+ }
+
+ &__prices {
+ display: flex;
+ gap: 8px;
+ }
+
+ &__fullprice {
+ font-size: 22px;
+ font-weight: 800;
+ line-height: 30px;
+ color: var(--color-black);
+ }
+
+ &__price {
+ font-size: 22px;
+ font-weight: 600;
+ line-height: 29px;
+ color: var(--color-grey);
+ text-decoration: line-through;
+ text-decoration-color: var(--color-grey);
+ position: relative;
+ }
+
+ &__line {
+ height: 1px;
+ width: 100%;
+ border-bottom: 1px solid var(--color-grey-second);
+ }
+
+ &__features-wrapper {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ padding-block: 8px;
+ }
+
+ &__features {
+ display: flex;
+ justify-content: space-between;
+ }
+
+ &__feature {
+ @include small-text;
+
+ color: var(--color-grey);
+ }
+
+ &__feature-value {
+ font-weight: 700;
+ line-height: 15.34px;
+ color: var(--color-black);
+ }
+
+ &__buttons {
+ display: flex;
+ gap: 8px;
+ }
+}
diff --git a/src/components/ProductCard/ProductCard.tsx b/src/components/ProductCard/ProductCard.tsx
new file mode 100644
index 0000000000..dc591e4105
--- /dev/null
+++ b/src/components/ProductCard/ProductCard.tsx
@@ -0,0 +1,60 @@
+import styles from './ProductCard.module.scss';
+import React from 'react';
+import { Products } from '../../utils/types';
+import { ToBuyButton } from '../ToBuyButton';
+import { AddToFavourites } from '../AddToFavourites';
+import { Link } from 'react-router-dom';
+import { BASE_URL } from '../../utils/constants';
+
+type Props = {
+ product: Products;
+ width?: number;
+};
+
+export const ProductCard: React.FC = ({ product, width }) => {
+ const { image, name, fullPrice, price, screen, capacity, ram } = product;
+
+ return (
+
+
+
+
+
+ {name}
+
+
+
{`$${fullPrice}`}
+
{price}
+
+
+
+
+
+
Capacity
+
{capacity}
+
+
+
+
+
+ );
+};
diff --git a/src/components/ProductCard/index.js b/src/components/ProductCard/index.js
new file mode 100644
index 0000000000..7ce031c382
--- /dev/null
+++ b/src/components/ProductCard/index.js
@@ -0,0 +1 @@
+export * from './ProductCard';
diff --git a/src/components/ProductSlider/ProductSlider.module.scss b/src/components/ProductSlider/ProductSlider.module.scss
new file mode 100644
index 0000000000..e234ba4bd4
--- /dev/null
+++ b/src/components/ProductSlider/ProductSlider.module.scss
@@ -0,0 +1,29 @@
+@import '../../utils/mixins';
+
+.productSlider {
+ &__top {
+ display: flex;
+ justify-content: space-between;
+ gap: 70px;
+ padding-bottom: 24px;
+
+ @include on-tablet {
+ gap: 0;
+ }
+ }
+
+ &__buttons {
+ display: flex;
+ gap: 16px;
+ }
+
+ &__outerwrapper {
+ overflow: hidden;
+ }
+
+ &__innerwrapper {
+ display: flex;
+ gap: 16px;
+ transition: transform 0.5s;
+ }
+}
diff --git a/src/components/ProductSlider/ProductSlider.tsx b/src/components/ProductSlider/ProductSlider.tsx
new file mode 100644
index 0000000000..f414d4cd86
--- /dev/null
+++ b/src/components/ProductSlider/ProductSlider.tsx
@@ -0,0 +1,90 @@
+import React, { useEffect, useState } from 'react';
+import { ArrowType, Products, SortType } from '../../utils/types';
+import { fetchProducts } from '../../utils/fetch';
+import { ProductCard } from '../ProductCard';
+import styles from './ProductSlider.module.scss';
+import { Title } from '../Title';
+import { ArrowButton } from '../ArrowButton';
+
+type Props = {
+ title: string;
+ category: string;
+ sortBy: SortType;
+};
+
+export const ProductSlider: React.FC = ({ title, category, sortBy }) => {
+ const [cardWidth, setCardWidth] = useState(0);
+ const [displayedProducts, setDisplayedProducts] = useState([]);
+ const [leftImageIndex, setLeftImageIndex] = useState(0);
+
+ const SLIDER_GAP = 16;
+ const widthToSlide =
+ leftImageIndex === 0 ? 0 : leftImageIndex * (cardWidth + SLIDER_GAP);
+
+ useEffect(() => {
+ const handleResize = () => {
+ if (window.innerWidth >= 320 && window.innerWidth < 640) {
+ setCardWidth(212);
+ } else if (window.innerWidth >= 640 && window.innerWidth < 1200) {
+ setCardWidth(237);
+ } else if (window.innerWidth >= 1200) {
+ setCardWidth(272);
+ }
+ };
+
+ handleResize();
+ window.addEventListener('resize', handleResize);
+
+ return () => {
+ window.removeEventListener('resize', handleResize);
+ };
+ }, []);
+
+ useEffect(() => {
+ fetchProducts(category, sortBy).then(result => {
+ setDisplayedProducts(result);
+ });
+ }, [category, sortBy]);
+
+ const handleNextClick = () => {
+ setLeftImageIndex(prevIndex => prevIndex + 1);
+ };
+
+ const handlePrevClick = () => {
+ setLeftImageIndex(prevIndex => prevIndex - 1);
+ };
+
+ return (
+
+
+
{title}
+
+
+
= displayedProducts.length - 1}
+ />
+
+
+
+
+ {displayedProducts?.map(displayedProduct => (
+
+ ))}
+
+
+
+ );
+};
diff --git a/src/components/ProductSlider/index.js b/src/components/ProductSlider/index.js
new file mode 100644
index 0000000000..5349238270
--- /dev/null
+++ b/src/components/ProductSlider/index.js
@@ -0,0 +1 @@
+export * from './ProductSlider';
diff --git a/src/components/ProductsList/ProductsList.module.scss b/src/components/ProductsList/ProductsList.module.scss
new file mode 100644
index 0000000000..1b6af699f3
--- /dev/null
+++ b/src/components/ProductsList/ProductsList.module.scss
@@ -0,0 +1,33 @@
+@import '../../utils/mixins';
+
+.products {
+ &__list {
+ display: flex;
+ flex-direction: column;
+ gap: 40px;
+
+ @include on-tablet {
+ display: grid;
+ grid-template-columns: repeat(12, 1fr);
+ gap: 40px 16px;
+ }
+
+ @include on-desktop {
+ grid-template-columns: repeat(24, 1fr);
+ }
+ }
+
+ &__item {
+ @include on-tablet {
+ grid-column: span 6;
+ }
+
+ @include on-media-screen {
+ grid-column: span 4;
+ }
+
+ @include on-desktop {
+ grid-column: span 6;
+ }
+ }
+}
diff --git a/src/components/ProductsList/ProductsList.tsx b/src/components/ProductsList/ProductsList.tsx
new file mode 100644
index 0000000000..3e07f9183a
--- /dev/null
+++ b/src/components/ProductsList/ProductsList.tsx
@@ -0,0 +1,21 @@
+import React from 'react';
+import { ProductCard } from '../ProductCard';
+import { Products } from '../../utils/types';
+import styles from './ProductsList.module.scss';
+
+type Props = {
+ products: Products[];
+};
+export const ProductsList: React.FC = ({ products }) => {
+ return (
+
+ {products.map((product: Products) => {
+ return (
+
+ );
+ })}
+
+ );
+};
diff --git a/src/components/ProductsList/index.js b/src/components/ProductsList/index.js
new file mode 100644
index 0000000000..09f9887f27
--- /dev/null
+++ b/src/components/ProductsList/index.js
@@ -0,0 +1 @@
+export * from './ProductsList';
diff --git a/src/components/QuantityButton/QuantityButton.module.scss b/src/components/QuantityButton/QuantityButton.module.scss
new file mode 100644
index 0000000000..85136946c1
--- /dev/null
+++ b/src/components/QuantityButton/QuantityButton.module.scss
@@ -0,0 +1,47 @@
+@import '../../utils/mixins';
+@import '../../utils/variables';
+
+.quantityButton {
+ display: block;
+ width: 32px;
+ height: 32px;
+
+ border: 1px solid var(--color-button-border);
+ background-color: var(--color-button);
+ border-radius: 48px;
+
+ background-size: 16px 16px;
+ background-repeat: no-repeat;
+ background-position: center;
+
+ &:hover {
+ border: 1px solid var(--color-button-border-hover);
+ background-color: var(--color-button-hover);
+ }
+
+ &--plus {
+ background-image: url(../../img/icons/plus.png);
+ }
+
+ &--plus-dark {
+ background-image: url(../../img/icons/plus-white.png);
+ }
+
+ &--minus {
+ background-image: url(../../img/icons/minus.png);
+ }
+
+ &--minus-dark {
+ background-image: url(../../img/icons/minus-white.png);
+ }
+
+ &--disabled {
+ background-image: url(../../img/icons/minus-grey.png);
+ background-color: var(--color-button-disabled);
+ border-color: var(--color-button-border-disabled);
+
+ &:hover {
+ background-color: var(--color-page-background);
+ }
+ }
+}
diff --git a/src/components/QuantityButton/QuantityButton.tsx b/src/components/QuantityButton/QuantityButton.tsx
new file mode 100644
index 0000000000..4f59763a18
--- /dev/null
+++ b/src/components/QuantityButton/QuantityButton.tsx
@@ -0,0 +1,31 @@
+import React from 'react';
+import styles from './QuantityButton.module.scss';
+import classNames from 'classnames';
+import { useTheme } from '../../context/ThemeContext';
+
+type Props = {
+ type: 'plus' | 'minus';
+ disabled?: boolean;
+ onClick?: () => void;
+};
+
+export const QuantityButton: React.FC = ({
+ type,
+ disabled,
+ onClick = () => {},
+}) => {
+ const { isDarkTheme } = useTheme();
+ const buttonClass = classNames(styles.quantityButton, {
+ [styles[`quantityButton--${type}`]]: !isDarkTheme,
+ [styles[`quantityButton--${type}-dark`]]: isDarkTheme,
+ [styles['quantityButton--disabled']]: disabled,
+ });
+
+ return (
+
+ );
+};
diff --git a/src/components/QuantityButton/index.js b/src/components/QuantityButton/index.js
new file mode 100644
index 0000000000..5c0c09404b
--- /dev/null
+++ b/src/components/QuantityButton/index.js
@@ -0,0 +1 @@
+export * from './QuantityButton';
diff --git a/src/components/QuantityIndicator/QuantityIndicator.module.scss b/src/components/QuantityIndicator/QuantityIndicator.module.scss
new file mode 100644
index 0000000000..acec5d2e85
--- /dev/null
+++ b/src/components/QuantityIndicator/QuantityIndicator.module.scss
@@ -0,0 +1,15 @@
+@import '../../utils/mixins';
+@import '../../utils/variables';
+
+.quantityIndicator {
+ display: flex;
+ width: 14px;
+ height: 14px;
+ background-color: var(--color-accent);
+ border: 1px solid #fff;
+ border-radius: 50%;
+ font-size: 9px;
+ color: #fff;
+ justify-content: center;
+ align-items: center;
+}
diff --git a/src/components/QuantityIndicator/QuantityIndicator.tsx b/src/components/QuantityIndicator/QuantityIndicator.tsx
new file mode 100644
index 0000000000..9ae8a57af8
--- /dev/null
+++ b/src/components/QuantityIndicator/QuantityIndicator.tsx
@@ -0,0 +1,10 @@
+import React from 'react';
+import styles from './QuantityIndicator.module.scss';
+
+type Props = {
+ number: number | null;
+};
+
+export const QuantityIndicator: React.FC = ({ number }) => {
+ return {number}
;
+};
diff --git a/src/components/QuantityIndicator/index.js b/src/components/QuantityIndicator/index.js
new file mode 100644
index 0000000000..e6dae3e3b9
--- /dev/null
+++ b/src/components/QuantityIndicator/index.js
@@ -0,0 +1 @@
+export * from './QuantityIndicator';
diff --git a/src/components/ShopByCategory/ShopByCategory.module.scss b/src/components/ShopByCategory/ShopByCategory.module.scss
new file mode 100644
index 0000000000..2e602041b1
--- /dev/null
+++ b/src/components/ShopByCategory/ShopByCategory.module.scss
@@ -0,0 +1,62 @@
+@import '../../utils/mixins';
+@import '../../utils/variables';
+
+.shopByCategory {
+ &__title {
+ margin-bottom: 24px;
+ }
+
+ &__categories {
+ display: flex;
+ flex-direction: column;
+ gap: 32px;
+
+ @include on-tablet {
+ display: grid;
+ grid-template-columns: repeat(12, 1fr);
+ gap: 16px;
+ }
+
+ @include on-desktop {
+ grid-template-columns: repeat(24, 32px);
+ }
+ }
+
+ &__category {
+ @include on-tablet {
+ grid-column: span 4;
+ }
+
+ @include on-desktop {
+ grid-column: span 8;
+ }
+ }
+
+ &__link {
+ display: block;
+ width: 100%;
+ aspect-ratio: 1 / 1;
+ }
+
+ &__image {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ margin-bottom: 24px;
+ transition: transform 0.3s ease-in-out;
+
+ &:hover {
+ transform: scale(1.05);
+ }
+ }
+
+ &__category-title {
+ margin-bottom: 4px;
+ }
+
+ &__quantity {
+ color: var(--color-grey);
+
+ @include body-text;
+ }
+}
diff --git a/src/components/ShopByCategory/ShopByCategory.tsx b/src/components/ShopByCategory/ShopByCategory.tsx
new file mode 100644
index 0000000000..be61dec85b
--- /dev/null
+++ b/src/components/ShopByCategory/ShopByCategory.tsx
@@ -0,0 +1,83 @@
+import styles from './ShopByCategory.module.scss';
+import { Title } from '../Title';
+import { fetchProducts } from '../../utils/fetch';
+import { useEffect, useState } from 'react';
+import { Products } from '../../utils/types';
+import { Link } from 'react-router-dom';
+import { BASE_URL } from '../../utils/constants';
+
+export const ShopByCategory = () => {
+ const [phones, setPhones] = useState([]);
+ const [tablets, setTablets] = useState([]);
+ const [accessories, setAccessories] = useState([]);
+
+ useEffect(() => {
+ fetchProducts('phones').then(res => setPhones(res));
+ fetchProducts('tablets').then(res => setTablets(res));
+ fetchProducts('accessories').then(res => setAccessories(res));
+ }, []);
+
+ return (
+
+
+
Shop by category
+
+
+
+
+
+
+
+
Mobile phones
+
+
{`${phones.length} models`}
+
+
+
+
+
+
+
Tablets
+
+
{`${tablets.length} models`}
+
+
+
+
+
+
+
Accessories
+
+
{`${accessories.length} models`}
+
+
+
+ );
+};
diff --git a/src/components/ShopByCategory/index.js b/src/components/ShopByCategory/index.js
new file mode 100644
index 0000000000..8081526324
--- /dev/null
+++ b/src/components/ShopByCategory/index.js
@@ -0,0 +1 @@
+export * from './ShopByCategory';
diff --git a/src/components/Slider/Slider.module.scss b/src/components/Slider/Slider.module.scss
new file mode 100644
index 0000000000..f364e515f5
--- /dev/null
+++ b/src/components/Slider/Slider.module.scss
@@ -0,0 +1,53 @@
+@import '../../utils/mixins';
+@import '../../utils/variables';
+
+.slider {
+ display: none;
+
+ @include on-tablet {
+ display: block;
+ }
+
+ &__images {
+ display: flex;
+ gap: 19px;
+ }
+
+ &__image {
+ display: block;
+ object-fit: cover;
+
+ width: 100%;
+ aspect-ratio: 1 / 1;
+ border: 1px solid var(--color-grey-second);
+
+ @include on-tablet {
+ border-radius: 8px;
+ width: 100%;
+ height: 189px;
+ }
+
+ @include on-desktop {
+ height: 400px;
+ }
+ }
+
+ &__buttons {
+ display: flex;
+ justify-content: center;
+ gap: 4px;
+ }
+
+ &__button {
+ display: block;
+ margin: 10px 5px;
+ width: 14px;
+ height: 4px;
+ border: none;
+ background-color: var(--color-grey-second);
+
+ &--active {
+ background-color: var(--color-black);
+ }
+ }
+}
diff --git a/src/components/Slider/Slider.tsx b/src/components/Slider/Slider.tsx
new file mode 100644
index 0000000000..8d8338957a
--- /dev/null
+++ b/src/components/Slider/Slider.tsx
@@ -0,0 +1,74 @@
+import styles from './Slider.module.scss';
+
+import { useEffect, useState } from 'react';
+import image1 from '/img/banners/image1.png';
+import image2 from '/img/banners/image2.png';
+import image3 from '/img/banners/image3.png';
+import { ArrowButton } from '../ArrowButton';
+import { ArrowType } from '../../utils/types';
+import classNames from 'classnames';
+
+const IMAGES = [image1, image2, image3];
+
+export const Slider = () => {
+ const [imageIndex, setImageIndex] = useState(0);
+
+ // #region functions
+
+ const goToPreviousImage = () => {
+ setImageIndex(prevIndex =>
+ prevIndex > 0 ? prevIndex - 1 : IMAGES.length - 1,
+ );
+ };
+
+ const goToNextImage = () => {
+ setImageIndex(prevIndex =>
+ prevIndex < IMAGES.length - 1 ? prevIndex + 1 : 0,
+ );
+ };
+
+ useEffect(() => {
+ const intervalId = setInterval(goToNextImage, 5000);
+
+ return () => clearInterval(intervalId);
+ }, [imageIndex]);
+
+ // #endregion
+
+ return (
+ <>
+
+
+
+
+
+
+
+ {IMAGES.map((_, index) => {
+ return (
+
+ );
+ })}
+
+
+ >
+ );
+};
diff --git a/src/components/Slider/index.js b/src/components/Slider/index.js
new file mode 100644
index 0000000000..f48a854158
--- /dev/null
+++ b/src/components/Slider/index.js
@@ -0,0 +1 @@
+export * from './Slider';
diff --git a/src/components/SortProducts/SortProducts.module.scss b/src/components/SortProducts/SortProducts.module.scss
new file mode 100644
index 0000000000..20c581f246
--- /dev/null
+++ b/src/components/SortProducts/SortProducts.module.scss
@@ -0,0 +1,15 @@
+@import '../../utils/mixins';
+@import '../../utils/variables';
+
+.selects {
+ margin-bottom: 24px;
+ display: flex;
+ gap: 16px;
+
+ &__label {
+ @include small-text;
+
+ color: var(--color-grey);
+ margin-bottom: 4px;
+ }
+}
diff --git a/src/components/SortProducts/SortProducts.tsx b/src/components/SortProducts/SortProducts.tsx
new file mode 100644
index 0000000000..9562c77c85
--- /dev/null
+++ b/src/components/SortProducts/SortProducts.tsx
@@ -0,0 +1,82 @@
+import React from 'react';
+import { SortType } from '../../utils/types';
+import Dropdown from 'react-bootstrap/Dropdown';
+import styles from './SortProducts.module.scss';
+import './dropdown.scss';
+
+type Props = {
+ selectedSortType: string;
+ searchParams: URLSearchParams;
+ setSearchParams: (value: URLSearchParams) => void;
+ itemsOnPage: string;
+};
+export const SortProducts: React.FC = ({
+ selectedSortType,
+ searchParams,
+ setSearchParams,
+ itemsOnPage,
+}) => {
+ const handleSortTypeChange = (sortType: string | null) => {
+ if (sortType !== null) {
+ const params = new URLSearchParams(searchParams);
+
+ params.set('sort', sortType);
+ setSearchParams(params);
+ }
+ };
+
+ const handlePagesChange = (selectedItemsOnPage: string | null) => {
+ const params = new URLSearchParams(searchParams);
+
+ if (selectedItemsOnPage) {
+ params.set('perPage', selectedItemsOnPage);
+ setSearchParams(params);
+ }
+ };
+
+ return (
+
+
+
+
+
+
+ {selectedSortType === SortType.newest
+ ? 'Newest'
+ : selectedSortType === SortType.alpha
+ ? 'Alphabetically'
+ : 'Cheapest'}
+
+
+
+ Newest
+
+ Alphabetically
+
+ Cheapest
+
+
+
+
+
+
+
+
+ {itemsOnPage === 'all' ? 'All' : itemsOnPage}
+
+
+
+ all
+ 4
+ 8
+ 16
+
+
+
+
+ );
+};
diff --git a/src/components/SortProducts/dropdown.scss b/src/components/SortProducts/dropdown.scss
new file mode 100644
index 0000000000..ae1d29f0ed
--- /dev/null
+++ b/src/components/SortProducts/dropdown.scss
@@ -0,0 +1,77 @@
+@import '../../utils/mixins';
+@import '../../utils/variables';
+@import 'bootstrap/scss/functions';
+@import 'bootstrap/scss/variables';
+@import 'bootstrap/scss/mixins';
+@import 'bootstrap/scss/dropdown';
+
+.dropdown-toggle {
+ display: flex !important;
+ align-items: center;
+ width: 136px;
+ height: 40px;
+ padding: 10px 12px !important;
+ font-family: inherit;
+ font-size: 14px !important;
+ line-height: 21px !important;
+ border: 1px solid var(--color-button-border) !important;
+ border-radius: 8px !important;
+ background-color: var(--color-dropdownfield-background) !important;
+ color: var(--color-black) !important;
+ position: relative;
+
+ &::after {
+ display: none;
+ }
+
+ &::before {
+ content: '';
+ background-image: url(../../img/icons/arrow_down_grey.png);
+ background-size: contain;
+ background-repeat: no-repeat;
+ width: 16px;
+ height: 16px;
+ position: absolute;
+ right: 12px;
+ top: 50%;
+ transform: translateY(-50%);
+ }
+
+ &:hover {
+ border-color: var(--color-grey) !important;
+ }
+
+ &:focus {
+ border-color: var(--color-black) !important;
+ }
+
+ @include on-tablet {
+ width: 187px;
+ }
+
+ @include on-desktop {
+ width: 176px;
+ }
+}
+
+.dropdown-menu {
+ width: 100%;
+ background-color: var(--color-dropdown-background);
+ border: 1px solid var(--color-grey-third) !important;
+ box-shadow: 0 2px 15px 0 #0000000d !important;
+ border-radius: 8px;
+ padding-block: 8px;
+}
+
+.dropdown-item {
+ @include body-text;
+
+ color: var(--color-grey);
+ padding: 6px 12px;
+ cursor: pointer;
+
+ &:hover {
+ color: var(--color-dropdown-hover-text);
+ background-color: var(--color-dropdown-hover);
+ }
+}
diff --git a/src/components/SortProducts/index.js b/src/components/SortProducts/index.js
new file mode 100644
index 0000000000..ac7f01588a
--- /dev/null
+++ b/src/components/SortProducts/index.js
@@ -0,0 +1 @@
+export * from './SortProducts';
diff --git a/src/components/Swiper/Swiper.scss b/src/components/Swiper/Swiper.scss
new file mode 100644
index 0000000000..8abe0fd669
--- /dev/null
+++ b/src/components/Swiper/Swiper.scss
@@ -0,0 +1,56 @@
+@import '../../utils/mixins';
+
+@media (min-width: 640px) {
+ .swiper {
+ display: none;
+ }
+}
+
+.swiper-container {
+ width: 100%;
+ height: 100%;
+ position: relative !important;
+ z-index: 0 !important;
+ padding-bottom: 20px;
+ overflow: hidden;
+}
+
+.swiper-slide {
+ display: flex;
+ align-items: flex-end;
+ justify-content: center;
+ background-color: transparent;
+}
+
+.swiper-slide img {
+ width: 100%;
+ aspect-ratio: 1 / 1;
+ object-fit: cover;
+ padding-bottom: 40px;
+ display: block;
+}
+
+.swiper-pagination {
+ bottom: 5px !important;
+ text-align: center;
+ position: absolute;
+ z-index: 1;
+ height: 25px;
+ background-color: transparent;
+ padding: 0;
+}
+
+.swiper-pagination-bullet {
+ width: 14px;
+ height: 4px;
+ background-color: #b4bdc3;
+ border-radius: 0;
+ display: inline-block;
+ margin: 0 15px 0 0 !important;
+ cursor: pointer;
+ opacity: 1;
+}
+
+.swiper-pagination-bullet-active {
+ background-color: #313237;
+}
diff --git a/src/components/Swiper/Swiper.tsx b/src/components/Swiper/Swiper.tsx
new file mode 100644
index 0000000000..7ea45f1027
--- /dev/null
+++ b/src/components/Swiper/Swiper.tsx
@@ -0,0 +1,31 @@
+// import React from 'react';
+import { Swiper, SwiperSlide } from 'swiper/react';
+import { Pagination } from 'swiper/modules';
+import 'swiper/css';
+import 'swiper/css/pagination';
+import './Swiper.scss';
+
+import image1 from '/img/banners/image1.png';
+import image2 from '/img/banners/image2.png';
+import image3 from '/img/banners/image3.png';
+
+export const MobileSwiper = () => {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/src/components/Swiper/index.js b/src/components/Swiper/index.js
new file mode 100644
index 0000000000..91e05d2527
--- /dev/null
+++ b/src/components/Swiper/index.js
@@ -0,0 +1 @@
+export * from './Swiper';
diff --git a/src/components/TechSpecs/TechSpecs.module.scss b/src/components/TechSpecs/TechSpecs.module.scss
new file mode 100644
index 0000000000..ebc7633bcf
--- /dev/null
+++ b/src/components/TechSpecs/TechSpecs.module.scss
@@ -0,0 +1,42 @@
+@import '../../utils/mixins';
+@import '../../utils/variables';
+
+.techSpecs {
+ display: flex;
+ flex-direction: column;
+ gap: 30px;
+
+ @include on-tablet {
+ gap: 25px;
+ }
+
+ &__title {
+ padding-bottom: 16px;
+ border-bottom: 1px solid var(--color-grey-second);
+ }
+
+ &__characteristics {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ }
+
+ &__characteristic {
+ display: flex;
+ justify-content: space-between;
+ }
+
+ &__characteristicName {
+ font-weight: 500;
+ color: var(--color-grey);
+
+ @include small-text;
+ }
+
+ &__characteristicValue {
+ font-weight: 500;
+ color: var(--color-black);
+
+ @include small-text;
+ }
+}
diff --git a/src/components/TechSpecs/TechSpecs.tsx b/src/components/TechSpecs/TechSpecs.tsx
new file mode 100644
index 0000000000..7ad27219d7
--- /dev/null
+++ b/src/components/TechSpecs/TechSpecs.tsx
@@ -0,0 +1,68 @@
+import React from 'react';
+import styles from './TechSpecs.module.scss';
+import { Product } from '../../utils/types';
+import { Title } from '../Title';
+
+type Props = {
+ product: Product;
+};
+
+export const TechSpecs: React.FC = ({ product }) => {
+ return (
+
+
+
Tech specs
+
+
+
+
Screen
+
+ {product.screen}
+
+
+
+
Resolution
+
+ {product.resolution}
+
+
+
+
Processor
+
+ {product.processor}
+
+
+
+
+
+ Built in memory
+
+
+ {product.capacity}
+
+
+
+
Camera
+
+ {product.camera}
+
+
+
+
Zoom
+
+ {product.zoom}
+
+
+
+
Cell
+
+ {product.cell}
+
+
+
+
+ );
+};
diff --git a/src/components/TechSpecs/index.js b/src/components/TechSpecs/index.js
new file mode 100644
index 0000000000..eada3132a0
--- /dev/null
+++ b/src/components/TechSpecs/index.js
@@ -0,0 +1 @@
+export * from './TechSpecs';
diff --git a/src/components/Title/Title.module.scss b/src/components/Title/Title.module.scss
new file mode 100644
index 0000000000..8db05d2d81
--- /dev/null
+++ b/src/components/Title/Title.module.scss
@@ -0,0 +1,58 @@
+@import '../../utils/mixins';
+
+.h1 {
+ font-size: 32px;
+ font-weight: 700;
+ line-height: 41px;
+ letter-spacing: -0.01em;
+ color: var(--color-titles);
+
+ @include on-tablet {
+ font-size: 48px;
+ line-height: 56px;
+ }
+}
+
+.h2 {
+ font-size: 22px;
+ font-weight: 700;
+ line-height: 31px;
+ letter-spacing: 0;
+ color: var(--color-titles);
+
+ @include on-tablet {
+ font-size: 32px;
+ line-height: 41px;
+ letter-spacing: -0.01em;
+ }
+}
+
+.h3 {
+ font-size: 20px;
+ font-weight: 600;
+ line-height: 26px;
+ letter-spacing: 0;
+ color: var(--color-titles);
+
+ @include on-tablet {
+ font-size: 22px;
+ font-weight: 700;
+ line-height: 31px;
+ letter-spacing: 0;
+ }
+}
+
+.h4 {
+ font-size: 16px;
+ font-weight: 600;
+ line-height: 20px;
+ letter-spacing: 0;
+ color: var(--color-titles);
+
+ @include on-tablet {
+ font-size: 20px;
+ font-weight: 600;
+ line-height: 26px;
+ letter-spacing: 0;
+ }
+}
diff --git a/src/components/Title/Title.tsx b/src/components/Title/Title.tsx
new file mode 100644
index 0000000000..9ac578c08d
--- /dev/null
+++ b/src/components/Title/Title.tsx
@@ -0,0 +1,12 @@
+import React from 'react';
+import styles from './Title.module.scss';
+
+type Props = {
+ level: 1 | 2 | 3 | 4;
+ children: React.ReactNode;
+};
+export const Title: React.FC = ({ level, children }) => {
+ const Tag = `h${level}` as keyof JSX.IntrinsicElements;
+
+ return {children};
+};
diff --git a/src/components/Title/index.js b/src/components/Title/index.js
new file mode 100644
index 0000000000..304b1af24b
--- /dev/null
+++ b/src/components/Title/index.js
@@ -0,0 +1 @@
+export * from './Title';
diff --git a/src/components/ToBuyButton/ToBuyButton.module.scss b/src/components/ToBuyButton/ToBuyButton.module.scss
new file mode 100644
index 0000000000..0d832621af
--- /dev/null
+++ b/src/components/ToBuyButton/ToBuyButton.module.scss
@@ -0,0 +1,34 @@
+@import '../../utils/mixins';
+@import '../../utils/variables';
+
+.toBuyButton {
+ max-width: 263px;
+ flex-grow: 1;
+ background-color: var(--color-accent);
+ border: 1px solid var(--color-accent-button-border);
+ border-radius: 48px;
+
+ @include buttons-text;
+
+ &:hover {
+ background-color: var(--color-accent-button-hover);
+ box-shadow: 0 3px 13px 0 #17203166;
+ }
+
+ &--dark {
+ &:hover {
+ box-shadow: none;
+ }
+ }
+
+ &--exist {
+ border: 1px solid var(--color-accent-button-border-selected);
+ background-color: var(--color-accent-button-selected);
+ color: var(--color-button-selected-text);
+
+ &:hover {
+ box-shadow: none;
+ background-color: var(--color-accent-button-selected);
+ }
+ }
+}
diff --git a/src/components/ToBuyButton/ToBuyButton.tsx b/src/components/ToBuyButton/ToBuyButton.tsx
new file mode 100644
index 0000000000..25fe1eb1af
--- /dev/null
+++ b/src/components/ToBuyButton/ToBuyButton.tsx
@@ -0,0 +1,42 @@
+import React, { useContext } from 'react';
+import styles from './ToBuyButton.module.scss';
+import { CartContext } from '../../context/CartContext';
+import { Products } from '../../utils/types';
+import classNames from 'classnames';
+import { findMaxId } from '../../utils/functions';
+import { useTheme } from '../../context/ThemeContext';
+
+type Props = {
+ height: string;
+ product: Products;
+};
+
+export const ToBuyButton: React.FC = ({ height, product }) => {
+ const { cart, setCart } = useContext(CartContext);
+ const { isDarkTheme } = useTheme();
+
+ const productExistinCart = cart.find(
+ cartItem => cartItem.product.itemId === product.itemId,
+ );
+
+ const handleProductBuy = () => {
+ setCart(prevCart => [
+ ...prevCart,
+ { id: findMaxId(cart), quantity: 1, product: product },
+ ]);
+ };
+
+ return (
+
+ );
+};
diff --git a/src/components/ToBuyButton/index.js b/src/components/ToBuyButton/index.js
new file mode 100644
index 0000000000..7ef9a3741e
--- /dev/null
+++ b/src/components/ToBuyButton/index.js
@@ -0,0 +1 @@
+export * from './ToBuyButton';
diff --git a/src/context/CartContext.tsx b/src/context/CartContext.tsx
new file mode 100644
index 0000000000..7c634039d0
--- /dev/null
+++ b/src/context/CartContext.tsx
@@ -0,0 +1,32 @@
+import React, { useEffect, useMemo, useState } from 'react';
+import { CartItem } from '../utils/types';
+
+type CartContextType = {
+ cart: CartItem[];
+ setCart: React.Dispatch>;
+};
+
+export const CartContext = React.createContext({
+ cart: [],
+ setCart: () => {},
+});
+
+type Props = {
+ children: React.ReactNode;
+};
+
+export const CartProvider: React.FC = ({ children }) => {
+ const [cart, setCart] = useState(() => {
+ const storedCart = localStorage.getItem('cart');
+
+ return storedCart ? JSON.parse(storedCart) : [];
+ });
+
+ useEffect(() => {
+ localStorage.setItem('cart', JSON.stringify(cart));
+ }, [cart]);
+
+ const value = useMemo(() => ({ cart, setCart }), [cart]);
+
+ return {children};
+};
diff --git a/src/context/FavoritesContext.tsx b/src/context/FavoritesContext.tsx
new file mode 100644
index 0000000000..c9c1b9a858
--- /dev/null
+++ b/src/context/FavoritesContext.tsx
@@ -0,0 +1,36 @@
+import React, { useEffect, useMemo, useState } from 'react';
+import { Products } from '../utils/types';
+
+type FavoritesContextType = {
+ favorites: Products[];
+ setFavorites: React.Dispatch>;
+};
+
+export const FavoritesContext = React.createContext({
+ favorites: [],
+ setFavorites: () => {},
+});
+
+type Props = {
+ children: React.ReactNode;
+};
+
+export const FavoritesProvider: React.FC = ({ children }) => {
+ const [favorites, setFavorites] = useState(() => {
+ const storedFavorites = localStorage.getItem('favorites');
+
+ return storedFavorites ? JSON.parse(storedFavorites) : [];
+ });
+
+ useEffect(() => {
+ localStorage.setItem('favorites', JSON.stringify(favorites));
+ }, [favorites]);
+
+ const value = useMemo(() => ({ favorites, setFavorites }), [favorites]);
+
+ return (
+
+ {children}
+
+ );
+};
diff --git a/src/context/ThemeContext.tsx b/src/context/ThemeContext.tsx
new file mode 100644
index 0000000000..2f4e21d573
--- /dev/null
+++ b/src/context/ThemeContext.tsx
@@ -0,0 +1,48 @@
+import React, { createContext, useState, useEffect, useContext } from 'react';
+
+type ThemeContextType = {
+ isDarkTheme: boolean;
+ toggleTheme: () => void;
+};
+
+const ThemeContext = createContext(undefined);
+
+export const useTheme = () => {
+ const context = useContext(ThemeContext);
+
+ if (!context) {
+ throw new Error('useTheme must be used within a ThemeProvider');
+ }
+
+ return context;
+};
+
+type Props = {
+ children: React.ReactNode;
+};
+
+export const ThemeProvider: React.FC = ({ children }) => {
+ const [isDarkTheme, setIsDarkTheme] = useState(false);
+
+ useEffect(() => {
+ const savedTheme = localStorage.getItem('theme');
+
+ if (savedTheme) {
+ setIsDarkTheme(savedTheme === 'dark');
+ }
+ }, []);
+
+ useEffect(() => {
+ localStorage.setItem('theme', isDarkTheme ? 'dark' : 'light');
+ }, [isDarkTheme]);
+
+ const toggleTheme = () => {
+ setIsDarkTheme(prevTheme => !prevTheme);
+ };
+
+ return (
+
+ {children}
+
+ );
+};
diff --git a/src/declarations.d.ts b/src/declarations.d.ts
new file mode 100644
index 0000000000..af3d030048
--- /dev/null
+++ b/src/declarations.d.ts
@@ -0,0 +1,9 @@
+declare module '*.scss' {
+ const content: { [className: string]: string };
+ export default content;
+}
+
+declare module '*.css' {
+ const content: { [className: string]: string };
+ export default content;
+}
diff --git a/src/images.d.ts b/src/images.d.ts
new file mode 100644
index 0000000000..1c5923252c
--- /dev/null
+++ b/src/images.d.ts
@@ -0,0 +1,4 @@
+declare module '*.png' {
+ const value: string;
+ export default value;
+}
diff --git a/src/img/icons/arrow_down_grey.png b/src/img/icons/arrow_down_grey.png
new file mode 100644
index 0000000000..9fc496046d
Binary files /dev/null and b/src/img/icons/arrow_down_grey.png differ
diff --git a/src/img/icons/arrow_left_black.png b/src/img/icons/arrow_left_black.png
new file mode 100644
index 0000000000..227203ad3c
Binary files /dev/null and b/src/img/icons/arrow_left_black.png differ
diff --git a/src/img/icons/arrow_left_grey.png b/src/img/icons/arrow_left_grey.png
new file mode 100644
index 0000000000..da9aa75f6a
Binary files /dev/null and b/src/img/icons/arrow_left_grey.png differ
diff --git a/src/img/icons/arrow_left_white.png b/src/img/icons/arrow_left_white.png
new file mode 100644
index 0000000000..35e14fb3c6
Binary files /dev/null and b/src/img/icons/arrow_left_white.png differ
diff --git a/src/img/icons/arrow_right_black.png b/src/img/icons/arrow_right_black.png
new file mode 100644
index 0000000000..2fd8e05ba3
Binary files /dev/null and b/src/img/icons/arrow_right_black.png differ
diff --git a/src/img/icons/arrow_right_grey.png b/src/img/icons/arrow_right_grey.png
new file mode 100644
index 0000000000..713b26d7e8
Binary files /dev/null and b/src/img/icons/arrow_right_grey.png differ
diff --git a/src/img/icons/arrow_right_white.png b/src/img/icons/arrow_right_white.png
new file mode 100644
index 0000000000..b8deafceb4
Binary files /dev/null and b/src/img/icons/arrow_right_white.png differ
diff --git a/src/img/icons/arrow_up_black.png b/src/img/icons/arrow_up_black.png
new file mode 100644
index 0000000000..b5a0c468a4
Binary files /dev/null and b/src/img/icons/arrow_up_black.png differ
diff --git a/src/img/icons/arrow_up_grey.png b/src/img/icons/arrow_up_grey.png
new file mode 100644
index 0000000000..626b450ef3
Binary files /dev/null and b/src/img/icons/arrow_up_grey.png differ
diff --git a/src/img/icons/arrow_up_white.png b/src/img/icons/arrow_up_white.png
new file mode 100644
index 0000000000..266e68ba08
Binary files /dev/null and b/src/img/icons/arrow_up_white.png differ
diff --git a/src/img/icons/cart.png b/src/img/icons/cart.png
new file mode 100644
index 0000000000..8a84a35b86
Binary files /dev/null and b/src/img/icons/cart.png differ
diff --git a/src/img/icons/close-grey.png b/src/img/icons/close-grey.png
new file mode 100644
index 0000000000..4d8beda285
Binary files /dev/null and b/src/img/icons/close-grey.png differ
diff --git a/src/img/icons/close-white.png b/src/img/icons/close-white.png
new file mode 100644
index 0000000000..c919f7f327
Binary files /dev/null and b/src/img/icons/close-white.png differ
diff --git a/src/img/icons/close.png b/src/img/icons/close.png
new file mode 100644
index 0000000000..198a69590a
Binary files /dev/null and b/src/img/icons/close.png differ
diff --git a/src/img/icons/favourites.png b/src/img/icons/favourites.png
new file mode 100644
index 0000000000..605aac6123
Binary files /dev/null and b/src/img/icons/favourites.png differ
diff --git a/src/img/icons/heart.png b/src/img/icons/heart.png
new file mode 100644
index 0000000000..605aac6123
Binary files /dev/null and b/src/img/icons/heart.png differ
diff --git a/src/img/icons/heart_yellow.png b/src/img/icons/heart_yellow.png
new file mode 100644
index 0000000000..63fbc5c7a2
Binary files /dev/null and b/src/img/icons/heart_yellow.png differ
diff --git a/src/img/icons/home.png b/src/img/icons/home.png
new file mode 100644
index 0000000000..ce4e1425f7
Binary files /dev/null and b/src/img/icons/home.png differ
diff --git a/src/img/icons/menu-white.png b/src/img/icons/menu-white.png
new file mode 100644
index 0000000000..a356e522f7
Binary files /dev/null and b/src/img/icons/menu-white.png differ
diff --git a/src/img/icons/menu.png b/src/img/icons/menu.png
new file mode 100644
index 0000000000..543d3dca50
Binary files /dev/null and b/src/img/icons/menu.png differ
diff --git a/src/img/icons/minus-grey.png b/src/img/icons/minus-grey.png
new file mode 100644
index 0000000000..0b813afd49
Binary files /dev/null and b/src/img/icons/minus-grey.png differ
diff --git a/src/img/icons/minus-white.png b/src/img/icons/minus-white.png
new file mode 100644
index 0000000000..2c5475efde
Binary files /dev/null and b/src/img/icons/minus-white.png differ
diff --git a/src/img/icons/minus.png b/src/img/icons/minus.png
new file mode 100644
index 0000000000..58d40d7f19
Binary files /dev/null and b/src/img/icons/minus.png differ
diff --git a/src/img/icons/night_theme_btn.png b/src/img/icons/night_theme_btn.png
new file mode 100644
index 0000000000..dd5c9207c1
Binary files /dev/null and b/src/img/icons/night_theme_btn.png differ
diff --git a/src/img/icons/night_theme_cart.png b/src/img/icons/night_theme_cart.png
new file mode 100644
index 0000000000..40496d13d3
Binary files /dev/null and b/src/img/icons/night_theme_cart.png differ
diff --git a/src/img/icons/night_theme_heart.png b/src/img/icons/night_theme_heart.png
new file mode 100644
index 0000000000..6393e242ba
Binary files /dev/null and b/src/img/icons/night_theme_heart.png differ
diff --git a/src/img/icons/night_theme_home.png b/src/img/icons/night_theme_home.png
new file mode 100644
index 0000000000..bb4f84759f
Binary files /dev/null and b/src/img/icons/night_theme_home.png differ
diff --git a/src/img/icons/plus-white.png b/src/img/icons/plus-white.png
new file mode 100644
index 0000000000..49bbf3175d
Binary files /dev/null and b/src/img/icons/plus-white.png differ
diff --git a/src/img/icons/plus.png b/src/img/icons/plus.png
new file mode 100644
index 0000000000..2371233173
Binary files /dev/null and b/src/img/icons/plus.png differ
diff --git a/src/img/logo.png b/src/img/logo.png
new file mode 100644
index 0000000000..f80332f91e
Binary files /dev/null and b/src/img/logo.png differ
diff --git a/src/img/night_theme_logo.png b/src/img/night_theme_logo.png
new file mode 100644
index 0000000000..3b8923807d
Binary files /dev/null and b/src/img/night_theme_logo.png differ
diff --git a/public/img/page-not-found.png b/src/img/notFound/page-not-found.png
similarity index 100%
rename from public/img/page-not-found.png
rename to src/img/notFound/page-not-found.png
diff --git a/public/img/product-not-found.png b/src/img/notFound/product-not-found.png
similarity index 100%
rename from public/img/product-not-found.png
rename to src/img/notFound/product-not-found.png
diff --git a/src/index.tsx b/src/index.tsx
index 50470f1508..b0620ab4c6 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/normilize.css b/src/normilize.css
new file mode 100644
index 0000000000..0968aba799
--- /dev/null
+++ b/src/normilize.css
@@ -0,0 +1,28 @@
+* {
+ box-sizing: border-box;
+}
+
+a,
+p,
+body,
+div,
+header,
+footer,
+html,
+img,
+li,
+nav,
+ul,
+h1,
+h2,
+h3,
+h4,
+button,
+article {
+ margin: 0;
+ padding: 0;
+}
+
+button {
+ cursor: pointer;
+}
diff --git a/src/pages/CartPage/CartPage.module.scss b/src/pages/CartPage/CartPage.module.scss
new file mode 100644
index 0000000000..731ada2fea
--- /dev/null
+++ b/src/pages/CartPage/CartPage.module.scss
@@ -0,0 +1,76 @@
+@import '../../utils/mixins';
+@import '../../utils/variables';
+
+.cartPage {
+ &__goback {
+ display: flex;
+ gap: 4px;
+ padding-block: 24px;
+
+ @include on-tablet {
+ padding-top: 40px;
+ padding-bottom: 16px;
+ }
+ }
+
+ &__title {
+ padding-bottom: 32px;
+ }
+
+ &__container {
+ display: flex;
+ flex-direction: column;
+ gap: 32px;
+ padding-bottom: 56px;
+
+ @include on-tablet {
+ padding-bottom: 64px;
+ }
+
+ @include on-desktop {
+ display: grid;
+ grid-template-columns: repeat(24, 32px);
+ column-gap: 16px;
+ padding-bottom: 80px;
+ }
+ }
+
+ &__items {
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+
+ @include on-desktop {
+ grid-column: 1 / 17;
+ }
+ }
+
+ &__total {
+ width: 100%;
+ height: 190px;
+ padding: 24px;
+ border: 1px solid var(--color-grey-third);
+ border-radius: 16px;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 16px;
+
+ @include on-desktop {
+ height: 206px;
+ grid-column: 17 / -1;
+ }
+ }
+
+ &__totalItems {
+ color: var(--color-grey);
+
+ @include body-text;
+ }
+
+ &__line {
+ height: 1px;
+ width: 100%;
+ border-bottom: 1px solid var(--color-grey-third);
+ }
+}
diff --git a/src/pages/CartPage/CartPage.tsx b/src/pages/CartPage/CartPage.tsx
new file mode 100644
index 0000000000..12e123acf3
--- /dev/null
+++ b/src/pages/CartPage/CartPage.tsx
@@ -0,0 +1,55 @@
+import { useContext } from 'react';
+import { CartContext } from '../../context/CartContext';
+import { GoBack } from '../../components/GoBack';
+import styles from './CartPage.module.scss';
+import { Title } from '../../components/Title';
+import { CartIsEmpty } from '../../components/CartIsEmpty';
+import { CartProduct } from '../../components/CartProduct';
+import {
+ calculateTotalAmount,
+ calculateTotalPrice,
+} from '../../utils/functions';
+import { CheckoutButton } from '../../components/CheckoutButton';
+import { CartItem } from '../../utils/types';
+import blackArrow from '../../img/icons/arrow_left_black.png';
+import whiteArrow from '../../img/icons/arrow_left_white.png';
+import { useTheme } from '../../context/ThemeContext';
+
+export const CartPage = () => {
+ const { cart } = useContext(CartContext);
+ const { isDarkTheme } = useTheme();
+
+ return (
+
+
+
+
Back
+
+
+
Cart
+
+
+ {cart.length === 0 &&
}
+
+ {cart.length > 0 && (
+
+
+ {cart.map((cartItem: CartItem) => (
+
+ ))}
+
+
+
+
{'$' + calculateTotalPrice(cart)}
+
{`Total for ${calculateTotalAmount(cart)} items`}
+
+
+
+
+
+ )}
+
+ );
+};
diff --git a/src/pages/CartPage/index.js b/src/pages/CartPage/index.js
new file mode 100644
index 0000000000..90c010237a
--- /dev/null
+++ b/src/pages/CartPage/index.js
@@ -0,0 +1 @@
+export * from './CartPage';
diff --git a/src/pages/Catalog/Catalog.module.scss b/src/pages/Catalog/Catalog.module.scss
new file mode 100644
index 0000000000..e5ef38da22
--- /dev/null
+++ b/src/pages/Catalog/Catalog.module.scss
@@ -0,0 +1,69 @@
+@import '../../utils/mixins';
+@import '../../utils/variables';
+
+.catalog {
+ &__navigation {
+ padding-top: 24px;
+ padding-bottom: 24px;
+ display: flex;
+ gap: 8px;
+
+ @include on-tablet {
+ padding-bottom: 40px;
+ }
+ }
+
+ &__current-page {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ font-weight: 600;
+ color: var(--color-grey);
+ line-height: 15px;
+ text-transform: capitalize;
+ }
+
+ &__title {
+ padding-bottom: 8px;
+ }
+
+ &__quantity {
+ color: var(--color-grey);
+ padding-bottom: 32px;
+
+ @include body-text;
+
+ @include on-tablet {
+ padding-bottom: 40px;
+ }
+ }
+ &__list {
+ padding-bottom: 24px;
+
+ @include on-tablet {
+ padding-bottom: 40px;
+ }
+ }
+
+ &__error {
+ display: flex;
+ flex-direction: column;
+ gap: 20px;
+ align-items: center;
+ }
+
+ &__reload {
+ background-color: var(--color-accent);
+ border: none;
+ border-radius: 48px;
+ padding: 12px 24px;
+ color: white;
+ cursor: pointer;
+
+ @include buttons-text;
+
+ &:hover {
+ box-shadow: 0 3px 13px 0 #17203166;
+ }
+ }
+}
diff --git a/src/pages/Catalog/Catalog.tsx b/src/pages/Catalog/Catalog.tsx
new file mode 100644
index 0000000000..9638937808
--- /dev/null
+++ b/src/pages/Catalog/Catalog.tsx
@@ -0,0 +1,177 @@
+import styles from './Catalog.module.scss';
+
+import { NavLink, useParams } from 'react-router-dom';
+import { Title } from '../../components/Title';
+import { useEffect, useState } from 'react';
+import { fetchProducts } from '../../utils/fetch';
+import { Products, SortType } from '../../utils/types';
+import { Loader } from '../../components/Loader';
+import { ProductsList } from '../../components/ProductsList';
+import { SortProducts } from '../../components/SortProducts';
+import { useSearchParams } from 'react-router-dom';
+import { Pagination } from '../../components/Pagination';
+import { NotFoundPage } from '../NotFoundPage';
+import homeIcon from '../../img/icons/home.png';
+import homeIconDark from '../../img/icons/night_theme_home.png';
+import { useTheme } from '../../context/ThemeContext';
+import { ArrowGrey } from '../../components/ArrowGrey';
+import { BASE_URL } from '../../utils/constants';
+
+export const Catalog = () => {
+ // #region state
+ const [products, setProducts] = useState([]);
+ const [displayedProducts, setDisplayedProducts] = useState(
+ [],
+ );
+ const [isLoading, setIsLoading] = useState(false);
+ const [errorMessage, setErrorMessage] = useState('');
+ const [updatedAt, setUpdatedAt] = useState(new Date());
+ const [searchParams, setSearchParams] = useSearchParams();
+ const { isDarkTheme } = useTheme();
+ // #endregion
+ // #region functions
+ const getTitleName = (currentCategory: string) => {
+ switch (currentCategory) {
+ case 'phones':
+ return 'Phones page';
+ case 'tablets':
+ return 'Tablets page';
+ case 'accessories':
+ return 'Accessories page';
+ default:
+ return '';
+ }
+ };
+
+ const reloadPage = () => {
+ setUpdatedAt(new Date());
+ setErrorMessage('');
+ };
+
+ const displayPageItems = (
+ allItems: Products[],
+ currentPage: number,
+ itemsOnPage: number,
+ ) => {
+ const indexOfFirst = (currentPage - 1) * itemsOnPage;
+ const indexOfLast = indexOfFirst + itemsOnPage;
+
+ return allItems.slice(indexOfFirst, indexOfLast);
+ };
+
+ // #endregion
+ // #region variables
+ const { category } = useParams();
+ const knownCategories = ['phones', 'tablets', 'accessories'];
+ const pageTitle = category ? getTitleName(category) : '';
+ const selectedSortType =
+ (searchParams.get('sort') as SortType) || SortType.newest;
+ const itemsOnPage = searchParams.get('perPage') || 'all';
+ const activePage = searchParams.get('page') || '1';
+ const pagesAmount =
+ itemsOnPage === 'all' ? 1 : Math.ceil(products.length / +itemsOnPage);
+ // #endregion
+
+ useEffect(() => {
+ if (!category || !knownCategories.includes(category)) {
+ return;
+ }
+
+ if (category) {
+ setIsLoading(true);
+ setProducts([]);
+ setDisplayedProducts([]);
+ fetchProducts(category, selectedSortType)
+ .then(res => {
+ setProducts(res);
+ })
+ .catch(() => {
+ setErrorMessage('Something went wrong');
+ })
+ .finally(() => setIsLoading(false));
+ }
+ }, [category, updatedAt, selectedSortType]);
+
+ // useEffect(() => {
+ // if (category && !searchParams.get('sort')) {
+ // searchParams.set('sort', SortType.newest);
+ // setSearchParams(searchParams);
+ // }
+ // }, [category, searchParams, setSearchParams]);
+
+ useEffect(() => {
+ if (itemsOnPage === 'all') {
+ setDisplayedProducts(products);
+ } else {
+ setDisplayedProducts(
+ displayPageItems(products, +activePage, +itemsOnPage),
+ );
+ }
+ }, [products, searchParams, category, activePage, itemsOnPage]);
+
+ if (!category || !knownCategories.includes(category)) {
+ return ;
+ }
+
+ return (
+
+
+
+
+
+
+
{category}
+
+
+ {isLoading &&
}
+
+ {errorMessage && (
+
+
{errorMessage}
+
+
+ )}
+
+ {!isLoading && !errorMessage && products.length === 0 && (
+
{`There are no ${category} yet`}
+ )}
+
+ {!isLoading && !errorMessage && displayedProducts.length > 0 && (
+ <>
+
+
{pageTitle}
+
+
+ {`${products.length} models`}
+
+
+
+ >
+ )}
+
+ {pagesAmount > 1 && (
+
+ )}
+
+ );
+};
diff --git a/src/pages/Catalog/index.js b/src/pages/Catalog/index.js
new file mode 100644
index 0000000000..50d66eb125
--- /dev/null
+++ b/src/pages/Catalog/index.js
@@ -0,0 +1 @@
+export * from './Catalog';
diff --git a/src/pages/Favorites/Favorites.module.scss b/src/pages/Favorites/Favorites.module.scss
new file mode 100644
index 0000000000..3fc5a03b69
--- /dev/null
+++ b/src/pages/Favorites/Favorites.module.scss
@@ -0,0 +1,52 @@
+@import '../../utils/mixins';
+@import '../../utils/variables';
+
+.favorites {
+ &__navigation {
+ padding-top: 24px;
+ padding-bottom: 24px;
+ display: flex;
+ gap: 8px;
+
+ @include on-tablet {
+ padding-bottom: 40px;
+ }
+ }
+
+ &__current-page {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ font-weight: 600;
+ color: var(--color-grey);
+ line-height: 15px;
+ text-transform: capitalize;
+ }
+
+ &__title {
+ padding-bottom: 8px;
+ }
+
+ &__quantity {
+ color: var(--color-grey);
+ padding-bottom: 32px;
+
+ @include body-text;
+
+ @include on-tablet {
+ padding-bottom: 40px;
+ }
+ }
+
+ &__list {
+ padding-bottom: 56px;
+
+ @include on-tablet {
+ padding-bottom: 64px;
+ }
+
+ @include on-desktop {
+ padding-bottom: 80px;
+ }
+ }
+}
diff --git a/src/pages/Favorites/Favorites.tsx b/src/pages/Favorites/Favorites.tsx
new file mode 100644
index 0000000000..7377122c65
--- /dev/null
+++ b/src/pages/Favorites/Favorites.tsx
@@ -0,0 +1,41 @@
+import { NavLink } from 'react-router-dom';
+import styles from './Favorites.module.scss';
+import { Title } from '../../components/Title';
+import { useContext } from 'react';
+import { FavoritesContext } from '../../context/FavoritesContext';
+import { ProductsList } from '../../components/ProductsList';
+import homeIcon from '../../img/icons/home.png';
+import homeIconDark from '../../img/icons/night_theme_home.png';
+import { useTheme } from '../../context/ThemeContext';
+import { ArrowGrey } from '../../components/ArrowGrey';
+import { BASE_URL } from '../../utils/constants';
+
+export const Favorites = () => {
+ const { favorites } = useContext(FavoritesContext);
+ const { isDarkTheme } = useTheme();
+
+ return (
+
+
+
+
+
+
+
Favorites
+
+
+
Favourites
+
+
{`${favorites.length} items`}
+
+
+ );
+};
diff --git a/src/pages/Favorites/index.js b/src/pages/Favorites/index.js
new file mode 100644
index 0000000000..c3ffcf8b38
--- /dev/null
+++ b/src/pages/Favorites/index.js
@@ -0,0 +1 @@
+export * from './Favorites';
diff --git a/src/pages/HomePage/HomePage.module.scss b/src/pages/HomePage/HomePage.module.scss
new file mode 100644
index 0000000000..76e67c7b95
--- /dev/null
+++ b/src/pages/HomePage/HomePage.module.scss
@@ -0,0 +1,27 @@
+@import '../../utils/mixins';
+
+.homePage {
+ &__title {
+ padding-block: 24px;
+
+ @include on-tablet {
+ padding-block: 32px;
+ }
+
+ @include on-desktop {
+ padding-block: 56px;
+ }
+ }
+
+ &__section {
+ margin-bottom: 56px;
+
+ @include on-tablet {
+ margin-bottom: 64px;
+ }
+
+ @include on-desktop {
+ margin-bottom: 80px;
+ }
+ }
+}
diff --git a/src/pages/HomePage/HomePage.tsx b/src/pages/HomePage/HomePage.tsx
new file mode 100644
index 0000000000..903d669e0c
--- /dev/null
+++ b/src/pages/HomePage/HomePage.tsx
@@ -0,0 +1,38 @@
+import { ProductSlider } from '../../components/ProductSlider';
+import { MobileSwiper } from '../../components/Swiper';
+import { Title } from '../../components/Title';
+import { ProductCategory, SortType } from '../../utils/types';
+import { Slider } from '../../components/Slider';
+import styles from './HomePage.module.scss';
+import { ShopByCategory } from '../../components/ShopByCategory';
+
+export const HomePage = () => {
+ return (
+
+
+
Welcome to Nice Gadgets store!
+
+
+
+
+
+
+ );
+};
diff --git a/src/pages/HomePage/index.js b/src/pages/HomePage/index.js
new file mode 100644
index 0000000000..11e53da674
--- /dev/null
+++ b/src/pages/HomePage/index.js
@@ -0,0 +1 @@
+export * from './HomePage';
diff --git a/src/pages/NotFoundPage/NotFoundPage.module.scss b/src/pages/NotFoundPage/NotFoundPage.module.scss
new file mode 100644
index 0000000000..53dc267d83
--- /dev/null
+++ b/src/pages/NotFoundPage/NotFoundPage.module.scss
@@ -0,0 +1,22 @@
+@import '../../utils/mixins';
+
+.notFoundPage {
+ &__container {
+ padding-top: 100px;
+ align-items: center;
+ text-align: center;
+ display: flex;
+ flex-direction: column;
+ gap: 50px;
+ }
+
+ &__image {
+ width: 200px;
+ height: 200px;
+
+ @include on-tablet {
+ width: 300px;
+ height: 300px;
+ }
+ }
+}
diff --git a/src/pages/NotFoundPage/NotFoundPage.tsx b/src/pages/NotFoundPage/NotFoundPage.tsx
new file mode 100644
index 0000000000..64fb69a13a
--- /dev/null
+++ b/src/pages/NotFoundPage/NotFoundPage.tsx
@@ -0,0 +1,18 @@
+import { Title } from '../../components/Title';
+import styles from './NotFoundPage.module.scss';
+import image from '../../img/notFound/page-not-found.png';
+
+export const NotFoundPage = () => {
+ return (
+
+
+
+
Oooops... page not found
+
+
+ );
+};
diff --git a/src/pages/NotFoundPage/index.js b/src/pages/NotFoundPage/index.js
new file mode 100644
index 0000000000..6197aa75aa
--- /dev/null
+++ b/src/pages/NotFoundPage/index.js
@@ -0,0 +1 @@
+export * from './NotFoundPage';
diff --git a/src/pages/ProductDetailsPage/ProductDetailsPage.module.scss b/src/pages/ProductDetailsPage/ProductDetailsPage.module.scss
new file mode 100644
index 0000000000..01240ab4f0
--- /dev/null
+++ b/src/pages/ProductDetailsPage/ProductDetailsPage.module.scss
@@ -0,0 +1,287 @@
+@import '../../utils/mixins';
+@import '../../utils/variables';
+
+.productDetails {
+ &__goback {
+ display: flex;
+ gap: 4px;
+ padding-bottom: 16px;
+ }
+
+ &__error {
+ text-align: center;
+ padding-top: 30px;
+ }
+
+ &__errorMessage {
+ padding-bottom: 20px;
+ }
+
+ &__errorImage {
+ width: 150px;
+ height: 150px;
+
+ @include on-tablet {
+ width: 200px;
+ height: 200px;
+ }
+
+ @include on-desktop {
+ width: 250px;
+ height: 250px;
+ }
+ }
+
+ &__title {
+ padding-bottom: 32px;
+
+ @include on-tablet {
+ padding-bottom: 40px;
+ }
+ }
+
+ &__top {
+ padding-bottom: 56px;
+
+ @include on-tablet {
+ display: grid;
+ grid-template-columns: repeat(12, 1fr);
+ column-gap: 16px;
+ padding-bottom: 64px;
+ }
+
+ @include on-desktop {
+ grid-template-columns: repeat(24, 32px);
+ padding-bottom: 80px;
+ }
+ }
+
+ &__imageContainer {
+ width: 100%;
+ aspect-ratio: 1 / 1;
+ padding-bottom: 16px;
+
+ @include on-tablet {
+ padding-bottom: 0;
+ grid-column: 2 / 8;
+ }
+
+ @include on-desktop {
+ grid-column: 3 / 13;
+ }
+ }
+
+ &__image {
+ width: 100%;
+ height: 100%;
+ object-position: center;
+ object-fit: contain;
+ }
+
+ &__images {
+ display: flex;
+ gap: 8px;
+ padding-bottom: 40px;
+
+ @include on-tablet {
+ flex-direction: column;
+ grid-column: 1 / 2;
+ grid-row: 1;
+ padding-bottom: 0;
+ }
+
+ @include on-desktop {
+ grid-column: 1 / 3;
+ grid-row: 1;
+ }
+ }
+
+ &__details {
+ width: 100%;
+
+ @include on-tablet {
+ grid-column: 8 / -1;
+ }
+
+ @include on-desktop {
+ grid-column: 14 / 21;
+ }
+ }
+
+ &__images-item {
+ height: 49px;
+ padding: 5px;
+ border-radius: 4px;
+ border: 1px solid var(--color-grey-second);
+ object-fit: contain;
+
+ @include on-tablet {
+ width: 100%;
+ height: auto;
+ aspect-ratio: 1 / 1;
+ padding: 3px;
+ }
+
+ &--active {
+ border: 1px solid var(--color-black);
+ }
+ }
+
+ &__label {
+ @include small-text;
+
+ color: var(--color-grey);
+ padding-bottom: 8px;
+ }
+
+ &__selectors {
+ display: flex;
+ flex-direction: column;
+ gap: 24px;
+ padding-bottom: 32px;
+ }
+
+ &__selectorButtons {
+ display: flex;
+ gap: 8px;
+ padding-bottom: 24px;
+ flex-wrap: wrap;
+ }
+
+ &__colorButtonContainer {
+ box-sizing: border-box;
+ width: 32px;
+ height: 32px;
+ border-radius: 36px;
+ border: 1px solid var(--color-border-icons);
+
+ &:hover {
+ border-color: var(--color-grey);
+ }
+
+ &--active {
+ border: 1px solid var(--color-black);
+ }
+ }
+
+ &__colorButton {
+ margin: 0;
+ appearance: none;
+ width: 100%;
+ height: 100%;
+ border: 2px solid var(--color-page-background);
+ border-radius: 36px;
+ }
+
+ &__line {
+ width: 100%;
+ border-bottom: 1px solid var(--color-grey-third);
+ }
+
+ &__capacity {
+ @include body-text;
+
+ height: 32px;
+ padding-inline: 8px;
+ font-family: inherit;
+ background-color: var(--color-page-background);
+ color: var(--color-black);
+ border-radius: 4px;
+ border: 1px solid var(--color-grey-second);
+
+ &--active {
+ color: var(--color-white);
+ background-color: var(--color-black);
+ border: none;
+ }
+ }
+
+ &__prices {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ padding-bottom: 16px;
+ }
+
+ &__discountPrice {
+ font-size: 22px;
+ font-weight: 500;
+ line-height: 28px;
+ text-decoration: line-through;
+ color: var(--color-grey);
+ }
+
+ &__buttons {
+ display: flex;
+ gap: 8px;
+ padding-bottom: 32px;
+ align-items: center;
+ }
+
+ &__characteristics {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ }
+
+ &__characteristic {
+ display: flex;
+ justify-content: space-between;
+ }
+
+ &__characteristicName {
+ font-weight: 500;
+ color: var(--color-grey);
+
+ @include small-text;
+ }
+ &__characteristicValue {
+ font-weight: 500;
+ color: var(--color-black);
+
+ @include small-text;
+ }
+
+ &__description {
+ display: flex;
+ flex-direction: column;
+ gap: 56px;
+ padding-bottom: 56px;
+
+ @include on-tablet {
+ gap: 64px;
+ padding-bottom: 64px;
+ }
+
+ @include on-desktop {
+ display: grid;
+ grid-template-columns: repeat(24, 32px);
+ column-gap: 16px;
+ padding-bottom: 80px;
+ }
+ }
+
+ &__about {
+ @include on-desktop {
+ grid-column: 1 / 13;
+ }
+ }
+
+ &__specs {
+ @include on-desktop {
+ grid-column: 14 / 24;
+ }
+ }
+
+ &__slider {
+ padding-bottom: 56px;
+
+ @include on-tablet {
+ padding-bottom: 64px;
+ }
+
+ @include on-desktop {
+ padding-bottom: 80px;
+ }
+ }
+}
diff --git a/src/pages/ProductDetailsPage/ProductDetailsPage.tsx b/src/pages/ProductDetailsPage/ProductDetailsPage.tsx
new file mode 100644
index 0000000000..cc4050e19c
--- /dev/null
+++ b/src/pages/ProductDetailsPage/ProductDetailsPage.tsx
@@ -0,0 +1,295 @@
+import { useNavigate, useParams } from 'react-router-dom';
+import { fetchProduct, fetchProductsItem } from '../../utils/fetch';
+import { useEffect, useState } from 'react';
+import { Product, Products, SortType } from '../../utils/types';
+import styles from './ProductDetailsPage.module.scss';
+import { Loader } from '../../components/Loader';
+import { Breadcrumbs } from '../../components/Breadcrumbs';
+import { GoBack } from '../../components/GoBack';
+import { Title } from '../../components/Title';
+import classNames from 'classnames';
+import { colors } from '../../utils/colors';
+import { ToBuyButton } from '../../components/ToBuyButton';
+import { AddToFavourites } from '../../components/AddToFavourites';
+import { AboutProduct } from '../../components/AboutProduct';
+import { TechSpecs } from '../../components/TechSpecs';
+import { ProductSlider } from '../../components/ProductSlider';
+import { useTheme } from '../../context/ThemeContext';
+import blackArrow from '../../img/icons/arrow_left_black.png';
+import whiteArrow from '../../img/icons/arrow_left_white.png';
+import { BASE_URL } from '../../utils/constants';
+
+export const ProductDetailsPage = () => {
+ // #region state
+ const [isLoading, setIsLoading] = useState(false);
+ const [displayedProduct, setDisplayedProduct] = useState(
+ null,
+ );
+ const [displayedProductInCategory, setDisplayedProductInCategory] =
+ useState(null);
+ const [errorMessage, setErrorMessage] = useState('');
+ const [displayedImageIndex, setDisplayedImageIndex] = useState(0);
+ const { category, productId } = useParams();
+ const navigate = useNavigate();
+ const { isDarkTheme } = useTheme();
+ // #endregion
+ // #region functions
+ const handleColorChange = (color: string) => {
+ if (category === 'accessories') {
+ const normalizedColor = color.split(' ').join('-');
+
+ navigate(
+ `${BASE_URL}/${category}/${displayedProduct?.namespaceId + '-' + displayedProduct?.capacity + '-' + normalizedColor}`,
+ );
+ }
+
+ if (category === 'phones' || category === 'tablets') {
+ navigate(
+ `${BASE_URL}/${category}/${displayedProduct?.namespaceId + '-' + displayedProduct?.capacity.toLowerCase() + '-' + color}`,
+ );
+ }
+ };
+
+ const handleCapacityChange = (capacity: string) => {
+ if (category === 'accessories') {
+ const normalizedColor = displayedProduct?.color.split(' ').join('-');
+
+ navigate(
+ `${BASE_URL}/${category}/${displayedProduct?.namespaceId + '-' + capacity.toLowerCase() + '-' + normalizedColor}`,
+ );
+ }
+
+ if (category === 'phones' || category === 'tablets') {
+ navigate(
+ `${BASE_URL}/${category}/${displayedProduct?.namespaceId + '-' + capacity.toLowerCase() + '-' + displayedProduct?.color}`,
+ );
+ }
+ };
+
+ // #endregion
+ useEffect(() => {
+ if (category && productId) {
+ setIsLoading(true);
+
+ Promise.all([
+ fetchProduct(category, productId),
+ fetchProductsItem(productId),
+ ])
+ .then(([productResult, productItemResult]) => {
+ if (productResult) {
+ setDisplayedProduct(productResult);
+ } else {
+ setErrorMessage('Product was not found');
+ }
+
+ if (productItemResult) {
+ setDisplayedProductInCategory(productItemResult);
+ } else {
+ setErrorMessage('Product was not found');
+ }
+ })
+ .catch(() => {
+ setErrorMessage('Something went wrong');
+ })
+ .finally(() => {
+ setIsLoading(false);
+ });
+ }
+ }, [productId, category]);
+
+ return (
+
+ {isLoading &&
}
+
+ {errorMessage && (
+
+
+ {errorMessage}
+
+
+
+ )}
+ {!isLoading && !errorMessage && displayedProduct && category && (
+ <>
+
+
+
+
+
Back
+
+
+
{displayedProduct.name}
+
+
+
+
+
+
+
+ {displayedProduct.images.map((image, index) => {
+ return (
+
{
+ setDisplayedImageIndex(index);
+ }}
+ />
+ );
+ })}
+
+
+
+
+
+
+ Available colors
+
+
+ {displayedProduct?.colorsAvailable.map(color => {
+ return (
+
+ handleColorChange(color)}
+ />
+
+ );
+ })}
+
+
+
+
+
+ Select capacity
+
+
+ {displayedProduct?.capacityAvailable.map(
+ (capacity: string) => {
+ return (
+
+ );
+ },
+ )}
+
+
+
+
+
+
{`$${displayedProduct.priceRegular}`}
+
{`$${displayedProduct.priceDiscount}`}
+
+
+ {displayedProductInCategory && (
+ <>
+
+
+ >
+ )}
+
+
+
+
+ Screen
+
+
+ {displayedProduct.screen}
+
+
+
+
+ Resolution
+
+
+ {displayedProduct.resolution}
+
+
+
+
+ Processor
+
+
+ {displayedProduct.processor}
+
+
+
+
+ RAM
+
+
+ {displayedProduct.ram}
+
+
+
+
+
+
+
+ >
+ )}
+
+ );
+};
diff --git a/src/pages/ProductDetailsPage/index.js b/src/pages/ProductDetailsPage/index.js
new file mode 100644
index 0000000000..6615089e5e
--- /dev/null
+++ b/src/pages/ProductDetailsPage/index.js
@@ -0,0 +1 @@
+export * from './ProductDetailsPage';
diff --git a/src/react-app-env.d.ts b/src/react-app-env.d.ts
new file mode 100644
index 0000000000..30da896298
--- /dev/null
+++ b/src/react-app-env.d.ts
@@ -0,0 +1 @@
+// /
diff --git a/src/utils/colors.tsx b/src/utils/colors.tsx
new file mode 100644
index 0000000000..db99a1afea
--- /dev/null
+++ b/src/utils/colors.tsx
@@ -0,0 +1,27 @@
+export type Colors = {
+ [key: string]: string;
+};
+export const colors: Colors = {
+ black: '#4C4C4C',
+ spaceblack: '#565C6A',
+ green: '#5b7E65',
+ yellow: '#EED96C',
+ white: '#F0F0F0',
+ purple: '#CACDF5',
+ red: '#B1081B',
+ gold: '#F4E4D1',
+ sierrablue: '#AAC6E3',
+ graphite: '#96959D',
+ silver: '#AAAAAF',
+ spacegray: '#B6ADA7',
+ 'space gray': '#B6ADA7',
+ blue: '#6B89B3',
+ 'sky blue': '#7CACD0',
+ 'rose gold': '#FDD8D3',
+ rosegold: '#FDD8D3',
+ starlight: '#E8E0D5',
+ pink: '#FD889A',
+ midnightgreen: '#626B64',
+ midnight: '#31373F',
+ coral: '#F19584',
+};
diff --git a/src/utils/constants.tsx b/src/utils/constants.tsx
new file mode 100644
index 0000000000..eb25701213
--- /dev/null
+++ b/src/utils/constants.tsx
@@ -0,0 +1,2 @@
+export const BASE_URL =
+ window.location.hostname === 'localhost' ? '' : '/react_phone-catalog';
diff --git a/src/utils/fetch.tsx b/src/utils/fetch.tsx
new file mode 100644
index 0000000000..848360c5e4
--- /dev/null
+++ b/src/utils/fetch.tsx
@@ -0,0 +1,74 @@
+import { BASE_URL } from './constants';
+import { Product, Products, SortType } from './types';
+
+export const fetchProducts = (
+ category: string,
+ sortBy?: SortType,
+): Promise => {
+ return fetch(`${BASE_URL}/api/products.json`)
+ .then(response => response.json())
+ .then(data => {
+ let filteredProducts: Products[] = data.filter(
+ (product: Products) => product.category === category,
+ );
+
+ switch (sortBy) {
+ case SortType.newest:
+ filteredProducts.sort((a, b) => b.year - a.year);
+ break;
+ case SortType.hotPrice:
+ filteredProducts.sort(
+ (a, b) => b.fullPrice - b.price - (a.fullPrice - a.price),
+ );
+ break;
+ case SortType.alpha:
+ filteredProducts.sort((a, b) => a.name.localeCompare(b.name));
+ break;
+ case SortType.cheapest:
+ filteredProducts.sort((a, b) => a.price - b.price);
+ break;
+ case SortType.random:
+ const randomItems = new Set();
+
+ while (randomItems.size < 10) {
+ const randomIndex = Math.floor(
+ Math.random() * filteredProducts.length,
+ );
+
+ randomItems.add(filteredProducts[randomIndex]);
+ }
+
+ filteredProducts = Array.from(randomItems) as Products[];
+ break;
+ default:
+ break;
+ }
+
+ return filteredProducts;
+ });
+};
+
+export const fetchProductsItem = (
+ itemId: string,
+): Promise => {
+ return fetch(`${BASE_URL}/api/products.json`)
+ .then(response => response.json())
+ .then((data: Products[]) => {
+ return data.find(item => item.itemId === itemId);
+ });
+};
+
+export const fetchProduct = (
+ category: string,
+ productId: string,
+): Promise => {
+ return fetch(`${BASE_URL}/api/${category}.json`)
+ .then(response => response.json())
+ .then(data => {
+ const product = data.find((item: Product) => {
+ return item.id === productId;
+ });
+
+ return product;
+ });
+};
diff --git a/src/utils/functions.tsx b/src/utils/functions.tsx
new file mode 100644
index 0000000000..dbf77f8d0e
--- /dev/null
+++ b/src/utils/functions.tsx
@@ -0,0 +1,29 @@
+import { CartItem } from './types';
+
+export function findMaxId(cart: CartItem[] | []) {
+ if (cart.length === 0) {
+ return 1;
+ }
+
+ return Math.max(...cart.map((item: CartItem) => item.id)) + 1;
+}
+
+export function calculateTotalPrice(cart: CartItem[]) {
+ let total = 0;
+
+ cart.forEach(item => {
+ total += item.quantity * item.product.price;
+ });
+
+ return total;
+}
+
+export function calculateTotalAmount(cart: CartItem[]) {
+ let total = 0;
+
+ cart.forEach(item => {
+ total += item.quantity;
+ });
+
+ return total;
+}
diff --git a/src/utils/mixins.scss b/src/utils/mixins.scss
new file mode 100644
index 0000000000..d1e5d53250
--- /dev/null
+++ b/src/utils/mixins.scss
@@ -0,0 +1,51 @@
+@mixin on-tablet {
+ @media (min-width: 640px) {
+ & {
+ @content;
+ }
+ }
+}
+
+@mixin on-media-screen {
+ @media (min-width: 768px) {
+ & {
+ @content;
+ }
+ }
+}
+
+@mixin on-desktop {
+ @media (min-width: 1200px) {
+ & {
+ @content;
+ }
+ }
+}
+
+@mixin nav-link-styles {
+ text-decoration: none;
+ text-transform: uppercase;
+ color: var(--color-grey);
+ font-weight: 800;
+ line-height: 11px;
+}
+
+@mixin buttons-text {
+ color: #fff;
+ font-weight: 600;
+ font-size: 14px;
+ line-height: 21px;
+ font-family: inherit;
+}
+
+@mixin body-text {
+ font-weight: 400;
+ font-size: 14px;
+ line-height: 21px;
+}
+
+@mixin small-text {
+ font-weight: 600;
+ font-size: 12px;
+ line-height: 15px;
+}
diff --git a/src/utils/types.tsx b/src/utils/types.tsx
new file mode 100644
index 0000000000..52a7418b50
--- /dev/null
+++ b/src/utils/types.tsx
@@ -0,0 +1,77 @@
+export enum IconType {
+ menu = 'menu',
+ favourites = 'favourites',
+ cart = 'cart',
+ close = 'close',
+}
+
+export enum ArrowType {
+ up = 'up',
+ down = 'down',
+ right = 'right',
+ left = 'left',
+}
+
+export enum SortType {
+ withoutSort = 'withoutSort',
+ newest = 'year',
+ hotPrice = 'hotPrice',
+ alpha = 'title',
+ cheapest = 'price',
+ random = 'random',
+}
+
+export enum ProductCategory {
+ phones = 'phones',
+ tablets = 'tablets',
+ accessories = 'accessories',
+ products = 'products',
+}
+
+export type Products = {
+ id: number;
+ category: string;
+ itemId: string;
+ name: string;
+ fullPrice: number;
+ price: number;
+ screen: string;
+ capacity: string;
+ color: string;
+ ram: string;
+ year: number;
+ image: string;
+};
+
+export type ProductDescription = {
+ title: string;
+ text: string[];
+};
+
+export type Product = {
+ id: string;
+ category: string;
+ namespaceId: string;
+ name: string;
+ capacityAvailable: string[];
+ capacity: string;
+ priceRegular: number;
+ priceDiscount: number;
+ colorsAvailable: string[];
+ color: string;
+ images: string[];
+ description: ProductDescription[];
+ screen: string;
+ resolution: string;
+ processor: string;
+ ram: string;
+ camera: string;
+ zoom: string;
+ cell: string[];
+};
+
+export type CartItem = {
+ id: number;
+ quantity: number;
+ product: Products;
+};
diff --git a/src/utils/variables.scss b/src/utils/variables.scss
new file mode 100644
index 0000000000..86734c1ee8
--- /dev/null
+++ b/src/utils/variables.scss
@@ -0,0 +1,81 @@
+:root {
+ --color-white: #fff;
+ --color-black: #0f0f11;
+ --color-titles: #0f0f11;
+ --color-grey: #89939a;
+ --color-grey-second: #b4bdc3;
+ --color-grey-third: #e2e6e9;
+ --color-page-background: #fafbfc;
+ --color-accent: #4219d0;
+ --color-accent-button-border: #4219d0;
+ --color-accent-button-hover: #4219d0;
+ --color-accent-button-selected: #fafbfc;
+ --color-accent-button-border-selected: #e2e6e9;
+ --color-button-selected-text: #4219d0;
+ --color-card-background: #fff;
+ --color-card-border: #e2e6e9;
+ --color-header-footer: #fff;
+ --color-button: #fafbfc;
+ --color-button-border: #b4bdc3;
+ --color-button-hover: #fafbfc;
+ --color-button-border-hover: #0f0f11;
+ --color-button-disabled: #fafbfc;
+ --color-button-border-disabled: #e2e6e9;
+ --color-footer-links: #89939a;
+ --color-dropdownfield-background: #fafbfc;
+ --color-dropdown-background: #fff;
+ --color-dropdown-border: #e2e6e9;
+ --color-dropdown-hover: #fafbfc;
+ --color-dropdown-hover-text: #0f0f11;
+ --color-pagination-background: #fff;
+ --color-pagination-border: #e2e6e9;
+ --color-pagination-background-hover: #fff;
+ --color-pagination-border-hover: #0f0f11;
+ --color-pagination-background-selected: #0f0f11;
+ --color-pagination-border-selected: #e2e6e9;
+}
+
+body.dark-theme {
+ --color-white: #0f1121;
+ --color-black: #f1f2f9;
+ --color-page-background: #0f1121;
+ --color-titles: #f1f2f9;
+ --color-grey: #75767f;
+ --color-grey-second: #4a4d58;
+ --color-grey-third: #3b3e4a;
+ --color-accent: #905bff;
+ --color-accent-button-border: #905bff;
+ --color-accent-button-hover: #a378ff;
+ --color-accent-button-selected: #323542;
+ --color-accent-button-border-selected: #323542;
+ --color-button-selected-text: #f1f2f9;
+ --color-button: #323542;
+ --color-button-border: #323542;
+ --color-button-hover: #4a4d58;
+ --color-button-border-hover: #4a4d58;
+ --color-button-disabled: #0f1121;
+ --color-button-border-disabled: #3b3e4a;
+ --color-card-background: #161827;
+ --color-card-border: #161827;
+ --color-header-footer: #0f1121;
+ --color-footer-links: #f1f2f9;
+ --color-dropdownfield-background: #323542;
+ --color-dropdown-background: #0f1121;
+ --color-dropdown-border: #3b3e4a;
+ --color-dropdown-hover: #323542;
+ --color-dropdown-hover-text: #f1f2f9;
+ --color-pagination-background: #161827;
+ --color-pagination-border: #161827;
+ --color-pagination-background-hover: #3b3e4a;
+ --color-pagination-border-hover: #3b3e4a;
+ --color-pagination-background-selected: #905bff;
+ --color-pagination-border-selected: #905bff;
+}
+
+// elements height
+$header-height-mobile: 48px;
+$header-height-tablet: 48px;
+$header-height-desktop: 64px;
+$footer-height-mobile: 257px;
+$footer-height-tablet: 96px;
+$footer-height-desktop: 96px;
diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts
deleted file mode 100644
index 11f02fe2a0..0000000000
--- a/src/vite-env.d.ts
+++ /dev/null
@@ -1 +0,0 @@
-///