diff --git a/index.html b/index.html index 095fb3a4537..18eac5712b1 100644 --- a/index.html +++ b/index.html @@ -3,7 +3,9 @@ - Vite + React + TS + + + N. Store
diff --git a/package-lock.json b/package-lock.json index 836b9e63b46..8405c76829b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,11 +16,12 @@ "react": "^18.3.1", "react-dom": "^18.3.1", "react-router-dom": "^6.25.1", - "react-transition-group": "^4.4.5" + "react-transition-group": "^4.4.5", + "swiper": "^12.1.3" }, "devDependencies": { "@cypress/react18": "^2.0.1", - "@mate-academy/scripts": "^1.8.5", + "@mate-academy/scripts": "^2.1.3", "@mate-academy/students-ts-config": "*", "@mate-academy/stylelint-config": "*", "@types/node": "^20.14.10", @@ -44,7 +45,7 @@ "mochawesome-merge": "^4.3.0", "mochawesome-report-generator": "^6.2.0", "prettier": "^3.3.2", - "sass": "^1.77.8", + "sass": "^1.99.0", "stylelint": "^16.7.0", "typescript": "^5.2.2", "vite": "^5.3.1" @@ -1184,10 +1185,11 @@ } }, "node_modules/@mate-academy/scripts": { - "version": "1.8.5", - "resolved": "https://registry.npmjs.org/@mate-academy/scripts/-/scripts-1.8.5.tgz", - "integrity": "sha512-mHRY2FkuoYCf5U0ahIukkaRo5LSZsxrTSgMJheFoyf3VXsTvfM9OfWcZIDIDB521kdPrScHHnRp+JRNjCfUO5A==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@mate-academy/scripts/-/scripts-2.1.3.tgz", + "integrity": "sha512-a07wHTj/1QUK2Aac5zHad+sGw4rIvcNl5lJmJpAD7OxeSbnCdyI6RXUHwXhjF5MaVo9YHrJ0xVahyERS2IIyBQ==", "dev": true, + "license": "MIT", "dependencies": { "@octokit/rest": "^17.11.2", "@types/get-port": "^4.2.0", @@ -1863,6 +1865,330 @@ "@octokit/openapi-types": "^22.2.0" } }, + "node_modules/@parcel/watcher": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.6.tgz", + "integrity": "sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "detect-libc": "^2.0.3", + "is-glob": "^4.0.3", + "node-addon-api": "^7.0.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "@parcel/watcher-android-arm64": "2.5.6", + "@parcel/watcher-darwin-arm64": "2.5.6", + "@parcel/watcher-darwin-x64": "2.5.6", + "@parcel/watcher-freebsd-x64": "2.5.6", + "@parcel/watcher-linux-arm-glibc": "2.5.6", + "@parcel/watcher-linux-arm-musl": "2.5.6", + "@parcel/watcher-linux-arm64-glibc": "2.5.6", + "@parcel/watcher-linux-arm64-musl": "2.5.6", + "@parcel/watcher-linux-x64-glibc": "2.5.6", + "@parcel/watcher-linux-x64-musl": "2.5.6", + "@parcel/watcher-win32-arm64": "2.5.6", + "@parcel/watcher-win32-ia32": "2.5.6", + "@parcel/watcher-win32-x64": "2.5.6" + } + }, + "node_modules/@parcel/watcher-android-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.6.tgz", + "integrity": "sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.6.tgz", + "integrity": "sha512-Z2ZdrnwyXvvvdtRHLmM4knydIdU9adO3D4n/0cVipF3rRiwP+3/sfzpAwA/qKFL6i1ModaabkU7IbpeMBgiVEA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.6.tgz", + "integrity": "sha512-HgvOf3W9dhithcwOWX9uDZyn1lW9R+7tPZ4sug+NGrGIo4Rk1hAXLEbcH1TQSqxts0NYXXlOWqVpvS1SFS4fRg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-freebsd-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.6.tgz", + "integrity": "sha512-vJVi8yd/qzJxEKHkeemh7w3YAn6RJCtYlE4HPMoVnCpIXEzSrxErBW5SJBgKLbXU3WdIpkjBTeUNtyBVn8TRng==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.6.tgz", + "integrity": "sha512-9JiYfB6h6BgV50CCfasfLf/uvOcJskMSwcdH1PHH9rvS1IrNy8zad6IUVPVUfmXr+u+Km9IxcfMLzgdOudz9EQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.6.tgz", + "integrity": "sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.6.tgz", + "integrity": "sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.6.tgz", + "integrity": "sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.6.tgz", + "integrity": "sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.6.tgz", + "integrity": "sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.6.tgz", + "integrity": "sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-ia32": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.6.tgz", + "integrity": "sha512-k35yLp1ZMwwee3Ez/pxBi5cf4AoBKYXj00CZ80jUz5h8prpiaQsiRPKQMxoLstNuqe2vR4RNPEAEcjEFzhEz/g==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.6.tgz", + "integrity": "sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher/node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/@pkgr/core": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.1.1.tgz", @@ -2581,6 +2907,7 @@ "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", "dev": true, + "peer": true, "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" @@ -2963,6 +3290,7 @@ "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", "dev": true, + "peer": true, "engines": { "node": ">=8" }, @@ -3216,6 +3544,7 @@ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", "dev": true, + "peer": true, "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", @@ -3240,6 +3569,7 @@ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", "dev": true, + "peer": true, "dependencies": { "is-glob": "^4.0.1" }, @@ -3904,6 +4234,17 @@ "integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==", "dev": true }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=8" + } + }, "node_modules/diff": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz", @@ -5937,10 +6278,11 @@ } }, "node_modules/immutable": { - "version": "4.3.6", - "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.6.tgz", - "integrity": "sha512-Ju0+lEMyzMVZarkTn/gqRpdqd5dOPaz1mCZ0SH3JV6iFw81PldE/PEB1hWVEA288HPt4WXW8O7AWxB10M+03QQ==", - "dev": true + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.5.tgz", + "integrity": "sha512-t7xcm2siw+hlUM68I+UEOK+z84RzmN59as9DZ7P1l0994DKUWV7UXBMQZVxaoMSRQ+PBZbHCOoBt7a2wxOMt+A==", + "dev": true, + "license": "MIT" }, "node_modules/import-fresh": { "version": "3.3.0", @@ -6096,6 +6438,7 @@ "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", "dev": true, + "peer": true, "dependencies": { "binary-extensions": "^2.0.0" }, @@ -7876,6 +8219,14 @@ "path-to-regexp": "^1.7.0" } }, + "node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "dev": true, + "license": "MIT", + "optional": true + }, "node_modules/node-fetch": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", @@ -8856,6 +9207,7 @@ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", "dev": true, + "peer": true, "dependencies": { "picomatch": "^2.2.1" }, @@ -9219,13 +9571,14 @@ "dev": true }, "node_modules/sass": { - "version": "1.77.8", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.77.8.tgz", - "integrity": "sha512-4UHg6prsrycW20fqLGPShtEvo/WyHRVRHwOP4DzkUrObWoWI05QBSfzU71TVB7PFaL104TwNaHpjlWXAZbQiNQ==", + "version": "1.99.0", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.99.0.tgz", + "integrity": "sha512-kgW13M54DUB7IsIRM5LvJkNlpH+WhMpooUcaWGFARkF1Tc82v9mIWkCbCYf+MBvpIUBSeSOTilpZjEPr2VYE6Q==", "dev": true, + "license": "MIT", "dependencies": { - "chokidar": ">=3.0.0 <4.0.0", - "immutable": "^4.0.0", + "chokidar": "^4.0.0", + "immutable": "^5.1.5", "source-map-js": ">=0.6.2 <2.0.0" }, "bin": { @@ -9233,6 +9586,39 @@ }, "engines": { "node": ">=14.0.0" + }, + "optionalDependencies": { + "@parcel/watcher": "^2.4.1" + } + }, + "node_modules/sass/node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/sass/node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" } }, "node_modules/scheduler": { @@ -9930,6 +10316,25 @@ "integrity": "sha512-ovssysQTa+luh7A5Weu3Rta6FJlFBBbInjOh722LIt6klpU2/HtdUbszju/G4devcvk8PGt7FCLv5wftu3THUA==", "dev": true }, + "node_modules/swiper": { + "version": "12.1.3", + "resolved": "https://registry.npmjs.org/swiper/-/swiper-12.1.3.tgz", + "integrity": "sha512-XcWlVmkHFICI4fuoJKgbp8PscDcS4i7pBH8nwJRBi3dpQvhCySwsWRYm4bOf/BzKVWkHOYaFw7qz9uBSrY3oug==", + "funding": [ + { + "type": "patreon", + "url": "https://www.patreon.com/swiperjs" + }, + { + "type": "open_collective", + "url": "http://opencollective.com/swiper" + } + ], + "license": "MIT", + "engines": { + "node": ">= 4.7.0" + } + }, "node_modules/synckit": { "version": "0.8.8", "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.8.8.tgz", diff --git a/package.json b/package.json index ae251685c8b..9feb327c26e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,5 @@ { "name": "react_phone-catalog", - "homepage": "react_phone-catalog", "version": "0.1.0", "keywords": [], "author": "Mate Academy", @@ -12,11 +11,12 @@ "react": "^18.3.1", "react-dom": "^18.3.1", "react-router-dom": "^6.25.1", - "react-transition-group": "^4.4.5" + "react-transition-group": "^4.4.5", + "swiper": "^12.1.3" }, "devDependencies": { "@cypress/react18": "^2.0.1", - "@mate-academy/scripts": "^1.8.5", + "@mate-academy/scripts": "^2.1.3", "@mate-academy/students-ts-config": "*", "@mate-academy/stylelint-config": "*", "@types/node": "^20.14.10", @@ -40,7 +40,7 @@ "mochawesome-merge": "^4.3.0", "mochawesome-report-generator": "^6.2.0", "prettier": "^3.3.2", - "sass": "^1.77.8", + "sass": "^1.99.0", "stylelint": "^16.7.0", "typescript": "^5.2.2", "vite": "^5.3.1" @@ -55,7 +55,7 @@ "format": "prettier --write './src/**/*.{ts,tsx}'", "lint": "npm run style-format && npm run format && npm run lint-js && npm run lint-css", "update": "mate-scripts update", - "postinstall": "npm run update && cypress verify", + "postinstall": "echo 'Skip cypress verify on Vercel'", "predeploy": "npm run build", "deploy": "mate-scripts deploy" }, diff --git a/public/img/icons/Close.png b/public/img/icons/Close.png new file mode 100644 index 00000000000..91e4c7b4f2a Binary files /dev/null and b/public/img/icons/Close.png differ diff --git a/public/img/icons/Close.svg b/public/img/icons/Close.svg new file mode 100644 index 00000000000..882d79d3821 --- /dev/null +++ b/public/img/icons/Close.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/img/icons/Home.png b/public/img/icons/Home.png new file mode 100644 index 00000000000..dd551d8c2cc Binary files /dev/null and b/public/img/icons/Home.png differ diff --git a/public/img/icons/Minus.png b/public/img/icons/Minus.png new file mode 100644 index 00000000000..560f93eebda Binary files /dev/null and b/public/img/icons/Minus.png differ diff --git a/public/img/icons/Plus.png b/public/img/icons/Plus.png new file mode 100644 index 00000000000..881e2c4b0c2 Binary files /dev/null and b/public/img/icons/Plus.png differ diff --git a/public/img/icons/Union.png b/public/img/icons/Union.png new file mode 100644 index 00000000000..2a34ae6c4cf Binary files /dev/null and b/public/img/icons/Union.png differ diff --git a/public/img/icons/arrowDown.png b/public/img/icons/arrowDown.png new file mode 100644 index 00000000000..a1f517ec6da Binary files /dev/null and b/public/img/icons/arrowDown.png differ diff --git a/public/img/icons/arrowLeft.png b/public/img/icons/arrowLeft.png new file mode 100644 index 00000000000..67af141d7ff Binary files /dev/null and b/public/img/icons/arrowLeft.png differ diff --git a/public/img/icons/backtotop.svg b/public/img/icons/backtotop.svg new file mode 100644 index 00000000000..b6aae004460 --- /dev/null +++ b/public/img/icons/backtotop.svg @@ -0,0 +1,4 @@ + + + + diff --git a/public/img/icons/bag.svg b/public/img/icons/bag.svg new file mode 100644 index 00000000000..425ee639766 --- /dev/null +++ b/public/img/icons/bag.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/public/img/icons/burger.svg b/public/img/icons/burger.svg new file mode 100644 index 00000000000..c8c52c08a95 --- /dev/null +++ b/public/img/icons/burger.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/public/img/icons/heart.svg b/public/img/icons/heart.svg new file mode 100644 index 00000000000..8fb5abef51d --- /dev/null +++ b/public/img/icons/heart.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/img/icons/logo.svg b/public/img/icons/logo.svg new file mode 100644 index 00000000000..ed1dabdd587 --- /dev/null +++ b/public/img/icons/logo.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/img/icons/logoFooter.svg b/public/img/icons/logoFooter.svg new file mode 100644 index 00000000000..ffad5f2fe9c --- /dev/null +++ b/public/img/icons/logoFooter.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/img/icons/phones-page-back.png b/public/img/icons/phones-page-back.png new file mode 100644 index 00000000000..770f5b3f382 Binary files /dev/null and b/public/img/icons/phones-page-back.png differ diff --git a/public/img/icons/rightArrow.png b/public/img/icons/rightArrow.png new file mode 100644 index 00000000000..d1a7a8e9ac6 Binary files /dev/null and b/public/img/icons/rightArrow.png differ diff --git a/public/img/icons/rightWhiteArrow.png b/public/img/icons/rightWhiteArrow.png new file mode 100644 index 00000000000..2ca7a6b5865 Binary files /dev/null and b/public/img/icons/rightWhiteArrow.png differ diff --git a/public/img/icons/whitetopbut.svg b/public/img/icons/whitetopbut.svg new file mode 100644 index 00000000000..424b9e385e0 --- /dev/null +++ b/public/img/icons/whitetopbut.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/img/shopbycategory/Phones.png b/public/img/shopbycategory/Phones.png new file mode 100644 index 00000000000..357f291bbfb Binary files /dev/null and b/public/img/shopbycategory/Phones.png differ diff --git a/public/img/shopbycategory/accessories.png b/public/img/shopbycategory/accessories.png new file mode 100644 index 00000000000..6b2fb2906f5 Binary files /dev/null and b/public/img/shopbycategory/accessories.png differ diff --git a/public/img/shopbycategory/tablets.png b/public/img/shopbycategory/tablets.png new file mode 100644 index 00000000000..0d18468d934 Binary files /dev/null and b/public/img/shopbycategory/tablets.png differ diff --git a/public/img/slider/16.svg b/public/img/slider/16.svg new file mode 100644 index 00000000000..9d893c123d4 --- /dev/null +++ b/public/img/slider/16.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/public/img/slider/17s.png b/public/img/slider/17s.png new file mode 100644 index 00000000000..c9a7ef8728b Binary files /dev/null and b/public/img/slider/17s.png differ diff --git a/public/img/slider/banner-desctop.png b/public/img/slider/banner-desctop.png new file mode 100644 index 00000000000..f8c7dda1e6b Binary files /dev/null and b/public/img/slider/banner-desctop.png differ diff --git a/public/img/slider/banner-tablet.png b/public/img/slider/banner-tablet.png new file mode 100644 index 00000000000..c9ed69bb0ab Binary files /dev/null and b/public/img/slider/banner-tablet.png differ diff --git a/public/img/slider/iphone-tablet.png b/public/img/slider/iphone-tablet.png new file mode 100644 index 00000000000..61fbdbe4393 Binary files /dev/null and b/public/img/slider/iphone-tablet.png differ diff --git a/public/img/slider/iphones17-tablet.png b/public/img/slider/iphones17-tablet.png new file mode 100644 index 00000000000..2381d6ace8e Binary files /dev/null and b/public/img/slider/iphones17-tablet.png differ diff --git a/public/img/slider/lastchance.png b/public/img/slider/lastchance.png new file mode 100644 index 00000000000..c48a7d57eae Binary files /dev/null and b/public/img/slider/lastchance.png differ diff --git a/public/img/slider/v3.png b/public/img/slider/v3.png new file mode 100644 index 00000000000..134f812939c Binary files /dev/null and b/public/img/slider/v3.png differ diff --git a/public/img/slider/vertical-second-slide.png b/public/img/slider/vertical-second-slide.png new file mode 100644 index 00000000000..fcbaab63034 Binary files /dev/null and b/public/img/slider/vertical-second-slide.png differ diff --git a/src/App.scss b/src/App.scss index 71bc413aade..2c8e229a25a 100644 --- a/src/App.scss +++ b/src/App.scss @@ -1 +1,30 @@ -// not empty +@use './styles/utils/variables' as *; +@use './styles/fonts'; + +html, +body { + margin: 0; + height: 100%; + background-color: $color-black; + color: $color-white; + font-family: Mont, sans-serif; +} + +a { + text-decoration: none; + color: $color-white; +} + +.app { + display: flex; + flex-direction: column; + min-height: 100vh; +} + +main { + flex-grow: 1; +} + +footer { + flex-shrink: 0; +} diff --git a/src/App.tsx b/src/App.tsx index 372e4b42066..be4ba4edab3 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,7 +1,44 @@ import './App.scss'; +import { Route, Routes } from 'react-router-dom'; +import { Header } from './components/Header/Header'; +import { Home } from './pages/Home/Home'; +import { Footer } from './components/Footer/Footer'; +import { CategoryPage } from './pages/CategoryPage/CategoryPage'; +/* eslint-disable max-len */ +import { ProductDetailsPage } from './pages/ProductDetailsPage/ProductDetailsPage'; +/* eslint-enable max-len */ +import { Favorites } from './pages/Favorites/Favorites'; -export const App = () => ( -
-

