- SportsMatchController syncs Video.title with SportsMatch.title on
store/update so the "Match title" field drives the video's display
title everywhere.
- headerData(): "championship" now prefers event_name (event title)
and adds a dedicated "stage" key sourced from SportsMatch.title
(shown between the yellow lines above the weight class in both the
in-player VS overlay and the vs-mini gallery card).
- Video cards render match titles with flags + corner colours
("FINALS – 🇧🇭 Blue vs 🇧🇭 Red (-61 kg)"), matching the video
page header, and clip long titles to one line.
- Video gallery grid uses repeat(N, minmax(0, 1fr)) + grid-auto-rows:1fr
so every card thumbnail renders at exactly the same size; hover-preview
videos use object-fit:cover to stop portrait sources looking smaller.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
136 lines
5.6 KiB
PHP
136 lines
5.6 KiB
PHP
<?php
|
|
|
|
namespace App\Models;
|
|
|
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
|
use Illuminate\Database\Eloquent\Model;
|
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
|
|
|
class SportsMatch extends Model
|
|
{
|
|
use HasFactory;
|
|
|
|
protected $table = 'sports_matches';
|
|
|
|
protected $fillable = [
|
|
'video_id', 'user_id', 'status',
|
|
'sport', 'title', 'event_name', 'match_type',
|
|
'match_date', 'match_time',
|
|
'participant1_name', 'participant2_name', 'referee_name', 'venue_name',
|
|
'competition', 'participants', 'media', 'officials',
|
|
'venue', 'result', 'segments', 'statistics', 'reviews',
|
|
];
|
|
|
|
protected $casts = [
|
|
'match_date' => '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';
|
|
}
|
|
}
|