Match videos: full re-hydration on Up Next SPA navigation
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) <noreply@anthropic.com>
This commit is contained in:
parent
716693a74f
commit
cba8e01d56
@ -2419,14 +2419,24 @@
|
|||||||
its click listener and the Highlights pane can't be opened. --}}
|
its click listener and the Highlights pane can't be opened. --}}
|
||||||
<script>
|
<script>
|
||||||
// Match Highlights Toggle with localStorage persistence
|
// Match Highlights Toggle with localStorage persistence
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
// Extracted into a globally-callable initializer so SPA navigation
|
||||||
|
// (recSwapContent / plSwapContent) can re-bind the click handler
|
||||||
|
// against the FRESH .events-sidebar node after the sidebar is
|
||||||
|
// swapped in from the target page.
|
||||||
|
window.initMatchHighlightsToggle = function () {
|
||||||
const toggleBtn = document.getElementById('matchHighlightsToggle');
|
const toggleBtn = document.getElementById('matchHighlightsToggle');
|
||||||
const sidebar = document.querySelector('.events-sidebar');
|
const sidebar = document.querySelector('.events-sidebar');
|
||||||
// NOTE: emit videoId with the json Blade directive (raw), never with
|
// Prefer the freshest videoId in case SPA nav updated it
|
||||||
// escaped echo braces — those HTML-escape the quotes and crash the script.
|
const videoId = window.videoId || @json(isset($video) ? $video->getRouteKey() : '');
|
||||||
const videoId = @json(isset($video) ? $video->getRouteKey() : '');
|
if (toggleBtn && toggleBtn._hlBound && sidebar && toggleBtn._hlSidebar === sidebar) {
|
||||||
|
return; // Already bound to this exact sidebar node — nothing to do
|
||||||
|
}
|
||||||
|
|
||||||
if (toggleBtn && sidebar) {
|
if (toggleBtn && sidebar) {
|
||||||
|
// Wipe any prior click listener bound to a stale sidebar
|
||||||
|
if (toggleBtn._hlHandler) toggleBtn.removeEventListener('click', toggleBtn._hlHandler);
|
||||||
|
toggleBtn._hlBound = true;
|
||||||
|
toggleBtn._hlSidebar = sidebar;
|
||||||
const backdrop = document.getElementById('hlSheetBackdrop');
|
const backdrop = document.getElementById('hlSheetBackdrop');
|
||||||
const grab = sidebar.querySelector('.hl-sheet-grab');
|
const grab = sidebar.querySelector('.hl-sheet-grab');
|
||||||
const mainScroll = document.getElementById('main');
|
const mainScroll = document.getElementById('main');
|
||||||
@ -2471,7 +2481,8 @@
|
|||||||
setOpen(true);
|
setOpen(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleBtn.addEventListener('click', () => setOpen(!sidebar.classList.contains('show')));
|
toggleBtn._hlHandler = () => setOpen(!sidebar.classList.contains('show'));
|
||||||
|
toggleBtn.addEventListener('click', toggleBtn._hlHandler);
|
||||||
if (backdrop) backdrop.addEventListener('click', () => setOpen(false));
|
if (backdrop) backdrop.addEventListener('click', () => setOpen(false));
|
||||||
if (grab) grab.addEventListener('click', () => setOpen(false));
|
if (grab) grab.addEventListener('click', () => setOpen(false));
|
||||||
|
|
||||||
@ -2508,7 +2519,13 @@
|
|||||||
document.addEventListener('webkitfullscreenchange', onFsChange);
|
document.addEventListener('webkitfullscreenchange', onFsChange);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
};
|
||||||
|
// First run on initial page load
|
||||||
|
if (document.readyState === 'loading') {
|
||||||
|
document.addEventListener('DOMContentLoaded', window.initMatchHighlightsToggle);
|
||||||
|
} else {
|
||||||
|
window.initMatchHighlightsToggle();
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
@ -2573,7 +2590,7 @@
|
|||||||
@endphp
|
@endphp
|
||||||
<div class="event-item" data-time-start="{{ (int) $point->timestamp_seconds }}"
|
<div class="event-item" data-time-start="{{ (int) $point->timestamp_seconds }}"
|
||||||
data-round="{{ $round->round_number }}" data-id="pt{{ $point->id }}">
|
data-round="{{ $round->round_number }}" data-id="pt{{ $point->id }}">
|
||||||
<div class="event-time">@{{ $fmtTime($point->timestamp_seconds) }}</div>
|
<div class="event-time">{{ '@' . $fmtTime($point->timestamp_seconds) }}</div>
|
||||||
<div>
|
<div>
|
||||||
<div class="event-label">
|
<div class="event-label">
|
||||||
{{ $label }}
|
{{ $label }}
|
||||||
@ -2810,6 +2827,14 @@
|
|||||||
document.body.removeChild(ns);
|
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); }
|
} 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);
|
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); }
|
} catch(e){ console.warn('recSwapContent', e); }
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -3657,11 +3690,14 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// NOTE: emit these with the json Blade directive (raw output). Escaped echo
|
// These live on `window` so SPA navigation (recSwapContent) can update
|
||||||
// braces HTML-escape the quotes (videoId becomes an entity-encoded string) and
|
// them for the newly-loaded match without a page refresh. Downstream
|
||||||
// crash this whole block — which loads match data, switches tabs, drives CRUD.
|
// code references `videoId` and `isOwner` as free identifiers, which
|
||||||
const videoId = @json(isset($video) ? $video->getRouteKey() : '');
|
// resolve to window.videoId / window.isOwner via the global scope.
|
||||||
const isOwner = @json(Auth::check() && isset($video) && Auth::id() === $video->user_id);
|
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() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
loadMatchData();
|
loadMatchData();
|
||||||
@ -3903,19 +3939,28 @@
|
|||||||
initSidebarHeightSync();
|
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 tabButtons = document.querySelectorAll('.tab-button');
|
||||||
const tabPanels = document.querySelectorAll('.tab-panel');
|
const tabPanels = document.querySelectorAll('.tab-panel');
|
||||||
tabButtons.forEach(button => {
|
tabButtons.forEach(button => {
|
||||||
|
if (button._tabBound) return;
|
||||||
|
button._tabBound = true;
|
||||||
button.addEventListener('click', function() {
|
button.addEventListener('click', function() {
|
||||||
const targetTab = this.getAttribute('data-tab');
|
const targetTab = this.getAttribute('data-tab');
|
||||||
tabButtons.forEach(btn => btn.classList.remove('active'));
|
// Re-query on every click — the sidebar may have been re-rendered
|
||||||
tabPanels.forEach(panel => panel.classList.remove('active'));
|
document.querySelectorAll('.tab-button').forEach(btn => btn.classList.remove('active'));
|
||||||
|
document.querySelectorAll('.tab-panel').forEach(panel => panel.classList.remove('active'));
|
||||||
this.classList.add('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();
|
attachEventListeners();
|
||||||
|
};
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
window.initMatchTabsAndEvents();
|
||||||
document.getElementById('addRoundForm')?.addEventListener('submit', function(e) {
|
document.getElementById('addRoundForm')?.addEventListener('submit', function(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
storeRound(
|
storeRound(
|
||||||
@ -3925,14 +3970,31 @@
|
|||||||
document.getElementById('roundStartTime').value
|
document.getElementById('roundStartTime').value
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
// pointForm removed — inline capture strip handles submit
|
|
||||||
document.getElementById('editRoundForm')?.addEventListener('submit', function(e) {
|
document.getElementById('editRoundForm')?.addEventListener('submit', function(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
updateRound();
|
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 reviewPlaybackStopHandler = null;
|
||||||
let reviewPlaybackEndTime = null;
|
let reviewPlaybackEndTime = null;
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user