305 lines
12 KiB
JavaScript
305 lines
12 KiB
JavaScript
// СпортПортал — данные и логика (мок-данные, фаза 1)
|
||
|
||
const SPORTS = [
|
||
{
|
||
icon: "trophy",
|
||
name: "Футбол",
|
||
desc: "Премьер-лига, Кубок, трансферы и лучшие моменты.",
|
||
chip: "12 матчей сегодня",
|
||
},
|
||
{
|
||
icon: "dashboard",
|
||
name: "Теннис",
|
||
desc: "Туры ATP/WTA, турниры Большой четвёрки, рейтинги.",
|
||
chip: "Полуфиналы турнира",
|
||
},
|
||
{
|
||
icon: "chart",
|
||
name: "Хоккей",
|
||
desc: "КХЛ, НХЛ: очки, шайбы и гонка бомбардиров.",
|
||
chip: "Турнирная таблица КХЛ",
|
||
},
|
||
{
|
||
icon: "users",
|
||
name: "Бокс",
|
||
desc: "Большие бои, рейтинги и история рекордов.",
|
||
chip: "Пояс WBA в игре",
|
||
},
|
||
];
|
||
|
||
// Демо-данные: работают только если API не доступно (например, в Предпросмотре без сервера)
|
||
const LEAGUES = {
|
||
premier: {
|
||
label: "АПЛ, Англия",
|
||
cols: ["И", "В", "Н", "П", "ГЗ", "ГП"],
|
||
rows: [
|
||
["Арсенал", 38, 24, 10, 4, 78, 32],
|
||
["Ливерпуль", 38, 23, 11, 4, 85, 34],
|
||
["Манчестер Сити", 38, 22, 8, 8, 80, 41],
|
||
["Челси", 38, 20, 8, 10, 64, 42],
|
||
["Лестер", 38, 19, 7, 12, 58, 44],
|
||
["Тоттенхэм", 38, 18, 7, 13, 54, 44],
|
||
],
|
||
},
|
||
laliga: {
|
||
label: "Ла Лига, Испания",
|
||
cols: ["И", "В", "Н", "П", "ГЗ", "ГП"],
|
||
rows: [
|
||
["Реал Мадрид", 38, 27, 4, 7, 78, 33],
|
||
["Барселона", 38, 26, 5, 7, 87, 38],
|
||
["Атлетико", 38, 21, 9, 8, 64, 35],
|
||
["Атлетик", 38, 17, 11, 10, 47, 36],
|
||
["Райо", 38, 15, 10, 13, 44, 45],
|
||
["Вильярреал", 38, 14, 12, 12, 55, 43],
|
||
],
|
||
},
|
||
khl: {
|
||
label: "КХЛ",
|
||
cols: ["И", "В", "Н", "П", "ШЗ", "ШП"],
|
||
rows: [
|
||
["СКА", 60, 42, 6, 12, 180, 98],
|
||
["ЦСКА", 60, 40, 6, 14, 172, 101],
|
||
["Ак Барс", 60, 38, 5, 17, 160, 97],
|
||
["Локомотив", 60, 35, 6, 19, 148, 104],
|
||
["Металлург", 60, 32, 5, 23, 137, 115],
|
||
["Авангард", 60, 30, 5, 25, 140, 121],
|
||
],
|
||
},
|
||
nhl: {
|
||
label: "НХЛ",
|
||
cols: ["И", "В", "Н", "П", "ШЗ", "ШП"],
|
||
rows: [
|
||
["Эдмонтон", 82, 48, 10, 24, 290, 238],
|
||
["Виннипег", 82, 46, 10, 26, 271, 227],
|
||
["Филадельфия", 82, 44, 10, 28, 263, 237],
|
||
["Даллас", 82, 44, 0, 38, 270, 215],
|
||
["Колорадо", 82, 42, 8, 32, 286, 235],
|
||
["Нэшвилл", 82, 41, 8, 33, 265, 240],
|
||
],
|
||
},
|
||
};
|
||
|
||
const MATCHES = [
|
||
{ sport: "Футбол", home: "Арсенал", away: "Челси", score: "2:1", time: "Финал" },
|
||
{ sport: "Футбол", home: "Реал Мадрид", away: "Барселона", score: "1:1", time: "110-я минута" },
|
||
{ sport: "Теннис", home: "Сафара", away: "Медведев", score: "3:2", time: "Победа Сафары" },
|
||
{ sport: "Хоккей", home: "СКА", away: "ЦСКА", score: "3:2", time: "После буллитов" },
|
||
{ sport: "Бокс", home: "Алимхан", away: "Гарсия", score: "TKO", time: "раунд 9" },
|
||
];
|
||
|
||
const ARTICLES = [
|
||
{
|
||
sport: "Футбол",
|
||
date: "22.09.2026",
|
||
title: "Как Премьер-лига дошла до рекордной гонки за титул",
|
||
excerpt: "Впервые за десять лет три команды делят первые места с разницей в два очка. Разбираем, что решает чемпионство.",
|
||
},
|
||
{
|
||
sport: "Теннис",
|
||
date: "21.09.2026",
|
||
title: "Медведев и Сафара: разбор полуфинала турнира",
|
||
excerpt: "Пять сетов, 3 часа 12 минут. Ключевые моменты и статистика матча, который войдёт в историю турнира.",
|
||
},
|
||
{
|
||
sport: "Хоккей",
|
||
date: "20.09.2026",
|
||
title: "Гонка бомбардиров КХЛ: кто впереди и почему",
|
||
excerpt: "Три форварда набрали больше 40 очков. Сравниваем их стили игры и роли в командах.",
|
||
},
|
||
{
|
||
sport: "Бокс",
|
||
date: "19.09.2026",
|
||
title: "Большой бой года: что ждать от реванша",
|
||
excerpt: "Два чемпиона, два стиля и титул в двух версиях. Прогнозы экспертов и история их предыдущего боя.",
|
||
},
|
||
{
|
||
sport: "Футбол",
|
||
date: "18.09.2026",
|
||
title: "Молодые вратари, которые изменят Европу",
|
||
excerpt: "Обзор пяти голкиперов до 22 лет, которые в этом сезоне уже доказали класс. Где посмотреть их игры.",
|
||
},
|
||
{
|
||
sport: "Теннис",
|
||
date: "17.09.2026",
|
||
title: "Как изменился грунтовый сезон за десять лет",
|
||
excerpt: "Цифры и тренды: скорости подачи, длина матчей и новые герои сезона на медленных кортах.",
|
||
},
|
||
];
|
||
|
||
const fmtDate = (s) => s;
|
||
|
||
function icon(name) {
|
||
return `<svg class="kt-ai-icon" aria-hidden="true"><use href="#${name}"></use></svg>`;
|
||
}
|
||
|
||
function renderSports() {
|
||
const grid = document.getElementById("sports-grid");
|
||
if (!grid) return;
|
||
grid.style.gridTemplateColumns = "repeat(4, 1fr)";
|
||
grid.innerHTML = SPORTS.map((s) => `
|
||
<article class="kt-ai-tile" style="display:flex; flex-direction:column;">
|
||
<div class="sport-tile-head">
|
||
<span class="sport-badge">${icon(s.icon)}</span>
|
||
<h3>${s.name}</h3>
|
||
</div>
|
||
<p>${s.desc}</p>
|
||
<div class="tile-more">
|
||
<span class="kt-ai-chip">${s.chip}</span>
|
||
</div>
|
||
</article>
|
||
`).join("");
|
||
}
|
||
|
||
const TABLE_COLS = ["И", "В", "Н", "П", "ГЗ", "ГП"];
|
||
const CURRENT = {
|
||
premier: { label: "АПЛ, Англия", cols: TABLE_COLS, rows: [], live: true },
|
||
laliga: { label: "Ла Лига, Испания", cols: TABLE_COLS, rows: [], live: true },
|
||
bundesliga: { label: "Бундеслига, Германия", cols: TABLE_COLS, rows: [], live: true },
|
||
seriea: { label: "Серия А, Италия", cols: TABLE_COLS, rows: [], live: true },
|
||
};
|
||
const ZONES = {
|
||
premier: { ucl: 4, uel: 1, releg: 3 },
|
||
laliga: { ucl: 4, uel: 1, releg: 3 },
|
||
bundesliga: { ucl: 4, uel: 1, releg: 3 },
|
||
seriea: { ucl: 4, uel: 0, releg: 3 },
|
||
};
|
||
|
||
function applyLive(key, payload) {
|
||
if (!payload || !Array.isArray(payload.rows)) return;
|
||
CURRENT[key] = {
|
||
label: payload.name || CURRENT[key].label,
|
||
cols: CURRENT[key].cols,
|
||
rows: payload.rows.map((r) => [r.name, r.played, r.win, r.draw, r.lost, r.gf, r.ga]),
|
||
live: true,
|
||
};
|
||
}
|
||
|
||
function liveError(key) {
|
||
CURRENT[key].rows = [];
|
||
CURRENT[key].live = false;
|
||
}
|
||
|
||
async function loadStandings() {
|
||
try {
|
||
const res = await fetch("api/standings");
|
||
const j = await res.json();
|
||
if (!j.ok || !j.data) throw new Error("bad payload");
|
||
for (const key of Object.keys(CURRENT)) {
|
||
const d = j.data[key];
|
||
if (!d || d.error) { liveError(key); continue; }
|
||
applyLive(key, d);
|
||
}
|
||
const anyLive = Object.values(CURRENT).some((l) => l.live);
|
||
const status = document.getElementById("table-status");
|
||
if (status) {
|
||
const when = new Date(j.updatedAt).toLocaleString("ru-RU", { day: "2-digit", month: "2-digit", hour: "2-digit", minute: "2-digit" });
|
||
status.innerHTML = anyLive
|
||
? `<span class="kt-ai-chip">обновлено в ${when}</span><span>источник: TheSportsDB · повтор через 15 минут</span>`
|
||
: `<span>нет связи с источником — показаны демо-данные</span>`;
|
||
}
|
||
for (const key of Object.keys(CURRENT)) {
|
||
if (!CURRENT[key].live) CURRENT[key].rows = (LEAGUES[key] && LEAGUES[key].rows) || [];
|
||
}
|
||
return anyLive;
|
||
} catch (_) {
|
||
for (const key of Object.keys(CURRENT)) {
|
||
CURRENT[key].rows = (LEAGUES[key] && LEAGUES[key].rows) || [];
|
||
CURRENT[key].live = false;
|
||
}
|
||
const status = document.getElementById("table-status");
|
||
if (status) status.innerHTML = `<span>нет связи с источником — показаны демо-данные</span>`;
|
||
return false;
|
||
}
|
||
}
|
||
|
||
function renderLeagueTabs() {
|
||
const tabs = document.getElementById("league-tabs");
|
||
if (!tabs) return;
|
||
tabs.innerHTML = Object.entries(CURRENT).map(([key, l], i) => `
|
||
<button class="kt-ai-btn" data-league="${key}" data-variant="${i === 0 ? "primary" : "ghost"}">${l.label}</button>
|
||
`).join("");
|
||
tabs.addEventListener("click", (e) => {
|
||
const btn = e.target.closest("[data-league]");
|
||
if (!btn) return;
|
||
tabs.querySelectorAll("button").forEach((b) => b.setAttribute("data-variant", "ghost"));
|
||
btn.setAttribute("data-variant", "primary");
|
||
renderTable(btn.dataset.league);
|
||
});
|
||
}
|
||
|
||
function zoneClass(key, i, total) {
|
||
const z = ZONES[key] || {};
|
||
if (i < (z.ucl || 0)) return "zone-1";
|
||
if (i < (z.ucl || 0) + (z.uel || 0)) return "zone-2";
|
||
if (i >= total - (z.releg || 0)) return "zone-4";
|
||
return "";
|
||
}
|
||
|
||
function renderTable(key) {
|
||
const league = CURRENT[key] || CURRENT.premier;
|
||
const table = document.getElementById("league-table");
|
||
if (!table) return;
|
||
if (!league.rows.length) {
|
||
table.innerHTML = `<tr><td style="padding: 16px; color: var(--kt-ai-fg-muted)">Данные по этой лиге временно недоступны.</td></tr>`;
|
||
return;
|
||
}
|
||
const total = league.rows.length;
|
||
const head = `<tr><th>#</th><th>Команда</th>${league.cols.map((c) => `<th>${c}</th>`).join("")}</tr>`;
|
||
const body = league.rows.map((r, i) => {
|
||
const pts = r[2] * 3 + r[3];
|
||
const zone = zoneClass(key, i, total);
|
||
return `
|
||
<tr class="${zone}">
|
||
<td><span class="pos${i === 0 ? " lead" : ""}">${i + 1}</span></td>
|
||
<td>${r[0]}</td>
|
||
<td style="text-align:center; font-variant-numeric: tabular-nums; font-weight:500">${pts}</td>
|
||
${r.slice(1).map((v) => `<td style="text-align:center; font-variant-numeric: tabular-nums;">${v}</td>`).join("")}
|
||
</tr>`;
|
||
}).join("");
|
||
table.innerHTML = head + `<tbody class="nums">${body}</tbody>`;
|
||
const legend = document.querySelector(".zone-legend");
|
||
if (legend) legend.innerHTML = `<span class="leg-white">Лига чемпионов</span><span class="leg-mut">Еврокубки</span><span class="leg-dim">Вылет</span>`;
|
||
}
|
||
|
||
function renderMatches() {
|
||
const strip = document.getElementById("match-strip");
|
||
if (!strip) return;
|
||
strip.innerHTML = MATCHES.map((m) => `
|
||
<div class="match">
|
||
<div class="vs"><span>${m.home}</span><span>${m.away}</span></div>
|
||
<div class="score">${m.score}</div>
|
||
<div class="info">${m.sport} · ${m.time}</div>
|
||
</div>
|
||
`).join("");
|
||
}
|
||
|
||
function renderArticles() {
|
||
const grid = document.getElementById("article-grid");
|
||
if (!grid) return;
|
||
grid.innerHTML = ARTICLES.map((a) => `
|
||
<article class="kt-ai-card article" style="padding: 16px;">
|
||
<div class="article-meta">
|
||
<span class="kt-ai-chip">${a.sport}</span>
|
||
<span>${fmtDate(a.date)}</span>
|
||
</div>
|
||
<h3>${a.title}</h3>
|
||
<p class="excerpt">${a.excerpt}</p>
|
||
<a class="kt-ai-link" href="#articles">Читать далее →</a>
|
||
</article>
|
||
`).join("");
|
||
}
|
||
|
||
document.addEventListener("DOMContentLoaded", async () => {
|
||
renderSports();
|
||
renderLeagueTabs();
|
||
renderMatches();
|
||
renderArticles();
|
||
await loadStandings();
|
||
renderTable("premier");
|
||
setInterval(async () => {
|
||
await loadStandings();
|
||
const activeBtn = document.querySelector("#league-tabs [data-variant='primary']");
|
||
renderTable(activeBtn ? activeBtn.dataset.league : "premier");
|
||
}, 15 * 60 * 1000);
|
||
});
|