diff --git a/.vibe42-run.log b/.vibe42-run.log new file mode 100644 index 0000000..22aad9a --- /dev/null +++ b/.vibe42-run.log @@ -0,0 +1,8 @@ +--- run Wed Sep 23 10:08:32 AM UTC 2026 :: npm start --silent + +up to date in 475ms +Сервер на порту 3000 +--- run Wed Sep 23 10:09:20 AM UTC 2026 :: npm start --silent + +up to date in 355ms +Сервер на порту 3000 diff --git a/.vibe42-run.pid b/.vibe42-run.pid new file mode 100644 index 0000000..8408670 --- /dev/null +++ b/.vibe42-run.pid @@ -0,0 +1 @@ +290 diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..6af7725 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,10 @@ +{ + "name": "sports-portal", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "sports-portal" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..810b897 --- /dev/null +++ b/package.json @@ -0,0 +1,6 @@ +{ + "name": "sports-portal", + "private": true, + "type": "commonjs", + "scripts": { "start": "node server.js" } +} diff --git a/script.js b/script.js new file mode 100644 index 0000000..9eac1bf --- /dev/null +++ b/script.js @@ -0,0 +1,219 @@ +// СпортПортал — данные и логика (мок-данные, фаза 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 в игре", + }, +]; + +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 ``; +} + +function renderSports() { + const grid = document.getElementById("sports-grid"); + if (!grid) return; + grid.style.gridTemplateColumns = "repeat(4, 1fr)"; + grid.innerHTML = SPORTS.map((s) => ` +
+
+ ${icon(s.icon)} +

${s.name}

+
+

${s.desc}

+
+ ${s.chip} +
+
+ `).join(""); +} + +function renderLeagueTabs() { + const tabs = document.getElementById("league-tabs"); + if (!tabs) return; + tabs.innerHTML = Object.entries(LEAGUES).map(([key, l], i) => ` + + `).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 renderTable(key) { + const league = LEAGUES[key] || LEAGUES.premier; + const table = document.getElementById("league-table"); + if (!table) return; + const head = `#Команда${league.cols.map((c) => `${c}`).join("")}`; + const body = league.rows.map((r, i) => { + const pts = r[1] * 3 + r[2]; + return ` + + ${i + 1} + ${r[0]} + ${pts} + ${r.slice(1).map((v) => `${v}`).join("")} + `; + }).join(""); + table.innerHTML = head + `${body}`; +} + +function renderMatches() { + const strip = document.getElementById("match-strip"); + if (!strip) return; + strip.innerHTML = MATCHES.map((m) => ` +
+
${m.home}${m.away}
+
${m.score}
+
${m.sport} · ${m.time}
+
+ `).join(""); +} + +function renderArticles() { + const grid = document.getElementById("article-grid"); + if (!grid) return; + grid.innerHTML = ARTICLES.map((a) => ` +
+
+ ${a.sport} + ${fmtDate(a.date)} +
+

${a.title}

+

${a.excerpt}

+ Читать далее → +
+ `).join(""); +} + +document.addEventListener("DOMContentLoaded", () => { + renderSports(); + renderLeagueTabs(); + renderTable("premier"); + renderMatches(); + renderArticles(); +}); diff --git a/server.js b/server.js new file mode 100644 index 0000000..f2e59c6 --- /dev/null +++ b/server.js @@ -0,0 +1,24 @@ +const http = require("http"); +const fs = require("fs"); +const path = require("path"); + +const PORT = process.env.PORT || 3000; + +http.createServer((req, res) => { + let file = req.url === "/" ? "/index.html" : req.url.split("?")[0]; + const decoded = decodeURIComponent(file); + const full = path.normalize(path.join(__dirname, decoded)); + if (!full.startsWith(__dirname)) { res.writeHead(403); return res.end(); } + if (!fs.existsSync(full) || !fs.statSync(full).isFile()) { res.writeHead(404); return res.end("Not found"); } + const ext = path.extname(full).toLowerCase(); + const type = { + ".html": "text/html", + ".css": "text/css", + ".js": "application/javascript", + ".svg": "image/svg+xml", + ".png": "image/png", + ".ico": "image/x-icon", + }[ext] || "application/octet-stream"; + res.writeHead(200, { "Content-Type": type + "; charset=utf-8" }); + res.end(fs.readFileSync(full)); +}).listen(PORT, () => console.log("Сервер на порту " + PORT));