Wire match header to real SportsMatch data; add Match#/Court + country flags

The match page header rendered hard-coded demo content (Sami Al Manea vs Hassan
Al Shallan, Match# 103, referee Ghassan Yusuf, external takeone-dev links) on
EVERY match video, ignoring the SportsMatch record the create form saves.

- Video::sportsMatch() hasOne relationship.
- SportsMatch::headerData() maps the record into the header's shape (fighters +
  country flags, championship, weight class, referee, venue, match#/court),
  null-safe, with a country-name/ISO2 -> flag-icons resolver.
- match.blade header now renders from headerData(): real names/flags, no
  external links, and degrades gracefully to just title/views/date when a video
  has no match data (instead of the fake card).
- sports-match-modal: add Match # and Court fields (stored in the existing
  competition JSON, no migration) and convert fighter/referee country inputs to
  the mandated <x-country-select> so header flags resolve from ISO2.
- Tracker updated for the new country-select usages.

Verified: empty state (no SportsMatch) shows only title/views/date; a populated
record renders fighters, flags (bh/us), championship, Match# 42, Court 3, U21,
referee; modal compiles with the new fields.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
ghassan 2026-07-29 01:57:07 +03:00
parent 8ea63a6720
commit af276f2cd1
5 changed files with 150 additions and 100 deletions

View File

@ -144,6 +144,9 @@ Stored value: ISO2 code (e.g. `"BH"`).
|---|---|---|
| `resources/views/user/profile.blade.php` | `nationality` | Edit Profile form |
| `resources/views/auth/register.blade.php` | `nationality` | Registration form — mandatory |
| `resources/views/layouts/partials/sports-match-modal.blade.php` | `participants[p1_country]` | Fighter 1 country → header flag |
| `resources/views/layouts/partials/sports-match-modal.blade.php` | `participants[p2_country]` | Fighter 2 country → header flag |
| `resources/views/layouts/partials/sports-match-modal.blade.php` | `participants[referee_country]` | Referee country → header flag |
---

View File

