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 +

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" + /> + img +

{product.name}

+
+
+
+
handleDecrementQuantity(product.id)} + style={{ + borderColor: product.quantity === 1 ? '#E2E6E9' : '', + cursor: + product.quantity === 1 ? 'not-allowed' : 'inherit', + }} + > + {product.quantity === 1 ? ( + minus + ) : ( + minus + )} +
+

+ {product.quantity} +

+
{ + handleAddQuantity(product.id); + }} + > + plus +
+
+

+ ${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 ( + <> +
+
+ + home + + home +

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 ( + <> +
+
+ + footer_logo + + +
+
    + +
  • github
  • + + +
  • contacts
  • + + +
  • rights
  • + +
+
+ +
+

Back to top

+ toTop +
+
+
+ + ); +}; 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 ( +
+
+ + home + + home +

{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 ( +
+
+ + Nice Gadgets + + + +
+ +
+
+ + favourites +
+ {favouriteAmount > 0 && {favouriteAmount}} +
+
+
+
+ + bucket_header +
+ {bucketAmount > 0 && {bucketAmount}} +
+
+
+
+ +
+ {isMenuOpen ? ( + + menu-close + + ) : ( + + menu + + )} +
+
+ ); +}; 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 ( +
+
+ + home + + home +

{ucFirst(currentProduct?.category)}

+ home +

{currentProduct?.name}

+
+ + + back +

Back

+ +

{currentProduct?.name}

+ +
+ {/* on tablet */} +
+ {currentProduct?.images.map(image => ( +
+ { + isClickedOnImage(image); + }} + src={`./${image}`} + alt="img" + /> +
+ ))} +
+ {/** */} + +
+ image +
+ + {/* 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 => ( + +
+

+ {capacity} +

+
+ + ))} +
+
+ +
+ +
+

${currentProduct?.priceDiscount}

+

${currentProduct?.priceRegular}

+
+ +
+
+ {isClickedOnBucket ? ( +

Selected

+ ) : ( +

Add to cart

+ )} +
+
{ + handleAddDeleteFavourites(); + }} + className={styles.itemInformation_generalInfo_buttons_favourite} + > + {isClickedOnFavourite ? ( + img + ) : ( + + )} +
+
+
+
+

+ 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 ( +
+ + image/product + +

{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 ? ( + like + ) : ( + dislike + )} +
+
+
+ ); +}; 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/', })