proshu-sozdat-sportivnyy-say/script.js

305 lines
12 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// СпортПортал — данные и логика (мок-данные, фаза 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);
});