feat: make scoreboard playoff-aware with banner, bracket, and series drill-down
Turn a regular-season-looking Tuesday into a full playoff experience: - Playoff banner with round + day + series + elimination counts, gold/silver Cup theme toggled by body.playoff-mode - Series context on each playoff card: round chip, series score, stake badges (GAME 7, CLINCHER, PIVOTAL), and one-line blurb - Game 7s pin to a new Spotlight section above Live - Playoff OT renders with SUDDEN DEATH badge and pulsing gold border - Client-side OT notifications via bell button in the banner - New /series/<id> drill-down with headline, next-game, and game-by-game history - New /bracket page: 7-column desktop grid, accordion on mobile - Day N banner count auto-anchors on first playoff scoreboard hit - SQLite cache for bracket + per-series schedules, stale-on-failure up to 24h Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,21 @@
|
||||
{% if m.empty %}
|
||||
<div class="bracket-matchup bracket-matchup-empty">
|
||||
<div class="bracket-team bracket-team-placeholder">TBD</div>
|
||||
<div class="bracket-team bracket-team-placeholder">TBD</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<a class="bracket-matchup bracket-matchup-{{ m.state }}" href="/series/{{ m.series_id }}">
|
||||
<div class="bracket-team {% if m.winner_abbrev == m.top.abbrev %}bracket-team-winner{% endif %}">
|
||||
{% if m.top.logo %}<img class="bracket-team-logo" src="{{ m.top.logo }}" alt="{{ m.top.abbrev }}">{% endif %}
|
||||
<span class="bracket-team-abbrev">{{ m.top.abbrev }}</span>
|
||||
{% if m.top.seed %}<span class="bracket-team-seed">{{ m.top.seed }}</span>{% endif %}
|
||||
<span class="bracket-team-wins">{{ m.top_wins }}</span>
|
||||
</div>
|
||||
<div class="bracket-team {% if m.winner_abbrev == m.bottom.abbrev %}bracket-team-winner{% endif %}">
|
||||
{% if m.bottom.logo %}<img class="bracket-team-logo" src="{{ m.bottom.logo }}" alt="{{ m.bottom.abbrev }}">{% endif %}
|
||||
<span class="bracket-team-abbrev">{{ m.bottom.abbrev }}</span>
|
||||
{% if m.bottom.seed %}<span class="bracket-team-seed">{{ m.bottom.seed }}</span>{% endif %}
|
||||
<span class="bracket-team-wins">{{ m.bottom_wins }}</span>
|
||||
</div>
|
||||
</a>
|
||||
{% endif %}
|
||||
@@ -0,0 +1,82 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>{{ bracket.year }} Stanley Cup Bracket</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="theme-color" content="#0f172a">
|
||||
<link rel="manifest" href="/manifest.json">
|
||||
<link rel="icon" type="image/png" href="/static/icon-32x32.png">
|
||||
<link rel="stylesheet" type="text/css" href="/static/styles.css">
|
||||
</head>
|
||||
<body class="playoff-mode bracket-mode">
|
||||
<header>
|
||||
<a class="header-title header-link" href="/">← NHL Scoreboard</a>
|
||||
</header>
|
||||
<main class="bracket-main">
|
||||
<section class="bracket-hero">
|
||||
<h1 class="bracket-title">{{ bracket.year }} Stanley Cup Playoffs</h1>
|
||||
<div class="bracket-subtitle">The road to 16 wins</div>
|
||||
</section>
|
||||
|
||||
{# Desktop: 7-column grid (East R1 | R2 | CF | Cup | CF | R2 | R1 West) #}
|
||||
<section class="bracket-grid" aria-label="Full playoff bracket">
|
||||
<div class="bracket-col bracket-col-r1 bracket-col-east">
|
||||
<h2 class="bracket-col-heading">First Round</h2>
|
||||
{% for m in bracket.east_r1 %}{% include "_bracket_matchup.html" %}{% endfor %}
|
||||
</div>
|
||||
<div class="bracket-col bracket-col-r2 bracket-col-east">
|
||||
<h2 class="bracket-col-heading">Second Round</h2>
|
||||
{% for m in bracket.east_r2 %}{% include "_bracket_matchup.html" %}{% endfor %}
|
||||
</div>
|
||||
<div class="bracket-col bracket-col-cf bracket-col-east">
|
||||
<h2 class="bracket-col-heading">East Final</h2>
|
||||
{% for m in bracket.east_cf %}{% include "_bracket_matchup.html" %}{% endfor %}
|
||||
</div>
|
||||
<div class="bracket-col bracket-col-cup">
|
||||
<h2 class="bracket-col-heading bracket-cup-heading">Cup Final</h2>
|
||||
{% for m in bracket.cup %}{% include "_bracket_matchup.html" %}{% endfor %}
|
||||
</div>
|
||||
<div class="bracket-col bracket-col-cf bracket-col-west">
|
||||
<h2 class="bracket-col-heading">West Final</h2>
|
||||
{% for m in bracket.west_cf %}{% include "_bracket_matchup.html" %}{% endfor %}
|
||||
</div>
|
||||
<div class="bracket-col bracket-col-r2 bracket-col-west">
|
||||
<h2 class="bracket-col-heading">Second Round</h2>
|
||||
{% for m in bracket.west_r2 %}{% include "_bracket_matchup.html" %}{% endfor %}
|
||||
</div>
|
||||
<div class="bracket-col bracket-col-r1 bracket-col-west">
|
||||
<h2 class="bracket-col-heading">First Round</h2>
|
||||
{% for m in bracket.west_r1 %}{% include "_bracket_matchup.html" %}{% endfor %}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{# Mobile: round-by-round accordion, round 1 open by default #}
|
||||
<section class="bracket-accordion" aria-label="Playoff bracket by round">
|
||||
{% for rnd in bracket.rounds %}
|
||||
<details class="bracket-round" {% if loop.first %}open{% endif %}>
|
||||
<summary class="bracket-round-summary">{{ rnd.label }}</summary>
|
||||
<div class="bracket-round-body">
|
||||
{% if rnd.get('east') %}
|
||||
<div class="bracket-round-half">
|
||||
<h3 class="bracket-round-half-heading">Eastern</h3>
|
||||
{% for m in rnd.east %}{% include "_bracket_matchup.html" %}{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if rnd.get('west') %}
|
||||
<div class="bracket-round-half">
|
||||
<h3 class="bracket-round-half-heading">Western</h3>
|
||||
{% for m in rnd.west %}{% include "_bracket_matchup.html" %}{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if rnd.get('cup') %}
|
||||
<div class="bracket-round-half">
|
||||
{% for m in rnd.cup %}{% include "_bracket_matchup.html" %}{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</details>
|
||||
{% endfor %}
|
||||
</section>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
@@ -16,7 +16,42 @@
|
||||
<header>
|
||||
<span class="header-title">NHL Scoreboard</span>
|
||||
</header>
|
||||
<section id="playoff-banner" class="playoff-banner hidden" aria-hidden="true">
|
||||
<svg class="banner-trophy" viewBox="0 0 32 40" aria-hidden="true">
|
||||
<defs>
|
||||
<linearGradient id="cup-gold" x1="0" x2="1" y1="0" y2="1">
|
||||
<stop offset="0%" stop-color="#f5d76e"/>
|
||||
<stop offset="60%" stop-color="#d4af37"/>
|
||||
<stop offset="100%" stop-color="#8a6d1a"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<path fill="url(#cup-gold)" d="M6 2h20v4c0 5-2 9-5 11l-1 5h-8l-1-5C8 15 6 11 6 6V2zm4 20h12v3H10v-3zm-2 4h16v3H8v-3zm1 4h14v6H9v-6z"/>
|
||||
<rect x="11" y="9" width="10" height="2" fill="#0a1628" opacity="0.35"/>
|
||||
<rect x="11" y="13" width="10" height="1.5" fill="#0a1628" opacity="0.35"/>
|
||||
</svg>
|
||||
<div class="banner-text">
|
||||
<div class="banner-title">
|
||||
STANLEY CUP PLAYOFFS
|
||||
<span class="banner-year"></span>
|
||||
</div>
|
||||
<div class="banner-meta">
|
||||
<span class="meta-round"></span>
|
||||
<span class="meta-day hidden"></span>
|
||||
<span class="meta-series"></span>
|
||||
<span class="meta-elim hidden"></span>
|
||||
<span class="meta-game7 hidden"></span>
|
||||
</div>
|
||||
</div>
|
||||
<a class="banner-bracket-link" href="/bracket">Bracket</a>
|
||||
<button class="banner-notify" type="button" title="Notify me on playoff OT" aria-label="Enable OT notifications">
|
||||
<span class="bell-label">OT alerts</span>
|
||||
</button>
|
||||
</section>
|
||||
<main>
|
||||
<section id="pinned-section" class="section pinned-section hidden">
|
||||
<h2 class="section-heading section-heading-gold">Spotlight · Game 7</h2>
|
||||
<div id="pinned-games-section" class="games-grid"></div>
|
||||
</section>
|
||||
<section id="live-section" class="section hidden">
|
||||
<h2 class="section-heading">Live</h2>
|
||||
<div id="live-games-section" class="games-grid"></div>
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>{{ series.top.abbrev }} vs {{ series.bottom.abbrev }} · {{ series.round_label }}</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="theme-color" content="#0f172a">
|
||||
<link rel="manifest" href="/manifest.json">
|
||||
<link rel="icon" type="image/png" href="/static/icon-32x32.png">
|
||||
<link rel="stylesheet" type="text/css" href="/static/styles.css">
|
||||
</head>
|
||||
<body class="playoff-mode series-mode">
|
||||
<header class="series-header">
|
||||
<a class="header-title header-link" href="/">← NHL Scoreboard</a>
|
||||
<a class="banner-bracket-link" href="/bracket">Bracket</a>
|
||||
</header>
|
||||
<main class="series-main">
|
||||
<section class="series-hero">
|
||||
<div class="series-hero-eyebrow">
|
||||
<span class="badge badge-round">{{ series.round_label|upper }}</span>
|
||||
{% if series.state.is_game7 %}<span class="badge badge-game7">GAME 7</span>
|
||||
{% elif series.state.is_clincher %}<span class="badge badge-clincher">CLINCHER</span>
|
||||
{% elif series.state.is_pivotal %}<span class="badge badge-pivotal">PIVOTAL</span>{% endif %}
|
||||
</div>
|
||||
<div class="series-teams">
|
||||
<div class="series-team {% if series.leader and series.leader.abbrev == series.top.abbrev %}series-team-leader{% endif %}">
|
||||
{% if series.top.logo %}<img class="series-team-logo" src="{{ series.top.logo }}" alt="{{ series.top.abbrev }}">{% endif %}
|
||||
<div class="series-team-name">{{ series.top.full }}</div>
|
||||
<div class="series-team-meta">
|
||||
{% if series.top.seed %}Seed {{ series.top.seed }}{% endif %}
|
||||
{% if series.top.division %} · {{ series.top.division }}{% endif %}
|
||||
</div>
|
||||
<div class="series-team-wins">{{ series.top_wins }}</div>
|
||||
</div>
|
||||
<div class="series-versus">
|
||||
<span class="series-versus-label">SERIES</span>
|
||||
<span class="series-versus-score">{{ series.top_wins }} – {{ series.bottom_wins }}</span>
|
||||
<span class="series-versus-best">Best of {{ series.length }}</span>
|
||||
</div>
|
||||
<div class="series-team {% if series.leader and series.leader.abbrev == series.bottom.abbrev %}series-team-leader{% endif %}">
|
||||
{% if series.bottom.logo %}<img class="series-team-logo" src="{{ series.bottom.logo }}" alt="{{ series.bottom.abbrev }}">{% endif %}
|
||||
<div class="series-team-name">{{ series.bottom.full }}</div>
|
||||
<div class="series-team-meta">
|
||||
{% if series.bottom.seed %}Seed {{ series.bottom.seed }}{% endif %}
|
||||
{% if series.bottom.division %} · {{ series.bottom.division }}{% endif %}
|
||||
</div>
|
||||
<div class="series-team-wins">{{ series.bottom_wins }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="series-headline">{{ series.headline }}</p>
|
||||
</section>
|
||||
|
||||
{% if series.next_game %}
|
||||
<section class="series-next">
|
||||
<h2 class="section-heading section-heading-gold">Next up · Game {{ series.next_game.game_number }}</h2>
|
||||
<div class="series-next-card">
|
||||
<div class="series-next-matchup">
|
||||
<span class="series-next-team">{{ series.next_game.away.abbrev }}</span>
|
||||
<span class="series-next-at">@</span>
|
||||
<span class="series-next-team">{{ series.next_game.home.abbrev }}</span>
|
||||
</div>
|
||||
<div class="series-next-meta">
|
||||
{% if series.next_game.start_date %}{{ series.next_game.start_date }}{% endif %}
|
||||
{% if series.next_game.start_local %} · {{ series.next_game.start_local }}{% endif %}
|
||||
{% if series.next_game.venue %} · {{ series.next_game.venue }}{% endif %}
|
||||
{% if series.next_game.if_necessary %} · <em>if necessary</em>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{% endif %}
|
||||
|
||||
<section class="series-history">
|
||||
<h2 class="section-heading">Games</h2>
|
||||
<ol class="series-games">
|
||||
{% for game in series.games %}
|
||||
<li class="series-game series-game-{{ game.state_group }}">
|
||||
<div class="series-game-col-number">Game {{ game.game_number }}{% if game.if_necessary and game.state_group != 'completed' %}*{% endif %}</div>
|
||||
<div class="series-game-col-matchup">
|
||||
<div class="series-game-team">
|
||||
<span class="series-game-abbrev">{{ game.away.abbrev }}</span>
|
||||
<span class="series-game-score {% if game.winner_abbrev == game.away.abbrev %}series-game-winner{% endif %}">
|
||||
{% if game.away.score is not none %}{{ game.away.score }}{% else %}—{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
<div class="series-game-team">
|
||||
<span class="series-game-abbrev">{{ game.home.abbrev }}</span>
|
||||
<span class="series-game-score {% if game.winner_abbrev == game.home.abbrev %}series-game-winner{% endif %}">
|
||||
{% if game.home.score is not none %}{{ game.home.score }}{% else %}—{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="series-game-col-state">
|
||||
{% if game.live %}
|
||||
<span class="badge badge-live">LIVE</span>
|
||||
{% if game.period_ot_label %}<span class="badge badge-sudden-death">{{ game.period_ot_label }}</span>{% endif %}
|
||||
{% elif game.state_group == 'completed' %}
|
||||
<span class="series-game-state">{{ game.state_label }}{% if game.ended_in_ot %} · {{ 'OT' if not game.ended_in_multi_ot else 'Multi-OT' }}{% endif %}</span>
|
||||
{% else %}
|
||||
<span class="series-game-state">
|
||||
{% if game.start_date %}{{ game.start_date }}{% endif %}
|
||||
{% if game.start_local %} · {{ game.start_local }}{% endif %}
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ol>
|
||||
</section>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user