From 04d63cccdb36db0129b8dcb152376be80a68fcee Mon Sep 17 00:00:00 2001 From: ghassan Date: Mon, 10 Aug 2026 18:34:31 +0300 Subject: [PATCH] VS intro screen (arena design), card VS mini + live scorebar, full country names, sports-match edit fix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Match player + preview - Rebuild VS intro as a 1920×1080 scaled canvas (arena design from drafts/): angled RED/BLUE panels with clip-paths, slam-in VS with pulse+shine, event/stage/weight header, Match/Court/Referee chips, per-fighter Record/Rank/Stats chip row. Every placeholder from the artifact is always in the DOM; empty ones are hidden by `.vs-nullable:empty` and `.vs-nullable-hide` so the layout adapts without losing the design's HTML shape. Country codes always render as full country names (via new App\Data\Countries::name()). - Real-time 6s countdown Skip button; video pinned at t=0 while the intro runs, released to play at zero or on Skip. - Standalone preview page at /videos/{video}/vs-preview. Video cards (home + everywhere) - Match cards get a `vs-mini` overlay of the same arena design (fixed canvas, per-card ResizeObserver, animations replay every time the mouse leaves the thumb). On hover the mini explodes outward (panels fly out, VS scales+blurs+fades) and the hover video plays over the cleared thumb. - Match cards also render a `scorebar-mini` overlay (verbatim of the full-player msb-scale scorebar + msb-feed) that shows during hover playback. Live-scoring is wired per-card via a data-sbm-state payload of rounds+points; RED/BLUE score, round label, per-round clock, and the top-right Live Scoring ticker (4 entries, sport-aware Yuko/Waza-ari/Ippon or Punch/Body kick/… labels) all update from the mini video's timeupdate. Bug fixes - Hover video wasn't playing sound: dropped the `muted` attribute I'd added while debugging match previews. - White flash strobing on the left of match cards: removed the `.vs-mini-flash` element (intro-only flourish that fired on every mouseleave replay). - Sports-match modal was showing "Please complete the required fields" when saving basic info for match videos that had no SportsMatch row yet: the submit gate only checked matchId, not the attached videoId; now it correctly skips uploadVideoFirst() in both edit and attach-existing-video modes. Country names project-wide - New helper App\Data\Countries::name($iso2) → full country name. - Swapped `?? country` fallbacks to `?? Countries::name(country)` in admin dashboard, video-analytics, video-insights payloads (VideoController + SuperAdminController), and the VS partials. Now user-visible country labels always show "Bahrain" instead of "BH", including chart tooltips and modal titles. Co-Authored-By: Claude Opus 4.7 (1M context) --- app/Data/Countries.php | 12 + app/Http/Controllers/SuperAdminController.php | 2 +- app/Http/Controllers/VideoController.php | 14 +- drafts/VS Screen Arena - Standalone(1).html | 391 ++++++++++++ drafts/VS Screen Arena - Standalone.html | 391 ------------ drafts/VS-ScreenArena-Standalone.html | 391 ++++++++++++ resources/views/admin/dashboard.blade.php | 17 +- .../views/admin/video-analytics.blade.php | 6 +- .../partials/scorebar-mini.blade.php | 377 ++++++++++++ .../components/partials/vs-mini.blade.php | 536 +++++++++++++++++ .../views/components/video-card.blade.php | 12 +- .../partials/sports-match-modal.blade.php | 8 +- .../videos/partials/match/vs/index.blade.php | 559 ++++++++++++++++++ resources/views/videos/types/match.blade.php | 4 +- resources/views/videos/vs-preview.blade.php | 38 ++ routes/web.php | 4 + 16 files changed, 2351 insertions(+), 411 deletions(-) create mode 100644 drafts/VS Screen Arena - Standalone(1).html delete mode 100644 drafts/VS Screen Arena - Standalone.html create mode 100644 drafts/VS-ScreenArena-Standalone.html create mode 100644 resources/views/components/partials/scorebar-mini.blade.php create mode 100644 resources/views/components/partials/vs-mini.blade.php create mode 100644 resources/views/videos/partials/match/vs/index.blade.php create mode 100644 resources/views/videos/vs-preview.blade.php diff --git a/app/Data/Countries.php b/app/Data/Countries.php index 4dc746d..afbdf98 100644 --- a/app/Data/Countries.php +++ b/app/Data/Countries.php @@ -8,6 +8,18 @@ class Countries * All countries keyed by ISO2 code. * Fields: name, iso2, iso3, flag, dial_code, timezone, currency */ + /** + * Resolve any ISO2 code (case-insensitive) to its full country name. + * Falls back to the uppercased code, then to null for empty input. + * Use this instead of printing raw two-letter codes to end users. + */ + public static function name(?string $iso2): ?string + { + $code = strtoupper(trim((string) $iso2)); + if ($code === '') return null; + return self::all()[$code]['name'] ?? $code; + } + public static function all(): array { // Return lowercase ISO2 code — used as the fi fi-{code} CSS class (flag-icons library) diff --git a/app/Http/Controllers/SuperAdminController.php b/app/Http/Controllers/SuperAdminController.php index 95926bf..f0ad885 100644 --- a/app/Http/Controllers/SuperAdminController.php +++ b/app/Http/Controllers/SuperAdminController.php @@ -506,7 +506,7 @@ class SuperAdminController extends Controller foreach ($uniqueViewers as $viewer) { if (!$viewer->country) continue; if (!isset($countryMap[$viewer->country])) { - $countryMap[$viewer->country] = ['country' => $viewer->country, 'country_name' => $viewer->country_name, 'total' => 0]; + $countryMap[$viewer->country] = ['country' => $viewer->country, 'country_name' => $viewer->country_name ?: \App\Data\Countries::name($viewer->country), 'total' => 0]; } $countryMap[$viewer->country]['total']++; } diff --git a/app/Http/Controllers/VideoController.php b/app/Http/Controllers/VideoController.php index 725fc96..b19241b 100644 --- a/app/Http/Controllers/VideoController.php +++ b/app/Http/Controllers/VideoController.php @@ -3383,7 +3383,7 @@ class VideoController extends Controller $totalGeo = $rawCountries->sum('cnt'); $countries = $rawCountries->map(fn ($c) => [ 'code' => $c->country, - 'name' => $c->country_name, + 'name' => $c->country_name ?: \App\Data\Countries::name($c->country), 'count' => (int) $c->cnt, 'pct' => $totalGeo > 0 ? round($c->cnt / $totalGeo * 100) : 0, ])->values(); @@ -3722,7 +3722,7 @@ class VideoController extends Controller : asset('images/default-avatar.svg')) : null, 'country' => $topCountry ? $topCountry->country : null, - 'country_name' => $topCountry ? $topCountry->country_name : null, + 'country_name' => $topCountry ? ($topCountry->country_name ?: \App\Data\Countries::name($topCountry->country)) : null, 'reach' => $accesses, 'created_at' => $s->created_at, ]; @@ -3868,7 +3868,7 @@ class VideoController extends Controller return response()->json([ 'country' => $country, - 'country_name' => $countryName ?? $country, + 'country_name' => $countryName ?: (\App\Data\Countries::name($country) ?? $country), 'total_views' => $totalViews, 'registered_users' => $registeredUsers, 'guest_count' => $guestCount, @@ -3943,7 +3943,7 @@ class VideoController extends Controller ->orderByDesc('cnt') ->limit(5) ->get() - ->map(fn ($c) => ['code' => $c->country, 'name' => $c->country_name, 'count' => (int) $c->cnt]); + ->map(fn ($c) => ['code' => $c->country, 'name' => $c->country_name ?: \App\Data\Countries::name($c->country), 'count' => (int) $c->cnt]); return response()->json([ 'date' => $day->format('M d, Y'), @@ -3971,7 +3971,7 @@ class VideoController extends Controller ->map(fn ($r) => [ 'type' => $r->type, 'country' => $r->country, - 'country_name' => $r->country_name, + 'country_name' => $r->country_name ?: \App\Data\Countries::name($r->country), 'at' => $r->downloaded_at, ]); @@ -4101,7 +4101,7 @@ class VideoController extends Controller foreach ($accesses as $a) { $code = $a->country ?: 'XX'; if (! isset($countries[$code])) { - $countries[$code] = ['code' => $code, 'name' => $a->country_name ?: $code, 'count' => 0]; + $countries[$code] = ['code' => $code, 'name' => $a->country_name ?: (\App\Data\Countries::name($code) ?? $code), 'count' => 0]; } $countries[$code]['count']++; @@ -4199,7 +4199,7 @@ class VideoController extends Controller foreach ($rows as $r) { $code = $r->country ?: 'XX'; if (! isset($countries[$code])) { - $countries[$code] = ['code' => $code, 'name' => $r->country_name ?: $code, 'count' => 0]; + $countries[$code] = ['code' => $code, 'name' => $r->country_name ?: (\App\Data\Countries::name($code) ?? $code), 'count' => 0]; } $countries[$code]['count']++; } diff --git a/drafts/VS Screen Arena - Standalone(1).html b/drafts/VS Screen Arena - Standalone(1).html new file mode 100644 index 0000000..e7b8920 --- /dev/null +++ b/drafts/VS Screen Arena - Standalone(1).html @@ -0,0 +1,391 @@ + + + + + Bundled Page + + + + +
+ + + + VS + +
+
Unpacking...
+ + + + + + + + + + + + \ No newline at end of file diff --git a/drafts/VS Screen Arena - Standalone.html b/drafts/VS Screen Arena - Standalone.html deleted file mode 100644 index cd08eed..0000000 --- a/drafts/VS Screen Arena - Standalone.html +++ /dev/null @@ -1,391 +0,0 @@ - - - - - Bundled Page - - - - -
- - - - VS - -
-
Unpacking...
- - - - - - - - - - - - \ No newline at end of file diff --git a/drafts/VS-ScreenArena-Standalone.html b/drafts/VS-ScreenArena-Standalone.html new file mode 100644 index 0000000..348ddba --- /dev/null +++ b/drafts/VS-ScreenArena-Standalone.html @@ -0,0 +1,391 @@ + + + + + Bundled Page + + + + +
+ + + + VS + +
+
Unpacking...
+ + + + + + + + + + + + \ No newline at end of file diff --git a/resources/views/admin/dashboard.blade.php b/resources/views/admin/dashboard.blade.php index 2573abb..64b3c14 100644 --- a/resources/views/admin/dashboard.blade.php +++ b/resources/views/admin/dashboard.blade.php @@ -479,10 +479,10 @@ @else @php $maxViews = $viewsByCountry->first()->total; @endphp @foreach($viewsByCountry as $i => $row) -
+
{{ $i + 1 }}
-
{!! $row->country ? countryCodeToFlag($row->country) : countryCodeToFlag('xx') !!}
-
{{ $row->country_name ?? 'Unknown' }}
+
{!! $row->country ? countryCodeToFlag($row->country) : countryCodeToFlag('xx') !!}
+
{{ $row->country_name ?? \App\Data\Countries::name($row->country) ?? 'Unknown' }}
@@ -742,7 +742,11 @@ new Chart(document.getElementById('typeChart'), { // ── Country Chart ─────────────────────────────────────────────── @if($viewsByCountry->isNotEmpty()) (function() { - const countryData = @json($viewsByCountry); + const countryData = @json($viewsByCountry->map(fn($r) => [ + 'country' => $r->country, + 'country_name' => $r->country_name ?? \App\Data\Countries::name($r->country), + 'total' => $r->total, + ])->values()); const labels = countryData.map(r => r.country_name || r.country || ''); const values = countryData.map(r => r.total); const maxVal = Math.max(...values); @@ -1015,7 +1019,10 @@ if (typeChartInst) { // Country bar chart click const countryChartInst = Chart.getChart('countryChart'); if (countryChartInst) { - const countryData = @json($viewsByCountry ?? collect()); + const countryData = @json(($viewsByCountry ?? collect())->map(fn($r) => [ + 'country' => $r->country, + 'country_name' => $r->country_name ?? \App\Data\Countries::name($r->country), + ])->values()); countryChartInst.options.onClick = function(evt, elements) { if (!elements.length) return; const row = countryData[elements[0].index]; diff --git a/resources/views/admin/video-analytics.blade.php b/resources/views/admin/video-analytics.blade.php index f91b1ba..5a5ce1d 100644 --- a/resources/views/admin/video-analytics.blade.php +++ b/resources/views/admin/video-analytics.blade.php @@ -294,7 +294,7 @@ function flagEmoji(string $code): string {
{{ $i + 1 }} {!! flagEmoji($row->country) !!} - {{ $row->country_name ?? $row->country }} + {{ $row->country_name ?? \App\Data\Countries::name($row->country) }}
@@ -423,7 +423,7 @@ function flagEmoji(string $code): string { @if($view->country) {!! flagEmoji($view->country) !!} - {{ $view->country_name ?? $view->country }} + {{ $view->country_name ?? \App\Data\Countries::name($view->country) }} @else Unknown @endif @@ -498,7 +498,7 @@ new Chart(document.getElementById('dailyChart'), { // ── Country chart ─────────────────────────────────────────────────────────── @if($viewsByCountry->isNotEmpty()) -const countryLabels = {!! json_encode($viewsByCountry->map(fn($r) => ($r->country_name ?? $r->country))->values()) !!}; +const countryLabels = {!! json_encode($viewsByCountry->map(fn($r) => ($r->country_name ?? \App\Data\Countries::name($r->country)))->values()) !!}; const countryData = {!! json_encode($viewsByCountry->pluck('total')->values()) !!}; const countryMax = Math.max(...countryData); diff --git a/resources/views/components/partials/scorebar-mini.blade.php b/resources/views/components/partials/scorebar-mini.blade.php new file mode 100644 index 0000000..f026cce --- /dev/null +++ b/resources/views/components/partials/scorebar-mini.blade.php @@ -0,0 +1,377 @@ +{{-- ══════════════════════════════════════════════════════════════════════ + SCOREBAR MINI — the same match scorebar shown on the full player, + rendered inside a video-card thumbnail during hover playback. + + Uses the design's fixed 1150 px canvas (identical to the main player's + msb-scale block). Every card scales its own canvas via ResizeObserver + so the scorebar keeps its pixel-perfect proportions at any card size. + + Static-only: fighter names, flags, clubs, corner labels, "Round 1" + and 0-0 / 0:00. No live-scoring script — hover previews cover the + first few seconds of the match where those values are always the + baseline anyway. +════════════════════════════════════════════════════════════════════════ --}} +@php + $sbm_hd = $video->sportsMatch?->headerData(); + $sbm_red = $sbm_hd['red'] ?? null; + $sbm_blue = $sbm_hd['blue'] ?? null; + $sbm_show = $sbm_hd && (($sbm_red['name'] ?? null) || ($sbm_blue['name'] ?? null)); +@endphp + +@if ($sbm_show) +@php + $sbm_redName = $sbm_red['name'] ?? 'RED'; + $sbm_blueName = $sbm_blue['name'] ?? 'BLUE'; + $sbm_redClub = $sbm_red['club'] ?? ''; + $sbm_blueClub = $sbm_blue['club'] ?? ''; + $sbm_redFlag = strtolower(trim((string) ($sbm_red['flag'] ?? ''))); + $sbm_blueFlag = strtolower(trim((string) ($sbm_blue['flag'] ?? ''))); + $sbm_redLogo = !empty($sbm_red['club_logo']) ? route('media.thumbnail', $sbm_red['club_logo']) : null; + $sbm_blueLogo = !empty($sbm_blue['club_logo']) ? route('media.thumbnail', $sbm_blue['club_logo']) : null; + + // Live-scoring payload — same shape as the full-player scoreboard uses. + // Rounds drive the round label + per-round clock. Points give us the + // score at any t (last point ≤ t defines the score). + $sbm_rounds = $video->matchRounds() + ->orderBy('round_number') + ->get(['round_number', 'name', 'start_time_seconds']) + ->map(fn ($r) => [ + 'n' => (int) $r->round_number, + 'name' => (string) ($r->name ?? ''), + 'start' => (float) ($r->start_time_seconds ?? 0), + ])->values()->all(); + if (empty($sbm_rounds)) $sbm_rounds = [['n' => 1, 'name' => '', 'start' => 0]]; + + $sbm_points = $video->matchPoints() + ->orderBy('timestamp_seconds') + ->get(['timestamp_seconds', 'action', 'points', 'competitor', 'score_red', 'score_blue']) + ->flatMap(function ($p) { + // Same rule the full-player uses: a "both" event emits two feed rows + // — one per corner — so the ticker shows both fighters scoring. + $side = ($p->competitor === 'both') ? 'red' : $p->competitor; + $rows = [[ + 't' => (float) $p->timestamp_seconds, + 'action' => (string) ($p->action ?? 'Point'), + 'pts' => (int) $p->points, + 'side' => $side, + 'sr' => (int) ($p->score_red ?? 0), + 'sb' => (int) ($p->score_blue ?? 0), + ]]; + if ($p->competitor === 'both') { + $rows[] = array_merge($rows[0], ['side' => 'blue', 't' => (float) $p->timestamp_seconds + 0.001]); + } + return $rows; + })->values()->all(); + + $sbm_sport = $sbm_hd['sport'] ?? null; + + $sbm_data = json_encode(['rounds' => $sbm_rounds, 'points' => $sbm_points, 'sport' => $sbm_sport], JSON_HEX_APOS | JSON_HEX_QUOT); +@endphp + +
+ {{-- Live scoring ticker — sits in its OWN scaled canvas at the + top-right of the thumb. Populated by the sbm live-sync JS. --}} +
+
+
Live scoring
+
+
+
+ +
+ {{-- ══ Scorebar markup — same design + inline styles as the + overlay.blade.php `data-msb-part="scorebar"` block, but stripped + of the live-sync data-msb hooks. ══ --}} +
+ + {{-- RED / AKA --}} +
+
+
+
+
+ @if ($sbm_redFlag !== '') + + @endif + {{ $sbm_redName }} +
+ @if ($sbm_redClub !== '') +
+ {{ $sbm_redClub }} +
+ @endif +
+
+ AKA + 0 +
+
+
+ + {{-- CENTER: round + clock --}} +
+
Round 1
+
0:00
+
+ + + +
+
+ + {{-- BLUE / AO --}} +
+
+
+ AO + 0 +
+
+
+ {{ $sbm_blueName }} + @if ($sbm_blueFlag !== '') + + @endif +
+ @if ($sbm_blueClub !== '') +
+ {{ $sbm_blueClub }} +
+ @endif +
+
+
+
+ +
+
+
+@endif + +@once + + + + + + + +@endonce diff --git a/resources/views/components/partials/vs-mini.blade.php b/resources/views/components/partials/vs-mini.blade.php new file mode 100644 index 0000000..2b60f8f --- /dev/null +++ b/resources/views/components/partials/vs-mini.blade.php @@ -0,0 +1,536 @@ +{{-- ══════════════════════════════════════════════════════════════════════ + VS MINI — the arena intro card rendered INSIDE a video-card thumbnail. + No countdown / no skip button; identical layout to the full-page VS + intro (fixed 1920×1080 stage, transform-scaled to fit the thumb). + + Behavior: + - Sits at z-index 1 (above the still image, BELOW the hover video + which uses z-index 2 + .active { opacity: 1 }). + - On mouseleave from the thumb, animations restart so the intro + plays again the next time the user's mouse is off the card. +════════════════════════════════════════════════════════════════════════ --}} +@php + $vsm_hd = $video->sportsMatch?->headerData(); + $vsm_red = $vsm_hd['red'] ?? null; + $vsm_blue = $vsm_hd['blue'] ?? null; + $vsm_show = $vsm_hd && (($vsm_red['name'] ?? null) || ($vsm_blue['name'] ?? null)); +@endphp + +@if ($vsm_show) +@php + $vsm_flag = fn(?string $c) => (($c = strtolower(trim((string) $c))) !== '' ? $c : 'xx'); + $vsm_cname = fn (?string $c) => \App\Data\Countries::name($c); + $vsm_img = fn(?string $p) => $p ? route('media.thumbnail', $p) : null; + $vsm_str = fn ($x) => (is_string($x) && trim($x) !== '') ? trim($x) : ''; + + // Every placeholder from the design is always in the DOM; empty ones + // are hidden by `.vsm-nullable:empty` / `.vsm-nullable-hide` in the + // CSS below. Same treatment as the full-page VS. + $vsm_event = $vsm_str($vsm_hd['championship'] ?? ''); + $vsm_stage = $vsm_str($vsm_hd['division'] ?? '') ?: $vsm_str($vsm_hd['format'] ?? ''); + $vsm_weight = $vsm_str($vsm_hd['weight_category'] ?? ''); + $vsm_matchNo = $vsm_str($vsm_hd['match_number'] ?? ''); + $vsm_court = $vsm_str($vsm_hd['court'] ?? ''); + $vsm_ref = $vsm_str($vsm_hd['referee']['name'] ?? ''); + + // Per-fighter chips (Record / Rank / Stats) — fields aren't in + // headerData() today, so fall back to raw participants JSON. + $vsm_parts = $video->sportsMatch?->participants ?? []; + $vsm_redRecord = $vsm_str($vsm_parts['p2_record'] ?? ''); + $vsm_redRank = $vsm_str($vsm_parts['p2_rank'] ?? ''); + $vsm_redStats = $vsm_str($vsm_parts['p2_stats'] ?? ''); + $vsm_blueRecord = $vsm_str($vsm_parts['p1_record'] ?? ''); + $vsm_blueRank = $vsm_str($vsm_parts['p1_rank'] ?? ''); + $vsm_blueStats = $vsm_str($vsm_parts['p1_stats'] ?? ''); + + $vsm_redName = $vsm_str($vsm_red['name'] ?? ''); + $vsm_blueName = $vsm_str($vsm_blue['name'] ?? ''); + $vsm_redClub = $vsm_str($vsm_red['club'] ?? ''); + $vsm_blueClub = $vsm_str($vsm_blue['club'] ?? ''); + $vsm_redFlag = $vsm_str($vsm_red['flag'] ?? ''); + $vsm_blueFlag = $vsm_str($vsm_blue['flag'] ?? ''); + $vsm_redLogo = $vsm_img($vsm_red['club_logo'] ?? null); + $vsm_blueLogo = $vsm_img($vsm_blue['club_logo'] ?? null); +@endphp + +
+
+ {{-- RED panel --}} +
+
+
+
+
+ {{-- BLUE panel --}} +
+
+
+
+
+ +
+ + {{-- RED identity (left) — every placeholder always in DOM ── --}} +
+
AKA · RED
+
+ +
{{ $vsm_redFlag !== '' ? strtoupper($vsm_cname($vsm_redFlag) ?? $vsm_redFlag) : '' }}
+
+
{{ $vsm_redName }}
+
+ +
{{ $vsm_redClub }}
+
+
+
{{ $vsm_redRecord }}
+
{{ $vsm_redRank }}
+
{{ $vsm_redStats }}
+
+
+ + {{-- BLUE identity (right) ── --}} +
+
AO · BLUE
+
+ +
{{ $vsm_blueFlag !== '' ? strtoupper($vsm_cname($vsm_blueFlag) ?? $vsm_blueFlag) : '' }}
+
+
{{ $vsm_blueName }}
+
+ +
{{ $vsm_blueClub }}
+
+
+
{{ $vsm_blueRecord }}
+
{{ $vsm_blueRank }}
+
{{ $vsm_blueStats }}
+
+
+ + {{-- Top: event + stage + weight (all in DOM) --}} +
+
{{ $vsm_event }}
+
+
+
{{ $vsm_stage }}
+
+
+
{{ $vsm_weight }}
+
+ + {{-- Center VS --}} +
+
VS +
+
+
+ {{-- .vs-mini-flash removed — the intro's full-thumb white flash at + t=1.5s was firing on every mouseleave-triggered replay, which + read as strobing on the left edge of the card. --}} + + {{-- Bottom chips — Match / Court / Referee always in DOM; each hides + via data-vsm-empty and the flanking diamonds collapse via :has(). --}} +
+
+ Match{{ $vsm_matchNo }} +
+
+
+ Court{{ $vsm_court }} +
+
+
+ Referee{{ $vsm_ref }} +
+
+
+
+@endif + +@once + + + + + + + +@endonce diff --git a/resources/views/components/video-card.blade.php b/resources/views/components/video-card.blade.php index dc2231b..fc41459 100644 --- a/resources/views/components/video-card.blade.php +++ b/resources/views/components/video-card.blade.php @@ -54,11 +54,21 @@ $sizeClasses = match($size) {
{{ $video->title ?? 'Video' }} + @if($video && $video->type === 'match' && $video->sportsMatch) + @include('components.partials.vs-mini', ['video' => $video]) + @endif @if($videoUrl) -