From cba8e01d56f1d2504428f7aa634de0c05cc2030b Mon Sep 17 00:00:00 2001 From: ghassan Date: Sat, 8 Aug 2026 07:20:50 +0300 Subject: [PATCH] Match videos: full re-hydration on Up Next SPA navigation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes a raft of things breaking when jumping between match videos via the Up Next / playlist sidebar without a page reload: - Highlights toggle stopped working (button held a click handler pointing at the OLD, detached .events-sidebar node). - Points / Coach review tabs went unclickable (new .tab-button elements had no bound handlers). - Event-item clicks (jump-to-timestamp on point / review cards) stopped seeking / playing. - window.matchRounds / window.matchReviews and const videoId were frozen at first page load, so edit/delete/save handlers routed to the wrong video. - Blade point card leaked "{{ $fmtTime(...) }}" as literal text because of a misused @{{ }} escape directive. Changes - videoId + isOwner now live on window so they can be updated at runtime; `var videoId` at module scope keeps existing handlers working. - Extracted tab-button + attachEventListeners wiring into window.initMatchTabsAndEvents (idempotent — _tabBound flag). - Added window.reloadMatchVideoState(url): parses the new slug from the URL, updates window.videoId, calls loadMatchData (refreshes cache + re-renders sidebar), then re-runs the tabs/events/toggle/sidebar-height inits. - recSwapContent and plSwapContent call reloadMatchVideoState whenever the target page has an .events-sidebar (falls back to just the toggle re-init for non-match targets). - Fixed the Blade point-card time: {{ '@' . $fmtTime(...) }} instead of @{{ ... }} so the timestamp actually renders instead of showing braces. Co-Authored-By: Claude Opus 4.7 (1M context) --- resources/views/videos/types/match.blade.php | 100 +++++++++++++++---- 1 file changed, 81 insertions(+), 19 deletions(-) diff --git a/resources/views/videos/types/match.blade.php b/resources/views/videos/types/match.blade.php index 52d863d..f79803f 100644 --- a/resources/views/videos/types/match.blade.php +++ b/resources/views/videos/types/match.blade.php @@ -2419,14 +2419,24 @@ its click listener and the Highlights pane can't be opened. --}} @@ -2573,7 +2590,7 @@ @endphp
-
@{{ $fmtTime($point->timestamp_seconds) }}
+
{{ '@' . $fmtTime($point->timestamp_seconds) }}
{{ $label }} @@ -2810,6 +2827,14 @@ document.body.removeChild(ns); } } + // Full re-hydration for match videos when navigating + // between them from a playlist + if (document.querySelector('.events-sidebar') && + typeof window.reloadMatchVideoState === 'function') { + window.reloadMatchVideoState(url); + } else if (typeof window.initMatchHighlightsToggle === 'function') { + window.initMatchHighlightsToggle(); + } } catch(e){ console.warn('plSwapContent',e); } } @@ -3078,6 +3103,14 @@ var ns=document.createElement('script'); ns.textContent=s.textContent; document.body.appendChild(ns); document.body.removeChild(ns); }); } + // Full re-hydration for match videos: updates videoId, + // reloads match data, rebinds tabs / clicks / toggle. + if (document.querySelector('.events-sidebar') && + typeof window.reloadMatchVideoState === 'function') { + window.reloadMatchVideoState(url); + } else if (typeof window.initMatchHighlightsToggle === 'function') { + window.initMatchHighlightsToggle(); + } } catch(e){ console.warn('recSwapContent', e); } } @@ -3657,11 +3690,14 @@ }); } - // NOTE: emit these with the json Blade directive (raw output). Escaped echo - // braces HTML-escape the quotes (videoId becomes an entity-encoded string) and - // crash this whole block — which loads match data, switches tabs, drives CRUD. - const videoId = @json(isset($video) ? $video->getRouteKey() : ''); - const isOwner = @json(Auth::check() && isset($video) && Auth::id() === $video->user_id); + // These live on `window` so SPA navigation (recSwapContent) can update + // them for the newly-loaded match without a page refresh. Downstream + // code references `videoId` and `isOwner` as free identifiers, which + // resolve to window.videoId / window.isOwner via the global scope. + window.videoId = @json(isset($video) ? $video->getRouteKey() : ''); + window.isOwner = @json(Auth::check() && isset($video) && Auth::id() === $video->user_id); + var videoId = window.videoId; + var isOwner = window.isOwner; document.addEventListener('DOMContentLoaded', function() { loadMatchData(); @@ -3903,19 +3939,28 @@ initSidebarHeightSync(); } - document.addEventListener('DOMContentLoaded', function() { + // Extracted so SPA nav can rebind against the fresh .tab-button / .event-item + // elements after the sidebar is swapped in. + window.initMatchTabsAndEvents = function () { const tabButtons = document.querySelectorAll('.tab-button'); - const tabPanels = document.querySelectorAll('.tab-panel'); + const tabPanels = document.querySelectorAll('.tab-panel'); tabButtons.forEach(button => { + if (button._tabBound) return; + button._tabBound = true; button.addEventListener('click', function() { const targetTab = this.getAttribute('data-tab'); - tabButtons.forEach(btn => btn.classList.remove('active')); - tabPanels.forEach(panel => panel.classList.remove('active')); + // Re-query on every click — the sidebar may have been re-rendered + document.querySelectorAll('.tab-button').forEach(btn => btn.classList.remove('active')); + document.querySelectorAll('.tab-panel').forEach(panel => panel.classList.remove('active')); this.classList.add('active'); - document.getElementById('tab-' + targetTab).classList.add('active'); + const target = document.getElementById('tab-' + targetTab); + if (target) target.classList.add('active'); }); }); attachEventListeners(); + }; + document.addEventListener('DOMContentLoaded', function() { + window.initMatchTabsAndEvents(); document.getElementById('addRoundForm')?.addEventListener('submit', function(e) { e.preventDefault(); storeRound( @@ -3925,14 +3970,31 @@ document.getElementById('roundStartTime').value ); }); - // pointForm removed — inline capture strip handles submit document.getElementById('editRoundForm')?.addEventListener('submit', function(e) { e.preventDefault(); updateRound(); }); - // reviewForm removed — inline review-capture strip handles submit }); + // Called from recSwapContent / plSwapContent when SPA navigation lands + // on a different match video. Updates window.videoId from the URL, then + // reloads the match data and re-binds all UI handlers. + window.reloadMatchVideoState = function (url) { + try { + const path = new URL(url, window.location.origin).pathname; + const parts = path.split('/').filter(Boolean); + const idx = parts.indexOf('videos'); + if (idx !== -1 && parts[idx + 1]) window.videoId = parts[idx + 1]; + } catch (_) {} + // Keep the local `videoId` reference in sync for handlers that + // captured the free identifier at module scope + try { videoId = window.videoId; } catch (_) {} + if (typeof loadMatchData === 'function') loadMatchData(); + if (typeof window.initMatchTabsAndEvents === 'function') window.initMatchTabsAndEvents(); + if (typeof window.initMatchHighlightsToggle === 'function') window.initMatchHighlightsToggle(); + if (typeof initSidebarHeightSync === 'function') initSidebarHeightSync(); + }; + let reviewPlaybackStopHandler = null; let reviewPlaybackEndTime = null;