mirror of
https://github.com/AmanTahiliani/box-box.git
synced 2026-08-07 11:54:59 -04:00
394 lines
18 KiB
HTML
394 lines
18 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8"/>
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
|
<title>box-box</title>
|
|
<link rel="stylesheet" href="/style.css"/>
|
|
<style>[x-cloak]{display:none!important}</style>
|
|
</head>
|
|
<body x-data="appState()" x-cloak>
|
|
|
|
<!-- Navigation -->
|
|
<nav class="nav">
|
|
<a class="nav-brand" href="#/">box‑box</a>
|
|
<div class="nav-links">
|
|
<a href="#/" :class="{active: page==='home'}">Home</a>
|
|
<a href="#/standings" :class="{active: page==='standings'}">Standings</a>
|
|
<a href="#/live" :class="{active: page==='live'}">
|
|
Live
|
|
<span x-show="isLive" class="live-dot"></span>
|
|
</a>
|
|
</div>
|
|
</nav>
|
|
|
|
<!-- Stale data banner -->
|
|
<div x-show="staleWarning" class="stale-banner">
|
|
<span>Showing cached data — live session in progress (free tier)</span>
|
|
<details>
|
|
<summary>Why?</summary>
|
|
<p>During live F1 sessions the free-tier OpenF1 API restricts access.
|
|
box-box automatically serves the last cached data.</p>
|
|
</details>
|
|
</div>
|
|
|
|
<!-- Home page -->
|
|
<template x-if="page==='home'">
|
|
<div x-data="homePage()" x-init="init()">
|
|
|
|
<!-- Next race card + championship sidebar -->
|
|
<div class="home-hero" x-show="!loading">
|
|
<!-- Next race -->
|
|
<div class="card next-race" x-show="nextRace">
|
|
<!-- Speed lines background -->
|
|
<div class="speed-lines" aria-hidden="true">
|
|
<div class="speed-line"></div><div class="speed-line"></div><div class="speed-line"></div>
|
|
<div class="speed-line"></div><div class="speed-line"></div><div class="speed-line"></div>
|
|
<div class="speed-line"></div><div class="speed-line"></div>
|
|
</div>
|
|
<!-- Circuit watermark -->
|
|
<div class="circuit-watermark" x-text="nextRace?.circuit_short_name||''"></div>
|
|
<!-- Checkered flag pattern -->
|
|
<div class="checkered-corner" aria-hidden="true"></div>
|
|
<div class="card-label" x-text="heroLabel"></div>
|
|
<div class="next-race-flag" x-text="flagEmoji(nextRace?.country_code)"></div>
|
|
<h2 class="next-race-name" x-text="nextRace?.meeting_name||''"></h2>
|
|
<div class="next-race-circuit" x-text="nextRace?.circuit_short_name||''"></div>
|
|
<div class="next-race-session" x-show="heroMeta" x-text="heroMeta"></div>
|
|
<div class="next-race-countdown" x-text="countdown"></div>
|
|
</div>
|
|
|
|
<!-- Championship sidebar -->
|
|
<div class="card champ-sidebar">
|
|
<div class="card-label">DRIVERS' CHAMPIONSHIP</div>
|
|
<template x-for="(d, idx) in champDrivers.slice(0,10)" :key="d.driver_number">
|
|
<div class="champ-row"
|
|
:class="idx === 0 ? 'podium-1' : (idx === 1 ? 'podium-2' : (idx === 2 ? 'podium-3' : ''))"
|
|
:style="`--team-color:#${d.team_colour}`">
|
|
<span class="champ-pos" x-text="d.position_current"></span>
|
|
<span class="team-swatch"></span>
|
|
<span class="champ-tla" x-text="d.name_acronym||d.driver_number"></span>
|
|
<div class="champ-bar-wrap">
|
|
<div class="champ-bar" :style="`width:${barWidth(d.points_current)}%`"></div>
|
|
</div>
|
|
<span class="champ-pts" x-text="d.points_current"></span>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Loading skeleton -->
|
|
<div x-show="loading" class="skeleton-wrap">
|
|
<div class="skeleton" style="height:120px;margin-bottom:1rem;"></div>
|
|
</div>
|
|
|
|
<!-- Season grid -->
|
|
<div class="section-label" x-text="season + ' SEASON'"></div>
|
|
<div class="meetings-grid" x-show="!loading">
|
|
<template x-for="(m, idx) in meetings" :key="m.meeting_key">
|
|
<a class="meeting-card"
|
|
:class="{past: isPast(m), 'meeting-next': m === nextRace}"
|
|
:href="`#/race/${m.meeting_key}`"
|
|
:style="meetingCardStyle(m.country_code)">
|
|
<div class="meeting-card-accent"></div>
|
|
<div class="meeting-card-body">
|
|
<div class="meeting-country-code" x-text="m.country_code||'?'"></div>
|
|
<div class="meeting-card-flag" x-text="flagEmoji(m.country_code)"></div>
|
|
</div>
|
|
<div class="meeting-card-foot">
|
|
<div class="meeting-name" x-text="m.meeting_name"></div>
|
|
<div class="meeting-circuit" x-text="m.circuit_short_name"></div>
|
|
<div class="meeting-date" x-text="fmtDate(m.date_start)"></div>
|
|
</div>
|
|
<div class="meeting-round" x-text="'R' + roundNum(m)"></div>
|
|
</a>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<!-- Race page -->
|
|
<template x-if="page==='race'">
|
|
<div x-data="racePage()" x-init="init()">
|
|
<div class="page-header">
|
|
<a class="back-btn" href="#/">← Back</a>
|
|
<h1 class="page-title" x-text="meetingName"></h1>
|
|
</div>
|
|
|
|
<!-- Session picker -->
|
|
<div class="session-pills" x-show="sessions.length">
|
|
<template x-for="sess in sessions" :key="sess.session_key">
|
|
<button class="pill"
|
|
:class="{active: activeSession?.session_key===sess.session_key}"
|
|
@click="selectSession(sess)"
|
|
x-text="sess.session_name">
|
|
</button>
|
|
</template>
|
|
</div>
|
|
|
|
<!-- Tabs -->
|
|
<div class="tabs" x-show="activeSession">
|
|
<button class="tab" :class="{active:tab==='results'}" @click="tab='results'">Results</button>
|
|
<button class="tab" :class="{active:tab==='strategy'}" @click="loadStrategy();tab='strategy'">Strategy</button>
|
|
<button class="tab" :class="{active:tab==='laps'}" @click="loadLaps();tab='laps'">Lap Times</button>
|
|
<button class="tab" :class="{active:tab==='track'}" @click="loadTrack();tab='track'">Track</button>
|
|
<button class="tab" :class="{active:tab==='telemetry'}" @click="tab='telemetry'">Telemetry</button>
|
|
</div>
|
|
|
|
<!-- Results tab -->
|
|
<div x-show="tab==='results'">
|
|
<div x-show="resultsLoading" class="skeleton-wrap">
|
|
<template x-for="i in [1,2,3,4,5]" :key="i">
|
|
<div class="skeleton" style="height:36px;margin-bottom:4px;"></div>
|
|
</template>
|
|
</div>
|
|
<div class="table-scroll" x-show="!resultsLoading && results.length">
|
|
<table class="data-table">
|
|
<thead>
|
|
<tr>
|
|
<th>POS</th><th>DRIVER</th><th>TEAM</th><th>TIME / GAP</th><th>PTS</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<template x-for="r in results" :key="r.driver_number">
|
|
<tr :style="`--team-color:#${r.team_colour}`">
|
|
<td class="pos-cell">
|
|
<span x-text="r.position||'-'"></span>
|
|
<span x-show="r.dnf" class="badge dnf">DNF</span>
|
|
<span x-show="r.dns" class="badge dns">DNS</span>
|
|
<span x-show="r.dsq" class="badge dsq">DSQ</span>
|
|
</td>
|
|
<td><span class="team-swatch"></span><span x-text="r.full_name||r.driver_number"></span></td>
|
|
<td x-text="r.team_name||'-'"></td>
|
|
<td x-text="fmtDuration(r.duration)"></td>
|
|
<td x-text="r.points||0"></td>
|
|
</tr>
|
|
</template>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<div x-show="!resultsLoading && !results.length" class="empty-msg">No results available.</div>
|
|
</div>
|
|
|
|
<!-- Strategy tab -->
|
|
<div x-show="tab==='strategy'" x-effect="tab==='strategy' && strategyData && renderStrategy()">
|
|
<div x-show="strategyLoading" class="skeleton-wrap">
|
|
<div class="skeleton" style="height:300px;"></div>
|
|
</div>
|
|
<div x-show="strategyNote" class="empty-msg" x-text="strategyNote"></div>
|
|
<div id="strategy-chart" x-show="!strategyLoading && !strategyNote" style="min-height:300px;overflow-x:auto;"></div>
|
|
</div>
|
|
|
|
<!-- Lap Times tab -->
|
|
<div x-show="tab==='laps'" x-effect="tab==='laps' && lapsData && renderLapTimes()">
|
|
<div x-show="lapsLoading" class="skeleton-wrap">
|
|
<div class="skeleton" style="height:300px;"></div>
|
|
</div>
|
|
<div id="laps-chart" x-show="!lapsLoading" style="min-height:300px;"></div>
|
|
</div>
|
|
|
|
<!-- Track tab -->
|
|
<div x-show="tab==='track'" x-effect="tab==='track' && trackData && renderTrack()">
|
|
<div x-show="trackLoading" class="skeleton-wrap">
|
|
<div class="skeleton" style="height:300px;"></div>
|
|
</div>
|
|
<div x-show="!trackData && !trackLoading" class="empty-msg">Track outline not available for this circuit.</div>
|
|
<div id="track-chart" x-show="!trackLoading && trackData" style="min-height:300px;"></div>
|
|
</div>
|
|
|
|
<!-- Telemetry tab -->
|
|
<div x-show="tab==='telemetry'">
|
|
<div class="driver-selector">
|
|
<label>Select driver:</label>
|
|
<template x-for="d in drivers" :key="d.driver_number">
|
|
<button class="pill"
|
|
:class="{active: telemetryDrivers.includes(d.driver_number)}"
|
|
:style="`--team-color:#${d.team_colour}`"
|
|
@click="toggleTelemetryDriver(d.driver_number)"
|
|
x-text="d.name_acronym">
|
|
</button>
|
|
</template>
|
|
</div>
|
|
<div x-show="telemetryLoading" class="skeleton-wrap">
|
|
<div class="skeleton" style="height:340px;"></div>
|
|
</div>
|
|
<div id="telemetry-chart" x-show="!telemetryLoading" style="min-height:340px;"
|
|
x-effect="telemetryData.length && tab==='telemetry' && renderTelemetry()"></div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<!-- Live page -->
|
|
<template x-if="page==='live'">
|
|
<div x-data="livePage()" x-init="init()" x-destroy="cleanup()">
|
|
<div class="page-header">
|
|
<h1 class="page-title">
|
|
Live Timing
|
|
<span x-show="isLive" class="live-dot large"></span>
|
|
</h1>
|
|
<div class="session-info" x-show="session.meeting_name">
|
|
<span x-text="session.meeting_name"></span> —
|
|
<span x-text="session.session_name"></span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- No session placeholder -->
|
|
<div x-show="!isLive" class="empty-msg large">
|
|
No live session active. Check back during a race weekend.
|
|
</div>
|
|
|
|
<!-- Timing tower -->
|
|
<div x-show="isLive">
|
|
<div class="live-meta">
|
|
<span>Lap <strong x-text="currentLap"></strong>/<strong x-text="totalLaps"></strong></span>
|
|
<span x-show="trackStatus" class="track-status" :class="trackStatusClass()" x-text="trackStatusText()"></span>
|
|
<span class="clock" x-text="clockDisplay"></span>
|
|
</div>
|
|
|
|
<div class="table-scroll">
|
|
<table class="timing-tower">
|
|
<thead>
|
|
<tr><th>POS</th><th>Δ</th><th>DRIVER</th><th>TYRE</th><th>LAST LAP</th><th>GAP</th><th>BEST</th></tr>
|
|
</thead>
|
|
<tbody>
|
|
<template x-for="d in sortedDrivers" :key="d.RacingNumber">
|
|
<tr :class="{in-pit: d.InPit, retired: d.Retired, pit-out: d.PitOut}"
|
|
:style="`--team-color:#${driverTeamColor(d.RacingNumber)}`">
|
|
<td class="pos-cell" x-text="d.Position"></td>
|
|
<td class="pos-delta" x-text="posDelta(d)"></td>
|
|
<td>
|
|
<div class="driver-cell">
|
|
<span class="team-swatch"></span><span x-text="driverTla(d.RacingNumber)"></span>
|
|
<span x-show="d.InPit" class="badge pit">PIT</span>
|
|
<span x-show="d.Retired" class="badge out">OUT</span>
|
|
</div>
|
|
</td>
|
|
<td>
|
|
<span class="tyre-badge" :class="tyreClass(d.RacingNumber)"
|
|
x-text="tyreLabel(d.RacingNumber)"></span>
|
|
</td>
|
|
<td :class="{pb: d.LastLapPB, ob: d.LastLapOB}" x-text="d.LastLapTime||'-'"></td>
|
|
<td x-text="d.GapToLeader||'-'"></td>
|
|
<td :class="{ob: d.BestLapOB}" x-text="d.BestLapTime||'-'"></td>
|
|
</tr>
|
|
</template>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<!-- Race control messages -->
|
|
<div class="rc-panel" x-show="rcMessages.length">
|
|
<div class="card-label">RACE CONTROL</div>
|
|
<template x-for="(msg, i) in rcMessages.slice().reverse().slice(0,10)" :key="i">
|
|
<div class="rc-message" :class="`rc-${msg.Category?.toLowerCase()}`">
|
|
<span class="rc-time" x-text="msg.Time"></span>
|
|
<span class="rc-flag" x-show="msg.Flag" x-text="msg.Flag"></span>
|
|
<span x-text="msg.Message"></span>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<!-- Standings page -->
|
|
<template x-if="page==='standings'">
|
|
<div x-data="standingsPage()" x-init="init()">
|
|
<div class="page-header">
|
|
<h1 class="page-title">Championship Standings</h1>
|
|
<div class="year-selector">
|
|
<button class="pill" :class="{active:year===2026}" @click="setYear(2026)">2026</button>
|
|
<button class="pill" :class="{active:year===2025}" @click="setYear(2025)">2025</button>
|
|
<button class="pill" :class="{active:year===2024}" @click="setYear(2024)">2024</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="tabs">
|
|
<button class="tab" :class="{active:view==='drivers'}" @click="view='drivers'">Drivers</button>
|
|
<button class="tab" :class="{active:view==='teams'}" @click="view='teams'">Constructors</button>
|
|
</div>
|
|
|
|
<div x-show="loading" class="skeleton-wrap">
|
|
<template x-for="i in [1,2,3,4,5,6,7,8,9,10]" :key="i">
|
|
<div class="skeleton" style="height:40px;margin-bottom:4px;"></div>
|
|
</template>
|
|
</div>
|
|
|
|
<!-- Drivers standings table -->
|
|
<div class="standings-table" x-show="!loading && view==='drivers'">
|
|
<div class="standings-header">
|
|
<span class="sh-pos">POS</span>
|
|
<span class="sh-driver">DRIVER</span>
|
|
<span class="sh-team">TEAM</span>
|
|
<span class="sh-bar"></span>
|
|
<span class="sh-pts">PTS</span>
|
|
<span class="sh-gap">GAP</span>
|
|
</div>
|
|
<template x-for="(d, idx) in driverStandings" :key="d.driver_number">
|
|
<div class="standings-row"
|
|
:class="idx === 0 ? 'podium-1' : (idx === 1 ? 'podium-2' : (idx === 2 ? 'podium-3' : ''))"
|
|
:style="`--team-color:#${d.team_colour}`">
|
|
<!-- Team color left border (via CSS) -->
|
|
<div class="sr-pos">
|
|
<span class="trophy-icon" x-show="idx < 3" x-text="['🥇','🥈','🥉'][idx]"></span>
|
|
<span x-show="idx >= 3" x-text="d.position_current" class="pos-num"></span>
|
|
</div>
|
|
<div class="sr-driver">
|
|
<span class="driver-num-badge" :style="`background:#${d.team_colour}22; color:#${d.team_colour}; border-color:#${d.team_colour}55`" x-text="d.driver_number"></span>
|
|
<div class="sr-driver-info">
|
|
<span class="sr-full-name" x-text="d.full_name||d.driver_number"></span>
|
|
<span class="sr-tla" x-text="d.name_acronym||''"></span>
|
|
</div>
|
|
</div>
|
|
<div class="sr-team" x-text="d.team_name||'-'"></div>
|
|
<div class="sr-bar-wrap">
|
|
<div class="sr-bar" :style="`width:${standingsBarWidth(d.points_current, driverStandings)}%;`"></div>
|
|
</div>
|
|
<div class="sr-pts" x-text="d.points_current"></div>
|
|
<div class="sr-gap" x-text="idx === 0 ? 'LEADER' : '-' + (driverStandings[0].points_current - d.points_current)"></div>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
|
|
<!-- Constructors standings table -->
|
|
<div class="standings-table" x-show="!loading && view==='teams'">
|
|
<div class="standings-header constructor-header">
|
|
<span class="sh-pos">POS</span>
|
|
<span class="sh-driver">CONSTRUCTOR</span>
|
|
<span class="sh-bar"></span>
|
|
<span class="sh-pts">PTS</span>
|
|
<span class="sh-gap">GAP</span>
|
|
</div>
|
|
<template x-for="(t, idx) in teamStandings" :key="t.team_name">
|
|
<div class="standings-row constructor-row"
|
|
:class="idx === 0 ? 'podium-1' : (idx === 1 ? 'podium-2' : (idx === 2 ? 'podium-3' : ''))"
|
|
:style="`--team-color:${teamColor(t.team_name)}`">
|
|
<div class="sr-pos">
|
|
<span class="trophy-icon" x-show="idx < 3" x-text="['🥇','🥈','🥉'][idx]"></span>
|
|
<span x-show="idx >= 3" x-text="t.position_current" class="pos-num"></span>
|
|
</div>
|
|
<div class="sr-driver">
|
|
<div class="constructor-color-block" :style="`background:${teamColor(t.team_name)}`"></div>
|
|
<div class="sr-driver-info">
|
|
<span class="sr-full-name" x-text="t.team_name"></span>
|
|
</div>
|
|
</div>
|
|
<div class="sr-bar-wrap">
|
|
<div class="sr-bar" :style="`width:${standingsBarWidth(t.points_current, teamStandings)}%;`"></div>
|
|
</div>
|
|
<div class="sr-pts" x-text="t.points_current"></div>
|
|
<div class="sr-gap" x-text="idx === 0 ? 'LEADER' : '-' + (teamStandings[0].points_current - t.points_current)"></div>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script src="https://cdn.jsdelivr.net/npm/d3@7/dist/d3.min.js"></script>
|
|
<script src="/charts.js"></script>
|
|
<script src="/track.js"></script>
|
|
<script src="/app.js"></script>
|
|
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.14.8/dist/cdn.min.js"></script>
|
|
</body>
|
|
</html>
|