diff --git a/.eslintrc.cjs b/.eslintrc.cjs
index b51149cf57..4f8c7063b6 100644
--- a/.eslintrc.cjs
+++ b/.eslintrc.cjs
@@ -47,7 +47,7 @@ module.exports = {
ignoreComments: true,
}],
'no-redeclare': [2, { builtinGlobals: true }],
- 'no-console': 2,
+ 'no-console': "off",
'operator-linebreak': 0,
'brace-style': [2, '1tbs'],
'arrow-body-style': 0,
diff --git a/index.html b/index.html
index 095fb3a453..3d3171bfa3 100644
--- a/index.html
+++ b/index.html
@@ -2,11 +2,13 @@
+
- Vite + React + TS
+ Phone catalog
+
-
+
diff --git a/package-lock.json b/package-lock.json
index 836b9e63b4..3eb621f000 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -11,16 +11,18 @@
"license": "GPL-3.0",
"dependencies": {
"@fortawesome/fontawesome-free": "^6.5.2",
+ "@reduxjs/toolkit": "^2.3.0",
"bulma": "^1.0.1",
"classnames": "^2.5.1",
"react": "^18.3.1",
"react-dom": "^18.3.1",
+ "react-redux": "^9.1.2",
"react-router-dom": "^6.25.1",
"react-transition-group": "^4.4.5"
},
"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",
@@ -29,7 +31,6 @@
"@types/react-transition-group": "^4.4.10",
"@typescript-eslint/parser": "^7.16.0",
"@vitejs/plugin-react": "^4.3.1",
- "cypress": "^13.13.0",
"eslint": "^8.57.0",
"eslint-config-airbnb-typescript": "^18.0.0",
"eslint-config-prettier": "^9.1.0",
@@ -39,7 +40,7 @@
"eslint-plugin-prettier": "^5.1.3",
"eslint-plugin-react": "^7.34.4",
"eslint-plugin-react-hooks": "^4.6.2",
- "gh-pages": "^6.1.1",
+ "gh-pages": "^6.2.0",
"mochawesome": "^7.1.3",
"mochawesome-merge": "^4.3.0",
"mochawesome-report-generator": "^6.2.0",
@@ -430,6 +431,7 @@
"integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==",
"dev": true,
"optional": true,
+ "peer": true,
"engines": {
"node": ">=0.1.90"
}
@@ -543,6 +545,7 @@
"resolved": "https://registry.npmjs.org/@cypress/request/-/request-3.0.1.tgz",
"integrity": "sha512-TWivJlJi8ZDx2wGOw1dbLuHJKUYX7bWySw377nlnGOW3hP9/MUKIsEdXT/YngWxVdgNCHRBmFlBipE+5/2ZZlQ==",
"dev": true,
+ "peer": true,
"dependencies": {
"aws-sign2": "~0.7.0",
"aws4": "^1.8.0",
@@ -572,6 +575,7 @@
"resolved": "https://registry.npmjs.org/@cypress/xvfb/-/xvfb-1.2.4.tgz",
"integrity": "sha512-skbBzPggOVYCbnGgV+0dmBdW/s77ZkAOXIC1knS8NagwDjBrNC1LuXtQJeiN6l+m7lzmHtaoUw/ctJKdqkG57Q==",
"dev": true,
+ "peer": true,
"dependencies": {
"debug": "^3.1.0",
"lodash.once": "^4.1.1"
@@ -582,6 +586,7 @@
"resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
"integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
"dev": true,
+ "peer": true,
"dependencies": {
"ms": "^2.1.1"
}
@@ -1184,10 +1189,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 +1881,30 @@
"url": "https://opencollective.com/unts"
}
},
+ "node_modules/@reduxjs/toolkit": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.3.0.tgz",
+ "integrity": "sha512-WC7Yd6cNGfHx8zf+iu+Q1UPTfEcXhQ+ATi7CV1hlrSAaQBdlPzg7Ww/wJHNQem7qG9rxmWoFCDCPubSvFObGzA==",
+ "license": "MIT",
+ "dependencies": {
+ "immer": "^10.0.3",
+ "redux": "^5.0.1",
+ "redux-thunk": "^3.1.0",
+ "reselect": "^5.1.0"
+ },
+ "peerDependencies": {
+ "react": "^16.9.0 || ^17.0.0 || ^18",
+ "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0"
+ },
+ "peerDependenciesMeta": {
+ "react": {
+ "optional": true
+ },
+ "react-redux": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@remix-run/router": {
"version": "1.18.0",
"resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.18.0.tgz",
@@ -2216,13 +2246,13 @@
"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
+ "devOptional": true
},
"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,
+ "devOptional": true,
"dependencies": {
"@types/prop-types": "*",
"csstype": "^3.0.2"
@@ -2250,13 +2280,21 @@
"version": "8.1.1",
"resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.1.tgz",
"integrity": "sha512-0kSuKjAS0TrGLJ0M/+8MaFkGsQhZpB6pxOmvS3K8FYI72K//YmdfoW9X2qPsAKh1mkwxGD5zib9s1FIFed6E8g==",
- "dev": true
+ "dev": true,
+ "peer": true
},
"node_modules/@types/sizzle": {
"version": "2.3.8",
"resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.8.tgz",
"integrity": "sha512-0vWLNK2D5MT9dg0iOo8GlKguPAU02QjmZitPEsXRuJXU/OGIOt9vT9Fc26wtYuavLxtO45v9PGleoL9Z0k1LHg==",
- "dev": true
+ "dev": true,
+ "peer": true
+ },
+ "node_modules/@types/use-sync-external-store": {
+ "version": "0.0.3",
+ "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz",
+ "integrity": "sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA==",
+ "license": "MIT"
},
"node_modules/@types/yauzl": {
"version": "2.10.3",
@@ -2264,6 +2302,7 @@
"integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==",
"dev": true,
"optional": true,
+ "peer": true,
"dependencies": {
"@types/node": "*"
}
@@ -2507,6 +2546,7 @@
"resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz",
"integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==",
"dev": true,
+ "peer": true,
"dependencies": {
"clean-stack": "^2.0.0",
"indent-string": "^4.0.0"
@@ -2536,6 +2576,7 @@
"resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz",
"integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==",
"dev": true,
+ "peer": true,
"engines": {
"node": ">=6"
}
@@ -2545,6 +2586,7 @@
"resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz",
"integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==",
"dev": true,
+ "peer": true,
"dependencies": {
"type-fest": "^0.21.3"
},
@@ -2607,7 +2649,8 @@
"type": "consulting",
"url": "https://feross.org/support"
}
- ]
+ ],
+ "peer": true
},
"node_modules/argparse": {
"version": "2.0.1",
@@ -2669,15 +2712,6 @@
"node": ">=8"
}
},
- "node_modules/array-uniq": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz",
- "integrity": "sha512-MNha4BWQ6JbwhFhj03YK552f7cb3AzoE8SzeljgChvL1dl3IcvggXVz1DilzySZkCja+CXuZbdW7yATchWn8/Q==",
- "dev": true,
- "engines": {
- "node": ">=0.10.0"
- }
- },
"node_modules/array.prototype.findlast": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz",
@@ -2819,6 +2853,7 @@
"resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz",
"integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==",
"dev": true,
+ "peer": true,
"dependencies": {
"safer-buffer": "~2.1.0"
}
@@ -2828,6 +2863,7 @@
"resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz",
"integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==",
"dev": true,
+ "peer": true,
"engines": {
"node": ">=0.8"
}
@@ -2857,7 +2893,8 @@
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
- "dev": true
+ "dev": true,
+ "peer": true
},
"node_modules/at-least-node": {
"version": "1.0.0",
@@ -2888,6 +2925,7 @@
"resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz",
"integrity": "sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==",
"dev": true,
+ "peer": true,
"engines": {
"node": "*"
}
@@ -2896,7 +2934,8 @@
"version": "1.13.0",
"resolved": "https://registry.npmjs.org/aws4/-/aws4-1.13.0.tgz",
"integrity": "sha512-3AungXC4I8kKsS9PuS4JH2nc+0bVY/mjgrephHTIi8fpEeGsTHBUJeosp0Wc1myYMElmD0B3Oc4XL/HVJ4PV2g==",
- "dev": true
+ "dev": true,
+ "peer": true
},
"node_modules/axe-core": {
"version": "4.9.1",
@@ -2940,13 +2979,15 @@
"type": "consulting",
"url": "https://feross.org/support"
}
- ]
+ ],
+ "peer": true
},
"node_modules/bcrypt-pbkdf": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz",
"integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==",
"dev": true,
+ "peer": true,
"dependencies": {
"tweetnacl": "^0.14.3"
}
@@ -2974,13 +3015,15 @@
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/blob-util/-/blob-util-2.0.2.tgz",
"integrity": "sha512-T7JQa+zsXXEa6/8ZhHcQEW1UFfVM49Ts65uBkFL6fz2QmrElqmbajIDJvuA0tEhRe5eIjpV9ZF+0RfZR9voJFQ==",
- "dev": true
+ "dev": true,
+ "peer": true
},
"node_modules/bluebird": {
"version": "3.7.2",
"resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz",
"integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==",
- "dev": true
+ "dev": true,
+ "peer": true
},
"node_modules/brace-expansion": {
"version": "2.0.1",
@@ -3061,6 +3104,7 @@
"url": "https://feross.org/support"
}
],
+ "peer": true,
"dependencies": {
"base64-js": "^1.3.1",
"ieee754": "^1.1.13"
@@ -3071,6 +3115,7 @@
"resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz",
"integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==",
"dev": true,
+ "peer": true,
"engines": {
"node": "*"
}
@@ -3085,6 +3130,7 @@
"resolved": "https://registry.npmjs.org/cachedir/-/cachedir-2.4.0.tgz",
"integrity": "sha512-9EtFOZR8g22CL7BWjJ9BUx1+A/djkofnyW3aOXZORNW2kxoUpx2h+uN2cOqwPmFhnpVmxg+KW2OjOSgChTEvsQ==",
"dev": true,
+ "peer": true,
"engines": {
"node": ">=6"
}
@@ -3186,7 +3232,8 @@
"version": "0.12.0",
"resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz",
"integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==",
- "dev": true
+ "dev": true,
+ "peer": true
},
"node_modules/chalk": {
"version": "2.4.2",
@@ -3207,6 +3254,7 @@
"resolved": "https://registry.npmjs.org/check-more-types/-/check-more-types-2.24.0.tgz",
"integrity": "sha512-Pj779qHxV2tuapviy1bSZNEL1maXr13bPYpsvSDB68HlYcYuhlDrmGd63i0JHMCLKzc7rUSNIrpdJlhVlNwrxA==",
"dev": true,
+ "peer": true,
"engines": {
"node": ">= 0.8.0"
}
@@ -3258,6 +3306,7 @@
"url": "https://github.com/sponsors/sibiraj-s"
}
],
+ "peer": true,
"engines": {
"node": ">=8"
}
@@ -3272,6 +3321,7 @@
"resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz",
"integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==",
"dev": true,
+ "peer": true,
"engines": {
"node": ">=6"
}
@@ -3281,6 +3331,7 @@
"resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz",
"integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==",
"dev": true,
+ "peer": true,
"dependencies": {
"restore-cursor": "^3.1.0"
},
@@ -3293,6 +3344,7 @@
"resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.5.tgz",
"integrity": "sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==",
"dev": true,
+ "peer": true,
"dependencies": {
"string-width": "^4.2.0"
},
@@ -3308,6 +3360,7 @@
"resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-2.1.0.tgz",
"integrity": "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==",
"dev": true,
+ "peer": true,
"dependencies": {
"slice-ansi": "^3.0.0",
"string-width": "^4.2.0"
@@ -3356,13 +3409,15 @@
"version": "2.0.20",
"resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz",
"integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==",
- "dev": true
+ "dev": true,
+ "peer": true
},
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"dev": true,
+ "peer": true,
"dependencies": {
"delayed-stream": "~1.0.0"
},
@@ -3384,6 +3439,7 @@
"resolved": "https://registry.npmjs.org/common-tags/-/common-tags-1.8.2.tgz",
"integrity": "sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==",
"dev": true,
+ "peer": true,
"engines": {
"node": ">=4.0.0"
}
@@ -3416,7 +3472,8 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
"integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==",
- "dev": true
+ "dev": true,
+ "peer": true
},
"node_modules/cosmiconfig": {
"version": "9.0.0",
@@ -3521,6 +3578,7 @@
"integrity": "sha512-ou/MQUDq4tcDJI2FsPaod2FZpex4kpIK43JJlcBgWrX8WX7R/05ZxGTuxedOuZBfxjZxja+fbijZGyxiLP6CFA==",
"dev": true,
"hasInstallScript": true,
+ "peer": true,
"dependencies": {
"@cypress/request": "^3.0.0",
"@cypress/xvfb": "^1.2.4",
@@ -3577,6 +3635,7 @@
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"dev": true,
+ "peer": true,
"dependencies": {
"color-convert": "^2.0.1"
},
@@ -3592,6 +3651,7 @@
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
"dev": true,
+ "peer": true,
"dependencies": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
@@ -3608,6 +3668,7 @@
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
"dev": true,
+ "peer": true,
"dependencies": {
"has-flag": "^4.0.0"
},
@@ -3620,6 +3681,7 @@
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"dev": true,
+ "peer": true,
"dependencies": {
"color-name": "~1.1.4"
},
@@ -3631,13 +3693,15 @@
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
- "dev": true
+ "dev": true,
+ "peer": true
},
"node_modules/cypress/node_modules/commander": {
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz",
"integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==",
"dev": true,
+ "peer": true,
"engines": {
"node": ">= 6"
}
@@ -3647,6 +3711,7 @@
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
"dev": true,
+ "peer": true,
"engines": {
"node": ">=8"
}
@@ -3656,6 +3721,7 @@
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
"integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
"dev": true,
+ "peer": true,
"dependencies": {
"has-flag": "^4.0.0"
},
@@ -3677,6 +3743,7 @@
"resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz",
"integrity": "sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==",
"dev": true,
+ "peer": true,
"dependencies": {
"assert-plus": "^1.0.0"
},
@@ -3748,7 +3815,8 @@
"version": "1.11.11",
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.11.tgz",
"integrity": "sha512-okzr3f11N6WuqYtZSvm+F776mB41wRZMhKP+hc34YdW+KmtYYK9iqvHSwo2k9FEH3fhGXvOPV6yz2IcSrfRUDg==",
- "dev": true
+ "dev": true,
+ "peer": true
},
"node_modules/debug": {
"version": "4.3.5",
@@ -3894,6 +3962,7 @@
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"dev": true,
+ "peer": true,
"engines": {
"node": ">=0.4.0"
}
@@ -3960,6 +4029,7 @@
"resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz",
"integrity": "sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==",
"dev": true,
+ "peer": true,
"dependencies": {
"jsbn": "~0.1.0",
"safer-buffer": "^2.1.0"
@@ -3997,6 +4067,7 @@
"resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.4.1.tgz",
"integrity": "sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==",
"dev": true,
+ "peer": true,
"dependencies": {
"ansi-colors": "^4.1.1",
"strip-ansi": "^6.0.1"
@@ -4949,13 +5020,15 @@
"version": "6.4.7",
"resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-6.4.7.tgz",
"integrity": "sha512-tYUSVOGeQPKt/eC1ABfhHy5Xd96N3oIijJvN3O9+TsC28T5V9yX9oEfEK5faP0EFSNVOG97qtAS68GBrQB2hDg==",
- "dev": true
+ "dev": true,
+ "peer": true
},
"node_modules/execa": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/execa/-/execa-4.1.0.tgz",
"integrity": "sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA==",
"dev": true,
+ "peer": true,
"dependencies": {
"cross-spawn": "^7.0.0",
"get-stream": "^5.0.0",
@@ -4979,6 +5052,7 @@
"resolved": "https://registry.npmjs.org/executable/-/executable-4.1.1.tgz",
"integrity": "sha512-8iA79xD3uAch729dUG8xaaBBFGaEa0wdD2VkYLFHwlqosEj/jT66AzcreRDSgV7ehnNLBW2WR5jIXwGKjVdTLg==",
"dev": true,
+ "peer": true,
"dependencies": {
"pify": "^2.2.0"
},
@@ -4990,13 +5064,15 @@
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
"integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
- "dev": true
+ "dev": true,
+ "peer": true
},
"node_modules/extract-zip": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz",
"integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==",
"dev": true,
+ "peer": true,
"dependencies": {
"debug": "^4.1.1",
"get-stream": "^5.1.0",
@@ -5019,7 +5095,8 @@
"dev": true,
"engines": [
"node >=0.6.0"
- ]
+ ],
+ "peer": true
},
"node_modules/fast-deep-equal": {
"version": "3.1.3",
@@ -5102,6 +5179,7 @@
"resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz",
"integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==",
"dev": true,
+ "peer": true,
"dependencies": {
"pend": "~1.2.0"
}
@@ -5111,6 +5189,7 @@
"resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz",
"integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==",
"dev": true,
+ "peer": true,
"dependencies": {
"escape-string-regexp": "^1.0.5"
},
@@ -5248,6 +5327,7 @@
"resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz",
"integrity": "sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==",
"dev": true,
+ "peer": true,
"engines": {
"node": "*"
}
@@ -5257,6 +5337,7 @@
"resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz",
"integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==",
"dev": true,
+ "peer": true,
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.6",
@@ -5397,6 +5478,7 @@
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz",
"integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==",
"dev": true,
+ "peer": true,
"dependencies": {
"pump": "^3.0.0"
},
@@ -5429,6 +5511,7 @@
"resolved": "https://registry.npmjs.org/getos/-/getos-3.2.1.tgz",
"integrity": "sha512-U56CfOK17OKgTVqozZjUKNdkfEv6jk5WISBJ8SHoagjE6L69zOwl3Z+O8myjY9MEW3i2HPWQBt/LTbCgcC973Q==",
"dev": true,
+ "peer": true,
"dependencies": {
"async": "^3.2.0"
}
@@ -5438,15 +5521,17 @@
"resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz",
"integrity": "sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==",
"dev": true,
+ "peer": true,
"dependencies": {
"assert-plus": "^1.0.0"
}
},
"node_modules/gh-pages": {
- "version": "6.1.1",
- "resolved": "https://registry.npmjs.org/gh-pages/-/gh-pages-6.1.1.tgz",
- "integrity": "sha512-upnohfjBwN5hBP9w2dPE7HO5JJTHzSGMV1JrLrHvNuqmjoYHg6TBrCcnEoorjG/e0ejbuvnwyKMdTyM40PEByw==",
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/gh-pages/-/gh-pages-6.2.0.tgz",
+ "integrity": "sha512-HMXJ8th9u5wRXaZCnLcs/d3oVvCHiZkaP5KQExQljYGwJjQbSPyTdHe/Gc1IvYUR/rWiZLxNobIqfoMHKTKjHQ==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"async": "^3.2.4",
"commander": "^11.0.0",
@@ -5454,7 +5539,7 @@
"filenamify": "^4.3.0",
"find-cache-dir": "^3.3.1",
"fs-extra": "^11.1.1",
- "globby": "^6.1.0"
+ "globby": "^11.1.0"
},
"bin": {
"gh-pages": "bin/gh-pages.js",
@@ -5464,28 +5549,6 @@
"node": ">=10"
}
},
- "node_modules/gh-pages/node_modules/array-union": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz",
- "integrity": "sha512-Dxr6QJj/RdU/hCaBjOfxW+q6lyuVE6JFWIrAUpuOOhoJJoQ99cUn3igRaHVB5P9WrgFVN0FfArM3x0cueOU8ng==",
- "dev": true,
- "dependencies": {
- "array-uniq": "^1.0.1"
- },
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/gh-pages/node_modules/brace-expansion": {
- "version": "1.1.11",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
- "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
- "dev": true,
- "dependencies": {
- "balanced-match": "^1.0.0",
- "concat-map": "0.0.1"
- }
- },
"node_modules/gh-pages/node_modules/commander": {
"version": "11.1.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz",
@@ -5509,55 +5572,6 @@
"node": ">=14.14"
}
},
- "node_modules/gh-pages/node_modules/glob": {
- "version": "7.2.3",
- "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
- "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
- "deprecated": "Glob versions prior to v9 are no longer supported",
- "dev": true,
- "dependencies": {
- "fs.realpath": "^1.0.0",
- "inflight": "^1.0.4",
- "inherits": "2",
- "minimatch": "^3.1.1",
- "once": "^1.3.0",
- "path-is-absolute": "^1.0.0"
- },
- "engines": {
- "node": "*"
- },
- "funding": {
- "url": "https://github.com/sponsors/isaacs"
- }
- },
- "node_modules/gh-pages/node_modules/globby": {
- "version": "6.1.0",
- "resolved": "https://registry.npmjs.org/globby/-/globby-6.1.0.tgz",
- "integrity": "sha512-KVbFv2TQtbzCoxAnfD6JcHZTYCzyliEaaeM/gH8qQdkKr5s0OP9scEgvdcngyk7AVdY6YVW/TJHd+lQ/Df3Daw==",
- "dev": true,
- "dependencies": {
- "array-union": "^1.0.1",
- "glob": "^7.0.3",
- "object-assign": "^4.0.1",
- "pify": "^2.0.0",
- "pinkie-promise": "^2.0.0"
- },
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/gh-pages/node_modules/minimatch": {
- "version": "3.1.2",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
- "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
- "dev": true,
- "dependencies": {
- "brace-expansion": "^1.1.7"
- },
- "engines": {
- "node": "*"
- }
- },
"node_modules/glob": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz",
@@ -5609,6 +5623,7 @@
"resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-3.0.1.tgz",
"integrity": "sha512-NBcGGFbBA9s1VzD41QXDG+3++t9Mn5t1FpLdhESY6oKY4gYTFpX4wO3sqGUa0Srjtbfj3szX0RnemmrVRUdULA==",
"dev": true,
+ "peer": true,
"dependencies": {
"ini": "2.0.0"
},
@@ -5889,6 +5904,7 @@
"resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.3.6.tgz",
"integrity": "sha512-3adrsD6zqo4GsTqtO7FyrejHNv+NgiIfAfv68+jVlFmSr9OGy7zrxONceFRLKvnnZA5jbxQBX1u9PpB6Wi32Gw==",
"dev": true,
+ "peer": true,
"dependencies": {
"assert-plus": "^1.0.0",
"jsprim": "^2.0.2",
@@ -5903,6 +5919,7 @@
"resolved": "https://registry.npmjs.org/human-signals/-/human-signals-1.1.1.tgz",
"integrity": "sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==",
"dev": true,
+ "peer": true,
"engines": {
"node": ">=8.12.0"
}
@@ -5925,7 +5942,8 @@
"type": "consulting",
"url": "https://feross.org/support"
}
- ]
+ ],
+ "peer": true
},
"node_modules/ignore": {
"version": "5.3.1",
@@ -5936,6 +5954,16 @@
"node": ">= 4"
}
},
+ "node_modules/immer": {
+ "version": "10.1.1",
+ "resolved": "https://registry.npmjs.org/immer/-/immer-10.1.1.tgz",
+ "integrity": "sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==",
+ "license": "MIT",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/immer"
+ }
+ },
"node_modules/immutable": {
"version": "4.3.6",
"resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.6.tgz",
@@ -5982,6 +6010,7 @@
"resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz",
"integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==",
"dev": true,
+ "peer": true,
"engines": {
"node": ">=8"
}
@@ -6008,6 +6037,7 @@
"resolved": "https://registry.npmjs.org/ini/-/ini-2.0.0.tgz",
"integrity": "sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==",
"dev": true,
+ "peer": true,
"engines": {
"node": ">=10"
}
@@ -6136,6 +6166,7 @@
"resolved": "https://registry.npmjs.org/is-ci/-/is-ci-3.0.1.tgz",
"integrity": "sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ==",
"dev": true,
+ "peer": true,
"dependencies": {
"ci-info": "^3.2.0"
},
@@ -6265,6 +6296,7 @@
"resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.4.0.tgz",
"integrity": "sha512-iwGqO3J21aaSkC7jWnHP/difazwS7SFeIqxv6wEtLU8Y5KlzFTjyqcSIT0d8s4+dDhKytsk9PJZ2BkS5eZwQRQ==",
"dev": true,
+ "peer": true,
"dependencies": {
"global-dirs": "^3.0.0",
"is-path-inside": "^3.0.2"
@@ -6400,6 +6432,7 @@
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
"integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==",
"dev": true,
+ "peer": true,
"engines": {
"node": ">=8"
},
@@ -6456,13 +6489,15 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz",
"integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==",
- "dev": true
+ "dev": true,
+ "peer": true
},
"node_modules/is-unicode-supported": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz",
"integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==",
"dev": true,
+ "peer": true,
"engines": {
"node": ">=10"
},
@@ -6538,7 +6573,8 @@
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz",
"integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==",
- "dev": true
+ "dev": true,
+ "peer": true
},
"node_modules/iterator.prototype": {
"version": "1.1.2",
@@ -6574,7 +6610,8 @@
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz",
"integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==",
- "dev": true
+ "dev": true,
+ "peer": true
},
"node_modules/jsesc": {
"version": "2.5.2",
@@ -6604,7 +6641,8 @@
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz",
"integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==",
- "dev": true
+ "dev": true,
+ "peer": true
},
"node_modules/json-schema-traverse": {
"version": "0.4.1",
@@ -6656,6 +6694,7 @@
"engines": [
"node >=0.6.0"
],
+ "peer": true,
"dependencies": {
"assert-plus": "1.0.0",
"extsprintf": "1.3.0",
@@ -6731,6 +6770,7 @@
"resolved": "https://registry.npmjs.org/lazy-ass/-/lazy-ass-1.6.0.tgz",
"integrity": "sha512-cc8oEVoctTvsFZ/Oje/kGnHbpWHYBe8IAJe4C0QNc3t8uM/0Y8+erSz/7Y1ALuXTEZTMvxXwO6YbX1ey3ujiZw==",
"dev": true,
+ "peer": true,
"engines": {
"node": "> 0.8"
}
@@ -6759,6 +6799,7 @@
"resolved": "https://registry.npmjs.org/listr2/-/listr2-3.14.0.tgz",
"integrity": "sha512-TyWI8G99GX9GjE54cJ+RrNMcIFBfwMPxc3XTFiAYGN4s10hWROGtOg7+O6u6LE3mNkyld7RSLE6nrKBvTfcs3g==",
"dev": true,
+ "peer": true,
"dependencies": {
"cli-truncate": "^2.1.0",
"colorette": "^2.0.16",
@@ -6800,7 +6841,8 @@
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
- "dev": true
+ "dev": true,
+ "peer": true
},
"node_modules/lodash.get": {
"version": "4.4.2",
@@ -6842,7 +6884,8 @@
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
"integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==",
- "dev": true
+ "dev": true,
+ "peer": true
},
"node_modules/lodash.truncate": {
"version": "4.4.2",
@@ -6855,6 +6898,7 @@
"resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz",
"integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==",
"dev": true,
+ "peer": true,
"dependencies": {
"chalk": "^4.1.0",
"is-unicode-supported": "^0.1.0"
@@ -6871,6 +6915,7 @@
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"dev": true,
+ "peer": true,
"dependencies": {
"color-convert": "^2.0.1"
},
@@ -6886,6 +6931,7 @@
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
"dev": true,
+ "peer": true,
"dependencies": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
@@ -6902,6 +6948,7 @@
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"dev": true,
+ "peer": true,
"dependencies": {
"color-name": "~1.1.4"
},
@@ -6913,13 +6960,15 @@
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
- "dev": true
+ "dev": true,
+ "peer": true
},
"node_modules/log-symbols/node_modules/has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
"dev": true,
+ "peer": true,
"engines": {
"node": ">=8"
}
@@ -6929,6 +6978,7 @@
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
"dev": true,
+ "peer": true,
"dependencies": {
"has-flag": "^4.0.0"
},
@@ -6941,6 +6991,7 @@
"resolved": "https://registry.npmjs.org/log-update/-/log-update-4.0.0.tgz",
"integrity": "sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg==",
"dev": true,
+ "peer": true,
"dependencies": {
"ansi-escapes": "^4.3.0",
"cli-cursor": "^3.1.0",
@@ -6959,6 +7010,7 @@
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"dev": true,
+ "peer": true,
"dependencies": {
"color-convert": "^2.0.1"
},
@@ -6974,6 +7026,7 @@
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"dev": true,
+ "peer": true,
"dependencies": {
"color-name": "~1.1.4"
},
@@ -6985,13 +7038,15 @@
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
- "dev": true
+ "dev": true,
+ "peer": true
},
"node_modules/log-update/node_modules/slice-ansi": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz",
"integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==",
"dev": true,
+ "peer": true,
"dependencies": {
"ansi-styles": "^4.0.0",
"astral-regex": "^2.0.0",
@@ -7009,6 +7064,7 @@
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
"integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
"dev": true,
+ "peer": true,
"dependencies": {
"ansi-styles": "^4.0.0",
"string-width": "^4.1.0",
@@ -7119,7 +7175,8 @@
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
"integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==",
- "dev": true
+ "dev": true,
+ "peer": true
},
"node_modules/merge2": {
"version": "1.4.1",
@@ -7148,6 +7205,7 @@
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"dev": true,
+ "peer": true,
"engines": {
"node": ">= 0.6"
}
@@ -7157,6 +7215,7 @@
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"dev": true,
+ "peer": true,
"dependencies": {
"mime-db": "1.52.0"
},
@@ -7169,6 +7228,7 @@
"resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz",
"integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==",
"dev": true,
+ "peer": true,
"engines": {
"node": ">=6"
}
@@ -7932,6 +7992,7 @@
"resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz",
"integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==",
"dev": true,
+ "peer": true,
"dependencies": {
"path-key": "^3.0.0"
},
@@ -8079,6 +8140,7 @@
"resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz",
"integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==",
"dev": true,
+ "peer": true,
"dependencies": {
"mimic-fn": "^2.1.0"
},
@@ -8148,7 +8210,8 @@
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/ospath/-/ospath-1.2.2.tgz",
"integrity": "sha512-o6E5qJV5zkAbIDNhGSIlyOhScKXgQrSRMilfph0clDfM0nEnBOlKlH4sWDmG95BW/CvwNz0vmm7dJVtU2KlMiA==",
- "dev": true
+ "dev": true,
+ "peer": true
},
"node_modules/p-finally": {
"version": "1.0.0",
@@ -8194,6 +8257,7 @@
"resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz",
"integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==",
"dev": true,
+ "peer": true,
"dependencies": {
"aggregate-error": "^3.0.0"
},
@@ -8304,13 +8368,15 @@
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz",
"integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==",
- "dev": true
+ "dev": true,
+ "peer": true
},
"node_modules/performance-now": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
"integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==",
- "dev": true
+ "dev": true,
+ "peer": true
},
"node_modules/picocolors": {
"version": "1.0.1",
@@ -8335,27 +8401,7 @@
"resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
"integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==",
"dev": true,
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/pinkie": {
- "version": "2.0.4",
- "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz",
- "integrity": "sha512-MnUuEycAemtSaeFSjXKW/aroV7akBbY+Sv+RkyqFjgAe73F+MR0TBWKBRDkmfWq/HiFmdavfZ1G7h4SPZXaCSg==",
- "dev": true,
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/pinkie-promise": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz",
- "integrity": "sha512-0Gni6D4UcLTbv9c57DfxDGdr41XfgUjqWZu492f0cIGr16zDU06BWP/RAEvOuo7CQ0CNjHaLlM59YJJFm3NWlw==",
- "dev": true,
- "dependencies": {
- "pinkie": "^2.0.0"
- },
+ "peer": true,
"engines": {
"node": ">=0.10.0"
}
@@ -8585,6 +8631,7 @@
"resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz",
"integrity": "sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==",
"dev": true,
+ "peer": true,
"engines": {
"node": ">=6"
},
@@ -8597,6 +8644,7 @@
"resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
"integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==",
"dev": true,
+ "peer": true,
"engines": {
"node": ">= 0.6.0"
}
@@ -8615,13 +8663,15 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.0.0.tgz",
"integrity": "sha512-F2JHgJQ1iqwnHDcQjVBsq3n/uoaFL+iPW/eAeL7kVxy/2RrWaN4WroKjjvbsoRtv0ftelNyC01bjRhn/bhcf4A==",
- "dev": true
+ "dev": true,
+ "peer": true
},
"node_modules/psl": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz",
"integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==",
- "dev": true
+ "dev": true,
+ "peer": true
},
"node_modules/pump": {
"version": "3.0.0",
@@ -8647,6 +8697,7 @@
"resolved": "https://registry.npmjs.org/qs/-/qs-6.10.4.tgz",
"integrity": "sha512-OQiU+C+Ds5qiH91qh/mg0w+8nwQuLjM4F4M/PbmhDOoYehPh+Fb0bDjtR1sOvy7YKxvj28Y/M0PhP5uVX0kB+g==",
"dev": true,
+ "peer": true,
"dependencies": {
"side-channel": "^1.0.4"
},
@@ -8661,7 +8712,8 @@
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz",
"integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==",
- "dev": true
+ "dev": true,
+ "peer": true
},
"node_modules/queue-microtask": {
"version": "1.2.3",
@@ -8734,6 +8786,29 @@
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
},
+ "node_modules/react-redux": {
+ "version": "9.1.2",
+ "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.1.2.tgz",
+ "integrity": "sha512-0OA4dhM1W48l3uzmv6B7TXPCGmokUU4p1M44DGN2/D9a1FjVPukVjER1PcPX97jIg6aUeLq1XJo1IpfbgULn0w==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/use-sync-external-store": "^0.0.3",
+ "use-sync-external-store": "^1.0.0"
+ },
+ "peerDependencies": {
+ "@types/react": "^18.2.25",
+ "react": "^18.0",
+ "redux": "^5.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "redux": {
+ "optional": true
+ }
+ }
+ },
"node_modules/react-refresh": {
"version": "0.14.2",
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz",
@@ -8893,6 +8968,21 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/redux": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
+ "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==",
+ "license": "MIT"
+ },
+ "node_modules/redux-thunk": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz",
+ "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==",
+ "license": "MIT",
+ "peerDependencies": {
+ "redux": "^5.0.0"
+ }
+ },
"node_modules/reflect.getprototypeof": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.6.tgz",
@@ -8942,6 +9032,7 @@
"resolved": "https://registry.npmjs.org/request-progress/-/request-progress-3.0.0.tgz",
"integrity": "sha512-MnWzEHHaxHO2iWiQuHrUPBi/1WeBf5PkxQqNyNvLl9VAYSdXkP8tQ3pBSeCPD+yw0v0Aq1zosWLz0BdeXpWwZg==",
"dev": true,
+ "peer": true,
"dependencies": {
"throttleit": "^1.0.0"
}
@@ -8974,7 +9065,14 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
"integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==",
- "dev": true
+ "dev": true,
+ "peer": true
+ },
+ "node_modules/reselect": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz",
+ "integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==",
+ "license": "MIT"
},
"node_modules/resolve": {
"version": "1.22.8",
@@ -9007,6 +9105,7 @@
"resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz",
"integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==",
"dev": true,
+ "peer": true,
"dependencies": {
"onetime": "^5.1.0",
"signal-exit": "^3.0.2"
@@ -9029,7 +9128,8 @@
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz",
"integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==",
- "dev": true
+ "dev": true,
+ "peer": true
},
"node_modules/rimraf": {
"version": "3.0.2",
@@ -9153,6 +9253,7 @@
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz",
"integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==",
"dev": true,
+ "peer": true,
"dependencies": {
"tslib": "^2.1.0"
}
@@ -9193,7 +9294,8 @@
"type": "consulting",
"url": "https://feross.org/support"
}
- ]
+ ],
+ "peer": true
},
"node_modules/safe-regex-test": {
"version": "1.0.3",
@@ -9216,7 +9318,8 @@
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
- "dev": true
+ "dev": true,
+ "peer": true
},
"node_modules/sass": {
"version": "1.77.8",
@@ -9411,6 +9514,7 @@
"resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz",
"integrity": "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==",
"dev": true,
+ "peer": true,
"dependencies": {
"ansi-styles": "^4.0.0",
"astral-regex": "^2.0.0",
@@ -9425,6 +9529,7 @@
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"dev": true,
+ "peer": true,
"dependencies": {
"color-convert": "^2.0.1"
},
@@ -9440,6 +9545,7 @@
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"dev": true,
+ "peer": true,
"dependencies": {
"color-name": "~1.1.4"
},
@@ -9451,7 +9557,8 @@
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
- "dev": true
+ "dev": true,
+ "peer": true
},
"node_modules/source-map-js": {
"version": "1.2.0",
@@ -9503,6 +9610,7 @@
"resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.18.0.tgz",
"integrity": "sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ==",
"dev": true,
+ "peer": true,
"dependencies": {
"asn1": "~0.2.3",
"assert-plus": "^1.0.0",
@@ -9685,6 +9793,7 @@
"resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz",
"integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==",
"dev": true,
+ "peer": true,
"engines": {
"node": ">=6"
}
@@ -10060,6 +10169,7 @@
"resolved": "https://registry.npmjs.org/throttleit/-/throttleit-1.0.1.tgz",
"integrity": "sha512-vDZpf9Chs9mAdfY046mcPt8fg5QSZr37hEH4TXYBnDF+izxgrbRGUAAaBvIk/fJm9aOFCGFd1EsNg5AZCbnQCQ==",
"dev": true,
+ "peer": true,
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
@@ -10068,13 +10178,15 @@
"version": "2.3.8",
"resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz",
"integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==",
- "dev": true
+ "dev": true,
+ "peer": true
},
"node_modules/tmp": {
"version": "0.2.3",
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz",
"integrity": "sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==",
"dev": true,
+ "peer": true,
"engines": {
"node": ">=14.14"
}
@@ -10105,6 +10217,7 @@
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz",
"integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==",
"dev": true,
+ "peer": true,
"dependencies": {
"psl": "^1.1.33",
"punycode": "^2.1.1",
@@ -10120,6 +10233,7 @@
"resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz",
"integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==",
"dev": true,
+ "peer": true,
"engines": {
"node": ">= 4.0.0"
}
@@ -10211,6 +10325,7 @@
"resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
"integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==",
"dev": true,
+ "peer": true,
"dependencies": {
"safe-buffer": "^5.0.1"
},
@@ -10222,7 +10337,8 @@
"version": "0.14.5",
"resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz",
"integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==",
- "dev": true
+ "dev": true,
+ "peer": true
},
"node_modules/type-check": {
"version": "0.4.0",
@@ -10250,6 +10366,7 @@
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz",
"integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==",
"dev": true,
+ "peer": true,
"engines": {
"node": ">=10"
},
@@ -10385,6 +10502,7 @@
"resolved": "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz",
"integrity": "sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==",
"dev": true,
+ "peer": true,
"engines": {
"node": ">=8"
}
@@ -10433,11 +10551,21 @@
"resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz",
"integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==",
"dev": true,
+ "peer": true,
"dependencies": {
"querystringify": "^2.1.1",
"requires-port": "^1.0.0"
}
},
+ "node_modules/use-sync-external-store": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.2.tgz",
+ "integrity": "sha512-PElTlVMwpblvbNqQ82d2n6RjStvdSoNe9FG28kNfz3WiXilJm4DdNkEzRhCZuIDwY8U08WVihhGR5iRqAwfDiw==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0"
+ }
+ },
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
@@ -10481,6 +10609,7 @@
"engines": [
"node >=0.6.0"
],
+ "peer": true,
"dependencies": {
"assert-plus": "^1.0.0",
"core-util-is": "1.0.2",
@@ -10953,6 +11082,7 @@
"resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz",
"integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==",
"dev": true,
+ "peer": true,
"dependencies": {
"buffer-crc32": "~0.2.3",
"fd-slicer": "~1.1.0"
diff --git a/package.json b/package.json
index ae251685c8..1644922faa 100644
--- a/package.json
+++ b/package.json
@@ -1,22 +1,24 @@
{
"name": "react_phone-catalog",
- "homepage": "react_phone-catalog",
+ "homepage": "https://avramenkomarina.github.io/react_phone-catalog",
"version": "0.1.0",
"keywords": [],
"author": "Mate Academy",
"license": "GPL-3.0",
"dependencies": {
"@fortawesome/fontawesome-free": "^6.5.2",
+ "@reduxjs/toolkit": "^2.3.0",
"bulma": "^1.0.1",
"classnames": "^2.5.1",
"react": "^18.3.1",
"react-dom": "^18.3.1",
+ "react-redux": "^9.1.2",
"react-router-dom": "^6.25.1",
"react-transition-group": "^4.4.5"
},
"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",
@@ -25,7 +27,6 @@
"@types/react-transition-group": "^4.4.10",
"@typescript-eslint/parser": "^7.16.0",
"@vitejs/plugin-react": "^4.3.1",
- "cypress": "^13.13.0",
"eslint": "^8.57.0",
"eslint-config-airbnb-typescript": "^18.0.0",
"eslint-config-prettier": "^9.1.0",
@@ -35,7 +36,7 @@
"eslint-plugin-prettier": "^5.1.3",
"eslint-plugin-react": "^7.34.4",
"eslint-plugin-react-hooks": "^4.6.2",
- "gh-pages": "^6.1.1",
+ "gh-pages": "^6.2.0",
"mochawesome": "^7.1.3",
"mochawesome-merge": "^4.3.0",
"mochawesome-report-generator": "^6.2.0",
diff --git a/public/img/homePage/banner-accessories.png b/public/img/homePage/banner-accessories.png
new file mode 100644
index 0000000000..ba41c4e8f0
Binary files /dev/null and b/public/img/homePage/banner-accessories.png differ
diff --git a/public/img/homePage/banner-phones.png b/public/img/homePage/banner-phones.png
new file mode 100644
index 0000000000..c8fea5b6ee
Binary files /dev/null and b/public/img/homePage/banner-phones.png differ
diff --git a/public/img/homePage/banner-tablets.png b/public/img/homePage/banner-tablets.png
new file mode 100644
index 0000000000..d8079734bc
Binary files /dev/null and b/public/img/homePage/banner-tablets.png differ
diff --git a/public/img/homePage/category/Accessories.svg b/public/img/homePage/category/Accessories.svg
new file mode 100644
index 0000000000..99ffb1812a
--- /dev/null
+++ b/public/img/homePage/category/Accessories.svg
@@ -0,0 +1,15 @@
+
diff --git a/public/img/homePage/category/Phones.svg b/public/img/homePage/category/Phones.svg
new file mode 100644
index 0000000000..1347afc820
--- /dev/null
+++ b/public/img/homePage/category/Phones.svg
@@ -0,0 +1,15 @@
+
diff --git a/public/img/homePage/category/Tablets.svg b/public/img/homePage/category/Tablets.svg
new file mode 100644
index 0000000000..8f61dbc376
--- /dev/null
+++ b/public/img/homePage/category/Tablets.svg
@@ -0,0 +1,15 @@
+
diff --git a/public/img/icons/Chevron-left-dis.svg b/public/img/icons/Chevron-left-dis.svg
new file mode 100644
index 0000000000..fe04db3006
--- /dev/null
+++ b/public/img/icons/Chevron-left-dis.svg
@@ -0,0 +1,3 @@
+
diff --git a/public/img/icons/Chevron-left.svg b/public/img/icons/Chevron-left.svg
new file mode 100644
index 0000000000..ac494c7a55
--- /dev/null
+++ b/public/img/icons/Chevron-left.svg
@@ -0,0 +1,3 @@
+
diff --git a/public/img/icons/Chevron-right-dis.svg b/public/img/icons/Chevron-right-dis.svg
new file mode 100644
index 0000000000..a15457b9ad
--- /dev/null
+++ b/public/img/icons/Chevron-right-dis.svg
@@ -0,0 +1,3 @@
+
diff --git a/public/img/icons/Chevron-right.svg b/public/img/icons/Chevron-right.svg
new file mode 100644
index 0000000000..b4f4687671
--- /dev/null
+++ b/public/img/icons/Chevron-right.svg
@@ -0,0 +1,3 @@
+
diff --git a/public/img/icons/Chevron.svg b/public/img/icons/Chevron.svg
new file mode 100644
index 0000000000..0da5241741
--- /dev/null
+++ b/public/img/icons/Chevron.svg
@@ -0,0 +1,3 @@
+
diff --git a/public/img/icons/Chevron_down.svg b/public/img/icons/Chevron_down.svg
new file mode 100644
index 0000000000..2ab0f27592
--- /dev/null
+++ b/public/img/icons/Chevron_down.svg
@@ -0,0 +1,3 @@
+
diff --git a/public/img/icons/FavouritesFilledHeart.svg b/public/img/icons/FavouritesFilledHeart.svg
new file mode 100644
index 0000000000..c0a8e3d8b5
--- /dev/null
+++ b/public/img/icons/FavouritesFilledHeart.svg
@@ -0,0 +1,3 @@
+
diff --git a/public/img/icons/Home.svg b/public/img/icons/Home.svg
new file mode 100644
index 0000000000..474476cb02
--- /dev/null
+++ b/public/img/icons/Home.svg
@@ -0,0 +1,4 @@
+
diff --git a/public/img/icons/Minus-dis.svg b/public/img/icons/Minus-dis.svg
new file mode 100644
index 0000000000..762e04664e
--- /dev/null
+++ b/public/img/icons/Minus-dis.svg
@@ -0,0 +1,3 @@
+
diff --git a/public/img/icons/Minus.svg b/public/img/icons/Minus.svg
new file mode 100644
index 0000000000..97c41038ac
--- /dev/null
+++ b/public/img/icons/Minus.svg
@@ -0,0 +1,3 @@
+
diff --git a/public/img/icons/Plus.svg b/public/img/icons/Plus.svg
new file mode 100644
index 0000000000..338f8c2f87
--- /dev/null
+++ b/public/img/icons/Plus.svg
@@ -0,0 +1,3 @@
+
diff --git a/public/img/icons/arrow-left.png b/public/img/icons/arrow-left.png
new file mode 100644
index 0000000000..0f0f1d3a15
Binary files /dev/null and b/public/img/icons/arrow-left.png differ
diff --git a/public/img/icons/arrow-right.png b/public/img/icons/arrow-right.png
new file mode 100644
index 0000000000..ef7cae9c91
Binary files /dev/null and b/public/img/icons/arrow-right.png differ
diff --git a/public/img/icons/favourites.svg b/public/img/icons/favourites.svg
new file mode 100644
index 0000000000..ca57cfedd8
--- /dev/null
+++ b/public/img/icons/favourites.svg
@@ -0,0 +1,3 @@
+
diff --git a/public/img/icons/icon-close.svg b/public/img/icons/icon-close.svg
new file mode 100644
index 0000000000..aadcc91fb1
--- /dev/null
+++ b/public/img/icons/icon-close.svg
@@ -0,0 +1,3 @@
+
diff --git a/public/img/icons/icon-menu.svg b/public/img/icons/icon-menu.svg
new file mode 100644
index 0000000000..2c535f4586
--- /dev/null
+++ b/public/img/icons/icon-menu.svg
@@ -0,0 +1,5 @@
+
diff --git a/public/img/icons/shopping-bag.svg b/public/img/icons/shopping-bag.svg
new file mode 100644
index 0000000000..4b8ebce70e
--- /dev/null
+++ b/public/img/icons/shopping-bag.svg
@@ -0,0 +1,5 @@
+
diff --git a/public/img/logo.png b/public/img/logo.png
new file mode 100644
index 0000000000..61d87fce33
Binary files /dev/null and b/public/img/logo.png differ
diff --git a/src/App.scss b/src/App.scss
index 71bc413aad..768d9b5ef2 100644
--- a/src/App.scss
+++ b/src/App.scss
@@ -1 +1,61 @@
-// not empty
+@import './utils/mixins';
+@import './utils/vars';
+
+@font-face {
+ font-family: Mont-Bold;
+ src: url('/fonts/Mont-Bold.otf') format('opentype');
+}
+
+@font-face {
+ font-family: Mont-Regular;
+ src: url('/fonts/Mont-Regular.otf') format('opentype');
+}
+
+html {
+ scroll-behavior: smooth;
+}
+
+body {
+ margin: 0;
+ padding: 0;
+}
+
+.header {
+ background-color: #fff;
+ position: sticky; top: 0;
+ z-index: 1;
+}
+
+.main {
+ min-height: calc(100vh - 170px);
+}
+
+h1, h2, h3, h4, li {
+ font-family: Mont-Bold, sans-serif;
+}
+
+p {
+ font-family: Mont-Regular, sans-serif;
+}
+
+.fade-enter {
+ opacity: 0;
+ transform: translateX(-100%);
+}
+
+.fade-enter-active {
+ opacity: 1;
+ transform: translateX(0);
+ transition: opacity 300ms, transform 300ms;
+}
+
+.fade-exit {
+ opacity: 1;
+ transform: translateX(0);
+}
+
+.fade-exit-active {
+ opacity: 0;
+ transform: translateX(100%);
+ transition: opacity 300ms, transform 300ms;
+}
diff --git a/src/App.tsx b/src/App.tsx
index 372e4b4206..47d9c3134f 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -1,7 +1,93 @@
+import { Navigate, Route, Routes, useLocation } from 'react-router-dom';
import './App.scss';
+import { useEffect, useState } from 'react';
+import { getProductsAsync } from './features/getProductsSlice';
+import { useAppDispatch } from './app/hooks';
+import { CSSTransition, SwitchTransition } from 'react-transition-group';
+import { Header } from './components/Header';
+import { AsideMenu } from './components/AsideMenu';
+import { HomePage } from './components/HomePage';
+import { GeneralProductsPage } from './components/GeneralProductsPage';
+import { ItemInformation } from './components/ItemInformation';
+import { BucketPage } from './components/BucketPage';
+import { FavoritesPage } from './components/FavoritesPage';
+import { Footer } from './components/Footer';
-export const App = () => (
-
-
Product Catalog
-
-);
+export const App = () => {
+ const dispatch = useAppDispatch();
+ const location = useLocation();
+ const [isMenuOpen, setIsMenuOpen] = useState(false);
+
+ useEffect(() => {
+ dispatch(getProductsAsync());
+ }, [dispatch]);
+
+ useEffect(() => {
+ setIsMenuOpen(location.pathname === '/menu');
+ }, [location.pathname]);
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+ {!isMenuOpen && (
+
+
+
+
+ } />
+
+ }
+ />
+
+ } />
+ } />
+
+
+ } />
+ } />
+
+
+ } />
+ } />
+
+ } />
+ } />
+ Page not found} />
+
+
+
+ )}
+
+
+ {!isMenuOpen && (
+
+ )}
+
+
+ >
+ );
+};
diff --git a/src/app/hooks.ts b/src/app/hooks.ts
new file mode 100644
index 0000000000..debd386107
--- /dev/null
+++ b/src/app/hooks.ts
@@ -0,0 +1,5 @@
+import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux';
+import { AppDispatch, RootState } from './store';
+
+export const useAppDispatch = () => useDispatch();
+export const useAppSelector: TypedUseSelectorHook = useSelector;
diff --git a/src/app/store.ts b/src/app/store.ts
new file mode 100644
index 0000000000..77fc634f28
--- /dev/null
+++ b/src/app/store.ts
@@ -0,0 +1,26 @@
+import { configureStore, ThunkAction, Action } from '@reduxjs/toolkit';
+import productSlice from '../features/getProductsSlice';
+import getAllProductsSlice from '../features/getAllProductsSlice';
+import addFavoritesSlice from '../features/addFavoritesSlice';
+import addBucketSlice from '../features/addProductSlice';
+
+export const store = configureStore({
+ reducer: {
+ products: productSlice,
+ allProducts: getAllProductsSlice,
+ addedFavorites: addFavoritesSlice,
+ addBucket: addBucketSlice,
+ },
+});
+
+export type AppDispatch = typeof store.dispatch;
+export type RootState = ReturnType;
+
+/* eslint-disable @typescript-eslint/indent */
+export type AppThunk = ThunkAction<
+ ReturnType,
+ RootState,
+ unknown,
+ Action
+>;
+/* eslint-enable @typescript-eslint/indent */
diff --git a/src/components/AsideMenu/AsideMenu.module.scss b/src/components/AsideMenu/AsideMenu.module.scss
new file mode 100644
index 0000000000..235a87ea7c
--- /dev/null
+++ b/src/components/AsideMenu/AsideMenu.module.scss
@@ -0,0 +1,69 @@
+@import '../../utils/mixins';
+@import '../../utils/vars';
+
+.menu {
+ height: calc(100vh - 69px);
+ display: flex;
+ flex-direction: column;
+ justify-content: space-between;
+
+
+ opacity: 0;
+ transform: translateX(-100%);
+ transition: transform 0.5s ease-out;
+
+ &_open {
+ opacity: 1;
+ transform: translateX(0);
+ }
+
+ &_buttons {
+ @include content-padding-inline;
+
+ margin-top: 32px;
+
+ ul {
+ padding: 0;
+ margin: 0;
+ display: flex;
+ color: $color-for-main-text;
+ flex-direction: column;
+ gap: 16px;
+ list-style-type: none;
+ font-size: 12px;
+ text-transform: uppercase;
+ text-align: center;
+ }
+
+ a {
+ position: relative;
+
+ &:hover {
+ color: black;
+ }
+ }
+ }
+
+ &_icons {
+ width: 100%;
+ border-top: 1px solid;
+ border-color: $color-for-borders;
+
+ display: flex;
+ flex-direction: row;
+
+ &_line {
+ width: 1px;
+ height: 100%;
+ background-color: $color-for-borders;
+ }
+
+ &_icon {
+ padding: 24px 0;
+ width: 50%;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ }
+ }
+}
diff --git a/src/components/AsideMenu/AsideMenu.tsx b/src/components/AsideMenu/AsideMenu.tsx
new file mode 100644
index 0000000000..6a181c539d
--- /dev/null
+++ b/src/components/AsideMenu/AsideMenu.tsx
@@ -0,0 +1,51 @@
+import { Link, NavLink, useLocation } from 'react-router-dom';
+import styles from './AsideMenu.module.scss';
+
+export const AsideMenu = () => {
+ const isMenuClicked = useLocation().pathname.slice(1);
+
+ return (
+
+ );
+};
diff --git a/src/components/AsideMenu/index.ts b/src/components/AsideMenu/index.ts
new file mode 100644
index 0000000000..dccc1cb665
--- /dev/null
+++ b/src/components/AsideMenu/index.ts
@@ -0,0 +1 @@
+export * from './AsideMenu';
diff --git a/src/components/BucketPage/BucketPage.module.scss b/src/components/BucketPage/BucketPage.module.scss
new file mode 100644
index 0000000000..69b52722f4
--- /dev/null
+++ b/src/components/BucketPage/BucketPage.module.scss
@@ -0,0 +1,189 @@
+@import '../../utils/mixins';
+@import '../../utils/vars';
+
+.bucket {
+ @include content-padding-inline;
+
+ margin: 20px 0 80px;
+
+ &__buttonBack {
+ margin-bottom: 20px;
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ gap: 4px;
+
+ p {
+ margin-top: 2.5px;
+ font-size: 12px;
+ color: $color-for-main-text;
+ }
+ }
+
+ &__title {
+ margin-bottom: 32px;
+ font-size: 32px;
+
+ @include on-tablet {
+ font-size: 48px;
+ }
+ }
+
+ &__generalProducts {
+ @include page-grid;
+
+ &_products {
+ grid-column: 1 / -1;
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+ margin-bottom: 32px;
+
+ @include on-desktop {
+ grid-column: 1 / 17;
+ }
+
+ &_product {
+ border: 1px solid;
+ border-color: $color-for-borders;
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+ justify-content: space-between;
+ padding: 16px;
+ max-width: 100%;
+ height: fit-content;
+
+ &:hover {
+ border-color: $color-for-buttons;
+ }
+
+ @include on-tablet {
+ padding: 24px;
+ flex-direction: row;
+ gap: 24px;
+ }
+
+ &_section {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ gap: 16px;
+
+ @include on-tablet {
+ gap: 24px;
+ }
+
+ &_close {
+ cursor: pointer;
+ }
+
+ &_image {
+ width: 80px;
+ height: 80px;
+ object-fit: contain;
+ }
+
+ p {
+ width: 70%;
+ }
+ }
+
+ &_secondSection {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ justify-content: space-between;
+
+ @include on-tablet {
+ gap: 24px;
+ }
+
+ &_buttons {
+ display: flex;
+ flex-direction: row;
+ gap: 14px;
+ align-items: center;
+ justify-content: center;
+
+ &_button {
+ width: 32px;
+ height: 32px;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ border: 1px solid;
+
+ &:hover {
+ cursor: pointer;
+ }
+ }
+
+ &_quantity {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 20px;
+ }
+ }
+
+ &_title {
+ width: 70px;
+ }
+ }
+ }
+ }
+
+ &_total {
+ grid-column: 1 / -1;
+ padding: 24px;
+ border: 1px solid;
+ border-color: $color-for-borders;
+ max-width: 100%;
+ height: fit-content;
+ display: flex;
+ flex-direction: column;
+ gap: 24px;
+ justify-content: center;
+ align-items: center;
+
+ @include on-desktop {
+ grid-column: 17 / 25;
+ }
+
+ &_info {
+ display: flex;
+ flex-direction: column;
+ gap: 1px;
+ align-items: center;
+
+ h3 {
+ font-size: 32px;
+ }
+
+ p {
+ color: $color-for-main-text;
+ font-size: 14px;
+ }
+ }
+
+ &_button {
+ cursor: pointer;
+ width: 100%;
+ height: 48px;
+ color: #fff;
+ background-color: $color-for-buttons;
+ font-size: 14px;
+
+ &:hover {
+ background-color: $color-for-main-text;
+ color: $color-for-title;
+ }
+ }
+ }
+ }
+}
+
+.empty {
+ font-size: 30px;
+}
diff --git a/src/components/BucketPage/BucketPage.tsx b/src/components/BucketPage/BucketPage.tsx
new file mode 100644
index 0000000000..dabfb66c59
--- /dev/null
+++ b/src/components/BucketPage/BucketPage.tsx
@@ -0,0 +1,173 @@
+import { Link } from 'react-router-dom';
+import styles from './BucketPage.module.scss';
+import { useAppDispatch, useAppSelector } from '../../app/hooks';
+import {
+ addQuantity,
+ clearBucket,
+ decrementQuantity,
+ removeBucket,
+} from '../../features/addProductSlice';
+import { Product } from '../../types/products';
+
+export const BucketPage = () => {
+ const products = useAppSelector(state => state.addBucket.items);
+ const dispatch = useAppDispatch();
+
+ const handleDeleteProduct = (item: Product) => {
+ dispatch(removeBucket(item.id));
+ };
+
+ const handleDecrementQuantity = (id: number) => {
+ dispatch(decrementQuantity(id));
+ };
+
+ const handleAddQuantity = (id: number) => {
+ dispatch(addQuantity(id));
+ };
+
+ const handleClearBucket = () => {
+ dispatch(clearBucket());
+ };
+
+ const finalSum = () => {
+ const finalSuma = products.reduce((sum, product) => {
+ return sum + product.price * product.quantity;
+ }, 0);
+
+ return finalSuma;
+ };
+
+ const bucketProducts = useAppSelector(state => state.addBucket.items);
+
+ const bucketAmount = bucketProducts.reduce((total, product) => {
+ return total + product.quantity;
+ }, 0);
+
+ return (
+ <>
+
+
+
+
+
Back
+
+
+
Cart
+
+ {products.length === 0 ? (
+
Your cart is empty
+ ) : (
+
+
+ {products.map(product => (
+
+
+
handleDeleteProduct(product)}
+ className={
+ // eslint-disable-next-line max-len
+ styles.bucket__generalProducts_products_product_section_close
+ }
+ src="img/icons/icon-close.svg"
+ alt="close"
+ />
+
+
{product.name}
+
+
+
+
handleDecrementQuantity(product.id)}
+ style={{
+ borderColor: product.quantity === 1 ? '#E2E6E9' : '',
+ cursor:
+ product.quantity === 1 ? 'not-allowed' : 'inherit',
+ }}
+ >
+ {product.quantity === 1 ? (
+
+ ) : (
+
+ )}
+
+
+ {product.quantity}
+
+
{
+ handleAddQuantity(product.id);
+ }}
+ >
+
+
+
+
+ ${product.price * product.quantity}
+
+
+
+ ))}
+
+
+
+
+ ${finalSum()}
+
+
Total for {bucketAmount} items
+
+
+
+
+ )}
+
+ >
+ );
+};
diff --git a/src/components/BucketPage/index.ts b/src/components/BucketPage/index.ts
new file mode 100644
index 0000000000..f6de2ace7a
--- /dev/null
+++ b/src/components/BucketPage/index.ts
@@ -0,0 +1 @@
+export * from './BucketPage';
diff --git a/src/components/Category/Category.module.scss b/src/components/Category/Category.module.scss
new file mode 100644
index 0000000000..80ca33ced5
--- /dev/null
+++ b/src/components/Category/Category.module.scss
@@ -0,0 +1,41 @@
+@import '../../utils/mixins';
+@import '../../utils/vars';
+
+.category_blocks_block {
+ margin-bottom: 40px;
+ grid-column: 1 / -1;
+
+ @include hover(transform, scale(1.05));
+
+ @include on-tablet {
+ margin-bottom: 0;
+ grid-column: span 4;
+ }
+
+ @include on-desktop {
+ grid-column: span 8;
+ }
+
+ img {
+ width: 100%;
+ margin-bottom: 24px;
+ }
+
+ h2 {
+ width: fit-content;
+ }
+
+ h3 {
+ font-size: 20px;
+ margin-bottom: 8px;
+ color: black;
+ width: fit-content;
+ }
+
+ p {
+ width: fit-content;
+ color: #89939A;
+ font-size: 14px;
+
+ }
+}
diff --git a/src/components/Category/Category.tsx b/src/components/Category/Category.tsx
new file mode 100644
index 0000000000..0960d3d579
--- /dev/null
+++ b/src/components/Category/Category.tsx
@@ -0,0 +1,45 @@
+import { useAppSelector } from '../../app/hooks';
+import styles from './Category.module.scss';
+import { Link } from 'react-router-dom';
+
+export const Category = () => {
+ const products = useAppSelector(state => state.products.items);
+
+ const mobileAmount = products.filter(
+ product => product.category === 'phones',
+ ).length;
+ const tabletsAmount = products.filter(
+ product => product.category === 'tablets',
+ ).length;
+ const accessoriesAmount = products.filter(
+ product => product.category === 'accessories',
+ ).length;
+
+ return (
+ <>
+
+
+
+
Mobile phones
+
{`${mobileAmount} models`}
+
+
+
+
+
+
+
Tablets
+
{`${tabletsAmount} models`}
+
+
+
+
+
+
+
Accessories
+
{`${accessoriesAmount} models`}
+
+
+ >
+ );
+};
diff --git a/src/components/Category/index.ts b/src/components/Category/index.ts
new file mode 100644
index 0000000000..5c45122686
--- /dev/null
+++ b/src/components/Category/index.ts
@@ -0,0 +1 @@
+export * from './Category';
diff --git a/src/components/DiscountItemList/DiscountItemList.module.scss b/src/components/DiscountItemList/DiscountItemList.module.scss
new file mode 100644
index 0000000000..25adba2eb4
--- /dev/null
+++ b/src/components/DiscountItemList/DiscountItemList.module.scss
@@ -0,0 +1,57 @@
+@import '../../utils/mixins';
+@import '../../utils/vars';
+
+.newModels {
+ &_title {
+ margin-bottom: 24px;
+
+ display: flex;
+ flex-direction: row;
+ justify-content: space-between;
+
+ h2 {
+ width: 40%;
+ font-size: 22px;
+
+ @include on-tablet {
+ width: 100%;
+ font-size: 32px;
+ }
+ }
+
+ &__buttons {
+ display: flex;
+ flex-direction: row;
+ gap: 16px;
+
+ button {
+ cursor: pointer;
+ background-color: unset;
+ width: 40px;
+ height: 40px;
+ box-sizing: border-box;
+ border: 1px solid;
+ border-color: #B4BDC3;
+ line-height: 45px;
+ text-align: center;
+
+ &:disabled {
+ border-color: $color-for-borders;
+ cursor: unset;
+ }
+ }
+ }
+ }
+}
+
+.itemsList {
+ @include page-grid;
+
+ div {
+ grid-column: span 4;
+
+ @include on-desktop {
+ grid-column: span 6;
+ }
+ }
+}
diff --git a/src/components/DiscountItemList/DiscountItemList.tsx b/src/components/DiscountItemList/DiscountItemList.tsx
new file mode 100644
index 0000000000..e154e65866
--- /dev/null
+++ b/src/components/DiscountItemList/DiscountItemList.tsx
@@ -0,0 +1,84 @@
+import { useState, useEffect } from 'react';
+import { useAppSelector } from '../../app/hooks';
+import styles from './DiscountItemList.module.scss';
+import { ItemsProduct } from '../ItemsProduct/ItemsProduct';
+
+export const DiscountItemList = () => {
+ const products = useAppSelector(state => state.products.items);
+ const [currentIndex, setCurrentIndex] = useState(0);
+ const [itemsPerPage, setItemsPerPage] = useState(4);
+
+ useEffect(() => {
+ const updateItemsPerPage = () => {
+ if (window.innerWidth <= 640) {
+ setItemsPerPage(1);
+ } else {
+ setItemsPerPage(4);
+ }
+ };
+
+ // Update items per page on component mount and window resize
+ updateItemsPerPage();
+ window.addEventListener('resize', updateItemsPerPage);
+
+ return () => {
+ window.removeEventListener('resize', updateItemsPerPage);
+ };
+ }, []);
+
+ const handleNext = () => {
+ if (currentIndex + itemsPerPage < products.length) {
+ setCurrentIndex(prevIndex => prevIndex + itemsPerPage);
+ }
+ };
+
+ const handlePrev = () => {
+ if (currentIndex - itemsPerPage >= 0) {
+ setCurrentIndex(prevIndex => prevIndex - itemsPerPage);
+ }
+ };
+
+ const currentItems = products.slice(
+ currentIndex,
+ currentIndex + itemsPerPage,
+ );
+
+ return (
+ <>
+
+
Hot prices
+
+
+
+
+
+
+
+ {currentItems.map(product => (
+
+ ))}
+
+ >
+ );
+};
diff --git a/src/components/DiscountItemList/index.ts b/src/components/DiscountItemList/index.ts
new file mode 100644
index 0000000000..384d5878b0
--- /dev/null
+++ b/src/components/DiscountItemList/index.ts
@@ -0,0 +1 @@
+export * from './DiscountItemList';
diff --git a/src/components/FavoritesPage/FavoritesPage.module.scss b/src/components/FavoritesPage/FavoritesPage.module.scss
new file mode 100644
index 0000000000..f93e5ce593
--- /dev/null
+++ b/src/components/FavoritesPage/FavoritesPage.module.scss
@@ -0,0 +1,53 @@
+@import '../../utils/mixins';
+@import '../../utils/vars';
+
+.favorites {
+ margin: 24px 0 80px;
+
+ @include content-padding-inline;
+
+ &_route {
+ margin-bottom: 24px;
+ display: flex;
+ flex-direction: row;
+ gap: 8px;
+ justify-content: left;
+ align-items: center;
+
+ @include on-tablet {
+ margin-bottom: 40px;
+ }
+
+ p {
+ color: $color-for-main-text;
+ font-size: 12px;
+ }
+ }
+
+ &_title {
+ font-size: 32px;
+ margin-bottom: 8px;
+
+ @include on-tablet {
+ font-size: 48px;
+ }
+ }
+
+ &_text {
+ margin-bottom: 40px;
+ color: $color-for-main-text;
+ font-size: 14px;
+ }
+
+ &_list {
+ @include page-grid;
+
+ div {
+ grid-column: span 4;
+
+ @include on-tablet {
+ grid-column: span 6;
+ }
+ }
+ }
+}
diff --git a/src/components/FavoritesPage/FavoritesPage.tsx b/src/components/FavoritesPage/FavoritesPage.tsx
new file mode 100644
index 0000000000..a492590ab5
--- /dev/null
+++ b/src/components/FavoritesPage/FavoritesPage.tsx
@@ -0,0 +1,36 @@
+import { Link } from 'react-router-dom';
+import styles from './FavoritesPage.module.scss';
+import { useAppSelector } from '../../app/hooks';
+import { ItemsProduct } from '../ItemsProduct';
+
+export const FavoritesPage = () => {
+ const favourites = useAppSelector(state => state.addedFavorites.items);
+
+ return (
+ <>
+
+
+
+
+
+
+
Favourites
+
+
+
Favourites
+
+
{favourites.length} items
+
+
+ {favourites.map(favorite => (
+
+ ))}
+
+
+ >
+ );
+};
diff --git a/src/components/FavoritesPage/index.ts b/src/components/FavoritesPage/index.ts
new file mode 100644
index 0000000000..b3a884b188
--- /dev/null
+++ b/src/components/FavoritesPage/index.ts
@@ -0,0 +1 @@
+export * from './FavoritesPage';
diff --git a/src/components/Footer/Footer.module.scss b/src/components/Footer/Footer.module.scss
new file mode 100644
index 0000000000..29cb8f4357
--- /dev/null
+++ b/src/components/Footer/Footer.module.scss
@@ -0,0 +1,82 @@
+@import '../../utils/mixins';
+@import '../../utils/vars';
+
+.container {
+ border-top: 1px solid;
+ border-color: $color-for-borders;
+
+ @include content-padding-inline;
+}
+
+.footer_wrapper {
+ padding: 32px 0;
+ display: flex;
+ flex-direction: column;
+ gap: 32px;
+
+ @include on-tablet {
+ align-items: center;
+ flex-direction: row;
+ justify-content: space-between;
+ }
+
+ &_logo {
+ width: fit-content;
+
+ @include hover(transform, scale(1.05));
+ }
+
+ &Navbar {
+ ul {
+ padding: 0;
+ margin: 0;
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+ list-style-type: none;
+ color: $color-for-main-text;
+ font-size: 12px;
+ text-transform: uppercase;
+
+
+ @include on-tablet {
+ flex-direction: row;
+ }
+
+ @include on-desktop {
+ gap: 100px;
+ }
+
+ li {
+ @include hover(transform, scale(1.05));
+ }
+ }
+
+ a {
+ color: $color-for-main-text;
+ }
+ }
+
+ &ButtonToTop {
+ justify-content: center;
+ font-size: 12px;
+ color: $color-for-main-text;
+ display: flex;
+ flex-direction: row;
+ gap: 16px;
+ align-items: center;
+
+ p {
+ white-space: nowrap;
+ }
+
+ img {
+ width: 16px;
+ padding: 8px;
+ border: 1px solid;
+ cursor: pointer;
+
+ @include hover(transform, scale(1.05));
+ }
+ }
+}
diff --git a/src/components/Footer/Footer.tsx b/src/components/Footer/Footer.tsx
new file mode 100644
index 0000000000..0e28f71efe
--- /dev/null
+++ b/src/components/Footer/Footer.tsx
@@ -0,0 +1,54 @@
+import { Link } from 'react-router-dom';
+import styles from './Footer.module.scss';
+
+export const Footer = () => {
+ const scrollToTop = () => {
+ window.scrollTo({ top: 0, behavior: 'smooth' });
+ };
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+ - github
+
+
+ - contacts
+
+
+ - rights
+
+
+
+
+
+
Back to top
+
+
+
+
+ >
+ );
+};
diff --git a/src/components/Footer/index.ts b/src/components/Footer/index.ts
new file mode 100644
index 0000000000..ddcc5a9cd1
--- /dev/null
+++ b/src/components/Footer/index.ts
@@ -0,0 +1 @@
+export * from './Footer';
diff --git a/src/components/GeneralItemsList.tsx/GeneralItemsList.module.scss b/src/components/GeneralItemsList.tsx/GeneralItemsList.module.scss
new file mode 100644
index 0000000000..3ba169825a
--- /dev/null
+++ b/src/components/GeneralItemsList.tsx/GeneralItemsList.module.scss
@@ -0,0 +1,17 @@
+@import '../../utils/mixins';
+@import '../../utils/vars';
+
+.itemsListPhones {
+ margin-bottom: 40px;
+ grid-row-gap: 40px;
+
+ @include page-grid;
+
+ div {
+ grid-column: 1 / -1;
+
+ @include on-tablet {
+ grid-column: span 6;
+ }
+ }
+}
diff --git a/src/components/GeneralItemsList.tsx/GeneralItemsList.tsx b/src/components/GeneralItemsList.tsx/GeneralItemsList.tsx
new file mode 100644
index 0000000000..337c1fa5c8
--- /dev/null
+++ b/src/components/GeneralItemsList.tsx/GeneralItemsList.tsx
@@ -0,0 +1,21 @@
+import { Product } from '../../types/products';
+import { ItemsProduct } from '../ItemsProduct/ItemsProduct';
+import styles from './GeneralItemsList.module.scss';
+
+export type GeneralItemsProps = {
+ filteredProducts: Product[];
+};
+
+export const GeneralItemsList: React.FC = ({
+ filteredProducts,
+}) => {
+ return (
+ <>
+
+ {filteredProducts.map(phone => (
+
+ ))}
+
+ >
+ );
+};
diff --git a/src/components/GeneralItemsList.tsx/index.ts b/src/components/GeneralItemsList.tsx/index.ts
new file mode 100644
index 0000000000..389593e012
--- /dev/null
+++ b/src/components/GeneralItemsList.tsx/index.ts
@@ -0,0 +1 @@
+export * from './GeneralItemsList';
diff --git a/src/components/GeneralProductsPage/GeneralProductsPage.module.scss b/src/components/GeneralProductsPage/GeneralProductsPage.module.scss
new file mode 100644
index 0000000000..6c6e4d0397
--- /dev/null
+++ b/src/components/GeneralProductsPage/GeneralProductsPage.module.scss
@@ -0,0 +1,159 @@
+@import '../../utils/mixins';
+@import '../../utils/vars';
+
+.phonesPage {
+ padding: 24px 0 80px;
+
+ @include content-padding-inline;
+
+ &_route {
+ margin-bottom: 24px;
+ display: flex;
+ flex-direction: row;
+ gap: 8px;
+ justify-content: left;
+ align-items: center;
+
+ @include on-tablet {
+ margin-bottom: 40px;
+ }
+
+ p {
+ color: $color-for-main-text;
+ font-size: 12px;
+ }
+ }
+
+ &_title {
+ margin-bottom: 8px;
+ font-size: 32px;
+
+ @include on-tablet {
+ font-size: 48px;
+ }
+ }
+
+ &_amountOfPhones {
+ margin-bottom: 40px;
+ font-size: 14px;
+ color: $color-for-main-text;
+ }
+
+ &_filters {
+ margin-bottom: 24px;
+ display: flex;
+ flex-direction: row;
+ gap: 16px;
+
+ @include page-grid;
+
+ &_dropdown {
+ grid-column: span 2;
+ position: relative;
+ display: inline-block;
+
+ &_text {
+ color: $color-for-main-text;
+ font-size: 14px;
+ margin-bottom: 4px;
+ }
+
+ @include on-tablet {
+ grid-column: span 4;
+ }
+
+ &_dropbtn {
+ // width: 100%;
+ white-space: nowrap;
+ background-color: unset;
+ border: 1px solid;
+ border-color: #B4BDC3;
+ padding: 10px 12px;
+ display: flex;
+ flex-direction: row;
+ justify-content: space-between;
+
+ &_text {
+ font-size: 16px;
+ color: $color-for-buttons;
+ }
+ }
+
+ &_dropdownContent {
+ margin-top: 2px;
+ display: none;
+ position: absolute;
+ background-color: #fff;
+ width: 100%;
+ box-shadow: 0 8px 16px 0 rgba(0,0,0,0.2);
+ z-index: 1;
+
+ a {
+ color: $color-for-main-text;
+ padding: 12px 16px;
+ text-decoration: none;
+ display: block;
+ }
+ }
+ }
+
+ &_dropdown:hover &_dropdown_dropdownContent {
+ display: block;
+ cursor: pointer;
+ }
+
+ &_dropdown_dropdownContent a:hover {color: $color-for-buttons;}
+ &_dropdown:hover &_dropdown_dropbtn {border-color: $color-for-buttons;}
+ }
+}
+
+.pagination {
+ display: flex;
+ flex-direction: row;
+ gap: 8px;
+ justify-content: center;
+
+ &__pageButton {
+ width: 32px;
+ height: 32px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border: none;
+ background-color: unset;
+ cursor: pointer;
+
+ &:hover {
+ border: 1px solid $color-for-borders;
+ }
+
+ &_press {
+ background-color: $color-for-buttons;
+ color: white;
+ }
+ }
+}
+
+.activePage {
+ cursor: pointer;
+ border: none;
+ width: 32px;
+ height: 32px;
+ color: white;
+ background-color: $color-for-buttons;
+}
+
+.page {
+ cursor: pointer;
+ border: none;
+ border: 1px solid;
+ border-color: $color-for-borders;
+ width: 32px;
+ height: 32px;
+ color: black;
+ background-color: white;
+
+ &_dots {
+ cursor: pointer;
+ }
+}
diff --git a/src/components/GeneralProductsPage/GeneralProductsPage.tsx b/src/components/GeneralProductsPage/GeneralProductsPage.tsx
new file mode 100644
index 0000000000..433d9ff9b5
--- /dev/null
+++ b/src/components/GeneralProductsPage/GeneralProductsPage.tsx
@@ -0,0 +1,211 @@
+import { Link, useLocation } from 'react-router-dom';
+import styles from './GeneralProductsPage.module.scss';
+import { useAppDispatch, useAppSelector } from '../../app/hooks';
+import { Product } from '../../types/products';
+import { useMemo, useState } from 'react';
+import {
+ setCurrentPage,
+ setItemsPerPage,
+} from '../../features/getProductsSlice';
+import { GeneralItemsList } from '../GeneralItemsList.tsx';
+import { Loader } from '../Loader';
+
+export const GeneralProductsPage = () => {
+ const dispatch = useAppDispatch();
+ const [selectedOrder, setSelectedOrder] = useState('Newest');
+ const [selectedAmount, setSelectedAmount] = useState('4');
+ const category = useLocation().pathname.slice(1);
+
+ const generalProducts = useAppSelector(state => state.products.items);
+ const loadedGeneralProducts = useAppSelector(state => state.products.loaded);
+ const errorGeneralProducts = useAppSelector(state => state.products.hasError);
+ const itemsPerPage = useAppSelector(state => state.products.itemsPerPage);
+ const currentPage = useAppSelector(state => state.products.currentPage);
+
+ function ucFirst(str: string | undefined) {
+ if (!str) {
+ return str;
+ }
+
+ return str[0].toUpperCase() + str.slice(1);
+ }
+
+ const specificProducts: Product[] = useMemo(() => {
+ return generalProducts.filter(product => product.category === category);
+ }, [generalProducts, category]);
+
+ const sortedProducts = useMemo(() => {
+ switch (selectedOrder) {
+ case 'Newest':
+ return [...specificProducts].sort((a, b) => b.year - a.year);
+ case 'Cheapest':
+ return [...specificProducts].sort((a, b) => a.price - b.price);
+ case 'Alphabetically':
+ return [...specificProducts].sort((a, b) =>
+ a.name.localeCompare(b.name),
+ );
+ default:
+ return specificProducts;
+ }
+ }, [specificProducts, selectedOrder]);
+
+ const startIndex = (currentPage - 1) * itemsPerPage;
+ const endIndex = startIndex + itemsPerPage;
+ const paginatedProducts = sortedProducts.slice(startIndex, endIndex);
+
+ const handleSort = (sortType: string) => {
+ setSelectedOrder(sortType);
+ };
+
+ const handleItemsPerPageChange = (amount: string) => {
+ setSelectedAmount(amount);
+ dispatch(setItemsPerPage(Number(amount) || specificProducts.length));
+ dispatch(setCurrentPage(1));
+ };
+
+ const handlePageChange = (pageNumber: number) => {
+ dispatch(setCurrentPage(pageNumber));
+ };
+
+ const totalPages = Math.ceil(specificProducts.length / itemsPerPage);
+
+ const pagesToShow = () => {
+ const range = 2;
+ const pages = [];
+
+ if (totalPages <= 5) {
+ for (let i = 1; i <= totalPages; i++) {
+ pages.push(i);
+ }
+ } else {
+ pages.push(1);
+
+ if (currentPage > range + 1) {
+ pages.push('...');
+ }
+
+ const start = Math.max(2, currentPage - range);
+ const end = Math.min(totalPages - 1, currentPage + range);
+
+ for (let i = start; i <= end; i++) {
+ pages.push(i);
+ }
+
+ if (currentPage < totalPages - range) {
+ pages.push('...');
+ }
+
+ pages.push(totalPages);
+ }
+
+ return pages;
+ };
+
+ if (!loadedGeneralProducts) {
+ return ;
+ }
+
+ if (errorGeneralProducts) {
+ return (
+
+ Failed to load products. Please try again.
+
+ );
+ }
+
+ if (generalProducts.length === 0) {
+ return There are no {category} yet.
;
+ }
+
+ return (
+
+
+
+
+
+
+
{ucFirst(category)}
+
+
+
+
{ucFirst(category)}
+
+ {specificProducts.length} models
+
+
+
+
+
+
+ {pagesToShow().map((page, index) =>
+ typeof page === 'number' ? (
+
+ ) : (
+
+ {page}
+
+ ),
+ )}
+
+
+ );
+};
diff --git a/src/components/GeneralProductsPage/index.ts b/src/components/GeneralProductsPage/index.ts
new file mode 100644
index 0000000000..b3a1eafbc4
--- /dev/null
+++ b/src/components/GeneralProductsPage/index.ts
@@ -0,0 +1 @@
+export * from './GeneralProductsPage';
diff --git a/src/components/Header/Header.module.scss b/src/components/Header/Header.module.scss
new file mode 100644
index 0000000000..639c2b9cc5
--- /dev/null
+++ b/src/components/Header/Header.module.scss
@@ -0,0 +1,190 @@
+@import '../../utils/mixins';
+@import '../../utils/vars';
+
+.topbar {
+ border-bottom: 1px solid;
+ border-color: $color-for-borders;
+ display: flex;
+ flex-direction: row;
+ justify-content: space-between;
+ transform: translateY(0);
+ transition: transform 0.5s ease-in-out;
+
+ .topbar.open {
+ transform: translateY(-20px);
+ }
+
+ @include on-desktop {
+ padding-inline: 18px;
+ }
+
+ &LogoAndButtons {
+ padding-inline: 16px;
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ gap: 16px;
+
+ @include on-desktop {
+ gap: 24px;
+ }
+
+ &__logo {
+ padding: 18px 0;
+ }
+
+ &_buttons {
+ display: none;
+
+ @include on-tablet {
+ display: block;
+ }
+ ul {
+ padding: 0;
+ margin: 0;
+ display: flex;
+ color: $color-for-main-text;
+ flex-direction: row;
+ gap: 32px;
+ list-style-type: none;
+ font-size: 13px;
+ text-transform: uppercase;
+ height: 100%;
+
+ @include on-desktop {
+ gap: 64px;
+ }
+
+ li {
+ height: 70px;
+ }
+ }
+
+ a {
+ line-height: 70px;
+ position: relative;
+ display: block;
+ height: 100%;
+
+ &:hover {
+ color: black;
+ }
+ }
+
+ &_isActive {
+ color: black;
+ &::after {
+ content: '';
+ position: absolute;
+ bottom: -2px;
+ left: 0;
+ width: 100%;
+ height: 3px;
+ background-color: black;
+ }
+ }
+ }
+
+ img {
+ @include hover(transform, scale(1.05));
+ }
+ }
+
+ &Icons {
+ position: relative;
+ flex-direction: row;
+ display: none;
+ align-items: center;
+
+ @include on-tablet {
+ display: flex;
+ }
+
+ & > :first-child {
+ border-left: 1px solid;
+ border-right: 1px solid;
+ border-color: $color-for-borders;
+ }
+
+ &_icon {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ height: 100%;
+ position: relative;
+
+ &_isActive {
+ color: black;
+ &::after {
+ content: '';
+ position: absolute;
+ bottom: 0;
+ left: 0;
+ width: 100%;
+ height: 3px;
+ background-color: black;
+ }
+ }
+
+ &_inside {
+ width: 16px;
+ padding: 0 24px;
+ }
+
+ &__amountFav {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 14px;
+ height: 14px;
+ border: 1px solid;
+ border-color: white;
+ box-sizing: border-box;
+ background-color: #EB5757;
+ color: white;
+ font-size: 9px;
+ border-radius: 50%;
+ position: absolute;
+ top: 18px;
+ right: 20px;
+ }
+
+ &__amountBucket {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 14px;
+ height: 14px;
+ border: 1px solid;
+ border-color: white;
+ box-sizing: border-box;
+ background-color: #EB5757;
+ color: white;
+ font-size: 9px;
+ border-radius: 50%;
+ position: absolute;
+ top: 18px;
+ right: 18px;
+ }
+ }
+
+ &_menu {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border-left: 1px solid;
+ border-color: $color-for-borders;
+ position: relative;
+ cursor: pointer;
+
+ &_button {
+ width: 16px;
+ padding: 0 24px;
+ }
+
+ @include on-tablet {
+ display: none;
+ }
+ }
+ }
+}
diff --git a/src/components/Header/Header.tsx b/src/components/Header/Header.tsx
new file mode 100644
index 0000000000..ee918e7087
--- /dev/null
+++ b/src/components/Header/Header.tsx
@@ -0,0 +1,131 @@
+import { Link, NavLink, useLocation } from 'react-router-dom';
+import styles from './Header.module.scss';
+import classNames from 'classnames';
+import { useEffect, useState } from 'react';
+import { useAppSelector } from '../../app/hooks';
+
+export const Header = () => {
+ const getLinkClass = ({ isActive }: { isActive: boolean }) =>
+ classNames({ [styles.topbarLogoAndButtons_buttons_isActive]: isActive });
+
+ const getAdditionalButtonsClass = ({ isActive }: { isActive: boolean }) =>
+ classNames({ [styles.topbarIcons_icon_isActive]: isActive });
+
+ const favouriteAmount = useAppSelector(
+ state => state.addedFavorites.items,
+ ).length;
+
+ const bucketProducts = useAppSelector(state => state.addBucket.items);
+
+ const bucketAmount = bucketProducts.reduce((total, product) => {
+ return total + product.quantity;
+ }, 0);
+
+ const [isMenuOpen, setIsMenuOpen] = useState(false);
+ const location = useLocation();
+
+ useEffect(() => {
+ if (location.pathname === '/menu') {
+ setIsMenuOpen(true);
+ } else {
+ setIsMenuOpen(false);
+ }
+ }, [location]);
+
+ const handleMenuToggle = () => {
+ setIsMenuOpen(prevState => !prevState);
+ };
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {favouriteAmount > 0 && {favouriteAmount}}
+
+
+
+
+
+
+
+ {bucketAmount > 0 && {bucketAmount}}
+
+
+
+
+
+
+ {isMenuOpen ? (
+
+
+
+ ) : (
+
+
+
+ )}
+
+
+ );
+};
diff --git a/src/components/Header/index.ts b/src/components/Header/index.ts
new file mode 100644
index 0000000000..266dec8a1b
--- /dev/null
+++ b/src/components/Header/index.ts
@@ -0,0 +1 @@
+export * from './Header';
diff --git a/src/components/HomePage/HomePage.module.scss b/src/components/HomePage/HomePage.module.scss
new file mode 100644
index 0000000000..438c3eb4ef
--- /dev/null
+++ b/src/components/HomePage/HomePage.module.scss
@@ -0,0 +1,58 @@
+@import '../../utils/mixins';
+@import '../../utils/vars';
+
+.generalHomePage {
+ display: flex;
+ flex-direction: column;
+ gap: 56px;
+
+ @include content-padding-inline;
+
+ @include on-tablet {
+ margin: 32px 0 96px;
+ }
+
+ @include on-desktop {
+ gap: 80px;
+ margin: 56px 0 112px;
+ margin-left: auto;
+ margin-right: auto;
+ }
+}
+
+.greeting {
+
+ &__title {
+ font-size: 32px;
+ margin-bottom: 24px;
+
+ @include on-tablet {
+ font-size: 48px;
+ margin-bottom: 32px;
+ }
+
+ @include on-desktop {
+ margin-bottom: 56px
+ }
+ }
+}
+
+.category {
+ &_title {
+ font-size: 22px;
+ margin-bottom: 24px;
+
+ @include on-tablet {
+ font-size: 32px;
+ }
+ }
+
+ &_blocks {
+ @include page-grid;
+
+ }
+}
+
+.hotPrices {
+ font-size: 22px;
+}
diff --git a/src/components/HomePage/HomePage.tsx b/src/components/HomePage/HomePage.tsx
new file mode 100644
index 0000000000..fa6b707059
--- /dev/null
+++ b/src/components/HomePage/HomePage.tsx
@@ -0,0 +1,55 @@
+import { useEffect } from 'react';
+import { useAppDispatch, useAppSelector } from '../../app/hooks';
+import { getProductsAsync } from '../../features/getProductsSlice';
+import { ItemsList } from '../ItemsList/ItemsList';
+import { Loader } from '../Loader';
+import { SliderMain } from '../SliderMain';
+import styles from './HomePage.module.scss';
+import { Category } from '../Category';
+import { DiscountItemList } from '../DiscountItemList';
+
+export const HomePage = () => {
+ const dispatch = useAppDispatch();
+ const loadedItems = useAppSelector(state => state.products.loaded);
+
+ useEffect(() => {
+ dispatch(getProductsAsync());
+ }, [dispatch]);
+
+ return (
+ <>
+
+
+
+ Welcome to Nice Gadgets store!
+
+
+
+
+ {loadedItems ? (
+
+ ) : (
+
+ )}
+
+
+ Shop by category
+
+
+
+
+
+
+ {loadedItems ? (
+
+ ) : (
+
+ )}
+
+ >
+ );
+};
diff --git a/src/components/HomePage/index.ts b/src/components/HomePage/index.ts
new file mode 100644
index 0000000000..11e53da674
--- /dev/null
+++ b/src/components/HomePage/index.ts
@@ -0,0 +1 @@
+export * from './HomePage';
diff --git a/src/components/ItemInformation/ItemInformation.module.scss b/src/components/ItemInformation/ItemInformation.module.scss
new file mode 100644
index 0000000000..e5f8fb2616
--- /dev/null
+++ b/src/components/ItemInformation/ItemInformation.module.scss
@@ -0,0 +1,457 @@
+@import '../../utils/mixins';
+@import '../../utils/vars';
+
+.itemInformation {
+ @include content-padding-inline;
+
+ margin: 24px 0 80px;
+
+ &_route {
+ margin-bottom: 24px;
+ display: flex;
+ flex-direction: row;
+ gap: 8px;
+ justify-content: left;
+ align-items: center;
+
+ @include on-tablet {
+ margin-bottom: 40px;
+ }
+
+ h3 {
+ margin-top: 2px;
+ color: $color-for-buttons;
+ font-size: 12px;
+ }
+
+ p {
+ color: $color-for-main-text;
+ font-size: 12px;
+ }
+ }
+
+ &_buttonBack {
+ margin-bottom: 16px;
+ display: flex;
+ flex-direction: row;
+ gap: 4px;
+
+ p {
+ margin-top: 2px;
+ color: $color-for-main-text;
+ font-size: 12px;
+ }
+ }
+
+ &_title {
+ margin-bottom: 36px;
+ font-size: 22px;
+
+ @include on-tablet {
+ font-size: 32px;
+ }
+ }
+
+ &_generalInfo {
+ @include page-grid;
+
+ margin-bottom: 40px;
+
+ @include on-tablet {
+ margin-bottom: 64px;
+ }
+
+ @include on-desktop {
+ margin-bottom: 80px;
+ }
+
+ &_mainImage {
+ grid-column: 1 /-1;
+ margin-bottom: 16px;
+
+ @include on-tablet {
+ grid-column: 2 / 8;
+ }
+
+ @include on-desktop {
+ grid-column: 3 / 13;
+ }
+
+ img {
+ width: 100%;
+ height: 280px;
+ object-fit: contain;
+
+ @include on-desktop {
+ height: 464px;
+ }
+ }
+ }
+
+ &_additionalImgOnPhone {
+ margin-bottom: 40px;
+ grid-column: 1 / -1;
+ display: flex;
+ flex-direction: row;
+ justify-content: space-between;
+
+ @include on-tablet {
+ display: none;
+ }
+
+ div {
+ width: 66px;
+ height: 66px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ }
+
+ img {
+ padding: 2px;
+ width: 60px;
+ height: 60px;
+ object-fit: contain;
+ }
+ }
+
+ &_additionalImgOnTablet {
+ display: none;
+
+ @include on-tablet {
+ grid-column: 1 / 2;
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+ }
+
+ @include on-desktop {
+ grid-column: 1 / 3;
+ }
+
+ div {
+ width: 66px;
+ height: 66px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ }
+
+ img {
+ width: 60px;
+ height: 60px;
+ object-fit: contain;
+ }
+ }
+
+ &_edit {
+ grid-column: 1 / -1;
+
+ @include on-tablet {
+ grid-column: 8 / 13;
+ }
+
+ @include on-desktop {
+ grid-column: 14 / 25;
+ }
+
+ &__wrapper {
+ display: flex;
+ flex-direction: column;
+ }
+
+ &__colours {
+ margin-bottom: 24px;
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+
+ p {
+ font-size: 12px;
+ color: $color-for-main-text;
+ }
+
+ &_circles {
+ display: flex;
+ flex-direction: row;
+ gap: 8px;
+
+ div {
+ width: 36px;
+ height: 36px;
+ border: 2px solid;
+ border-color: $color-for-borders;
+ border-radius: 100%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+
+ div {
+ width: 30px;
+ height: 30px;
+ }
+
+ &:hover {
+ border-color: #B4BDC3;
+ }
+
+ }
+ }
+ }
+
+ &__string {
+ margin-bottom: 24px;
+ width: 100%;
+ height: 1px;
+ background-color: $color-for-borders;
+
+ @include on-tablet {
+ width: 70%;
+ }
+
+ &1 {
+ margin-bottom: 32px;
+ width: 100%;
+ height: 1px;
+ background-color: $color-for-borders;
+
+ @include on-tablet {
+ width: 70%;
+ }
+ }
+ }
+
+ &_storage {
+ margin-bottom: 24px;
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+
+ p {
+ font-size: 12px;
+ color: $color-for-main-text;
+ }
+ }
+
+ .capacityOptions {
+ margin-top: 8px;
+ display: flex;
+ gap: 8px;
+ }
+
+ .capacityLink {
+ text-decoration: none;
+ }
+
+ .capacityBox {
+ width: 56px;
+ height: 32px;
+ border: 1px solid $color-for-borders;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+
+ &:hover {
+ border-color: $color-for-buttons;
+ }
+
+ &.active {
+ background-color: black;
+ }
+ }
+
+ .capacityText {
+ color: black;
+ font-size: 14px;
+ line-height: 32px;
+ }
+
+ .activeText {
+ color: white;
+ }
+
+ &__prices {
+ margin-bottom: 16px;
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ gap: 8px;
+
+
+ h3 {
+ color: $color-for-title;
+ font-size: 32px;
+
+ }
+
+ p {
+ color: $color-for-main-text;
+ font-size: 22px;
+ text-decoration: line-through;
+ }
+ }
+ }
+
+ &_buttons {
+ display: flex;
+ flex-direction: row;
+ height: 48px;
+ justify-content: space-between;
+ margin-bottom: 32px;
+
+ @include on-tablet {
+ justify-content: unset;
+ gap: 8px;
+ }
+
+ &_add {
+ height: 48px;
+ color: $color-for-white-text;
+ background-color: $color-for-buttons;
+ border: 1px solid;
+ border-color: $color-for-buttons;
+ box-sizing: border-box;
+ width: 85%;
+ line-height: 48px;
+ text-align: center;
+ font-size: 14px;
+
+ @include on-tablet {
+ width: 70%;
+ }
+
+ &:hover {
+ cursor: pointer;
+ box-shadow: 4px 4px 4px 0 rgba(0,0,0,0.2);
+ }
+ }
+
+ &_favourite {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ width: 48px;
+ height: 48px;
+ border: 1px solid;
+ box-sizing: border-box;
+ border-color: #B4BDC3;
+ line-height: 50px;
+ text-align: center;
+
+ &:hover {
+ cursor: pointer;
+ border-color: $color-for-buttons;
+ }
+ }
+ }
+
+ &_info {
+ display: flex;
+ flex-direction: column;
+ justify-content: space-between;
+ gap: 8px;
+ width: 100%;
+
+ @include on-tablet {
+ width: 90%;
+ }
+
+ @include on-desktop {
+ width: 81%;
+ }
+
+ &_string {
+ display: flex;
+ flex-direction: row;
+ justify-content: space-between;
+
+ &__title {
+ color: $color-for-main-text;
+ font-size: 14px;
+ }
+
+ &__text {
+ font-size: 14px;
+ color: $color-for-buttons;
+ }
+ }
+ }
+ }
+
+ &_info {
+ margin-bottom: 64px;
+
+ @include page-grid;
+
+ @include on-tablet {
+ margin-bottom: 80px;
+ }
+ &_title {
+ margin-bottom: 16px;
+ font-size: 22px;
+ }
+
+ &_string {
+ margin-bottom: 32px;
+ width: 100%;
+ height: 1px;
+ background-color: $color-for-borders;
+ }
+
+ &_about {
+ grid-column: 1 / -1;
+
+ @include on-desktop {
+ grid-column: span 12;
+ }
+
+ &_description {
+ display: flex;
+ flex-direction: column;
+ gap: 32px;
+
+ &_title {
+ margin-bottom: 16px;
+ font-size: 20px;
+ color: $color-for-buttons;
+ }
+
+ &_text {
+ font-size: 14px;
+ color: $color-for-main-text;
+ }
+ }
+ }
+
+ &_techSpecs {
+ grid-column: 1 / -1;
+
+ @include on-desktop {
+ grid-column: 14 / 25;
+ }
+
+ &_description {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+
+
+ &_opt {
+ display: flex;
+ flex-direction: row;
+ justify-content: space-between;
+
+ &__title {
+ color: $color-for-main-text;
+ font-size: 14px;
+ }
+
+ &__text {
+ font-size: 14px;
+ color: $color-for-buttons;
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/src/components/ItemInformation/ItemInformation.tsx b/src/components/ItemInformation/ItemInformation.tsx
new file mode 100644
index 0000000000..1a5d46c729
--- /dev/null
+++ b/src/components/ItemInformation/ItemInformation.tsx
@@ -0,0 +1,556 @@
+import { Link, useParams } from 'react-router-dom';
+import styles from './ItemInformation.module.scss';
+import { useAppDispatch, useAppSelector } from '../../app/hooks';
+import { getAllProductsAsync } from '../../features/getAllProductsSlice';
+import React, { useEffect, useState } from 'react';
+import { Loader } from '../Loader';
+import { addFavorite, removeFavorite } from '../../features/addFavoritesSlice';
+import { addBucket, removeBucket } from '../../features/addProductSlice';
+import { ItemsList } from '../ItemsList/ItemsList';
+
+export const ItemInformation = () => {
+ const { productId } = useParams();
+
+ useEffect(() => {
+ window.scrollTo(0, 0);
+ }, [productId]);
+
+ // список, лоадер та можлива помилка для конкретного селектора
+ const items = useAppSelector(state => state.allProducts.items);
+ const loaded = useAppSelector(state => state.allProducts.loaded);
+ const error = useAppSelector(state => state.allProducts.error);
+ const currentProduct = items.find(item => item.id === productId); // продукт, з яким потрібно працювати в цьому компоненті
+ const [currentImage, setCurrentImage] = useState(currentProduct?.images[0]);
+ const dispatch = useAppDispatch();
+ const favourites = useAppSelector(state => state.addedFavorites.items);
+ const bucketProducts = useAppSelector(state => state.addBucket.items);
+ const isClickedOnFavourite = favourites.some(
+ fav => fav.itemId === currentProduct?.id,
+ );
+ const isClickedOnBucket = bucketProducts.some(
+ bucket => bucket.itemId === currentProduct?.id,
+ );
+
+ // функція для добавлення favourite
+ const generalProducts = useAppSelector(state => state.products.items);
+
+ const handleAddDeleteFavourites = () => {
+ const productToToggle = generalProducts.find(
+ product => product.itemId === currentProduct?.id,
+ );
+
+ if (!productToToggle) {
+ return;
+ }
+
+ if (isClickedOnFavourite) {
+ dispatch(removeFavorite(productToToggle));
+ } else {
+ dispatch(addFavorite(productToToggle));
+ }
+ };
+
+ const handleAddDeleteBucket = () => {
+ const productToToggle = generalProducts.find(
+ product => product.itemId === currentProduct?.id,
+ );
+
+ if (!productToToggle) {
+ return;
+ }
+
+ if (isClickedOnBucket) {
+ dispatch(removeBucket(productToToggle.id));
+ } else {
+ dispatch(addBucket({ ...productToToggle, quantity: 1 }));
+ }
+ };
+
+ // оновлення картинки при запуску сторінки
+ useEffect(() => {
+ setCurrentImage(currentProduct?.images[0]);
+ }, [currentProduct]);
+
+ // виклик redux
+ useEffect(() => {
+ dispatch(getAllProductsAsync());
+ }, [dispatch]);
+
+ const isClickedOnImage = (image: string) => {
+ setCurrentImage(image);
+ };
+
+ // обробка лоадера
+ if (!loaded) {
+ return ;
+ }
+
+ // обробка помилки
+ if (error) {
+ return (
+
+ Failed to load products. Please try again.
+
+ );
+ }
+
+ function ucFirst(str: string | undefined) {
+ if (!str) {
+ return str;
+ }
+
+ return str[0].toUpperCase() + str.slice(1);
+ }
+
+ return (
+
+
+
+
+
+
+
{ucFirst(currentProduct?.category)}
+
+
{currentProduct?.name}
+
+
+
+
+
Back
+
+
{currentProduct?.name}
+
+
+ {/* on tablet */}
+
+ {currentProduct?.images.map(image => (
+
+
{
+ isClickedOnImage(image);
+ }}
+ src={`./${image}`}
+ alt="img"
+ />
+
+ ))}
+
+ {/** */}
+
+
+
+
+
+ {/* on phone */}
+
+ {currentProduct?.images.map(image => (
+
+
{
+ isClickedOnImage(image);
+ }}
+ src={`./${currentImage}`}
+ alt="img"
+ style={{
+ border: currentImage === image ? '1px solid #313237' : '',
+ }}
+ />
+
+ ))}
+
+ {/** */}
+
+
+
+
+
Available colors
+
+ {currentProduct?.colorsAvailable.map(color => (
+
+
+
+ ))}
+
+
+
+
+
+
Select capacity
+
+ {currentProduct?.capacityAvailable.map(capacity => (
+
+
+
+ ))}
+
+
+
+
+
+
+
${currentProduct?.priceDiscount}
+
${currentProduct?.priceRegular}
+
+
+
+
+ {isClickedOnBucket ? (
+
Selected
+ ) : (
+
Add to cart
+ )}
+
+
{
+ handleAddDeleteFavourites();
+ }}
+ className={styles.itemInformation_generalInfo_buttons_favourite}
+ >
+ {isClickedOnFavourite ? (
+
+ ) : (
+
+ )}
+
+
+
+
+
+ Screen
+
+
+ {currentProduct?.screen}
+
+
+
+
+ Resolution
+
+
+ {currentProduct?.resolution}
+
+
+
+
+ Processor
+
+
+ {currentProduct?.processor}
+
+
+
+
+ RAM
+
+
+ {currentProduct?.ram}
+
+
+
+
+
+
+
+
+
+
About
+
+
+ {currentProduct?.description.map((desc, index) => (
+
+
+
+ {desc.title}
+
+
+ {desc.text}
+
+
+
+ ))}
+
+
+
+
+
Tech specs
+
+
+
+
+
+ Screen
+
+
+ {currentProduct?.screen}
+
+
+
+
+
+ Resolution
+
+
+ {currentProduct?.resolution}
+
+
+
+
+
+ Processor
+
+
+ {currentProduct?.processor}
+
+
+
+
+
+ RAM
+
+
+ {currentProduct?.ram}
+
+
+
+
+
+ Built in memory
+
+
+ {currentProduct?.capacity}
+
+
+
+ {currentProduct?.category !== 'accessories' ? (
+
+
+
+ Camera
+
+
+ {currentProduct?.camera}
+
+
+
+
+
+ Zoom
+
+
+ {currentProduct?.zoom}
+
+
+
+ ) : (
+
+ )}
+
+
+
+ Cell
+
+
+ {currentProduct?.cell}
+
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/src/components/ItemInformation/index.ts b/src/components/ItemInformation/index.ts
new file mode 100644
index 0000000000..4ce539d56a
--- /dev/null
+++ b/src/components/ItemInformation/index.ts
@@ -0,0 +1 @@
+export * from './ItemInformation';
diff --git a/src/components/ItemsList/ItemsList.module.scss b/src/components/ItemsList/ItemsList.module.scss
new file mode 100644
index 0000000000..25adba2eb4
--- /dev/null
+++ b/src/components/ItemsList/ItemsList.module.scss
@@ -0,0 +1,57 @@
+@import '../../utils/mixins';
+@import '../../utils/vars';
+
+.newModels {
+ &_title {
+ margin-bottom: 24px;
+
+ display: flex;
+ flex-direction: row;
+ justify-content: space-between;
+
+ h2 {
+ width: 40%;
+ font-size: 22px;
+
+ @include on-tablet {
+ width: 100%;
+ font-size: 32px;
+ }
+ }
+
+ &__buttons {
+ display: flex;
+ flex-direction: row;
+ gap: 16px;
+
+ button {
+ cursor: pointer;
+ background-color: unset;
+ width: 40px;
+ height: 40px;
+ box-sizing: border-box;
+ border: 1px solid;
+ border-color: #B4BDC3;
+ line-height: 45px;
+ text-align: center;
+
+ &:disabled {
+ border-color: $color-for-borders;
+ cursor: unset;
+ }
+ }
+ }
+ }
+}
+
+.itemsList {
+ @include page-grid;
+
+ div {
+ grid-column: span 4;
+
+ @include on-desktop {
+ grid-column: span 6;
+ }
+ }
+}
diff --git a/src/components/ItemsList/ItemsList.tsx b/src/components/ItemsList/ItemsList.tsx
new file mode 100644
index 0000000000..6ba73a339c
--- /dev/null
+++ b/src/components/ItemsList/ItemsList.tsx
@@ -0,0 +1,96 @@
+import { useAppSelector } from '../../app/hooks';
+import { ItemsProduct } from '../ItemsProduct';
+import styles from './ItemsList.module.scss';
+import { useEffect, useState } from 'react';
+
+export const ItemsList = () => {
+ const products = useAppSelector(state => state.products.items);
+
+ const newProducts = products.filter(product => product.year === 2022);
+
+ const [currentIndex, setCurrentIndex] = useState(0);
+ const [itemsPerPage, setItemsPerPage] = useState(4);
+
+ useEffect(() => {
+ const updateItemsPerPage = () => {
+ if (window.innerWidth <= 640) {
+ setItemsPerPage(1);
+ } else {
+ setItemsPerPage(4);
+ }
+ };
+
+ updateItemsPerPage();
+ window.addEventListener('resize', updateItemsPerPage);
+
+ return () => {
+ window.removeEventListener('resize', updateItemsPerPage);
+ };
+ }, []);
+
+ const handleNext = () => {
+ if (currentIndex + itemsPerPage < newProducts.length) {
+ setCurrentIndex(prevIndex => prevIndex + itemsPerPage);
+ }
+ };
+
+ const handlePrev = () => {
+ if (currentIndex - itemsPerPage >= 0) {
+ setCurrentIndex(prevIndex => prevIndex - itemsPerPage);
+ }
+ };
+
+ const currentItems = newProducts.slice(
+ currentIndex,
+ currentIndex + itemsPerPage,
+ );
+
+ let title = 'Brand new models';
+
+ if (
+ location.pathname.startsWith('/phones/') ||
+ location.pathname.startsWith('/tablets/') ||
+ location.pathname.startsWith('/accessories/')
+ ) {
+ title = 'You may also like';
+ }
+
+ return (
+ <>
+
+
{title}
+
+
+
+
+
+
+
+ {currentItems.map(product => (
+
+ ))}
+
+ >
+ );
+};
diff --git a/src/components/ItemsList/index.ts b/src/components/ItemsList/index.ts
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/src/components/ItemsProduct/ItemsProduct.module.scss b/src/components/ItemsProduct/ItemsProduct.module.scss
new file mode 100644
index 0000000000..5fb338f12a
--- /dev/null
+++ b/src/components/ItemsProduct/ItemsProduct.module.scss
@@ -0,0 +1,117 @@
+@import '../../utils/mixins';
+@import '../../utils/vars';
+
+.product {
+ padding: 32px;
+ border: 1px solid;
+ border-color: $color-for-borders;
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ overflow: hidden;
+
+ &:hover {
+ box-shadow: 8px 8px 8px 0 rgba(0,0,0,0.2);
+ }
+
+ &_image {
+ margin-bottom: 16px;
+ max-width: 100%;
+ height: 55%;
+ display: block;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+
+ &_img {
+ max-width: 100%;
+ max-height: 100%;
+ object-fit: contain;
+ }
+ }
+
+ &_title {
+ height: 30px;
+ font-size: 14px;
+ text-align: left;
+ }
+
+ &_price {
+ font-size: 22px;
+ display: flex;
+ flex-direction: row;
+ gap: 8px;
+
+
+ &_noDisc {
+ color: $color-for-main-text;
+ text-decoration: line-through;
+ }
+ }
+
+ &_line {
+ width: 100%;
+ height: 1px;
+ background-color: $color-for-borders;
+ }
+
+ &_characteristic {
+ padding: 8px 0;
+
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+
+ &_cond {
+ display: flex;
+ flex-direction: row;
+ justify-content: space-between;
+
+ font-size: 12px;
+
+ &_name {
+ color: $color-for-main-text;
+ }
+ }
+ }
+
+ &_buttons {
+ display: flex;
+ flex-direction: row;
+ justify-content: space-between;
+
+ &_add {
+ height: 40px;
+ color: $color-for-white-text;
+ background-color: $color-for-buttons;
+ border: 1px solid;
+ border-color: $color-for-borders;
+ box-sizing: border-box;
+ width: 70%;
+ line-height: 40px;
+ text-align: center;
+ font-size: 14px;
+
+ &:hover {
+ cursor: pointer;
+ box-shadow: 2px 2px 2px 0 rgba(0,0,0,0.2);
+ }
+ }
+
+ &_favourite {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ width: 40px;
+ height: 40px;
+ border: 1px solid;
+ box-sizing: border-box;
+ border-color: #B4BDC3;
+
+ &:hover {
+ cursor: pointer;
+ border-color: $color-for-buttons;
+ }
+ }
+ }
+}
diff --git a/src/components/ItemsProduct/ItemsProduct.tsx b/src/components/ItemsProduct/ItemsProduct.tsx
new file mode 100644
index 0000000000..fe01f9fdcc
--- /dev/null
+++ b/src/components/ItemsProduct/ItemsProduct.tsx
@@ -0,0 +1,121 @@
+import { Link } from 'react-router-dom';
+import { useAppDispatch, useAppSelector } from '../../app/hooks';
+import { Product } from '../../types/products';
+import { addFavorite, removeFavorite } from '../../features/addFavoritesSlice';
+import { addBucket, removeBucket } from '../../features/addProductSlice';
+import styles from './ItemsProduct.module.scss';
+
+type Props = {
+ product: Product;
+ discount: boolean;
+};
+
+export const ItemsProduct: React.FC = ({ product, discount }) => {
+ const favourites = useAppSelector(state => state.addedFavorites.items);
+ const bucketProducts = useAppSelector(state => state.addBucket.items);
+ const dispatch = useAppDispatch();
+ const isClickedOnFavourite = favourites.some(fav => fav.id === product.id);
+ const isClickedOnBucket = bucketProducts.some(
+ bucket => bucket.id === product.id,
+ );
+
+ const handleAddDeleteFavourites = () => {
+ if (isClickedOnFavourite) {
+ dispatch(removeFavorite(product));
+ } else {
+ dispatch(addFavorite(product));
+ }
+ };
+
+ const handleAddDeleteBucket = () => {
+ if (isClickedOnBucket) {
+ dispatch(removeBucket(product.id));
+ } else {
+ dispatch(addBucket({ ...product, quantity: 1 }));
+ }
+ };
+
+ return (
+
+
+
+
+
{product.name}
+
+
+ {discount ? (
+ <>
+
+ ${product.price || product.priceDiscount}
+
+
+ ${product.fullPrice || product.priceRegular}
+
+ >
+ ) : (
+
+ ${product.fullPrice || product.priceRegular}
+
+ )}
+
+
+
+
+
+
+
Screen
+
+ {product.screen}
+
+
+
+
+
Capacity
+
+ {product.capacity}
+
+
+
+
+
RAM
+
+ {product.ram}
+
+
+
+
+
+
+ {isClickedOnBucket ? (
+
Selected
+ ) : (
+
Add to cart
+ )}
+
+
+ {isClickedOnFavourite ? (
+
+ ) : (
+
+ )}
+
+
+
+ );
+};
diff --git a/src/components/ItemsProduct/index.ts b/src/components/ItemsProduct/index.ts
new file mode 100644
index 0000000000..c57162120f
--- /dev/null
+++ b/src/components/ItemsProduct/index.ts
@@ -0,0 +1 @@
+export * from '../ItemsProduct/ItemsProduct';
diff --git a/src/components/Loader/Loader.scss b/src/components/Loader/Loader.scss
new file mode 100644
index 0000000000..18d5fcf375
--- /dev/null
+++ b/src/components/Loader/Loader.scss
@@ -0,0 +1,25 @@
+.Loader {
+ display: flex;
+ width: 100%;
+ justify-content: center;
+ align-items: center;
+
+ &__content {
+ border-radius: 50%;
+ width: 2em;
+ height: 2em;
+ margin: 1em auto;
+ border: 0.3em solid #ddd;
+ border-left-color: #000;
+ animation: load8 1.2s infinite linear;
+ }
+}
+
+@keyframes load8 {
+ 0% {
+ transform: rotate(0deg);
+ }
+ 100% {
+ transform: rotate(360deg);
+ }
+}
\ No newline at end of file
diff --git a/src/components/Loader/Loader.tsx b/src/components/Loader/Loader.tsx
new file mode 100644
index 0000000000..dc5a52d28a
--- /dev/null
+++ b/src/components/Loader/Loader.tsx
@@ -0,0 +1,7 @@
+import './Loader.scss';
+
+export const Loader = () => (
+
+);
diff --git a/src/components/Loader/index.ts b/src/components/Loader/index.ts
new file mode 100644
index 0000000000..d5ce981151
--- /dev/null
+++ b/src/components/Loader/index.ts
@@ -0,0 +1 @@
+export * from './Loader';
diff --git a/src/components/SliderMain/SiderMain.tsx b/src/components/SliderMain/SiderMain.tsx
new file mode 100644
index 0000000000..15e3bc935a
--- /dev/null
+++ b/src/components/SliderMain/SiderMain.tsx
@@ -0,0 +1,66 @@
+import { useCallback, useEffect, useState } from 'react';
+import styles from './SliderMain.module.scss';
+
+export const SliderMain = () => {
+ const [currentIndex, setCurrentIndex] = useState(0);
+ const images = [
+ 'img/homePage/banner-accessories.png',
+ 'img/homePage/banner-phones.png',
+ 'img/homePage/banner-tablets.png',
+ ];
+
+ const handlePrev = () => {
+ setCurrentIndex((prevIndex: number) =>
+ prevIndex === 0 ? images.length - 1 : prevIndex - 1,
+ );
+ };
+
+ const handleNext = useCallback(() => {
+ setCurrentIndex((prevIndex: number) =>
+ prevIndex === images.length - 1 ? 0 : prevIndex + 1,
+ );
+ }, [images.length]);
+
+ const handleClickOnLowerSwitch = (index: number) => {
+ setCurrentIndex(index);
+ };
+
+ useEffect(() => {
+ const interval = setInterval(() => {
+ handleNext();
+ }, 5000);
+
+ return () => clearInterval(interval);
+ }, [images.length, handleNext]);
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+ {images.map((_, index) => (
+
{
+ handleClickOnLowerSwitch(index);
+ }}
+ >
+ ))}
+
+ >
+ );
+};
diff --git a/src/components/SliderMain/SliderMain.module.scss b/src/components/SliderMain/SliderMain.module.scss
new file mode 100644
index 0000000000..e348b42bdd
--- /dev/null
+++ b/src/components/SliderMain/SliderMain.module.scss
@@ -0,0 +1,63 @@
+@import '../../utils/mixins';
+@import '../../utils/vars';
+
+.slider {
+ position: relative;
+ height: 60vh;
+ align-items: center;
+ margin-bottom: 8px;
+
+ @include page-grid;
+
+ &_buttonLeft, &_buttonRight {
+ grid-column: span 1;
+ border: 1px solid #B4BDC3;
+ background: none;
+ width: 40px;
+ height: 100%;
+ display: none;
+ cursor: pointer;
+
+ img {
+ width: 20px;
+ height: 20px;
+ }
+
+ @include on-tablet {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ }
+ }
+
+ &_img {
+ background-position: center top;
+ background-repeat: no-repeat;
+ background-size: cover;
+ height: 100%;
+ grid-column: 1 / -1;
+ transition: background-image 0.3s ease-out;
+
+ @include on-tablet {
+ grid-column: 2 / -2;
+ }
+ }
+
+ &_lowerSwitches {
+ display: flex;
+ justify-content: center;
+ gap: 8px;
+
+ .switch {
+ background-color: #E2E6E9;
+ width: 14px;
+ height: 4px;
+ cursor: pointer;
+ transition: background-color 0.3s ease;
+ }
+
+ .active {
+ background-color:#313237;
+ }
+ }
+}
diff --git a/src/components/SliderMain/index.ts b/src/components/SliderMain/index.ts
new file mode 100644
index 0000000000..77f724302b
--- /dev/null
+++ b/src/components/SliderMain/index.ts
@@ -0,0 +1 @@
+export * from './SiderMain';
diff --git a/src/features/addFavoritesSlice.tsx b/src/features/addFavoritesSlice.tsx
new file mode 100644
index 0000000000..da1b735d26
--- /dev/null
+++ b/src/features/addFavoritesSlice.tsx
@@ -0,0 +1,33 @@
+/* eslint-disable no-param-reassign */
+import { createSlice, PayloadAction } from '@reduxjs/toolkit';
+import { Product } from '../types/products';
+
+export interface FavoritesState {
+ items: Product[];
+}
+
+const initialState: FavoritesState = {
+ items: JSON.parse(localStorage.getItem('favorites') || '[]'),
+};
+
+const addFavoritesSlice = createSlice({
+ name: 'addedFavorites',
+ initialState,
+ reducers: {
+ addFavorite: (state, action: PayloadAction) => {
+ const exists = state.items.find(item => item.id === action.payload.id);
+
+ if (!exists) {
+ state.items.push(action.payload);
+ localStorage.setItem('favorites', JSON.stringify(state.items));
+ }
+ },
+ removeFavorite: (state, action: PayloadAction) => {
+ state.items = state.items.filter(item => item.id !== action.payload.id);
+ localStorage.setItem('favorites', JSON.stringify(state.items));
+ },
+ },
+});
+
+export const { addFavorite, removeFavorite } = addFavoritesSlice.actions;
+export default addFavoritesSlice.reducer;
diff --git a/src/features/addProductSlice.tsx b/src/features/addProductSlice.tsx
new file mode 100644
index 0000000000..f5591c6365
--- /dev/null
+++ b/src/features/addProductSlice.tsx
@@ -0,0 +1,59 @@
+/* eslint-disable no-param-reassign */
+import { createSlice, PayloadAction } from '@reduxjs/toolkit';
+import { ProductList } from '../types/products';
+
+export interface AddBucketSlice {
+ items: ProductList[];
+}
+
+const initialState: AddBucketSlice = {
+ items: JSON.parse(localStorage.getItem('bucket') || '[]'),
+};
+
+const addBucketSlice = createSlice({
+ name: 'addBucket',
+ initialState,
+ reducers: {
+ addBucket: (state, action: PayloadAction) => {
+ const exists = state.items.find(item => item.id === action.payload.id);
+
+ if (!exists) {
+ state.items.push(action.payload);
+ localStorage.setItem('bucket', JSON.stringify(state.items));
+ }
+ },
+ removeBucket: (state, action: PayloadAction) => {
+ state.items = state.items.filter(item => item.id !== action.payload);
+ localStorage.setItem('bucket', JSON.stringify(state.items));
+ },
+ addQuantity: (state, action: PayloadAction) => {
+ const product = state.items.find(item => item.id === action.payload);
+
+ if (product) {
+ product.quantity += 1;
+ localStorage.setItem('bucket', JSON.stringify(state.items));
+ }
+ },
+ decrementQuantity: (state, action: PayloadAction) => {
+ const product = state.items.find(item => item.id === action.payload);
+
+ if (product && product.quantity > 1) {
+ product.quantity -= 1;
+ localStorage.setItem('bucket', JSON.stringify(state.items));
+ }
+ },
+ clearBucket: state => {
+ state.items = [];
+ },
+ },
+});
+
+export const {
+ addBucket,
+ removeBucket,
+ addQuantity,
+ decrementQuantity,
+ clearBucket,
+} = addBucketSlice.actions;
+
+export default addBucketSlice.reducer;
diff --git a/src/features/getAllProductsSlice.tsx b/src/features/getAllProductsSlice.tsx
new file mode 100644
index 0000000000..2c55a9d7d9
--- /dev/null
+++ b/src/features/getAllProductsSlice.tsx
@@ -0,0 +1,67 @@
+/* eslint-disable no-param-reassign */
+import { createAsyncThunk, createSlice } from '@reduxjs/toolkit';
+import { Phone } from '../types/phones';
+
+export interface SetAllProductsInterface {
+ items: Phone[];
+ error: boolean;
+ loaded: boolean;
+}
+
+const initialState: SetAllProductsInterface = {
+ items: [],
+ error: false,
+ loaded: false,
+};
+
+export const getAllProductsAsync = createAsyncThunk(
+ 'products/getAllProducts',
+ async () => {
+ // Завантажуємо всі три файли одночасно
+ /* eslint-disable max-len */
+ const [phonesResponse, tabletsResponse, accessoriesResponse] =
+ await Promise.all([
+ fetch(
+ 'https://avramenkomarina.github.io/react_phone-catalog/api/phones.json',
+ ),
+ fetch(
+ 'https://avramenkomarina.github.io/react_phone-catalog/api/tablets.json',
+ ),
+ fetch(
+ 'https://avramenkomarina.github.io/react_phone-catalog/api/accessories.json',
+ ),
+ ]);
+ /* eslint-enable max-len */
+ // Парсимо JSON з кожного файлу
+ const [phones, tablets, accessories] = await Promise.all([
+ phonesResponse.json(),
+ tabletsResponse.json(),
+ accessoriesResponse.json(),
+ ]);
+
+ // Об'єднуємо всі дані в один масив
+ return [...phones, ...tablets, ...accessories];
+ },
+);
+
+export const getAllProductsSlice = createSlice({
+ name: 'allProducts',
+ initialState,
+ reducers: {},
+ extraReducers: builder => {
+ builder
+ .addCase(getAllProductsAsync.pending, state => {
+ state.loaded = false;
+ })
+ .addCase(getAllProductsAsync.fulfilled, (state, action) => {
+ state.loaded = true;
+ state.items = action.payload;
+ })
+ .addCase(getAllProductsAsync.rejected, state => {
+ state.error = true;
+ state.loaded = true;
+ });
+ },
+});
+
+export default getAllProductsSlice.reducer;
diff --git a/src/features/getProductsSlice.tsx b/src/features/getProductsSlice.tsx
new file mode 100644
index 0000000000..7f51e314d1
--- /dev/null
+++ b/src/features/getProductsSlice.tsx
@@ -0,0 +1,65 @@
+/* eslint-disable no-param-reassign */
+import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
+import { Product } from '../types/products';
+
+export interface SetProductsInterface {
+ items: Product[];
+ filteredProducts: Product[];
+ hasError: boolean;
+ loaded: boolean;
+ itemsPerPage: number;
+ currentPage: number;
+}
+
+const initialState: SetProductsInterface = {
+ items: [],
+ filteredProducts: [],
+ hasError: false,
+ loaded: false,
+ itemsPerPage: 4,
+ currentPage: 1,
+};
+
+export const getProductsAsync = createAsyncThunk(
+ 'products/getProductsSlice',
+ async () => {
+ const response = await fetch(
+ 'https://avramenkomarina.github.io/react_phone-catalog/api/products.json',
+ );
+ const products = await response.json();
+
+ return products;
+ },
+);
+
+export const productSlice = createSlice({
+ name: 'products',
+ initialState,
+ reducers: {
+ setItemsPerPage: (state, action) => {
+ state.itemsPerPage = action.payload;
+ state.currentPage = 1;
+ },
+ setCurrentPage: (state, action) => {
+ state.currentPage = action.payload;
+ },
+ },
+ extraReducers: builder => {
+ builder
+ .addCase(getProductsAsync.pending, state => {
+ state.loaded = false;
+ })
+ .addCase(getProductsAsync.fulfilled, (state, action) => {
+ state.loaded = true;
+ state.items = action.payload;
+ state.filteredProducts = action.payload;
+ })
+ .addCase(getProductsAsync.rejected, state => {
+ state.loaded = true;
+ state.hasError = true;
+ });
+ },
+});
+
+export const { setItemsPerPage, setCurrentPage } = productSlice.actions;
+export default productSlice.reducer;
diff --git a/src/main.tsx b/src/main.tsx
new file mode 100644
index 0000000000..a7c119ddcc
--- /dev/null
+++ b/src/main.tsx
@@ -0,0 +1,17 @@
+import { StrictMode } from 'react';
+import { createRoot } from 'react-dom/client';
+import { Provider } from 'react-redux';
+import { HashRouter } from 'react-router-dom';
+
+import { store } from './app/store';
+import { App } from './App';
+
+createRoot(document.getElementById('root')!).render(
+
+
+
+
+
+
+ ,
+);
diff --git a/src/types/accessories.ts b/src/types/accessories.ts
new file mode 100644
index 0000000000..3646d75b87
--- /dev/null
+++ b/src/types/accessories.ts
@@ -0,0 +1,26 @@
+interface Description {
+ title: string;
+ text: string[];
+}
+
+export interface Accessories {
+ id: string;
+ category: string;
+ namespaceId: string;
+ name: string;
+ capacityAvailable: string[];
+ capacity: string;
+ priceRegular: number;
+ priceDiscount: number;
+ colorsAvailable: string[];
+ color: string;
+ images: string[];
+ description: Description[];
+ screen: string;
+ resolution: string;
+ processor: string;
+ ram: string;
+ cell: string[];
+ camera: string;
+ zoom: string;
+}
diff --git a/src/types/phones.ts b/src/types/phones.ts
new file mode 100644
index 0000000000..62c1c6713e
--- /dev/null
+++ b/src/types/phones.ts
@@ -0,0 +1,26 @@
+interface Description {
+ title: string;
+ text: string[];
+}
+
+export interface Phone {
+ id: string;
+ category: string;
+ namespaceId: string;
+ name: string;
+ capacityAvailable: string[];
+ capacity: string;
+ priceRegular: number;
+ priceDiscount: number;
+ colorsAvailable: string[];
+ color: string;
+ images: string[];
+ description: Description[];
+ screen: string;
+ resolution: string;
+ processor: string;
+ ram: string;
+ camera: string;
+ zoom: string;
+ cell: string[];
+}
diff --git a/src/types/products.ts b/src/types/products.ts
new file mode 100644
index 0000000000..d5ff63eaf5
--- /dev/null
+++ b/src/types/products.ts
@@ -0,0 +1,21 @@
+export interface Product {
+ id: number;
+ category: string;
+ itemId: string;
+ name: string;
+ fullPrice: number;
+ price: number;
+ screen: string;
+ capacity: string;
+ color: string;
+ ram: string;
+ year: number;
+ image: string;
+ images?: string[];
+ priceRegular?: string;
+ priceDiscount?: string;
+}
+
+export interface ProductList extends Product {
+ quantity: number;
+}
diff --git a/src/types/tablets.ts b/src/types/tablets.ts
new file mode 100644
index 0000000000..ebc0e6b1aa
--- /dev/null
+++ b/src/types/tablets.ts
@@ -0,0 +1,26 @@
+interface Description {
+ title: string;
+ text: string[];
+}
+
+export interface Tablet {
+ id: string;
+ category: string;
+ namespaceId: string;
+ name: string;
+ capacityAvailable: string[];
+ capacity: string;
+ priceRegular: number;
+ priceDiscount: number;
+ colorsAvailable: string[];
+ color: string;
+ images: string[];
+ description: Description[];
+ screen: string;
+ resolution: string;
+ processor: string;
+ ram: string;
+ camera: string;
+ zoom: string;
+ cell: string[];
+}
diff --git a/src/utils/mixins.scss b/src/utils/mixins.scss
new file mode 100644
index 0000000000..245a39987f
--- /dev/null
+++ b/src/utils/mixins.scss
@@ -0,0 +1,78 @@
+@import '../utils/vars';
+
+@mixin on-tablet() {
+ @media (min-width: $tablet-min-width) {
+ @content;
+ }
+}
+
+@mixin on-desktop() {
+ @media (min-width: $desktop-min-width) {
+ @content;
+ }
+}
+
+@mixin on-huge-desktop() {
+ @media (min-width: $huge-desktop-min-width) {
+ @content;
+ }
+}
+
+@mixin content-padding-inline() {
+ padding-inline: $padding-inline-for-mobile;
+
+ @include on-tablet {
+ padding-inline: $padding-inline-for-tablet;
+ }
+
+ @include on-desktop {
+ max-width: 1200px;
+ margin-inline: auto;
+ }
+
+ @include on-huge-desktop {
+ padding-inline: $padding-inline-for-huge-desktop;
+ }
+}
+
+@mixin hover($property, $toValue) {
+ transition: #{$property} 0.3s;
+
+ &:hover {
+ #{$property}: $toValue;
+ }
+}
+
+@mixin page-grid {
+ --columns: 4;
+
+ display: grid;
+ column-gap: 16px;
+ grid-template-columns: repeat(var(--columns), 1fr);
+
+ @include on-tablet {
+ --columns: 12;
+ }
+
+ @include on-desktop {
+ --columns: 24;
+ }
+}
+
+a {
+ text-decoration: none;
+ color: inherit;
+}
+
+h1, h2, h3, h4, p, img {
+ padding: 0;
+ margin: 0;
+}
+
+.hidden {
+ display: none;
+}
+
+a, h1, h2, h3, h4, img, div {
+ transition: all 0.3s ease-out;
+}
diff --git a/src/utils/vars.scss b/src/utils/vars.scss
new file mode 100644
index 0000000000..b14f0268cd
--- /dev/null
+++ b/src/utils/vars.scss
@@ -0,0 +1,11 @@
+$tablet-min-width: 640px;
+$desktop-min-width: 1200px;
+$huge-desktop-min-width: 1201px;
+$padding-inline-for-mobile: 16px;
+$padding-inline-for-tablet: 24px;
+$padding-inline-for-huge-desktop: 150px;
+$color-for-borders: #E2E6E9;
+$color-for-main-text: #89939A;
+$color-for-white-text: #fff;
+$color-for-title: #000;
+$color-for-buttons: #313237;
diff --git a/vite.config.ts b/vite.config.ts
index 5a33944a9b..31b4bc5c5d 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -4,4 +4,5 @@ import react from '@vitejs/plugin-react'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
+ base: '/react_phone-catalog/',
})