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 ( + + {/* Шапка */} + + + + Календарь чемпионатов + + + {/* Фильтры */} + + + {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} + + + {/* Кнопка загрузки регламента */} + + + + + {/* Список этапов */} + Этапы чемпионата + + + + {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 && ( + + )} + + + + + © 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,