diff --git a/index.html b/index.html index 095fb3a453..fc9bda2eef 100644 --- a/index.html +++ b/index.html @@ -3,7 +3,8 @@ - Vite + React + TS + Phone Catalog +
diff --git a/package-lock.json b/package-lock.json index 836b9e63b4..602e190ac1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,11 +16,14 @@ "react": "^18.3.1", "react-dom": "^18.3.1", "react-router-dom": "^6.25.1", - "react-transition-group": "^4.4.5" + "react-slick": "^0.30.2", + "react-transition-group": "^4.4.5", + "slick-carousel": "^1.8.1", + "swiper": "^11.1.14" }, "devDependencies": { "@cypress/react18": "^2.0.1", - "@mate-academy/scripts": "^1.8.5", + "@mate-academy/scripts": "^1.9.12", "@mate-academy/students-ts-config": "*", "@mate-academy/stylelint-config": "*", "@types/node": "^20.14.10", @@ -1184,10 +1187,11 @@ } }, "node_modules/@mate-academy/scripts": { - "version": "1.8.5", - "resolved": "https://registry.npmjs.org/@mate-academy/scripts/-/scripts-1.8.5.tgz", - "integrity": "sha512-mHRY2FkuoYCf5U0ahIukkaRo5LSZsxrTSgMJheFoyf3VXsTvfM9OfWcZIDIDB521kdPrScHHnRp+JRNjCfUO5A==", + "version": "1.9.12", + "resolved": "https://registry.npmjs.org/@mate-academy/scripts/-/scripts-1.9.12.tgz", + "integrity": "sha512-/OcmxMa34lYLFlGx7Ig926W1U1qjrnXbjFJ2TzUcDaLmED+A5se652NcWwGOidXRuMAOYLPU2jNYBEkKyXrFJA==", "dev": true, + "license": "MIT", "dependencies": { "@octokit/rest": "^17.11.2", "@types/get-port": "^4.2.0", @@ -3992,6 +3996,12 @@ "once": "^1.4.0" } }, + "node_modules/enquire.js": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/enquire.js/-/enquire.js-2.1.6.tgz", + "integrity": "sha512-/KujNpO+PT63F7Hlpu4h3pE3TokKRHN26JYmQpPyjkRD/N57R7bPDNojMXdi7uveAKjYB7yQnartCxZnFWr0Xw==", + "license": "MIT" + }, "node_modules/enquirer": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.4.1.tgz", @@ -6553,6 +6563,13 @@ "set-function-name": "^2.0.1" } }, + "node_modules/jquery": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.1.tgz", + "integrity": "sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg==", + "license": "MIT", + "peer": true + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -6624,6 +6641,15 @@ "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", "dev": true }, + "node_modules/json2mq": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/json2mq/-/json2mq-0.2.0.tgz", + "integrity": "sha512-SzoRg7ux5DWTII9J2qkrZrqV1gt+rTaoufMxEzXbS26Uid0NwaJd123HcoB80TgubEppxxIGdNxCx50fEoEWQA==", + "license": "MIT", + "dependencies": { + "string-convert": "^0.2.0" + } + }, "node_modules/json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", @@ -6802,6 +6828,12 @@ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "dev": true }, + "node_modules/lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", + "license": "MIT" + }, "node_modules/lodash.get": { "version": "4.4.2", "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", @@ -8773,6 +8805,23 @@ "react-dom": ">=16.8" } }, + "node_modules/react-slick": { + "version": "0.30.2", + "resolved": "https://registry.npmjs.org/react-slick/-/react-slick-0.30.2.tgz", + "integrity": "sha512-XvQJi7mRHuiU3b9irsqS9SGIgftIfdV5/tNcURTb5LdIokRA5kIIx3l4rlq2XYHfxcSntXapoRg/GxaVOM1yfg==", + "license": "MIT", + "dependencies": { + "classnames": "^2.2.5", + "enquire.js": "^2.1.6", + "json2mq": "^0.2.0", + "lodash.debounce": "^4.0.8", + "resize-observer-polyfill": "^1.5.0" + }, + "peerDependencies": { + "react": "^0.14.0 || ^15.0.1 || ^16.0.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^0.14.0 || ^15.0.1 || ^16.0.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/react-transition-group": { "version": "4.4.5", "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", @@ -8976,6 +9025,12 @@ "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", "dev": true }, + "node_modules/resize-observer-polyfill": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz", + "integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==", + "license": "MIT" + }, "node_modules/resolve": { "version": "1.22.8", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", @@ -9453,6 +9508,15 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, + "node_modules/slick-carousel": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/slick-carousel/-/slick-carousel-1.8.1.tgz", + "integrity": "sha512-XB9Ftrf2EEKfzoQXt3Nitrt/IPbT+f1fgqBdoxO3W/+JYvtEOW6EgxnWfr9GH6nmULv7Y2tPmEX3koxThVmebA==", + "license": "MIT", + "peerDependencies": { + "jquery": ">=1.8.0" + } + }, "node_modules/source-map-js": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", @@ -9535,6 +9599,12 @@ "node": ">= 0.4" } }, + "node_modules/string-convert": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/string-convert/-/string-convert-0.2.1.tgz", + "integrity": "sha512-u/1tdPl4yQnPBjnVrmdLo9gtuLvELKsAoRapekWggdiQNvvvum+jYF329d84NAa660KQw7pB2n36KrIKVoXa3A==", + "license": "MIT" + }, "node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", @@ -9930,6 +10000,25 @@ "integrity": "sha512-ovssysQTa+luh7A5Weu3Rta6FJlFBBbInjOh722LIt6klpU2/HtdUbszju/G4devcvk8PGt7FCLv5wftu3THUA==", "dev": true }, + "node_modules/swiper": { + "version": "11.1.14", + "resolved": "https://registry.npmjs.org/swiper/-/swiper-11.1.14.tgz", + "integrity": "sha512-VbQLQXC04io6AoAjIUWuZwW4MSYozkcP9KjLdrsG/00Q/yiwvhz9RQyt0nHXV10hi9NVnDNy1/wv7Dzq1lkOCQ==", + "funding": [ + { + "type": "patreon", + "url": "https://www.patreon.com/swiperjs" + }, + { + "type": "open_collective", + "url": "http://opencollective.com/swiper" + } + ], + "license": "MIT", + "engines": { + "node": ">= 4.7.0" + } + }, "node_modules/synckit": { "version": "0.8.8", "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.8.8.tgz", diff --git a/package.json b/package.json index ae251685c8..f4d699db12 100644 --- a/package.json +++ b/package.json @@ -12,11 +12,14 @@ "react": "^18.3.1", "react-dom": "^18.3.1", "react-router-dom": "^6.25.1", - "react-transition-group": "^4.4.5" + "react-slick": "^0.30.2", + "react-transition-group": "^4.4.5", + "slick-carousel": "^1.8.1", + "swiper": "^11.1.14" }, "devDependencies": { "@cypress/react18": "^2.0.1", - "@mate-academy/scripts": "^1.8.5", + "@mate-academy/scripts": "^1.9.12", "@mate-academy/students-ts-config": "*", "@mate-academy/stylelint-config": "*", "@types/node": "^20.14.10", diff --git a/public/img/Accessories.png b/public/img/Accessories.png new file mode 100644 index 0000000000..4ca21b520a Binary files /dev/null and b/public/img/Accessories.png differ diff --git a/public/img/Add to fovourites - Added.svg b/public/img/Add to fovourites - Added.svg new file mode 100644 index 0000000000..d18095ca04 --- /dev/null +++ b/public/img/Add to fovourites - Added.svg @@ -0,0 +1,4 @@ + + + + diff --git a/public/img/Add to fovourites - Hover.svg b/public/img/Add to fovourites - Hover.svg new file mode 100644 index 0000000000..28c7b9c255 --- /dev/null +++ b/public/img/Add to fovourites - Hover.svg @@ -0,0 +1,4 @@ + + + + diff --git a/public/img/Banner.png b/public/img/Banner.png new file mode 100644 index 0000000000..67edb9bfe3 Binary files /dev/null and b/public/img/Banner.png differ diff --git a/public/img/Buttons_Slider button - Default (right) (1).svg b/public/img/Buttons_Slider button - Default (right) (1).svg new file mode 100644 index 0000000000..975f706cd2 --- /dev/null +++ b/public/img/Buttons_Slider button - Default (right) (1).svg @@ -0,0 +1,10 @@ + + + Created with Pixso. + + + + + + + diff --git a/public/img/Buttons_Slider button - Default (right) (2).svg b/public/img/Buttons_Slider button - Default (right) (2).svg new file mode 100644 index 0000000000..7dfc193f89 --- /dev/null +++ b/public/img/Buttons_Slider button - Default (right) (2).svg @@ -0,0 +1,10 @@ + + + Created with Pixso. + + + + + + + diff --git a/public/img/Buttons_Slider button - Default (right).svg b/public/img/Buttons_Slider button - Default (right).svg new file mode 100644 index 0000000000..76e93598d4 --- /dev/null +++ b/public/img/Buttons_Slider button - Default (right).svg @@ -0,0 +1,10 @@ + + + Created with Pixso. + + + + + + + diff --git a/public/img/Buttons_Slider button - Disabled (right).svg b/public/img/Buttons_Slider button - Disabled (right).svg new file mode 100644 index 0000000000..aba02a8386 --- /dev/null +++ b/public/img/Buttons_Slider button - Disabled (right).svg @@ -0,0 +1,10 @@ + + + Created with Pixso. + + + + + + + diff --git a/public/img/Cart (1).svg b/public/img/Cart (1).svg new file mode 100644 index 0000000000..bbc6bbb290 --- /dev/null +++ b/public/img/Cart (1).svg @@ -0,0 +1,11 @@ + + + Created with Pixso. + + + + + + + + diff --git a/public/img/Cart.svg b/public/img/Cart.svg new file mode 100644 index 0000000000..a5bfa7c5d2 --- /dev/null +++ b/public/img/Cart.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/public/img/Cart_nav.svg b/public/img/Cart_nav.svg new file mode 100644 index 0000000000..7162a81fc0 --- /dev/null +++ b/public/img/Cart_nav.svg @@ -0,0 +1,24 @@ + + + Created with Pixso. + + + + + + + + + + + + + + + + + + + + + diff --git a/public/img/Chevron (Arrow Right).svg b/public/img/Chevron (Arrow Right).svg new file mode 100644 index 0000000000..81a760ecf7 --- /dev/null +++ b/public/img/Chevron (Arrow Right).svg @@ -0,0 +1,3 @@ + + + diff --git a/public/img/Favourites (1).svg b/public/img/Favourites (1).svg new file mode 100644 index 0000000000..b01e5feb0a --- /dev/null +++ b/public/img/Favourites (1).svg @@ -0,0 +1,9 @@ + + + Created with Pixso. + + + + + + diff --git a/public/img/Favourites.svg b/public/img/Favourites.svg new file mode 100644 index 0000000000..9b7e912084 --- /dev/null +++ b/public/img/Favourites.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/public/img/Favourites_nav.svg b/public/img/Favourites_nav.svg new file mode 100644 index 0000000000..8cef07bf29 --- /dev/null +++ b/public/img/Favourites_nav.svg @@ -0,0 +1,22 @@ + + + Created with Pixso. + + + + + + + + + + + + + + + + + + + diff --git a/public/img/Footer-slide.svg b/public/img/Footer-slide.svg new file mode 100644 index 0000000000..d82eff3b56 --- /dev/null +++ b/public/img/Footer-slide.svg @@ -0,0 +1,10 @@ + + + Created with Pixso. + + + + + + + diff --git a/public/img/Home.svg b/public/img/Home.svg new file mode 100644 index 0000000000..347bc16c0c --- /dev/null +++ b/public/img/Home.svg @@ -0,0 +1,4 @@ + + + + diff --git a/public/img/Icons_Chevron (Arrow Down).svg b/public/img/Icons_Chevron (Arrow Down).svg new file mode 100644 index 0000000000..cc110f5b31 --- /dev/null +++ b/public/img/Icons_Chevron (Arrow Down).svg @@ -0,0 +1,8 @@ + + + Created with Pixso. + + + + + diff --git a/public/img/Icons_Chevron (Arrow Right).svg b/public/img/Icons_Chevron (Arrow Right).svg new file mode 100644 index 0000000000..9a0608372d --- /dev/null +++ b/public/img/Icons_Chevron (Arrow Right).svg @@ -0,0 +1,8 @@ + + + Created with Pixso. + + + + + diff --git a/public/img/Icons_Close (1).png b/public/img/Icons_Close (1).png new file mode 100644 index 0000000000..aca504f818 Binary files /dev/null and b/public/img/Icons_Close (1).png differ diff --git a/public/img/Icons_Close.png b/public/img/Icons_Close.png new file mode 100644 index 0000000000..7c67dbf79b Binary files /dev/null and b/public/img/Icons_Close.png differ diff --git a/public/img/Icons_Favourites.svg b/public/img/Icons_Favourites.svg new file mode 100644 index 0000000000..6574219769 --- /dev/null +++ b/public/img/Icons_Favourites.svg @@ -0,0 +1,8 @@ + + + Created with Pixso. + + + + + diff --git a/public/img/Logo.png b/public/img/Logo.png new file mode 100644 index 0000000000..5143eb0b64 Binary files /dev/null and b/public/img/Logo.png differ diff --git a/public/img/Menu.png b/public/img/Menu.png new file mode 100644 index 0000000000..561139f115 Binary files /dev/null and b/public/img/Menu.png differ diff --git a/public/img/Phones.png b/public/img/Phones.png new file mode 100644 index 0000000000..6891468bac Binary files /dev/null and b/public/img/Phones.png differ diff --git a/public/img/Slider button - Default (right).png b/public/img/Slider button - Default (right).png new file mode 100644 index 0000000000..f2d229f59f Binary files /dev/null and b/public/img/Slider button - Default (right).png differ diff --git a/public/img/Slider button - Default (right).svg b/public/img/Slider button - Default (right).svg new file mode 100644 index 0000000000..8ccaf2341b --- /dev/null +++ b/public/img/Slider button - Default (right).svg @@ -0,0 +1,4 @@ + + + + diff --git a/public/img/Tablets.png b/public/img/Tablets.png new file mode 100644 index 0000000000..ae4709c6f1 Binary files /dev/null and b/public/img/Tablets.png differ diff --git a/public/img/add-to-cart.svg b/public/img/add-to-cart.svg new file mode 100644 index 0000000000..afbe9ee722 --- /dev/null +++ b/public/img/add-to-cart.svg @@ -0,0 +1,10 @@ + + + Created with Pixso. + + + + + + + diff --git a/public/img/slider-first-photo.png b/public/img/slider-first-photo.png new file mode 100644 index 0000000000..84a37237bb Binary files /dev/null and b/public/img/slider-first-photo.png differ diff --git a/public/img/slider-second-photo.jpg b/public/img/slider-second-photo.jpg new file mode 100644 index 0000000000..926b0e1978 Binary files /dev/null and b/public/img/slider-second-photo.jpg differ diff --git a/public/img/slider-second-photo.webp b/public/img/slider-second-photo.webp new file mode 100644 index 0000000000..e50fb21e3f Binary files /dev/null and b/public/img/slider-second-photo.webp differ diff --git a/public/img/slider-third-photo.jpg b/public/img/slider-third-photo.jpg new file mode 100644 index 0000000000..41d9cd676d Binary files /dev/null and b/public/img/slider-third-photo.jpg differ diff --git a/src/App.scss b/src/App.scss index 71bc413aad..75ff76230a 100644 --- a/src/App.scss +++ b/src/App.scss @@ -1 +1,695 @@ -// not empty +.pagination { + margin-top: 30px; + display: flex; + justify-content: center; + column-gap: 4px; + + &--previous { + cursor: pointer; + width: 32px; + height: 32px; + box-sizing: border-box; + border: 1px solid rgb(180, 189, 196); + border-radius: 48px; + } + + &--next { + cursor: pointer; + color: #000; + width: 32px; + height: 32px; + box-sizing: border-box; + border: 1px solid rgb(180, 189, 196); + border-radius: 48px; + } + + &--str { + cursor: pointer; + font-family: Mont, sans-serif; + font-size: 14px; + font-weight: 500; + width: 32px; + height: 32px; + box-sizing: border-box; + border: 1px solid rgb(226, 230, 233); + border-radius: 48px; + } + + &--str.active { + cursor: pointer; + color: #fff; + font-family: Mont, sans-serif; + font-size: 14px; + font-weight: 500; + width: 32px; + height: 32px; + box-sizing: border-box; + border-radius: 48px; + background: rgb(15, 15, 17); + } +} + +.cart__counter { + display: flex; + column-gap: 12px; + + align-items: center; + width: 120px; + justify-content: space-between; /* добавляем это свойство */ + + &-text { + box-sizing: border-box; + border: 1px solid rgb(180, 189, 196); + border-radius: 48px; + width: 32px; + height: 32px; + margin: 0; + color: rgb(0, 0, 0); + font-family: Mont, sans-serif; + font-size: 14px; + font-weight: 500; + } +} + +.nav__count--first, +.nav__count--second { + display: flex; + align-items: center; + justify-content: center; + color: #fff; + position: absolute; + top: 27%; + right: 27%; + width: 15px; + height: 15px; + font-size: 10px; + border: 1px solid rgb(255, 255, 255); + background: rgb(66, 25, 208); + border-radius: 100%; + box-sizing: border-box; +} + +@media screen and (min-width: 640px) { + .qwerty { + display: grid; + grid-template-columns: repeat(12, 1fr); + column-gap: 16px; + align-items: center; + + @media screen and (min-width: 1200px) { + grid-template-columns: repeat(24, 1fr); + } + } + + .about__grid { + display: grid; + + grid-template-columns: repeat(12, 1fr); + column-gap: 64px; + + @media screen and (min-width: 1200px) { + grid-template-columns: repeat(24, 1fr); + column-gap: 16px; + } + + &-el { + grid-column: span 12; + + @media screen and (min-width: 1200px) { + grid-column: span 12; + } + } + } + + .details__image--more { + grid-column: 1 / 2; + } + + .details__image { + grid-column: 3 / 10; + height: 464px; + } + + .q { + grid-column: 11 / 20; + } + + .card__ram-info { + display: flex; + align-items: center; + margin-bottom: 8px; + } +} + +.card__ram-circle { + width: 20px; + height: 20px; + border-radius: 50%; + display: inline-block; + margin-right: 8px; +} + +.card__buy-cart { + margin: 0; + color: rgb(255, 255, 255); + font-family: Mont, sans-serif; + font-size: 13px; + font-weight: 700; + transition: all 0.3s; +} + +.carousel--slider__second { + height: 189px; + + @media screen and (max-width: 640px) { + height: 320px; + } + + @media screen and (min-width: 1200px) { + height: 400px; + } +} + +.carousel--slider__third { + height: 189px; + + @media screen and (max-width: 640px) { + height: 320px; + } + + @media screen and (min-width: 1200px) { + height: 400px; + } +} + +.carousel--slider__first { + height: 189px; + + @media screen and (max-width: 640px) { + height: 320px; + } + + @media screen and (min-width: 1200px) { + height: 400px; + } +} + +.nav__button-n-t { + display: none; +} + +.ret { + display: none; +} + +@media screen and (min-width: 1200px) { + .cart__flex { + display: grid; + column-gap: 16px; + + grid-template-columns: repeat(16, 1fr); + } + + .cart__card { + grid-column: span 10; + } + + .cart__checkout { + grid-column: span 6; + } +} + +.dfg { + grid-column: span 10; +} + +.cart__card { + display: flex; + flex-direction: column; + padding: 16px; + box-sizing: border-box; + border: 1px solid rgb(226, 230, 233); + border-radius: 16px; + background: rgb(255, 255, 255); + + margin-bottom: 10px; + + @media screen and (min-width: 640px) { + flex-direction: row; + justify-content: space-between; + } + + &--second-div { + @media screen and (min-width: 640px) { + column-gap: 8px; + } + + @media screen and (min-width: 640px) { + column-gap: 16px; + } + } + + &-img { + height: 76px; + width: 66px; + } + + &-close { + cursor: pointer; + height: 16px; + width: 16px; + transition: all 0.3s; + } + + &-price { + color: rgb(15, 15, 17); + font-family: Mont, sans-serif; + font-size: 22px; + font-weight: 700; + width: 120px; + text-align: right; /* добавляем это свойство */ + } +} + +@media screen and (min-width: 640px) { + .ret { + display: block; + } + + .nav__button-n-t { + display: flex; + gap: 64px; + list-style: none; + color: rgb(137, 147, 154); + font-family: Mont, sans-serif; + font-size: 12px; + font-weight: 700; + + @media screen and (max-width: 1200px) { + gap: 32px; + } + + li { + cursor: pointer; + } + } +} + +.added-to-cart { + box-sizing: border-box; + color: rgb(66, 25, 208); + font-family: Mont, sans-serif; + font-size: 14px; + font-weight: 700; + border: 1px solid rgb(226, 230, 233); + border-radius: 48px; + width: 100%; + height: 40px; + transition: all 0.3s; + + &:hover { + cursor: pointer; + + box-shadow: 0 3px 13px 0 rgba(23, 32, 49, 0.4); + } +} + +.card__buy-cart:hover { + cursor: pointer; + + border-radius: 48px; + + box-shadow: 0 8px 13px 0 rgba(6, 11, 19, 0.4); + background: rgb(66, 25, 208); +} + +.cart__checkout { + align-self: baseline; + border: 1px solid rgb(226, 230, 233); + border-radius: 16px; + display: flex; + flex-direction: column; + align-items: center; + + padding: 24px; +} + +.cart__text { + margin: 0; +} + +.cart__line { + border: 1px solid rgb(226, 230, 233); + width: 100%; + margin-bottom: 16.5px; +} + +.page-home-card__favorite { + width: 40px; + height: 40px; + cursor: pointer; + + transition: all 0.3s; +} + +.cart__card-close:hover { + content: url('../img/Icons_Close (1).png'); +} + +.page-home-card__favorite:hover { + content: url('../img/Add to fovourites - Hover.svg'); + + /* Меняем изображение при наведении */ +} + +.card__image:hover { + cursor: pointer; +} + +.page-home { + &__list-new { + margin-bottom: 24px; + } + + &__new-models { + display: flex; + justify-content: space-between; + align-items: center; + gap: 72px; + + margin-bottom: 24px; + + &__text { + margin: 0; + } + + &--arrow { + display: flex; + column-gap: 16px; + } + } + + &-card { + border: 1px solid rgb(226, 230, 233); + border-radius: 8px; + + background: rgb(255, 255, 255); + padding: 32px; + + &__image { + display: block; + height: 110px; + + margin: 0 auto; + margin-bottom: 24px; + } + + &__name { + color: rgb(15, 15, 17); + font-size: 14px; + font-weight: 500; + + margin: 0 0 8px; + } + + &__price-regular { + color: rgb(15, 15, 17); + font-size: 22px; + font-weight: 700; + + margin: 0 0 8px; + } + + &__line { + border-top: 1px solid rgb(226, 230, 233); + width: 100%; + + margin: 0 0 16px; + } + + &__screen { + display: flex; + justify-content: space-between; + align-items: center; + + &-name { + color: rgb(137, 147, 154); + font-size: 12px; + font-weight: 700; + margin: 0; + margin-bottom: 8px; + } + + &-info { + margin: 0; + + color: rgb(15, 15, 17); + font-size: 12px; + font-weight: 700; + margin-bottom: 8px; + } + } + + &__capacity { + display: flex; + justify-content: space-between; + align-items: center; + + &-name { + color: rgb(137, 147, 154); + font-size: 12px; + font-weight: 700; + margin: 0; + margin-bottom: 8px; + } + + &-info { + color: rgb(15, 15, 17); + font-size: 12px; + font-weight: 700; + margin: 0; + margin-bottom: 8px; + } + } + + &__ram { + display: flex; + justify-content: space-between; + align-items: center; + + &-name { + color: rgb(137, 147, 154); + font-size: 12px; + font-weight: 700; + margin: 0; + margin-bottom: 16px; + } + + &-info { + color: rgb(15, 15, 17); + font-size: 12px; + font-weight: 700; + margin: 0; + margin-bottom: 16px; + } + } + + &__buy { + display: flex; + + column-gap: 8px; + + &-cart { + color: white; + background: rgb(66, 25, 208); + + border-radius: 48px; + border: 0; + + width: 100%; + height: 40px; + } + } + } +} + +.flex-capacity { + display: flex; + column-gap: 8px; + margin-bottom: 23.5px; +} + +.capacity { + width: 56px; + height: 32px; + border-radius: 4px; + background: rgb(15, 15, 17); + color: #fff; + display: flex; + justify-content: center; + align-items: center; + + &-default { + width: 56px; + height: 32px; + box-sizing: border-box; + border: 1px solid rgb(180, 189, 196); + border-radius: 4px; + display: flex; + justify-content: center; + align-items: center; + transition: all 0.3s; + + &:hover { + border: 1px solid rgb(62, 62, 63); + } + + &-text { + font-size: 14px; + font-weight: 500; + line-height: 21px; + } + } + + &-text { + font-size: 14px; + font-weight: 500; + line-height: 21px; + } +} + +.details { + &__text { + margin: 0; + margin-bottom: 32px; + } + + &__back { + display: flex; + align-items: center; + column-gap: 4px; + margin-bottom: 16px; + + &--text { + color: rgb(137, 147, 154); + font-size: 12px; + margin: 0; + line-height: 15px; + transition: all 0.3s; + + &:hover { + color: black; + } + } + } + + &__color { + cursor: pointer; + + border: 2px solid rgb(180, 189, 196); + transition: all 0.3s; + + &:hover { + border: 2px solid rgb(62, 62, 63); + } + + &--is-active { + border: 2px solid rgb(62, 62, 63); + } + } + + &__colors-container { + display: flex; + margin-bottom: 23.5px; + } + + &__image { + width: 100%; + margin-bottom: 16px; + display: flex; + justify-content: center; + + @media screen and (min-width: 640px) { + display: block; + } + + & img { + max-width: 100%; + max-height: 100%; + } + + &--more { + display: flex; + justify-content: center; + gap: 8px; + margin-bottom: 40px; + + @media screen and (min-width: 640px) { + flex-direction: column; + } + + @media screen and (min-width: 1200px) { + gap: 16px; + } + + &__wrapper { + border: 1px solid #ccc; // Цвет бордера + border-radius: 8px; // Скругление углов (по желанию) + width: 48px; // Ширина контейнера + height: 48px; // Высота контейнера + display: flex; // Центрирование изображения + justify-content: center; // Центрирование изображения + align-items: center; // Центрирование изображения + + cursor: pointer; + + @media screen and (min-width: 1200px) { + width: 70px; // Ширина контейнера + height: 70px; // Высота контейнера + } + } + + &__img { + width: auto; // Занимает всю ширину контейнера + height: 90%; // Высота автоматически + } + + &__wrapper.selected { + border: 1px solid #000; // Измените на желаемый цвет для выделенного изображения + } + } + } + + &-flex { + display: flex; + justify-content: space-between; + + &-text { + color: rgb(137, 147, 154); + margin: 0; + margin-bottom: 8px; + } + } +} + +@import './normolize'; +@import './components/Header/Header.module'; +@import './components/HomeCarousel/HomeCarousel.module'; +@import './components/Sorted/Sorted.module'; +@import './components/ShopByCategory/BrandNewModelsHome.module'; +@import './components/Footer/Footer.module'; +@import './components/ProductHome/ProductHome.module'; +@import './components/List/List.module'; +@import './components/BurgerMenu/BurgerMenu.module'; + +.b-s-d { + width: 50%; + display: flex; + justify-content: center; +} + +// .ertyu::before { +// content: ''; +// border: 1px solid rgb(226, 230, 233); +// position: absolute; +// } diff --git a/src/App.tsx b/src/App.tsx index 372e4b4206..cafbf6e41f 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,7 +1,23 @@ +import React, { useState } from 'react'; import './App.scss'; +import { Outlet } from 'react-router-dom'; +import { Header } from './components/Header'; +import { BurgerMenu } from './components/BurgerMenu'; +import { Footer } from './components/Footer/Footer'; -export const App = () => ( -
-