Product Catalog

-
-); +import { Cart } from './pages/Cart/Cart'; + +export const App = () => { + return ( +
+
+ +
+ + } /> + } + /> + } /> + } + /> + + } + /> + }> + }> + +
+
+ ); +}; diff --git a/src/components/BrandNewModels/BrandNewModels.module.scss b/src/components/BrandNewModels/BrandNewModels.module.scss new file mode 100644 index 00000000000..1e372129c42 --- /dev/null +++ b/src/components/BrandNewModels/BrandNewModels.module.scss @@ -0,0 +1,108 @@ +@use '../../styles/utils/variables' as *; + +.title { + padding-top: 24px; + padding-left: 16px; + padding-right: 16px; +} + +.top { + display: flex; + justify-content: space-between; + align-items: center; + margin-top: 56px; + width: 100%; + box-sizing: border-box; +} + +.h2_title { + font-weight: 800; + font-size: 22px; + line-height: 140%; + letter-spacing: 0; + color: #f1f2f9; + + @media (min-width: $tablet) { + font-weight: 800; + font-size: 32px; + line-height: 41px; + letter-spacing: -1%; + } +} + +.section { + display: flex; + flex-direction: column; + +} + +.gridWrapper { + overflow: hidden; + + // width: 100%; + box-sizing: border-box; + + // border: 2px solid red; +} + +.gridContainer { + display: flex; + grid-template-columns: repeat(2, 1fr); + gap: 16px; + margin-bottom: 56px; +} + +.arrows { + gap: 16px; + display: flex; +} + +.arrow { + width: 32px; + height: 32px; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + transition: all 0.3s ease; + + img { + width: 16px; + height: 16px; + opacity: 0.5; + transition: all 0.3s ease; + } + + &:disabled { + background-color: transparent; + border: 1px solid #4e5261; + cursor: default; + } + + &:not(:disabled) { + background-color: #323542; + border: 1px solid #323542; + + img { + opacity: 1; + filter: brightness(0) invert(1); + } + + &:hover { + background-color: #4e5261; + } + } +} + +.slideCard { + flex-shrink: 0; + width: 212px; + + @media (min-width: $tablet) { + width: 237px; + } + + @media (min-width: $desktop) { + width: 272px; + } +} diff --git a/src/components/BrandNewModels/BrandNewModels.tsx b/src/components/BrandNewModels/BrandNewModels.tsx new file mode 100644 index 00000000000..2c041b72dc7 --- /dev/null +++ b/src/components/BrandNewModels/BrandNewModels.tsx @@ -0,0 +1,62 @@ +import styles from './BrandNewModels.module.scss'; +import products from '../../../public/api/products.json'; +import { useState } from 'react'; +import { ProductCard } from '../ProductCard/ProductCard'; + +export const BrandNewModels = () => { + const newModels = [...products].sort((a, b) => b.year - a.year); + const [offset, setOffset] = useState(0); + const visibleCards = + window.innerWidth >= 1200 ? 4 : window.innerWidth >= 640 ? 3 : 2; + const cardWidth = + window.innerWidth >= 1200 ? 272 : window.innerWidth >= 640 ? 237 : 212; + const step = cardWidth + 16; + const maxOffset = (newModels.length - visibleCards) * step; + + const handleNext = () => { + setOffset(prev => Math.min(prev + step, maxOffset)); + }; + + const handlePrev = () => { + setOffset(prev => Math.max(prev - step, 0)); + }; + + return ( +
+
+

Brand new models

+
+ + +
+
+
+
+ {newModels.map(product => ( +
+ +
+ ))} +
+
+
+ ); +}; diff --git a/src/components/Footer/Footer.module.scss b/src/components/Footer/Footer.module.scss new file mode 100644 index 00000000000..685acb6bd43 --- /dev/null +++ b/src/components/Footer/Footer.module.scss @@ -0,0 +1,77 @@ +@use '../../styles/utils/variables' as *; + +.container { + display: flex; + flex-direction: column; + padding: 0 16px; + gap: 32px; + + @media (min-width: $tablet) { + flex-direction: row; + justify-content: space-between; + align-items: center; + gap: 0; + padding: 0 32px; + } +} + +.footer { + display: flex; + flex-direction: column; + border-top: 1px solid #32353d; + padding: 32px 0; + width: 100%; +} + +.logoFooter { + align-items: flex-start; + .img { + display: block; + } +} + +.navi { + // display: grid; + font-weight: 800; + text-transform: uppercase; + gap: 16px; + padding: 0; + font-size: 12px; + line-height: 11px; + letter-spacing: 4%; + display: flex; + flex-direction: column; + margin: 0; + + @media (min-width: $tablet) { + flex-direction: row; + gap: 13.5px; + } + + @media (min-width: $desktop) { + gap: 106.83px; + } +} + +.but_foot { + cursor: pointer; + background-color: #4a4d58; + border: none; + width: 32px; + height: 32px; +} + +.buttonTop { + display: flex; + align-items: center; + justify-content: center; + gap: 16px; +} + +.text { + font-weight: 700; + font-size: 12px; + line-height: 11px; + letter-spacing: 4%; + color: #75767f; +} diff --git a/src/components/Footer/Footer.tsx b/src/components/Footer/Footer.tsx new file mode 100644 index 00000000000..1568131279b --- /dev/null +++ b/src/components/Footer/Footer.tsx @@ -0,0 +1,40 @@ +import { Link } from 'react-router-dom'; +import styles from './Footer.module.scss'; + +export const Footer = () => { + const scrollTo = () => { + window.scrollTo({ top: 0, behavior: 'smooth' }); + }; + + return ( + + ); +}; diff --git a/src/components/Header/Header.module.scss b/src/components/Header/Header.module.scss new file mode 100644 index 00000000000..bdb6a4be6a6 --- /dev/null +++ b/src/components/Header/Header.module.scss @@ -0,0 +1,240 @@ +@use '../../styles/utils/variables' as *; + +.list { + display: flex; + list-style: none; + margin: 0; + padding: 0; + gap: 32px; + font-weight: 800; + font-size: 12px; + line-height: 11px; + text-decoration: none; + letter-spacing: 4%; + height: 100%; +} + +.header { + text-transform: uppercase; + display: flex; + align-items: center; + height: 48px; + padding: 0; + border-bottom: 1px solid #32353d; + gap: 48px; + + // justify-content: space-between; + + @media (min-width: 640px) { + .navi { + display: flex; + } + .burgerButton { + display: none; + } + .backline { + display: flex; + } + } +} + +.nav { + margin-left: 48px; +} + +.backline { + margin-left: auto; + height: 100%; + display: none; +} + +.iconBox { + display: flex; + align-items: center; + justify-content: center; + width: 64px; + height: 100%; + border-left: 1px solid #32353d; + text-decoration: none; +} + +.mainlogo { + display: flex; + align-items: center; + justify-content: center; + height: 100%; + flex-shrink: 0; +} + +.link { + position: relative; + display: flex; + align-items: center; + height: 100%; + color: #75767f; + text-decoration: none; + font-weight: 800; + transition: color 0.3s; + + &::after { + content: ''; + position: absolute; + bottom: 0; + left: 0; + width: 100%; + height: 3px; + background-color: #fff; + transform: scaleX(0); + transition: transform 0.2s ease; + } + + &:hover { + color: #fff; + } +} + +.activeLink { + position: relative; + display: flex; + align-items: center; + height: 100%; + color: #fff; + text-decoration: none; + font-weight: 800; + + &::after { + content: ''; + position: absolute; + bottom: 0; + left: 0; + width: 100%; + height: 3px; + background-color: #fff; + transform: scaleX(1); + } +} + +.navi { + height: 100%; + display: none; +} + +.burgerButton { + display: flex; + align-items: center; + justify-content: center; + background-color: transparent; + border: none; + padding: 0 16px; + + height: 100%; + cursor: pointer; + + border-left: 1px solid #3b3e4a; +} + +.rightSection { + display: flex; + height: 100%; + align-items: center; + margin-left: auto; +} + +.mobileMenu { + position: fixed; + inset: 48px 0 0; + background-color: $color-black; + z-index: 1000; + + transform: translateX(100%); + transition: transform 0.3s ease; + + display: flex; + flex-direction: column; + justify-content: space-between; + + @media (min-width: 768px) { + display: none; + } +} + +.mobileIcons { + display: flex; + border-top: 1px solid #32353d; + height: 64px; + + a { + flex: 1; + border-right: 1px solid #32353d; + display: flex; + align-items: center; + justify-content: center; + position: relative; + } + &.active::after { + content: ''; + position: absolute; + bottom: 0; + left: 0; + width: 100%; + height: 3px; + background-color: #fff; + } +} + +.mobileList { + li { + display: flex; + flex-direction: column; + align-items: center; + font-size: 12px; + font-weight: 800; + line-height: 11px; + letter-spacing: 4%; + text-transform: uppercase; + list-style: none; + } + li a { + display: flex; + align-items: center; + height: 56px; + width: fit-content; + } +} + +.mobileIconActive { + &::after { + content: ''; + position: absolute; + bottom: 0; + left: 0; + width: 100%; + height: 3px; + background-color: #fff; + } +} + +.mobileMenuOpen { + border-top: 1px solid #32353d; + transform: translateX(0); +} + +.badge { + position: relative; + top: -6px; + right: 6px; + + background-color: #eb5757; + color: white; + font-size: 9px; + font-weight: bold; + + width: 14px; + height: 14px; + border-radius: 50%; + border: 2px solid #0f1121; + display: flex; + align-items: center; + justify-content: center; + padding-bottom: -2px; +} diff --git a/src/components/Header/Header.tsx b/src/components/Header/Header.tsx new file mode 100644 index 00000000000..b310036cf2a --- /dev/null +++ b/src/components/Header/Header.tsx @@ -0,0 +1,128 @@ +import { Link, NavLink } from 'react-router-dom'; +import styles from './Header.module.scss'; +import { useContext, useState } from 'react'; +import { FavoritesContext } from '../../context/FavoritesContext'; +import { CartContext, CartItem } from '../../context/CartContext'; + +export const Header = () => { + const { favorites } = useContext(FavoritesContext); + const { cart } = useContext(CartContext); + const [isOpen, setIsOpen] = useState(false); + + const totalCartItems = cart.reduce( + (sum: number, item: CartItem) => sum + item.quantity, + 0, + ); + + const toggleMenu = () => { + setIsOpen(prev => { + document.body.style.overflow = !prev ? 'hidden' : ''; + + return !prev; + }); + }; + + const closeMenu = () => { + setIsOpen(false); + document.body.style.overflow = ''; + }; + + const getNavClassLink = ({ isActive }: { isActive: boolean }) => + isActive ? styles.activeLink : styles.link; + + const links = ( + <> +
  • + + home + +
  • +
  • + + phones + +
  • +
  • + + tablets + +
  • +
  • + + accessories + +
  • + + ); + + return ( +
    + + Logo + + +
    +
    + + heart + {favorites.length > 0 && ( + {favorites.length} + )} + + + bag + {totalCartItems > 0 && ( + {totalCartItems} + )} + +
    + +
    + +
    + +
    + + isActive ? styles.mobileIconActive : '' + } + onClick={closeMenu} + > + heart + {favorites.length > 0 && ( + {favorites.length} + )} + + + isActive ? styles.mobileIconActive : '' + } + > + bag + {totalCartItems > 0 && ( + {totalCartItems} + )} + +
    +
    +
    + ); +}; diff --git a/src/components/HotPrices/HotPrices.module.scss b/src/components/HotPrices/HotPrices.module.scss new file mode 100644 index 00000000000..6d81c766baa --- /dev/null +++ b/src/components/HotPrices/HotPrices.module.scss @@ -0,0 +1,121 @@ +@use '../../styles/utils/variables' as *; + +.h3_title { + font-weight: 800; + font-size: 22px; + line-height: 140%; + letter-spacing: 0%; + + @media (min-width: $tablet) { + font-weight: 800; + font-size: 32px; + line-height: 41px; + letter-spacing: -1%; + } +} + +.title { + padding-top: 24px; + padding-left: 16px; + padding-right: 16px; +} + +.top { + display: flex; + justify-content: space-between; + align-items: center; + margin-top: 56px; + width: 100%; + box-sizing: border-box; +} + +// .gridContainer { +// display: flex; +// grid-template-columns: repeat(2, 1fr); +// gap: 16px; +// margin-bottom: 64px; +// } +.gridContainer { + display: flex; + gap: 16px; + + // каждый прямой дочерний элемент (карточка) + & > * { + flex-shrink: 0; + width: 212px; + + @media (min-width: $tablet) { + width: 237px; + } + + @media (min-width: $desktop) { + width: 272px; + } + } +} + +.gridWrapper { + overflow: hidden; + width: 100%; +} + +// .gridContainer { +// display: flex; +// gap: 16px; +// } +.arrows { + gap: 16px; + display: flex; +} + +.arrow { + width: 32px; + height: 32px; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + transition: all 0.3s ease; + + img { + width: 16px; + height: 16px; + opacity: 0.5; + transition: all 0.3s ease; + } + + &:disabled { + background-color: transparent; + border: 1px solid #4e5261; + cursor: default; + } + + &:not(:disabled) { + background-color: #323542; + border: 1px solid #323542; + + img { + opacity: 1; + filter: brightness(0) invert(1); + } + + &:hover { + background-color: #4e5261; + } + } +} + +.slideCard { + flex-shrink: 0; + width: 212px; + margin-bottom: 64px; + + @media (min-width: $tablet) { + width: 237px; + } + + @media (min-width: $desktop) { + width: 272px; + margin-bottom: 80px; + } +} diff --git a/src/components/HotPrices/HotPrices.tsx b/src/components/HotPrices/HotPrices.tsx new file mode 100644 index 00000000000..879db7b46d8 --- /dev/null +++ b/src/components/HotPrices/HotPrices.tsx @@ -0,0 +1,72 @@ +import { useState } from 'react'; +import styles from './HotPrices.module.scss'; +import productsData from '../../../public/api/products.json'; +import { ProductCard } from '../ProductCard/ProductCard'; +// import { HotProduct } from './types/HotProduct'; +import { Product } from '../types/Product'; + +export const HotPrices = () => { + const products = productsData as Product[]; + + const hotPrices: Product[] = [...products] + .filter(p => p.fullPrice && p.price) + .sort((a, b) => { + const discountA = (a.priceRegular || 0) - (a.priceDiscount || 0); + const discountB = (b.priceRegular || 0) - (b.priceDiscount || 0); + + return discountB - discountA; + }) + .slice(0, 15); + + const [offset, setOffset] = useState(0); + const step = 212 + 16; + const maxOffset = (hotPrices.length - 4) * step; + // const maxOffset = Math.max(0, (hotPrices.length - 4) * step); + + const handleNext = () => { + setOffset(prev => Math.min(prev + step, maxOffset)); + }; + + const handlePrev = () => { + setOffset(prev => Math.max(prev - step, 0)); + }; + + return ( +
    +
    +

    Hot prices

    +
    + + +
    +
    +
    +
    + {hotPrices.map(product => ( +
    + +
    + ))} +
    +
    +
    + ); +}; diff --git a/src/components/HotPrices/types/HotProduct.ts b/src/components/HotPrices/types/HotProduct.ts new file mode 100644 index 00000000000..f86ba9cfafe --- /dev/null +++ b/src/components/HotPrices/types/HotProduct.ts @@ -0,0 +1,22 @@ +export interface HotProduct { + id: string; + category: string; + namespaceId: string; + name: string; + capacityAvailable: string[]; + capacity: string; + priceRegular: number; + priceDiscount: number; + colorsAvailable: string[]; + color: string; + images: string[]; + // description: any[]; + description: string; + screen: string; + resolution: string; + processor: string; + ram: string; + camera?: string; + zoom?: string; + cell: string[]; +} diff --git a/src/components/Loader/Loader.module.scss b/src/components/Loader/Loader.module.scss new file mode 100644 index 00000000000..e8f1616a7ce --- /dev/null +++ b/src/components/Loader/Loader.module.scss @@ -0,0 +1,26 @@ +.loaderContainer { + display: flex; + justify-content: center; + align-items: center; + padding: 80px 0; + width: 100%; +} + +.spinner { + width: 48px; + height: 48px; + border: 4px solid rgba(137, 65, 255, 0.2); + border-top: 4px solid #8941ff; + border-radius: 50%; + + animation: spin 1s linear infinite; +} + +@keyframes spin { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } +} diff --git a/src/components/Loader/Loader.tsx b/src/components/Loader/Loader.tsx new file mode 100644 index 00000000000..2a63fc77b23 --- /dev/null +++ b/src/components/Loader/Loader.tsx @@ -0,0 +1,9 @@ +import styles from './Loader.module.scss'; + +export const Loader = () => { + return ( +
    +
    +
    + ); +}; diff --git a/src/components/Price/Price.module.scss b/src/components/Price/Price.module.scss new file mode 100644 index 00000000000..8af1755a098 --- /dev/null +++ b/src/components/Price/Price.module.scss @@ -0,0 +1,28 @@ +@use '../../styles/utils/variables' as *; + +.container { + display: flex; + align-items: center; + gap: 8px; + + .discount { + font-weight: 800; + font-size: 22px; + color: #f1f2f9; + } + .regular { + font-weight: 500; + font-size: 14px; + text-decoration: line-through; + color: #89939a; + } + + &.large { + .discount { + font-size: 32px; + } + .regular { + font-size: 22px; + } + } +} diff --git a/src/components/Price/Price.tsx b/src/components/Price/Price.tsx new file mode 100644 index 00000000000..6826fafc0d4 --- /dev/null +++ b/src/components/Price/Price.tsx @@ -0,0 +1,16 @@ +interface Props { + discount: number; + regular: number; + showDiscount?: boolean; +} + +export const Price: React.FC = ({ discount, regular, showDiscount }) => { + const hasDiscount = showDiscount && discount < regular; + + return ( + <> + ${discount} + {hasDiscount && ${regular}} + + ); +}; diff --git a/src/components/ProductCard/ProductCard.module.scss b/src/components/ProductCard/ProductCard.module.scss new file mode 100644 index 00000000000..3aa7255ae92 --- /dev/null +++ b/src/components/ProductCard/ProductCard.module.scss @@ -0,0 +1,147 @@ +@use '../../styles/utils/variables' as *; + +.card { + display: flex; + flex-direction: column; + background-color: #161827; + padding: 32px; + border: #3b3e49; + + // width: 100%; + box-sizing: border-box; + + // width: 212px; + // height: 440px; + + &:hover { + transform: scale(1.05); + transition: transform 0.6s ease; + } + + @media (min-width: $tablet) { + height: 512px; + } + + @media (min-width: $desktop) { + height: 506px; + } +} + +.image { + display: flex; + justify-content: center; + align-items: center; +} + +.itemImage { + width: 148px; + height: 129px; + object-fit: contain; +} + +.title { + font-weight: 600; + font-size: 14px; + line-height: 21px; + letter-spacing: 0%; + margin-bottom: 8px; + min-height: 42px; +} + +.prices { + display: flex; + gap: 8px; + align-items: center; + /* stylelint-disable-next-line selector-pseudo-class-no-unknown */ + :global(.price-current) { + font-size: 22px; + font-weight: 800; + color: #f1f2f9; + } + /* stylelint-disable-next-line selector-pseudo-class-no-unknown */ + :global(.price-old) { + font-size: 22px; + color: #89939a; + text-decoration: line-through; + } +} + +.strip { + border: 1px solid #3b3e4a; +} + +.specRow { + font-weight: 700; + font-size: 12px; + line-height: 100%; + letter-spacing: 0%; + display: flex; + align-items: center; + justify-content: center; + justify-content: space-between; + margin-bottom: 8px; + + span { + color: #75767f; + } + p { + font-weight: 700; + font-size: 12px; + line-height: 100%; + letter-spacing: 0%; + color: #f1f2f9; + } +} + +.buttons { + margin: auto; + display: flex; + gap: 8px; + width: 100%; +} + +.addButton { + cursor: pointer; + font-weight: 700; + font-size: 14px; + line-height: 21px; + letter-spacing: 0%; + width: 148px; + height: 40px; + color: #f1f2f9; + background-color: #905bff; + border: none; + flex: 1; + &:hover { + background-color: #a378ff; + } + + &Active { + background-color: #323542; + } +} + +.addedButton { + background-color: #323542; +} + +.favotiteButton { + flex-shrink: 0; + border: none; + cursor: pointer; + width: 40px; + height: 40px; + background-color: #323542; + &:hover { + background-color: #4a4d58; + } +} + +.oldPrice { + font-weight: 600; + font-size: 22px; + line-height: 100%; + letter-spacing: 0%; + text-decoration: line-through; + color: #89939a; +} diff --git a/src/components/ProductCard/ProductCard.tsx b/src/components/ProductCard/ProductCard.tsx new file mode 100644 index 00000000000..a8fc9372044 --- /dev/null +++ b/src/components/ProductCard/ProductCard.tsx @@ -0,0 +1,110 @@ +import styles from './ProductCard.module.scss'; +import { Product } from '../types/Product'; +import { Link } from 'react-router-dom'; +import { Price } from '../Price/Price'; +import { useContext } from 'react'; +import { FavoritesContext } from '../../context/FavoritesContext'; +import { CartContext, CartItem } from '../../context/CartContext'; + +interface Props { + product: Product; + showDiscount?: boolean; +} + +export const ProductCard: React.FC = ({ + product, + showDiscount = false, +}) => { + const { + price, + fullPrice, + name, + priceRegular, + priceDiscount, + screen, + capacity, + ram, + // image, + // images, + itemId, + } = product; + const { favorites, toggleFavorite } = useContext(FavoritesContext); + const { cart, addToCart, removeFromCart } = useContext(CartContext); + const isFavorite = favorites.some((item: Product) => item.itemId === itemId); + const currentPrice = priceDiscount || price; + const oldPrice = priceRegular || fullPrice; + // const finalImage = images ? images[0] : image; + + const productUrl = `/product/${product.itemId}`; + const rawImage = product.images?.length ? product.images[0] : product.image; // ? + const finalImage = rawImage?.startsWith('http') ? rawImage : `/${rawImage}`; + const isInCart = cart.some((item: CartItem) => item.id === itemId); + + const handleCartClick = () => { + const targetId = String(product.itemId || product.id); + + if (isInCart) { + removeFromCart(targetId); + } else { + addToCart(product); + } + }; + + return ( +
    +
    + + {itemId} + +
    + + +

    {name}

    + + +
    + +
    +
    + +
    +
    + Screen +

    {screen}

    +
    +
    + Capacity +

    {capacity}

    +
    +
    + RAM +

    {ram}

    +
    +
    + +
    + + + +
    +
    + ); +}; diff --git a/src/components/ProductCard/types/Product.ts b/src/components/ProductCard/types/Product.ts new file mode 100644 index 00000000000..a7131be0448 --- /dev/null +++ b/src/components/ProductCard/types/Product.ts @@ -0,0 +1,16 @@ +export interface Product { + id: number; + category: string; + itemId: string; + name: string; + fullPrice: number; + price: number; + priceRegular?: number; + priceDiscount?: number; + screen: string; + capacity: string; + color: string; + ram: string; + year: number; + image: string; +} diff --git a/src/components/ProductDetailsPageComponents/AboutBlock/AboutBlock.module.scss b/src/components/ProductDetailsPageComponents/AboutBlock/AboutBlock.module.scss new file mode 100644 index 00000000000..e24211d9ce9 --- /dev/null +++ b/src/components/ProductDetailsPageComponents/AboutBlock/AboutBlock.module.scss @@ -0,0 +1,26 @@ +@use '../../../styles/utils/variables' as *; + +.aboutTitle { + color: #f1f2f9; + font-weight: 700; + font-size: 20px; + line-height: 100%; + border-bottom: 1px solid #3b3e4a; + padding-bottom: 16px; +} + +.subTitle { + font-weight: 700; + font-size: 16px; + line-height: 100%; + color: #f1f2f9; + margin-top: 32px; +} + +.descriptionText { + font-weight: 600; + font-size: 14px; + line-height: 21px; + color: #89939a; + margin-bottom: 32px; +} diff --git a/src/components/ProductDetailsPageComponents/AboutBlock/AboutBlock.tsx b/src/components/ProductDetailsPageComponents/AboutBlock/AboutBlock.tsx new file mode 100644 index 00000000000..9b0c45bd97f --- /dev/null +++ b/src/components/ProductDetailsPageComponents/AboutBlock/AboutBlock.tsx @@ -0,0 +1,31 @@ +import styles from './AboutBlock.module.scss'; + +interface DescriptionItem { + title: string; + text: string[]; +} + +interface Props { + description: DescriptionItem[]; +} + +export const AboutBlock: React.FC = ({ description }) => { + return ( + <> +

    About

    +
    + {description.map((item, index) => ( +
    +

    {item.title}

    + + {item.text.map((paragraph, pIndex) => ( +

    + {paragraph} +

    + ))} +
    + ))} +
    + + ); +}; diff --git a/src/components/ProductDetailsPageComponents/CapacityBlock/CapacityBlock.module.scss b/src/components/ProductDetailsPageComponents/CapacityBlock/CapacityBlock.module.scss new file mode 100644 index 00000000000..3a6b781d139 --- /dev/null +++ b/src/components/ProductDetailsPageComponents/CapacityBlock/CapacityBlock.module.scss @@ -0,0 +1,42 @@ +.capacityBlock { + border-bottom: 1px solid #3b3e4a; + padding-bottom: 24px; + margin-bottom: 24px; +} + +.labelCapacity { + font-weight: 700; + font-size: 12px; + line-height: 100%; + color: #75767f; +} + +.capacityList { + display: flex; + gap: 8px; + margin-top: 8px; +} + +.capacityButton { + display: flex; + justify-content: center; + align-items: center; + height: 32px; + padding: 0 12px; + border: 1px solid #3b3e4a; + color: #f1f2f9; + font-size: 14px; + text-decoration: none; + transition: all 0.2s ease-in-out; + cursor: pointer; + + &:hover { + border-color: #f1f2f9; + } + + &Active { + background-color: #f1f2f9; + color: #0f1121; + border-color: #f1f2f9; + } +} diff --git a/src/components/ProductDetailsPageComponents/CapacityBlock/CapacityBlock.tsx b/src/components/ProductDetailsPageComponents/CapacityBlock/CapacityBlock.tsx new file mode 100644 index 00000000000..3a451d0d4c2 --- /dev/null +++ b/src/components/ProductDetailsPageComponents/CapacityBlock/CapacityBlock.tsx @@ -0,0 +1,44 @@ +import { Link } from 'react-router-dom'; +import styles from './CapacityBlock.module.scss'; + +interface Props { + capacityAvailable: string[]; + currentCapacity: string; + productId: string; +} + +export const CapacityBlock: React.FC = ({ + capacityAvailable, + currentCapacity, + productId, +}) => { + return ( +
    + Select capacity +
    + {capacityAvailable.map((capacity: string) => { + const currentCapFormatted = currentCapacity + .toLowerCase() + .replace(' ', ''); + const newCapFormatted = capacity.toLowerCase().replace(' ', ''); + const newProductId = productId.replace( + currentCapFormatted, + newCapFormatted, + ); + + return ( + +

    {capacity}

    + + ); + })} +
    +
    + ); +}; diff --git a/src/components/ProductDetailsPageComponents/ColorSelector/ColorSelector.module.scss b/src/components/ProductDetailsPageComponents/ColorSelector/ColorSelector.module.scss new file mode 100644 index 00000000000..9b4c3e00211 --- /dev/null +++ b/src/components/ProductDetailsPageComponents/ColorSelector/ColorSelector.module.scss @@ -0,0 +1,59 @@ +@use '../../../styles/utils/variables' as *; + +.colorsBlock { + border-bottom: 1px solid #3b3e4a; + padding-bottom: 24px; + margin-bottom: 24px; +} + +.colorsHeader { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 8px; + width: 100%; + margin-top: 40px; + + @media (min-width: $desktop) { + // gap: 150px ; + } +} + +.colorsList { + display: flex; + gap: 8px; +} + +.colorCircle { + width: 32px; + height: 32px; + border-radius: 50%; + border: 1px solid #0f1121; + cursor: pointer; + transition: + transform 0.2s, + border-color 0.2s; + + &:hover { + transform: scale(1.1); + } + + &Active { + border-color: #f1f2f9; + outline: 2px solid #0f1121; + outline-offset: -4px; + } +} + +.label { + font-weight: 700; + font-size: 12px; + line-height: 100%; + color: #75767f; +} + +.productId { + font-weight: 700; + font-size: 12px; + color: #4a4d58; +} diff --git a/src/components/ProductDetailsPageComponents/ColorSelector/ColorSelector.tsx b/src/components/ProductDetailsPageComponents/ColorSelector/ColorSelector.tsx new file mode 100644 index 00000000000..37862402e21 --- /dev/null +++ b/src/components/ProductDetailsPageComponents/ColorSelector/ColorSelector.tsx @@ -0,0 +1,61 @@ +import { Link } from 'react-router-dom'; +import styles from './ColorSelector.module.scss'; + +interface Props { + colorsAvailable: string[]; + currentColor: string; + productId: string; +} + +const colorMap: Record = { + black: '#1f2020', + green: '#aee1cd', + yellow: '#ffe681', + white: '#f9f6ef', + purple: '#d1cdda', + red: '#ba0c2e', + spacegray: '#535150', + midnightgreen: '#4e5851', + gold: '#f9e5c9', + silver: '#ebebe3', + rosegold: '#b76e79', + midnight: '#171e27', + blue: '#215e7c', + pink: '#fae0d8', +}; + +export const ColorSelector: React.FC = ({ + colorsAvailable, + currentColor, + productId, +}) => { + return ( +
    +
    + Available colors + {/* ID: {productId} */} + + ID: {productId.split('-').pop()} + +
    + +
    + {colorsAvailable.map((color: string) => { + const newProductId = productId.replace(currentColor, color); + + return ( + console.log(`${newProductId}`)} + className={`${styles.colorCircle} ${ + currentColor === color ? styles.colorCircleActive : '' + }`} + style={{ backgroundColor: colorMap[color] || '#cccccc' }} + /> + ); + })} +
    +
    + ); +}; diff --git a/src/components/ProductDetailsPageComponents/RecommendedSection/RecommendedSection.module.scss b/src/components/ProductDetailsPageComponents/RecommendedSection/RecommendedSection.module.scss new file mode 100644 index 00000000000..93f3d71fbf6 --- /dev/null +++ b/src/components/ProductDetailsPageComponents/RecommendedSection/RecommendedSection.module.scss @@ -0,0 +1,71 @@ +@use '../../../styles/utils/variables' as *; + +.recommended { + margin-top: 56.5px; + display: flex; + justify-content: space-between; + align-items: center; + width: 100%; + box-sizing: border-box; +} + +.recTitle { + font-weight: 800; + font-size: 22px; + line-height: 140%; + color: #f1f2f9; +} + +.arrows { + gap: 16px; + display: flex; +} + +.arrow { + width: 32px; + height: 32px; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + transition: all 0.3s ease; + + img { + width: 16px; + height: 16px; + opacity: 0.5; + transition: all 0.3s ease; + } + + &:disabled { + background-color: transparent; + border: 1px solid #4e5261; + cursor: default; + } + + &:not(:disabled) { + background-color: #323542; + border: 1px solid #323542; + + img { + opacity: 1; + filter: brightness(0) invert(1); + } + + &:hover { + background-color: #4e5261; + } + } +} + +.gridContainer { + display: flex; + grid-template-columns: repeat(2, 1fr); + gap: 16px; + margin-bottom: 64px; +} + +.gridWrapper { + overflow: hidden; + width: 100%; +} diff --git a/src/components/ProductDetailsPageComponents/RecommendedSection/RecommendedSection.tsx b/src/components/ProductDetailsPageComponents/RecommendedSection/RecommendedSection.tsx new file mode 100644 index 00000000000..6434040986a --- /dev/null +++ b/src/components/ProductDetailsPageComponents/RecommendedSection/RecommendedSection.tsx @@ -0,0 +1,72 @@ +import { useState } from 'react'; +import styles from './RecommendedSection.module.scss'; +import { Product } from '../../types/Product'; +import productsData from '../../../../public/api/products.json'; +import { ProductCard } from '../../ProductCard/ProductCard'; + +interface Props { + currentProduct: Product; +} + +export const RecommendedSection = ({ currentProduct }: Props) => { + const products = productsData as Product[]; + const [offset, setOffset] = useState(0); + const suggestedProducts = [...products] + .filter(p => p.id !== currentProduct.id) + .filter(p => p.category === currentProduct.category) + .filter(p => p.fullPrice && p.price) + // .sort(() => 0.5 - Math.random()) + .slice(0, 15); + + const step = 212 + 16; + const maxOffset = Math.max((suggestedProducts.length - 4) * step, 0); + + const handleNext = () => { + setOffset(prev => Math.min(prev + step, maxOffset)); + }; + + const handlePrev = () => { + setOffset(prev => Math.max(prev - step, 0)); + }; + + return ( +
    +
    +

    You may also like

    +
    + + +
    +
    +
    +
    + {suggestedProducts.map(product => ( + + ))} +
    +
    +
    + ); +}; diff --git a/src/components/ProductDetailsPageComponents/TechSpecBlock/TechSpecBlock.module.scss b/src/components/ProductDetailsPageComponents/TechSpecBlock/TechSpecBlock.module.scss new file mode 100644 index 00000000000..dfef298c533 --- /dev/null +++ b/src/components/ProductDetailsPageComponents/TechSpecBlock/TechSpecBlock.module.scss @@ -0,0 +1,35 @@ +.techTitle { + color: #f1f2f9; + font-size: 20px; + line-height: 100%; + font-weight: 700; + border-bottom: 1px solid #3b3e4a; + padding-bottom: 16px; +} + +.specs { + margin-top: 30px; + display: flex; + flex-direction: column; + gap: 8px; + + .labelText { + font-weight: 700; + font-size: 12px; + line-height: 100%; + color: #75767f; + text-transform: capitalize; + } + .valueText { + font-weight: 700; + font-size: 12px; + line-height: 100%; + color: #f1f2f9; + } + + .specRow { + display: flex; + justify-content: space-between; + align-items: center; + } +} diff --git a/src/components/ProductDetailsPageComponents/TechSpecBlock/TechSpecBlock.tsx b/src/components/ProductDetailsPageComponents/TechSpecBlock/TechSpecBlock.tsx new file mode 100644 index 00000000000..3b0877d0825 --- /dev/null +++ b/src/components/ProductDetailsPageComponents/TechSpecBlock/TechSpecBlock.tsx @@ -0,0 +1,40 @@ +import { Product } from '../../types/Product'; +import styles from './TechSpecBlock.module.scss'; + +interface Props { + product: Product; +} + +export const TechSpecBlock: React.FC = ({ product }) => { + const techSpecs = [ + { label: 'screen', value: product.screen }, + { label: 'resolution', value: product.resolution }, + { label: 'processor', value: product.processor }, + { + label: 'RAM', + value: product.ram ? product.ram.replace(/([0-9])([A-Z])/, '$1 $2') : '', + }, + { label: 'camera', value: product.camera }, + { label: 'zoom', value: product.zoom }, + { + label: 'cell', + value: Array.isArray(product.cell) + ? product.cell.join(', ') + : product.cell, + }, + ]; + + return ( + <> +

    Tech specs

    +
    + {techSpecs.map(spec => ( +
    + {spec.label} + {spec.value} +
    + ))} +
    + + ); +}; diff --git a/src/components/ProductSlider/ProductSlider.module.scss b/src/components/ProductSlider/ProductSlider.module.scss new file mode 100644 index 00000000000..1837cea4b2e --- /dev/null +++ b/src/components/ProductSlider/ProductSlider.module.scss @@ -0,0 +1,116 @@ +@use '../../styles/utils/variables' as *; + +.slide { + min-width: 100%; + flex-shrink: 0; + height: 100%; + + picture { + display: block; + width: 100%; + height: 100%; + } + + img { + width: 100%; + height: 100%; + object-fit: contain; + + // object-fit: cover; + display: block; + } +} + +.slider { + min-width: 0; + display: flex; + flex: 1; + flex-direction: column; + gap: 8px; +} + +.screen { + width: 100%; + overflow: hidden; + aspect-ratio: 1 / 1; + + @media (min-width: $tablet) { + aspect-ratio: unset; + height: 189px; + } + + @media (min-width: $desktop) { + aspect-ratio: unset; + height: 400px; + } +} + +.photos { + display: flex; + height: 100%; + width: 100%; + transition: transform 0.5s ease-in-out; +} + +.pagination { + display: flex; + justify-content: center; + gap: 8px; + margin: 0; + padding: 0; +} + +.dot { + width: 14px; + height: 4px; + border: none; + background-color: #3b3e4a; + cursor: pointer; + padding: 0; + transition: all 0.3s; +} + +.activeDot { + background-color: #fff; +} + +.sliderWrapper { + display: flex; + flex-direction: column; + + @media (min-width: $tablet) { + flex-direction: row; + align-items: center; + gap: 19px; + padding: 0 24px; + } + + @media (min-width: $desktop) { + padding: 0 152px; + } +} + +.arrowLeft, +.arrowRight { + display: none; + flex-shrink: 0; + width: 32px; + + // height: 32px; + background: #323542; + border: none; + cursor: pointer; + font-size: 32px; + color: #f1f2f9; + align-items: center; + justify-content: center; + align-self: stretch; + + @media (min-width: $tablet) { + display: flex; + } + + &:hover { + opacity: 0.7; + } +} diff --git a/src/components/ProductSlider/ProductSlider.tsx b/src/components/ProductSlider/ProductSlider.tsx new file mode 100644 index 00000000000..f2a7dfd00c0 --- /dev/null +++ b/src/components/ProductSlider/ProductSlider.tsx @@ -0,0 +1,86 @@ +import { useCallback, useEffect, useState } from 'react'; +import styles from './ProductSlider.module.scss'; + +export const ProductSlider = () => { + const [currentIndex, setCurrentIndex] = useState(0); + const IMAGES = [ + { + desktop: '/img/slider/banner-desctop.png', + tablet: '/img/slider/banner-tablet.png', + mobile: '/img/slider/16.svg', + }, + { + desktop: '/img/slider/v3.png', + tablet: '/img/slider/v3.png', + mobile: '/img/slider/17s.png', + }, + { + desktop: '/img/slider/vertical-second-slide.png', + tablet: '/img/slider/vertical-second-slide.png', + mobile: '/img/slider/lastchance.png', + }, + ]; + + const nextSlide = useCallback(() => { + setCurrentIndex(prevIndex => + prevIndex === IMAGES.length - 1 ? 0 : prevIndex + 1, + ); + }, [IMAGES.length]); + + useEffect(() => { + const interval = setInterval(() => { + nextSlide(); + }, 5000); + + return () => clearInterval(interval); + }, [nextSlide]); + + const prevSlide = () => { + setCurrentIndex(prevIndex => + prevIndex === 0 ? IMAGES.length - 1 : prevIndex - 1, + ); + }; + + return ( +
    + + +
    +
    +
    + {IMAGES.map((imgObj, index) => ( +
    + + + + {`slide + +
    + ))} +
    +
    + +
    + {IMAGES.map((_, index) => ( + + ))} +
    +
    + +
    + ); +}; diff --git a/src/components/ProductsList/ProductsList.module.scss b/src/components/ProductsList/ProductsList.module.scss new file mode 100644 index 00000000000..9bf561f00c9 --- /dev/null +++ b/src/components/ProductsList/ProductsList.module.scss @@ -0,0 +1,19 @@ +@use '../../styles/utils/variables' as *; + +.list { + display: grid; + gap: 16px; + width: 100%; + max-width: 1440px; + margin: 0 auto; + grid-template-columns: repeat(1, 1fr); + + @media (min-width: $tablet) { + grid-template-columns: repeat(2, 1fr); + gap: 16px; + } + + @media (min-width: $desktop) { + grid-template-columns: repeat(4, 1fr); + } +} diff --git a/src/components/ProductsList/ProductsList.tsx b/src/components/ProductsList/ProductsList.tsx new file mode 100644 index 00000000000..027c3190535 --- /dev/null +++ b/src/components/ProductsList/ProductsList.tsx @@ -0,0 +1,17 @@ +import { ProductCard } from '../ProductCard/ProductCard'; +import { Product } from '../types/Product'; +import styles from './ProductsList.module.scss'; + +interface Props { + products: Product[]; +} + +export const ProductsList = ({ products }: Props) => { + return ( +
    + {products.map(product => ( + + ))} +
    + ); +}; diff --git a/src/components/ShopByCategory/ShopByCategory.module.scss b/src/components/ShopByCategory/ShopByCategory.module.scss new file mode 100644 index 00000000000..dcb7ceba1e5 --- /dev/null +++ b/src/components/ShopByCategory/ShopByCategory.module.scss @@ -0,0 +1,70 @@ +@use '../../styles/utils/variables' as *; + +.section { + margin-top: 56px; + margin-bottom: 56px; +} + +.h2_title { + margin-bottom: 24px; + color: #f1f2f9; + font-weight: 800; + font-size: 22px; + line-height: 31px; + letter-spacing: 0%; + + @media (min-width: $tablet) { + font-weight: 800; + font-size: 32px; + line-height: 41px; + letter-spacing: -1%; + } +} + +.categoryGrid { + display: grid; + grid-template-columns: 1fr; + gap: 32px; + + @media (min-width: $tablet) { + grid-template-columns: repeat(3, 1fr); + gap: 16px; + } + + @media (min-width: $desktop) { + gap: 16px; + } +} + +.categoryCard { + width: 100%; + display: block; + &:hover { + transform: scale(1.02); + transition: transform 0.6s ease; + } + + p { + margin-top: 24px; + font-weight: 700; + font-size: 20px; + line-height: 100%; + letter-spacing: 0%; + color: #f1f2f9; + padding-left: 0; + } + span { + display: block; + color: #89939a; + font-weight: 600; + font-size: 14px; + line-height: 21px; + letter-spacing: 0%; + padding-left: 0; + } + .categoryImage { + width: 100%; + display: block; + object-fit: contain; + } +} diff --git a/src/components/ShopByCategory/ShopByCategory.tsx b/src/components/ShopByCategory/ShopByCategory.tsx new file mode 100644 index 00000000000..939a09f23f7 --- /dev/null +++ b/src/components/ShopByCategory/ShopByCategory.tsx @@ -0,0 +1,42 @@ +import { Link } from 'react-router-dom'; +import styles from './ShopByCategory.module.scss'; +import phones from '../../../public/api/phones.json'; +import tablets from '../../../public/api/tablets.json'; +import accessories from '../../../public/api/accessories.json'; + +export const ShopByCategory = () => { + return ( +
    +

    Shop by category

    + +
    + + +

    Mobile phones

    + {phones.length} models + + + + +

    Tablets

    + {tablets.length} models + + + + +

    Accessories

    + {accessories.length} models + +
    +
    + ); +}; diff --git a/src/components/types/Phones.ts b/src/components/types/Phones.ts new file mode 100644 index 00000000000..2f285507c8e --- /dev/null +++ b/src/components/types/Phones.ts @@ -0,0 +1,21 @@ +export interface Phones { + id: string; + category: string; + namespaceId: string; + name: string; + capacityAvailable: string[]; + capacity: string; + priceRegular: number; + priceDiscount: number; + colorsAvailable: string[]; + color: string; + images: string[]; + description: { title: string; text: string[] }[]; + screen: string; + resolution: string; + processor: string; + ram: string; + camera: string; + zoom: string; + cell: string[]; +} diff --git a/src/components/types/Product.ts b/src/components/types/Product.ts new file mode 100644 index 00000000000..46a7a6b3ee6 --- /dev/null +++ b/src/components/types/Product.ts @@ -0,0 +1,28 @@ +export interface Product { + id: string | number; + category: string; + name: string; + screen: string; + capacity: string; + color: string; + ram: string; + + itemId?: string; + fullPrice?: number; + price?: number; + year?: number; + image?: string; + + namespaceId?: string; + priceRegular?: number; + priceDiscount?: number; + images?: string[]; + capacityAvailable?: string[]; + colorsAvailable?: string[]; + description?: { title: string; text: string[] }[]; + resolution?: string; + processor?: string; + camera?: string; + zoom?: string; + cell?: string[]; +} diff --git a/src/context/CartContext.tsx b/src/context/CartContext.tsx new file mode 100644 index 00000000000..d95b59eb2b4 --- /dev/null +++ b/src/context/CartContext.tsx @@ -0,0 +1,86 @@ +import { createContext, useState, useEffect } from 'react'; +import { Product } from '../components/types/Product'; + +export interface CartItem { + id: string; + quantity: number; + product: Product; +} + +export interface CartContextType { + cart: CartItem[]; + addToCart: (product: Product) => void; + removeFromCart: (id: string) => void; + updateQuantity: (id: string, amount: number) => void; + clearCart: () => void; +} + +export const CartContext = createContext( + {} as CartContextType, +); + +export const CartProvider = ({ children }: { children: React.ReactNode }) => { + const [cart, setCart] = useState([]); + + useEffect(() => { + const savedCart = localStorage.getItem('cart'); + + if (savedCart) { + setCart(JSON.parse(savedCart)); + } + }, []); + + useEffect(() => { + localStorage.setItem('cart', JSON.stringify(cart)); + }, [cart]); + + const addToCart = (product: Product) => { + setCart(prev => { + const safeId = String(product.itemId || product.id); + const isExist = prev.find(item => item.id === safeId); + + if (isExist) { + return prev; + } + + // return [...prev, { id: product.itemId || '', quantity: 1, product }]; + return [...prev, { id: safeId, quantity: 1, product }]; + }); + }; + + const removeFromCart = (id: string) => { + setCart(prev => prev.filter(item => item.id !== id)); + }; + + const updateQuantity = (id: string, amount: number) => { + setCart(prev => + prev.map(item => { + if (item.id === id) { + const newQuantity = item.quantity + amount; + + return { ...item, quantity: newQuantity > 0 ? newQuantity : 1 }; + } + + return item; + }), + ); + }; + + const clearCart = () => { + setCart([]); + }; + + return ( + + {children} + + ); +}; diff --git a/src/context/FavoritesContext.tsx b/src/context/FavoritesContext.tsx new file mode 100644 index 00000000000..dd67d900eab --- /dev/null +++ b/src/context/FavoritesContext.tsx @@ -0,0 +1,59 @@ +import React, { createContext, useState, useEffect, ReactNode } from 'react'; +import { Product } from '../components/types/Product'; + +interface FavoritesContextType { + favorites: Product[]; + toggleFavorite: (product: Product) => void; +} + +export const FavoritesContext = createContext( + null, +); + +interface Props { + children: ReactNode; +} + +export const FavoritesProvider: React.FC = ({ children }) => { + const [favorites, setFavorites] = useState([]); + + useEffect(() => { + const savedFavorites = localStorage.getItem('favorites'); + + if (savedFavorites) { + try { + setFavorites(JSON.parse(savedFavorites)); + } catch (error) { + // console.error('Failed to parse favorites', error); + } + } + }, []); + + useEffect(() => { + localStorage.setItem('favorites', JSON.stringify(favorites)); + }, [favorites]); + + const toggleFavorite = (product: Product) => { + setFavorites(prevFavorites => { + const targetId = String(product.itemId || product.id); + + const isExist = prevFavorites.some( + (item: Product) => String(item.itemId || item.id) === targetId, + ); + + if (isExist) { + return prevFavorites.filter( + (item: Product) => String(item.itemId || item.id) !== targetId, + ); + } + + return [...prevFavorites, product]; + }); + }; + + return ( + + {children} + + ); +}; diff --git a/src/index.tsx b/src/index.tsx index 50470f1508d..1207745a3e5 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,4 +1,16 @@ import { createRoot } from 'react-dom/client'; import { App } from './App'; +import './App.scss'; +import { BrowserRouter } from 'react-router-dom'; +import { FavoritesProvider } from './context/FavoritesContext'; +import { CartProvider } from './context/CartContext'; -createRoot(document.getElementById('root') as HTMLElement).render(); +createRoot(document.getElementById('root') as HTMLElement).render( + + + + + + + , +); diff --git a/src/pages/Cart/Cart.module.scss b/src/pages/Cart/Cart.module.scss new file mode 100644 index 00000000000..d8f9f3ff35e --- /dev/null +++ b/src/pages/Cart/Cart.module.scss @@ -0,0 +1,259 @@ +@use '../../styles/utils/variables' as *; + +.cartBlock { + margin-top: 24px; + padding: 0 16px; + margin-bottom: 56px; + + @media (min-width: $tablet) { + padding: 0 24px; + margin-bottom: 64px; + margin-top: 40px; + } + + @media (min-width: $desktop) { + padding: 0 32px; + margin-bottom: 80px; + } +} + +.title { + margin-top: 24px; + color: #f1f2f9; + font-size: 32px; + font-weight: 800; + line-height: 41px; + letter-spacing: -1%; + + @media (min-width: $tablet) { + font-size: 48px; + line-height: 56px; + } +} + +.cartContent { + display: flex; + flex-direction: column; + gap: 16px; + margin-top: 32px; + margin-bottom: 32px; + + @media (min-width: 1200px) { + flex-direction: row; + align-items: flex-start; + gap: 16px; + } +} + +.backBtn { + display: flex; + align-items: center; + gap: 4px; + background-color: transparent; + border: none; + color: #f1f2f9; + font-weight: 700; + font-size: 12px; + text-transform: capitalize; + line-height: 100%; + cursor: pointer; +} + +.totalBlock { + border: 1px solid #3b3e4a; + display: flex; + flex-direction: column; + align-items: center; + padding: 24px; + width: 100%; + + @media (min-width: 1200px) { + width: 320px; + flex-shrink: 0; + } + .totalInfo { + display: flex; + flex-direction: column; + align-items: center; + text-align: center; + } + .totalPrice { + margin: 0; + margin-bottom: 4px; + font-weight: 800; + color: #f1f2f9; + font-size: 32px; + line-height: 41px; + letter-spacing: -1%; + } + .totalItems { + color: #75767f; + font-weight: 600; + font-size: 14px; + line-height: 21px; + margin: 0; + } + .divider { + width: 100%; + height: 1px; + background-color: #3b3e4a; + margin-top: 16px; + margin-bottom: 16px; + } +} + +.checkoutButton { + background-color: #905bff; + width: 250px; + height: 48px; + color: #f1f2f9; + font-size: 14px; + line-height: 21px; + font-weight: 700; + border: none; + cursor: pointer; + + &:hover { + background-color: #a378ff; + } +} + +.gridItems { + display: flex; + flex-direction: column; + gap: 16px; + width: 100%; + + @media (min-width: 1200px) { + flex-grow: 1; + } +} + +.cartItem { + background-color: #161827; + padding: 16px; + + display: flex; + flex-direction: column; + gap: 16px; + + @media (min-width: $tablet) { + flex-direction: row; + align-items: center; + justify-content: space-between; + } + + .itemTop { + display: flex; + align-items: center; + gap: 16px; + + @media (min-width: $tablet) { + flex-grow: 1; + } + } + + .removeButton { + background: none; + border: none; + color: #4a4d58; + font-size: 16px; + cursor: pointer; + &:hover { + color: #f1f2f9; + } + } + + .itemImageWrapper { + display: flex; + justify-content: center; + align-items: center; + flex-shrink: 0; + .itemImage { + object-fit: contain; + width: 80px; + height: 80px; + } + } + + .itemName { + font-weight: 600; + font-size: 14px; + line-height: 21px; + color: #f1f2f9; + } + + .itemBottom { + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + + @media (min-width: $tablet) { + gap: 48px; + } + + .quantityControls { + display: flex; + align-items: center; + } + + .qtyButton { + background: transparent; + border: none; + color: #f1f2f9; + width: 32px; + height: 32px; + font-size: 18px; + cursor: pointer; + display: flex; + justify-content: center; + align-items: center; + + img { + width: 14px; + height: 14px; + object-fit: contain; + display: block; + } + + &:first-child { + background-color: transparent; + border: 1px solid #3b3e4a; + } + + &:last-child { + background-color: #3b3e4a; + border: 1px solid #3b3e4a; + } + + &:disabled { + color: #3b3e4a; + cursor: default; + img { + opacity: 0.3; + } + } + } + + .qtyValue { + color: #f1f2f9; + width: 32px; + height: 32px; + display: flex; + justify-content: center; + align-items: center; + font-weight: 600; + font-size: 14px; + line-height: 21px; + } + } + + .itemPrice { + font-weight: 800; + font-size: 22px; + line-height: 140%; + color: #f1f2f9; + margin: 0; + } +} diff --git a/src/pages/Cart/Cart.tsx b/src/pages/Cart/Cart.tsx new file mode 100644 index 00000000000..4372dffce23 --- /dev/null +++ b/src/pages/Cart/Cart.tsx @@ -0,0 +1,135 @@ +import { Link, useNavigate } from 'react-router-dom'; +import styles from './Cart.module.scss'; +import { useContext } from 'react'; +import { CartContext, CartItem } from '../../context/CartContext'; +import { Product } from '../../components/types/Product'; + +export const Cart = () => { + const { cart, clearCart, updateQuantity, removeFromCart } = + useContext(CartContext); + + const totalItems = cart.reduce( + (sum: number, item: { quantity: number }) => sum + item.quantity, + 0, + ); + const totalPrice = cart.reduce( + (sum: number, item: { product: Product; quantity: number }) => { + const actualPrice = item.product.priceDiscount || item.product.price || 0; + + return sum + actualPrice * item.quantity; + }, + 0, + ); + const handleCheckout = () => { + const isConfirmed = window.confirm( + 'Checkout is not implemented yet. Do you want to clear the Cart?', + ); + + if (isConfirmed) { + clearCart(); + } + }; + + const navigate = useNavigate(); + + return ( +
    +
    + +
    +

    Cart

    + +
    +
    + {cart.length === 0 ? ( +

    Your cart is empty

    + ) : ( + cart.map((item: CartItem) => { + const actualPrice = + item.product.priceDiscount || item.product.price || 0; + const rawImage = item.product.images?.length + ? item.product.images[0] + : item.product.image; + const finalImage = rawImage?.startsWith('http') + ? rawImage + : `/${rawImage}`; + + return ( +
    +
    + + +
    + {item.product.name} +
    + + +

    {item.product.name}

    + +
    + +
    +
    + + {item.quantity} + +
    +

    + ${actualPrice * item.quantity} +

    +
    +
    + ); + }) + )} +
    + + {cart.length > 0 && ( +
    +
    +

    ${totalPrice}

    +

    Total for {totalItems} items

    +
    + +
    +
    + +
    +
    + )} +
    +
    + ); +}; diff --git a/src/pages/CategoryPage/CategoryPage.module.scss b/src/pages/CategoryPage/CategoryPage.module.scss new file mode 100644 index 00000000000..29488f26d6f --- /dev/null +++ b/src/pages/CategoryPage/CategoryPage.module.scss @@ -0,0 +1,175 @@ +@use '../../styles/utils/variables.scss' as *; + +.pageContainer { + padding: 0 16px; + box-sizing: border-box; + overflow: hidden; + width: 100%; + padding-bottom: 64px; + margin: 0 auto; + + @media (min-width: $tablet) { + padding: 0 24px; + } + + @media (min-width: $desktop) { + padding: 0 300px; + } +} + +.headerBlock { + .title { + color: #f1f2f9; + font-weight: 800; + font-size: 32px; + line-height: 41px; + letter-spacing: -1%; + } + .modelsCount { + color: #75767f; + font-weight: 600; + font-size: 14px; + line-height: 21px; + letter-spacing: 0%; + } +} + +.label { + color: #75767f; + font-weight: 700; + font-size: 12px; + line-height: 100%; + letter-spacing: 0%; +} + +.separator { + width: 16px; + height: 16px; +} + +.homeIcon { + display: flex; + align-items: center; + justify-content: center; +} + +.breadCrumbs { + margin-top: 24px; + display: flex; + align-items: center; + gap: 8px; +} + +.currentCategory { + text-transform: capitalize; + font-weight: 700; + font-size: 12px; + line-height: 100%; + letter-spacing: 0%; + color: #75767f; +} + +.select { + font-size: 14px; + font-weight: 700; + line-height: 21px; + letter-spacing: 0%; + appearance: none; + -webkit-appearance: none; + background-color: #323542; + color: #f1f2f9; + padding: 10px 71px 9px 12px; + border: 1px solid #323542; + + background-image: url('/img/icons/arrowDown.png'); + background-repeat: no-repeat; + background-size: 16px; + background-position: right 12px center; +} + +.filtersRow { + display: flex; + gap: 16px; + margin-bottom: 24px; + margin-top: 32px; + + @media (min-width: $tablet) { + width: 176px; + } +} + +.filterBlock { + display: flex; + flex-direction: column; + gap: 8px; + flex: 1; +} + +.pagination { + display: flex; + justify-content: center; + gap: 8px; + + // margin-bottom: 64px; + margin-top: 24px; + + @media (min-width: $tablet) { + margin-top: 40px; + margin-bottom: 64px; + } + + @media (min-width: $desktop) { + margin-top: 40px; + margin-bottom: 80px; + } +} + +.pageButton { + width: 32px; + height: 32px; + display: flex; + align-items: center; + justify-content: center; + + background-color: #161827; + border: none; + font-weight: 600; + font-size: 14px; + line-height: 21px; + letter-spacing: 0%; + color: #f1f2f9; + + &:hover { + background-color: #3b3e4a; + } +} + +.active { + background-color: #905bff; + + &:hover { + cursor: default; + } +} + +.errorContainer { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 24px; + padding: 64px 0; + + .reloadButton { + padding: 12px 24px; + background-color: #313237; + color: white; + border: none; + cursor: pointer; + font-family: inherit; + + &:hover { + background-color: #8941ff; + } + } +} diff --git a/src/pages/CategoryPage/CategoryPage.tsx b/src/pages/CategoryPage/CategoryPage.tsx new file mode 100644 index 00000000000..d2b0b7867a5 --- /dev/null +++ b/src/pages/CategoryPage/CategoryPage.tsx @@ -0,0 +1,217 @@ +import { Link, useSearchParams } from 'react-router-dom'; +import styles from './CategoryPage.module.scss'; +import productsData from '../../../public/api/products.json'; +import { ProductsList } from '../../components/ProductsList/ProductsList'; +import { useEffect, useState } from 'react'; +import { Loader } from '../../components/Loader/Loader'; + +interface Props { + title: string; + category: 'phones' | 'tablets' | 'accessories'; +} + +export const CategoryPage = ({ title, category }: Props) => { + const [searchParams, setSearchParams] = useSearchParams(); + const [isLoading, setIsLoading] = useState(true); + const [hasError, setHasError] = useState(false); + const perPage = searchParams.get('perPage') || 'all'; + + useEffect(() => { + setIsLoading(true); + setHasError(false); + + const timer = setTimeout(() => { + setIsLoading(false); + }, 1000); + + return () => clearTimeout(timer); + }, [category]); + + const handlePageChange = (newPage: number) => { + searchParams.set('page', newPage.toString()); + setSearchParams(searchParams); + }; + + const handlePerPageChange = (e: React.ChangeEvent) => { + searchParams.set('perPage', e.target.value); + searchParams.set('page', '1'); + setSearchParams(searchParams); + }; + + const filteredProducts = productsData.filter( + product => product.category === category, + ); + const sort = searchParams.get('sort') || 'age'; + const handleSortChange = (e: React.ChangeEvent) => { + searchParams.set('sort', e.target.value); + setSearchParams(searchParams); + }; + + const sortedProducts = [...filteredProducts].sort((a, b) => { + if (sort === 'title') { + return a.name.localeCompare(b.name); + } + + if (sort === 'price') { + const priceA = a.price || a.fullPrice || 0; + const priceB = b.price || b.fullPrice || 0; + + return priceA - priceB; + } + + if (sort === 'age') { + return b.year - a.year; + } + + return 0; + }); + + const currentPage = Number(searchParams.get('page')) || 1; + const itemsPerPage = + perPage === 'all' || perPage === 'All' + ? filteredProducts.length + : Number(perPage); + const lastItemIndex = currentPage * itemsPerPage; + const firstItemIndex = lastItemIndex - itemsPerPage; + const visibleProducts = sortedProducts.slice(firstItemIndex, lastItemIndex); + + // const visibleProducts = filteredProducts.slice(firstItemIndex, lastItemIndex); + const totalPages = Math.ceil(filteredProducts.length / itemsPerPage); + + if (isLoading) { + return ; + } + + if (hasError) { + return ( +
    +

    Somethig went wrong

    + +
    + ); + } + + if (filteredProducts.length === 0) { + return

    There are no {category} yet

    ; + } + + const visiblePages = (() => { + const maxVisible = 4; + + let start = currentPage - 1; + + if (start <= 0) { + start = 1; + } + + let end = start + maxVisible - 1; + + if (end > totalPages) { + end = totalPages; + start = Math.max(1, end - maxVisible + 1); + } + + const pages = []; + + for (let i = start; i <= end; i++) { + pages.push(i); + } + + return pages; + })(); + + return ( +
    +
    + + Home + + right + {category} +
    + +
    +

    {title}

    +

    {filteredProducts.length} models

    +
    + +
    +
    + + +
    + +
    + + +
    +
    + +
    + +
    + + {totalPages > 1 && ( +
    + + {visiblePages.map(pageNum => ( + + ))} + + +
    + )} +
    + ); +}; diff --git a/src/pages/Favorites/Favorites.module.scss b/src/pages/Favorites/Favorites.module.scss new file mode 100644 index 00000000000..972dd2af253 --- /dev/null +++ b/src/pages/Favorites/Favorites.module.scss @@ -0,0 +1,62 @@ +@use '../../styles/utils/variables' as *; + +.favouriteBlock { + padding: 0 16px; + margin-bottom: 56px; + + @media (min-width: $tablet) { + padding: 0 24px; + margin-bottom: 64px; + } + + @media (min-width: $desktop) { + padding: 0 32px; + margin-bottom: 80px; + } + + @media (min-width: 1201px) { + padding: 0 152px; + } +} + +.title { + color: #f1f2f9; + font-weight: 800; + font-size: 32px; + line-height: 41px; + letter-spacing: -1%; + + @media (min-width: $tablet) { + font-size: 48px; + line-height: 56px; + } +} + +.breadCrumbs { + margin-top: 24px; + display: flex; + gap: 8px; + align-items: center; + .productName { + color: #75767f; + font-size: 12px; + font-weight: 700; + line-height: 100%; + } +} + +.productGrid { + display: flex; + flex-direction: column; + gap: 40px; + + @media (min-width: $tablet) { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 16px; + } + + @media (min-width: $desktop) { + grid-template-columns: repeat(4, 1fr); + } +} diff --git a/src/pages/Favorites/Favorites.tsx b/src/pages/Favorites/Favorites.tsx new file mode 100644 index 00000000000..dc6e4ce170b --- /dev/null +++ b/src/pages/Favorites/Favorites.tsx @@ -0,0 +1,34 @@ +import { Link } from 'react-router-dom'; +import styles from './Favorites.module.scss'; +import { useContext } from 'react'; +import { FavoritesContext } from '../../context/FavoritesContext'; +import { ProductCard } from '../../components/ProductCard/ProductCard'; +import { Product } from '../../components/types/Product'; + +export const Favorites = () => { + const { favorites } = useContext(FavoritesContext) || { favorites: [] }; + + return ( +
    +
    + + Home + + right + + Favourites +
    + +

    Favourites

    +
    + {favorites.map((product: Product) => ( + + ))} +
    +
    + ); +}; diff --git a/src/pages/Home/Home.module.scss b/src/pages/Home/Home.module.scss new file mode 100644 index 00000000000..4100c2a3022 --- /dev/null +++ b/src/pages/Home/Home.module.scss @@ -0,0 +1,61 @@ +@use '../../styles//utils/variables' as *; + +.container { + padding: 0 16px; + box-sizing: border-box; + overflow: hidden; + width: 100%; + + @media (min-width: $tablet) { + padding: 0 24px; + } + + @media (min-width: $desktop) { + padding: 0 32px; + } + + @media (min-width: 1201px) { + padding: 0 152px; + } +} + +.title { + padding-top: 24px; + + @media (min-width: $tablet) { + padding-top: 32px; + } + + @media (min-width: $desktop) { + padding-top: 56px; + } +} + +.h1_title { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border: 0; + font-size: 32px; + line-height: 41px; + letter-spacing: -0.01em; + font-weight: Bold; + + @media (min-width: $tablet) { + font-size: 48px; + line-height: 56px; + letter-spacing: -1%; + font-weight: 800; + color: #f1f2f9; + } +} + +// .section { +// display: flex; +// flex-direction: column; +// } diff --git a/src/pages/Home/Home.tsx b/src/pages/Home/Home.tsx new file mode 100644 index 00000000000..ab99f9b44bc --- /dev/null +++ b/src/pages/Home/Home.tsx @@ -0,0 +1,27 @@ +import styles from './Home.module.scss'; +import { ProductSlider } from '../../components/ProductSlider/ProductSlider'; +import { ShopByCategory } from '../../components/ShopByCategory/ShopByCategory'; +import { BrandNewModels } from '../../components/BrandNewModels/BrandNewModels'; +import { HotPrices } from '../../components/HotPrices/HotPrices'; + +export const Home = () => { + return ( +
    +
    +
    +

    + {/* Welcome to Nice Gadgets store! */} + Product Catalog +

    +
    +
    + + +
    + + + +
    +
    + ); +}; diff --git a/src/pages/ProductDetailsPage/ProductDetailsPage.module.scss b/src/pages/ProductDetailsPage/ProductDetailsPage.module.scss new file mode 100644 index 00000000000..005d5e809ab --- /dev/null +++ b/src/pages/ProductDetailsPage/ProductDetailsPage.module.scss @@ -0,0 +1,306 @@ +@use '../../styles/utils/variables' as *; + +.detailPage { + padding-top: 24px; + padding-left: 16px; + padding-right: 17px; + + @media (min-width: $tablet) { + padding-left: 24px; + padding-right: 24px; + } + + @media (min-width: $desktop) { + padding-left: 152px; + padding-right: 152px; + padding-bottom: 81px; + } +} + +.mainGrid { + display: flex; + flex-direction: column; + + @media (min-width: $tablet) { + flex-direction: row; + align-items: flex-start; + gap: 17px; + } + + @media (min-width: $desktop) { + gap: 64px; + } +} + +.breadCrumbs { + display: flex; + gap: 8px; + align-items: center; +} + +.categoryLink, +.productName { + font-weight: 700; + font-size: 12px; + line-height: 100%; +} + +.categoryLink { + color: #f1f2f9; + text-transform: capitalize; +} + +.productName { + color: #75767f; +} + +.backButton { + margin-top: 24px; +} + +.backBtn { + display: flex; + align-items: center; + gap: 4px; + background-color: transparent; + border: none; + color: #f1f2f9; + font-weight: 700; + font-size: 12px; + text-transform: capitalize; + line-height: 100%; + cursor: pointer; +} + +.mainInfo { + margin-top: 16px; +} + +.title { + font-weight: 800; + font-size: 22px; + line-height: 140%; + color: #f1f2f9; +} + +.errorTitle { + display: flex; + justify-content: center; + align-items: center; +} + +.gallery { + display: flex; + flex-direction: column-reverse; + gap: 16px; + flex: 1; + max-width: 560px; + + @media (min-width: $tablet) { + flex-direction: row; + gap: 17px; + } +} + +.mainImageContainer { + display: flex; + justify-content: center; + align-items: center; + + .mainImage { + width: 100%; + max-width: 320px; + height: auto; + object-fit: contain; + + @media (min-width: $tablet) { + max-width: 100%; + } + } +} + +.thumbnails { + display: flex; + flex-direction: row; + justify-content: center; + gap: 8px; + + @media (min-width: $tablet) { + flex-direction: column; + justify-content: flex-start; + } +} + +.thumb { + width: 42.24px; + height: 40.43px; + border: 1px solid #3b3e4a; + background: transparent; + padding: 4px; + cursor: pointer; + transition: border-color 0.2s; + flex: 1; + + @media (min-width: $tablet) { + flex: unset; + width: 35px; + height: 35px; + } + + @media (min-width: $desktop) { + flex: unset; + width: 80px; + height: 80px; + } + + &Active { + border-color: #c4c4c4; + } + + img { + width: 100%; + height: 100%; + object-fit: contain; + } +} + +.actions { + // flex-shrink: 1; + // margin-top: 40px; + margin-top: 0; + width: 100%; + + @media (min-width: $tablet) { + max-width: 320px; + flex-shrink: 0; + } +} + +.text { + font-weight: 600; + font-size: 14px; + line-height: 21px; +} + +.detailsPrices { + display: flex; + gap: 8px; + align-items: center; + /* stylelint-disable-next-line selector-pseudo-class-no-unknown */ + :global(.price-current) { + font-size: 32px; + font-weight: 800; + color: #f1f2f9; + } + /* stylelint-disable-next-line selector-pseudo-class-no-unknown */ + :global(.price-old) { + font-size: 22px; + color: #89939a; + text-decoration: line-through; + } +} + +.buttonsBlock { + margin: auto; + display: flex; + gap: 8px; + width: 100%; + margin-top: 16px; +} + +.addButton { + cursor: pointer; + font-weight: 700; + font-size: 14px; + line-height: 21px; + letter-spacing: 0%; + width: 231px; + height: 48px; + color: #f1f2f9; + background-color: #905bff; + border: none; + flex: 1; + &:hover { + background-color: #a378ff; + } +} + +.addedButton { + background-color: #323542 !important; + + &:hover { + background-color: #4a4d58 !important; + } +} + +.favotiteButton { + flex-shrink: 0; + border: none; + cursor: pointer; + width: 48px; + height: 48px; + background-color: #323542; + + &:hover { + background-color: #4a4d58; + } +} + +.specs { + margin-top: 32px; + display: flex; + flex-direction: column; + gap: 8px; + + .labelText { + font-weight: 700; + font-size: 12px; + line-height: 100%; + color: #75767f; + text-transform: capitalize; + } + .valueText { + font-weight: 700; + font-size: 12px; + line-height: 100%; + color: #f1f2f9; + } + + .specRow { + display: flex; + justify-content: space-between; + align-items: center; + } +} + +.aboutBlock { + margin-top: 56px; + + @media (min-width: $tablet) { + width: 100%; + } +} + +.techSpecBlock { + margin-top: 56px; + flex: 1; + min-width: 0; + + @media (min-width: $desktop) { + width: 512px; + min-width: 512px; + flex-shrink: 0; + } +} + +.bottomSection { + display: flex; + flex-direction: column; + width: 100%; + + @media (min-width: $desktop) { + flex-direction: row; + gap: 64px; + align-items: flex-start; + } +} diff --git a/src/pages/ProductDetailsPage/ProductDetailsPage.tsx b/src/pages/ProductDetailsPage/ProductDetailsPage.tsx new file mode 100644 index 00000000000..05bddae5574 --- /dev/null +++ b/src/pages/ProductDetailsPage/ProductDetailsPage.tsx @@ -0,0 +1,251 @@ +import { useContext, useEffect, useState } from 'react'; +import { Link, useNavigate, useParams } from 'react-router-dom'; +import { Loader } from '../../components/Loader/Loader'; +import styles from './ProductDetailsPage.module.scss'; +import { Price } from '../../components/Price/Price'; +/* eslint-disable max-len */ +import { AboutBlock } from '../../components/ProductDetailsPageComponents/AboutBlock/AboutBlock'; +import { TechSpecBlock } from '../../components/ProductDetailsPageComponents/TechSpecBlock/TechSpecBlock'; +import { CapacityBlock } from '../../components/ProductDetailsPageComponents/CapacityBlock/CapacityBlock'; +import { ColorSelector } from '../../components/ProductDetailsPageComponents/ColorSelector/ColorSelector'; +import { RecommendedSection } from '../../components/ProductDetailsPageComponents/RecommendedSection/RecommendedSection'; +/* eslint-enable max-len */ +import { CartContext } from '../../context/CartContext'; +import { FavoritesContext } from '../../context/FavoritesContext'; +import { Product } from '../../components/types/Product'; +interface ProductExtended extends Product { + description: { title: string; text: string[] }[]; + screen: string; + resolution: string; + processor: string; + ram: string; + camera?: string; + zoom?: string; + cell?: string[]; + colorsAvailable: string[]; + color: string; + capacityAvailable: string[]; + capacity: string; + images: string[]; + priceRegular: number; + priceDiscount: number; +} + +export const ProductDetailsPage = () => { + const { cart, addToCart, removeFromCart } = useContext(CartContext); + const [selectedImage, setSelectedImage] = useState(''); + const navigate = useNavigate(); + const { productId } = useParams(); + const [product, setProduct] = useState(null); + const [isLoading, setIsLoading] = useState(true); + const [hasError, setHasError] = useState(false); + const currentProductId = product ? String(product.itemId || product.id) : ''; + const isInCart = cart.some( + (item: { id: string }) => item.id === currentProductId, + ); + + // const { favorites, toggleFavorite } = useContext(FavoritesContext); + const favoriteContext = useContext(FavoritesContext); + const favorites = favoriteContext?.favorites || []; + const toggleFavorite = favoriteContext?.toggleFavorite || (() => {}); + + const isFavorite = favorites.some( + (item: Product) => String(item.itemId || item.id) === currentProductId, + ); + + const handleCartClick = () => { + if (!product) { + return; + } + + const targetId = String(product.itemId || product.id); + + if (isInCart) { + removeFromCart(targetId); + } else { + addToCart(product as Product); + } + }; + + useEffect(() => { + const fetchProduct = async () => { + setIsLoading(true); + setHasError(false); + + try { + const [phones, tablets, accessories] = await Promise.all([ + fetch('/api/phones.json').then(res => res.json()), + fetch('/api/tablets.json').then(res => res.json()), + fetch('/api/accessories.json').then(res => res.json()), + ]); + + const allDetailedProducts = [...phones, ...tablets, ...accessories]; + + const found = allDetailedProducts.find(p => p.id === productId); + + if (found) { + setProduct(found); + if (found.images && found.images.length > 0) { + setSelectedImage(found.images[0]); + } + } else { + setHasError(true); + } + } catch (error) { + setHasError(true); + // console.error('Download error:', error); + } finally { + setTimeout(() => setIsLoading(false), 500); + } + }; + + fetchProduct(); + }, [productId]); + + if (isLoading) { + return ; + } + + if (hasError || !product) { + return

    Product was not found

    ; + } + + const specs = [ + { label: 'screen', value: product.screen }, + { label: 'resolution', value: product.resolution }, + { label: 'processor', value: product.processor }, + { + label: 'RAM', + value: product.ram ? product.ram.replace(/([0-9])([A-Z])/, '$1 $2') : '', + }, + ]; + + return ( +
    +
    + + Home + + right + + {product.category} + + right + + {product.name} +
    +
    + +
    +
    +

    {product.name}

    +
    + +
    +
    +
    + {product.images?.map((img: string) => ( +
    setSelectedImage(img)} + className={`${styles.thumb} ${selectedImage === img ? styles.thumbActive : ''}`} + > + preview +
    + ))} +
    + +
    + {product.name} +
    +
    + +
    + + + + +
    + +
    + +
    + + + +
    + +
    + {specs.map(spec => ( +
    + {spec.label} + {spec.value} +
    + ))} +
    +
    +
    +
    +
    + +
    +
    + +
    +
    + + +
    + ); +}; diff --git a/src/styles/_fonts.scss b/src/styles/_fonts.scss new file mode 100644 index 00000000000..383a22c0a2b --- /dev/null +++ b/src/styles/_fonts.scss @@ -0,0 +1,17 @@ +@font-face { + font-family: Mont; + src: url('/fonts/Mont-Regular.otf') format('opentype'); + font-weight: 400; +} + +@font-face { + font-family: Mont; + src: url('/fonts/Mont-SemiBold.otf') format('opentype'); + font-weight: 600; +} + +@font-face { + font-family: Mont; + src: url('/fonts/Mont-Bold.otf') format('opentype'); + font-weight: 700; +} diff --git a/src/styles/utils/_variables.scss b/src/styles/utils/_variables.scss new file mode 100644 index 00000000000..4d6c3836957 --- /dev/null +++ b/src/styles/utils/_variables.scss @@ -0,0 +1,13 @@ +$color-secondary: #75767f; +$color-icons: #4a4d58; +$color-elements: #3b3e4a; +$color-surface-1: #161827; +$color-surface-2: #323542; +$color-black: #0f1121; +$color-white: #f1f2f9; +$color-accent: #905bff; +$color-green: #27ae60; +$color-red: #eb5757; +$mobile: 320px; +$tablet: 640px; +$desktop: 1200px; diff --git a/vercel.json b/vercel.json new file mode 100644 index 00000000000..1323cdac34c --- /dev/null +++ b/vercel.json @@ -0,0 +1,8 @@ +{ + "rewrites": [ + { + "source": "/(.*)", + "destination": "/index.html" + } + ] +} diff --git a/vite.config.ts b/vite.config.ts index 5a33944a9b4..0f6f7fbf80c 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,7 +1,8 @@ -import { defineConfig } from 'vite' -import react from '@vitejs/plugin-react' +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; // https://vitejs.dev/config/ export default defineConfig({ + base: './', plugins: [react()], -}) +});