diff --git a/app/static/script.js b/app/static/script.js index dcf9ab3..19e87c5 100644 --- a/app/static/script.js +++ b/app/static/script.js @@ -1,246 +1,152 @@ -// Function to fetch scoreboard data using AJAX -function fetchScoreboardData() { - var xhr = new XMLHttpRequest(); - xhr.open("GET", "/scoreboard", true); - xhr.onreadystatechange = function () { - if (xhr.readyState === XMLHttpRequest.DONE) { - if (xhr.status === 200) { - updateScoreboard(JSON.parse(xhr.responseText)); - } else { - console.error("Failed to fetch scoreboard data."); - } - } - }; - xhr.send(); +async function fetchScoreboardData() { + try { + const res = await fetch('/scoreboard'); + if (!res.ok) throw new Error(res.status); + updateScoreboard(await res.json()); + } catch (e) { + console.error('Failed to fetch scoreboard data:', e); + } } -// Function to update scoreboard with fetched data function updateScoreboard(data) { - var liveGamesSection = document.getElementById('live-games-section'); - var preGamesSection = document.getElementById('pre-games-section'); - var finalGamesSection = document.getElementById('final-games-section'); + const sections = [ + { sectionId: 'live-section', gridId: 'live-games-section', games: data.live_games, render: renderLiveGame }, + { sectionId: 'pre-section', gridId: 'pre-games-section', games: data.pre_games, render: renderPreGame }, + { sectionId: 'final-section', gridId: 'final-games-section', games: data.final_games, render: renderFinalGame }, + ]; - if (liveGamesSection) { - var liveGamesExist = data && data.live_games && data.live_games.length > 0; - if (liveGamesExist) { - if (!document.getElementById('live-games')) { - var targetElement = document.getElementById('live-games-section'); - var newElement = document.createElement('h1'); - newElement.setAttribute('id', 'live-games'); - newElement.innerText = 'Live Games'; - targetElement.parentNode.insertBefore(newElement, targetElement); - } - liveGamesSection.innerHTML = generateGameBoxes(data.live_games, 'LIVE'); - } else { - var liveGamesElement = document.getElementById('live-games'); - if (liveGamesElement) { - liveGamesElement.remove(); - } - liveGamesSection.innerHTML = ''; - } + for (const { sectionId, gridId, games, render } of sections) { + const section = document.getElementById(sectionId); + const grid = document.getElementById(gridId); + const hasGames = games && games.length > 0; + section.classList.toggle('hidden', !hasGames); + grid.innerHTML = hasGames ? games.map(render).join('') : ''; } - if (preGamesSection) { - var preGamesExist = data && data.pre_games && data.pre_games.length > 0; - if (preGamesExist) { - if (!document.getElementById('on-later')) { - var targetElement = document.getElementById('pre-games-section'); - var newElement = document.createElement('h1'); - newElement.setAttribute('id', 'on-later'); - newElement.innerText = 'Scheduled Games'; - targetElement.parentNode.insertBefore(newElement, targetElement); - } - preGamesSection.innerHTML = generateGameBoxes(data.pre_games, 'PRE'); - } else { - var onLaterElement = document.getElementById('on-later'); - if (onLaterElement) { - onLaterElement.remove(); - } - preGamesSection.innerHTML = ''; - } - } - - if (finalGamesSection) { - var finalGamesExist = data && data.final_games && data.final_games.length > 0; - - // Check if final games exist - if (finalGamesExist) { - // Create or update "Game Over" heading - if (!document.getElementById('game-over')) { - var targetElement = document.getElementById('final-games-section'); - var newElement = document.createElement('h1'); - newElement.setAttribute('id', 'game-over'); - newElement.innerText = 'Game Over'; - targetElement.parentNode.insertBefore(newElement, targetElement); - } - - // Update final games section with generated game boxes - finalGamesSection.innerHTML = generateGameBoxes(data.final_games, 'FINAL'); - } else { - // Remove "Game Over" heading if it exists and clear final games section - var gameOverElement = document.getElementById('game-over'); - if (gameOverElement) { - gameOverElement.remove(); - } - finalGamesSection.innerHTML = ''; - } - } - - updateGauge() + updateGauges(); } -function updateGauge() { - document.querySelectorAll('.gauge').forEach(function(gauge) { - // Get the score value from the data-score attribute - var score = parseInt(gauge.getAttribute('data-score')); +// ── Renderers ──────────────────────────────────────── - // Clamp the score value between 0 and 700 - score = Math.min(700, Math.max(0, score)); +function renderLiveGame(game) { + const intermission = game['Intermission']; + const period = game['Period']; + const time = game['Time Remaining']; + const running = game['Time Running']; - // Calculate the gauge width as a percentage - var gaugeWidth = (score / 700) * 100; + const periodLabel = intermission + ? `${intermissionLabel(period)}` + : `${ordinalPeriod(period)}`; - // Set the width of the gauge - gauge.style.width = gaugeWidth + '%'; + const dot = running ? `` : ''; - if (score <=300) { - gauge.style.backgroundColor = '#4A90E2' - } else if (score <= 550) { - gauge.style.backgroundColor = '#FF4500' - } else { - gauge.style.backgroundColor = '#FF0033' - } + const hype = !intermission ? ` +
+ Hype Meter +
+
+
+
` : ''; + + return ` +
+
+
+ ${periodLabel} + ${time} +
+ ${dot} +
+ ${teamRow(game, 'Away', 'live')} + ${teamRow(game, 'Home', 'live')} + ${hype} +
`; +} + +function renderPreGame(game) { + return ` +
+
+
+ ${game['Start Time']} +
+
+ ${teamRow(game, 'Away', 'pre')} + ${teamRow(game, 'Home', 'pre')} +
`; +} + +function renderFinalGame(game) { + const labels = { REG: 'Final', OT: 'Final/OT', SO: 'Final/SO' }; + const label = labels[game['Last Period Type']] ?? 'Final'; + return ` +
+
+
+ ${label} +
+
+ ${teamRow(game, 'Away', 'final')} + ${teamRow(game, 'Home', 'final')} +
`; +} + +// ── Team Row ───────────────────────────────────────── + +function teamRow(game, side, state) { + const name = game[`${side} Team`]; + const logo = game[`${side} Logo`]; + const score = game[`${side} Score`]; + const sog = game[`${side} Shots`]; + const pp = game[`${side} Power Play`]; + const record = game[`${side} Record`]; + + const sogHtml = (state !== 'pre' && sog !== undefined) + ? `${sog} SOG` : ''; + const ppHtml = pp ? `${pp}` : ''; + + const right = state === 'pre' + ? `${record}` + : `${score}`; + + return ` +
+ +
+ ${name} + ${sogHtml}${ppHtml} +
+ ${right} +
`; +} + +// ── Gauge ──────────────────────────────────────────── + +function updateGauges() { + document.querySelectorAll('.gauge').forEach(el => { + const score = Math.min(700, Math.max(0, parseInt(el.dataset.score, 10))); + el.style.width = `${(score / 700) * 100}%`; + el.style.backgroundColor = score <= 300 ? '#4a90e2' + : score <= 550 ? '#f97316' + : '#ef4444'; }); } -// Function to generate HTML for game boxes -function generateGameBoxes(games, state) { - var html = ''; - games.forEach(function(game) { - if (game['Game State'] === state) { - html += '
'; - if (state === 'LIVE') { - if (game['Time Running']) { - html += '
'; // Display the red dot if the game is live - } - html += '
'; - html += ''; - html += '
'; - html += '' + game['Away Team'] + ''; - html += 'SOG: ' + game['Away Shots'] + ''; - html += '' + game['Away Power Play'] + ''; - html += '
'; - html += '' + game['Away Score'] + ''; - html += '
'; - html += '
'; - html += ''; - html += '
'; - html += '' + game['Home Team'] + ''; - html += 'SOG: ' + game['Home Shots'] + ''; - html += '' + game['Home Power Play'] + ''; - html += '
'; - html += '' + game['Home Score'] + ''; - html += '
'; - html += '
'; - if (game['Intermission']) { - html += '
' - if (game['Period'] == 1 ) { - html += '1st Int'; - } - if (game['Period'] == 2 ) { - html += '2nd Int'; - } - if (game['Period'] == 3 ) { - html += '3rd Int'; - } - html += '
'; - html += '
' + game['Time Remaining'] + '
'; - } else { - html += '
'; - if (game['Period'] == 1 ) { - html += '1st'; - } - else if (game['Period'] == 2 ) { - html += '2nd'; - } - else if (game['Period'] == 3 ) { - html += '3rd'; - } - else if (game['Period'] == 4 ) { - html += 'OT'; - } - else { - html += 'SO'; - } - html += '
'; - html += '
' + game['Time Remaining'] + '
'; - } - html += '
'; - if (!game['Intermission']) { - html += '
'; - html += 'Hype Meter'; - html += '
'; +// ── Helpers ────────────────────────────────────────── - html += '
'; - html += '
'; - html += '
'; - html += '
'; - } - - - html += ''; - } else if (state === 'PRE') { - html += '
' + game['Start Time'] + '
'; - html += '
'; - html += ''; - html += '' + game['Away Team'] + ''; - html += '' + game['Away Record'] + ''; - html += '
'; - html += '
'; - html += ''; - html += '' + game['Home Team'] + ''; - html += '' + game['Home Record'] + ''; - html += '
'; - } else if (state === 'FINAL') { - html += '
'; - if (game['Last Period Type'] === 'REG') { - html += 'FINAL'; - } else if (game['Last Period Type'] === 'OT') { - html += 'FINAL/OT'; - } else { - html += 'FINAL/SO'; - } - html += '
'; - html += '
'; - html += ''; - html += '
'; - html += '' + game['Away Team'] + ''; - html += 'SOG: ' + game['Away Shots'] + ''; - html += '
'; - html += '' + game['Away Score'] + ''; - html += '
'; - html += '
'; - html += ''; - html += '
'; - html += '' + game['Home Team'] + ''; - html += 'SOG: ' + game['Home Shots'] + ''; - html += '
'; - html += '' + game['Home Score'] + ''; - html += '
'; - } - html += ''; - } - }); - return html; +function ordinalPeriod(period) { + return ['1st', '2nd', '3rd', 'OT'][period - 1] ?? 'SO'; } -// Function to reload the scoreboard every 20 seconds +function intermissionLabel(period) { + return ['1st Int', '2nd Int', '3rd Int'][period - 1] ?? 'Int'; +} + +// ── Init ───────────────────────────────────────────── + function autoRefresh() { fetchScoreboardData(); - setTimeout(autoRefresh, 5000); // 20 seconds + setTimeout(autoRefresh, 5000); } -// Call the autoRefresh function when the page loads -window.onload = function() { - autoRefresh(); -}; \ No newline at end of file +window.addEventListener('load', autoRefresh); diff --git a/app/static/styles.css b/app/static/styles.css index b234ca6..1aeae95 100644 --- a/app/static/styles.css +++ b/app/static/styles.css @@ -1,381 +1,239 @@ -body { - background-color: #121212; - font-family: Arial, sans-serif; - color: #fff; +:root { + --bg: #111; + --card: #1c1c1c; + --card-border: #2a2a2a; + --badge-bg: #2a2a2a; + --text: #f0f0f0; + --text-muted: #777; + --green-bg: #14532d; + --green-text: #86efac; + --red: #ef4444; + --gap: 0.875rem; + --radius: 10px; + --card-w: 250px; +} + +*, *::before, *::after { + box-sizing: border-box; margin: 0; + padding: 0; } -h1 { - text-align: center; - margin-top: 0.8%; - margin-bottom: 1.5%; - color: #f2f2f2; - font-size: 2.2em; +body { + background: var(--bg); + color: var(--text); + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; + min-height: 100vh; } -.scoreboard { +/* ── Header ─────────────────────────────────────── */ + +header { + padding: 1rem 1.25rem 0.5rem; +} + +.header-title { + font-size: 0.7rem; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.15em; + color: var(--text-muted); +} + +/* ── Layout ─────────────────────────────────────── */ + +main { + padding: 0.75rem 1.25rem 2rem; +} + +.section { + margin-bottom: 2rem; +} + +.section.hidden { + display: none; +} + +.section-heading { + font-size: 0.7rem; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.12em; + color: var(--text-muted); + margin-bottom: 0.75rem; +} + +.games-grid { display: flex; flex-wrap: wrap; - justify-content: space-around; - margin-top: 20px; + gap: var(--gap); } +/* ── Game Card ──────────────────────────────────── */ + .game-box { - background-color: #333; - border-radius: 12px; - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); - padding: 1%; - width: 16%; - max-width: 350px; - position: relative; - margin-left: 1%; - margin-right: 1%; - margin-bottom: 1.15%; + background: var(--card); + border: 1px solid var(--card-border); + border-radius: var(--radius); + padding: 0.875rem; + width: var(--card-w); + flex-shrink: 0; } -.team-info { +/* ── Card Header (badges + live dot) ───────────── */ + +.card-header { display: flex; align-items: center; - margin-bottom: 2%; - margin-top: 9%; + justify-content: space-between; + margin-bottom: 0.5rem; + min-height: 1.25rem; } -.team-info-column { +.badges { display: flex; - flex-direction: column; + gap: 0.3rem; + align-items: center; + flex-wrap: wrap; } -.team-logo { - width: 18%; - height: auto; - margin-right: 2.25%; +.badge { + font-size: 0.65rem; + font-weight: 700; + padding: 0.2rem 0.45rem; + border-radius: 4px; + text-transform: uppercase; + letter-spacing: 0.05em; + background: var(--badge-bg); + color: var(--text); + white-space: nowrap; } -.team-name { - font-size: 1rem; - font-weight: bold; +.badge-live { + background: var(--green-bg); + color: var(--green-text); } -/* Add a media query for screens between 769px and 900px */ -@media only screen and (max-width: 950px) and (min-width: 769px) { - .team-name { - font-size: 0.55rem; /* Adjusted font size for screens between 769px and 900px */ - } -} - -.team-score { - font-size: 1.35rem; - font-weight: bold; - margin-left: auto; -} - -.team-record { - font-size: 0.8rem; - font-weight: bold; - margin-left: auto; -} - -/* Add a media query for screens between 769px and 900px */ -@media only screen and (max-width: 950px) and (min-width: 769px) { - .team-record { - font-size: 0.45rem; /* Adjusted font size for screens between 769px and 900px */ - } -} - -.team-sog { - font-size: 0.75rem; - color: #ddd; -} - -.team-power-play { - font-size: 12px; - color: red; - margin-left: 10px; -} - -.game-info { - margin-top: 12px; - color: #aaa; - text-align: center; - font-size: 80%; -} - -.hype-meter-label { - margin-top: 3%; - color: #aaa; - text-align: center; - font-size: 80%; - margin-bottom: 3%; +.badge-muted { + color: var(--text-muted); } .live-dot { - position: absolute; - top: 5px; - right: 5px; - width: 10px; - height: 10px; - background-color: red; + width: 7px; + height: 7px; + background: var(--red); border-radius: 50%; + flex-shrink: 0; + animation: pulse 1.8s ease-in-out infinite; } -.pre-state { - position: absolute; - top: 5%; - left: 3%; - background-color: #444; - padding: 1.5%; - border-radius: 5px; - font-size: 0.75rem; - color: #fff; - font-weight: bolder; - z-index: 1; - width: auto; - height: 7%; +@keyframes pulse { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.4; } +} + +/* ── Team Rows ──────────────────────────────────── */ + +.team-row { display: flex; - justify-content: space-evenly; align-items: center; + gap: 0.5rem; + padding: 0.45rem 0; } -/* Add a media query for screens between 769px and 900px */ -@media only screen and (max-width: 950px) and (min-width: 769px) { - .pre-state { - font-size: 0.55rem; /* Adjusted font size for screens between 769px and 900px */ - } +.team-row + .team-row { + border-top: 1px solid var(--card-border); } -.final-state { - position: absolute; - top: 5%; - left: 3%; - background-color: #444; - padding: 1.5%; - border-radius: 5px; - font-size: 0.7rem; - color: #ddd; - z-index: 1; - font-weight: bold; - width: auto; - height: 7%; - display: flex; - justify-content: space-evenly; - align-items: center; +.team-logo { + width: 30px; + height: 30px; + object-fit: contain; + flex-shrink: 0; } -.live-state { - position: absolute; - top: 4%; - left: 4%; - background-color: #0b6e31; - padding: 1.5%; - border-radius: 5px; +.team-meta { + flex: 1; + min-width: 0; + display: flex; + flex-direction: column; + gap: 0.1rem; +} + +.team-name { + font-size: 0.825rem; + font-weight: 600; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.team-sog { + font-size: 0.68rem; + color: var(--text-muted); +} + +.team-pp { + font-size: 0.68rem; + color: var(--red); + font-weight: 600; +} + +.team-score { + font-size: 1.2rem; + font-weight: 700; + margin-left: auto; + flex-shrink: 0; + min-width: 1.5rem; + text-align: right; +} + +.team-record { font-size: 0.72rem; - color: #fff; - font-weight: bolder; - z-index: 1; - width: 7%; - height: 7%; - display: flex; - justify-content: space-evenly; - align-items: center; + color: var(--text-muted); + margin-left: auto; + flex-shrink: 0; + white-space: nowrap; } -.live-time { - position: absolute; - top: 4%; - left: 15%; - background-color: #444; - padding: 1.5%; - border-radius: 5px; - font-size: 0.75rem; - color: #ddd; - z-index: 1; - display: flex; - justify-content: space-evenly; - align-items: center; - width: 10%; - height: 7%; +/* ── Hype Meter ─────────────────────────────────── */ + +.hype-meter { + margin-top: 0.75rem; } -.live-state-intermission { - position: absolute; - top: 4%; - left: 4%; - background-color: #444; - padding: 1.5%; - border-radius: 5px; - font-size: 80%; - color: #fff; - font-weight: bolder; - z-index: 1; - width: 11%; - height: 8.5%; - display: flex; - justify-content: space-evenly; - align-items: center; +.hype-label { + display: block; + font-size: 0.6rem; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.1em; + color: var(--text-muted); + margin-bottom: 0.3rem; } -.live-time-intermission { - position: absolute; - top: 4%; - left: 19%; - background-color: #444; - padding: 1.5%; - border-radius: 5px; - font-size: 0.75rem; - color: #ddd; - z-index: 1; - width: 10%; - height: 8.5%; - display: flex; - justify-content: space-evenly; - align-items: center; -} - -#live-games-section { - display: flex; - align-items: start; - flex-wrap: wrap; - justify-content: flex-start; -} - -#pre-games-section { - display: flex; - align-items: start; - flex-wrap: wrap; - justify-content: flex-start; -} - -#final-games-section { - display: flex; - align-items: start; - flex-wrap: wrap; - justify-content: flex-start; -} - -/* Add styles for the game score gauge */ -.game-score-gauge { - height: 1%; - background-color: #ccc; - border-radius: 5px; - overflow: hidden; +.gauge-track { + height: 4px; + background: var(--badge-bg); + border-radius: 99px; + overflow: hidden; } .gauge { - height: 10px; /* Adjust height as needed */ - /*#8A2BE2*/ - /*#6699CC*/ + height: 100%; + border-radius: 99px; + width: 0; + transition: width 0.5s ease; } -/* Add media query for smaller screens */ -@media only screen and (max-width: 768px) { - .scoreboard { - flex-direction: column; /* Change direction to column for smaller screens */ - align-items: center; /* Center align items */ +/* ── Mobile ─────────────────────────────────────── */ + +@media (max-width: 640px) { + :root { + --card-w: 100%; } - .game-box { - width: 90%; - padding: 4%; - margin-bottom: 4%; - margin-left: 2%; - margin-right: 2%; - max-width: 1000px; + .games-grid { + flex-direction: column; } - - .team-info { - align-items: center; - margin-top: 10%; - margin-bottom: 2%; - } - - .team-logo { - width: 12%; - height: auto; - } - - .team-name { - font-size: 100%; - font-weight: bold; - } - - .team-score { - font-size: 140%; - font-weight: bold; - } - - .team-sog { - font-size: 70%; - } - - .game-info { - font-size: 90%; - } - - .live-state { - top: 5%; - left: 3.5%; - padding: 1.5%; - border-radius: 5px; - font-size: 80%; - width: 5.5%; - height: 7.2%; - } - - .live-time { - top: 5%; - left: 13%; - padding: 1.5%; - border-radius: 5px; - font-size: 80%; - display: flex; - justify-content: space-evenly; - align-items: center; - width: 7%; - height: 7.2%; - } - - .live-state-intermission { - top: 5%; - left: 3.5%; - padding: 1.5%; - border-radius: 5px; - font-size: 80%; - width: 11%; - height: 7.5%; - display: flex; - justify-content: space-evenly; - align-items: center; - } - - .live-time-intermission { - top: 5%; - left: 18.5%; - padding: 1.5%; - border-radius: 5px; - font-size: 80%; - width: 8%; - height: 7.5%; - display: flex; - justify-content: space-evenly; - align-items: center; - } - - .final-state { - top: 5%; - left: 3.5%; - padding: 1.5%; - border-radius: 5px; - font-size: 72%; - width: auto; - height: 7.5%; - } - - .pre-state { - top: 5%; - left: 3.5%; - padding: 1.5%; - border-radius: 5px; - font-size: 80%; - } - -} \ No newline at end of file +} diff --git a/app/templates/index.html b/app/templates/index.html index f0b7068..815a3e3 100644 --- a/app/templates/index.html +++ b/app/templates/index.html @@ -6,9 +6,23 @@ -
-
-
+
+ NHL Scoreboard +
+
+ + + +