Product Catalog

-
-); +export const App = () => { + const [burgerMenu, setBurgerMenu] = useState(false); + + return ( +
+
+ + {/* Меню всегда в DOM, но его видимость контролируется классами */} + + + {/* Основной контент */} + +
+ ); +}; diff --git a/src/ContextStor.tsx b/src/ContextStor.tsx new file mode 100644 index 0000000000..a2e0ef16ef --- /dev/null +++ b/src/ContextStor.tsx @@ -0,0 +1,45 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import React, { createContext, useContext, ReactNode } from 'react'; +import { useLocalStorage } from './LocaleStorage'; +import { Products } from './types/products'; + +interface AppContextProps { + favorites: Products[]; + setFavorites: (favorites: Products[]) => void; + cart: Products[]; + setCart: (cart: Products[]) => void; +} + +const AppContext = createContext({ + favorites: [], + setFavorites: () => {}, + cart: [], + setCart: () => {}, +}); + +interface AppProviderProps { + children: ReactNode; +} + +export const AppProvider: React.FC = ({ children }) => { + const [favorites, setFavorites] = useLocalStorage( + 'favorites', + [], + ); + const [cart, setCart] = useLocalStorage('cart', []); + + return ( + + {children} + + ); +}; + +export const useAppContext = () => useContext(AppContext); diff --git a/src/LocaleStorage.ts b/src/LocaleStorage.ts new file mode 100644 index 0000000000..80715c5bac --- /dev/null +++ b/src/LocaleStorage.ts @@ -0,0 +1,29 @@ +import { useState } from 'react'; + +export function useLocalStorage( + key: string, + startValue: T, +): [T, (v: T) => void] { + const [value, setValue] = useState(() => { + const data = localStorage.getItem(key); + + if (data == null) { + return startValue; + } + + try { + return JSON.parse(data) as T; + } catch (e) { + localStorage.removeItem(key); + + return startValue; + } + }); + + const save = (newValue: T) => { + localStorage.setItem(key, JSON.stringify(newValue)); + setValue(newValue); + }; + + return [value, save]; +} diff --git a/src/QuantityContext.tsx b/src/QuantityContext.tsx new file mode 100644 index 0000000000..b5ea872c42 --- /dev/null +++ b/src/QuantityContext.tsx @@ -0,0 +1,41 @@ +import React, { createContext, useContext, useEffect, useState } from 'react'; +import { useAppContext } from './ContextStor'; + +type QuantityContextType = { + quantities: number[]; + setQuantities: React.Dispatch>; +}; + +const QuantityContext = createContext( + undefined, +); + +export const QuantityProvider: React.FC<{ children: React.ReactNode }> = ({ + children, +}) => { + const { cart } = useAppContext(); // Достаем `cart` из основного контекста + const [quantities, setQuantities] = useState( + cart.length > 0 ? cart.map(() => 1) : [], + ); + + useEffect(() => { + // При изменении `cart`, обновляем `quantities` с количеством `1` для новых элементов + setQuantities(cart.map(() => 1)); + }, [cart]); + + return ( + + {children} + + ); +}; + +export const useQuantityContext = () => { + const context = useContext(QuantityContext); + + if (!context) { + throw new Error('useQuantityContext must be used within QuantityProvider'); + } + + return context; +}; diff --git a/src/Root.tsx b/src/Root.tsx new file mode 100644 index 0000000000..1f56c2a901 --- /dev/null +++ b/src/Root.tsx @@ -0,0 +1,49 @@ +import React from 'react'; +import { Route, HashRouter as Router, Routes } from 'react-router-dom'; +import { App } from './App'; +import { HomePage } from './pages/HomePage'; +import { PhonesPage } from './pages/PhonesPage'; +import { AccessoriesPage } from './pages/AccessoriesPage'; +import { TabletsPage } from './pages/TabletsPage'; +import { CartItem } from './pages/CartItem'; +import { ProductDetailsPage } from './pages/ProductDetailsPage'; +import { Favorites } from './pages/Favorites'; +import { AppProvider } from './ContextStor'; +import { NotFoundPage } from './pages/NotFoundPageю'; +import { QuantityProvider } from './QuantityContext'; + +export const Root = () => { + return ( + + + + + }> + } /> + + + } /> + } /> + + + + } /> + } /> + + + + } /> + } /> + + + } /> + } /> + + + } /> + + + + + ); +}; diff --git a/src/components/BrandNewModelsHome/BrandNewModelsHome.module.scss b/src/components/BrandNewModelsHome/BrandNewModelsHome.module.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/components/BrandNewModelsHome/BrandNewModelsHome.tsx b/src/components/BrandNewModelsHome/BrandNewModelsHome.tsx new file mode 100644 index 0000000000..7d784e3b52 --- /dev/null +++ b/src/components/BrandNewModelsHome/BrandNewModelsHome.tsx @@ -0,0 +1,223 @@ +import React, { useEffect, useMemo, useState } from 'react'; +import { Navigation } from 'swiper/modules'; +import { Swiper, SwiperSlide } from 'swiper/react'; +import 'swiper/css'; +import 'swiper/css/navigation'; +import 'swiper/css/pagination'; +import 'swiper/css/scrollbar'; +import { Products } from '../../types/products'; +import { Link, useLocation } from 'react-router-dom'; +import classNames from 'classnames'; +import { useWindowResize } from '../../useWindowSize'; +import { useAppContext } from '../../ContextStor'; +import { useLocalStorage } from '../../LocaleStorage'; +import { useQuantityContext } from '../../QuantityContext'; + +type Props = { + type: 'Hot Prices' | 'Brand new models' | 'You may also like'; +}; + +export const BrandNewModelsHome: React.FC = ({ type }) => { + const [width] = useWindowResize(); + + const { favorites, cart, setCart, setFavorites } = useAppContext(); + const { quantities, setQuantities } = useQuantityContext(); + + const [models, setModels] = useState([]); + const [sortedModels, setSortedModels] = useState([]); + const location = useLocation(); + + function hotPrices(products: Products[]) { + return [...products].sort((a, b) => { + const discountA = a.fullPrice - a.price; + const discountB = b.fullPrice - b.price; + + return discountB - discountA; + }); + } + + const toggleFavorite = (product: Products) => { + const isFavorite = favorites.some(fav => fav.id === product.id); + + if (isFavorite) { + setFavorites(favorites.filter(fav => fav.id !== product.id)); + } else { + setFavorites([...favorites, product]); + } + }; + + const toogleCart = (product: Products) => { + const isCart = cart.some(el => el.id === product.id); + + if (isCart) { + setCart(cart.filter(el => el.id !== product.id)); + setQuantities(prevQuantities => + prevQuantities.filter((_, index) => cart[index].id !== product.id), + ); + } else { + setCart([...cart, product]); + setQuantities(prevQuantities => [...prevQuantities, 1]); + } + }; + + function brandNewModels(products: Products[]) { + return [...products].sort((a, b) => { + return new Date(b.year).getTime() - new Date(a.year).getTime(); + }); + } + + function alsoLike(products: Products[]) { + return products; + } + + const slidesPerView = useMemo(() => { + return width > 1200 + ? 4 + : width > 900 + ? 4 + : width > 800 + ? 3.5 + : width > 700 + ? 3 + : width > 600 + ? 2.5 + : width > 450 + ? 2 + : width > 400 + ? 1.6 + : 1.4; + }, [width]); + + useEffect(() => { + fetch('./api/products.json') + .then(response => response.json()) + .then(data => { + setModels(data); + }); + }, []); + + useEffect(() => { + let sorted; + + switch (type) { + case 'Hot Prices': + sorted = hotPrices(models); + break; + case 'Brand new models': + sorted = brandNewModels(models); + break; + case 'You may also like': + sorted = alsoLike(models); + break; + default: + sorted = models; + } + + setSortedModels(sorted.slice(0, 10)); + }, [type, models]); + + return ( + <> +
+

{type}

+ +
+ Disabled + Default +
+
+ +
+ {sortedModels.length > 0 && ( + + {sortedModels.map(product => ( + +
+ + {product.image} + + +

+ {product.name} +

+

{`${product.price}$`}

+ +
+ +
+

Screen

+

+ {product.screen} +

+
+ +
+

Capacity

+

+ {product.capacity} +

+
+ +
+

Ram

+

{product.ram}

+
+ +
+ + toggleFavorite(product)} + className="page-home-card__favorite" + src={ + favorites.some(fav => fav.id === product.id) + ? './img/Add to fovourites - Added.svg' + : './img/add-to-cart.svg' + } + alt="favorite" + /> +
+
+
+ ))} +
+ )} +
+ + ); +}; diff --git a/src/components/BrandNewModelsHome/index.ts b/src/components/BrandNewModelsHome/index.ts new file mode 100644 index 0000000000..1a65c83e23 --- /dev/null +++ b/src/components/BrandNewModelsHome/index.ts @@ -0,0 +1 @@ +export * from './BrandNewModelsHome'; diff --git a/src/components/BurgerMenu/BurgerMenu.module.scss b/src/components/BurgerMenu/BurgerMenu.module.scss new file mode 100644 index 0000000000..c5bffe9293 --- /dev/null +++ b/src/components/BurgerMenu/BurgerMenu.module.scss @@ -0,0 +1,87 @@ +.burger-menu { + position: fixed; // Меню поверх всего + top: 48px; // Убедись, что это соответствует высоте хедера + right: 0; + height: calc(100vh - 35px); // Учитываем высоту хедера + width: 100vw; + background-color: white; + z-index: 1000; + + transform: translateX(100%); + transition: transform 0.5s ease-in-out; + &--active { + transform: translateX(-3px); + + body { + overflow: hidden; + } + } + &__inner { + padding: 20px; + display: flex; + flex-direction: column; + justify-content: center; + } + + &--text { + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + gap: 20px; + + &-button { + position: relative; + color: rgb(137, 147, 154); + } + } + + &--button { + position: absolute; + bottom: 0; + left: 0; + width: 100%; + display: flex; + justify-content: space-around; /* Равномерное распределение */ + + &::before { + content: ''; + position: absolute; + top: 0; + + border: 1px solid rgb(226, 230, 233); + width: 100%; + } + + &::after { + content: ''; + box-sizing: border-box; + position: absolute; + top: 50%; + + border: 1px solid rgb(226, 230, 233); + width: 64px; + + transform: rotate(90deg); + } + } +} + + +.burger-menu--text-button--is-active::before { + content: ''; + position: absolute; + bottom: 0; + + border: 1px solid #313237; + width: 100%; +} + +.b-s-d--is-active::before { + content: ''; + position: absolute; + bottom: 10px; + + border: 2px solid #313237; + width: 47%; +} diff --git a/src/components/BurgerMenu/BurgerMenu.tsx b/src/components/BurgerMenu/BurgerMenu.tsx new file mode 100644 index 0000000000..1bd4245ef5 --- /dev/null +++ b/src/components/BurgerMenu/BurgerMenu.tsx @@ -0,0 +1,104 @@ +import classNames from 'classnames'; +import React, { useEffect } from 'react'; +import { NavLink, useLocation } from 'react-router-dom'; + +type Props = { + burgerMenu: boolean; + setBurgerMenu: React.Dispatch>; +}; + +export const BurgerMenu: React.FC = ({ burgerMenu, setBurgerMenu }) => { + const location = useLocation(); + + useEffect(() => { + if (burgerMenu) { + document.body.style.overflow = 'hidden'; + } else { + document.body.style.overflow = 'auto'; + } + + // Очистка эффекта + return () => { + document.body.style.overflow = 'auto'; // Восстанавливаем скролл при демонтировании + }; + }, [burgerMenu]); + + return ( +
+
+
+ + classNames('burger-menu--text-button', { + 'burger-menu--text-button--is-active': isActive, + }) + } + onClick={() => setBurgerMenu(false)} + to="/" + > + HOME + + + classNames('burger-menu--text-button', { + 'burger-menu--text-button--is-active': isActive, + }) + } + onClick={() => setBurgerMenu(false)} + to="/phones" + > + PHONES + + + classNames('burger-menu--text-button', { + 'burger-menu--text-button--is-active': isActive, + }) + } + onClick={() => setBurgerMenu(false)} + to="/tablets" + > + TABLETS + + + classNames('burger-menu--text-button', { + 'burger-menu--text-button--is-active': isActive, + }) + } + onClick={() => setBurgerMenu(false)} + to="/accessories" + > + ACCESSORIES + +
+ +
+ + classNames('b-s-d ertyu', { + 'b-s-d--is-active': isActive, + }) + } + onClick={() => setBurgerMenu(false)} + to="/favourites" + > + Favourites + + + classNames('b-s-d', { + 'b-s-d--is-active': isActive, + }) + } + state={{ from: location.pathname }} + onClick={() => setBurgerMenu(false)} + to="/cart" + > + Cart + +
+
+
+ ); +}; diff --git a/src/components/BurgerMenu/index.ts b/src/components/BurgerMenu/index.ts new file mode 100644 index 0000000000..cc518a4328 --- /dev/null +++ b/src/components/BurgerMenu/index.ts @@ -0,0 +1 @@ +export * from './BurgerMenu'; diff --git a/src/components/DetailsCard/DetailsCard.module.scss b/src/components/DetailsCard/DetailsCard.module.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/components/DetailsCard/DetailsCard.tsx b/src/components/DetailsCard/DetailsCard.tsx new file mode 100644 index 0000000000..8b99d31a47 --- /dev/null +++ b/src/components/DetailsCard/DetailsCard.tsx @@ -0,0 +1,206 @@ +import React, { useEffect, useState } from 'react'; +import { Phone } from '../../types/phone'; +import { Tablet } from '../../types/tablet'; +import { Accessory } from '../../types/accessory'; +import classNames from 'classnames'; +import { NavLink, useNavigate } from 'react-router-dom'; +import { Products } from '../../types/products'; +import { COLORS } from '../../variables'; +import { useAppContext } from '../../ContextStor'; +import { useLocalStorage } from '../../LocaleStorage'; +import { useQuantityContext } from '../../QuantityContext'; + +type Product = Phone | Tablet | Accessory; + +type Props = { + product: Product; +}; + +export const DetailsCard: React.FC = ({ product }) => { + const { favorites, cart, setCart, setFavorites } = useAppContext(); + const { quantities, setQuantities } = useQuantityContext(); + + const navigate = useNavigate(); + const [selectedImage, setSelectedImage] = useState(product.images[0]); + const [products, setProducts] = useState([]); + + const handleMemoryChange = (newCapacity: string) => { + let updatedURL; + + if (product.category === 'accessories') { + updatedURL = `/${product.category}/${product.namespaceId}-${newCapacity}-${product.color.replace(/\s+/g, '-')}`; + } else { + updatedURL = `/${product.category}/${product.namespaceId}-${newCapacity}-${product.color}`; + } + + navigate(updatedURL); + }; + + useEffect(() => { + fetch('./api/products.json') + .then(response => response.json()) + .then(data => setProducts(data)); + }, [product]); + + const toogleFavoritesOfDetails = (productId: string) => { + const productToAdd = products.find(el => el.itemId === productId); + + const isInCart = favorites.some(el => el.id === productToAdd?.id); + + if (isInCart) { + // Если товар уже в корзине, удаляем его + setFavorites(favorites.filter(el => el.id !== productToAdd?.id)); + } else if (productToAdd) { + // Если товара нет в корзине, добавляем его + setFavorites([...favorites, productToAdd]); + } + }; + + const toogleCartOfDetails = (productId: string) => { + const productToAdd = products.find(el => el.itemId === productId); + + const isInCart = cart.some(el => el.id === productToAdd?.id); + + if (isInCart) { + // Если товар уже в корзине, удаляем его + setCart(cart.filter(el => el.id !== productToAdd?.id)); + setQuantities(prevQuantities => + prevQuantities.filter((_, index) => cart[index].id !== product.id), + ); + } else if (productToAdd) { + // Если товара нет в корзине, добавляем его + setCart([...cart, productToAdd]); + setQuantities(prevQuantities => [...prevQuantities, 1]); + } + }; + + return ( + <> +

{product.name}

+ +
+
+ {product.images.map(img => ( +
{ + setSelectedImage(img); + }} + className={`details__image--more__wrapper ${selectedImage === img ? 'selected' : ''}`} + key={`${img}-${product.id}`} + > + image +
+ ))} +
+ +
+ image +
+ +
+
+

Aviables colors

+
+ +
+ {product.colorsAvailable.map(color => ( + + classNames({ + details__color: !isActive, + 'details__color--is-active': isActive, + }) + } + style={{ + backgroundColor: COLORS[color as keyof typeof COLORS], + width: '32px', + height: '32px', + borderRadius: '50%', + marginRight: '4px', + }} + /> + ))} +
+ +
+ +

Select capacity

+ +
+ {product.capacityAvailable.map(el => ( +
handleMemoryChange(el.toLowerCase())} + className={classNames({ + 'capacity-default': el !== product.capacity, + capacity: el === product.capacity, + })} + > +
{el}
+
+ ))} +
+ +
+ +

