Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 84 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
@@ -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
62 changes: 59 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Современный интернет-магазин электроники с каталогом товаров, корзиной и системой промокодов.
Expand Down Expand Up @@ -40,6 +41,14 @@ TechStore — это одностраничное приложение (SPA) и
- **Сетка товаров** — отображение карточек с изображениями, ценами и описаниями
- **Индикаторы скидок** — визуальное отображение товаров со скидкой
- **Добавление в корзину** — быстрое добавление товаров из каталога
- **Слайдер баннеров** — промо-баннеры на главной странице
- **Популярные товары** — секция с рекомендуемыми товарами

### Страница товара
- **Детальная информация** — полное описание товара с характеристиками
- **Галерея изображений** — просмотр фотографий товара с навигацией
- **Связанные товары** — рекомендации похожих товаров
- **Блок цены** — отображение цены со скидкой и без

### Корзина покупок
- **Таблица товаров** — структурированное отображение выбранных товаров
Expand All @@ -52,8 +61,14 @@ TechStore — это одностраничное приложение (SPA) и
- **Автоматический расчёт** — применение скидки 15% при вводе кода `Кекс`
- **Пересчёт итогов** — автоматическое обновление суммы заказа

### Карта и навигация
- **Интерактивная карта** — отображение местоположения на базе Leaflet
- **Маршрутизация** — построение маршрута между точками
- **Секция локации** — информация о местоположении магазина

### Уведомления
- **Информирование о лимитах** — предупреждение при достижении максимального количества товара
- **Страница 404** — страница для несуществующих маршрутов

---

Expand All @@ -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 | Построение маршрутов |

### Качество кода
| Технология | Версия | Назначение |
|------------|--------|------------|
Expand All @@ -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 # Точка входа
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"name": "ai-project",
"name": "tech-store",
"private": true,
"version": "0.0.0",
"type": "module",
Expand Down
2 changes: 1 addition & 1 deletion src/components/BackButton/BackButton.test.jsx
Original file line number Diff line number Diff line change
@@ -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();
Expand Down
8 changes: 4 additions & 4 deletions src/components/BannerSlider/BannerSlider.css
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand All @@ -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 {
Expand Down
8 changes: 5 additions & 3 deletions src/components/BannerSlider/BannerSlider.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@ export function BannerSlider() {
};

useEffect(() => {
if (!isAutoPlaying) return;
if (!isAutoPlaying) {
return;
}

const interval = setInterval(goToNext, 5000);
return () => clearInterval(interval);
Expand Down Expand Up @@ -87,9 +89,9 @@ export function BannerSlider() {
</IconButton>

<Box className="banner-slider__dots">
{banners.map((_, index) => (
{banners.map((banner, index) => (
<button
key={index}
key={banner.id}
className={`banner-slider__dot ${index === currentIndex ? 'banner-slider__dot--active' : ''}`}
onClick={() => goToSlide(index)}
aria-label={`Перейти к слайду ${index + 1}`}
Expand Down
2 changes: 1 addition & 1 deletion src/components/GalleryDot/GalleryDot.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { GalleryDot } from './GalleryDot';

describe('GalleryDot', () => {
it('Active dot: Имеет класс модификатор --active и aria-label "Текущее изображение"', () => {
render(<GalleryDot isActive={true} onClick={vi.fn()} />);
render(<GalleryDot isActive onClick={vi.fn()} />);

const dot = screen.getByLabelText('Текущее изображение');
expect(dot).toBeInTheDocument();
Expand Down
12 changes: 9 additions & 3 deletions src/components/GallerySlide/GallerySlide.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 '';
}

Expand Down
6 changes: 3 additions & 3 deletions src/components/Header/Header.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
4 changes: 2 additions & 2 deletions src/components/IntroSection/IntroSection.css
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,15 @@
}

.intro-section__title {
margin: 0 0 24px 0;
margin: 0 0 24px;
text-align: center;
}

.intro-section__description {
margin: 0;
color: #666666;
line-height: 1.6;
font-size: 1.125rem;
font-size: 1.13rem;
}

@media (max-width: 768px) {
Expand Down
7 changes: 2 additions & 5 deletions src/components/Map/Map.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -47,5 +44,5 @@
.map-loader__text {
font-family: "Inter", "Roboto", "Helvetica", "Arial", sans-serif;
font-size: 14px;
color: #666;
color: #666666;
}
2 changes: 1 addition & 1 deletion src/components/NotFoundState/NotFoundState.test.jsx
Original file line number Diff line number Diff line change
@@ -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 { NotFoundState } from './NotFoundState';

const mockNavigate = vi.fn();
Expand Down
4 changes: 2 additions & 2 deletions src/components/ProductGallery/ProductGallery.test.jsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { render, screen, fireEvent } from '@testing-library/react';
import { render, screen } from '@testing-library/react';
import { describe, it, expect, vi } from 'vitest';
import { ProductGallery } from './ProductGallery';

vi.mock('../../hooks/useGallery/useGallery', () => ({
useGallery: (total) => ({
useGallery: () => ({
currentIndex: 0,
direction: 'next',
goTo: vi.fn(),
Expand Down
4 changes: 2 additions & 2 deletions src/components/ProductGrid/ProductGrid.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,8 @@ import './ProductGrid.css';
*/
export function ProductGrid({ products, onAddToCart, cartItems = [], onDecreaseQuantity, onIncreaseQuantity }) {
const getProductQuantity = (productId) => {
const item = cartItems.find((item) => item.id === productId);
return item ? item.quantity : 0;
const cartItem = cartItems.find((item) => item.id === productId);
return cartItem ? cartItem.quantity : 0;
};

return (
Expand Down
4 changes: 2 additions & 2 deletions src/hooks/useGallery/useGallery.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
2 changes: 1 addition & 1 deletion src/utils/array/array.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
});
Expand Down
1 change: 1 addition & 0 deletions vite.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
},
Expand Down
Loading