@ -34,6 +34,63 @@ class SportsMatch extends Model
'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 ?? [];
return [
'blue' => [
'name' => $this->participant1_name ?: null,
'club' => $p['p1_club'] ?? null,
'flag' => $this->flagFor($p['p1_country'] ?? null),
],
'red' => [
'name' => $this->participant2_name ?: null,
'club' => $p['p2_club'] ?? null,
'flag' => $this->flagFor($p['p2_country'] ?? 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,
'referee' => [
'name' => $this->referee_name ?: null,
'flag' => $this->flagFor($p['referee_country'] ?? null),
],
'venue' => [
'name' => $this->venue_name ?: ($v['name'] ?? null),
'map_link' => $v['map_link'] ?? null,
],
'event_logo' => $this->media['event_poster'] ?? null, // rel path or null
];
}
/** 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);

View File

@ -434,6 +434,12 @@ class Video extends Model
return $this->hasMany(CoachReview::class)->orderBy('start_time_seconds');
}
// The sports match record backing a type=match video (fighters, referee, etc.)
public function sportsMatch()
{
return $this->hasOne(SportsMatch::class);
}
// Get recent views count (within hours)
public function getRecentViews($hours = 48)
{

View File

@ -91,6 +91,16 @@
<div class="invalid-feedback" data-field="match_time"></div>
</div>
</div>
<div class="row g-3 mt-0">
<div class="col-sm-6">
<label for="sm-match-number" class="form-label">Match #</label>
<input type="text" class="form-control" name="competition[match_number]" id="sm-match-number" placeholder="e.g. 103">
</div>
<div class="col-sm-6">
<label for="sm-court" class="form-label">Court / field</label>
<input type="text" class="form-control" name="competition[court]" id="sm-court" placeholder="e.g. 2">
</div>
</div>
</div>
{{-- Participants --}}
@ -126,8 +136,16 @@
<div class="d-flex align-items-center gap-3">
<x-sports-image name="media_referee_photo" label="Photo" class="sm-img-avatar" />
<div class="flex-grow-1">
<label for="sm-referee" class="form-label">Referee name</label>
<input type="text" class="form-control" name="referee_name" id="sm-referee" placeholder="Referee name">
<div class="row g-3">
<div class="col-sm-7">
<label for="sm-referee" class="form-label">Referee name</label>
<input type="text" class="form-control" name="referee_name" id="sm-referee" placeholder="Referee name">
</div>
<div class="col-sm-5">
<label class="form-label">Country</label>
<x-country-select name="participants[referee_country]" placeholder="Country" />
</div>
</div>
</div>
</div>
</div>
@ -196,7 +214,7 @@
<div class="col-sm-6"><label class="form-label">Type</label>
<input type="text" class="form-control" name="participants[p1_type]" placeholder="Individual / Team / Pair"></div>
<div class="col-sm-6"><label class="form-label">Country</label>
<input type="text" class="form-control" name="participants[p1_country]"></div>
<x-country-select name="participants[p1_country]" placeholder="Select country" /></div>
<div class="col-sm-6"><label class="form-label">Role</label>
<input type="text" class="form-control" name="participants[p1_role]" placeholder="home, away, red, blue…"></div>
</div>
@ -207,7 +225,7 @@
<div class="col-sm-6"><label class="form-label">Type</label>
<input type="text" class="form-control" name="participants[p2_type]" placeholder="Individual / Team / Pair"></div>
<div class="col-sm-6"><label class="form-label">Country</label>
<input type="text" class="form-control" name="participants[p2_country]"></div>
<x-country-select name="participants[p2_country]" placeholder="Select country" /></div>
<div class="col-sm-6"><label class="form-label">Role</label>
<input type="text" class="form-control" name="participants[p2_role]" placeholder="home, away, red, blue…"></div>
</div>

View File

@ -2014,62 +2014,10 @@
@section('content')
@php
// ===== DUMMY DATA FALLBACKS =====
$blueFighter =
$blueFighter ??
(object) [
'name' => 'Sami Al Manea',
'flag' => 'bh',
'photo' => 'https://takeone-dev.innovator.bh/storage/images/profiles/profile_5.png?v=1773157950',
'team' => 'Emperor TaeKwonDo Academy',
'country' => 'Bahrain',
'logo' => 'https://takeone-dev.innovator.bh/storage/clubs/1/branding/logo_1771488666.png',
'member_id' => 5,
'club_url' => 'https://takeone-dev.innovator.bh/mobile/eta',
];
$redFighter =
$redFighter ??
(object) [
'name' => 'Hassan Al Shallan',
'flag' => 'bh',
'photo' => 'https://takeone-dev.innovator.bh/storage/images/profiles/profile_11.png?t=1773268056907',
'team' => 'Legend TaeKwonDo Academy',
'country' => 'Bahrain',
'logo' => 'https://takeone-dev.innovator.bh/storage/clubs/logos/logo_1773267551.png',
'member_id' => 11,
'club_url' => 'https://takeone-dev.innovator.bh/clubs/legend-taekwondo-academy',
];
$match =
$match ??
(object) [
'number' => '103',
'court' => '2',
'weight_category' => 'U18',
'score_blue' => 7,
'score_red' => 5,
'features' =>
'Electronic scoring system (PSS & headgear) • Video replay available for head kicks and gam-jeom appeals',
];
$referee =
$referee ??
(object) [
'name' => 'Ghassan Yusuf',
'flag' => 'bh',
'photo' => 'https://takeone-dev.innovator.bh/storage/images/profiles/profile_8.png?t=1773267305519',
'member_id' => 4,
];
$championship =
$championship ??
(object) [
'name' => 'Championship 2026',
'url' => '/championship/2026',
];
$venue =
$venue ??
(object) [
'name' => 'Manama Sports Hall',
'map_link' => 'https://maps.google.com/?q=Manama+Sports+Hall+Bahrain',
];
// Real match data (fighters, referee, championship, match#/court, venue)
// for this video. null when no SportsMatch record exists — the header then
// degrades to just the title/views/date instead of showing demo content.
$hd = $video->sportsMatch?->headerData();
@endphp
<!-- Video Layout Container -->
@ -2096,52 +2044,70 @@
<div class="event-header">
<div class="event-header-left">
<div class="event-logo">
<img src="https://picsum.photos/seed/event-logo/200/200" alt="Event logo">
@if ($hd && $hd['event_logo'])
<img src="{{ route('media.thumbnail', $hd['event_logo']) }}" alt="Event logo">
@else
<i class="bi bi-trophy" style="font-size: 26px; color: var(--text-secondary);"></i>
@endif
</div>
<div class="event-text">
<!-- Title with Trophy Icon BEFORE "Taekwondo Finals" -->
@php
$flagStyle = 'width:20px;height:15px;border-radius:2px;display:inline-block;vertical-align:middle;';
$hasBlue = $hd && $hd['blue']['name'];
$hasRed = $hd && $hd['red']['name'];
// Ordered inline meta pieces — only the ones that actually have data.
$metaPieces = [];
if ($hd) {
if ($hd['championship']) $metaPieces[] = ['type' => 'champ', 'val' => $hd['championship']];
if ($hd['match_number']) $metaPieces[] = ['type' => 'num', 'val' => $hd['match_number']];
if ($hd['court']) $metaPieces[] = ['type' => 'court', 'val' => $hd['court']];
if ($hd['referee']['name']) $metaPieces[] = ['type' => 'ref', 'val' => $hd['referee']];
}
@endphp
<h1 class="title">
<i class="bi bi-trophy trophy-icon" title="Match Type"></i>
<span>{{ $video->title }} </span>
<span style="color: #2563eb; font-weight: bold">
<span class="fi fi-{{ $blueFighter->flag ?? 'bh' }}" style="width:20px;height:15px;border-radius:2px;display:inline-block;vertical-align:middle;"></span>
<a href="https://takeone-dev.innovator.bh/member/{{ $blueFighter->member_id ?? 5 }}"
title="View {{ $blueFighter->name ?? 'Sami Al Manea' }}'s profile">
{{ $blueFighter->name ?? 'Sami Al Manea' }}
</a>
</span>
<span>vs</span>
<span style="color: #ef4444; font-weight: bold">
<span class="fi fi-{{ $redFighter->flag ?? 'bh' }}" style="width:20px;height:15px;border-radius:2px;display:inline-block;vertical-align:middle;"></span>
<a href="https://takeone-dev.innovator.bh/member/{{ $redFighter->member_id ?? 11 }}"
title="View {{ $redFighter->name ?? 'Hassan Al Shallan' }}'s profile">
{{ $redFighter->name ?? 'Hassan Al Shallan' }}
</a>
</span>
<span>({{ $match->weight_category ?? 'U18' }})</span>
<i class="bi bi-trophy trophy-icon" title="Sports match"></i>
<span>{{ $video->title }}@if ($hasBlue || $hasRed) @endif</span>
@if ($hasBlue)
<span style="color: #2563eb; font-weight: bold">
@if ($hd['blue']['flag'])<span class="fi fi-{{ $hd['blue']['flag'] }}" style="{{ $flagStyle }}"></span>@endif
{{ $hd['blue']['name'] }}
</span>
@endif
@if ($hasBlue && $hasRed)<span>vs</span>@endif
@if ($hasRed)
<span style="color: #ef4444; font-weight: bold">
@if ($hd['red']['flag'])<span class="fi fi-{{ $hd['red']['flag'] }}" style="{{ $flagStyle }}"></span>@endif
{{ $hd['red']['name'] }}
</span>
@endif
@if ($hd && $hd['weight_category'])<span>({{ $hd['weight_category'] }})</span>@endif
</h1>
<!-- Meta: Championship Link Match# • Court • Referee (all inline) -->
<a class="meta-link" href="{{ $championship->url ?? '/championship/2026' }}"
style="white-space: nowrap; display: inline-flex; align-items: center; gap: 4px; flex-wrap: wrap; line-height: 1.4;">
<span
style="color: #ffffff; font-weight: bold; font-size: 0.85rem;">{{ $championship->name ?? 'Championship 2026' }}</span>
<span style="color: var(--text-secondary);"></span>
<span style="color: #ff00dd; font-weight: bold; font-size: 0.85rem;">Match#
{{ $match->number ?? '103' }}</span>
<span style="color: var(--text-secondary);"></span>
<span style="color: #00af09; font-weight: bold; font-size: 0.85rem;">Court
{{ $match->court ?? '2' }}</span>
<span style="color: var(--text-secondary);"></span>
<span
style="color: #ffd900; font-weight: bold; font-size: 0.85rem; display: inline-flex; align-items: center; gap: 2px;">
<span class="fi fi-{{ $referee->flag ?? 'bh' }}" style="width:20px;height:15px;border-radius:2px;display:inline-block;vertical-align:middle;"></span> {{ $referee->name ?? 'Ghassan Yusuf' }}
{{-- Inline meta: championship Match# • Court • referee • views/date.
Only non-empty pieces render; views/date always show. --}}
<div class="meta-link"
style="display: inline-flex; align-items: center; gap: 4px; flex-wrap: wrap; line-height: 1.4;">
@foreach ($metaPieces as $piece)
@if (!$loop->first)<span style="color: var(--text-secondary);"></span>@endif
@if ($piece['type'] === 'champ')
<span style="color: #ffffff; font-weight: bold; font-size: 0.85rem;">{{ $piece['val'] }}</span>
@elseif ($piece['type'] === 'num')
<span style="color: #ff00dd; font-weight: bold; font-size: 0.85rem;">Match# {{ $piece['val'] }}</span>
@elseif ($piece['type'] === 'court')
<span style="color: #00af09; font-weight: bold; font-size: 0.85rem;">Court {{ $piece['val'] }}</span>
@elseif ($piece['type'] === 'ref')
<span style="color: #ffd900; font-weight: bold; font-size: 0.85rem; display: inline-flex; align-items: center; gap: 2px;">
@if ($piece['val']['flag'])<span class="fi fi-{{ $piece['val']['flag'] }}" style="{{ $flagStyle }}"></span>@endif
{{ $piece['val']['name'] }}
</span>
@endif
@endforeach
@if (count($metaPieces))<span style="color: var(--text-secondary);"></span>@endif
<span style="color: var(--text-secondary); font-weight: bold; font-size: 0.85rem;">
{{ number_format($video->view_count) }} views {{ $video->created_at->format('M d, Y') }}
</span>
<span
style="color: var(--text-secondary); font-weight: bold; font-size: 0.85rem; display: inline-flex; align-items: center; gap: 2px;">
- {{ number_format($video->view_count) }} views
{{ $video->created_at->format('M d, Y') }}</span>
</a>
</div>
</div>
</div>