{`${product.priceDiscount}$`}

+ +
+ + { + toogleFavoritesOfDetails(product.id); + }} + className="page-home-card__favorite" + src={ + favorites.some(fav => fav.itemId === product.id) + ? './img/Add to fovourites - Added.svg' + : './img/add-to-cart.svg' + } + alt="favorite" + /> +
+ +
+
+

Screen

+

{product.screen}

+
+ +
+

Resolution

+

{product.resolution}

+
+ +
+

Processor

+

{product.processor}

+
+ +
+

RAM

+

{product.ram}

+
+
+
+
+ + ); +}; diff --git a/src/components/DetailsCard/index.ts b/src/components/DetailsCard/index.ts new file mode 100644 index 0000000000..a3db26c802 --- /dev/null +++ b/src/components/DetailsCard/index.ts @@ -0,0 +1 @@ +export * from './DetailsCard'; diff --git a/src/components/DetailsHome/DetailsBack.module.scss b/src/components/DetailsHome/DetailsBack.module.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/components/DetailsHome/DetailsBack.tsx b/src/components/DetailsHome/DetailsBack.tsx new file mode 100644 index 0000000000..38aece2fb3 --- /dev/null +++ b/src/components/DetailsHome/DetailsBack.tsx @@ -0,0 +1,77 @@ +import React from 'react'; +import { Link, useLocation } from 'react-router-dom'; +import { Phone } from '../../types/phone'; +import { Tablet } from '../../types/tablet'; +import { Accessory } from '../../types/accessory'; + +type Product = Phone | Tablet | Accessory; + +type Props = { + product: Product; +}; + +export const DetailsBack: React.FC = ({ product }) => { + const location = useLocation(); + + return ( + <> +
+ + Home + + Chevron + +

+ {location.pathname.includes('/tablets') + ? 'Tablets' + : location.pathname.includes('/phones') + ? 'Mobile phones' + : location.pathname.includes('/accessories') + ? 'Accessories' + : ''} +

+ + + Chevron + +
{product.name}
+
+ + +
+ Back +

Back

+
+ + + ); +}; diff --git a/src/components/DetailsHome/index.ts b/src/components/DetailsHome/index.ts new file mode 100644 index 0000000000..b5c53e7f96 --- /dev/null +++ b/src/components/DetailsHome/index.ts @@ -0,0 +1 @@ +export * from './DetailsBack'; diff --git a/src/components/DetailsTechSpecs/DetailsTechSpecs.module.scss b/src/components/DetailsTechSpecs/DetailsTechSpecs.module.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/components/DetailsTechSpecs/DetailsTechSpecs.tsx b/src/components/DetailsTechSpecs/DetailsTechSpecs.tsx new file mode 100644 index 0000000000..b632483287 --- /dev/null +++ b/src/components/DetailsTechSpecs/DetailsTechSpecs.tsx @@ -0,0 +1,112 @@ +import React from 'react'; +import { Phone } from '../../types/phone'; +import { Tablet } from '../../types/tablet'; +import { Accessory } from '../../types/accessory'; + +type Product = Phone | Tablet | Accessory; + +type Props = { + product: Product; +}; + +export const DetailsTechSpecs: React.FC = ({ product }) => { + return ( + <> +
+
+

About

+
+ +

And then there was pro

+

+ A transformative triple‑camera system that adds tons of capability + without complexity. +

+

+ An unprecedented leap in battery life. And a mind‑blowing chip that + doubles down on machine learning and pushes the boundaries of what a + smartphone can do. Welcome to the first iPhone powerful enough to be + called Pro. +

+ +

Camera

+

+ Meet the first triple‑camera system to combine cutting‑edge + technology with the legendary simplicity of iPhone. Capture up to + four times more scene. Get beautiful images in drastically lower + light. Shoot the highest‑quality video in a smartphone — then edit + with the same tools you love for photos. You’ve never shot with + anything like it. +

+ +

+ Shoot it. Flip it. Zoom it. Crop it. Cut it. Light it. Tweak it. + Love it. +

+

+ iPhone 11 Pro lets you capture videos that are beautifully true to + life, with greater detail and smoother motion. Epic processing power + means it can shoot 4K video with extended dynamic range and + cinematic video stabilization — all at 60 fps. You get more creative + control, too, with four times more scene and powerful new editing + tools to play with. +

+
+ +
+

Tech specs

+
+ +
+

Screen

+

{product.screen}

+
+ +
+

Resolution

+

{product.resolution}

+
+ +
+

Processor

+

{product.processor}

+
+ +
+

RAM

+

{product.ram}

+
+ +
+

Built in memory

+

{product.capacity}

+
+ +
+

Camera

+

{product.camera}

+
+ +
+

Zoom

+

{product.zoom}

+
+ +
+

Cell

+
+ {product.cell.map((el, index) => ( +

+ {el} +

+ ))} +
+
+
+
+ + ); +}; diff --git a/src/components/DetailsTechSpecs/index.ts b/src/components/DetailsTechSpecs/index.ts new file mode 100644 index 0000000000..5fdec93161 --- /dev/null +++ b/src/components/DetailsTechSpecs/index.ts @@ -0,0 +1 @@ +export * from './DetailsTechSpecs'; diff --git a/src/components/Footer/Footer.module.scss b/src/components/Footer/Footer.module.scss new file mode 100644 index 0000000000..6aa94f2b83 --- /dev/null +++ b/src/components/Footer/Footer.module.scss @@ -0,0 +1,99 @@ +@media screen and (min-width: 640px) { + .footer { + height: 96px; + } + + .footer__flex { + display: flex; + justify-content: space-between; + align-items: center; + } + + .footer__about-us { + flex-direction: row; + column-gap: 15px; + } + + .footer__logo { + margin: 0; + } +} + +@media screen and (min-width: 1200px) { + .footer__about-us { + flex-direction: row; + column-gap: 105px; + } +} + +@media screen and (max-width: 640px) { + .footer__about-us { + display: flex; + flex-direction: column; + column-gap: 8px; + margin-bottom: 32px; + } + + .footer__logo { + margin: 0; + margin-bottom: 32px; + + } +} +.footer { + position: relative; + box-sizing: border-box; + margin-top: 64px; + padding: 32px 16px; + + &__line { + border-top: 1px solid rgb(226, 230, 233); + width:100%; + + position: absolute; + top: 0; // Позиционирование линии сверху + left: 0; // Привязываем к левому краю + + margin: 0 0 16px; + } + + &__about-us { + color: #89939A; + + display: flex; + } + + &__logo { + width: 89px; + height: 32px; + } + + &__text { + margin: 0; + cursor: pointer; + color: #89939A; + transition: all 0.3s; + + + &:hover { + color: #2d2d2e; + + } + } + + &__slide { + display: flex; + justify-content: center; + align-items: center; + column-gap: 16px; + + &-text { + margin: 0; + cursor: pointer; + } + + &-button { + cursor: pointer; + } + } +} diff --git a/src/components/Footer/Footer.tsx b/src/components/Footer/Footer.tsx new file mode 100644 index 0000000000..9c60d98d2c --- /dev/null +++ b/src/components/Footer/Footer.tsx @@ -0,0 +1,52 @@ +import React from 'react'; + +export const Footer = () => { + return ( +
+
+ +
+ Logo + + + +
+

window.scrollTo({ top: 0, behavior: 'smooth' })} + className="footer__slide-text" + > + Back to top +

+ window.scrollTo({ top: 0, behavior: 'smooth' })} + className="footer__slide-button" + src="./img/Footer-slide.svg" + alt="Footer-slide" + /> +
+
+
+ ); +}; diff --git a/src/components/Footer/index.ts b/src/components/Footer/index.ts new file mode 100644 index 0000000000..ddcc5a9cd1 --- /dev/null +++ b/src/components/Footer/index.ts @@ -0,0 +1 @@ +export * from './Footer'; diff --git a/src/components/Header/Header.module.scss b/src/components/Header/Header.module.scss new file mode 100644 index 0000000000..4b4259de2e --- /dev/null +++ b/src/components/Header/Header.module.scss @@ -0,0 +1,155 @@ +header { + background-color: #fff; + z-index: 1000; + position: sticky; + top: 0; +} + +.nav { + display: flex; + justify-content: space-between; + align-items: center; + height: 48px; + margin-top: -13px; + margin-bottom: 24px; + position: relative; + + &::before { + content: ''; + position: absolute; + bottom: 0; + + border: 1px solid rgb(226, 230, 233); + width: 100%; + right: 0; + + } + + &-item { + color: rgb(137, 147, 154); + font-family: Mont, sans-serif; + font-size: 12px; + font-weight: 700; + position: relative; + transition: all 0.3s; + + &::before { + content: ''; + position: absolute; + bottom: -20px; + left: 50%; + width: 0; // Початкове значення ширини + border: 1px solid rgb(15, 15, 17); + opacity: 0; // Початкове значення opacity + transform: translateX(-50%); // Сдвигаем элемент влево на 50% ширини + transition: width 0.3s ease, opacity 0.3s ease; // Плавна анімація width і opacity + } + + &:hover { + &::before { + width: 100%; // Коли наведено, ширина 100% + opacity: 1; // Коли наведено + } + } + + &-is-active { + color: rgb(0, 0, 0); + position: relative; + + &::before { + content: ''; + position: absolute; + bottom: -20px; + width: 100%; + border: 1px solid rgb(15, 15, 17); + + @media screen and (max-width: 1200px) { + bottom: -13px; + } + + } + } + } + + @media screen and (min-width: 1200px) { + height: 64px; + } + + &__logo { + width: 64px; + height: 22px; + padding-left: 16px; + + + @media screen and (min-width: 1200px) { + height: 28px; + width: 80px; + } + } + + &__menu { + width: 16px; + height: 16px; + } + &__menu-container { + width: 16px; + height: 16px; + position: relative; + transition: all 0.3s; + padding-right: 16px; + + + &::before { + content: ""; + position: absolute; + left: calc(-100% - 8px); + bottom: 50%; + border: 1px solid rgb(226, 230, 233); + width: 48px; + + transform: rotate(90deg); + } + + + &:hover { + cursor: pointer; + } + + @media screen and (min-width: 640px) { + display: none; + } + } + + &__button { + display: none; + + @media screen and (min-width: 640px) { + display: block; + display: flex; + } + + &--first { + @media screen and (min-width: 640px) { + height: 48px; + width: 48px; + } + + @media screen and (min-width: 1200px) { + height: 65px; + width: 65px; + } + } + + &--second { + @media screen and (min-width: 640px) { + height: 48px; + width: 48px; + } + + @media screen and (min-width: 1200px) { + height: 65px; + width: 65px; + } + } + } +} diff --git a/src/components/Header/Header.tsx b/src/components/Header/Header.tsx new file mode 100644 index 0000000000..d25756f739 --- /dev/null +++ b/src/components/Header/Header.tsx @@ -0,0 +1,123 @@ +import classNames from 'classnames'; +import React from 'react'; +import { Link, NavLink, useLocation } from 'react-router-dom'; +import { useCartAndFavorites } from '../../hooks/useCartAndFavorites'; +import { useAppContext } from '../../ContextStor'; +import { useLocalStorage } from '../../LocaleStorage'; +import { useQuantityContext } from '../../QuantityContext'; + +type Props = { + burgerMenu: boolean; + setBurgerMenu: React.Dispatch>; +}; + +export const Header: React.FC = ({ burgerMenu, setBurgerMenu }) => { + const location = useLocation(); + const { cart } = useAppContext(); + const { quantities, setQuantities } = useQuantityContext(); + + + const totalItems = () => { + return quantities.reduce((total, qty) => total + qty, 0); + }; + + const { favoritesCount, cartCount } = useCartAndFavorites(); + + return ( +
+
+ + Logo + +
+ setBurgerMenu(!burgerMenu)} + className="nav__menu" + src="./img/Menu.png" + alt="Menu" + /> +
+ +
    + + classNames({ + 'nav-item-is-active': isActive, + 'nav-item': !isActive, + }) + } + to="/" + > + HOME + + + classNames({ + 'nav-item-is-active': isActive, + 'nav-item': !isActive, + }) + } + to="/phones" + > + PHONES + + + classNames({ + 'nav-item-is-active': isActive, + 'nav-item': !isActive, + }) + } + to="/tablets" + > + TABLETS + + + classNames({ + 'nav-item-is-active': isActive, + 'nav-item': !isActive, + }) + } + to="/accessories" + > + ACCESSORIES + +
