Match scoreboard overlay: identity/ticker/scorebar/timeline + VS intro + gear-menu toggles

- Scoreboard overlay driven by video.currentTime (scores/ticker/timeline as pure
  functions of playback time; scrub-back removes points; slow-mo doesn't affect).
- Score bar + timeline + VS intro rendered verbatim from the design handoff
  (drafts/score-bar.html, drafts/vs-screen.html) — only {{ }} data holes filled.
- Fixed 1150 px design canvas scaled to any player size via ResizeObserver so
  the design's pixel-perfect measurements survive at every player width.
- Gear-menu injection adds a "Scoreboard" section with 7 toggle rows (Score bar,
  Live scoring feed, Match info, Point timeline, Club logos, Country flags,
  Penalties) using a custom 26x14 track/knob toggle matching the design.
  Persists per user in localStorage['msb_prefs']; penalties defaults off.
- VS card fades in on first play of a session (skippable by click/tap/key,
  2 s hold then 0.5 s fade); skipped entirely when no athlete metadata.
- SportsMatch::headerData() extended to expose sport, headshots, club logos,
  division, format, match_date and per-video scoreboard defaults from existing
  JSON columns (no schema migration).
- Every missing field hides its element (no "NOT SET" placeholders); athlete
  name falls back to RED/BLUE per brief.
- Pushed .replay-badge down from top:2.5cqi to top:8cqi so it clears the
  new KARATE KUMITE identity block.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
This commit is contained in:
ghassanandClaude Opus 4.7 committed 2026-08-09 22:09:04 +03:00
1 parent ed921a4ccf
commit 19741f489b
16 files changed
+1779 -15

No files matched your search

+31 -14
View File
@@ -44,31 +44,48 @@ class SportsMatch extends Model
$p = $this->participants ?? [];
$c = $this->competition ?? [];
$v = $this->venue ?? [];
$m = $this->media ?? [];
$trim = fn($x) => (is_string($x) && trim($x) !== '') ? trim($x) : null;
return [
'sport' => $trim($this->sport),
'blue' => [
'name' => $this->participant1_name ?: null,
'club' => $p['p1_club'] ?? null,
'flag' => $this->flagFor($p['p1_country'] ?? null),
'name' => $trim($this->participant1_name),
'club' => $trim($p['p1_club'] ?? null),
'flag' => $this->flagFor($p['p1_country'] ?? null),
'club_logo' => $trim($m['p1_club_logo'] ?? null),
'headshot' => $trim($m['p1_headshot'] ?? null),
],
'red' => [
'name' => $this->participant2_name ?: null,
'club' => $p['p2_club'] ?? null,
'flag' => $this->flagFor($p['p2_country'] ?? null),
'name' => $trim($this->participant2_name),
'club' => $trim($p['p2_club'] ?? null),
'flag' => $this->flagFor($p['p2_country'] ?? null),
'club_logo' => $trim($m['p2_club_logo'] ?? null),
'headshot' => $trim($m['p2_headshot'] ?? null),
],
'weight_category' => $p['weight_class'] ?? null,
'championship' => $c['championship_name'] ?? ($this->event_name ?: null),
'match_number' => $c['match_number'] ?? null,
'court' => $c['court'] ?? null,
'weight_category' => $trim($p['weight_class'] ?? null),
'division' => $trim($c['division'] ?? null),
'championship' => $trim($c['championship_name'] ?? null) ?? $trim($this->event_name),
'match_number' => $trim(($c['match_number'] ?? null) !== null ? (string) $c['match_number'] : null),
'court' => $trim($c['court'] ?? null),
'format' => $trim($c['format'] ?? null), // "3 min", "Best of 3", …
'match_date' => $this->match_date?->format('Y-m-d'),
'referee' => [
'name' => $this->referee_name ?: null,
'name' => $trim($this->referee_name),
'flag' => $this->flagFor($p['referee_country'] ?? null),
],
'venue' => [
'name' => $this->venue_name ?: ($v['name'] ?? null),
'map_link' => $v['map_link'] ?? null,
'name' => $trim($this->venue_name) ?? $trim($v['name'] ?? null),
'map_link' => $trim($v['map_link'] ?? null),
],
'event_logo' => $this->media['event_poster'] ?? null, // rel path or null
'event_logo' => $trim($m['event_poster'] ?? null),
/*
* Per-video overlay defaults set by the uploader. Shape:
* ['scorebar'=>bool, 'feed'=>bool, 'matchInfo'=>bool, 'timeline'=>bool,
* 'clubs'=>bool, 'flags'=>bool, 'penalties'=>bool, 'cornerLabels'=>'auto|redblue|karate|taekwondo']
* Anything not set falls back to the app defaults in the JS.
*/
'scoreboard_defaults' => is_array($c['scoreboard'] ?? null) ? $c['scoreboard'] : [],
];
}