diff --git a/front/src/app/pages/calendar/CalendarPage.js b/front/src/app/pages/calendar/CalendarPage.js
new file mode 100644
index 0000000..2c64b09
--- /dev/null
+++ b/front/src/app/pages/calendar/CalendarPage.js
@@ -0,0 +1,403 @@
+import React, { useState } from 'react';
+import {
+ Container,
+ Typography,
+ Box,
+ Drawer,
+ List,
+ ListItem,
+ ListItemText,
+ Chip,
+ Select,
+ MenuItem,
+ FormControl,
+ InputLabel,
+ Button,
+ Divider,
+ useMediaQuery,
+ useTheme,
+ Paper,
+} from '@mui/material';
+import { CalendarToday as CalendarIcon, FilterList as FilterIcon } from '@mui/icons-material';
+import { DateCalendar } from '@mui/x-date-pickers/DateCalendar';
+import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider';
+import { AdapterDateFns } from '@mui/x-date-pickers/AdapterDateFns';
+import ruLocale from 'date-fns/locale/ru';
+
+const CHAMPIONSHIP_STAGES = [
+ {
+ id: 1,
+ title: 'SWC Зимний чемпионат',
+ stage: '2‑й этап',
+ date: '2026-02-08',
+ class: 'Юниоры',
+ status: 'Идёт',
+ },
+ {
+ id: 2,
+ title: 'Honda Winter Cup',
+ stage: '1‑й этап',
+ date: '2026-01-31',
+ class: 'Pro',
+ status: 'Регистрация открыта',
+ },
+ {
+ id: 3,
+ title: 'Кубок Покровска (онлайн)',
+ stage: '1‑й этап',
+ date: '2026-02-01',
+ class: 'Симулятор A',
+ status: 'Предрегистрация',
+ },
+];
+
+const CalendarPage = () => {
+ const theme = useTheme();
+ const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
+
+ const [viewDate, setViewDate] = useState(new Date()); // Дата для отображения месяца
+ const [selectedClass, setSelectedClass] = useState('Все');
+ const [showFilters, setShowFilters] = useState(false);
+ const [openDrawer, setOpenDrawer] = useState(false);
+
+ // Фильтрованные этапы для текущего месяца/класса
+ const filteredStages = CHAMPIONSHIP_STAGES.filter((stage) => {
+ const stageDate = new Date(stage.date);
+ const isSameMonth = stageDate.getMonth() === viewDate.getMonth();
+ const isSameYear = stageDate.getFullYear() === viewDate.getFullYear();
+ const classMatch = selectedClass === 'Все' || stage.class === selectedClass;
+ return isSameMonth && isSameYear && classMatch;
+ });
+
+ // Группируем этапы по дате
+ const stagesByDate = filteredStages.reduce((acc, stage) => {
+ const dayKey = new Date(stage.date).toDateString();
+ acc[dayKey] = acc[dayKey] || [];
+ acc[dayKey].push(stage);
+ return acc;
+ }, {});
+
+ // Содержимое drawer для мобильных
+ const drawerContent = (
+
+
+ Этапы на {viewDate.toLocaleDateString('ru-RU', { day: 'numeric', month: 'long', year: 'numeric' })}
+
+ {filteredStages.length === 0 ? (
+
+ Нет этапов в этот день
+
+ ) : (
+
+ {filteredStages.map((stage) => (
+
+
+
+ {stage.title}
+
+
+ {stage.stage} · {stage.class}
+
+
+
+
+
+
+ ))}
+
+ )}
+
+ );
+
+ return (
+
+ {/* Шапка */}
+
+
+
+ Календарь чемпионатов
+
+
+ {/* Фильтры */}
+
+ }
+ onClick={() => setShowFilters(!showFilters)}
+ size="small"
+ >
+ Фильтры
+
+ {showFilters && (
+
+ Класс
+
+
+ )}
+
+
+
+ {/* Основной контент */}
+
+ {/* Календарь */}
+
+
+ {
+ // Не меняем viewDate при клике на день — только навигация по месяцам
+ if (newValue.getMonth() !== viewDate.getMonth()) {
+ setViewDate(newValue);
+ }
+ }}
+ showDaysOutsideCurrentMonth
+ skipDisabledDateSelection
+ sx={{
+ height: '100%',
+ '.MuiDayCalendar-dayButton': {
+ minHeight: { xs: 80, sm: 100 },
+ display: 'flex',
+ flexDirection: 'column',
+ alignItems: 'center',
+ justifyContent: 'flex-start',
+ p: 1,
+ },
+ '.MuiPickersDay-root': {
+ fontSize: { xs: '1rem', sm: '1.1rem' },
+ },
+ // Подсветка дней с событиями
+ '&.MuiPickersDay-root[data-selected="true"]': {
+ backgroundColor: 'primary.main',
+ color: 'white',
+ },
+ '&.MuiPickersDay-root:has(.event-chip)': {
+ border: '2px solid',
+ borderColor: 'primary.main',
+ '&:hover': {
+ borderColor: 'primary.dark',
+ }
+ }
+ }}
+ dayRenderer={(params) => {
+ const dayKey = params.day.toDateString();
+ const events = stagesByDate[dayKey] || [];
+
+
+ return (
+
+
+ {params.day.getDate()}
+
+ {events.length > 0 && (
+
+ {events.map((event, idx) => (
+
+ ))}
+
+ )}
+
+ );
+ }}
+ />
+
+
+
+ {/* Список этапов (только на десктопе) */}
+ {!isMobile && (
+
+
+
+
+ Этапы на {viewDate.toLocaleDateString('ru-RU', { month: 'long', year: 'numeric' })}
+
+ {filteredStages.length === 0 ? (
+
+ Нет этапов в выбранном периоде
+
+ ) : (
+ filteredStages.map((stage) => (
+
+
+ {stage.title}
+
+
+ {stage.stage} · {stage.class}
+
+
+
+
+
+ {new Date(stage.date).toLocaleDateString('ru-RU', {
+ day: 'numeric',
+ month: 'long',
+ weekday: 'short',
+ })}
+
+
+ ))
+ )}
+
+
+
+ )}
+
+
+ {/* Drawer для мобильных */}
+ {isMobile && (
+ setOpenDrawer(false)}
+ PaperProps={{ sx: { borderRadius: '16px 16px 0 0' } }}
+ >
+ {drawerContent}
+
+ )}
+
+ {/* Кнопка для открытия drawer на мобильных */}
+ {isMobile && filteredStages.length > 0 && (
+
+
+
+ )}
+
+ {/* Подвал */}
+
+
+ © 2026 КартХолл. Календарь соревнований.
+
+
+ );
+};
+
+export default CalendarPage;
diff --git a/front/src/app/pages/championship/ChampionshipPage.js b/front/src/app/pages/championship/ChampionshipPage.js
new file mode 100644
index 0000000..090e28c
--- /dev/null
+++ b/front/src/app/pages/championship/ChampionshipPage.js
@@ -0,0 +1,226 @@
+// src/pages/ChampionshipPage.jsx
+import React, { useState, useEffect } from 'react';
+import {
+ Container,
+ Typography,
+ Paper,
+ Box,
+ Button,
+ Chip,
+ Divider,
+ Alert,
+ CircularProgress
+} from '@mui/material';
+import { Launch as LaunchIcon, Download as DownloadIcon } from '@mui/icons-material';
+import { useParams } from 'react-router-dom';
+
+// Импортируем константы (из предыдущего файла)
+import { MOCK_STAGES, STAGE_STATUSES } from '../../../data/constants';
+
+const ChampionshipPage = () => {
+ const { id } = useParams();
+ const [championship, setChampionship] = useState(null);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(false);
+
+ useEffect(() => {
+ // Имитируем запрос на сервер
+ const fetchChampionship = async () => {
+ setLoading(true);
+ setError(false);
+
+ try {
+ // В реальном проекте тут будет fetch(`/api/championships/${id}`)
+ // Для примера фильтруем MOCK_STAGES по id этапа (упрощённо)
+ const stagesForChampionship = MOCK_STAGES.filter(stage => stage.id === Number(id));
+
+ if (stagesForChampionship.length === 0) {
+ setError(true);
+ } else {
+ // Формируем данные чемпионата на основе первого подходящего этапа
+ const stage = stagesForChampionship[0];
+ setChampionship({
+ id: stage.id,
+ title: stage.title,
+ description: stage.description || 'Информация о чемпионате отсутствует.',
+ regulationsUrl: '/dummy-regulations.pdf', // Условный URL регламента
+ stages: MOCK_STAGES.filter(s => s.title === stage.title) // Все этапы этого чемпионата
+ });
+ }
+ } catch (err) {
+ setError(true);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ fetchChampionship();
+ }, [id]);
+
+ if (loading) {
+ return (
+
+
+ Загрузка данных чемпионата...
+
+ );
+ }
+
+ if (error || !championship) {
+ return (
+
+
+ Чемпионат не найден или произошла ошибка при загрузке данных.
+
+
+
+ );
+ }
+
+ // Сортируем этапы: сначала предстоящие (регистрация открыта / идёт), потом прошедшие
+ const sortedStages = [...championship.stages].sort((a, b) => {
+ const isAPast = a.status === STAGE_STATUSES.COMPLETED;
+ const isBPast = b.status === STAGE_STATUSES.COMPLETED;
+ if (isAPast && !isBPast) return 1;
+ if (!isAPast && isBPast) return -1;
+ return 0; // Сохраняем порядок для этапов одного типа
+ });
+
+ return (
+
+ {/* Заголовок и основная информация */}
+
+ {championship.title}
+
+
+
+ {championship.description}
+
+
+ {/* Кнопка загрузки регламента */}
+ }
+ href={championship.regulationsUrl}
+ target="_blank"
+ rel="noopener noreferrer"
+ sx={{ mb: 4 }}
+ >
+ Скачать регламент
+
+
+
+
+ {/* Список этапов */}
+ Этапы чемпионата
+
+
+
+ {sortedStages.map((stage) => (
+ window.location.replace("/stages/" + stage.id)}
+ >
+
+
+ {stage.stage}
+
+
+
+
+
+ {new Date(stage.date).toLocaleDateString('ru-RU', {
+ weekday: 'long',
+ day: 'numeric',
+ month: 'long',
+ year: 'numeric'
+ })}
+
+
+
+ Место: {stage.location}
+
+
+ {stage.description && (
+
+ {stage.description}
+
+ )}
+
+ {/* Бейджи вместо кнопок */}
+
+ {stage.status === STAGE_STATUSES.REGISTRATION_OPEN && (
+ }
+ />
+ )}
+ {stage.status === STAGE_STATUSES.COMPLETED && (
+ }
+ />
+ )}
+ {(stage.status === STAGE_STATUSES.GOING ||
+ stage.status === STAGE_STATUSES.PRE_REGISTRATION) && (
+
+ )}
+
+
+ ))}
+
+
+
+
+
+ © 2026 КартХолл. Все права защищены.
+
+
+ );
+};
+
+export default ChampionshipPage;
diff --git a/front/src/app/pages/championship/ChampionshipsPage.js b/front/src/app/pages/championship/ChampionshipsPage.js
new file mode 100644
index 0000000..d223c25
--- /dev/null
+++ b/front/src/app/pages/championship/ChampionshipsPage.js
@@ -0,0 +1,195 @@
+import React from 'react';
+import {
+ Box,
+ Button,
+ Card,
+ CardActions,
+ CardContent,
+ Container,
+ Grid,
+ IconButton,
+ Paper,
+ Table,
+ TableBody,
+ TableCell,
+ TableRow,
+ Tooltip,
+ Typography,
+} from '@mui/material';
+import {Add as AddIcon, Edit as EditIcon, Visibility as VisibilityIcon} from '@mui/icons-material';
+import Divider from "@mui/material/Divider";
+
+// Данные чемпионатов (можно заменить на API-запрос)
+const CHAMPIONSHIPS = [
+ {
+ id: 1,
+ title: 'SWC Зимний чемпионат 2025–2026',
+ season: 'Зима 2025–2026',
+ stages: 5,
+ status: 'Идёт',
+ classes: ['Юниоры', 'Взрослые', 'Богатыри', '35+'],
+ startDate: '18.01.2026',
+ endDate: '08.03.2026',
+ },
+ {
+ id: 2,
+ title: 'Honda Winter Cup 2026',
+ season: 'Зима 2026',
+ stages: 3,
+ status: 'Регистрация открыта',
+ classes: ['Pro', 'Amateur'],
+ startDate: '31.01.2026',
+ endDate: '28.02.2026',
+ },
+ {
+ id: 3,
+ title: 'Кубок Покровска 2026 (онлайн)',
+ season: '2026',
+ stages: 4,
+ status: 'Предрегистрация',
+ classes: ['Симулятор A', 'Симулятор B'],
+ startDate: '01.02.2026',
+ endDate: '25.03.2026',
+ },
+];
+
+const ChampionshipsPage = () => {
+ return (
+
+ {/* Заголовок и кнопка создания */}
+
+
+ Все чемпионаты
+
+
+
+
+
+ {/* Список чемпионатов */}
+
+ {CHAMPIONSHIPS.map((champ) => (
+
+
+
+
+
+
+ {champ.title}
+
+
+
+ {champ.status}
+
+
+
+ {champ.season}
+
+
+
+
+
+ {/* Таблица с основными данными */}
+
+
+
+ Этапы
+ {champ.stages}
+
+
+ Классы
+
+ {champ.classes.join(', ')}
+
+
+
+ Начало
+ {champ.startDate}
+
+
+ Конец
+ {champ.endDate}
+
+
+
+
+
+ {/* Действия */}
+
+
+
+
+
+
+
+
+
+
+
+ ))}
+
+
+ {/* Сообщение, если чемпионатов нет */}
+ {CHAMPIONSHIPS.length === 0 && (
+
+
+ Пока нет ни одного чемпионата. Создайте первый!
+
+
+
+ )}
+
+ {/* Подвал */}
+
+
+ © 2026 КартХолл. Управление чемпионатами.
+
+
+ );
+};
+
+export default ChampionshipsPage;
diff --git a/front/src/app/pages/cocktails/MenuPage.js b/front/src/app/pages/cocktails/MenuPage.js
deleted file mode 100644
index 2d7bfde..0000000
--- a/front/src/app/pages/cocktails/MenuPage.js
+++ /dev/null
@@ -1,7 +0,0 @@
-import CocktailsPageContent from "./CocktailsPageContent";
-
-export function MenuPage() {
- return (
-
- )
-}
\ No newline at end of file
diff --git a/front/src/app/pages/cocktails/EditCocktailPage.js b/front/src/app/pages/home/EditCocktailPage.js
similarity index 100%
rename from front/src/app/pages/cocktails/EditCocktailPage.js
rename to front/src/app/pages/home/EditCocktailPage.js
diff --git a/front/src/app/pages/home/HomePageContent.js b/front/src/app/pages/home/HomePageContent.js
new file mode 100644
index 0000000..af843f6
--- /dev/null
+++ b/front/src/app/pages/home/HomePageContent.js
@@ -0,0 +1,332 @@
+import Grid from "@mui/material/Grid";
+import * as React from "react";
+import {Card, CardContent} from "@mui/material";
+import {useUser} from "../../../hooks/useUser";
+import Typography from "@mui/material/Typography";
+import Box from "@mui/material/Box";
+import Button from "@mui/material/Button";
+import Divider from "@mui/material/Divider";
+import Container from "@mui/material/Container";
+import Avatar from "@mui/material/Avatar";
+import {Trophy} from "@phosphor-icons/react";
+
+// Компонент таймера (упрощённый)
+const CountdownTimer = ({targetDate}) => {
+ const [timeLeft, setTimeLeft] = React.useState({});
+
+ React.useEffect(() => {
+ const updateTimer = () => {
+ const now = new Date().getTime();
+ const distance = new Date(targetDate).getTime() - now;
+
+ const days = Math.floor(distance / (1000 * 60 * 60 * 24));
+ const hours = Math.floor((distance % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
+ const minutes = Math.floor((distance % (1000 * 60 * 60)) / (1000 * 60));
+
+ const seconds = Math.floor((distance % (1000 * 60)) / 1000);
+
+ setTimeLeft({days, hours, minutes, seconds});
+ };
+
+ const interval = setInterval(updateTimer, 1000);
+ updateTimer();
+
+ return () => clearInterval(interval);
+ }, [targetDate]);
+
+
+ return (
+
+ До старта: {timeLeft.days}д {timeLeft.hours}ч {timeLeft.minutes}м {timeLeft.seconds}с
+
+ );
+};
+
+
+// Основной компонент приветственного блока
+const HomePageContent = () => {
+ const {user} = useUser();
+
+ // Данные чемпионатов (можно подтянуть из API/state)
+ const championships = [
+ {
+ title: 'SWC Зимний чемпионат 2025–2026',
+ stage: '2‑й этап',
+ date: '08.02.2026',
+ link: '/swc-registration',
+ icon: ,
+ },
+ {
+ title: 'Honda Winter Cup 2026',
+ stage: '1‑й этап',
+ date: '31.01.2026',
+ link: '/hwc-info',
+ icon: ,
+ },
+ {
+ title: 'Кубок Покровска 2026 (онлайн)',
+ stage: '1‑й этап',
+ date: '01.02.2026',
+ link: '/pokrovsk-sim',
+ icon: ,
+ },
+ ];
+
+ // Результаты последнего этапа
+ const results = {
+ Юниоры: ['Пупкин Петя', 'Иванов Ваня', 'Кот Кирилл'],
+ Взрослые: ['Пупкин Петя', 'Иванов Ваня', 'Кот Кирилл'],
+ Богатыри: ['Пупкин Петя', 'Иванов Ваня', 'Кот Кирилл'],
+ '35+': ['Пупкин Петя', 'Иванов Ваня', 'Кот Кирилл'],
+ };
+
+
+ // Спонсоры (макеты URL)
+ const sponsors = [
+ {name: 'Минеральная вода Ульянка', logo: '/sponsors/ulyanka.png'},
+ {name: 'Gosha Racing Team', logo: '/sponsors/gosha.png'},
+ {name: 'Diff', logo: '/sponsors/diff.png'},
+ ];
+
+
+ return (
+
+ {/* 1. Приветствие */}
+
+ Добро пожаловать на платформу «КартХолл»!
+
+
+ Ваш гид по гоночным чемпионатам, этапам и онлайн‑соревнованиям.
+
+
+ {/* 2. Карточки анонсов */}
+
+ {championships.map((champ, index) => (
+
+
+
+
+ {champ.icon}
+
+ {champ.title}
+
+
+
+ {champ.stage} — {champ.date}
+
+
+
+
+
+ ))}
+
+
+ {/* 3. Быстрые действия */}
+
+
+
+
+
+
+ {/* 4. Спонсоры */}
+
+ {sponsors.map((sponsor, idx) => (
+
+ ))}
+
+
+ Наши партнёры: поддержка чемпионатов и призов
+
+
+ {/* 5. Прокат */}
+
+
+ Не гоняешь? Попробуй прокат!
+
+
+ Ощутите адреналин за рулём прокатного карта. Доступно ежедневно с 10:00 до 22:00.
+
+
+
+
+ {/* 6. Таймер до ближайшего этапа (Honda Winter Cup) */}
+
+
+ Следующий этап стартует уже:
+
+
+
+
+ {/* 7. Результаты последнего этапа */}
+
+
+ Итоги 1‑го этапа SWC (18.01.2026)
+
+ {Object.entries(results).map(([category, winners]) => (
+
+
+ {category}:
+
+
+ 1 место: {winners[0]}, 2 место: {winners[1]}, 3 место: {winners[2]}
+
+
+ ))}
+
+
+
+ {/* Разделитель и подвал */}
+
+
+ © 2026 КартХолл. Все права защищены.
+
+
+ );
+
+ // return (
+ //
+ //
+ //
+ // Добро пожаловать на платформу «КартХолл»!
+ //
+ //
+ // Ваш гид по гоночным чемпионатам, этапам и онлайн‑соревнованиям.
+ //
+ //
+ //
+ //
+ //
+ //
+ // {anounce.map((item, index) => {
+ // return (
+ // // Карточка анонса
+ //
+ //
+ //
+ // {item.championship}
+ //
+ // handleSelect(row)}
+ // component="img"
+ // alt={item.name}
+ // height="300"
+ //
+ // image={item.photo}
+ // />
+ //
+ // {item.name} - {item.message}
+ //
+ // {/**/}
+ //
+ //
+ //
+ //
+ //
+ //
+ // )
+ // })}
+ //
+ //
+ //
+ //
+ // {/*todo: под вопросом*/}
+ // Быстрые действия
+ //
+ //
+ //
+ //
+ //
+ //
+ //
+ //
+ // {
+ // sponsors.map((item) => {
+ // return (
+ //
+ // handleSelect(row)}
+ // component="img"
+ // alt={item.alt}
+ // height="60"
+ //
+ // image={item.photo}
+ // />
+ //
+ // )
+ // })
+ // }
+ //
+ // Наши партнёры: поддержка чемпионатов и призов
+ //
+ //
+ //
+ //
+ //
+ // window.window.scrollTo(0, 0)}
+ // aria-label='Expand'
+ // color='inherit'>
+ //
+ //
+ //
+ // );
+}
+
+export default HomePageContent;
\ No newline at end of file
diff --git a/front/src/app/pages/home/MenuPage.js b/front/src/app/pages/home/MenuPage.js
new file mode 100644
index 0000000..9000ffc
--- /dev/null
+++ b/front/src/app/pages/home/MenuPage.js
@@ -0,0 +1,7 @@
+import HomePageContent from "./HomePageContent";
+
+export function MenuPage() {
+ return (
+
+ )
+}
\ No newline at end of file
diff --git a/front/src/app/pages/home/anounce.js b/front/src/app/pages/home/anounce.js
new file mode 100644
index 0000000..57e5765
--- /dev/null
+++ b/front/src/app/pages/home/anounce.js
@@ -0,0 +1,26 @@
+export const anounce = [
+ {
+ championship: 'SWC Зимний чемпионат 2025–2026',
+ name: '2‑й этап',
+ date: '08.02.2026',
+ message: 'Открыта регистрация!',
+ new: false,
+ photo: '/assets/background.png'
+ },
+ {
+ championship: 'Honda Winter Cup 2026',
+ name: '1‑й этап',
+ date: '31.01.2026',
+ message: 'Успейте зарегистрироваться: осталось 12 мест',
+ new: true,
+ photo: '/assets/background.png'
+ },
+ {
+ championship: 'Кубок Покровска 2026 (онлайн)',
+ name: '1‑й этап',
+ date: '01.02.2026',
+ message: 'Участвуйте из дома: подключение через симулятор',
+ new: true,
+ photo: '/assets/background.png'
+ },
+]
\ No newline at end of file
diff --git a/front/src/app/pages/home/sponsors.js b/front/src/app/pages/home/sponsors.js
new file mode 100644
index 0000000..e0db765
--- /dev/null
+++ b/front/src/app/pages/home/sponsors.js
@@ -0,0 +1,38 @@
+export const sponsors = [
+ {
+ alt: 'Ульянка',
+ photo: '/assets/logo_ulyanka-1.svg',
+ link: '#'
+ },
+ {
+ alt: 'Ульянка',
+ photo: '/assets/logo_ulyanka-1.svg',
+ link: '#'
+ },
+ {
+ alt: 'Ульянка',
+ photo: '/assets/logo_ulyanka-1.svg',
+ link: '#'
+ },
+ {
+ alt: 'Ульянка',
+ photo: '/assets/logo_ulyanka-1.svg',
+ link: '#'
+ },
+ {
+ alt: 'Ульянка',
+ photo: '/assets/logo_ulyanka-1.svg',
+ link: '#'
+ },
+ {
+ alt: 'Ульянка',
+ photo: '/assets/logo_ulyanka-1.svg',
+ link: '#'
+ },
+ {
+ alt: 'Ульянка',
+ photo: '/assets/logo_ulyanka-1.svg',
+ link: '#'
+ },
+
+]
\ No newline at end of file
diff --git a/front/src/app/pages/stages/StagePage.js b/front/src/app/pages/stages/StagePage.js
new file mode 100644
index 0000000..9c15ff3
--- /dev/null
+++ b/front/src/app/pages/stages/StagePage.js
@@ -0,0 +1,137 @@
+// src/pages/StagePage.jsx
+import React, { useState, useEffect } from 'react';
+import {
+ Container, Typography, Button, Paper, Box, Divider, Alert, Chip
+} from '@mui/material';
+import { Download as DownloadIcon } from '@mui/icons-material';
+import { useParams } from 'react-router-dom';
+import {MOCK_STAGES, STAGE_STATUSES} from '../../../data/constants';
+
+
+const StagePage = () => {
+ const { id } = useParams();
+ const [stage, setStage] = useState(null);
+ const [error, setError] = useState(false);
+
+ useEffect(() => {
+ const foundStage = MOCK_STAGES.find(s => s.id === Number(id));
+ if (foundStage) {
+ setStage(foundStage);
+ } else {
+ setError(true);
+ }
+ }, [id]);
+
+ if (error) {
+ return (
+
+
+ Этап не найден.
+
+
+
+ );
+ }
+
+ if (!stage) {
+ return null; // или спиннер
+ }
+
+ return (
+
+ {stage.title}
+
+ {stage.stage}
+
+
+
+ Информация об этапе
+
+
+
+ Дата
+
+
+ {new Date(stage.date).toLocaleDateString('ru-RU', {
+ weekday: 'long',
+ day: 'numeric',
+ month: 'long',
+ year: 'numeric'
+ })}
+
+
+
+
+ Место проведения
+
+ {stage.location}
+
+
+
+ Класс
+
+ {stage.class}
+
+
+
+ Статус
+
+
+
+
+
+
+
+
+ {stage.description}
+
+
+ {stage.registrationLink && (
+ }
+ sx={{ mt: 2 }}
+ >
+ Перейти на регистрацию
+
+ )}
+
+
+
+
+ © 2026 КартХолл. Все права защищены.
+
+
+ );
+};
+
+export default StagePage;
diff --git a/front/src/app/pages/stages/StagesPage.js b/front/src/app/pages/stages/StagesPage.js
new file mode 100644
index 0000000..aa9b5da
--- /dev/null
+++ b/front/src/app/pages/stages/StagesPage.js
@@ -0,0 +1,230 @@
+import React, { useState, useEffect } from 'react';
+import {
+ Container,
+ Typography,
+ Paper,
+ Table,
+ TableHead,
+ TableRow,
+ TableCell,
+ TableBody,
+ TableContainer,
+ Select,
+ MenuItem,
+ InputLabel,
+ FormControl,
+ Box,
+ Pagination,
+ Divider, Chip
+} from '@mui/material';
+import { Sort as SortIcon } from '@mui/icons-material';
+
+// Моковые данные этапов
+const MOCK_STAGES = [
+ {
+ id: 1,
+ title: 'SWC Зимний чемпионат',
+ stage: '2‑й этап',
+ date: '2026-02-08',
+ class: 'Юниоры',
+ status: 'Идёт',
+ },
+ {
+ id: 2,
+ title: 'Honda Winter Cup',
+ stage: '1‑й этап',
+ date: '2026-01-31',
+ class: 'Pro',
+ status: 'Регистрация открыта',
+ },
+ {
+ id: 3,
+ title: 'Кубок Покровска (онлайн)',
+ stage: '1‑й этап',
+ date: '2026-02-01',
+ class: 'Симулятор A',
+ status: 'Предрегистрация',
+ },
+ {
+ id: 4,
+ title: 'Гран-при Урала',
+ stage: 'Финальный этап',
+ date: '2026-03-15',
+ class: 'Взрослые',
+ status: 'Предрегистрация',
+ },
+ {
+ id: 5,
+ title: 'Открытый кубок Москвы',
+ stage: 'Квалификационный раунд',
+ date: '2026-01-20',
+ class: 'Amateur',
+ status: 'Завершено',
+ },
+ // Добавим ещё для демонстрации пагинации
+ { id: 6, title: 'Этап 6', stage: 'Тест', date: '2026-04-01', class: 'Pro', status: 'Регистрация открыта' },
+ { id: 7, title: 'Этап 7', stage: 'Тест', date: '2026-05-01', class: 'Юниоры', status: 'Идёт' },
+ { id: 8, title: 'Этап 8', stage: 'Тест', date: '2026-06-01', class: 'Взрослые', status: 'Предрегистрация' },
+];
+
+const StagesPage = () => {
+ const [page, setPage] = useState(1);
+ const [rowsPerPage, setRowsPerPage] = useState(5);
+ const [sortOrder, setSortOrder] = useState('asc');
+ const [filteredStages, setFilteredStages] = useState([]);
+
+ useEffect(() => {
+ let sorted = [...MOCK_STAGES];
+ sorted.sort((a, b) => {
+ const dateA = new Date(a.date);
+ const dateB = new Date(b.date);
+ return sortOrder === 'asc' ? dateA - dateB : dateB - dateA;
+ });
+ setFilteredStages(sorted);
+ }, [sortOrder]);
+
+ const handlePageChange = (event, newPage) => {
+ setPage(newPage);
+ };
+
+ const handleRowsPerPageChange = (event) => {
+ setRowsPerPage(Number(event.target.value));
+ setPage(1);
+ };
+
+ const currentStages = filteredStages.slice(
+ (page - 1) * rowsPerPage,
+ page * rowsPerPage
+ );
+
+ // Обработчик клика по строке
+ const handleRowClick = (stage) => {
+ // Здесь можно:
+ // - перенаправить на страницу этапа: navigate(`/stages/${stage.id}`)
+ // - открыть модалку с деталями
+ // - показать alert (пример ниже)
+ alert(`
+ Этап: ${stage.title}
+ Дата: ${new Date(stage.date).toLocaleDateString('ru-RU')}
+ Статус: ${stage.status}
+ Описание: ${stage.description}
+ `);
+ };
+
+ return (
+
+
+ Этапы соревнований
+
+
+
+
+ Сортировка
+
+
+
+
+ Строк на странице
+
+
+
+
+
+
+
+
+ Название
+ Этап
+ Дата
+ Класс
+ Статус
+
+
+
+ {currentStages.map((stage) => (
+ handleRowClick(stage)}
+ >
+ {stage.title}
+ {stage.stage}
+
+ {new Date(stage.date).toLocaleDateString('ru-RU')}
+
+ {stage.class}
+
+
+
+
+ ))}
+
+
+
+
+
+
+ Показано {currentStages.length} из {filteredStages.length} этапов
+
+
+
+
+
+
+
+ © 2026 КартХолл. Все права защищены.
+
+
+ );
+};
+
+export default StagesPage;
diff --git a/front/src/components/Ingredients/IngredientCard.js b/front/src/components/Ingredients/IngredientCard.js
index 1289405..8af3cbe 100644
--- a/front/src/components/Ingredients/IngredientCard.js
+++ b/front/src/components/Ingredients/IngredientCard.js
@@ -26,7 +26,7 @@ export function IngredientCard({row, value, infoHandler, changeHandler}) {
infoHandler(row)}>
-
+
diff --git a/front/src/components/navigation/MainNav.js b/front/src/components/navigation/MainNav.js
index 62bd779..5609b6c 100644
--- a/front/src/components/navigation/MainNav.js
+++ b/front/src/components/navigation/MainNav.js
@@ -8,6 +8,10 @@ import {List as ListIcon} from '@phosphor-icons/react/dist/ssr/List';
import {usePopover} from "../../hooks/usePopover";
import {MobileNav} from "./MobileNav";
import {UserPopover} from "../core/UserPopover";
+import Button from "@mui/material/Button";
+import {renderNavItems} from "./NavItem";
+import {navItems} from "../../navItems";
+import {useLocation} from "react-router-dom";
// import Tooltip from "@mui/material/Tooltip";
// import {Badge} from "@mui/material";
// import {useAlert} from "../../hooks/useAlert";
@@ -15,6 +19,8 @@ import {UserPopover} from "../core/UserPopover";
export function MainNav() {
const [openNav, setOpenNav] = React.useState(false);
const userPopover = usePopover();
+ const location = useLocation();
+ const pathname = location.pathname;
return (
<>
@@ -32,9 +38,12 @@ export function MainNav() {
- setOpenNav(true)} sx={{display: {xl: 'none'}}}>
+ setOpenNav(true)} sx={{display: {sm: 'none'}}}>
+
+ {renderNavItems({items: navItems, pathname: pathname, direction: 'row'})}
+
{
const {key, ...item} = curr;
acc.push();
@@ -13,7 +13,7 @@ export function renderNavItems({items = [], pathname}) {
}, []);
return (
-
+
{children}
);
diff --git a/front/src/components/navigation/NavigationMenu.js b/front/src/components/navigation/NavigationMenu.js
index b95118c..f2bf4d1 100644
--- a/front/src/components/navigation/NavigationMenu.js
+++ b/front/src/components/navigation/NavigationMenu.js
@@ -7,15 +7,6 @@ import {navItems} from "../../navItems";
import React, {useEffect, useState} from "react";
import {useLocation} from "react-router-dom";
import {useUser} from "../../hooks/useUser";
-import Typography from "@mui/material/Typography";
-
-function renderSpecialItems(items, label, pathname) {
- return (
-
- {renderNavItems({items: items, pathname: pathname})}
-
- )
-}
export function NavigationMenu() {
const location = useLocation();
@@ -23,17 +14,10 @@ export function NavigationMenu() {
const {user} = useUser();
const [items, setItems] = useState(null)
- const userChild = navItems.filter((item) => !item.forBarmen && !item.forAdmin)
- const barmenChild = navItems.filter((item) => item.forBarmen)
- const adminChild = navItems.filter((item) => item.forAdmin)
-
useEffect(() => {
- const role = !user ? "USER" : Object.keys(user).length === 0 ? "USER" : user.role
const newState = (
- {renderNavItems({items: userChild, pathname: pathname})}
- {role !== "USER" && renderSpecialItems(barmenChild, "Для бармена:", pathname)}
- {role === "ADMIN" && renderSpecialItems(adminChild, "Для админа", pathname)}
+ {renderNavItems({items: navItems, pathname: pathname, direction: 'column'})}
)
setItems(newState)
diff --git a/front/src/app/NavigationRoutes.js b/front/src/components/navigation/NavigationRoutes.js
similarity index 58%
rename from front/src/app/NavigationRoutes.js
rename to front/src/components/navigation/NavigationRoutes.js
index 499a1bc..3161a3c 100644
--- a/front/src/app/NavigationRoutes.js
+++ b/front/src/components/navigation/NavigationRoutes.js
@@ -1,19 +1,18 @@
import {Route, Routes} from "react-router-dom";
-import {paths} from "../path";
-import {useAuth} from "../hooks/useAuth";
-import NotFoundPage from "./pages/notFound/NotFoundPage";
-import {UserLayout} from "./layout/UserLayout";
-import {HomeRedirect} from "./HomeRedirect";
-import {PublicLayout} from "./layout/PublicLayout";
-import LoginPage from "./pages/auth/sign-in/loginPage";
-import {TelegramCode} from "./pages/auth/sign-in/telegram-code";
-import {IngredientsPage} from "./pages/ingredients/IngredientsPage";
-import {MenuPage} from "./pages/cocktails/MenuPage";
-import {EditIngredientPage} from "./pages/ingredients/EditIngredientPage";
-import {EditCocktailPage} from "./pages/cocktails/EditCocktailPage";
+import {paths} from "../../path";
+import {useAuth} from "../../hooks/useAuth";
+import NotFoundPage from "../../app/pages/notFound/NotFoundPage";
+import {UserLayout} from "../../app/layout/UserLayout";
+import {PublicLayout} from "../../app/layout/PublicLayout";
+import LoginPage from "../../app/pages/auth/sign-in/loginPage";
+import {TelegramCode} from "../../app/pages/auth/sign-in/telegram-code";
+import {MenuPage} from "../../app/pages/home/MenuPage";
import {useEffect, useState} from "react";
-import {BarChangePage} from "./pages/BarChangePage";
-import {CalcPage} from "./pages/calc/CalcPage";
+import ChampionshipsPage from "../../app/pages/championship/ChampionshipsPage";
+import CalendarPage from "../../app/pages/calendar/CalendarPage";
+import StagesPage from "../../app/pages/stages/StagesPage";
+import ChampionshipPage from "../../app/pages/championship/ChampionshipPage";
+import StagePage from "../../app/pages/stages/StagePage";
export function NavigationRoutes() {
const {auth} = useAuth();
@@ -51,9 +50,10 @@ function ElementProvider({isPrivate, children}) {
const authPages = [
{
- children: (),
- isPrivate: false,
path: paths.home,
+ isPrivate: true,
+ children: (),
+ exact: true,
},
{
path: paths.auth.signIn,
@@ -61,37 +61,29 @@ const authPages = [
isPrivate: false,
},
{
- path: paths.bar.calc,
- children: (),
- isPrivate: true,
+ path: paths.chp.championships,
+ isPrivate: true,
+ children: (),
},
{
- path: paths.dashboard.overview,
+ path: paths.chp.championship,
isPrivate: true,
- children: (),
- exact: true,
+ children: (),
},
{
- path: paths.bar.list,
+ path: paths.calendar,
isPrivate: true,
- children: (),
+ children: ()
},
{
- path: paths.bar.ingredients,
+ path: paths.stg.stages,
isPrivate: true,
- children: ()
+ children: ()
},
{
- path: paths.bar.ingredientEdit,
+ path: paths.stg.stage,
isPrivate: true,
- forAdmin: true,
- children: ()
- },
- {
- path: paths.bar.cocktailEdit,
- isPrivate: true,
- forAdmin: true,
- children: ()
+ children: ()
},
{
path: paths.notFound,
@@ -102,16 +94,11 @@ const authPages = [
const guestPages = [
{
- path: paths.dashboard.overview,
+ path: paths.home,
isPrivate: true,
children: (),
exact: true,
},
- {
- children: (),
- isPrivate: false,
- path: paths.home,
- },
{
path: paths.auth.tg,
isPrivate: false,