diff --git a/package-lock.json b/package-lock.json index 3f3efaac38..d42809809e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1329,6 +1329,162 @@ } } }, + "@emotion/babel-plugin": { + "version": "11.11.0", + "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.11.0.tgz", + "integrity": "sha512-m4HEDZleaaCH+XgDDsPF15Ht6wTLsgDTeR3WYj9Q/k76JtWhrJjcP4+/XlG8LGT/Rol9qUfOIztXeA84ATpqPQ==", + "requires": { + "@babel/helper-module-imports": "^7.16.7", + "@babel/runtime": "^7.18.3", + "@emotion/hash": "^0.9.1", + "@emotion/memoize": "^0.8.1", + "@emotion/serialize": "^1.1.2", + "babel-plugin-macros": "^3.1.0", + "convert-source-map": "^1.5.0", + "escape-string-regexp": "^4.0.0", + "find-root": "^1.1.0", + "source-map": "^0.5.7", + "stylis": "4.2.0" + }, + "dependencies": { + "@babel/runtime": { + "version": "7.23.1", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.1.tgz", + "integrity": "sha512-hC2v6p8ZSI/W0HUzh3V8C5g+NwSKzKPtJwSpTjwl0o297GP9+ZLQSkdvHz46CM3LqyoXxq+5G9komY+eSqSO0g==", + "requires": { + "regenerator-runtime": "^0.14.0" + } + }, + "@emotion/memoize": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.1.tgz", + "integrity": "sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA==" + }, + "escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==" + }, + "regenerator-runtime": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz", + "integrity": "sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==" + } + } + }, + "@emotion/cache": { + "version": "11.11.0", + "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.11.0.tgz", + "integrity": "sha512-P34z9ssTCBi3e9EI1ZsWpNHcfY1r09ZO0rZbRO2ob3ZQMnFI35jB536qoXbkdesr5EUhYi22anuEJuyxifaqAQ==", + "requires": { + "@emotion/memoize": "^0.8.1", + "@emotion/sheet": "^1.2.2", + "@emotion/utils": "^1.2.1", + "@emotion/weak-memoize": "^0.3.1", + "stylis": "4.2.0" + }, + "dependencies": { + "@emotion/memoize": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.1.tgz", + "integrity": "sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA==" + } + } + }, + "@emotion/hash": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.1.tgz", + "integrity": "sha512-gJB6HLm5rYwSLI6PQa+X1t5CFGrv1J1TWG+sOyMCeKz2ojaj6Fnl/rZEspogG+cvqbt4AE/2eIyD2QfLKTBNlQ==" + }, + "@emotion/is-prop-valid": { + "version": "0.8.8", + "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-0.8.8.tgz", + "integrity": "sha512-u5WtneEAr5IDG2Wv65yhunPSMLIpuKsbuOktRojfrEiEvRyC85LgPMZI63cr7NUqT8ZIGdSVg8ZKGxIug4lXcA==", + "optional": true, + "requires": { + "@emotion/memoize": "0.7.4" + } + }, + "@emotion/memoize": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.7.4.tgz", + "integrity": "sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw==", + "optional": true + }, + "@emotion/react": { + "version": "11.11.1", + "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.11.1.tgz", + "integrity": "sha512-5mlW1DquU5HaxjLkfkGN1GA/fvVGdyHURRiX/0FHl2cfIfRxSOfmxEH5YS43edp0OldZrZ+dkBKbngxcNCdZvA==", + "requires": { + "@babel/runtime": "^7.18.3", + "@emotion/babel-plugin": "^11.11.0", + "@emotion/cache": "^11.11.0", + "@emotion/serialize": "^1.1.2", + "@emotion/use-insertion-effect-with-fallbacks": "^1.0.1", + "@emotion/utils": "^1.2.1", + "@emotion/weak-memoize": "^0.3.1", + "hoist-non-react-statics": "^3.3.1" + }, + "dependencies": { + "@babel/runtime": { + "version": "7.23.1", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.1.tgz", + "integrity": "sha512-hC2v6p8ZSI/W0HUzh3V8C5g+NwSKzKPtJwSpTjwl0o297GP9+ZLQSkdvHz46CM3LqyoXxq+5G9komY+eSqSO0g==", + "requires": { + "regenerator-runtime": "^0.14.0" + } + }, + "regenerator-runtime": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz", + "integrity": "sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==" + } + } + }, + "@emotion/serialize": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.1.2.tgz", + "integrity": "sha512-zR6a/fkFP4EAcCMQtLOhIgpprZOwNmCldtpaISpvz348+DP4Mz8ZoKaGGCQpbzepNIUWbq4w6hNZkwDyKoS+HA==", + "requires": { + "@emotion/hash": "^0.9.1", + "@emotion/memoize": "^0.8.1", + "@emotion/unitless": "^0.8.1", + "@emotion/utils": "^1.2.1", + "csstype": "^3.0.2" + }, + "dependencies": { + "@emotion/memoize": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.1.tgz", + "integrity": "sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA==" + } + } + }, + "@emotion/sheet": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.2.2.tgz", + "integrity": "sha512-0QBtGvaqtWi+nx6doRwDdBIzhNdZrXUppvTM4dtZZWEGTXL/XE/yJxLMGlDT1Gt+UHH5IX1n+jkXyytE/av7OA==" + }, + "@emotion/unitless": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.1.tgz", + "integrity": "sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ==" + }, + "@emotion/use-insertion-effect-with-fallbacks": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.0.1.tgz", + "integrity": "sha512-jT/qyKZ9rzLErtrjGgdkMBn2OP8wl0G3sQlBb3YPryvKHsjvINUhVaPFfP+fpBcOkmrVOVEEHQFJ7nbj2TH2gw==" + }, + "@emotion/utils": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.2.1.tgz", + "integrity": "sha512-Y2tGf3I+XVnajdItskUCn6LX+VUDmP6lTL4fcqsXAv43dnlbZiuW4MWQW38rW/BVWSE7Q/7+XQocmpnRYILUmg==" + }, + "@emotion/weak-memoize": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.3.1.tgz", + "integrity": "sha512-EsBwpc7hBUJWAsNPBmJy4hxWx12v6bshQsldrVmjxJoc3isbxhOrF2IcCpaXxfvq03NwkI7sbsOLXbYuqF/8Ww==" + }, "@eslint/eslintrc": { "version": "0.4.3", "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.4.3.tgz", @@ -2121,9 +2277,9 @@ } }, "@mate-academy/scripts": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@mate-academy/scripts/-/scripts-1.2.0.tgz", - "integrity": "sha512-tQJkIK7KlRHSXkNiI79yGoSFAocFFY765RHOvCoPjCmLjLWeKVx9MY0x2rFMnnneg83rvcvGdVg9DvvUqRXfag==", + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@mate-academy/scripts/-/scripts-1.2.8.tgz", + "integrity": "sha512-MqvuqrG8UUzQkRc375ZUIOd23nJ0BYqae/Nn5t01aDutSqZnz1ye65W4sLHiSuQJGIuHRO0CEyJxAO72wX1efw==", "dev": true, "requires": { "@octokit/rest": "^17.11.2", @@ -2264,12 +2420,12 @@ }, "dependencies": { "@octokit/types": { - "version": "6.34.0", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-6.34.0.tgz", - "integrity": "sha512-s1zLBjWhdEI2zwaoSgyOFoKSl109CUcVBCc7biPJ3aAf6LGLU6szDvi31JPU7bxfla2lqfhjbbg/5DdFNxOwHw==", + "version": "6.41.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-6.41.0.tgz", + "integrity": "sha512-eJ2jbzjdijiL3B4PrSQaSjuF2sPEQPVCPzBvTHJD9Nz+9dw2SGH4K4xeQJ77YfTq5bRQ+bD8wT11JbeDPmxmGg==", "dev": true, "requires": { - "@octokit/openapi-types": "^11.2.0" + "@octokit/openapi-types": "^12.11.0" } } } @@ -2300,12 +2456,12 @@ }, "dependencies": { "@octokit/types": { - "version": "6.34.0", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-6.34.0.tgz", - "integrity": "sha512-s1zLBjWhdEI2zwaoSgyOFoKSl109CUcVBCc7biPJ3aAf6LGLU6szDvi31JPU7bxfla2lqfhjbbg/5DdFNxOwHw==", + "version": "6.41.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-6.41.0.tgz", + "integrity": "sha512-eJ2jbzjdijiL3B4PrSQaSjuF2sPEQPVCPzBvTHJD9Nz+9dw2SGH4K4xeQJ77YfTq5bRQ+bD8wT11JbeDPmxmGg==", "dev": true, "requires": { - "@octokit/openapi-types": "^11.2.0" + "@octokit/openapi-types": "^12.11.0" } }, "is-plain-object": { @@ -2334,12 +2490,12 @@ }, "dependencies": { "@octokit/types": { - "version": "6.34.0", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-6.34.0.tgz", - "integrity": "sha512-s1zLBjWhdEI2zwaoSgyOFoKSl109CUcVBCc7biPJ3aAf6LGLU6szDvi31JPU7bxfla2lqfhjbbg/5DdFNxOwHw==", + "version": "6.41.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-6.41.0.tgz", + "integrity": "sha512-eJ2jbzjdijiL3B4PrSQaSjuF2sPEQPVCPzBvTHJD9Nz+9dw2SGH4K4xeQJ77YfTq5bRQ+bD8wT11JbeDPmxmGg==", "dev": true, "requires": { - "@octokit/openapi-types": "^11.2.0" + "@octokit/openapi-types": "^12.11.0" } }, "universal-user-agent": { @@ -2351,27 +2507,27 @@ } }, "@octokit/openapi-types": { - "version": "11.2.0", - "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-11.2.0.tgz", - "integrity": "sha512-PBsVO+15KSlGmiI8QAzaqvsNlZlrDlyAJYcrXBCvVUxCp7VnXjkwPoFHgjEJXx3WF9BAwkA6nfCUA7i9sODzKA==", + "version": "12.11.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-12.11.0.tgz", + "integrity": "sha512-VsXyi8peyRq9PqIz/tpqiL2w3w80OgVMwBHltTml3LmVvXiphgeqmY9mvBw9Wu7e0QWk/fqD37ux8yP5uVekyQ==", "dev": true }, "@octokit/plugin-paginate-rest": { - "version": "2.17.0", - "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-2.17.0.tgz", - "integrity": "sha512-tzMbrbnam2Mt4AhuyCHvpRkS0oZ5MvwwcQPYGtMv4tUa5kkzG58SVB0fcsLulOZQeRnOgdkZWkRUiyBlh0Bkyw==", + "version": "2.21.3", + "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-2.21.3.tgz", + "integrity": "sha512-aCZTEf0y2h3OLbrgKkrfFdjRL6eSOo8komneVQJnYecAxIej7Bafor2xhuDJOIFau4pk0i/P28/XgtbyPF0ZHw==", "dev": true, "requires": { - "@octokit/types": "^6.34.0" + "@octokit/types": "^6.40.0" }, "dependencies": { "@octokit/types": { - "version": "6.34.0", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-6.34.0.tgz", - "integrity": "sha512-s1zLBjWhdEI2zwaoSgyOFoKSl109CUcVBCc7biPJ3aAf6LGLU6szDvi31JPU7bxfla2lqfhjbbg/5DdFNxOwHw==", + "version": "6.41.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-6.41.0.tgz", + "integrity": "sha512-eJ2jbzjdijiL3B4PrSQaSjuF2sPEQPVCPzBvTHJD9Nz+9dw2SGH4K4xeQJ77YfTq5bRQ+bD8wT11JbeDPmxmGg==", "dev": true, "requires": { - "@octokit/openapi-types": "^11.2.0" + "@octokit/openapi-types": "^12.11.0" } } } @@ -2418,12 +2574,12 @@ }, "dependencies": { "@octokit/types": { - "version": "6.34.0", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-6.34.0.tgz", - "integrity": "sha512-s1zLBjWhdEI2zwaoSgyOFoKSl109CUcVBCc7biPJ3aAf6LGLU6szDvi31JPU7bxfla2lqfhjbbg/5DdFNxOwHw==", + "version": "6.41.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-6.41.0.tgz", + "integrity": "sha512-eJ2jbzjdijiL3B4PrSQaSjuF2sPEQPVCPzBvTHJD9Nz+9dw2SGH4K4xeQJ77YfTq5bRQ+bD8wT11JbeDPmxmGg==", "dev": true, "requires": { - "@octokit/openapi-types": "^11.2.0" + "@octokit/openapi-types": "^12.11.0" } }, "is-plain-object": { @@ -2452,12 +2608,12 @@ }, "dependencies": { "@octokit/types": { - "version": "6.34.0", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-6.34.0.tgz", - "integrity": "sha512-s1zLBjWhdEI2zwaoSgyOFoKSl109CUcVBCc7biPJ3aAf6LGLU6szDvi31JPU7bxfla2lqfhjbbg/5DdFNxOwHw==", + "version": "6.41.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-6.41.0.tgz", + "integrity": "sha512-eJ2jbzjdijiL3B4PrSQaSjuF2sPEQPVCPzBvTHJD9Nz+9dw2SGH4K4xeQJ77YfTq5bRQ+bD8wT11JbeDPmxmGg==", "dev": true, "requires": { - "@octokit/openapi-types": "^11.2.0" + "@octokit/openapi-types": "^12.11.0" } } } @@ -2503,6 +2659,29 @@ } } }, + "@reduxjs/toolkit": { + "version": "1.9.5", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-1.9.5.tgz", + "integrity": "sha512-Rt97jHmfTeaxL4swLRNPD/zV4OxTes4la07Xc4hetpUW/vc75t5m1ANyxG6ymnEQ2FsLQsoMlYB2vV1sO3m8tQ==", + "requires": { + "immer": "^9.0.21", + "redux": "^4.2.1", + "redux-thunk": "^2.4.2", + "reselect": "^4.1.8" + }, + "dependencies": { + "immer": { + "version": "9.0.21", + "resolved": "https://registry.npmjs.org/immer/-/immer-9.0.21.tgz", + "integrity": "sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==" + } + } + }, + "@remix-run/router": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.8.0.tgz", + "integrity": "sha512-mrfKqIHnSZRyIzBcanNJmVQELTnX+qagEDlcKO90RgRBVOZGSGvZKeDihTRfWcqoDn5N/NkUcwWTccnpN18Tfg==" + }, "@rollup/plugin-node-resolve": { "version": "7.1.3", "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-7.1.3.tgz", @@ -2569,9 +2748,9 @@ } }, "@sinonjs/text-encoding": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.1.tgz", - "integrity": "sha512-+iTbntw2IZPb/anVDbypzfQa+ay64MW0Zo8aJ8gZPWMMK6/OubMVb6lUPMagqjOPnmtauXnFCACVl3O7ogjeqQ==", + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.2.tgz", + "integrity": "sha512-sXXKG+uL9IrKqViTtao2Ws6dy0znu9sOaP1di/jKGW1M6VssO8vlpXCQcpZ+jisQ1tTFAC5Jo/EOzFbggBagFQ==", "dev": true }, "@stylelint/postcss-css-in-js": { @@ -2793,6 +2972,15 @@ "@types/node": "*" } }, + "@types/hoist-non-react-statics": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz", + "integrity": "sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==", + "requires": { + "@types/react": "*", + "hoist-non-react-statics": "^3.3.0" + } + }, "@types/html-minifier-terser": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-5.1.2.tgz", @@ -2829,6 +3017,12 @@ "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", "integrity": "sha1-7ihweulOEdK4J7y+UnC86n8+ce4=" }, + "@types/lodash": { + "version": "4.14.197", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.197.tgz", + "integrity": "sha512-BMVOiWs0uNxHVlHBgzTIqJYmj+PgCo4euloGF+5m4okL3rEYzM2EEv78mw8zWSMM57dM7kVIgJ2QDvwHSoCI5g==", + "dev": true + }, "@types/mdast": { "version": "3.0.10", "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.10.tgz", @@ -2872,8 +3066,7 @@ "@types/prop-types": { "version": "15.7.4", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.4.tgz", - "integrity": "sha512-rZ5drC/jWjrArrS8BR6SIr4cWpW09RNTYt9AMZo3Jwwif+iacXAqgVjm0B0Bv/S1jhDXKHqRVNCbACkJ89RAnQ==", - "dev": true + "integrity": "sha512-rZ5drC/jWjrArrS8BR6SIr4cWpW09RNTYt9AMZo3Jwwif+iacXAqgVjm0B0Bv/S1jhDXKHqRVNCbACkJ89RAnQ==" }, "@types/q": { "version": "1.5.5", @@ -2884,7 +3077,6 @@ "version": "17.0.43", "resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.43.tgz", "integrity": "sha512-8Q+LNpdxf057brvPu1lMtC5Vn7J119xrP1aq4qiaefNioQUYANF/CYeK4NsKorSZyUGJ66g0IM+4bbjwx45o2A==", - "dev": true, "requires": { "@types/prop-types": "*", "@types/scheduler": "*", @@ -2911,8 +3103,7 @@ "@types/scheduler": { "version": "0.16.2", "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz", - "integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==", - "dev": true + "integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==" }, "@types/sinonjs__fake-timers": { "version": "8.1.1", @@ -2962,6 +3153,11 @@ "integrity": "sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ==", "dev": true }, + "@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==" + }, "@types/webpack": { "version": "4.41.32", "resolved": "https://registry.npmjs.org/@types/webpack/-/webpack-4.41.32.tgz", @@ -3118,6 +3314,11 @@ "eslint-visitor-keys": "^2.0.0" } }, + "@uiball/loaders": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@uiball/loaders/-/loaders-1.3.0.tgz", + "integrity": "sha512-w372e7PMt/s6LZ321HoghgDDU8fomamAzJfrVAdBUhsWERJEpxJMqG37NFztUq/T4J7nzzjkvZI4UX7Z2F/O6A==" + }, "@webassemblyjs/ast": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.9.0.tgz", @@ -4142,9 +4343,9 @@ } }, "before-after-hook": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.2.tgz", - "integrity": "sha512-3pZEU3NT5BFUo/AD5ERPWOgQOCZITni6iavr5AUw5AUwQjMlI0kzu5btnyD39AF0gUEsDPwJT+oY1ORBJijPjQ==", + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.3.tgz", + "integrity": "sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==", "dev": true }, "bfj": { @@ -4779,6 +4980,11 @@ } } }, + "classnames": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.2.tgz", + "integrity": "sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw==" + }, "clean-css": { "version": "4.2.4", "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-4.2.4.tgz", @@ -5646,8 +5852,7 @@ "csstype": { "version": "3.0.11", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.11.tgz", - "integrity": "sha512-sa6P2wJ+CAbgyy4KFssIb/JNMLxFvKF1pCYCSXS8ZMuqZnMsrxqI2E5sPyoTpxoPU/gVZMzr2zjOfg8GIZOMsw==", - "dev": true + "integrity": "sha512-sa6P2wJ+CAbgyy4KFssIb/JNMLxFvKF1pCYCSXS8ZMuqZnMsrxqI2E5sPyoTpxoPU/gVZMzr2zjOfg8GIZOMsw==" }, "cyclist": { "version": "1.0.1", @@ -8633,6 +8838,11 @@ "pkg-dir": "^3.0.0" } }, + "find-root": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", + "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==" + }, "find-up": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", @@ -8807,6 +9017,41 @@ "map-cache": "^0.2.2" } }, + "framer-motion": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-4.1.17.tgz", + "integrity": "sha512-thx1wvKzblzbs0XaK2X0G1JuwIdARcoNOW7VVwjO8BUltzXPyONGAElLu6CiCScsOQRI7FIk/45YTFtJw5Yozw==", + "requires": { + "@emotion/is-prop-valid": "^0.8.2", + "framesync": "5.3.0", + "hey-listen": "^1.0.8", + "popmotion": "9.3.6", + "style-value-types": "4.1.4", + "tslib": "^2.1.0" + }, + "dependencies": { + "tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + } + } + }, + "framesync": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/framesync/-/framesync-5.3.0.tgz", + "integrity": "sha512-oc5m68HDO/tuK2blj7ZcdEBRx3p1PjrgHazL8GYEpvULhrtGIFbQArN6cQS2QhW8mitffaB+VYzMjDqBxxQeoA==", + "requires": { + "tslib": "^2.1.0" + }, + "dependencies": { + "tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + } + } + }, "fresh": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", @@ -9432,6 +9677,11 @@ "resolved": "https://registry.npmjs.org/hex-color-regex/-/hex-color-regex-1.1.0.tgz", "integrity": "sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ==" }, + "hey-listen": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/hey-listen/-/hey-listen-1.0.8.tgz", + "integrity": "sha512-COpmrF2NOg4TBWUJ5UVyaCU2A88wEMkUPK4hNqyCkqHbxT92BbvfjoSozkAIIm6XhicGlJHhFdullInrdhwU8Q==" + }, "hmac-drbg": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", @@ -9442,6 +9692,14 @@ "minimalistic-crypto-utils": "^1.0.1" } }, + "hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "requires": { + "react-is": "^16.7.0" + } + }, "hoopy": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/hoopy/-/hoopy-0.1.4.tgz", @@ -12219,7 +12477,7 @@ "lodash.get": { "version": "4.4.2", "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", - "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=", + "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", "dev": true }, "lodash.isempty": { @@ -12402,9 +12660,9 @@ "integrity": "sha512-WY9wjJNQt9+PZilnLbuFKM+SwDull9+6IAguOrarOMoOHTcJ9GnXSO11+Gw6c7xtDkBkthR57OZMtZKYr+1CEw==" }, "macos-release": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/macos-release/-/macos-release-2.5.0.tgz", - "integrity": "sha512-EIgv+QZ9r+814gjJj0Bt5vSLJLzswGmSUbUpbi9AIr/fsN2IWFBl2NucV9PAiek+U1STK468tEkxmVYUtuAN3g==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/macos-release/-/macos-release-2.5.1.tgz", + "integrity": "sha512-DXqXhEM7gW59OjZO8NIjBCz9AQ1BEMrfiOAl4AYByHCtVHRF4KoGNO8mqQeM8lRCtQe/UnJ4imO/d2HdkKsd+A==", "dev": true }, "magic-string": { @@ -13267,7 +13525,7 @@ "isarray": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", + "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==", "dev": true }, "path-to-regexp": { @@ -13298,9 +13556,9 @@ } }, "node-fetch": { - "version": "2.6.7", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", - "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", "dev": true, "requires": { "whatwg-url": "^5.0.0" @@ -13309,19 +13567,19 @@ "tr46": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", "dev": true }, "webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", "dev": true }, "whatwg-url": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha1-lmRU6HZUYuN2RNNib2dCzotwll0=", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", "dev": true, "requires": { "tr46": "~0.0.3", @@ -14238,6 +14496,24 @@ "ts-pnp": "^1.1.6" } }, + "popmotion": { + "version": "9.3.6", + "resolved": "https://registry.npmjs.org/popmotion/-/popmotion-9.3.6.tgz", + "integrity": "sha512-ZTbXiu6zIggXzIliMi8LGxXBF5ST+wkpXGEjeTUDUOCdSQ356hij/xjeUdv0F8zCQNeqB1+PR5/BB+gC+QLAPw==", + "requires": { + "framesync": "5.3.0", + "hey-listen": "^1.0.8", + "style-value-types": "4.1.4", + "tslib": "^2.1.0" + }, + "dependencies": { + "tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + } + } + }, "portfinder": { "version": "1.0.28", "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.28.tgz", @@ -15654,6 +15930,22 @@ "whatwg-fetch": "^3.4.1" } }, + "react-awesome-reveal": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/react-awesome-reveal/-/react-awesome-reveal-4.2.5.tgz", + "integrity": "sha512-oHYoYvgNgfMVQ13UfgO650AkDS58aFGXhoIZDsHMAVjyHfmpG46T1ZXL8t07gO+yPnQ9dQREo7v2p40shxS3GA==", + "requires": { + "react-intersection-observer": "^9.4.3", + "react-is": "^18.2.0" + }, + "dependencies": { + "react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==" + } + } + }, "react-dev-utils": { "version": "11.0.4", "resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-11.0.4.tgz", @@ -15793,21 +16085,89 @@ "scheduler": "^0.20.2" } }, + "react-easy-swipe": { + "version": "0.0.21", + "resolved": "https://registry.npmjs.org/react-easy-swipe/-/react-easy-swipe-0.0.21.tgz", + "integrity": "sha512-OeR2jAxdoqUMHIn/nS9fgreI5hSpgGoL5ezdal4+oO7YSSgJR8ga+PkYGJrSrJ9MKlPcQjMQXnketrD7WNmNsg==", + "requires": { + "prop-types": "^15.5.8" + } + }, "react-error-overlay": { "version": "6.0.10", "resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.10.tgz", "integrity": "sha512-mKR90fX7Pm5seCOfz8q9F+66VCc1PGsWSBxKbITjfKVQHMNF2zudxHnMdJiB1fRCb+XsbQV9sO9DCkgsMQgBIA==" }, + "react-intersection-observer": { + "version": "9.5.2", + "resolved": "https://registry.npmjs.org/react-intersection-observer/-/react-intersection-observer-9.5.2.tgz", + "integrity": "sha512-EmoV66/yvksJcGa1rdW0nDNc4I1RifDWkT50gXSFnPLYQ4xUptuDD4V7k+Rj1OgVAlww628KLGcxPXFlOkkU/Q==" + }, "react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, + "react-redux": { + "version": "8.1.2", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-8.1.2.tgz", + "integrity": "sha512-xJKYI189VwfsFc4CJvHqHlDrzyFTY/3vZACbE+rr/zQ34Xx1wQfB4OTOSeOSNrF6BDVe8OOdxIrAnMGXA3ggfw==", + "requires": { + "@babel/runtime": "^7.12.1", + "@types/hoist-non-react-statics": "^3.3.1", + "@types/use-sync-external-store": "^0.0.3", + "hoist-non-react-statics": "^3.3.2", + "react-is": "^18.0.0", + "use-sync-external-store": "^1.0.0" + }, + "dependencies": { + "react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==" + } + } + }, "react-refresh": { "version": "0.8.3", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.8.3.tgz", "integrity": "sha512-X8jZHc7nCMjaCqoU+V2I0cOhNW+QMBwSUkeXnTi8IPe6zaRWfn60ZzvFDZqWPfmSJfjub7dDW1SP0jaHWLu/hg==" }, + "react-responsive-carousel": { + "version": "3.2.23", + "resolved": "https://registry.npmjs.org/react-responsive-carousel/-/react-responsive-carousel-3.2.23.tgz", + "integrity": "sha512-pqJLsBaKHWJhw/ItODgbVoziR2z4lpcJg+YwmRlSk4rKH32VE633mAtZZ9kDXjy4wFO+pgUZmDKPsPe1fPmHCg==", + "requires": { + "classnames": "^2.2.5", + "prop-types": "^15.5.8", + "react-easy-swipe": "^0.0.21" + } + }, + "react-reveal": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/react-reveal/-/react-reveal-1.2.2.tgz", + "integrity": "sha512-JCv3fAoU6Z+Lcd8U48bwzm4pMZ79qsedSXYwpwt6lJNtj/v5nKJYZZbw3yhaQPPgYePo3Y0NOCoYOq/jcsisuw==", + "requires": { + "prop-types": "^15.5.10" + } + }, + "react-router": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.15.0.tgz", + "integrity": "sha512-NIytlzvzLwJkCQj2HLefmeakxxWHWAP+02EGqWEZy+DgfHHKQMUoBBjUQLOtFInBMhWtb3hiUy6MfFgwLjXhqg==", + "requires": { + "@remix-run/router": "1.8.0" + } + }, + "react-router-dom": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.15.0.tgz", + "integrity": "sha512-aR42t0fs7brintwBGAv2+mGlCtgtFQeOzK0BM1/OiqEzRejOZtpMZepvgkscpMUnKb8YO84G7s3LsHnnDNonbQ==", + "requires": { + "@remix-run/router": "1.8.0", + "react-router": "6.15.0" + } + }, "react-scripts": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/react-scripts/-/react-scripts-4.0.3.tgz", @@ -15987,6 +16347,19 @@ "strip-indent": "^3.0.0" } }, + "redux": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz", + "integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==", + "requires": { + "@babel/runtime": "^7.9.2" + } + }, + "redux-thunk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.4.2.tgz", + "integrity": "sha512-+P3TjtnP0k/FEjcBL5FZpoovtvrTNT/UXd4/sluaSyrURlSlhLSzEdfsTBW7WsKB6yPvgd7q/iZPICFjW4o57Q==" + }, "regenerate": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", @@ -16315,6 +16688,11 @@ "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", "integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=" }, + "reselect": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-4.1.8.tgz", + "integrity": "sha512-ab9EmR80F/zQTMNeneUr4cv+jSwPJgIlvEmVwLerwrWVbpLlBuls9XHzIeTFy4cegU2NHBp3va0LKOzU5qFEYQ==" + }, "resolve": { "version": "1.18.1", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.18.1.tgz", @@ -17507,6 +17885,11 @@ "tweetnacl": "~0.14.0" } }, + "ssr-window": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/ssr-window/-/ssr-window-4.0.2.tgz", + "integrity": "sha512-ISv/Ch+ig7SOtw7G2+qkwfVASzazUnvlDTwypdLoPoySv+6MqlOV10VwPSE6EWkGjhW50lUmghPmpYZXMu/+AQ==" + }, "ssri": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/ssri/-/ssri-8.0.1.tgz", @@ -17765,6 +18148,22 @@ "integrity": "sha1-eVjHk+R+MuB9K1yv5cC/jhLneQI=", "dev": true }, + "style-value-types": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/style-value-types/-/style-value-types-4.1.4.tgz", + "integrity": "sha512-LCJL6tB+vPSUoxgUBt9juXIlNJHtBMy8jkXzUJSBzeHWdBu6lhzHqCvLVkXFGsFIlNa2ln1sQHya/gzaFmB2Lg==", + "requires": { + "hey-listen": "^1.0.8", + "tslib": "^2.1.0" + }, + "dependencies": { + "tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + } + } + }, "stylehacks": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-4.0.3.tgz", @@ -17992,6 +18391,11 @@ "postcss-value-parser": "^4.1.0" } }, + "stylis": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", + "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==" + }, "sugarss": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/sugarss/-/sugarss-2.0.0.tgz", @@ -18069,6 +18473,14 @@ "util.promisify": "~1.0.0" } }, + "swiper": { + "version": "9.4.1", + "resolved": "https://registry.npmjs.org/swiper/-/swiper-9.4.1.tgz", + "integrity": "sha512-1nT2T8EzUpZ0FagEqaN/YAhRj33F2x/lN6cyB0/xoYJDMf8KwTFT3hMOeoB8Tg4o3+P/CKqskP+WX0Df046fqA==", + "requires": { + "ssr-window": "^4.0.2" + } + }, "symbol-tree": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", @@ -18865,6 +19277,11 @@ "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==" }, + "use-sync-external-store": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz", + "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==" + }, "util": { "version": "0.11.1", "resolved": "https://registry.npmjs.org/util/-/util-0.11.1.tgz", diff --git a/package.json b/package.json index b554f8b8b0..17ac95889e 100755 --- a/package.json +++ b/package.json @@ -7,19 +7,32 @@ "license": "GPL-3.0", "dependencies": { "@cypress/react": "^5.12.4", + "@emotion/react": "^11.11.1", + "@reduxjs/toolkit": "^1.9.5", + "@uiball/loaders": "^1.3.0", "bulma": "^0.9.3", + "classnames": "^2.3.2", + "framer-motion": "^4.1.17", + "lodash": "^4.17.21", "react": "^17.0.2", + "react-awesome-reveal": "^4.2.5", "react-dom": "^17.0.2", - "react-scripts": "^4.0.3" + "react-redux": "^8.1.2", + "react-responsive-carousel": "^3.2.23", + "react-reveal": "^1.2.2", + "react-router-dom": "^6.15.0", + "react-scripts": "^4.0.3", + "swiper": "^9.4.1" }, "devDependencies": { "@cypress/webpack-dev-server": "^1.8.4", "@mate-academy/cypress-tools": "^1.0.4", "@mate-academy/eslint-config-react": "*", "@mate-academy/eslint-config-react-typescript": "*", - "@mate-academy/scripts": "^1.2.1", + "@mate-academy/scripts": "^1.2.8", "@mate-academy/students-ts-config": "*", "@mate-academy/stylelint-config": "*", + "@types/lodash": "^4.14.197", "@types/node": "^17.0.23", "@types/react": "^17.0.43", "@types/react-dom": "^17.0.14", diff --git a/public/img/fav-icon.png b/public/img/fav-icon.png new file mode 100644 index 0000000000..ceafde1dbe Binary files /dev/null and b/public/img/fav-icon.png differ diff --git a/public/index.html b/public/index.html index 4b622dad39..ee4b9f54ab 100644 --- a/public/index.html +++ b/public/index.html @@ -4,8 +4,10 @@ Phone catalog +
+ diff --git a/src/App.scss b/src/App.scss index 71bc413aad..033e829e59 100644 --- a/src/App.scss +++ b/src/App.scss @@ -1 +1,26 @@ -// not empty +@import "styles/global-imports"; + +.app { + min-height: 100vh; + display: flex; + flex-direction: column; + + &__section { + flex: 1 0 auto; + display: flex; + flex-direction: column; + } + + &-with-menu { + overflow: hidden; + } +} + +::-webkit-scrollbar { + width: 8px; +} + +::-webkit-scrollbar-thumb { + background-color: $c-primary; + border-radius: 4px; +} diff --git a/src/App.tsx b/src/App.tsx index a1715e52b3..d4a5ed00e6 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,9 +1,79 @@ +import { Navigate, Route, Routes } from 'react-router-dom'; +import { AnimatePresence } from 'framer-motion'; import './App.scss'; +import { Header } from './components/Header'; +import { HomePage } from './pages/HomePage'; +import { Footer } from './components/Footer'; +import { PhonesPage } from './pages/PhonesPage'; +import { TabletPage } from './pages/TabletsPage/TabletsPage'; +import { AccessoriesPage } from './pages/AccessoriesPage/AccessoriesPage'; +import { ProductDetailsPage } from './pages/ProductDetailsPage'; +import { CartPage } from './pages/CartPage/CartPage'; +import { FavoritesPage } from './pages/FavoritesPage/FavoritesPage'; +import { PageNotFound } from './pages/PageNotFound/PageNotFound'; + const App = () => ( -
-

React Phone Catalog

-
+ +
+
+ +
+
+ + } /> + } /> + + + } /> + + } + /> + + + + } /> + + } + /> + + + + } /> + + } + /> + + + } + /> + + } + /> + + } + /> + +
+
+ +
+
+
+
); export default App; diff --git a/src/components/Button/ButtonBack/ButtonBack.scss b/src/components/Button/ButtonBack/ButtonBack.scss new file mode 100644 index 0000000000..04fea323db --- /dev/null +++ b/src/components/Button/ButtonBack/ButtonBack.scss @@ -0,0 +1,22 @@ +@import "../../../styles/global-imports"; + +.button-back { + display: flex; + gap: 6px; + + padding: 40px 0 16px; + background-color: $c-white; + cursor: pointer; + + @extend %small-text; + color: $c-secondary; + + @include hover(color, $c-primary); + + &--arrow { + width: 16px; + height: 16px; + background: url("../../../images/icons/Arrow.svg") no-repeat center; + transform: rotate(180deg); + } +} diff --git a/src/components/Button/ButtonBack/ButtonBack.tsx b/src/components/Button/ButtonBack/ButtonBack.tsx new file mode 100644 index 0000000000..c6d8e6a993 --- /dev/null +++ b/src/components/Button/ButtonBack/ButtonBack.tsx @@ -0,0 +1,18 @@ +import './ButtonBack.scss'; + +export const ButtonBack = () => { + const handleGoBack = () => ( + window.history.back() + ); + + return ( + + ); +}; diff --git a/src/components/Button/ButtonCart/ButtonCart.scss b/src/components/Button/ButtonCart/ButtonCart.scss new file mode 100644 index 0000000000..233b931cb0 --- /dev/null +++ b/src/components/Button/ButtonCart/ButtonCart.scss @@ -0,0 +1,37 @@ +@import "../../../styles/global-imports"; + +.button-cart { + width: 100%; + height: 40px; + + display: flex; + align-items: center; + justify-content: center; + + color: $c-white; + background-color: $c-primary; + border: 1px solid $c-primary; + + cursor: pointer; + + @extend %body-text; + + &:not(.button-cart--added) { + @include onTabletAndDesktop { + @include hover(background-color, $c-white); + @include hover(color, $c-primary); + } + } + + &--added { + color: $c-green; + background-color: $c-white; + + @include onTabletAndDesktop { + @include hover(background-color, $c-green); + @include hover(border-color, $c-green); + @include hover(color, $c-white); + } + + } +} diff --git a/src/components/Button/ButtonCart/ButtonCart.tsx b/src/components/Button/ButtonCart/ButtonCart.tsx new file mode 100644 index 0000000000..8936a23282 --- /dev/null +++ b/src/components/Button/ButtonCart/ButtonCart.tsx @@ -0,0 +1,49 @@ +import { useContext } from 'react'; + +import classNames from 'classnames'; +import './ButtonCart.scss'; + +import { Product } from '../../../types/Product'; +import { CartContext } from '../../../contexts/CartContextProvider'; + +type Props = { + product: Product, +}; + +export const ButtonCart: React.FC = ({ product }) => { + const { + cart, + addToCart, + removeFromCart, + } = useContext(CartContext); + + const isAdded = cart.find(currentItem => ( + currentItem.product.phoneId === product.phoneId)); + + const handleAddCart = () => { + if (isAdded) { + removeFromCart(product.phoneId); + } else { + const cartItem = { + id: product.phoneId, + quantity: 1, + product, + }; + + addToCart(cartItem); + } + }; + + return ( + + ); +}; diff --git a/src/components/Button/ButtonCart/index.ts b/src/components/Button/ButtonCart/index.ts new file mode 100644 index 0000000000..3df95c6f39 --- /dev/null +++ b/src/components/Button/ButtonCart/index.ts @@ -0,0 +1 @@ +export * from './ButtonCart'; diff --git a/src/components/Button/ButtonFavorites/ButtonFavorites.scss b/src/components/Button/ButtonFavorites/ButtonFavorites.scss new file mode 100644 index 0000000000..29da471c62 --- /dev/null +++ b/src/components/Button/ButtonFavorites/ButtonFavorites.scss @@ -0,0 +1,18 @@ +@import "../../../styles/global-imports"; + +.button-favorites { + width: 40px; + height: 40px; + + background: url("../../../images/icons/icon-favourites.svg") no-repeat center; + + border: 1px solid $c-icons; + + cursor: pointer; + + @include hover(border-color, $c-primary); + + &-active { + background-image: url("../../../images/icons/icon-favourites-active.svg"); + } +} diff --git a/src/components/Button/ButtonFavorites/ButtonFavorites.tsx b/src/components/Button/ButtonFavorites/ButtonFavorites.tsx new file mode 100644 index 0000000000..877ceeb963 --- /dev/null +++ b/src/components/Button/ButtonFavorites/ButtonFavorites.tsx @@ -0,0 +1,41 @@ +/* eslint-disable jsx-a11y/control-has-associated-label */ +import { useContext } from 'react'; + +import classNames from 'classnames'; +import './ButtonFavorites.scss'; + +import { FavoriteContext } from '../../../contexts/FavoriteContextProvider'; +import { Product } from '../../../types/Product'; + +type Props = { + product: Product; +}; + +export const ButtonFavorites: React.FC = ({ product }) => { + const { + favorites, + addToFavorite, + removeFromFavorite, + } = useContext(FavoriteContext); + + const isAdded = favorites.find(({ phoneId }) => phoneId === product.phoneId); + + const handleAddFavorites = () => { + if (isAdded) { + removeFromFavorite(product.phoneId); + } else { + addToFavorite(product); + } + }; + + return ( + + ); +}; diff --git a/src/components/Button/ButtonToUp/index.ts b/src/components/Button/ButtonToUp/index.ts new file mode 100644 index 0000000000..b91cf22580 --- /dev/null +++ b/src/components/Button/ButtonToUp/index.ts @@ -0,0 +1 @@ +export * from './ButtonToUp'; diff --git a/src/components/DropDown/DropDown.scss b/src/components/DropDown/DropDown.scss new file mode 100644 index 0000000000..be198f058d --- /dev/null +++ b/src/components/DropDown/DropDown.scss @@ -0,0 +1,88 @@ +@import "../../styles/global-imports"; + +.drop-down { + width: 182px; + margin: 40px 0 26px; + + position: relative; + z-index: 10; + + @include onMobile { + width: 100%; + margin: 10px 0 0; + } + + &-open { + z-index: 11; + + opacity: 1; + } + + &--title { + color: $c-secondary; + @extend %small-text; + } + + &__top { + display: flex; + justify-content: space-between; + align-items: center; + width: 182px; + padding: 10px; + + cursor: pointer; + + @extend %body-text; + + background-color: $c-white; + border: 1px solid $c-icons; + + @include hover(border-color, $c-primary); + + @include onMobile { + width: 100%; + } + } + + &--icon { + background: $arrow-image no-repeat center; + width: 16px; + height: 16px; + + transform: rotate(90deg); + } + + &--current { + padding-left: 12px; + } + + &__content { + position: absolute; + left: 0; + right: 0; + + background-color: $c-white; + border: 1px solid $c-elements; + } + + &--is-active { + display: none; + } + + &--item { + display: flex; + align-items: center; + + width: 94%; + height: 32px; + padding-left: 12px; + + color: $c-secondary; + + cursor: pointer; + + @extend %body-text; + @include hover(background-color, $c-hover_bg); + @include hover(color, $c-primary); + } +} diff --git a/src/components/DropDown/DropDown.tsx b/src/components/DropDown/DropDown.tsx new file mode 100644 index 0000000000..3180fe7ce4 --- /dev/null +++ b/src/components/DropDown/DropDown.tsx @@ -0,0 +1,124 @@ +import { useState, useEffect } from 'react'; +import { Link, useSearchParams } from 'react-router-dom'; +import { motion } from 'framer-motion'; + +import classNames from 'classnames'; + +import './DropDown.scss'; + +import { Option } from '../../types/SortTypes'; +import { getSearchWith } from '../../helpers/searchHelper'; + +type Props = { + options: Option[], + startValue: string, + searchName: string, + label: string, +}; + +export const DropDown: React.FC = ({ + options, + startValue, + searchName, + label, +}) => { + const [isOpen, setIsOpen] = useState(false); + const [selectedOption, setSelectedOption] = useState(startValue); + const [searchParams] = useSearchParams(); + + const toggleDropDown = () => { + setIsOpen(!isOpen); + searchParams.set('page', '1'); + }; + + const handleOptionSelect = (option: string) => { + setSelectedOption(option); + setIsOpen(false); + }; + + useEffect(() => { + const paramsValue = searchParams.get(searchName); + + if (paramsValue + && options.find(currentOption => currentOption.value === paramsValue)) { + setSelectedOption(paramsValue); + } else { + setSelectedOption(startValue); + } + }, [ + options, + searchName, + startValue, + searchParams, + ]); + + const subMenuAnimate = { + enter: { + opacity: 1, + rotateX: 0, + transition: { + duration: 0.3, + }, + display: 'block', + }, + exit: { + opacity: 0, + rotateX: -15, + transition: { + duration: 0.2, + }, + transitionEnd: { + display: 'none', + }, + }, + }; + + return ( +
+ + + + + {selectedOption} + + +
+ + + + {options.map(currentOption => ( +
  • + handleOptionSelect(currentOption.value)} + > + {currentOption.value} + +
  • + ))} +
    +
    + ); +}; diff --git a/src/components/DropDown/index.ts b/src/components/DropDown/index.ts new file mode 100644 index 0000000000..69092242f7 --- /dev/null +++ b/src/components/DropDown/index.ts @@ -0,0 +1 @@ +export * from './DropDown'; diff --git a/src/components/Footer/Footer.scss b/src/components/Footer/Footer.scss new file mode 100644 index 0000000000..08a1767386 --- /dev/null +++ b/src/components/Footer/Footer.scss @@ -0,0 +1,43 @@ +@import "../../styles/global-imports"; + +.footer { + display: flex; + align-items: center; + + height: 96px; + + border: 1px solid $c-elements; + + @include onMobile { + height: 160px; + } + + &-content { + display: flex; + align-items: center; + justify-content: space-between; + + width: 100%; + + @include onMobile { + flex-direction: column; + gap: 20px; + } + } + + &__list { + display: flex; + text-align: center; + + gap: 64px; + } + + &--item { + &-link { + @extend %uppercase; + color: $c-secondary; + + @include hover(color, $c-primary); + } + } +} diff --git a/src/components/Footer/Footer.tsx b/src/components/Footer/Footer.tsx new file mode 100644 index 0000000000..db02653a90 --- /dev/null +++ b/src/components/Footer/Footer.tsx @@ -0,0 +1,44 @@ +/* eslint-disable react/jsx-no-target-blank */ +import { ButtonToUp } from '../Button/ButtonToUp'; +import { Logo } from '../Logo'; + +import './Footer.scss'; + +const FOOTER_NAVIGATION = { + github: 'https://github.com/ValeraViachalo', + contacts: 'https://www.instagram.com/hiwrldp/', + rights: 'https://github.com/mate-academy/react_phone-catalog', +}; + +export const Footer = () => ( +
    +
    + + + + + +
    +
    +); 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/Header/Header.scss b/src/components/Header/Header.scss new file mode 100644 index 0000000000..f619668116 --- /dev/null +++ b/src/components/Header/Header.scss @@ -0,0 +1,22 @@ +@import "../../styles/global-imports"; + +.header { + display: flex; + justify-content: space-between; + + border-bottom: 1px solid $c-elements; + + @include onTablet { + justify-content: space-around; + } + + &__navigation { + display: flex; + gap: 64px; + margin: 0 10px; + + @include onTabletAndDesktop { + margin: 0 24px; + } + } +} diff --git a/src/components/Header/Header.tsx b/src/components/Header/Header.tsx new file mode 100644 index 0000000000..4890d48a58 --- /dev/null +++ b/src/components/Header/Header.tsx @@ -0,0 +1,22 @@ +import { Logo } from '../Logo'; +import { Navigation } from './Navigation'; +import { Search } from './Search/Search'; + +import './Header.scss'; +import { Menu } from './Menu'; +import { HeaderActions } from './HeaderActions'; + +export const Header = () => ( +
    +
    + + +
    + + + + + + +
    +); diff --git a/src/components/Header/HeaderActions/HeaderActions.scss b/src/components/Header/HeaderActions/HeaderActions.scss new file mode 100644 index 0000000000..daf84e544a --- /dev/null +++ b/src/components/Header/HeaderActions/HeaderActions.scss @@ -0,0 +1,9 @@ +@import "../../../styles/global-imports"; + +.Actions { + display: none; + + @include onDesktop { + display: flex; + } +} diff --git a/src/components/Header/HeaderActions/HeaderActions.tsx b/src/components/Header/HeaderActions/HeaderActions.tsx new file mode 100644 index 0000000000..0584e5c83c --- /dev/null +++ b/src/components/Header/HeaderActions/HeaderActions.tsx @@ -0,0 +1,20 @@ +import { NavigationLink } from '../Navigation/NavigatioLink'; +import { IconLinks } from './IconLinks'; + +import './HeaderActions.scss'; + +const ACTIONS_PAGES = ['favorites', 'cart']; + +export const HeaderActions = () => ( +
    + {ACTIONS_PAGES.map(currentAction => ( + + + + ))} +
    +); diff --git a/src/components/Header/HeaderActions/IconLinks/IconLinks.scss b/src/components/Header/HeaderActions/IconLinks/IconLinks.scss new file mode 100644 index 0000000000..52893edcda --- /dev/null +++ b/src/components/Header/HeaderActions/IconLinks/IconLinks.scss @@ -0,0 +1,31 @@ +@import "../../../../styles/global-imports"; + +.icon-links { + position: relative; + display: inline-block; + + @include onTabletAndDesktop { + display: block; + margin: 0; + } + + &--active { + display: flex; + align-items: flex-start; + justify-content: center; + + width: 14px; + height: 14px; + border-radius: 100%; + background-color: $c-red; + position: absolute; + top: -8px; + left: 10px; + + font-weight: 300; + font-size: 10px; + + color: $c-white; + @extend %small-text; + } +} diff --git a/src/components/Header/HeaderActions/IconLinks/IconLinks.tsx b/src/components/Header/HeaderActions/IconLinks/IconLinks.tsx new file mode 100644 index 0000000000..c58a34c139 --- /dev/null +++ b/src/components/Header/HeaderActions/IconLinks/IconLinks.tsx @@ -0,0 +1,43 @@ +import { useContext } from 'react'; + +import { FavoriteContext } from '../../../../contexts/FavoriteContextProvider'; +import cartIcon from '../../../../images/icons/icon-cart.svg'; + +import { CartContext } from '../../../../contexts/CartContextProvider'; +import favoritesIcon from '../../../../images/icons/icon-favourites.svg'; + +import './IconLinks.scss'; + +type Props = { + type: 'favorites' | 'cart', +}; + +export const IconLinks: React.FC = ({ type }) => { + const images = { + favorites: favoritesIcon, + cart: cartIcon, + }; + + const { cart } = useContext(CartContext); + const { favorites } = useContext(FavoriteContext); + + const contexts = { + favorites, + cart, + }; + + return ( +
    + {type} + {contexts[type].length > 0 && ( +
    + {contexts[type].length} +
    + )} +
    + ); +}; diff --git a/src/components/Header/HeaderActions/IconLinks/index.ts b/src/components/Header/HeaderActions/IconLinks/index.ts new file mode 100644 index 0000000000..d0801f28a0 --- /dev/null +++ b/src/components/Header/HeaderActions/IconLinks/index.ts @@ -0,0 +1 @@ +export * from './IconLinks'; diff --git a/src/components/Header/HeaderActions/index.ts b/src/components/Header/HeaderActions/index.ts new file mode 100644 index 0000000000..f0ff49cea7 --- /dev/null +++ b/src/components/Header/HeaderActions/index.ts @@ -0,0 +1 @@ +export * from './HeaderActions'; diff --git a/src/components/Header/Menu/Menu.scss b/src/components/Header/Menu/Menu.scss new file mode 100644 index 0000000000..c96033e2c1 --- /dev/null +++ b/src/components/Header/Menu/Menu.scss @@ -0,0 +1,121 @@ +@import "../../../styles/global-imports"; + +.menu { + background: center no-repeat; + + @include onDesktop { + display: none; + } + + &__content--top { + position: relative; + display: flex; + justify-content: flex-end; + } + + &__open, + &__close { + width: 32px; + height: 32px; + margin: 16px 5px 0; + + cursor: pointer; + background: none; + + } + + &__open { + background-image: url("../../../images/icons/menu-burger.svg"); + } + + &__close { + background-image: url("../../../images/icons/icon-close.svg"); + + position: absolute; + right: 7%; + } + + &__content { + display: none; + position: fixed; + top: 0; + bottom: 0; + left: 0; + right: 0; + + background-color: #fff; + z-index: 1000; + opacity: 0; + + animation: fadeIn 0.3s forwards; + + &.isActive { + display: block; + } + + @keyframes fadeIn { + 0% { + opacity: 0; + } + + 100% { + opacity: 1; + } + } + } + + &__list { + margin-top: 50px; + position: relative; + } + + &__item { + display: flex; + justify-content: center; + + padding: 18px 0; + + @extend %uppercase; + + &:active { + background-color: $c-hover_bg; + } + + & > a { + display: flex; + align-items: center; + justify-content: center; + gap: 4px; + + width: 100%; + height: 100%; + + transition: all $effect-duration; + color: $c-secondary; + } + + &-action { + position: relative; + + &-length { + display: flex; + align-items: flex-start; + justify-content: center; + + width: 14px; + height: 14px; + border-radius: 100%; + background-color: $c-red; + color: $c-white; + + @extend %small-text; + } + } + + &--link { + &-active { + color: $c-primary !important; + } + } + } +} diff --git a/src/components/Header/Menu/Menu.tsx b/src/components/Header/Menu/Menu.tsx new file mode 100644 index 0000000000..1073cf898b --- /dev/null +++ b/src/components/Header/Menu/Menu.tsx @@ -0,0 +1,114 @@ +/* eslint-disable jsx-a11y/control-has-associated-label */ +import classNames from 'classnames'; +import { useContext, useState } from 'react'; +import { NavLink } from 'react-router-dom'; + +import './Menu.scss'; +import { CartContext } from '../../../contexts/CartContextProvider'; +import { FavoriteContext } from '../../../contexts/FavoriteContextProvider'; + +const navigation = [ + 'home', + 'phones', + 'tablets', + 'accessories', +]; + +type ContextKey = 'favorites' | 'cart'; + +const navigationAction: ContextKey[] = [ + 'favorites', + 'cart', +]; + +export const Menu: React.FC = () => { + const classListBody = document.body.classList; + const [isActiveMenu, setIsActiveMenu] = useState(false); + + const handleOpenMenu = () => { + setIsActiveMenu(true); + classListBody.add('app-with-menu'); + }; + + const handleCloseMenu = () => { + setIsActiveMenu(false); + classListBody.remove('app-with-menu'); + }; + + const { cart } = useContext(CartContext); + const { favorites } = useContext(FavoriteContext); + + const contexts = { + favorites, + cart, + }; + + return ( +
    +
    + +
      + {navigation.map(currentLink => ( +
    • + classNames( + 'menu__item--link', + { 'menu__item--link-active': isActive }, + )} + > + {currentLink} + +
    • + ))} + + {navigationAction.map((currentLink: ContextKey) => ( +
    • + classNames( + 'menu__item--link', + { 'menu__item--link-active': isActive }, + )} + > + {currentLink} + {contexts[currentLink as ContextKey].length > 0 && ( +
      + {contexts[currentLink as ContextKey].length} +
      + )} +
      +
    • + ))} +
    +
    + + ); +}; diff --git a/src/components/Header/Menu/index.ts b/src/components/Header/Menu/index.ts new file mode 100644 index 0000000000..629d3d0aa1 --- /dev/null +++ b/src/components/Header/Menu/index.ts @@ -0,0 +1 @@ +export * from './Menu'; diff --git a/src/components/Header/Navigation/NavigatioLink/NavigationLink.scss b/src/components/Header/Navigation/NavigatioLink/NavigationLink.scss new file mode 100644 index 0000000000..f9394d01cb --- /dev/null +++ b/src/components/Header/Navigation/NavigatioLink/NavigationLink.scss @@ -0,0 +1,48 @@ +@import "../../../../styles/global-imports"; + +.navigation-link { + position: relative; + color: $c-secondary; + + transition: all $effect-duration; + + &::after { + display: block; + + content: ""; + position: absolute; + bottom: 0; + + height: 3px; + width: 100%; + + opacity: 0; + background-color: $c-primary; + + transition: opacity $effect-duration; + } + + &:hover, + &.is-active { + color: $c-primary; + + &::after { + opacity: 1; + } + } + + &--text { + padding: 24px 0; + } + + &--icon { + display: flex; + justify-content: center; + align-items: center; + + padding: 18px 24px; + + border: 1px solid $c-elements; + border-bottom: none; + } +} diff --git a/src/components/Header/Navigation/NavigatioLink/NavigationLink.tsx b/src/components/Header/Navigation/NavigatioLink/NavigationLink.tsx new file mode 100644 index 0000000000..9e5b9a577a --- /dev/null +++ b/src/components/Header/Navigation/NavigatioLink/NavigationLink.tsx @@ -0,0 +1,25 @@ +import { NavLink, NavLinkProps } from 'react-router-dom'; + +import classNames from 'classnames'; +import './NavigationLink.scss'; + +type Props = NavLinkProps & { + type: 'text' | 'icon'; +}; + +export const NavigationLink: React.FC = ({ + type, + children, + ...props +}) => ( + classNames( + 'navigation-link', + { [`navigation-link--${type}`]: type }, + { 'is-active': isActive }, + )} + {...props} + > + {children} + +); diff --git a/src/components/Header/Navigation/NavigatioLink/index.ts b/src/components/Header/Navigation/NavigatioLink/index.ts new file mode 100644 index 0000000000..ae65def7ab --- /dev/null +++ b/src/components/Header/Navigation/NavigatioLink/index.ts @@ -0,0 +1 @@ +export * from './NavigationLink'; diff --git a/src/components/Header/Navigation/Navigation.scss b/src/components/Header/Navigation/Navigation.scss new file mode 100644 index 0000000000..0db2173eee --- /dev/null +++ b/src/components/Header/Navigation/Navigation.scss @@ -0,0 +1,19 @@ +@import "../../../styles/global-imports"; + +.Navigation { + display: none; + + @include onDesktop { + display: flex; + } + + &__list { + display: flex; + align-items: center; + gap: 64px; + } + + &__item { + @extend %uppercase; + } +} diff --git a/src/components/Header/Navigation/Navigation.tsx b/src/components/Header/Navigation/Navigation.tsx new file mode 100644 index 0000000000..38d3077062 --- /dev/null +++ b/src/components/Header/Navigation/Navigation.tsx @@ -0,0 +1,24 @@ +import { NavigationLink } from './NavigatioLink'; +import './Navigation.scss'; + +const NAVIGATION_PAGES = ['home', 'phones', 'tablets', 'accessories']; + +export const Navigation = () => ( + +); diff --git a/src/components/Header/Navigation/index.ts b/src/components/Header/Navigation/index.ts new file mode 100644 index 0000000000..95e14a93f1 --- /dev/null +++ b/src/components/Header/Navigation/index.ts @@ -0,0 +1 @@ +export * from './Navigation'; diff --git a/src/components/Header/Search/Search.scss b/src/components/Header/Search/Search.scss new file mode 100644 index 0000000000..e43af5f8b0 --- /dev/null +++ b/src/components/Header/Search/Search.scss @@ -0,0 +1,73 @@ +@import "../../../styles/global-imports"; + +.Search { + position: relative; + flex: 0 1 327px; + + opacity: 0; + pointer-events: none; + box-shadow: 1px 0 0 0 $c-elements; + + @include onDesktop { + margin-left: auto; + + box-shadow: none; + } + + &.isVisible { + opacity: 1; + pointer-events: all; + } + + &--input { + width: 90%; + min-width: 0; + padding: 20px 0 20px 15px; + + font-family: "Mont Semibold", sans-serif; + font-size: 14px; + color: $c-primary; + + border: none; + box-shadow: -1px 0 0 0 $c-elements; + + @include onTabletAndDesktop { + padding: 22px 0 22px 24px; + } + + @include onDesktop { + min-width: 327px; + } + + &.has-icon { + background: no-repeat url("../../../images/icons/icon-search.svg"); + background-position: top 24px right 24px; + } + + &::placeholder { + color: $c-icons; + } + + &:focus { + background-color: $c-hover_bg; + outline: none; + } + } + + &--clear-button { + display: none; + position: absolute; + top: 24px; + right: 24px; + width: 16px; + height: 16px; + + background: no-repeat center url("../../../images/icons/icon-close-primary.svg"); + + cursor: pointer; + + &.isActive { + display: block; + } + } +} diff --git a/src/components/Header/Search/Search.tsx b/src/components/Header/Search/Search.tsx new file mode 100644 index 0000000000..e25557de33 --- /dev/null +++ b/src/components/Header/Search/Search.tsx @@ -0,0 +1,94 @@ +/* eslint-disable jsx-a11y/control-has-associated-label */ +import { useLocation, useSearchParams } from 'react-router-dom'; +import { + ChangeEvent, + useCallback, + useEffect, + useMemo, + useState, +} from 'react'; + +import './Search.scss'; +import classNames from 'classnames'; + +import { isSearchVisible } from '../../../helpers/isSearchVisible'; +import { getSearchWith } from '../../../helpers/searchHelper'; +import { debounceQuery } from '../../../helpers/debounceQuery'; + +export const Search = () => { + const [searchParams, setSearchParams] = useSearchParams(); + const queryParam = searchParams.get('query') || ''; + const [query, setQuery] = useState(queryParam); + + const location = useLocation(); + + const currentPath = useMemo(() => { + return location.pathname.split('/')[1]; + }, [location]); + + useEffect(() => { + setQuery(queryParam); + }, [currentPath]); + + const isVisible = useMemo(() => { + return isSearchVisible(location); + }, [currentPath]); + + const applyQuery = useCallback( + debounceQuery(setSearchParams, 600), + [currentPath], + ); + + const onQueryChange = (event: ChangeEvent) => { + const { value } = event.target; + + setQuery(value); + + const params = { + query: value.trim() || null, + }; + + applyQuery( + getSearchWith(searchParams, params), + ); + }; + + const onClearQuery = () => { + setQuery(''); + + setSearchParams( + getSearchWith(searchParams, { query: null }), + ); + }; + + return ( +
    + +
    + ); +}; diff --git a/src/components/Header/Search/index.ts b/src/components/Header/Search/index.ts new file mode 100644 index 0000000000..addd53308b --- /dev/null +++ b/src/components/Header/Search/index.ts @@ -0,0 +1 @@ +export * from './Search'; 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/HistoryLocation/HistoryLocation.scss b/src/components/HistoryLocation/HistoryLocation.scss new file mode 100644 index 0000000000..7e5d3ec9bd --- /dev/null +++ b/src/components/HistoryLocation/HistoryLocation.scss @@ -0,0 +1,40 @@ +@import "../../styles/global-imports"; + +.history-location { + display: flex; + align-items: center; + justify-content: flex-start; + gap: 10px; + margin: 20px 0; + + &__list { + display: flex; + } + + &--item { + display: flex; + align-items: center; + gap: 8px; + } + + &--home-link { + background: url("../../images/icons/Home.svg") no-repeat center; + width: 16px; + height: 16px; + } + + &--location { + @extend %small-text; + color: $c-primary; + + &-active { + color: $c-secondary; + } + } + + &--arrow-right { + background: $arrow-image no-repeat center; + width: 16px; + height: 16px; + } +} diff --git a/src/components/HistoryLocation/HistoryLocation.tsx b/src/components/HistoryLocation/HistoryLocation.tsx new file mode 100644 index 0000000000..5a0559f887 --- /dev/null +++ b/src/components/HistoryLocation/HistoryLocation.tsx @@ -0,0 +1,53 @@ +import classNames from 'classnames'; +import { Link, useLocation } from 'react-router-dom'; +import { getTitle } from '../../helpers/getTitle'; + +import './HistoryLocation.scss'; + +export const HistoryLocation = () => { + const location = useLocation(); + const { pathname } = location; + const pathnameSegmets = pathname.split('/') + .filter(segment => segment !== ''); + + const historyLocation = pathnameSegmets.map((segment, index) => { + const link = `/${pathnameSegmets.slice(0, index + 1).join('/')}`; + + return { label: segment, link }; + }); + + return ( + + ); +}; diff --git a/src/components/HistoryLocation/index.ts b/src/components/HistoryLocation/index.ts new file mode 100644 index 0000000000..df36c342b1 --- /dev/null +++ b/src/components/HistoryLocation/index.ts @@ -0,0 +1 @@ +export * from './HistoryLocation'; diff --git a/src/components/Loader/Loader.scss b/src/components/Loader/Loader.scss new file mode 100644 index 0000000000..569d378fb9 --- /dev/null +++ b/src/components/Loader/Loader.scss @@ -0,0 +1,6 @@ +.loader { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); +} diff --git a/src/components/Loader/Loader.tsx b/src/components/Loader/Loader.tsx new file mode 100644 index 0000000000..3415a10852 --- /dev/null +++ b/src/components/Loader/Loader.tsx @@ -0,0 +1,12 @@ +import { Ping } from '@uiball/loaders'; +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/Logo/Logo.scss b/src/components/Logo/Logo.scss new file mode 100644 index 0000000000..f2fce0239f --- /dev/null +++ b/src/components/Logo/Logo.scss @@ -0,0 +1,5 @@ +@import "../../styles/global-imports"; + +.Logo { + padding: 20px 0; +} diff --git a/src/components/Logo/Logo.tsx b/src/components/Logo/Logo.tsx new file mode 100644 index 0000000000..ab658b4810 --- /dev/null +++ b/src/components/Logo/Logo.tsx @@ -0,0 +1,16 @@ +import { Link } from 'react-router-dom'; +import './Logo.scss'; +import LogoImage from '../../images/icons/LOGO.svg'; + +export const Logo = () => ( + + Logo + +); diff --git a/src/components/Logo/index.ts b/src/components/Logo/index.ts new file mode 100644 index 0000000000..d97c6951e2 --- /dev/null +++ b/src/components/Logo/index.ts @@ -0,0 +1 @@ +export * from './Logo'; diff --git a/src/components/NoResults/NoResults.tsx b/src/components/NoResults/NoResults.tsx new file mode 100644 index 0000000000..c57a4328fa --- /dev/null +++ b/src/components/NoResults/NoResults.tsx @@ -0,0 +1,13 @@ +type Props = { + category: string; +}; + +export const NoResult: React.FC = ({ category }) => { + const categoryTitle = category[0].toUpperCase() + category.slice(1); + + return ( +

    + {`${categoryTitle} not found`} +

    + ); +}; diff --git a/src/components/NoResults/index.ts b/src/components/NoResults/index.ts new file mode 100644 index 0000000000..20eae4db4b --- /dev/null +++ b/src/components/NoResults/index.ts @@ -0,0 +1 @@ +export * from './NoResults'; diff --git a/src/components/Pagination/Pagination.scss b/src/components/Pagination/Pagination.scss new file mode 100644 index 0000000000..010d1a06c5 --- /dev/null +++ b/src/components/Pagination/Pagination.scss @@ -0,0 +1,85 @@ +@import "../../styles/global-imports"; + +.pagination { + display: flex; + align-items: center; + justify-content: center; + gap: 16px; + + &--button { + display: flex; + justify-content: center; + align-items: center; + width: 32px; + height: 32px; + border: 1px solid $c-icons; + background: $c-white $arrow-image no-repeat center; + + &-left { + transform: rotate(180deg); + + &:not(.pagination--button-left-disabled) { + @include hover(border-color, $c-primary); + + } + } + + &-disabled { + cursor: none; + pointer-events: none; + + opacity: 0.5; + } + + &-right { + &:not(.pagination--button-right-disabled) { + @include hover(border-color, $c-primary); + + } + } + } + + &__list { + display: flex; + gap: 8px; + + @include onMobile { + flex-wrap: wrap; + } + } + + &--item { + &-active { + color: $c-white; + background-color: $c-primary; + } + } + + &--break { + display: flex; + align-items: center; + justify-content: center; + + width: 16px; + height: 32px; + } + + &--link { + display: flex; + justify-content: center; + align-items: center; + + width: 32px; + height: 32px; + + border: 1px solid $c-icons; + + @extend %body-text; + + @include hover(border-color, $c-primary); + + &-active { + color: $c-white; + } + } +} diff --git a/src/components/Pagination/Pagination.tsx b/src/components/Pagination/Pagination.tsx new file mode 100644 index 0000000000..5b167912ea --- /dev/null +++ b/src/components/Pagination/Pagination.tsx @@ -0,0 +1,143 @@ +import { Link, useSearchParams } from 'react-router-dom'; +import classNames from 'classnames'; + +import { getNumbers } from '../../helpers/getNumbers'; +import { getSearchWith } from '../../helpers/searchHelper'; + +import './Pagination.scss'; +import { handleBackToTop } from '../../helpers/handleToUp'; + +type Props = { + totalItems: number, + currentPage: number, + perPageLength: number, +}; + +export const Pagination: React.FC = ({ + totalItems, + currentPage, + perPageLength, +}) => { + const [searchParams] = useSearchParams(); + + const totalPages = Math.ceil(totalItems / perPageLength); + + const isFirstPage = currentPage === 1; + const isLastPage = currentPage === totalPages; + + let startPage = Math.max(1, currentPage - 2); + + const endPage = Math.min(startPage + 4, totalPages); + + if (endPage - startPage < 4) { + startPage = Math.max(1, endPage - 4); + } + + const pageLength = getNumbers(startPage, endPage); + + return ( +
    + + +
      + {startPage > 1 && ( +
    • + + 1 + +
    • + )} + + {startPage > 2 && ( +
    • + ... +
    • + )} + + {pageLength.map(pageNumber => ( +
    • + + {pageNumber} + +
    • + ))} + + {endPage < totalPages - 1 && ( +
    • + ... +
    • + )} + + {endPage < totalPages && ( +
    • + + {totalPages} + +
    • + )} +
    + + +
    + ); +}; diff --git a/src/components/Pagination/index.ts b/src/components/Pagination/index.ts new file mode 100644 index 0000000000..e016c96b72 --- /dev/null +++ b/src/components/Pagination/index.ts @@ -0,0 +1 @@ +export * from './Pagination'; diff --git a/src/components/Product/Capacity/Capacity.scss b/src/components/Product/Capacity/Capacity.scss new file mode 100644 index 0000000000..7eca092aad --- /dev/null +++ b/src/components/Product/Capacity/Capacity.scss @@ -0,0 +1,40 @@ +@import "../../../styles/global-imports"; + +.capacity { + border-bottom: 1px solid $c-elements; + padding-bottom: 24px; + margin: 24px 0; + + @include onTablet { + display: flex; + flex-direction: column; + align-items: center; + } + + &--title { + @extend %small-text; + color: $c-secondary; + } + + &__list { + display: flex; + gap: 8px; + margin-top: 10px; + } + + &--link { + @extend %body-text; + + color: $c-primary; + border: 1px solid $c-primary; + padding: 2px 8px; + + &-active { + color: $c-white; + background-color: $c-primary; + } + + @include hover(background-color, $c-primary); + @include hover(color, $c-white); + } +} diff --git a/src/components/Product/Capacity/Capacity.tsx b/src/components/Product/Capacity/Capacity.tsx new file mode 100644 index 0000000000..c47581f9f5 --- /dev/null +++ b/src/components/Product/Capacity/Capacity.tsx @@ -0,0 +1,41 @@ +import { Link } from 'react-router-dom'; + +import classNames from 'classnames'; +import './Capacity.scss'; + +import { ProductDetails } from '../../../types/ProductDetails'; +import { getCurrentLink } from '../../../helpers/getCurrentLink'; + +type Props = { + capacities: string[], + productDetails: ProductDetails | null, +}; + +export const Capacity: React.FC = ({ + capacities, + productDetails, +}) => ( +
    +

    + Select capacity +

    + +
      + {capacities.map(currentCapacity => { + const isActive = productDetails?.capacity === currentCapacity; + + return ( + + {currentCapacity} + + ); + })} +
    +
    +); diff --git a/src/components/Product/Capacity/index.ts b/src/components/Product/Capacity/index.ts new file mode 100644 index 0000000000..c9af0de726 --- /dev/null +++ b/src/components/Product/Capacity/index.ts @@ -0,0 +1 @@ +export * from './Capacity'; diff --git a/src/components/Product/Card/Card.scss b/src/components/Product/Card/Card.scss new file mode 100644 index 0000000000..30566240d9 --- /dev/null +++ b/src/components/Product/Card/Card.scss @@ -0,0 +1,103 @@ +@import "../../../styles/global-imports"; + +.product-card { + display: flex; + flex-direction: column; + justify-content: space-around; + + max-width: 220px; + + padding: 32px 24px 24px; + + border: 1px solid $c-elements; + + transition: all, 0.3s ease-in-out !important; + + @include onDesktop { + @include hover(transform, scale(104%)); + } + + @include onTablet { + @include hover(transform, scale(98%)); + } + + @include onMobile { + max-width: 100%; + } + + &__link { + display: flex; + flex-direction: column; + } + + &--image { + margin: 0 auto 24px; + height: 208px; + + object-fit: contain; + + @include onMobile { + height: 170px; + } + } + + &__title { + @extend %body-text; + height: 40px; + + @include onMobile { + height: 66px; + } + } + + &__price { + display: flex; + margin-bottom: 16px; + padding-bottom: 6.5px; + padding-top: 6.5px; + + border-bottom: 1px solid $c-secondary; + + &-discount { + color: $c-secondary; + text-decoration: line-through; + font-size: 18px; + } + + &-regular { + margin-right: 8px; + } + } + + &__property { + display: grid; + grid-template-columns: repeat(2, 1fr); + justify-items: stretch; + margin-bottom: 10px; + + @extend %small-text; + + &--title { + color: $c-secondary; + } + + &--value { + color: $c-primary; + text-align: end; + } + } + + &__buttons { + display: flex; + justify-content: space-between; + gap: 8px; + + &--cart { + width: 170px; + } + + &--favourites { + width: 40px; + } + } +} diff --git a/src/components/Product/Card/Card.tsx b/src/components/Product/Card/Card.tsx new file mode 100644 index 0000000000..e6ae92527b --- /dev/null +++ b/src/components/Product/Card/Card.tsx @@ -0,0 +1,112 @@ +import { Link } from 'react-router-dom'; +import { Product } from '../../../types/Product'; +import { ProductTitles } from '../../../types/ProductTitles'; + +import './Card.scss'; + +import { ButtonCart } from '../../Button/ButtonCart'; +import { ButtonFavorites } from '../../Button/ButtonFavorites'; +import { IMAGE_URL } from '../../../helpers/IMAGE_URL'; +import { handleBackToTop } from '../../../helpers/handleToUp'; + +type Props = { + product: Product, + title?: ProductTitles, +}; + +export const ProductCard: React.FC = ({ + product, +}) => { + const { + screen, + capacity, + ram, + price, + fullPrice, + name, + category, + phoneId, + image, + } = product; + + return ( +
    + + {name} +

    + {name} +

    +
    + {price === fullPrice ? ( +

    + {`$${fullPrice}`} +

    + ) : ( + <> +

    + {`$${price}`} +

    + +

    + {`$${fullPrice}`} +

    + + )} + +
    + +
    +
    +

    + Screen +

    +

    + {screen} +

    +
    + +
    +

    + Capacity +

    +

    + {capacity} +

    +
    + +
    +

    + Ram +

    +

    + {ram} +

    +
    +
    + + + +
    +
    + +
    + +
    + +
    +
    +
    + ); +}; diff --git a/src/components/Product/Card/index.ts b/src/components/Product/Card/index.ts new file mode 100644 index 0000000000..ca0b060473 --- /dev/null +++ b/src/components/Product/Card/index.ts @@ -0,0 +1 @@ +export * from './Card'; diff --git a/src/components/Product/ColorChoose/ColorChoose.scss b/src/components/Product/ColorChoose/ColorChoose.scss new file mode 100644 index 0000000000..769ae6f1e1 --- /dev/null +++ b/src/components/Product/ColorChoose/ColorChoose.scss @@ -0,0 +1,51 @@ +@import "../../../styles/global-imports"; + +.color-choose { + border-bottom: 1px solid $c-elements; + padding-bottom: 24px; + + @include onTablet { + display: flex; + flex-direction: column; + align-items: center; + } + + &--title { + @extend %small-text; + color: $c-secondary; + } + + &__list { + display: flex; + gap: 8px; + + margin-top: 10px; + } + + &--link { + display: flex; + align-items: center; + justify-content: center; + + border: 1px solid $c-elements; + border-radius: 100%; + + width: 32px; + height: 32px; + + @include hover(border-color, $c-primary); + + &-rug { + width: 26px; + height: 26px; + border-radius: 100%; + + @include hover(width, 30px); + @include hover(height, 30px); + } + + &-active { + border-color: $c-primary; + } + } +} diff --git a/src/components/Product/ColorChoose/ColorChoose.tsx b/src/components/Product/ColorChoose/ColorChoose.tsx new file mode 100644 index 0000000000..e1e181c8e9 --- /dev/null +++ b/src/components/Product/ColorChoose/ColorChoose.tsx @@ -0,0 +1,49 @@ +import { Link } from 'react-router-dom'; + +import classNames from 'classnames'; +import './ColorChoose.scss'; + +import { ProductDetails } from '../../../types/ProductDetails'; +import { getCurrentLink } from '../../../helpers/getCurrentLink'; +import { getColor } from '../../../helpers/getColor'; + +type Props = { + colors: string[], + currentColor: string, + productDetails: ProductDetails | null, +}; + +export const ColorChoose: React.FC = ({ + colors, + currentColor, + productDetails, +}) => ( +
    +

    + Availeble colors +

    + +
      + {colors.map(c => { + const isActive = currentColor === c; + + return ( + +
      + + ); + })} +
    +
    +); diff --git a/src/components/Product/ColorChoose/index.ts b/src/components/Product/ColorChoose/index.ts new file mode 100644 index 0000000000..2cf2a4924a --- /dev/null +++ b/src/components/Product/ColorChoose/index.ts @@ -0,0 +1 @@ +export * from './ColorChoose'; diff --git a/src/components/Product/Description/Description.scss b/src/components/Product/Description/Description.scss new file mode 100644 index 0000000000..8aa542a60c --- /dev/null +++ b/src/components/Product/Description/Description.scss @@ -0,0 +1,49 @@ +@import "../../../styles/global-imports"; + +.description { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 116px; + + @include onMobileAndTablet { + display: flex; + flex-direction: column-reverse; + gap: 40px; + + margin-bottom: 40px; + } + + &--title { + border-bottom: 1px solid $c-elements; + padding-bottom: 16px; + margin-bottom: 16px; + } + + &__about { + &--title { + margin-bottom: 16px; + } + + &--paragraph { + @extend %body-text; + color: $c-secondary; + margin-bottom: 32px; + } + } + + &__tech-specs { + &--item { + display: grid; + grid-template-columns: repeat(2, 1fr); + justify-items: stretch; + + margin-bottom: 10px; + + @extend %small-text; + + &-title { + color: $c-secondary; + } + } + } +} diff --git a/src/components/Product/Description/Description.tsx b/src/components/Product/Description/Description.tsx new file mode 100644 index 0000000000..36da592c6f --- /dev/null +++ b/src/components/Product/Description/Description.tsx @@ -0,0 +1,129 @@ +import { ProductDetails } from '../../../types/ProductDetails'; + +import './Description.scss'; + +type Props = { + details: ProductDetails, +}; + +export const Description: React.FC = ({ details }) => ( +
    +
    +

    + About +

    + + {details.description.map(({ title, text }) => ( +
    +

    + {title} +

    + +

    + {text.map(paragraph => paragraph)} +

    +
    + ))} +
    + +
      +

      + Tech specs +

      + +
    • + +
      + Screen +
      + +
      + {details.screen} +
      + +
    • +
    • + +
      + Resolution +
      + +
      + {details.resolution} +
      + +
    • +
    • + +
      + Processor +
      + +
      + {details.processor} +
      + +
    • +
    • + +
      + RAM +
      + +
      + {details.ram} +
      + +
    • +
    • + +
      + Built in memory +
      + +
      + {details.capacity} +
      + +
    • +
    • + +
      + Camera +
      + +
      + {details.camera} +
      + +
    • +
    • + +
      + Zoom +
      + +
      + {details.zoom} +
      + +
    • +
    • + +
      + Cell +
      + +
      + {details.cell.join(', ')} +
      + +
    • +
    +
    +); diff --git a/src/components/Product/Galery/Galery.scss b/src/components/Product/Galery/Galery.scss new file mode 100644 index 0000000000..1c2adaf763 --- /dev/null +++ b/src/components/Product/Galery/Galery.scss @@ -0,0 +1,84 @@ +@import "../../../styles/global-imports"; + +.galery { + display: flex; + + @include onMobile { + flex-direction: column-reverse; + } + + &__list { + display: flex; + flex-direction: column; + gap: 16px; + + @include onMobile { + flex-direction: row; + gap: 8px; + overflow-x: scroll; + max-width: 80%; + margin: 20px auto 0; + } + } + + &__photo { + display: flex; + align-items: center; + justify-content: center; + + cursor: pointer; + + height: 80px; + width: 80px; + + border: 1px solid $c-elements; + + background-color: $c-white; + + @include onMobile { + height: 60px; + width: 60px; + } + + &:not(.galery__photo-active) { + @include hover(border-color, $c-secondary); + } + + &-active { + border-color: $c-primary; + } + + &-container { + display: flex; + justify-content: center; + + width: 66px; + height: 66px; + + @include onMobile { + width: 33px; + height: 33px; + } + } + } + + &__main { + display: flex; + justify-content: center; + align-items: center; + height: 464px; + width: 464px; + margin-right: 64px; + + @include onMobile { + height: 90vw; + width: 80vw; + margin: 0 auto; + } + + &--image { + max-width: 100%; + max-height: 100%; + } + } +} diff --git a/src/components/Product/Galery/Galery.tsx b/src/components/Product/Galery/Galery.tsx new file mode 100644 index 0000000000..c0e0b9c063 --- /dev/null +++ b/src/components/Product/Galery/Galery.tsx @@ -0,0 +1,58 @@ +import classNames from 'classnames'; +import { AnimatePresence, motion } from 'framer-motion'; + +import './Galery.scss'; + +import { IMAGE_URL } from '../../../helpers/IMAGE_URL'; + +type Props = { + photos: string[], + mainPhoto: string, + photoClick: (photo: string) => void, +}; + +export const Galery: React.FC = ({ + photos, + mainPhoto, + photoClick, +}) => ( +
    +
      + {photos.map(currentImage => ( + + ))} +
    + + + + product main + + +
    +); diff --git a/src/components/Product/Galery/index.ts b/src/components/Product/Galery/index.ts new file mode 100644 index 0000000000..65a0acc351 --- /dev/null +++ b/src/components/Product/Galery/index.ts @@ -0,0 +1 @@ +export * from './Galery'; diff --git a/src/components/Product/List/List.scss b/src/components/Product/List/List.scss new file mode 100644 index 0000000000..cb1753df4f --- /dev/null +++ b/src/components/Product/List/List.scss @@ -0,0 +1,81 @@ +@import "../../../styles/global-imports"; + +.product-list { + + @include onMobile { + display: flex; + flex-direction: column; + align-items: center; + } + + &--quantity { + @extend %body-text; + color: $c-secondary; + } + + &__dropdowns { + display: flex; + justify-content: flex-start; + gap: 24px; + + @include onTablet { + justify-content: space-around; + } + + @include onMobile { + flex-direction: column; + width: 82%; + } + + &--item { + &:first-child { + z-index: 12; + } + } + } + + &__list { + display: flex; + flex-wrap: wrap; + gap: 16px; + margin-bottom: 40px; + + @include onMobile { + margin-top: 20px; + justify-content: center; + } + + @include onTablet { + gap: 16px 6px; + justify-content: center; + } + } + &--item { + display: block; + + @include onSmallMobile { + max-width: 66vw; + } + + @include fromMediumMobile { + max-width: 36vw !important; + } + } + + &__pagination { + display: flex; + justify-content: center; + } + + .product-card { + @include onMobile { + padding: 18px 12px; + } + + &--image { + @include onMobile { + height: 120px; + } + } + } +} diff --git a/src/components/Product/List/List.tsx b/src/components/Product/List/List.tsx new file mode 100644 index 0000000000..0f1f241959 --- /dev/null +++ b/src/components/Product/List/List.tsx @@ -0,0 +1,124 @@ +import { + useState, + useMemo, + useEffect, +} from 'react'; +import { useSearchParams } from 'react-router-dom'; +import { Fade } from 'react-awesome-reveal'; + +import './List.scss'; +import { productFilter } from '../../../helpers/productFilter'; +import { getSortedProducts } from '../../../helpers/getSortedProducts'; +import { DropDown } from '../../DropDown'; +import { itemsOnPage, sortParam } from '../../../types/SortTypes'; +import { ProductCard } from '../Card'; +import { ProductTitles } from '../../../types/ProductTitles'; +import { Pagination } from '../../Pagination'; +import { Product } from '../../../types/Product'; + +type Props = { + products: Product[], + isError: boolean, + isLoading: boolean, +}; + +export const List: React.FC = ({ products }) => { + const [searchParams] = useSearchParams(); + const currentPage = Number(searchParams.get('page')) || 1; + let perPageLength = Number(searchParams.get('perPage')) || 16; + + if (searchParams.get('perPage') === 'All') { + perPageLength = products.length; + } + + const startPage = (currentPage * perPageLength) - perPageLength; + const endPage = Math.min(currentPage * perPageLength, products.length); + + const sortBy = searchParams.get('sortBy'); + + const query = searchParams.get('query') || ''; + const [debouncedQuery, setDebouncedQuery] = useState(query); + + const filteredProducts = useMemo(() => ( + productFilter(products, debouncedQuery) + ), [products, debouncedQuery]); + + const sortedProducts = useMemo(() => ( + getSortedProducts(filteredProducts, sortBy) + ), [filteredProducts, sortBy]); + + const visibleProducts = sortedProducts.slice(startPage, endPage); + + useEffect(() => { + const debounceTimer = setTimeout(() => { + setDebouncedQuery(query); + }, 500); + + if (!searchParams.get('page=1')) { + searchParams.set('page', '1'); + } + + return () => clearTimeout(debounceTimer); + }, [query]); + + return ( +
    +

    + {`${sortedProducts.length} models`} +

    + + {!sortedProducts.length ? ( +

    + There are no products matching the current search criteria +

    + ) : ( +
    +
    + +
    + +
    + +
    +
    + )} + +
      + + {visibleProducts.map(currentProduct => ( +
    • + +
    • + ))} +
      +
    + + {(perPageLength < sortedProducts.length && filteredProducts.length > 0) + && ( +
    + +
    + )} +
    + ); +}; diff --git a/src/components/Product/List/index.ts b/src/components/Product/List/index.ts new file mode 100644 index 0000000000..4994c18137 --- /dev/null +++ b/src/components/Product/List/index.ts @@ -0,0 +1 @@ +export * from './List'; diff --git a/src/components/Product/Slider/Slider.scss b/src/components/Product/Slider/Slider.scss new file mode 100644 index 0000000000..3601f1e505 --- /dev/null +++ b/src/components/Product/Slider/Slider.scss @@ -0,0 +1,56 @@ +@import "../../../styles/global-imports"; + +.product-slider { + display: flex; + flex-direction: column-reverse; + padding: 0 6px 14px !important; + + &__header { + position: relative; + display: flex; + justify-content: space-between; + align-items: center; + } + + &--item { + height: 508px; + + &:not(:first-child) { + margin-left: 16px; + } + } + + .swiper { + &-button-prev, + &-button-next { + width: 32px; + height: 32px; + + top: 30px !important; + left: 96% !important; + + border: 1px solid $c-primary; + + &::after { + font-size: 12px !important; + color: $c-primary; + } + + @include onTablet { + left: 92% !important; + } + + @include onMobile { + display: none; + } + } + + &-button-prev { + left: 92% !important; + + @include onTablet { + left: 84% !important; + } + } + } +} diff --git a/src/components/Product/Slider/Slider.tsx b/src/components/Product/Slider/Slider.tsx new file mode 100644 index 0000000000..6f63e0732f --- /dev/null +++ b/src/components/Product/Slider/Slider.tsx @@ -0,0 +1,76 @@ +import { Swiper, SwiperSlide } from 'swiper/react'; +import { Navigation } from 'swiper'; + +import 'swiper/swiper.scss'; +import 'swiper/modules/navigation/navigation.scss'; +import 'swiper/modules/pagination/pagination.scss'; + +import './Slider.scss'; + +import { Product } from '../../../types/Product'; +import { ProductTitles } from '../../../types/ProductTitles'; +import { ProductCard } from '../Card'; + +type Props = { + title: ProductTitles, + products: Product[] +}; + +export const ProductSlider: React.FC = ({ + title, + products, +}) => { + const visibleProducts = products.slice(0, 21); + + const navigationMode = true; + + return ( + + +
    +

    + {title} +

    +
    + + {visibleProducts.map(currentProduct => ( + + + + ))} + +
    + ); +}; diff --git a/src/components/Product/Slider/index.ts b/src/components/Product/Slider/index.ts new file mode 100644 index 0000000000..f48a854158 --- /dev/null +++ b/src/components/Product/Slider/index.ts @@ -0,0 +1 @@ +export * from './Slider'; diff --git a/src/contexts/CartContextProvider.tsx b/src/contexts/CartContextProvider.tsx new file mode 100644 index 0000000000..9b9fc38232 --- /dev/null +++ b/src/contexts/CartContextProvider.tsx @@ -0,0 +1,76 @@ +import { ReactNode, createContext } from 'react'; +import { CartItem } from '../types/CartItem'; +import { useLocalStorage } from '../hooks/useLocalStorage'; + +type Props = { + children: ReactNode, +}; + +type CartContextType = { + cart: CartItem[], + addToCart: (newProduct: CartItem) => void, + removeFromCart: (productId: string) => void, + cartAmount: (productId: string, action: Action) => void, +}; + +export const CartContext = createContext({ + cart: [], + addToCart: () => {}, + removeFromCart: () => {}, + cartAmount: () => {}, +}); + +export enum Action { + Increase = 'increase', + Decrease = 'decrease', +} + +export const CartContextProvider: React.FC = ({ children }) => { + const [cart, setCart] = useLocalStorage('cart', []); + + const addToCart = (product: CartItem) => { + setCart([ + ...cart, + product, + ]); + }; + + const cartAmount = (productId: string, action: Action) => { + const newCart = cart.map(item => { + if (productId === item.id) { + switch (action) { + case 'increase': + + return { ...item, quantity: item.quantity + 1 }; + + case 'decrease': + return { ...item, quantity: item.quantity - 1 }; + + default: return item; + } + } + + return item; + }); + + setCart(newCart); + }; + + const removeFromCart = (productId: string) => { + setCart([ + ...cart.filter(item => item.product.itemId !== productId), + ]); + }; + + return ( + + {children} + + ); +}; diff --git a/src/contexts/FavoriteContextProvider.tsx b/src/contexts/FavoriteContextProvider.tsx new file mode 100644 index 0000000000..e600a1d653 --- /dev/null +++ b/src/contexts/FavoriteContextProvider.tsx @@ -0,0 +1,43 @@ +import { createContext, ReactNode } from 'react'; +import { Product } from '../types/Product'; +import { useLocalStorage } from '../hooks/useLocalStorage'; + +type FavoriteContextProps = { + favorites: Product[] + addToFavorite: (product: Product) => void, + removeFromFavorite: (productId: string) => void, +}; + +export const FavoriteContext = createContext({ + favorites: [], + addToFavorite: () => { }, + removeFromFavorite: () => { }, +}); + +type Props = { + children: ReactNode +}; + +export const FavoriteContextProvider: React.FC = ({ children }) => { + const [favorites, setFavorites] = useLocalStorage( + 'favorites', [], + ); + + const addToFavorite = (product: Product) => { + setFavorites([...favorites, product]); + }; + + const removeFromFavorite = (productId: string) => { + setFavorites([ + ...favorites.filter(item => item.phoneId !== productId), + ]); + }; + + return ( + + {children} + + ); +}; diff --git a/public/fonts/Mont-Bold.otf b/src/fonts/Mont-Bold.otf old mode 100755 new mode 100644 similarity index 100% rename from public/fonts/Mont-Bold.otf rename to src/fonts/Mont-Bold.otf diff --git a/public/fonts/Mont-Regular.otf b/src/fonts/Mont-Regular.otf old mode 100755 new mode 100644 similarity index 100% rename from public/fonts/Mont-Regular.otf rename to src/fonts/Mont-Regular.otf diff --git a/public/fonts/Mont-SemiBold.otf b/src/fonts/Mont-SemiBold.otf old mode 100755 new mode 100644 similarity index 100% rename from public/fonts/Mont-SemiBold.otf rename to src/fonts/Mont-SemiBold.otf diff --git a/src/fonts/Montserrat-Thin.ttf b/src/fonts/Montserrat-Thin.ttf new file mode 100644 index 0000000000..7d085bba94 Binary files /dev/null and b/src/fonts/Montserrat-Thin.ttf differ diff --git a/src/helpers/IMAGE_URL.ts b/src/helpers/IMAGE_URL.ts new file mode 100644 index 0000000000..73b5c2c208 --- /dev/null +++ b/src/helpers/IMAGE_URL.ts @@ -0,0 +1,2 @@ +export const IMAGE_URL + = 'https://mate-academy.github.io/react_phone-catalog/_new/'; diff --git a/src/helpers/calculateDiscount.ts b/src/helpers/calculateDiscount.ts new file mode 100644 index 0000000000..46edd7ab46 --- /dev/null +++ b/src/helpers/calculateDiscount.ts @@ -0,0 +1,10 @@ +type Product = { + price: number; + discount: number; +}; + +export const calculateDiscount = (product: Product) => { + const { price, discount } = product; + + return price - ((price / 100) * discount); +}; diff --git a/src/helpers/debounceQuery.ts b/src/helpers/debounceQuery.ts new file mode 100644 index 0000000000..d9b7580998 --- /dev/null +++ b/src/helpers/debounceQuery.ts @@ -0,0 +1,15 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +export const debounceQuery = ( + callback: (...args: any[]) => void, + delay: number, +) => { + let timerId = 0; + + return (...args: any[]) => { + window.clearTimeout(timerId); + + timerId = window.setTimeout(() => { + callback(...args); + }, delay); + }; +}; diff --git a/src/helpers/fetchProducts.ts b/src/helpers/fetchProducts.ts new file mode 100644 index 0000000000..7230ba571a --- /dev/null +++ b/src/helpers/fetchProducts.ts @@ -0,0 +1,61 @@ +import { Product } from '../types/Product'; +import { ProductDetails } from '../types/ProductDetails'; + +export const URL_NEW + = 'https://mate-academy.github.io/react_phone-catalog/_new/products.json'; +export const detailsURL + = 'https://mate-academy.github.io/react_phone-catalog/_new/products/'; + +export const getProducts = (url: string) => { + return fetch(url) + .then((response) => { + if (!response.ok) { + throw Error(); + } + + if (!response.headers.get('content-type')?.includes('application/json')) { + throw new Error(); + } + + return response.json(); + }); +}; + +export const getAllProducts = async () => { + const products: Product[] = await getProducts(URL_NEW); + + return products; +}; + +export const getHotPriceProducts = async () => { + const products: Product[] = await getProducts(URL_NEW); + + return products.sort((a, b) => ( + (b.fullPrice - b.price) - (a.fullPrice - a.price))); +}; + +export const getBrandNewProducts = async () => { + const products: Product[] = await getProducts(URL_NEW); + + return products.sort((a, b) => (b.price - a.price)); +}; + +export const getProductDetails = async (id: string) => { + const productDetails: ProductDetails = await getProducts(`${detailsURL}${id}.json`); + + return productDetails; +}; + +export const getSuggestedProducts = async () => { + const products: Product[] = await getProducts(URL_NEW); + + const shuffledProducts = products.sort(() => Math.random() - 0.5); + + return shuffledProducts; +}; + +export const getProductsById = async (productId: string) => { + const products: Product[] = await getProducts(URL_NEW); + + return products.find(({ phoneId }) => phoneId === productId); +}; diff --git a/src/helpers/getCartPrice.ts b/src/helpers/getCartPrice.ts new file mode 100644 index 0000000000..f0dfdec6d5 --- /dev/null +++ b/src/helpers/getCartPrice.ts @@ -0,0 +1,11 @@ +import { CartItem } from '../types/CartItem'; + +export const getCartPrice = ( + cartProduct: CartItem[], +) => { + const totalPrice = cartProduct.reduce((total, currentItem) => ( + currentItem.product.price * currentItem.quantity + total + ), 0); + + return totalPrice; +}; diff --git a/src/helpers/getColor.ts b/src/helpers/getColor.ts new file mode 100644 index 0000000000..94d4298c1c --- /dev/null +++ b/src/helpers/getColor.ts @@ -0,0 +1,21 @@ +export const getColor = (color: string) => { + switch (color) { + case 'midnightgreen': + return '#5f7170'; + + case 'spacegray': + return '#4c4c4c'; + + case 'gold': + return '#fcdbc1'; + + case 'white': + return '#e2e5e9'; + + case 'rosegold': + return 'pink'; + + default: + return color; + } +}; diff --git a/src/helpers/getCurrentLink.ts b/src/helpers/getCurrentLink.ts new file mode 100644 index 0000000000..bf76ac2ff8 --- /dev/null +++ b/src/helpers/getCurrentLink.ts @@ -0,0 +1,21 @@ +import { ProductDetails } from '../types/ProductDetails'; + +export const getCurrentLink = ( + details: ProductDetails | null, + newColor?: string, + newCapacity?: string, +) => { + const productName = details?.namespaceId; + const capacity = details?.capacity; + const color = details?.color; + + if (newColor && !newCapacity) { + return `${productName}-${capacity}-${newColor}`.toLowerCase(); + } + + if (!newColor && newCapacity) { + return `${productName}-${newCapacity}-${color}`.toLowerCase(); + } + + return `${productName}-${capacity}-${color}`; +}; diff --git a/src/helpers/getNumbers.ts b/src/helpers/getNumbers.ts new file mode 100644 index 0000000000..e996e9169d --- /dev/null +++ b/src/helpers/getNumbers.ts @@ -0,0 +1,12 @@ +export function getNumbers( + start: number, + end: number, +): number[] { + const numbers = []; + + for (let n = start; n <= end; n += 1) { + numbers.push(n); + } + + return numbers; +} diff --git a/src/helpers/getSortedProducts.ts b/src/helpers/getSortedProducts.ts new file mode 100644 index 0000000000..de6f5ea380 --- /dev/null +++ b/src/helpers/getSortedProducts.ts @@ -0,0 +1,29 @@ +import { Product } from '../types/Product'; +import { SortTypes } from '../types/SortTypes'; + +export const getSortedProducts = ( + products: Product[], + sortBy: string | null, +) => { + const sotredProducts = [...products]; + + if (sortBy) { + sotredProducts.sort((firstProduct, secondProduct) => { + switch (sortBy) { + case SortTypes.Newest: + return secondProduct.year - firstProduct.year; + + case SortTypes.Alphabetically: + return firstProduct.name.localeCompare(secondProduct.name); + + case SortTypes.Cheapest: + return firstProduct.price - secondProduct.price; + + default: + throw new Error('Sort type error'); + } + }); + } + + return sotredProducts; +}; diff --git a/src/helpers/getTitle.ts b/src/helpers/getTitle.ts new file mode 100644 index 0000000000..d1acf7840c --- /dev/null +++ b/src/helpers/getTitle.ts @@ -0,0 +1,5 @@ +export const getTitle = (title: string) => ( + title.split('-').map(name => ( + name[0].toUpperCase() + name.slice(1))) + .join(' ') +); diff --git a/src/helpers/handleToUp.ts b/src/helpers/handleToUp.ts new file mode 100644 index 0000000000..8f82df8f68 --- /dev/null +++ b/src/helpers/handleToUp.ts @@ -0,0 +1,6 @@ +export const handleBackToTop = () => { + window.scrollTo({ + top: 0, + behavior: 'smooth', + }); +}; diff --git a/src/helpers/isSearchVisible.ts b/src/helpers/isSearchVisible.ts new file mode 100644 index 0000000000..684eb3addd --- /dev/null +++ b/src/helpers/isSearchVisible.ts @@ -0,0 +1,15 @@ +import { Location } from 'react-router-dom'; + +const ACTIVE_PAGES = ['phones', 'tablets', 'accessories', 'favourites']; + +export const isSearchVisible = (location: Location) => { + const locationSplited = location.pathname.split('/'); + + if (locationSplited.length > 2) { + return false; + } + + return ACTIVE_PAGES.some(currentPage => { + return locationSplited.includes(currentPage); + }); +}; diff --git a/src/helpers/motionParametr.ts b/src/helpers/motionParametr.ts new file mode 100644 index 0000000000..7b44805e9a --- /dev/null +++ b/src/helpers/motionParametr.ts @@ -0,0 +1,6 @@ +export const motionParametr = { + initial: { opacity: 0 }, + animate: { opacity: 1 }, + exit: { opacity: 0 }, + transition: { duration: 0.3 }, +}; diff --git a/src/helpers/productFilter.ts b/src/helpers/productFilter.ts new file mode 100644 index 0000000000..54e005a851 --- /dev/null +++ b/src/helpers/productFilter.ts @@ -0,0 +1,11 @@ +import { Product } from '../types/Product'; + +export const productFilter = (products: Product[], query: string) => { + const preparedQuery = query.toLowerCase(); + + return products.filter(product => { + const productInfo = `${product.name.toLowerCase()} ${product.screen.toLowerCase()} ${product.ram.toLowerCase()} ${String(product.price).toLowerCase()}`; + + return productInfo.includes(preparedQuery); + }); +}; diff --git a/src/helpers/searchHelper.ts b/src/helpers/searchHelper.ts new file mode 100644 index 0000000000..9bb2d090b1 --- /dev/null +++ b/src/helpers/searchHelper.ts @@ -0,0 +1,29 @@ +export type SearchParam = { + [key: string]: string | string[] | null, +}; + +export const getSearchWith = ( + currentParam: URLSearchParams, + paramToUpdate: SearchParam, +) => { + const newParam = new URLSearchParams( + currentParam.toString(), + ); + + Object.entries(paramToUpdate) + .forEach(([key, value]) => { + if (value === null) { + newParam.delete(key); + } else if (Array.isArray(value)) { + newParam.delete(key); + + value.forEach(part => { + newParam.append(key, part); + }); + } else { + newParam.set(key, value); + } + }); + + return newParam.toString(); +}; diff --git a/src/hooks/useLocalStorage.ts b/src/hooks/useLocalStorage.ts new file mode 100644 index 0000000000..882f68ce6b --- /dev/null +++ b/src/hooks/useLocalStorage.ts @@ -0,0 +1,29 @@ +import { useState } from 'react'; + +export function useLocalStorage( + key: string, + initialValue: T, +): [T, (value: T) => void] { + const [value, setValue] = useState(() => { + const data = localStorage.getItem(key); + + if (data === null) { + return initialValue; + } + + try { + return JSON.parse(data) as T; + } catch (error) { + localStorage.removeItem(key); + + return initialValue; + } + }); + + const save = (newValue: T) => { + localStorage.setItem(key, JSON.stringify(newValue)); + setValue(newValue); + }; + + return [value, save]; +} diff --git a/src/images-imports.d.ts b/src/images-imports.d.ts new file mode 100644 index 0000000000..cf74100197 --- /dev/null +++ b/src/images-imports.d.ts @@ -0,0 +1,14 @@ +declare module '*.svg' { + const content: string; + export default content; +} + +declare module '*.png' { + const content: string; + export default content; +} + +declare module '*.jpg' { + const content: string; + export default content; +} diff --git a/src/images/Categories/Accessories.png b/src/images/Categories/Accessories.png new file mode 100644 index 0000000000..b9522af6d7 Binary files /dev/null and b/src/images/Categories/Accessories.png differ diff --git a/src/images/Categories/Phones.png b/src/images/Categories/Phones.png new file mode 100644 index 0000000000..039c853594 Binary files /dev/null and b/src/images/Categories/Phones.png differ diff --git a/src/images/Categories/Tablets.png b/src/images/Categories/Tablets.png new file mode 100644 index 0000000000..7f5081f656 Binary files /dev/null and b/src/images/Categories/Tablets.png differ diff --git a/src/images/banner-accessories.png b/src/images/banner-accessories.png new file mode 100644 index 0000000000..2fd72d599f Binary files /dev/null and b/src/images/banner-accessories.png differ diff --git a/src/images/banner-ipad.png b/src/images/banner-ipad.png new file mode 100644 index 0000000000..7c7fe1c926 Binary files /dev/null and b/src/images/banner-ipad.png differ diff --git a/src/images/banner-iphone.png b/src/images/banner-iphone.png new file mode 100644 index 0000000000..49836e43c1 Binary files /dev/null and b/src/images/banner-iphone.png differ diff --git a/src/images/fav-icon.png b/src/images/fav-icon.png new file mode 100644 index 0000000000..ceafde1dbe Binary files /dev/null and b/src/images/fav-icon.png differ diff --git a/src/images/icons/Arrow.svg b/src/images/icons/Arrow.svg new file mode 100644 index 0000000000..b96451529f --- /dev/null +++ b/src/images/icons/Arrow.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/images/icons/Home.svg b/src/images/icons/Home.svg new file mode 100644 index 0000000000..1c1fb7b596 --- /dev/null +++ b/src/images/icons/Home.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/images/icons/LOGO.svg b/src/images/icons/LOGO.svg new file mode 100644 index 0000000000..8b5444eb0c --- /dev/null +++ b/src/images/icons/LOGO.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/images/icons/close-icon.svg b/src/images/icons/close-icon.svg new file mode 100644 index 0000000000..3f0794f2b9 --- /dev/null +++ b/src/images/icons/close-icon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/images/icons/icon-cart.svg b/src/images/icons/icon-cart.svg new file mode 100644 index 0000000000..526d65c41e --- /dev/null +++ b/src/images/icons/icon-cart.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/images/icons/icon-close-primary.svg b/src/images/icons/icon-close-primary.svg new file mode 100644 index 0000000000..2e578c91a8 --- /dev/null +++ b/src/images/icons/icon-close-primary.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/images/icons/icon-close.svg b/src/images/icons/icon-close.svg new file mode 100644 index 0000000000..f5e7566b2f --- /dev/null +++ b/src/images/icons/icon-close.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/images/icons/icon-favourites-active.svg b/src/images/icons/icon-favourites-active.svg new file mode 100644 index 0000000000..089b4d6d6c --- /dev/null +++ b/src/images/icons/icon-favourites-active.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/images/icons/icon-favourites.svg b/src/images/icons/icon-favourites.svg new file mode 100644 index 0000000000..7362d82a90 --- /dev/null +++ b/src/images/icons/icon-favourites.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/images/icons/icon-minus.svg b/src/images/icons/icon-minus.svg new file mode 100644 index 0000000000..f6075c9ee8 --- /dev/null +++ b/src/images/icons/icon-minus.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/images/icons/icon-plus.svg b/src/images/icons/icon-plus.svg new file mode 100644 index 0000000000..ab3c34061b --- /dev/null +++ b/src/images/icons/icon-plus.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/images/icons/icon-search.svg b/src/images/icons/icon-search.svg new file mode 100644 index 0000000000..b185746731 --- /dev/null +++ b/src/images/icons/icon-search.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/images/icons/menu-burger.svg b/src/images/icons/menu-burger.svg new file mode 100644 index 0000000000..a6ced7642b --- /dev/null +++ b/src/images/icons/menu-burger.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/index.tsx b/src/index.tsx index 5f7410fd92..037e34506c 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,8 +1,21 @@ import ReactDOM from 'react-dom'; +import { + HashRouter as Router, +} from 'react-router-dom'; +import { CartContextProvider } from './contexts/CartContextProvider'; +import { FavoriteContextProvider } from './contexts/FavoriteContextProvider'; import App from './App'; +import './styles/main.scss'; + ReactDOM.render( - , + + + + + + + , document.getElementById('root'), ); diff --git a/src/pages/AccessoriesPage/AccessoriesPage.scss b/src/pages/AccessoriesPage/AccessoriesPage.scss new file mode 100644 index 0000000000..570e5ebafb --- /dev/null +++ b/src/pages/AccessoriesPage/AccessoriesPage.scss @@ -0,0 +1 @@ +@import "../../styles/global-imports"; diff --git a/src/pages/AccessoriesPage/AccessoriesPage.tsx b/src/pages/AccessoriesPage/AccessoriesPage.tsx new file mode 100644 index 0000000000..a5ea195cb3 --- /dev/null +++ b/src/pages/AccessoriesPage/AccessoriesPage.tsx @@ -0,0 +1,54 @@ +import { useState, useEffect } from 'react'; +import { motion } from 'framer-motion'; + +import { Product } from '../../types/Product'; +import { getAllProducts } from '../../helpers/fetchProducts'; +import { ProductType } from '../../types/ProductType'; +import { ProductsPage } from '../ProductsPage'; + +import './AccessoriesPage.scss'; +import { motionParametr } from '../../helpers/motionParametr'; + +export const AccessoriesPage = () => { + const [productsAccessorie, setProductsAccessorie] = useState([]); + const [isLoading, setIsLoading] = useState(false); + const [isError, setIsError] = useState(false); + + const fetchAccessories = async () => { + setIsLoading(true); + + try { + const getAccessoriesFromServer = (await getAllProducts()) + .filter(currentProduct => ( + currentProduct.category === ProductType.Accessory + )); + + setProductsAccessorie(getAccessoriesFromServer); + } catch { + setIsError(true); + } finally { + setIsLoading(false); + } + }; + + useEffect(() => { + fetchAccessories(); + }, []); + + return ( + +
    + +
    +
    + ); +}; diff --git a/src/pages/CartPage/CartInfo/CartInfo.scss b/src/pages/CartPage/CartInfo/CartInfo.scss new file mode 100644 index 0000000000..f1cf4f2501 --- /dev/null +++ b/src/pages/CartPage/CartInfo/CartInfo.scss @@ -0,0 +1,70 @@ +@import "../../../styles/global-imports"; + +.cart-info { + width: 370px; + padding: 24px; + + text-align: center; + border: 1px solid $c-elements; + + @include onMobileAndTablet { + display: flex; + flex-direction: column; + align-items: center; + + width: 65vw; + max-width: 752px; + margin: 50px 0; + } + + @include onMobile { + margin: 14px 0; + } + + &__total-price { + border-bottom: 1px solid $c-elements; + padding-bottom: 24px; + + &--quantity { + @extend %body-text; + color: $c-secondary; + } + } + + &--button { + width: 320px; + height: 48px; + + margin-top: 16px; + + background-color: $c-primary; + + border: 1px solid $c-primary; + + color: $c-white; + + cursor: pointer; + + @include hover(background-color, $c-white); + @include hover(color, $c-primary); + + @include onMobile { + width: 50vw; + } + } + + &--checkout-text { + padding: 24px; + + margin: 16px auto 0; + + border: 1px solid $c-red; + max-width: 80%; + + @include onMobileAndTablet { + padding: 14px; + width: inherit; + margin: 16px auto 0; + } + } +} diff --git a/src/pages/CartPage/CartInfo/CartInfo.tsx b/src/pages/CartPage/CartInfo/CartInfo.tsx new file mode 100644 index 0000000000..bf2e73f0fa --- /dev/null +++ b/src/pages/CartPage/CartInfo/CartInfo.tsx @@ -0,0 +1,106 @@ +import { useContext, useState } from 'react'; +import { motion } from 'framer-motion'; + +import { CartContext } from '../../../contexts/CartContextProvider'; +import { getCartPrice } from '../../../helpers/getCartPrice'; + +import './CartInfo.scss'; + +export const CartInfo = () => { + const [isClicked, setIsClicked] = useState(false); + const { cart } = useContext(CartContext); + + const totalItems = cart.reduce((total, currentItem) => ( + currentItem.quantity + total + ), 0); + + const handleCheckoutClick = () => { + setIsClicked(true); + + setTimeout(() => { + setIsClicked(false); + }, 3000); + }; + + const checkoutAnimate = { + enter: { + opacity: 1, + transition: { + delay: 0.3, + duration: 0.3, + animationTimingFunction: 'ease-in-out', + }, + display: 'block', + }, + exit: { + opacity: 0, + transition: { + duration: 0.2, + animationTimingFunction: 'ease-in-out', + }, + transitionEnd: { + display: 'none', + }, + }, + }; + + const checkoutBlockAnimate = { + enter: { + height: '278px', + transition: { + duration: 0.3, + animationTimingFunction: 'ease-in-out', + }, + }, + exit: { + height: '142px', + transition: { + duration: 0.6, + animationTimingFunction: 'ease-in-out', + }, + }, + }; + + return ( + +
    +
    +

    + {`$${getCartPrice(cart)}`} +

    + +
    + {` + Total for + ${totalItems} + ${totalItems <= 1 ? 'item' : 'items'} + `} +
    +
    + + Checkout + + +
    +
    + + {'Sorry, this feature isn\'t inplement yet'} + +
    +
    + ); +}; diff --git a/src/pages/CartPage/CartItem/CartItem.scss b/src/pages/CartPage/CartItem/CartItem.scss new file mode 100644 index 0000000000..564757ff17 --- /dev/null +++ b/src/pages/CartPage/CartItem/CartItem.scss @@ -0,0 +1,139 @@ +@import "../../../styles/global-imports"; + +.cart-item { + display: flex; + align-items: center; + justify-content: space-around; + + width: 752px; + height: 128px; + border: 1px solid $c-elements; + + @include onMobileAndTablet { + width: 80vw; + max-width: 800px; + height: 328px; + + position: relative; + + flex-direction: column; + } + + &--button-delete { + background-color: $c-white; + height: 16px; + width: 16px; + + cursor: pointer; + + &-image { + width: 16px; + height: 16px; + background: url("../../../images/icons/close-icon.svg") no-repeat center; + + @include onMobileAndTablet { + background-image: url("../../../images/icons/icon-close.svg"); + width: 26px; + height: 26px; + } + } + + @include onMobileAndTablet { + position: absolute; + top: 14px; + left: 14px; + } + } + + &__link { + display: flex; + align-items: center; + gap: 30px; + + @include onMobile { + flex-direction: column; + } + + &--image { + height: 66px; + } + + &--name { + @extend %body-text; + color: $c-primary; + + width: 294px; + + @include onMobile { + width: 60vw; + } + } + } + + &__amount { + display: flex; + flex-direction: row; + align-items: center; + + @include onMobile { + flex-direction: column; + gap: 14px; + } + + &--button { + display: flex; + justify-content: center; + align-items: center; + + width: 32px; + height: 32px; + background-color: $c-white; + border: 1px solid $c-primary; + + cursor: pointer; + + @include hover(transform, scale(110%)); + + &-image { + width: 16px; + height: 16px; + + &-plus { + background: url("../../../images/icons/icon-plus.svg") no-repeat center; + } + + &-minus { + background: url("../../../images/icons/icon-minus.svg") no-repeat center; + + &-disabled { + opacity: 0.5; + } + } + } + + &-decrease { + &-disabled { + pointer-events: none; + border-color: $c-elements; + } + } + } + } + + &__quantity { + &-container { + display: flex; + align-items: center; + gap: 14px; + } + } + + &--quantity { + @extend %body-text; + } + + &--price { + margin-left: 40px; + min-width: 100px; + } +} diff --git a/src/pages/CartPage/CartItem/CartItem.tsx b/src/pages/CartPage/CartItem/CartItem.tsx new file mode 100644 index 0000000000..c230ab355a --- /dev/null +++ b/src/pages/CartPage/CartItem/CartItem.tsx @@ -0,0 +1,123 @@ +import { Link } from 'react-router-dom'; + +import classNames from 'classnames'; +import './CartItem.scss'; + +import { useContext } from 'react'; +import { CartItem as CartItemType } from '../../../types/CartItem'; +import { Action, CartContext } from '../../../contexts/CartContextProvider'; +import { IMAGE_URL } from '../../../helpers/IMAGE_URL'; + +type Props = { + name: string, + image: string, + price: number, + category: string, + phoneId: string, + cartItem: CartItemType, +}; + +export const CartItem: React.FC = ({ + name, + price, + image, + phoneId, + cartItem, + category, +}) => { + const { + removeFromCart, + cartAmount, + } = useContext(CartContext); + + const handleRemoveItem = (productId: string) => { + removeFromCart(productId); + }; + + const handleQuantityPlus = (productId: string) => { + cartAmount(productId, Action.Increase); + }; + + const handleQuantityMinus = (productId: string) => { + cartAmount(productId, Action.Decrease); + }; + + return ( +
  • + + + +
    + {image} +
    + +
    + {name} +
    + + +
    +
    + + +
    + {cartItem.quantity} +
    + + +
    + +

    + {`$${price * cartItem.quantity}`} +

    +
    + +
  • + ); +}; diff --git a/src/pages/CartPage/CartItemList/CartItemList.scss b/src/pages/CartPage/CartItemList/CartItemList.scss new file mode 100644 index 0000000000..fd9c1f776e --- /dev/null +++ b/src/pages/CartPage/CartItemList/CartItemList.scss @@ -0,0 +1,7 @@ +@import "../../../styles/global-imports"; + +.cart-items-list { + display: flex; + flex-direction: column; + gap: 16px; +} diff --git a/src/pages/CartPage/CartItemList/CartItemList.tsx b/src/pages/CartPage/CartItemList/CartItemList.tsx new file mode 100644 index 0000000000..10e2b2a9c7 --- /dev/null +++ b/src/pages/CartPage/CartItemList/CartItemList.tsx @@ -0,0 +1,35 @@ +import { useContext } from 'react'; +import { CartContext } from '../../../contexts/CartContextProvider'; +import { CartItem } from '../CartItem/CartItem'; + +import './CartItemList.scss'; + +export const CartItemList = () => { + const { cart } = useContext(CartContext); + + return ( +
      + {cart.map(currentItem => { + const { + name, + image, + price, + phoneId, + category, + } = currentItem.product; + + return ( + + ); + })} +
    + ); +}; diff --git a/src/pages/CartPage/CartPage.scss b/src/pages/CartPage/CartPage.scss new file mode 100644 index 0000000000..ef7bb67eb6 --- /dev/null +++ b/src/pages/CartPage/CartPage.scss @@ -0,0 +1,13 @@ +@import "../../styles/global-imports"; + +.cart-page { + &__content { + display: flex; + gap: 16px; + + @include onMobileAndTablet { + flex-direction: column; + align-items: center; + } + } +} diff --git a/src/pages/CartPage/CartPage.tsx b/src/pages/CartPage/CartPage.tsx new file mode 100644 index 0000000000..7914d756c5 --- /dev/null +++ b/src/pages/CartPage/CartPage.tsx @@ -0,0 +1,45 @@ +import { useContext } from 'react'; +import { motion } from 'framer-motion'; + +import { CartContext } from '../../contexts/CartContextProvider'; +import { ButtonBack } from '../../components/Button/ButtonBack/ButtonBack'; +import { CartItemList } from './CartItemList/CartItemList'; +import { CartInfo } from './CartInfo/CartInfo'; + +import './CartPage.scss'; +import { motionParametr } from '../../helpers/motionParametr'; + +export const CartPage = () => { + const { cart } = useContext(CartContext); + + return ( + +
    + +
    + +

    + Cart +

    + +
    + {cart.length ? ( + <> +
    + +
    + + + + ) : ( +

    + Your cart is empty +

    + )} +
    +
    + ); +}; diff --git a/src/pages/FavoritesPage/FavoritesItemList/FavoritesItemList.scss b/src/pages/FavoritesPage/FavoritesItemList/FavoritesItemList.scss new file mode 100644 index 0000000000..0b85a657d9 --- /dev/null +++ b/src/pages/FavoritesPage/FavoritesItemList/FavoritesItemList.scss @@ -0,0 +1,38 @@ +@import "../../../styles/global-imports"; + +.favorites-list { + display: flex; + flex-wrap: wrap; + gap: 16px; + + @include onMobile { + align-items: center; + justify-content: center; + } + + @include onSmallMobile { + flex-direction: column; + } + + &--item { + @include onSmallMobile { + max-width: 66vw; + } + + @include fromMediumMobile { + max-width: 36vw !important; + } + } + + .product-card { + @include onMobile { + padding: 18px 12px; + } + + &--image { + @include onMobile { + height: 120px; + } + } + } +} diff --git a/src/pages/FavoritesPage/FavoritesItemList/FavoritesItemList.tsx b/src/pages/FavoritesPage/FavoritesItemList/FavoritesItemList.tsx new file mode 100644 index 0000000000..c2cb8f2a4f --- /dev/null +++ b/src/pages/FavoritesPage/FavoritesItemList/FavoritesItemList.tsx @@ -0,0 +1,55 @@ +import { useSearchParams } from 'react-router-dom'; +import { + useContext, + useEffect, + useMemo, + useState, +} from 'react'; +import { FavoriteContext } from '../../../contexts/FavoriteContextProvider'; +import { ProductCard } from '../../../components/Product/Card'; +import { Product } from '../../../types/Product'; +import { productFilter } from '../../../helpers/productFilter'; + +import './FavoritesItemList.scss'; + +export const FavoritesItemList = () => { + const { favorites } = useContext(FavoriteContext); + const [searchParams] = useSearchParams(); + const query = searchParams.get('query') || ''; + const [debouncedQuery, setDebouncedQuery] = useState(query); + + useEffect(() => { + const debounceTimer = setTimeout(() => { + setDebouncedQuery(query); + }, 500); + + return () => clearTimeout(debounceTimer); + }, [query]); + + const filterProducts = useMemo(() => ( + productFilter(favorites, query) + ), [debouncedQuery, favorites]); + + return ( + <> +
      + {filterProducts.map((currentProduct: Product) => { + return ( +
    • + +
    • + ); + })} +
    + + {!filterProducts.length && ( +

    + No search results +

    + )} + + ); +}; diff --git a/src/pages/FavoritesPage/FavoritesPage.scss b/src/pages/FavoritesPage/FavoritesPage.scss new file mode 100644 index 0000000000..f6663cb1d8 --- /dev/null +++ b/src/pages/FavoritesPage/FavoritesPage.scss @@ -0,0 +1,10 @@ +@import "../../styles/global-imports"; + +.favorites { + &--count { + @extend %body-text; + color: $c-secondary; + + margin-bottom: 40px; + } +} diff --git a/src/pages/FavoritesPage/FavoritesPage.tsx b/src/pages/FavoritesPage/FavoritesPage.tsx new file mode 100644 index 0000000000..796976fcca --- /dev/null +++ b/src/pages/FavoritesPage/FavoritesPage.tsx @@ -0,0 +1,37 @@ +import { useContext } from 'react'; +import { motion } from 'framer-motion'; + +import { FavoriteContext } from '../../contexts/FavoriteContextProvider'; +import { HistoryLocation } from '../../components/HistoryLocation'; +import { FavoritesItemList } from './FavoritesItemList/FavoritesItemList'; + +import './FavoritesPage.scss'; +import { motionParametr } from '../../helpers/motionParametr'; + +export const FavoritesPage = () => { + const { favorites } = useContext(FavoriteContext); + const legthOfFavorites = favorites.length; + + return ( + +
    + +
    + +

    + Favorites +

    + +
    + { + `${legthOfFavorites} ${legthOfFavorites <= 1 ? 'item' : 'items'}` + } +
    + + +
    + ); +}; diff --git a/src/pages/HomePage/Banner/Banner.scss b/src/pages/HomePage/Banner/Banner.scss new file mode 100644 index 0000000000..3b081fa879 --- /dev/null +++ b/src/pages/HomePage/Banner/Banner.scss @@ -0,0 +1,90 @@ +@import "../../../styles/global-imports"; + +.banner { + @include onTabletAndDesktop { + margin: 40px auto 60px; + width: 80vw; + max-width: 1156px; + } + + @include onMobile { + margin-bottom: 20px; + } + + &__slide { + width: 90%; + margin: 0 auto; + + @include onMobile { + width: 100%; + } + } + + &--link { + width: 100%; + height: 100%; + z-index: 10; + } + + .carousel { + padding-bottom: 30px; + + @include onTablet { + padding: 0 10px 30px; + } + + &-slider { + width: 97%; + } + + .control { + &-arrow { + border: 1px solid $c-icons; + padding: 0 6px; + bottom: 30px; + opacity: 1; + + &:hover { + border-color: $c-primary; + background: none; + } + + &::before { + border: none; + width: 16px; + height: 16px; + background: url("../../../images/icons/Arrow.svg") center no-repeat; + background-size: contain; + + @include onTablet { + width: 10px; + height: 10px; + } + } + + @include onTablet { + padding: 0 4px; + } + + @include onMobile { + display: none; + } + } + + &-prev { + &::before { + transform: rotate(180deg); + } + } + } + + } + + .dot { + width: 16px !important; + height: 6px !important; + background: $c-primary !important; + border-radius: 0% !important; + box-shadow: none !important; + } +} diff --git a/src/pages/HomePage/Banner/Banner.tsx b/src/pages/HomePage/Banner/Banner.tsx new file mode 100644 index 0000000000..4ccb542e71 --- /dev/null +++ b/src/pages/HomePage/Banner/Banner.tsx @@ -0,0 +1,47 @@ +import 'react-responsive-carousel/lib/styles/carousel.min.css'; // requires a loader +import { Carousel } from 'react-responsive-carousel'; + +import phonesImage from '../../../images/banner-iphone.png'; +import tabletsImage from '../../../images/banner-ipad.png'; +import accessoriesImage from '../../../images/banner-accessories.png'; + +import './Banner.scss'; + +const BannerContent = [ + { + name: 'phones', + photo: phonesImage, + }, + { + name: 'tablets', + photo: tabletsImage, + }, + { + name: 'accessories', + photo: accessoriesImage, + }, +]; + +export const Banner = () => { + return ( + + {BannerContent.map(currentItem => ( +
    + {currentItem.name} +
    + ))} +
    + ); +}; diff --git a/src/pages/HomePage/Banner/index.ts b/src/pages/HomePage/Banner/index.ts new file mode 100644 index 0000000000..bc95f09d62 --- /dev/null +++ b/src/pages/HomePage/Banner/index.ts @@ -0,0 +1 @@ +export * from './Banner'; diff --git a/src/pages/HomePage/Categories/Categories.scss b/src/pages/HomePage/Categories/Categories.scss new file mode 100644 index 0000000000..d05a6e7c50 --- /dev/null +++ b/src/pages/HomePage/Categories/Categories.scss @@ -0,0 +1,37 @@ +@import "../../../styles/global-imports"; + +.categories { + margin-top: 80px; + + &--title { + margin-bottom: 16px; + } + + &__content { + display: flex; + gap: 16px; + + @include onMobile { + flex-direction: column; + } + } + + &__category { + transition: all, 0.3s ease-in-out !important; + + @include hover(transform, scale(104%)); + + &--image { + width: 100%; + } + } + + &__description { + margin-top: 16px; + + &--subtitle { + @extend %body-text; + color: $c-secondary; + } + } +} diff --git a/src/pages/HomePage/Categories/Categories.tsx b/src/pages/HomePage/Categories/Categories.tsx new file mode 100644 index 0000000000..fe2a6396d3 --- /dev/null +++ b/src/pages/HomePage/Categories/Categories.tsx @@ -0,0 +1,79 @@ +import { Link } from 'react-router-dom'; +import './Categories.scss'; + +import phoneImage from '../../../images/Categories/Phones.png'; +import tabletsImage from '../../../images/Categories/Tablets.png'; +import accessoriesImage from '../../../images/Categories/Accessories.png'; + +import { Product } from '../../../types/Product'; + +type Props = { + products: Product[]; +}; + +const CATEGORIES = ['phones', 'tablets', 'accessories']; + +export const Categories: React.FC = ({ products }) => { + const amountProducts = (category: string) => { + return products.filter(currentProduct => ( + currentProduct.category === category + )).length; + }; + + const handleImageCategory = (category: string) => { + switch (category) { + case 'phones': + return phoneImage; + + case 'tablets': + return tabletsImage; + + case 'accessories': + return accessoriesImage; + + default: + return ''; + } + }; + + const handleTitleCategory = (category: string): string => { + if (category === 'phones') { + return handleTitleCategory('mobile phones'); + } + + return category[0].toUpperCase() + category.slice(1); + }; + + return ( +
    +

    + Shop by category +

    + +
    + {CATEGORIES.map(currentCategory => ( + + {` + +
    +

    + {handleTitleCategory(currentCategory)} +

    + +

    + {`${amountProducts(currentCategory)} models`} +

    +
    + + ))} +
    +
    + ); +}; diff --git a/src/pages/HomePage/Categories/index.ts b/src/pages/HomePage/Categories/index.ts new file mode 100644 index 0000000000..79c7c7dcde --- /dev/null +++ b/src/pages/HomePage/Categories/index.ts @@ -0,0 +1 @@ +export * from './Categories'; diff --git a/src/pages/HomePage/HomePage.scss b/src/pages/HomePage/HomePage.scss new file mode 100644 index 0000000000..dcc1e4f609 --- /dev/null +++ b/src/pages/HomePage/HomePage.scss @@ -0,0 +1,10 @@ +@import "../../styles/global-imports"; + +.home-page { + &__loader { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + } +} diff --git a/src/pages/HomePage/HomePage.tsx b/src/pages/HomePage/HomePage.tsx new file mode 100644 index 0000000000..db532cef0a --- /dev/null +++ b/src/pages/HomePage/HomePage.tsx @@ -0,0 +1,105 @@ +import { + useState, + useEffect, +} from 'react'; +import { motion } from 'framer-motion'; + +import { + getAllProducts, + getBrandNewProducts, + getHotPriceProducts, +} from '../../helpers/fetchProducts'; + +import { Banner } from './Banner'; +import { ProductSlider } from '../../components/Product/Slider'; +import { Product } from '../../types/Product'; +import { Loader } from '../../components/Loader'; + +import './HomePage.scss'; + +import { ProductTitles } from '../../types/ProductTitles'; +import { Categories } from './Categories'; +import { motionParametr } from '../../helpers/motionParametr'; + +export const HomePage = () => { + const [products, setProducts] = useState([]); + + const [hotProducts, setHotProducts] = useState([]); + const [brandNewProducts, setBrandNewProducts] = useState([]); + + const [error, setError] = useState(''); + + const fetchHotProducts = async () => { + try { + const hotProductsFromServer: Product[] = await getHotPriceProducts(); + + setHotProducts(hotProductsFromServer); + } catch { + setError('Unable to load a hot products'); + } + }; + + const fetchAllProducts = async () => { + try { + const productsFromServer: Product[] = await getAllProducts(); + + setProducts(productsFromServer); + } catch { + setError('Unable to load a products'); + } + }; + + const fetchBrandNewProducts = async () => { + try { + const brandNewProductsFromServer: Product[] + = await getBrandNewProducts(); + + setBrandNewProducts(brandNewProductsFromServer); + } catch { + setError('Unable to load a brand new products'); + } + }; + + useEffect(() => { + fetchHotProducts(); + fetchAllProducts(); + fetchBrandNewProducts(); + }, []); + + return ( + + + +
    +
    + {!error && hotProducts ? ( + + ) : ( + + )} +
    +
    + +
    + +
    + +
    + {!error && brandNewProducts ? ( + + ) : ( + + )} +
    +
    + ); +}; diff --git a/src/pages/HomePage/index.ts b/src/pages/HomePage/index.ts new file mode 100644 index 0000000000..11e53da674 --- /dev/null +++ b/src/pages/HomePage/index.ts @@ -0,0 +1 @@ +export * from './HomePage'; diff --git a/src/pages/PageNotFound/PageNotFound.scss b/src/pages/PageNotFound/PageNotFound.scss new file mode 100644 index 0000000000..5a43476919 --- /dev/null +++ b/src/pages/PageNotFound/PageNotFound.scss @@ -0,0 +1,26 @@ +@import "../../styles/global-imports"; + +.page-not-found { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + + text-align: center; + + &--title { + font-size: 100px; + line-height: 94px; + } + + &--subtitle { + margin-top: 40px; + color: $c-secondary; + } + + &--link { + color: $c-secondary; + + @include hover(color, $c-primary); + } +} diff --git a/src/pages/PageNotFound/PageNotFound.tsx b/src/pages/PageNotFound/PageNotFound.tsx new file mode 100644 index 0000000000..71dc9c1839 --- /dev/null +++ b/src/pages/PageNotFound/PageNotFound.tsx @@ -0,0 +1,43 @@ +import { useState, useEffect } from 'react'; +import { motion } from 'framer-motion'; +import { NavLink } from 'react-router-dom'; +import { Loader } from '../../components/Loader'; +import './PageNotFound.scss'; + +export const PageNotFound = () => { + const [showLoader, setShowLoader] = useState(true); + + useEffect(() => { + const timer = setTimeout(() => { + setShowLoader(false); + }, 2000); + + return () => { + clearTimeout(timer); + }; + }, []); + + return ( + + {showLoader && } + {!showLoader && ( + <> +

    + It looks like you`re lost +

    + +

    + So let`s just go to the + +   + Home + +

    + + )} +
    + ); +}; diff --git a/src/pages/PhonesPage/PhonesPage.scss b/src/pages/PhonesPage/PhonesPage.scss new file mode 100644 index 0000000000..570e5ebafb --- /dev/null +++ b/src/pages/PhonesPage/PhonesPage.scss @@ -0,0 +1 @@ +@import "../../styles/global-imports"; diff --git a/src/pages/PhonesPage/PhonesPage.tsx b/src/pages/PhonesPage/PhonesPage.tsx new file mode 100644 index 0000000000..dd30f5ba7d --- /dev/null +++ b/src/pages/PhonesPage/PhonesPage.tsx @@ -0,0 +1,53 @@ +import { useState, useEffect } from 'react'; +import { motion } from 'framer-motion'; +import { Product } from '../../types/Product'; +import { getAllProducts } from '../../helpers/fetchProducts'; +import { ProductType } from '../../types/ProductType'; +import { ProductsPage } from '../ProductsPage'; + +import './PhonesPage.scss'; +import { motionParametr } from '../../helpers/motionParametr'; + +export const PhonesPage = () => { + const [productsPhone, setProductsPhone] = useState([]); + const [isLoading, setIsLoading] = useState(false); + const [isError, setIsError] = useState(false); + + const fetchPhones = async () => { + setIsLoading(true); + + try { + const getPhonesFromServer = (await getAllProducts()) + .filter(currentProduct => ( + currentProduct.category === ProductType.Phone + )); + + setProductsPhone(getPhonesFromServer); + } catch { + setIsError(true); + } finally { + setIsLoading(false); + } + }; + + useEffect(() => { + fetchPhones(); + }, []); + + return ( + +
    + +
    +
    + ); +}; diff --git a/src/pages/PhonesPage/index.ts b/src/pages/PhonesPage/index.ts new file mode 100644 index 0000000000..380be65cc7 --- /dev/null +++ b/src/pages/PhonesPage/index.ts @@ -0,0 +1 @@ +export * from './PhonesPage'; diff --git a/src/pages/ProductDetailsPage/ProductDetailsPage.scss b/src/pages/ProductDetailsPage/ProductDetailsPage.scss new file mode 100644 index 0000000000..2824fe78d6 --- /dev/null +++ b/src/pages/ProductDetailsPage/ProductDetailsPage.scss @@ -0,0 +1,82 @@ +@import "../../styles/global-imports"; + +.product-details { + &__top-content { + display: flex; + margin-top: 40px; + + @include onMobileAndTablet { + flex-direction: column; + align-items: center; + row-gap: 30px; + } + } + + &__virables { + width: 320px; + + @include onMobileAndTablet { + display: flex; + flex-wrap: wrap; + justify-content: center; + column-gap: 40px; + } + } + + &__price { + display: flex; + align-items: end; + margin-bottom: 16px; + } + + &__actions { + display: flex; + justify-content: space-between; + + @include onMobileAndTablet { + gap: 20px; + } + + &--cart { + width: 264px; + + @include onMobile { + width: 50vw; + } + } + } + + &__info { + display: flex; + flex-direction: column; + justify-content: space-around; + + margin-top: 32px; + + @include onMobileAndTablet { + display: none; + } + + &--item { + display: grid; + grid-template-columns: repeat(2, 1fr); + justify-items: stretch; + margin-bottom: 12px; + + @extend %small-text; + + &-title { + color: $c-secondary; + } + + &-value { + color: $c-primary; + text-align: end; + } + } + } + + &__about { + margin-top: 80px; + } +} diff --git a/src/pages/ProductDetailsPage/ProductDetailsPage.tsx b/src/pages/ProductDetailsPage/ProductDetailsPage.tsx new file mode 100644 index 0000000000..ad6a304d37 --- /dev/null +++ b/src/pages/ProductDetailsPage/ProductDetailsPage.tsx @@ -0,0 +1,272 @@ +import './ProductDetailsPage.scss'; +import { useEffect, useState } from 'react'; +import { useLocation } from 'react-router-dom'; +import { motion } from 'framer-motion'; + +import { Product } from '../../types/Product'; +import { ProductDetails } from '../../types/ProductDetails'; +import { + getProductDetails, + getProductsById, + getSuggestedProducts, +} from '../../helpers/fetchProducts'; +import { ProductTitles } from '../../types/ProductTitles'; + +import { HistoryLocation } from '../../components/HistoryLocation'; +import { Loader } from '../../components/Loader'; +import { ButtonCart } from '../../components/Button/ButtonCart'; +import { ButtonFavorites } from '../../components/Button/ButtonFavorites'; +import { ProductSlider } from '../../components/Product/Slider'; +import { Galery } from '../../components/Product/Galery'; +import { ColorChoose } from '../../components/Product/ColorChoose'; +import { Capacity } from '../../components/Product/Capacity'; +import { Description } from '../../components/Product/Description/Description'; +import { PageNotFound } from '../PageNotFound/PageNotFound'; +import { motionParametr } from '../../helpers/motionParametr'; + +export const ProductDetailsPage = () => { + const location = useLocation(); + const { pathname } = location; + + const productPath = pathname.split('/').filter(part => part !== ''); + const productId = productPath[productPath.length - 1]; + + const [isLoading, setIsLoading] = useState(false); + const [isError, setIsError] = useState(false); + const [mainPhoto, setMainPhoto] = useState(); + const [products, setProducts] = useState([]); + const [currentProduct, setCurrentProduct] = useState(); + const [productDetails, setProductDetails] + = useState(null); + + const fetchProductDetails = async () => { + setIsLoading(true); + + try { + const getDetailsFromServer = await getProductDetails(productId); + + setProductDetails(getDetailsFromServer); + setMainPhoto(productDetails?.images[0]); + } catch { + setIsError(true); + } finally { + setIsLoading(false); + } + }; + + const fetchProducts = async () => { + const productFromServer = await getProductsById(productId); + + setCurrentProduct(productFromServer); + }; + + const fetchSuggestedProducts = async () => { + setIsLoading(true); + + try { + const suggestedProductsFromServer: Product[] + = await getSuggestedProducts(); + + setProducts(suggestedProductsFromServer); + } catch { + setIsError(true); + } finally { + setIsLoading(false); + } + }; + + useEffect(() => { + if (productDetails?.images.length) { + setMainPhoto(productDetails.images[0]); + } + }, [productDetails]); + + useEffect(() => { + fetchProductDetails(); + fetchProducts(); + }, [productId]); + + useEffect(() => { + fetchSuggestedProducts(); + }, []); + + const colors = productDetails?.colorsAvailable || []; + const currentColor = productDetails?.color || ''; + const images = productDetails?.images || []; + const mainImg = mainPhoto || ''; + const capacities = productDetails?.capacityAvailable || []; + + const handlePhotoChosen = (photo: string) => { + setMainPhoto(photo); + }; + + const [showLoader, setShowLoader] = useState(true); + + useEffect(() => { + const timer = setTimeout(() => { + setShowLoader(false); + }, 150); + + return () => { + clearTimeout(timer); + }; + }, []); + + return ( + <> + {showLoader && } + {!showLoader && ( + + + + {!currentProduct && } + + {!isError && isLoading && } + + {!isError && !isLoading && currentProduct && ( + <> + + {productDetails?.name} + + + +
    + +
    + +
    + +
    +
    + +
    + +
    + +
    + +
    + {productDetails?.priceDiscount + === productDetails?.priceRegular + ? ( +

    + {`$${productDetails?.priceRegular}`} +

    + ) : ( + <> +

    + {`$${productDetails?.priceDiscount}`} +

    + +

    + {`$${productDetails?.priceRegular}`} +

    + + )} +
    +
    +
    + +
    + +
    +
    + +
    +
    + +
      +
    • +
      + Screen +
      +
      + {productDetails?.screen} +
      +
    • +
    • +
      + Resolution +
      +
      + {productDetails?.resolution} +
      +
    • +
    • +
      + Processor +
      +
      + {productDetails?.processor} +
      +
    • +
    • +
      + RAM +
      +
      + {productDetails?.ram} +
      +
    • +
    +
    +
    + + + {productDetails && ( + + )} + + + + + )} +
    + )} + + ); +}; diff --git a/src/pages/ProductDetailsPage/index.ts b/src/pages/ProductDetailsPage/index.ts new file mode 100644 index 0000000000..6615089e5e --- /dev/null +++ b/src/pages/ProductDetailsPage/index.ts @@ -0,0 +1 @@ +export * from './ProductDetailsPage'; diff --git a/src/pages/ProductsPage/ProductsPage.scss b/src/pages/ProductsPage/ProductsPage.scss new file mode 100644 index 0000000000..59df2ad48c --- /dev/null +++ b/src/pages/ProductsPage/ProductsPage.scss @@ -0,0 +1,11 @@ +@import "../../styles/global-imports"; + +.products-page { + &__nav { + margin: 24px 0 40px; + } + + &--title { + margin-bottom: 10px; + } +} diff --git a/src/pages/ProductsPage/ProductsPage.tsx b/src/pages/ProductsPage/ProductsPage.tsx new file mode 100644 index 0000000000..c442993233 --- /dev/null +++ b/src/pages/ProductsPage/ProductsPage.tsx @@ -0,0 +1,55 @@ +import { Product } from '../../types/Product'; +import { HistoryLocation } from '../../components/HistoryLocation'; +import { Loader } from '../../components/Loader'; +import { NoResult } from '../../components/NoResults'; +import { List } from '../../components/Product/List'; + +import './ProductsPage.scss'; + +type Props = { + isLoading: boolean, + isError: boolean, + products: Product[], + category: string, + title: string, +}; + +export const ProductsPage: React.FC = ({ + isLoading, + isError, + products, + category, + title, +}) => ( +
    + + +
    + {isLoading && !isError ? ( + + ) : ( + <> + {products.length > 0 ? ( + <> +

    + {title} +

    + + {!isLoading && !isError && ( + + )} + + ) : ( + + )} + + )} +
    +
    +); diff --git a/src/pages/ProductsPage/index.ts b/src/pages/ProductsPage/index.ts new file mode 100644 index 0000000000..8e350f20bf --- /dev/null +++ b/src/pages/ProductsPage/index.ts @@ -0,0 +1 @@ +export * from './ProductsPage'; diff --git a/src/pages/TabletsPage/TabletsPage.scss b/src/pages/TabletsPage/TabletsPage.scss new file mode 100644 index 0000000000..570e5ebafb --- /dev/null +++ b/src/pages/TabletsPage/TabletsPage.scss @@ -0,0 +1 @@ +@import "../../styles/global-imports"; diff --git a/src/pages/TabletsPage/TabletsPage.tsx b/src/pages/TabletsPage/TabletsPage.tsx new file mode 100644 index 0000000000..a18c575d5c --- /dev/null +++ b/src/pages/TabletsPage/TabletsPage.tsx @@ -0,0 +1,54 @@ +import { useState, useEffect } from 'react'; +import { motion } from 'framer-motion'; + +import { Product } from '../../types/Product'; +import { getAllProducts } from '../../helpers/fetchProducts'; +import { ProductType } from '../../types/ProductType'; + +import './TabletsPage.scss'; +import { ProductsPage } from '../ProductsPage'; +import { motionParametr } from '../../helpers/motionParametr'; + +export const TabletPage = () => { + const [productsTablet, setProductsTablet] = useState([]); + const [isLoading, setIsLoading] = useState(false); + const [isError, setIsError] = useState(false); + + const fetchTablets = async () => { + setIsLoading(true); + + try { + const getTabletsFromServer = (await getAllProducts()) + .filter(currentProduct => ( + currentProduct.category === ProductType.Tablet + )); + + setProductsTablet(getTabletsFromServer); + } catch { + setIsError(true); + } finally { + setIsLoading(false); + } + }; + + useEffect(() => { + fetchTablets(); + }, []); + + return ( + +
    + +
    +
    + ); +}; diff --git a/src/styles/_container.scss b/src/styles/_container.scss new file mode 100644 index 0000000000..065df8a8ca --- /dev/null +++ b/src/styles/_container.scss @@ -0,0 +1,22 @@ +@import "./utils/mixins"; + +.container { + margin: 0 auto 80px; + max-width: 1156px; + + @include onTablet { + max-width: 80%; + } + + @include onMobile { + max-width: 80%; + } + + &-footer { + margin-bottom: 0; + } + + &--title { + margin-bottom: 24px; + } +} diff --git a/src/styles/_fonts.scss b/src/styles/_fonts.scss new file mode 100644 index 0000000000..6cb7e55b93 --- /dev/null +++ b/src/styles/_fonts.scss @@ -0,0 +1,27 @@ +@font-face { + font-family: "Mont Bold"; + src: url("../fonts/Mont-Bold.otf") format("opentype"); + font-weight: 700; + font-style: normal; +} + +@font-face { + font-family: "Mont SemiBold"; + src: url("../fonts/Mont-SemiBold.otf") format("opentype"); + font-weight: 600; + font-style: normal; +} + +@font-face { + font-family: "Mont Regular"; + src: url("../fonts/Mont-Regular.otf") format("opentype"); + font-weight: 500; + font-style: normal; +} + +@font-face { + font-family: "Mont Thin"; + src: url("../fonts/Montserrat-Thin.ttf") format("opentype"); + font-weight: 300; + font-style: normal; +} diff --git a/src/styles/_global-imports.scss b/src/styles/_global-imports.scss new file mode 100644 index 0000000000..80490d3467 --- /dev/null +++ b/src/styles/_global-imports.scss @@ -0,0 +1,4 @@ +@import "./utils/variables"; +@import "./utils/extends"; +@import "./utils/mixins"; +@import "./container"; diff --git a/src/styles/_global.scss b/src/styles/_global.scss new file mode 100644 index 0000000000..e8267bf38c --- /dev/null +++ b/src/styles/_global.scss @@ -0,0 +1,3 @@ +body.with-menu { + overflow: hidden; +} diff --git a/src/styles/_typography.scss b/src/styles/_typography.scss new file mode 100644 index 0000000000..96b8e447b8 --- /dev/null +++ b/src/styles/_typography.scss @@ -0,0 +1,21 @@ +@import "./global-imports"; + +h1 { + @extend %h1; +} + +h2 { + @extend %h2; +} + +h3 { + @extend %h3; +} + +.uppercase { + @extend %uppercase; +} + +button { + @extend %buttons; +} diff --git a/src/styles/main.scss b/src/styles/main.scss new file mode 100644 index 0000000000..c11dbeb748 --- /dev/null +++ b/src/styles/main.scss @@ -0,0 +1,3 @@ +@import "./fonts"; +@import "./typography"; +@import "./utils/reset"; diff --git a/src/styles/utils/_extends.scss b/src/styles/utils/_extends.scss new file mode 100644 index 0000000000..da7e29fd70 --- /dev/null +++ b/src/styles/utils/_extends.scss @@ -0,0 +1,56 @@ +%h1 { + font-weight: 700; + font-size: 32px; + line-height: 41px; + letter-spacing: -0.01em; + color: $c-primary; + font-family: "Mont Bold", sans-serif; +} + +%h2 { + font-weight: 700; + font-size: 22px; + line-height: 31px; + color: $c-primary; + font-family: "Mont Bold", sans-serif; +} + +%h3 { + font-weight: 600; + font-size: 20px; + line-height: 26px; + color: $c-primary; + font-family: "Mont SemiBold", sans-serif; +} + +%body-text { + font-weight: 500; + font-size: 14px; + line-height: 21px; + color: $c-primary; + font-family: "Mont Regular", sans-serif; +} + +%small-text { + font-weight: 600; + font-size: 12px; + line-height: 15px; + color: $c-primary; + font-family: "Mont SemiBold", sans-serif; +} + +%uppercase { + font-weight: 700; + font-size: 12px; + line-height: 11px; + font-family: "Mont Bold", sans-serif; + text-transform: uppercase; + letter-spacing: 0.04em; +} + +%buttons { + font-weight: 600; + font-size: 14px; + line-height: 21px; + font-family: "Mont SemiBold", sans-serif; +} diff --git a/src/styles/utils/_mixins.scss b/src/styles/utils/_mixins.scss new file mode 100644 index 0000000000..c719b34e3d --- /dev/null +++ b/src/styles/utils/_mixins.scss @@ -0,0 +1,51 @@ +@import "./variables"; + +@mixin hover($property, $toValue) { + transition: #{$property}, $effect-duration ease-in-out; + + &:hover { + #{$property}: $toValue; + } +} + +@mixin onMobile { + @media (min-width: 320px) and (max-width: 767px) { + @content; + } +} + +@mixin onSmallMobile { + @media (min-width: 319px) and (max-width: 374px) { + @content; + } +} + +@mixin fromMediumMobile { + @media (min-width: 375px) and (max-width: 766px) { + @content; + } +} + +@mixin onTablet { + @media (min-width: 767px) and (max-width: 1199px) { + @content; + } +} + +@mixin onDesktop { + @media (min-width: 1199px) { + @content; + } +} + +@mixin onMobileAndTablet { + @media (min-width: 320px) and (max-width: 1199px) { + @content; + } +} + +@mixin onTabletAndDesktop { + @media (min-width: 768px) { + @content; + } +} diff --git a/src/styles/utils/_reset.scss b/src/styles/utils/_reset.scss new file mode 100644 index 0000000000..7aadbfe2e8 --- /dev/null +++ b/src/styles/utils/_reset.scss @@ -0,0 +1,39 @@ +h1, +h2, +h3, +p, +ul, +body { + margin: 0; +} + +a { + text-decoration: none; +} + +ul { + padding: 0; + + list-style: none; +} + +button { + margin: 0; + padding: 0; + + border: 0; +} + +table { + border-spacing: 0; + + & td { + padding: 0; + } +} + +select, +option { + margin: 0; + padding: 0; +} diff --git a/src/styles/utils/_variables.scss b/src/styles/utils/_variables.scss new file mode 100644 index 0000000000..63c2661fbb --- /dev/null +++ b/src/styles/utils/_variables.scss @@ -0,0 +1,15 @@ +$c-primary: #313237; +$c-secondary: #89939a; +$c-icons: #b4bdc3; +$c-elements: #e2e5e9; +$c-hover_bg: #fafbfc; +$c-white: #fff; +$c-green: #27ae60; +$c-red: #eb5757; + +$arrow-image: url("../../images/icons/Arrow.svg"); + +$effect-duration: 0.3s; + +$header-height: 64px; +$footer-height: 96px; diff --git a/src/types/CartItem.ts b/src/types/CartItem.ts new file mode 100644 index 0000000000..03788ae372 --- /dev/null +++ b/src/types/CartItem.ts @@ -0,0 +1,7 @@ +import { Product } from './Product'; + +export interface CartItem { + id: string, + quantity: number, + product: Product, +} diff --git a/src/types/Product.ts b/src/types/Product.ts new file mode 100644 index 0000000000..6d37d1d7e4 --- /dev/null +++ b/src/types/Product.ts @@ -0,0 +1,15 @@ +export interface Product { + id: string, + category: string, + phoneId: string, + itemId: string, + name: string, + fullPrice: number, + price: number, + screen: string, + capacity: string, + color: string, + ram: string, + year: number, + image: string, +} diff --git a/src/types/ProductDetails.ts b/src/types/ProductDetails.ts new file mode 100644 index 0000000000..58367114a9 --- /dev/null +++ b/src/types/ProductDetails.ts @@ -0,0 +1,25 @@ +export interface ProductDetails { + id: string, + namespaceId: string, + name: string, + capacityAvailable: string[], + capacity: string, + priceRegular: number, + priceDiscount: number, + colorsAvailable: string[], + color: string, + images: string[], + description: ProductDescription[], + screen: string, + resolution: string, + processor: string, + ram: string, + camera: string, + zoom: string, + cell: string[] +} + +export interface ProductDescription { + title: string, + text: string[] +} diff --git a/src/types/ProductTitles.ts b/src/types/ProductTitles.ts new file mode 100644 index 0000000000..942b0c4de2 --- /dev/null +++ b/src/types/ProductTitles.ts @@ -0,0 +1,5 @@ +export enum ProductTitles { + HotPrice = 'Hot prices', + NewBrand = 'Brand new models', + RandomProducts = 'You may also like', +} diff --git a/src/types/ProductType.ts b/src/types/ProductType.ts new file mode 100644 index 0000000000..ae6e9cfbab --- /dev/null +++ b/src/types/ProductType.ts @@ -0,0 +1,5 @@ +export enum ProductType { + Phone = 'phones', + Tablet = 'tablets', + Accessory = 'accessories', +} diff --git a/src/types/SortTypes.ts b/src/types/SortTypes.ts new file mode 100644 index 0000000000..3004323571 --- /dev/null +++ b/src/types/SortTypes.ts @@ -0,0 +1,23 @@ +export enum SortTypes { + Newest = 'Newest', + Alphabetically = 'Alphabetically', + Cheapest = 'Cheapest', +} + +export interface Option { + label: string; + value: string; +} + +export const sortParam = [ + { label: 'Newest', value: 'Newest' }, + { label: 'Alphabetically', value: 'Alphabetically' }, + { label: 'Cheapest', value: 'Cheapest' }, +]; + +export const itemsOnPage = [ + { label: 'All', value: 'All' }, + { label: 'Four', value: '4' }, + { label: 'Eight', value: '8' }, + { label: 'Sixteen', value: '16' }, +];