'date', 'competition' => 'array', 'participants' => 'array', 'media' => 'array', 'officials' => 'array', 'venue' => 'array', 'result' => 'array', 'segments' => 'array', 'statistics' => 'array', 'reviews' => 'array', ]; /** * Shape the record into exactly what the match page header renders. Every value * is null-safe so a video with no/partial match data degrades to just its title * instead of falling back to hard-coded demo content. */ public function headerData(): array { $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' => $trim($this->participant1_name), 'club' => $trim($p['p1_club'] ?? null), 'flag' => $this->flagFor($p['p1_country'] ?? null), // Uploader form (sports-match modal) stores these under the // canonical media.* keys — mirror them to headerData() so the // scoreboard/VS partials get real image paths. 'club_logo' => $trim($m['club1_logo'] ?? $m['p1_club_logo'] ?? null), 'headshot' => $trim($m['participant1_photo'] ?? $m['p1_headshot'] ?? null), ], 'red' => [ 'name' => $trim($this->participant2_name), 'club' => $trim($p['p2_club'] ?? null), 'flag' => $this->flagFor($p['p2_country'] ?? null), 'club_logo' => $trim($m['club2_logo'] ?? $m['p2_club_logo'] ?? null), 'headshot' => $trim($m['participant2_photo'] ?? $m['p2_headshot'] ?? null), ], 'weight_category' => $trim($p['weight_class'] ?? null), 'division' => $trim($c['division'] ?? null), // Sub-header "stage" shown between the two yellow lines above // the weight class (e.g. "Finals", "SEMI FINAL"). Sourced from // the SportsMatch->title column — the form's "Match title" input. 'stage' => $trim($this->title), // "Event title" = the championship / tournament name shown above the // fighters. Prefer the SportsMatch->event_name column (the field the // uploader labels "Event title"), fall back to competition.championship_name. 'championship' => $trim($this->event_name) ?? $trim($c['championship_name'] ?? null), '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' => $trim($this->referee_name), 'flag' => $this->flagFor($p['referee_country'] ?? null), ], 'venue' => [ 'name' => $trim($this->venue_name) ?? $trim($v['name'] ?? null), 'map_link' => $trim($v['map_link'] ?? 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'] : [], ]; } /** Resolve a country name or ISO2 code to a lowercase flag-icons code (or null). */ private function flagFor(?string $country): ?string { $country = trim((string) $country); if ($country === '') { return null; } $countries = \App\Data\Countries::all(); if (preg_match('/^[A-Za-z]{2}$/', $country) && isset($countries[strtoupper($country)])) { return strtolower($country); } foreach ($countries as $iso2 => $meta) { if (strcasecmp($meta['name'], $country) === 0) { return strtolower($iso2); } } return null; } public function video(): BelongsTo { return $this->belongsTo(Video::class); } public function user(): BelongsTo { return $this->belongsTo(User::class); } public function isDraft(): bool { return $this->status === 'draft'; } }