+ +
+
+
+
+ +
+ + Favourites + {favoritesCount > 0 && ( +
{favoritesCount}
+ )} + + + + Cart + {cartCount > 0 && ( +
{totalItems()}
+ )} + +
+
+
+ ); +}; diff --git a/src/components/Header/index.ts b/src/components/Header/index.ts new file mode 100644 index 0000000000..266dec8a1b --- /dev/null +++ b/src/components/Header/index.ts @@ -0,0 +1 @@ +export * from './Header'; diff --git a/src/components/HomeCarousel/HomeCarousel.module.scss b/src/components/HomeCarousel/HomeCarousel.module.scss new file mode 100644 index 0000000000..c8b5511e7a --- /dev/null +++ b/src/components/HomeCarousel/HomeCarousel.module.scss @@ -0,0 +1,89 @@ +.carousel { + &--text { + margin: 0; + margin-bottom: 24px; + } + + &--slider { + max-width: 1200px; + width: 100vw; + margin-bottom: 20px; + + @media screen and (min-width: 640px) { + column-gap: 16px; + + width: 100%; + transform: translateX(0); + + display: flex; + } + + @media screen and (min-width: 1200px) { + margin-bottom: 80px; + } + + &-banner { + display: none; + + @media screen and (min-width: 640px) { + display: block; + max-height: 400px; + width: 100%; + + } + } + + &__first { + width: 100%; + + @media screen and (min-width: 640px) { + display: none; + } + } + + &__second { + width: 100%; + + @media screen and (min-width: 640px) { + object-fit: cover; + } + } + + &__third { + width: 100%; + + @media screen and (min-width: 640px) { + object-fit: cover; + } + } + + &--first-button { + width: 32px; + + display: none; + cursor: pointer; + + @media screen and (min-width: 640px) { + display: block; + } + + @media screen and (min-width: 1200px) { + content: url('../img/Slider button - Default (right).svg'); + } + } + + &--second-button { + width: 32px; + display: none; + cursor: pointer; + + @media screen and (min-width: 640px) { + display: block; + } + + @media screen and (min-width: 1200px) { + content: url('../img/Slider button - Default (right).png'); + } + } + } +} diff --git a/src/components/HomeCarousel/HomeCarousel.tsx b/src/components/HomeCarousel/HomeCarousel.tsx new file mode 100644 index 0000000000..bc0096fcce --- /dev/null +++ b/src/components/HomeCarousel/HomeCarousel.tsx @@ -0,0 +1,81 @@ +import React, { useMemo } from 'react'; + +import { Pagination, A11y, Autoplay, Navigation } from 'swiper/modules'; +import { Swiper, SwiperSlide } from 'swiper/react'; +import 'swiper/css'; +import 'swiper/css/navigation'; +import 'swiper/css/pagination'; +import 'swiper/css/scrollbar'; +import 'swiper/css/autoplay'; +import './pagination.scss'; +import { useWindowResize } from '../../useWindowSize'; + +export const HomeCarousel = () => { + const [width] = useWindowResize(); + const allowTouchMove = useMemo(() => { + return width < 640; + }, [width]); + const swiperKey = useMemo(() => `swiper-${width}`, [width]); + + return ( + <> +

Welcome to Nice Gadgets store!

+ +
+
+ button + + + Menu + + + + Menu + + + + Menu + + + button +
+
+ + ); +}; diff --git a/src/components/HomeCarousel/index.ts b/src/components/HomeCarousel/index.ts new file mode 100644 index 0000000000..af5fd95b62 --- /dev/null +++ b/src/components/HomeCarousel/index.ts @@ -0,0 +1 @@ +export * from './HomeCarousel'; diff --git a/src/components/HomeCarousel/pagination.scss b/src/components/HomeCarousel/pagination.scss new file mode 100644 index 0000000000..3d3df15035 --- /dev/null +++ b/src/components/HomeCarousel/pagination.scss @@ -0,0 +1,15 @@ +.swiper-pagination { + transform: translateY(0%); +} + +.swiper-pagination-bullet { + background: rgb(226, 230, 233); + border-radius: 0; + width: 14px; + height: 4px; + opacity: 1; +} + +.swiper-pagination-bullet-active { + background: rgb(15, 15, 17); +} diff --git a/src/components/List/List.module.scss b/src/components/List/List.module.scss new file mode 100644 index 0000000000..d71629cbf4 --- /dev/null +++ b/src/components/List/List.module.scss @@ -0,0 +1,148 @@ +.card { + border: 1px solid rgb(226, 230, 233); + border-radius: 8px; + + background: rgb(255, 255, 255); + padding: 32px; + width: 240px; + margin: 0 auto; + margin-bottom: 10px; + + &__grid { + display: grid; + grid-template-columns: repeat(1, 1fr); + + @media screen and (min-width: 640px) { + grid-template-columns: repeat(2, 1fr); + gap: 40px 16px; + } + + @media screen and (min-width: 1200px) { + grid-template-columns: repeat(4, 1fr); + } + } + + &__image { + height: 200px; + + display: block; + margin: 0 auto; + + margin-bottom: 24px; + + @media screen and (min-width: 1200px) { + transition: all 0.3s; + + &:hover { + transform: scale(1.05); + } + } + } + + &__name { + color: rgb(15, 15, 17); + font-size: 14px; + font-weight: 500; + + margin: 0 0 8px; + } + + &__price-regular { + color: rgb(15, 15, 17); + font-size: 22px; + font-weight: 700; + + margin: 0 0 8px; + } + + &__line { + border-top: 1px solid rgb(226, 230, 233); + width: 100%; + + margin: 0 0 16px; + } + + &__screen { + display: flex; + justify-content: space-between; + align-items: center; + + &-name { + color: rgb(137, 147, 154); + font-size: 12px; + font-weight: 700; + margin: 0; + margin-bottom: 8px; + } + + &-info { + margin: 0; + + color: rgb(15, 15, 17); + font-size: 12px; + font-weight: 700; + margin-bottom: 8px; + } + } + + &__capacity { + display: flex; + justify-content: space-between; + align-items: center; + + &-name { + color: rgb(137, 147, 154); + font-size: 12px; + font-weight: 700; + margin: 0; + margin-bottom: 8px; + } + + &-info { + color: rgb(15, 15, 17); + font-size: 12px; + font-weight: 700; + margin: 0; + margin-bottom: 8px; + } + } + + &__ram { + display: flex; + justify-content: space-between; + align-items: center; + + &-name { + color: rgb(137, 147, 154); + font-size: 12px; + font-weight: 700; + margin: 0; + margin-bottom: 16px; + } + + &-info { + color: rgb(15, 15, 17); + font-size: 12px; + font-weight: 700; + margin: 0; + margin-bottom: 16px; + } + } + + &__buy { + display: flex; + + column-gap: 8px; + + &-cart { + color: white; + background: rgb(66, 25, 208); + + border-radius: 48px; + border: 0; + + width: 100%; + height: 40px; + } + } +} diff --git a/src/components/List/List.tsx b/src/components/List/List.tsx new file mode 100644 index 0000000000..e5f6831285 --- /dev/null +++ b/src/components/List/List.tsx @@ -0,0 +1,207 @@ +import React, { useEffect, useState } from 'react'; +import { Link, useLocation, useSearchParams } from 'react-router-dom'; +import { Tablet } from '../../types/tablet'; +import { Accessory } from '../../types/accessory'; +import { Phone } from '../../types/phone'; +import { Products } from '../../types/products'; +import { useAppContext } from '../../ContextStor'; +import { useLocalStorage } from '../../LocaleStorage'; +import { useQuantityContext } from '../../QuantityContext'; + +type Props = { + products: Phone[] | Tablet[] | Accessory[]; + type: 'phones' | 'tablets' | 'accessories'; +}; + +export const List: React.FC = ({ type }) => { + const { favorites, cart, setCart, setFavorites } = useAppContext(); + const { quantities, setQuantities } = useQuantityContext(); + + const location = useLocation(); + const [searchParams] = useSearchParams(location.search); + const [products, setProducts] = useState([]); + const [sortedProducts, setSortedProducts] = useState([]); + + const [currentPage, setCurrentPage] = useState(1); + const [itemsPerPage, setItemsPerPage] = useState(4); + const totalPages = Math.ceil(products.length / itemsPerPage); + + const visiblePageCount = 3; + + const startPage = Math.max(currentPage - Math.floor(visiblePageCount / 2), 1); + const endPage = Math.min(startPage + visiblePageCount - 1, totalPages); + + useEffect(() => { + fetch('./api/products.json') + .then(response => response.json()) + .then((data: Products[]) => { + const filteredProducts = data.filter(el => el.category === type); + + setProducts(filteredProducts); + setSortedProducts(filteredProducts); + }); + }, [type]); + + useEffect(() => { + let sortedData = [...products]; + + const sortParam = searchParams.get('sort') || 'Default'; + + switch (sortParam) { + case 'Alphabetically': + sortedData = sortedData.sort((one, two) => + one.name.localeCompare(two.name), + ); + break; + + case 'Cheapest': + sortedData = sortedData.sort((one, two) => one.price - two.price); + break; + + case 'Default': + sortedData = sortedData.sort((one, two) => two.year - one.year); + break; + } + + const itemsOnPage = searchParams.get('perPage'); + + setItemsPerPage(itemsOnPage ? Number(itemsOnPage) : products.length); // Показываем все, если perPage нет + + setCurrentPage(1); + + const paginatedData = sortedData.slice(0, itemsPerPage); + + setSortedProducts(paginatedData); + }, [products, searchParams, itemsPerPage]); + + useEffect(() => { + // Когда меняется текущая страница, обновляем отображаемые продукты + const paginatedData = products.slice( + (currentPage - 1) * itemsPerPage, + currentPage * itemsPerPage, + ); + + setSortedProducts(paginatedData); + }, [currentPage, products, itemsPerPage]); + + const toggleFavorite = (product: Products) => { + const isFavorite = favorites.some(fav => fav.id === product.id); + + if (isFavorite) { + setFavorites(favorites.filter(fav => fav.id !== product.id)); + } else { + setFavorites([...favorites, product]); + } + }; + + const toggleCart = (product: Products) => { + const isCart = cart.some(el => el.id === product.id); + + if (isCart) { + setCart(cart.filter(el => el.id !== product.id)); + setQuantities(prevQuantities => + prevQuantities.filter((_, index) => cart[index].id !== product.id), + ); + } else { + setCart([...cart, product]); + setQuantities(prevQuantities => [...prevQuantities, 1]); + } + }; + + const isPaginationEnabled = searchParams.has('perPage'); + + return ( + <> +
    + {sortedProducts.length > 0 && + sortedProducts.map(product => ( +
    + + card-image + +

    {product.name}

    +

    {`${product.price}$`}

    + +
    + +
    +

    Screen

    +

    {product.screen}

    +
    + +
    +

    Capacity

    +

    {product.capacity}

    +
    + +
    +

    Ram

    +

    {product.ram}

    +
    + +
    + + toggleFavorite(product)} + className="page-home-card__favorite" + src={ + favorites.some(fav => fav.id === product.id) + ? './img/Add to fovourites - Added.svg' + : './img/add-to-cart.svg' + } + alt="favorite" + /> +
    +
    + ))} +
+ + {isPaginationEnabled && itemsPerPage && ( +
+ + + {Array.from({ length: endPage - startPage + 1 }, (_, index) => { + const page = startPage + index; + + return ( + + ); + })} + + +
+ )} + + ); +}; diff --git a/src/components/List/index.ts b/src/components/List/index.ts new file mode 100644 index 0000000000..4994c18137 --- /dev/null +++ b/src/components/List/index.ts @@ -0,0 +1 @@ +export * from './List'; diff --git a/src/components/ProductHome/ProductHome.module.scss b/src/components/ProductHome/ProductHome.module.scss new file mode 100644 index 0000000000..97e96979dd --- /dev/null +++ b/src/components/ProductHome/ProductHome.module.scss @@ -0,0 +1,29 @@ +.page-phones { + display: flex; + column-gap: 8px; + margin-bottom: 24px; + + &__house { + height: 16px; + width: 16px; + } + + &__arrow { + height: 16px; + width: 16px; + } + + &__catygory-text { + margin: 0; + } + + &__main-text { + margin: 0; + margin-bottom: 8px; + } + + &__conter-models { + margin: 0; + margin-bottom: 16px; + } +} diff --git a/src/components/ProductHome/ProductHome.tsx b/src/components/ProductHome/ProductHome.tsx new file mode 100644 index 0000000000..66d5265de6 --- /dev/null +++ b/src/components/ProductHome/ProductHome.tsx @@ -0,0 +1,51 @@ +import React from 'react'; +import { Link, useLocation } from 'react-router-dom'; +import { Phone } from '../../types/phone'; +import { Tablet } from '../../types/tablet'; +import { Accessory } from '../../types/accessory'; +import { Sorted } from '../Sorted'; + +type Props = { + product: Phone[] | Tablet[] | Accessory[]; +}; + +export const ProductHome: React.FC = ({ product }) => { + const location = useLocation(); + + return ( + <> +
+ + Home + + Chevron +

+ {location.pathname.includes('/tablets') + ? 'Tablets' + : location.pathname.includes('/phones') + ? 'Mobile phones' + : location.pathname.includes('/accessories') + ? 'Accessories' + : ''} +

+
+ +

+ {location.pathname.includes('/tablets') + ? 'Tablets' + : location.pathname.includes('/phones') + ? 'Mobile phones' + : location.pathname.includes('/accessories') + ? 'Accessories' + : ''} +

+

{product.length} models

+ + + + ); +}; diff --git a/src/components/ProductHome/index.ts b/src/components/ProductHome/index.ts new file mode 100644 index 0000000000..1e7a1630f7 --- /dev/null +++ b/src/components/ProductHome/index.ts @@ -0,0 +1 @@ +export * from './ProductHome'; diff --git a/src/components/ShopByCategory/BrandNewModelsHome.module.scss b/src/components/ShopByCategory/BrandNewModelsHome.module.scss new file mode 100644 index 0000000000..cd5a8806a2 --- /dev/null +++ b/src/components/ShopByCategory/BrandNewModelsHome.module.scss @@ -0,0 +1,28 @@ +.category { + + &--text { + font-size: 22px; + + @media screen and (min-width: 640px) { + font-size: 32px; + } + } + + &__type { + @media screen and (min-width: 640px) { + display: flex; + + column-gap: 16px; + } + } + + &-home__img { + width: 100%; + + transition: all 0.3s; + + &:hover { + transform: scale(1.02); + } + } +} diff --git a/src/components/ShopByCategory/ShopByCategory.tsx b/src/components/ShopByCategory/ShopByCategory.tsx new file mode 100644 index 0000000000..1817540f71 --- /dev/null +++ b/src/components/ShopByCategory/ShopByCategory.tsx @@ -0,0 +1,38 @@ +import React from 'react'; +import { Link } from 'react-router-dom'; + +export const ShopByCategory = () => { + return ( + <> +

Shop by category

+ +
+
+ + + +

Mobile phones

+

95 models

+
+ +
+ + + + +

Tablets

+

24 models

+
+ +
+ + + + +

Accessories

+

100 models

+
+
+ + ); +}; diff --git a/src/components/ShopByCategory/index.ts b/src/components/ShopByCategory/index.ts new file mode 100644 index 0000000000..8081526324 --- /dev/null +++ b/src/components/ShopByCategory/index.ts @@ -0,0 +1 @@ +export * from './ShopByCategory'; diff --git a/src/components/Sorted/Sorted.module.scss b/src/components/Sorted/Sorted.module.scss new file mode 100644 index 0000000000..a96a62e6a5 --- /dev/null +++ b/src/components/Sorted/Sorted.module.scss @@ -0,0 +1,100 @@ +.sort-list { + display: grid; + grid-template-columns: repeat(4, 1fr); + column-gap: 16px; + margin-bottom: 24px; + + color: #89939A; + + @media screen and (min-width: 640px) { + grid-template-columns: repeat(12, 1fr); + } + + @media screen and (min-width: 1200px) { + grid-template-columns: repeat(16, 1fr); + } + + &--select { + box-sizing: border-box; + + position: absolute; + width: 100%; + background-color: #fff; + border: 1px solid rgb(180, 189, 196); + + border-radius: 10px; + padding: 10px 12px; + display: flex; + flex-direction: column; + row-gap: 3px; + color: #000; + + z-index: 1000; + + &-text { + margin: 0; + cursor: pointer; + transition: all 0.3s; + + + &:hover { + background-color: #ebe3e3; + } + } + } + + &__by { + position: relative; + grid-column: span 2; + + @media screen and (min-width: 640px) { + grid-column: span 4; + } + + } + + &__items-page { + position: relative; + + grid-column: span 2; + + @media screen and (min-width: 640px) { + grid-column: span 3; + } + + &--button { + position: relative; + cursor: pointer; + + box-sizing: border-box; + + padding-left: 12px; + margin-bottom: 8px; + + display: flex; + align-items: center; + + height: 40px; + width: 100%; + + border: 1px solid rgb(180, 189, 196); + color: #000; + + border-radius: 8px; + transition: all 0.3s; + + + &:hover { + border: 1px solid rgb(64, 65, 67); + } + + &::before { + content: url('../img/Icons_Chevron (Arrow Down).svg'); + position: absolute; + right: 12px; + height: 16px; + width: 16px; + } + } + } +} diff --git a/src/components/Sorted/Sorted.tsx b/src/components/Sorted/Sorted.tsx new file mode 100644 index 0000000000..867f98254f --- /dev/null +++ b/src/components/Sorted/Sorted.tsx @@ -0,0 +1,126 @@ +import React, { useState } from 'react'; +import { useLocation, useSearchParams } from 'react-router-dom'; + +export const Sorted = () => { + const [newestList, setNewestList] = useState(false); + const [allList, setAllList] = useState(false); + const location = useLocation(); + const [searchParams, setSearchParams] = useSearchParams(location.search); + + function updateSearchParam(key: string, value: string) { + const newSearchParams = new URLSearchParams(searchParams); + + newSearchParams.set(key, value); + setSearchParams(newSearchParams); + } + + function removeSearchParam(key: string) { + const newSearchParams = new URLSearchParams(searchParams); + + newSearchParams.delete(key); + setSearchParams(newSearchParams); + } + + function clickBy() { + setNewestList(!newestList); + setAllList(false); + } + + function clickCount() { + setAllList(!allList); + setNewestList(false); + } + + return ( +
+ {/* Сортировка */} +
+

Sort by

+
+ {searchParams.get('sort') || 'Newest'} +
+ + {newestList && ( +
+

{ + removeSearchParam('sort'); + setNewestList(false); + }} + className="sort-list--select-text" + > + Newest +

+

{ + updateSearchParam('sort', 'Alphabetically'); + setNewestList(false); + }} + className="sort-list--select-text" + > + Alphabetically +

+

{ + updateSearchParam('sort', 'Cheapest'); + setNewestList(false); + }} + className="sort-list--select-text" + > + Cheapest +

