/* parts2.jsx — расписание с фильтрами, таблицы групп, паблы, бронирование */ const { useState: useState2, useMemo } = React; const W2 = window.WC; const U2 = window.U; /* ════════ РАСПИСАНИЕ ════════ */ function Schedule({ cur, favs, toggleFav }) { const [mode, setMode] = useState2('group'); const [day, setDay] = useState2(null); // null = все дни const [favOnly, setFavOnly] = useState2(false); const days = W2.groupDays; const byDay = useMemo(() => { const map = {}; W2.matches.forEach((m) => {(map[m.date] = map[m.date] || []).push(m);}); Object.values(map).forEach((arr) => arr.sort((a, b) => a.time.localeCompare(b.time))); return map; }, []); const passFav = (m) => !favOnly || favs.has(m.home) || favs.has(m.away); const shownDays = (day ? [day] : days).filter((d) => (byDay[d] || []).some(passFav)); return (
Расписание

Все матчи
турнира

{mode === 'group' ? <>
{days.map((d) => )}
{day ? U2.fmtLong(day) : 'Туры 1–3 · 11–27 июня'}
{shownDays.length === 0 ?
Нет матчей с избранными командами в этот день. Отметьте команды звёздочкой ★ или снимите фильтр.
: shownDays.map((d) =>
{U2.fmtLong(d)} · {U2.wdShort(d)}
{byDay[d].filter(passFav).map((m) => )}
)} :
{W2.knockout.map((r) =>
{r.name}
{r.dates}{r.city ? ' · ' + r.city : ''} · пары по итогам групп
{r.count}{matchWord(r.count)}
)}
}
); } /* ════════ ТАБЛИЦЫ ГРУПП ════════ */ function Standings({ favs }) { const [g, setG] = useState2('A'); const keys = Object.keys(W2.groups); const teams = W2.groups[g]; return (
Турнирная таблица

Группы
A — L

{keys.map((k) => )}
{teams.map((code, i) =>
{i + 1} {U2.teamName(code)} {favs.has(code) && } 0 матчей
)}
1–2 место — в плей-офф напрямую
Плюс 8 лучших сборных с 3-го места проходят в 1/16 финала.
); } /* ════════ ПАБЫ ════════ */ function BarMonogram({ name }) { return {name[0]}; } function Bars({ cur, onBook }) { return (
Где смотреть

ПАБЫ

Гастрономические пабы Петербурга. Выбирайте ближайший и бронируйте стол под нужный матч.
{W2.bars.map((b) =>
{b.name}
Гастрономический паб
{b.addr}
{b.phones.map((p) =>
{p}
)}
)}
); } /* ════════ МОДАЛКА БРОНИРОВАНИЯ ════════ */ function BookingModal({ bar, cur, onClose }) { const futureDays = W2.groupDays.filter((d) => d >= cur); const days = futureDays.length ? futureDays : W2.groupDays; const [date, setDate] = useState2(days[0]); const [matchId, setMatchId] = useState2(''); const [guests, setGuests] = useState2(2); const [name, setName] = useState2(''); const [phone, setPhone] = useState2(''); const [done, setDone] = useState2(false); const dayMatches = U2.matchesOn(date).sort((a, b) => a.time.localeCompare(b.time)); function submit(e) { e.preventDefault(); setDone(true); } return (
e.stopPropagation()}>
{done ?

Заявка принята

{bar.name} · {U2.fmtLong(date)} · {guests} {guestWord(guests)}.
Мы перезвоним для подтверждения на {phone || 'указанный номер'}.

Демо-форма. Реальная отправка не выполняется.
:

Бронь · {bar.name}

{bar.addr} · {bar.phones[0]}
setName(e.target.value)} placeholder="Как к вам обращаться" />
setPhone(e.target.value)} placeholder="+7 (___) ___-__-__" inputMode="tel" />
Демонстрационная форма для прототипа.
}
); } function guestWord(n) { const a = n % 10,b = n % 100; if (a === 1 && b !== 11) return 'гость'; if (a >= 2 && a <= 4 && (b < 10 || b >= 20)) return 'гостя'; return 'гостей'; } Object.assign(window, { Schedule, Standings, Bars, BookingModal, guestWord });