From 64e31cbeeba45d6bf659cab16148f5284859203e Mon Sep 17 00:00:00 2001 From: Sergei Dmitriev Date: Sat, 25 Apr 2026 23:05:47 +0300 Subject: [PATCH 1/5] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8=D1=82?= =?UTF-8?q?=20=D0=BA=D0=BE=D0=BD=D1=84=D0=B8=D0=B3=20=D0=B4=D0=B5=D0=BF?= =?UTF-8?q?=D0=BB=D0=BE=D1=8F=20=D0=B4=D0=BB=D1=8F=20gh-pages?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/build.yml | 84 +++++++++++++++++++++++++++++++++++++ vite.config.js | 1 + 2 files changed, 85 insertions(+) create mode 100644 .github/workflows/build.yml diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..7a9a059 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,84 @@ +name: Build + +on: + push: + branches: [ main, master ] + pull_request: + branches: [ main, master ] + +jobs: + build: + runs-on: ubuntu-latest + + strategy: + matrix: + node-version: [18.x, 20.x] + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Run lint scripts + run: npm run lint:scripts + + - name: Run lint styles + run: npm run lint:styles + + - name: Run tests + run: npm test + + - name: Build project + run: npm run build + + deploy: + runs-on: ubuntu-latest + needs: build + if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master' + + permissions: + contents: read + pages: write + id-token: write + + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20.x' + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Build for GitHub Pages + run: npm run build + env: + VITE_BASE_URL: /tech-store/ + + - name: Setup Pages + uses: actions/configure-pages@v5 + + - name: Upload artifact + uses: actions/upload-pages-artifact@v3 + with: + path: ./dist + + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/vite.config.js b/vite.config.js index e196c26..40e6f82 100644 --- a/vite.config.js +++ b/vite.config.js @@ -4,6 +4,7 @@ import react from '@vitejs/plugin-react'; // https://vite.dev/config/ export default defineConfig({ plugins: [react()], + base: process.env.VITE_BASE_URL || '/', server: { host: '127.0.0.1', }, From 171df7f2e4b020d5da7f934d2dffadd1256e9b54 Mon Sep 17 00:00:00 2001 From: Sergei Dmitriev Date: Sat, 25 Apr 2026 23:10:59 +0300 Subject: [PATCH 2/5] =?UTF-8?q?=D0=9E=D0=B1=D0=BD=D0=BE=D0=B2=D0=B8=D1=82?= =?UTF-8?q?=20readme?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 62 ++++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 59 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index b3cc7a0..7ec472a 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,7 @@ [![React](https://img.shields.io/badge/React-19-blue)](https://react.dev) [![Vite](https://img.shields.io/badge/Vite-7-646CFF)](https://vitejs.dev) [![MUI](https://img.shields.io/badge/MUI-9-007FFF)](https://mui.com) +[![Leaflet](https://img.shields.io/badge/Leaflet-1.9-green)](https://leafletjs.com) [![Vitest](https://img.shields.io/badge/Vitest-4-6E9F18)](https://vitest.dev) Современный интернет-магазин электроники с каталогом товаров, корзиной и системой промокодов. @@ -40,6 +41,14 @@ TechStore — это одностраничное приложение (SPA) и - **Сетка товаров** — отображение карточек с изображениями, ценами и описаниями - **Индикаторы скидок** — визуальное отображение товаров со скидкой - **Добавление в корзину** — быстрое добавление товаров из каталога +- **Слайдер баннеров** — промо-баннеры на главной странице +- **Популярные товары** — секция с рекомендуемыми товарами + +### Страница товара +- **Детальная информация** — полное описание товара с характеристиками +- **Галерея изображений** — просмотр фотографий товара с навигацией +- **Связанные товары** — рекомендации похожих товаров +- **Блок цены** — отображение цены со скидкой и без ### Корзина покупок - **Таблица товаров** — структурированное отображение выбранных товаров @@ -52,8 +61,14 @@ TechStore — это одностраничное приложение (SPA) и - **Автоматический расчёт** — применение скидки 15% при вводе кода `Кекс` - **Пересчёт итогов** — автоматическое обновление суммы заказа +### Карта и навигация +- **Интерактивная карта** — отображение местоположения на базе Leaflet +- **Маршрутизация** — построение маршрута между точками +- **Секция локации** — информация о местоположении магазина + ### Уведомления - **Информирование о лимитах** — предупреждение при достижении максимального количества товара +- **Страница 404** — страница для несуществующих маршрутов --- @@ -74,6 +89,13 @@ TechStore — это одностраничное приложение (SPA) и | [MUI Icons](https://mui.com/material-ui/material-icons/) | 9.0.0 | Иконки | | [Emotion](https://emotion.sh) | 11.14.x | CSS-in-JS стилизация | +### Карты и геолокация +| Технология | Версия | Назначение | +|------------|--------|------------| +| [Leaflet](https://leafletjs.com) | 1.9.4 | Интерактивные карты | +| [React Leaflet](https://react-leaflet.js.org) | 5.0.0 | React-компоненты для Leaflet | +| [Leaflet Routing Machine](https://www.liedman.net/leaflet-routing-machine/) | 3.2.12 | Построение маршрутов | + ### Качество кода | Технология | Версия | Назначение | |------------|--------|------------| @@ -96,20 +118,54 @@ TechStore — это одностраничное приложение (SPA) и ``` src/ +├── pages/ # Страницы приложения +│ ├── CartPage/ # Страница корзины +│ ├── CatalogPage/ # Страница каталога +│ ├── HomePage/ # Главная страница +│ └── ProductDetailPage/ # Страница детальной информации о товаре ├── components/ # React компоненты │ ├── App/ # Корневой компонент приложения +│ ├── BackButton/ # Кнопка возврата +│ ├── BannerSlider/ # Слайдер баннеров │ ├── CartItem/ # Элемент корзины │ ├── CartTable/ # Таблица корзины │ ├── CartTotals/ # Итоговые суммы корзины +│ ├── Footer/ # Подвал сайта +│ ├── GalleryDot/ # Точка навигации галереи +│ ├── GallerySlide/ # Слайд галереи │ ├── Header/ # Шапка сайта +│ ├── IntroSection/ # Вводная секция │ ├── LimitNotification/ # Уведомление о лимите +│ ├── LocationSection/ # Секция локации +│ ├── Map/ # Интерактивная карта +│ ├── NotFoundState/ # Страница 404 +│ ├── PopularProductsSection/ # Секция популярных товаров +│ ├── PriceBlock/ # Блок цены │ ├── ProductCard/ # Карточка товара +│ ├── ProductGallery/ # Галерея товара │ ├── ProductGrid/ # Сетка товаров -│ └── PromoCodeForm/ # Форма промокода +│ ├── ProductInfo/ # Информация о товаре +│ ├── PromoCodeForm/ # Форма промокода +│ ├── QuantityControl/ # Контроль количества +│ ├── RelatedProductCard/ # Карточка связанного товара +│ ├── RelatedProducts/ # Связанные товары +│ └── RoutingControl/ # Контроль маршрутизации +├── hooks/ # Кастомные хуки +│ ├── useCartItem/ # Логика элемента корзины +│ ├── useGallery/ # Логика галереи +│ ├── useLimitNotification/ # Логика уведомлений о лимите +│ └── useProductDetail/ # Логика страницы товара +├── utils/ # Утилиты +│ ├── array/ # Функции для работы с массивами +│ ├── cart/ # Функции для работы с корзиной +│ └── price/ # Функции для работы с ценами ├── constants/ # Константы приложения -│ └── cart.js # Параметры корзины (лимиты, промокоды) +│ ├── cart.js # Параметры корзины (лимиты, промокоды) +│ └── map.js # Координаты для карты ├── data/ # Данные приложения -│ └── products.js # Каталог товаров +│ ├── banners.js # Данные баннеров +│ ├── products.js # Каталог товаров +│ └── productsDetail.js # Детальная информация о товарах ├── assets/ # Статические ресурсы ├── index.css # Глобальные стили └── main.jsx # Точка входа From 520133e7135e3ad3f4906a62224ef8756e02bf06 Mon Sep 17 00:00:00 2001 From: Sergei Dmitriev Date: Sat, 25 Apr 2026 23:16:43 +0300 Subject: [PATCH 3/5] =?UTF-8?q?=D0=98=D1=81=D0=BF=D1=80=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D1=82=20=D0=BE=D1=88=D0=B8=D0=B1=D0=BA=D0=B8=20=D0=BB=D0=B8?= =?UTF-8?q?=D0=BD=D1=82=D0=B5=D1=80=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/BannerSlider/BannerSlider.css | 8 ++++---- src/components/BannerSlider/BannerSlider.jsx | 4 +++- src/components/GalleryDot/GalleryDot.test.jsx | 2 +- src/components/GallerySlide/GallerySlide.jsx | 12 +++++++++--- src/components/Header/Header.css | 6 +++--- src/components/IntroSection/IntroSection.css | 4 ++-- src/components/Map/Map.css | 7 ++----- src/hooks/useGallery/useGallery.js | 4 ++-- src/utils/array/array.test.js | 2 +- 9 files changed, 27 insertions(+), 22 deletions(-) diff --git a/src/components/BannerSlider/BannerSlider.css b/src/components/BannerSlider/BannerSlider.css index 27b05a9..cc48d21 100644 --- a/src/components/BannerSlider/BannerSlider.css +++ b/src/components/BannerSlider/BannerSlider.css @@ -45,7 +45,7 @@ } .banner-slider__title { - margin: 0 0 16px 0; + margin: 0 0 16px; font-weight: 700; text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.5); } @@ -60,15 +60,15 @@ position: absolute; top: 50%; transform: translateY(-50%); - background-color: rgba(255, 255, 255, 0.8) !important; - color: #333333 !important; + background-color: rgba(255, 255, 255, 0.8); + color: #333333; width: 48px; height: 48px; z-index: 10; } .banner-slider__nav:hover { - background-color: rgba(255, 255, 255, 0.95) !important; + background-color: rgba(255, 255, 255, 0.95); } .banner-slider__nav--prev { diff --git a/src/components/BannerSlider/BannerSlider.jsx b/src/components/BannerSlider/BannerSlider.jsx index a340d9f..6117e4a 100644 --- a/src/components/BannerSlider/BannerSlider.jsx +++ b/src/components/BannerSlider/BannerSlider.jsx @@ -27,7 +27,9 @@ export function BannerSlider() { }; useEffect(() => { - if (!isAutoPlaying) return; + if (!isAutoPlaying) { + return; + } const interval = setInterval(goToNext, 5000); return () => clearInterval(interval); diff --git a/src/components/GalleryDot/GalleryDot.test.jsx b/src/components/GalleryDot/GalleryDot.test.jsx index be34338..6750122 100644 --- a/src/components/GalleryDot/GalleryDot.test.jsx +++ b/src/components/GalleryDot/GalleryDot.test.jsx @@ -4,7 +4,7 @@ import { GalleryDot } from './GalleryDot'; describe('GalleryDot', () => { it('Active dot: Имеет класс модификатор --active и aria-label "Текущее изображение"', () => { - render(); + render(); const dot = screen.getByLabelText('Текущее изображение'); expect(dot).toBeInTheDocument(); diff --git a/src/components/GallerySlide/GallerySlide.jsx b/src/components/GallerySlide/GallerySlide.jsx index 7bd6c4f..eef902f 100644 --- a/src/components/GallerySlide/GallerySlide.jsx +++ b/src/components/GallerySlide/GallerySlide.jsx @@ -8,9 +8,15 @@ import { Box } from '@mui/material'; * @returns {string} CSS class suffix */ function getSlidePositionClass(index, currentIndex, total) { - if (index === currentIndex) return 'active'; - if (index === (currentIndex - 1 + total) % total) return 'prev'; - if (index === (currentIndex + 1) % total) return 'next'; + if (index === currentIndex) { + return 'active'; + } + if (index === (currentIndex - 1 + total) % total) { + return 'prev'; + } + if (index === (currentIndex + 1) % total) { + return 'next'; + } return ''; } diff --git a/src/components/Header/Header.css b/src/components/Header/Header.css index 27a7174..bbdbfc8 100644 --- a/src/components/Header/Header.css +++ b/src/components/Header/Header.css @@ -21,7 +21,7 @@ } .header__catalog-link { - color: #ffffff !important; - text-transform: none !important; - font-weight: 500 !important; + color: #ffffff; + text-transform: none; + font-weight: 500; } diff --git a/src/components/IntroSection/IntroSection.css b/src/components/IntroSection/IntroSection.css index 6c39bca..315939e 100644 --- a/src/components/IntroSection/IntroSection.css +++ b/src/components/IntroSection/IntroSection.css @@ -16,7 +16,7 @@ } .intro-section__title { - margin: 0 0 24px 0; + margin: 0 0 24px; text-align: center; } @@ -24,7 +24,7 @@ margin: 0; color: #666666; line-height: 1.6; - font-size: 1.125rem; + font-size: 1.13rem; } @media (max-width: 768px) { diff --git a/src/components/Map/Map.css b/src/components/Map/Map.css index d2c79e5..09ebe3b 100644 --- a/src/components/Map/Map.css +++ b/src/components/Map/Map.css @@ -31,10 +31,7 @@ .map-loader { position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; + inset: 0; display: flex; flex-direction: column; align-items: center; @@ -47,5 +44,5 @@ .map-loader__text { font-family: "Inter", "Roboto", "Helvetica", "Arial", sans-serif; font-size: 14px; - color: #666; + color: #666666; } diff --git a/src/hooks/useGallery/useGallery.js b/src/hooks/useGallery/useGallery.js index b320199..dfd6687 100644 --- a/src/hooks/useGallery/useGallery.js +++ b/src/hooks/useGallery/useGallery.js @@ -16,12 +16,12 @@ export function useGallery(totalImages) { const next = useCallback(() => { setDirection('next'); - setCurrentIndex((prev) => (prev === totalImages - 1 ? 0 : prev + 1)); + setCurrentIndex((prevState) => (prevState === totalImages - 1 ? 0 : prevState + 1)); }, [totalImages]); const prev = useCallback(() => { setDirection('prev'); - setCurrentIndex((prev) => (prev === 0 ? totalImages - 1 : prev - 1)); + setCurrentIndex((prevState) => (prevState === 0 ? totalImages - 1 : prevState - 1)); }, [totalImages]); return { diff --git a/src/utils/array/array.test.js b/src/utils/array/array.test.js index 69ddb1a..097d566 100644 --- a/src/utils/array/array.test.js +++ b/src/utils/array/array.test.js @@ -34,7 +34,7 @@ describe('getRandomItems', () => { it('Все возвращаемые элементы из исходного массива: Проверяет содержимое', () => { const items = [1, 2, 3, 4, 5]; const result = getRandomItems(items, 3); - result.forEach(item => { + result.forEach((item) => { expect(items).toContain(item); }); }); From e8500fba25a71bb19985f2cbc998f35444588154 Mon Sep 17 00:00:00 2001 From: Sergei Dmitriev Date: Sat, 25 Apr 2026 23:18:47 +0300 Subject: [PATCH 4/5] =?UTF-8?q?=D0=98=D1=81=D0=BF=D1=80=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D1=82=20=D0=BE=D1=88=D0=B8=D0=B1=D0=BA=D0=B8=20=D0=B4=D0=B5?= =?UTF-8?q?=D0=BF=D0=BB=D0=BE=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/BackButton/BackButton.test.jsx | 2 +- src/components/BannerSlider/BannerSlider.jsx | 2 +- src/components/NotFoundState/NotFoundState.test.jsx | 2 +- src/components/ProductGallery/ProductGallery.test.jsx | 4 ++-- src/components/ProductGrid/ProductGrid.jsx | 4 ++-- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/components/BackButton/BackButton.test.jsx b/src/components/BackButton/BackButton.test.jsx index aa0aade..b2882a8 100644 --- a/src/components/BackButton/BackButton.test.jsx +++ b/src/components/BackButton/BackButton.test.jsx @@ -1,6 +1,6 @@ import { render, screen, fireEvent } from '@testing-library/react'; import { describe, it, expect, vi } from 'vitest'; -import { MemoryRouter, useNavigate } from 'react-router-dom'; +import { MemoryRouter } from 'react-router-dom'; import { BackButton } from './BackButton'; const mockNavigate = vi.fn(); diff --git a/src/components/BannerSlider/BannerSlider.jsx b/src/components/BannerSlider/BannerSlider.jsx index 6117e4a..339a23f 100644 --- a/src/components/BannerSlider/BannerSlider.jsx +++ b/src/components/BannerSlider/BannerSlider.jsx @@ -91,7 +91,7 @@ export function BannerSlider() { {banners.map((_, index) => (