+
+ )} +
+ + {/* Элементы на странице */} +
+

Items on page

+
+ {searchParams.get('perPage') || 'All'} +
+ + {allList && ( +
+

{ + updateSearchParam('perPage', '4'); + setAllList(false); + }} + className="sort-list--select-text" + > + 4 +

+

{ + updateSearchParam('perPage', '8'); + setAllList(false); + }} + className="sort-list--select-text" + > + 8 +

+

{ + updateSearchParam('perPage', '16'); + setAllList(false); + }} + className="sort-list--select-text" + > + 16 +

+

{ + removeSearchParam('perPage'); + setAllList(false); + }} + className="sort-list--select-text" + > + All +

+
+ )} +
+
+ ); +}; diff --git a/src/components/Sorted/index.ts b/src/components/Sorted/index.ts new file mode 100644 index 0000000000..f19e381e9c --- /dev/null +++ b/src/components/Sorted/index.ts @@ -0,0 +1 @@ +export * from './Sorted'; diff --git a/src/hooks/useCartAndFavorites.ts b/src/hooks/useCartAndFavorites.ts new file mode 100644 index 0000000000..957f192f5d --- /dev/null +++ b/src/hooks/useCartAndFavorites.ts @@ -0,0 +1,33 @@ +import { useCallback, useEffect, useState } from 'react'; +import { useAppContext } from '../ContextStor'; +import { useLocalStorage } from '../LocaleStorage'; +import { useQuantityContext } from '../QuantityContext'; + +export const useCartAndFavorites = () => { + const { cart, favorites } = useAppContext(); + const { quantities, setQuantities } = useQuantityContext(); + + + const [favoritesCount, setFavoritesCount] = useState(favorites.length); + const [cartCount, setCartCount] = useState(0); + + // Функция для подсчета общего количества товаров + const calculateTotalItems = useCallback(() => { + return cart.reduce( + (total, _, index) => total + (quantities[index] || 0), + 0, + ); + }, [cart, quantities]); + + // Обновляем количество избранного + useEffect(() => { + setFavoritesCount(favorites.length); + }, [favorites.length]); + + // Обновляем cartCount при изменении корзины или quantities + useEffect(() => { + setCartCount(calculateTotalItems()); + }, [cart, quantities, calculateTotalItems]); + + return { favoritesCount, cartCount }; +}; diff --git a/src/index.tsx b/src/index.tsx index 50470f1508..d62a94e382 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,4 +1,6 @@ +import React from 'react'; import { createRoot } from 'react-dom/client'; -import { App } from './App'; +import { Root } from './Root'; -createRoot(document.getElementById('root') as HTMLElement).render(); +createRoot(document.getElementById('root') as HTMLElement).render(); +import './App.scss'; diff --git a/src/normolize.scss b/src/normolize.scss new file mode 100644 index 0000000000..8ba450db00 --- /dev/null +++ b/src/normolize.scss @@ -0,0 +1,57 @@ +@font-face { + font-family: Mont; + src: url('/fonts/Mont-Regular.otf') format('opentype'); + font-weight: normal; + font-style: normal; +} + +@font-face { + font-family: Mont; + src: url('/fonts/Mont-Bold.otf') format('opentype'); + font-weight: bold; + font-style: normal; +} + +@font-face { + font-family: Mont; + src: url('/fonts/Mont-SemiBold.otf') format('opentype'); + font-weight: 600; + font-style: normal; +} + +a { + text-decoration: none; + color: #000; +} + +.container { + max-width: 1200px; /* Максимальная ширина для контента */ + margin: 0 auto; + padding: 13px 16px; + min-height: 70vh; /* Высота контейнера на весь экран */ +} + +body { + font-family: Mont, sans-serif; + margin: 0; /* Шрифт для всего тела */ +} + +@media screen and (min-width: 640px) { + .container { + padding-left: 24px; + padding-right: 24px; + } +} + +@media screen and (min-width: 1200px) { + .container { + padding-left: 32px; + padding-right: 32px; + } +} + + +ul { + margin: 0; + padding: 0; +} diff --git a/src/pages/AccessoriesPage.tsx b/src/pages/AccessoriesPage.tsx new file mode 100644 index 0000000000..7aa33695f3 --- /dev/null +++ b/src/pages/AccessoriesPage.tsx @@ -0,0 +1,24 @@ +import React, { useEffect, useState } from 'react'; +import { Accessory } from '../types/accessory'; +import { List } from '../components/List'; +import { ProductHome } from '../components/ProductHome/ProductHome'; + +export const AccessoriesPage = () => { + const [accessoriesList, setAccessoriesList] = useState([]); + + useEffect(() => { + fetch('./api/accessories.json') + .then(response => response.json()) + .then(data => setAccessoriesList(data)); + }, []); + + return ( + <> +
+ + + +
+ + ); +}; diff --git a/src/pages/CartItem.tsx b/src/pages/CartItem.tsx new file mode 100644 index 0000000000..1cb10d8a67 --- /dev/null +++ b/src/pages/CartItem.tsx @@ -0,0 +1,162 @@ +import React from 'react'; +import { useLocation, useNavigate } from 'react-router-dom'; +import { useAppContext } from '../ContextStor'; +import { useQuantityContext } from '../QuantityContext'; + +export const CartItem = () => { + const { cart, setCart } = useAppContext(); + const navigate = useNavigate(); + const location = useLocation(); + const { quantities, setQuantities } = useQuantityContext(); + + const sumCart = () => { + let sum = 0; + + for (let i = 0; i < cart.length; i++) { + sum += cart[i].price * quantities[i]; // Зміна тут: множимо ціну на кількість + } + + return sum; + }; + + const handleRemoveItem = (productId: number) => { + const updatedCart = cart.filter(product => product.id !== productId); + + // Находим индекс удаляемого продукта в корзине + const indexToRemove = cart.findIndex(product => product.id === productId); + + // Обновляем массив quantities, удаляя элемент по индексу + const updatedQuantities = quantities.filter( + (_, index) => index !== indexToRemove, + ); + + // Устанавливаем новые значения для cart и quantities + setCart(updatedCart); + setQuantities(updatedQuantities); + }; + + const handleIncrease = (index: number) => { + const newQuantities = [...quantities]; + + newQuantities[index] += 1; + + setQuantities(newQuantities); + }; + + const handleDecrease = (index: number) => { + const newQuantities = [...quantities]; + + if (newQuantities[index] > 1) { + newQuantities[index] -= 1; + } + + setQuantities(newQuantities); + }; + + const totalItems = () => { + return quantities.reduce((total, qty) => total + qty, 0); + }; + + return ( + <> +
+
+ Home +

{ + navigate(`${location.state.from}`); + }} + className="details__back--text" + > + Back +

+
+ +

Cart

+ {cart.length > 0 ? ( + <> +

{totalItems()} items

+ +
+
+ {cart.map((el, index) => ( +
+
+ handleRemoveItem(el.id)} + className="cart__card-close" + src="./img/Icons_Close.png" + /> + navigate(`/${el.category}/${el.itemId}`)} + className="cart__card-img" + src={el.image} + style={{ cursor: 'pointer' }} + /> +

{el.name}

+
+ +
+
+ +

{quantities[index]}

+ +
+

+ ${el.price * quantities[index]} +

+
+
+ ))} +
+ +
+

{`$${sumCart()}`}

+

Total for {totalItems()} items

+
+ +
+
+ + ) : ( + + )} +
+ + ); +}; diff --git a/src/pages/Favorites.tsx b/src/pages/Favorites.tsx new file mode 100644 index 0000000000..b55c5674d8 --- /dev/null +++ b/src/pages/Favorites.tsx @@ -0,0 +1,127 @@ +import React from 'react'; +import { Link, useLocation, useNavigate } from 'react-router-dom'; +import { Products } from '../types/products'; +import { useAppContext } from '../ContextStor'; +import { useQuantityContext } from '../QuantityContext'; + +export const Favorites = () => { + const location = useLocation(); + const navigate = useNavigate(); + const { favorites, setFavorites, setCart, cart } = useAppContext(); + const { quantities, setQuantities } = useQuantityContext(); + + const toggleFavorite = (product: Products) => { + const isFavorite = favorites.some(fav => fav.id === product.id); + + if (isFavorite) { + setFavorites(favorites.filter(fav => fav.id !== product.id)); + } else { + setFavorites([...favorites, product]); + } + }; + + const toggleCart = (product: Products) => { + const isCart = cart.some(el => el.id === product.id); + + if (isCart) { + setCart(cart.filter(el => el.id !== product.id)); + setQuantities(prevQuantities => + prevQuantities.filter((_, index) => cart[index].id !== product.id), + ); + } else { + setCart([...cart, product]); + setQuantities(prevQuantities => [...prevQuantities, 1]); + } + }; + + return ( + <> +
+
+ + Home + + Chevron +

+ {location.pathname.includes('/tablets') + ? 'Tablets' + : location.pathname.includes('/phones') + ? 'Phones' + : location.pathname.includes('/accessories') + ? 'Accessories' + : location.pathname.includes('/favourites') + ? 'Favourites' + : ''} +

+
+ +

Favourites

+

{favorites.length} items

+ +
    + {favorites.length > 0 && + favorites.map(product => ( +
    + { + navigate(`/${product.category}/${product.itemId}`); + }} + className="card__image" + src={product.image} + alt="card-image" + /> +

    {product.name}

    +

    {`${product.price}$`}

    + +
    + +
    +

    Screen

    +

    {product.screen}

    +
    + +
    +

    Capacity

    +

    {product.capacity}

    +
    + +
    +

    Ram

    +

    {product.ram}

    +
    + +
    + + toggleFavorite(product)} + className="page-home-card__favorite" + src={ + favorites.some(fav => fav.id === product.id) + ? './img/Add to fovourites - Added.svg' + : './img/add-to-cart.svg' + } + alt="favorite" + /> +
    +
    + ))} +
