import { RefObject, useEffect } from 'react'; import { buildMomentumUrl } from '../football/opta-match-stats/api'; const LIVE_PERIODS = new Set([ 'FirstHalf', 'SecondHalf', 'ExtraFirstHalf', 'ExtraSecondHalf' ]); const POLL_INTERVAL_MS = 30000; type MinuteCache = Map; const getMatchMinuteFromFeed = (feed: any): number | undefined => { const liveData = feed && feed.liveData; const matchDetails = liveData && liveData.matchDetails; if (!matchDetails) { return undefined; } return matchDetails.matchTime != null ? matchDetails.matchTime : matchDetails.matchLengthMin; }; const getLiveFixtures = ( container: HTMLElement ): Array<{ el: Element; matchId: string }> => { const results: Array<{ el: Element; matchId: string }> = []; const fixtures = container.querySelectorAll('.Opta-fixture'); fixtures.forEach(el => { const period = el.getAttribute('data-period'); const matchId = el.getAttribute('data-match'); if (period && LIVE_PERIODS.has(period) && matchId) { results.push({ el, matchId }); } }); return results; }; const applyMinutesToDom = (container: HTMLElement, cache: MinuteCache) => { const liveFixtures = getLiveFixtures(container); liveFixtures.forEach(({ el, matchId }) => { const minute = cache.get(matchId); if (minute === undefined) { return; } const timeEl = el.querySelector('.Opta-Time'); if (!timeEl) { return; } const minuteText = `${minute}'`; // Check if already showing the correct minute const abbrEl = timeEl.querySelector('abbr'); if (abbrEl) { if (abbrEl.textContent !== minuteText) { abbrEl.textContent = minuteText; abbrEl.setAttribute('title', `${minute} minutes`); } } else if ((timeEl.textContent || '').trim() !== minuteText) { timeEl.textContent = minuteText; } }); }; const fetchMinutesForMatches = async ( matchIds: string[], apiBaseUrl: string, cache: MinuteCache, abortSignal: AbortSignal ): Promise => { const fetches = matchIds.map(async matchId => { try { const url = buildMomentumUrl(apiBaseUrl, matchId); const response = await fetch(url, { signal: abortSignal }); if (!response.ok) { return; } const feed = await response.json(); const minute = getMatchMinuteFromFeed(feed); if (minute !== undefined) { cache.set(matchId, minute); } } catch { // Silently ignore fetch errors (aborted, network, etc.) } }); await Promise.all(fetches); }; const setupLiveMinuteUpdater = ( ref: RefObject, apiBaseUrl: string ): (() => void) => { const cache: MinuteCache = new Map(); const abortController = new AbortController(); let observer: MutationObserver | null = null; let pollTimer: ReturnType | null = null; let initInterval: ReturnType | null = null; const updateMinutes = async () => { if (!ref.current) { return; } const liveFixtures = getLiveFixtures(ref.current); if (liveFixtures.length === 0) { // No live fixtures remaining — stop polling and disconnect observer if (pollTimer) { clearInterval(pollTimer); pollTimer = null; } if (observer) { observer.disconnect(); observer = null; } return; } const matchIds = [...new Set(liveFixtures.map(f => f.matchId))]; await fetchMinutesForMatches( matchIds, apiBaseUrl, cache, abortController.signal ); if (ref.current) { applyMinutesToDom(ref.current, cache); } }; const applyFromCache = () => { if (ref.current && cache.size > 0) { applyMinutesToDom(ref.current, cache); } }; // Poll for initial Opta widget load let initCount = 0; initInterval = setInterval(() => { if (initCount >= 25) { if (initInterval) { clearInterval(initInterval); } return; } initCount++; if (!ref.current) { return; } const liveFixtures = getLiveFixtures(ref.current); if (liveFixtures.length === 0) { return; } // Found live fixtures — start the updater if (initInterval) { clearInterval(initInterval); } // Initial fetch updateMinutes(); // Set up MutationObserver to re-apply cached minutes when Opta re-renders observer = new MutationObserver(() => { applyFromCache(); }); observer.observe(ref.current, { childList: true, subtree: true }); // Poll for fresh minutes every 30s pollTimer = setInterval(updateMinutes, POLL_INTERVAL_MS); }, 500); return () => { abortController.abort(); if (initInterval) { clearInterval(initInterval); } if (pollTimer) { clearInterval(pollTimer); } if (observer) { observer.disconnect(); } }; }; const API_BASE_URL = '/api/match-stats'; export const useLiveMinuteUpdater = (args: { ref: RefObject; }) => { const { ref } = args; useEffect( () => { return setupLiveMinuteUpdater(ref, API_BASE_URL); }, [ref] ); };