+
+ + ); +}; diff --git a/src/pages/HomePage.tsx b/src/pages/HomePage.tsx new file mode 100644 index 0000000000..54ff7166b9 --- /dev/null +++ b/src/pages/HomePage.tsx @@ -0,0 +1,18 @@ +import React from 'react'; +import { ShopByCategory } from '../components/ShopByCategory'; +import { HomeCarousel } from '../components/HomeCarousel'; +import { BrandNewModelsHome } from '../components/BrandNewModelsHome'; + +export const HomePage = () => { + return ( + <> +
+ + + + + +
+ + ); +}; diff --git "a/src/pages/NotFoundPage\321\216.tsx" "b/src/pages/NotFoundPage\321\216.tsx" new file mode 100644 index 0000000000..7bae3c401b --- /dev/null +++ "b/src/pages/NotFoundPage\321\216.tsx" @@ -0,0 +1,3 @@ +import React from 'react'; + +export const NotFoundPage = () =>

Page not found

; diff --git a/src/pages/PhonesPage.tsx b/src/pages/PhonesPage.tsx new file mode 100644 index 0000000000..f640ed3115 --- /dev/null +++ b/src/pages/PhonesPage.tsx @@ -0,0 +1,24 @@ +import React, { useEffect, useState } from 'react'; +import { Phone } from '../types/phone'; +import { List } from '../components/List'; +import { ProductHome } from '../components/ProductHome/ProductHome'; + +export const PhonesPage = () => { + const [phonesList, setPhonesList] = useState([]); + + useEffect(() => { + fetch('./api/phones.json') + .then(response => response.json()) + .then(data => setPhonesList(data)); + }, []); + + return ( + <> +
+ + + +
+ + ); +}; diff --git a/src/pages/ProductDetailsPage.tsx b/src/pages/ProductDetailsPage.tsx new file mode 100644 index 0000000000..940274d2a3 --- /dev/null +++ b/src/pages/ProductDetailsPage.tsx @@ -0,0 +1,79 @@ +import React, { useEffect, useState } from 'react'; +import { useParams, useLocation } from 'react-router-dom'; +import { Phone } from '../types/phone'; +import { Tablet } from '../types/tablet'; +import { Accessory } from '../types/accessory'; +import { DetailsBack } from '../components/DetailsHome'; +import { DetailsCard } from '../components/DetailsCard'; +import { DetailsTechSpecs } from '../components/DetailsTechSpecs'; +import { BrandNewModelsHome } from '../components/BrandNewModelsHome'; + +type Product = Phone | Tablet | Accessory; + +export const ProductDetailsPage = () => { + const { productId } = useParams(); + const location = useLocation(); + const [product, setProduct] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + const fetchProductDetails = async () => { + try { + setLoading(true); + + let apiUrl = ''; + + if (location.pathname.includes('/phones')) { + apiUrl = './api/phones.json'; + } else if (location.pathname.includes('/tablets')) { + apiUrl = './api/tablets.json'; + } else if (location.pathname.includes('/accessories')) { + apiUrl = './api/accessories.json'; + } + + const response = await fetch(apiUrl); + const data: Product[] = await response.json(); + const selectedProduct = data.find(item => item.id === productId); + + if (!selectedProduct) { + throw new Error('Product not found'); + } + + setProduct(selectedProduct); + } catch (errorN) { + setError('Failed to fetch product details'); + } finally { + setLoading(false); + } + }; + + fetchProductDetails(); + }, [productId, location.pathname]); + + if (loading) { + return

Loading...

; + } + + if (error) { + return

{error}

; + } + + if (!product) { + return

Product not found

; + } + + return ( +
+
+ + + + + + + +
+
+ ); +}; diff --git a/src/pages/TabletsPage.tsx b/src/pages/TabletsPage.tsx new file mode 100644 index 0000000000..b6d0634264 --- /dev/null +++ b/src/pages/TabletsPage.tsx @@ -0,0 +1,24 @@ +import React, { useEffect, useState } from 'react'; +import { Tablet } from '../types/tablet'; +import { List } from '../components/List'; +import { ProductHome } from '../components/ProductHome/ProductHome'; + +export const TabletsPage = () => { + const [tabletsList, setTabletsList] = useState([]); + + useEffect(() => { + fetch('./api/tablets.json') + .then(response => response.json()) + .then(data => setTabletsList(data)); + }, []); + + return ( + <> +
+ + + +
+ + ); +}; diff --git a/src/types/accessory.ts b/src/types/accessory.ts new file mode 100644 index 0000000000..b1fac79e38 --- /dev/null +++ b/src/types/accessory.ts @@ -0,0 +1,22 @@ +export type Accessory = { + 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; + cell: string[]; +}; diff --git a/src/types/phone.ts b/src/types/phone.ts new file mode 100644 index 0000000000..140775a7e0 --- /dev/null +++ b/src/types/phone.ts @@ -0,0 +1,24 @@ +export interface Phone { + id: string; + category: string; + namespaceId: string; + name: string; + capacityAvailable: string[]; + capacity: string; + priceRegular: number; + priceDiscount: number; + colorsAvailable: string[]; + color: string; + images: string[]; + description: Array<{ + title: string; + text: string[]; + }>; + screen: string; + resolution: string; + processor: string; + ram: string; + camera: string; + zoom: string; + cell: string[]; +} diff --git a/src/types/products.ts b/src/types/products.ts new file mode 100644 index 0000000000..5b4e189cff --- /dev/null +++ b/src/types/products.ts @@ -0,0 +1,14 @@ +export interface Products { + id: number; + category: string; + itemId: string; + name: string; + fullPrice: number; + price: number; + screen: string; + capacity: string; + color: string; + ram: string; + year: number; + image: string; +} diff --git a/src/types/tablet.ts b/src/types/tablet.ts new file mode 100644 index 0000000000..6f2c8d0cef --- /dev/null +++ b/src/types/tablet.ts @@ -0,0 +1,24 @@ +export type Tablet = { + 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/useWindowSize.ts b/src/useWindowSize.ts new file mode 100644 index 0000000000..5c594e5611 --- /dev/null +++ b/src/useWindowSize.ts @@ -0,0 +1,25 @@ +import { useEffect, useState } from 'react'; + +type WidthAndHeightTuple = [number, number]; + +function getCurrentWidthAndHeight(): WidthAndHeightTuple { + return [window.innerWidth, window.innerHeight]; +} + +export function useWindowResize() { + const [widthAndHeight, setWidthAndHeight] = useState( + getCurrentWidthAndHeight(), + ); + + function handler() { + setWidthAndHeight(getCurrentWidthAndHeight()); + } + + useEffect(() => { + window.addEventListener('resize', handler); + + return () => window.removeEventListener('resize', handler); + }, []); + + return widthAndHeight; +} diff --git a/src/variables.ts b/src/variables.ts new file mode 100644 index 0000000000..f8ab0b6e38 --- /dev/null +++ b/src/variables.ts @@ -0,0 +1,17 @@ +export const COLORS = { + black: '#0F0F11', + gold: '#F4BA47', + silver: '#C0C0C0', + spacegray: '#A9A9A9', + coral: '#FF7F50', + white: '#FFFFFF', + midnightgreen: '#003030', + yellow: '#FFFF00', + purple: '#800080', + green: '#008000', + red: '#FF0000', + rosegold: '#B76E79', + midnight: '#191970', + graphite: '#7F7F7F', + sierrablue: '#A8B3C8', +}; diff --git a/tsconfig.json b/tsconfig.json index cfb168bb26..e2c46af00c 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -5,6 +5,7 @@ ], "compilerOptions": { "sourceMap": false, - "types": ["node", "cypress"] + "types": ["node", "cypress"], + "jsx": "react" // или "react-jsx" для React 17+ } }