/* ========================================================= Match Intelligence UI - Phase 5.2 Tabs always render. Safe when fixture JSON is missing. ========================================================= */ let playerCache = {}; /* TRANSLATION HELPER */ function t(key){ return (window.DW_I18N && window.DW_I18N[key]) || key } document.addEventListener("DOMContentLoaded", async () => { // WAIT for translations while (!window.DW_I18N) { await new Promise(r => setTimeout(r, 50)) } // Load player image cache try { const res = await fetch("/data/player_media_cache.json?ts=" + Date.now()); if (res.ok) { playerCache = await res.json(); console.log("[CACHE] Loaded player media:", Object.keys(playerCache).length); } } catch (e) { console.warn("[CACHE] Failed to load player cache"); } const fixtureId = new URLSearchParams(location.search).get("matchId") if (!fixtureId) return const anchor = document.getElementById("match-container") if (!anchor) return const root = document.createElement("section") root.id = "match-intel-root" root.className = "match-intel" anchor.after(root) let fx = null try { const r = await fetch(`/data/fixture_details/fixture_${fixtureId}.json?ts=${Date.now()}`) if (r.ok) fx = (await r.json())?.response?.[0] || null } catch {} let h2hData = null try { const r2 = await fetch(`/data/h2h_data.json?ts=${Date.now()}`) if (r2.ok) { const all = await r2.json() h2hData = all[fixtureId] || null } } catch {} const index = fx ? indexPlayers(fx.players || []) : {} const subs = fx ? indexSubs(fx.events || []) : { out:{}, in:{} } root.innerHTML = `

${t("matchIntelligence")}

${fx ? fx.teams.home.name + " " + t("vs") + " " + fx.teams.away.name : t("unavailable")}

${fx ? renderMomentum(fx) : "

No momentum data available

"}
${fx ? "" : renderGenericLineup()}
${fx ? renderSummary(fx.events, fx.teams) : "

" + t("summaryUnavailable") + "

"}
${fx ? renderStats(fx.statistics) : "

" + t("statsUnavailable") + "

"}

Loading H2H...

` const topWidget = document.getElementById("intel-top-widget") if (topWidget) { topWidget.innerHTML = fx ? renderTopPolls(fx) : `
${t("pollsUnavailable")}
` } const track = topWidget?.querySelector(".polls-track") const dots = topWidget?.querySelectorAll(".poll-dots .dot") if (track && dots.length) { track.addEventListener("scroll", () => { const index = Math.round(track.scrollLeft / track.offsetWidth) dots.forEach((d, i) => { d.classList.toggle("active", i === index) }) }) } if (fx) { document.getElementById("intel-lineups").innerHTML = `
${renderFormation(fx.lineups[0], "home", index, subs)}
${renderFormation(fx.lineups[1], "away", index, subs)}
` document.getElementById("bench-panel").innerHTML = renderBench(fx.lineups[0], index, subs) bindBench(fx, index, subs) bindPlayerClicks(index, subs) } const h2hPanel = document.getElementById("intel-h2h") if (h2hPanel) { if (!h2hData || !Array.isArray(h2hData.matches) || h2hData.matches.length === 0) { h2hPanel.innerHTML = `

No H2H data

` } else { h2hPanel.innerHTML = renderH2H(h2hData) } } document.dispatchEvent(new Event("lang:changed")) /* =============================== LIVE SCORE POLLER (20s) ================================ */ let scorePollerId = null if (fx?.fixture?.status?.short && !["FT","AET","PEN","NS"].includes(fx.fixture.status.short)) { scorePollerId = setInterval(async () => { try { const r = await fetch(`/data/fixture_details/fixture_${fixtureId}.json?ts=${Date.now()}`) if (!r.ok) return const fresh = (await r.json())?.response?.[0] if (!fresh) return // Update score only const scoreEl = document.getElementById("live-score-display") if (scoreEl) { const hg = fresh.goals?.home ?? "–" const ag = fresh.goals?.away ?? "–" scoreEl.textContent = hg + " – " + ag } // Update live badge const badgeEl = document.querySelector("#intel-momentum .momentum-live-badge") if (badgeEl) { const st = fresh.fixture?.status?.short const el = fresh.fixture?.status?.elapsed || 0 const isFinished = ["FT","AET","PEN"].includes(st) const isLive = ["1H","2H","HT","ET","P"].includes(st) if (isFinished) { badgeEl.style.color = "#aaa" badgeEl.innerHTML = "⬛ Full Time" clearInterval(scorePollerId) scorePollerId = null } else if (isLive) { badgeEl.innerHTML = ` LIVE · ${el}'` } } } catch {} }, 20000) } /* =============================== LIVE FIXTURE POLLER ================================ */ let pollerId = null let lastEventCount = fx?.events?.length || 0 let lastStatus = fx?.fixture?.status?.short || null if (lastStatus && lastStatus !== "FT") { pollerId = setInterval(async () => { try { const r = await fetch(`/data/fixture_details/fixture_${fixtureId}.json?ts=${Date.now()}`) if (!r.ok) return const fresh = (await r.json())?.response?.[0] if (!fresh) return fx = fresh if (Array.isArray(fresh.events) && fresh.events.length > lastEventCount) { const newEvents = fresh.events.slice(lastEventCount) const timeline = document.querySelector(".summary-timeline") if (timeline) { newEvents.forEach(e => { timeline.insertAdjacentHTML("beforeend", renderEventRow(e, fresh.teams)) }) } lastEventCount = fresh.events.length } if (Array.isArray(fresh.players)) { const newIndex = indexPlayers(fresh.players) document.querySelectorAll(".player, .bench-player").forEach(el => { const d = newIndex[el.dataset.id] if (!d) return const rating = formatRating(d.stats?.games?.rating) const badge = el.querySelector(".badge, .b-rate") if (badge) { badge.textContent = rating badge.className = badge.className.replace(/rate-\w+/g, "") badge.classList.add(ratingClass(rating)) } }) } const statsPanel = document.getElementById("intel-stats") if (statsPanel && Array.isArray(fresh.statistics)) { statsPanel.innerHTML = renderStats(fresh.statistics) } const momPanel = document.getElementById("intel-momentum") if (momPanel) { momPanel.innerHTML = renderMomentum(fresh) } const newStatus = fresh.fixture?.status?.short if (newStatus === "FT") { clearInterval(pollerId) pollerId = null } lastStatus = newStatus } catch {} }, 120000) } /* ✅ CORRECT POSITION */ document.addEventListener("lang:changed", () => { if (!fx) return const statsPanel = document.getElementById("intel-stats") if (statsPanel && Array.isArray(fx.statistics)) { statsPanel.innerHTML = renderStats(fx.statistics) } }) }) /* =========================== GENERIC LINEUP (no fixture) =========================== */ function renderGenericLineup() { const ICON = "https://cdn-icons-png.flaticon.com/512/847/847969.png" const formation442 = [ { row: [{ x: 50, y: 12 }] }, { row: [{ x: 15, y: 32 }, { x: 38, y: 32 }, { x: 62, y: 32 }, { x: 85, y: 32 }] }, { row: [{ x: 15, y: 54 }, { x: 38, y: 54 }, { x: 62, y: 54 }, { x: 85, y: 54 }] }, { row: [{ x: 30, y: 74 }, { x: 70, y: 74 }] } ] let html = "
" html += "
" formation442.forEach(({ row }) => { row.forEach(({ x, y }) => { html += `
` }) }) html += "
" html += "
" formation442.forEach(({ row }) => { row.forEach(({ x, y }) => { html += `
` }) }) html += "
" html += "

" + t("lineupsUnavailable") + "

" return html } /* =========================== FORMATIONS & POSITIONS =========================== */ const FORMATIONS = { "4-4-2":[1,4,4,2],"4-3-3":[1,4,3,3],"4-2-3-1":[1,4,2,3,1], "4-3-1-2":[1,4,3,1,2],"4-1-4-1":[1,4,1,4,1], "3-5-2":[1,3,5,2],"5-3-2":[1,5,3,2], "5-4-1":[1,5,4,1],"3-4-3":[1,3,4,3] } const ROW_Y_HOME = [12,32,54,74,92] const ROW_Y_AWAY = [92,72,50,28,8] function renderFormation(team, side, index, subs) { if (!team?.startXI) return "" const layout = FORMATIONS[team.formation] || FORMATIONS["4-4-2"] let players = [...team.startXI] let html = "" let row = 0 layout.forEach(count => { const rowPlayers = players.splice(0, count) const y = side === "home" ? ROW_Y_HOME[row] : ROW_Y_AWAY[row] const xs = getSlots(count, side) rowPlayers.forEach((p,i)=>{ const d = index[p.player.id] || {} const s = d.stats || {} const rating = formatRating(s.games?.rating) const subOut = subs.out[p.player.id] const isCap = d.stats?.games?.captain === true html += `
${rating} ${isCap ? `©` : ""} ${s.cards?.yellow ? ``:""} ${s.cards?.red ? ``:""}
${s.goals?.total > 0 ? `⚽ ${s.goals.total}`:""} ${s.goals?.assists > 0 ? `🅰 ${s.goals.assists}`:""}
${subOut ? `⇄ ${subOut}'`:""} ${p.player.name.split(" ").pop()}
` }) row++ }) return html } function getSlots(c,s){ const b={1:[50],2:[30,70],3:[20,50,80],4:[15,38,62,85],5:[10,30,50,70,90]}[c]||[50] return s==="away"?b.map(x=>100-x):b } /* =========================== DATA & MODAL =========================== */ function indexPlayers(players){ const m={} players.forEach(t=>t.players.forEach(p=>{ m[p.player.id]={name:p.player.name,photo:p.player.photo,stats:p.statistics?.[0]||{}} })) return m } function indexSubs(events){ const o={},i={} events.forEach(e=>{ if(e.type==="subst"&&e.time?.elapsed){ if(e.player?.id)o[e.player.id]=e.time.elapsed if(e.assist?.id)i[e.assist.id]=e.time.elapsed } }) return{out:o,in:i} } function bindBench(fx,index,subs){ document.querySelectorAll(".bench-btn").forEach(btn=>{ btn.onclick=()=>{ document.querySelectorAll(".bench-btn").forEach(b=>b.classList.remove("active")) btn.classList.add("active") const t=btn.dataset.side==="home"?fx.lineups[0]:fx.lineups[1] document.getElementById("bench-panel").innerHTML=renderBench(t,index,subs) bindPlayerClicks(index,subs) } }) } function renderBench(team,index,subs){ if(!team?.substitutes)return"" return`
${team.substitutes.map(p=>{ const d=index[p.player.id]||{},r=formatRating(d.stats?.games?.rating),i=subs.in[p.player.id] return`
${r} ${i?`⇄ ${i}'`:""} ${p.player.name.split(" ").pop()}
` }).join("")}
` } function bindPlayerClicks(index,subs){ document.querySelectorAll(".player,.bench-player").forEach(el=>{ el.onclick=()=>showPlayer(el.dataset.id,index,subs) }) } function showPlayer(id,index,subs){ const modal=document.getElementById("player-modal") const p=index[id]; if(!p)return const s=p.stats||{} const sub=subs.out[id]||subs.in[id] const pass=calcPassAccuracy(s.passes?.total,s.passes?.accuracy) const duelPct=(s.duels?.total&&s.duels?.won)?Math.round((s.duels.won/s.duels.total)*100)+"%":"–" const dribblePct=(s.dribbles?.attempts&&s.dribbles?.success)?Math.round((s.dribbles.success/s.dribbles.attempts)*100)+"%":"–" const isCaptain=s.games?.captain===true modal.innerHTML=` ` modal.classList.add("show") modal.onclick=()=>modal.classList.remove("show") modal.querySelector(".modal-close").onclick=()=>modal.classList.remove("show") } function formatRating(r){return r==null?"–":parseFloat(r).toFixed(1)}function calcPassAccuracy(t,c){return!t||!c?"–":Math.round((c/t)*100)+"%"} function ratingClass(r){ const n=parseFloat(r) if(isNaN(n))return"rate-na" if(n<6)return"rate-red" if(n<7)return"rate-yellow" if(n<8)return"rate-green" if(n<9)return"rate-lightblue" return"rate-darkblue" } function fallback(){ return`

${t("lineups")}

${t("unavailable")}

` } /* =============================== POLL INTERACTION ================================ */ document.addEventListener("click", e => { const option = e.target.closest(".poll-option") const reset = e.target.closest(".poll-reset") // SELECT OPTION if (option) { const card = option.closest(".poll-card") if (!card || card.classList.contains("locked")) return // remove previous card.querySelectorAll(".poll-option").forEach(o => { o.classList.remove("selected") }) // select new option.classList.add("selected") // lock card card.classList.add("locked") return } // RESET if (reset) { const card = reset.closest(".poll-card") if (!card) return reset.blur() // 👈 ADD THIS LINE HERE card.classList.remove("locked") card.querySelectorAll(".poll-option").forEach(o => { o.classList.remove("selected") }) } }) /* =========================== TAB SWITCHING =========================== */ document.addEventListener("click", e => { const btn = e.target.closest(".intel-tab") if (!btn) return const tab = btn.dataset.tab document.querySelectorAll(".intel-tab").forEach(b => b.classList.toggle("active", b === btn) ) document.querySelectorAll(".intel-panel").forEach(p => p.classList.toggle("active", p.id === "intel-" + tab) ) }) /* =============================== MATCH SUMMARY ================================ */ function renderSummary(events, teams) { if (!Array.isArray(events) || events.length === 0) { return `

${t("noEvents")}

` } let htInserted = false const rows = events.map(e => { let divider = "" if (!htInserted && e.time?.elapsed > 45) { htInserted = true divider = `
— Half Time —
` } return divider + renderEventRow(e, teams) }).join("") return `
${rows}
` } function renderEventRow(e, teams) { const minute = formatMinute(e.time) const side = e.team?.id === teams.home.id ? "home" : e.team?.id === teams.away.id ? "away" : "neutral" return `
${minute}
${eventIcon(e)} ${eventText(e)}
` } function formatMinute(time) { if (!time?.elapsed) return "–" if (time.extra) return `${time.elapsed}+${time.extra}'` return `${time.elapsed}'` } function eventIcon(e) { if (e.type === "Goal") return `` if (e.type === "Card" && e.detail === "Yellow Card") return `🟨` if (e.type === "Card" && e.detail === "Red Card") return `🟥` if (e.type === "subst") return `` if (e.type === "Var") return `` return `` } function eventText(e) { const player = e.player?.name || t("unknown") const assist = e.assist?.name const comment = e.comments ? `, ${e.comments}` : "" if (e.type === "Goal") return assist ? `${player}, ${t("assist")} ${assist}` : player if (e.type === "Card") return `${player}${comment}` if (e.type === "subst") return assist ? `${assist} ⇢ ${player}` : player if (e.type === "Var") return `VAR: ${e.detail || "Review"}${e.player?.name ? " — " + e.player.name : ""}` return player } /* =============================== MATCH STATS ================================ */ function renderStats(stats) { if (!Array.isArray(stats) || stats.length !== 2) { return `

${t("statsUnavailable")}

` } const home = normalizeStats(stats[0].statistics) const away = normalizeStats(stats[1].statistics) const keys = Object.keys(home) return `
${keys.map(k => renderStatRow(k, home[k], away[k])).join("")}
` } function normalizeStats(list) { const out = {} list.forEach(s => out[s.type] = normalizeValue(s.value)) return out } function normalizeValue(v) { if (v === null || v === undefined) return "–" if (typeof v === "number") return v if (typeof v === "string") return v return "–" } function renderStatRow(label, home, away) { const h = parseStatValue(home) const a = parseStatValue(away) const max = Math.max(h.value, a.value, 1) const hPct = Math.round((h.value / max) * 100) const aPct = Math.round((a.value / max) * 100) const isXG = label.toLowerCase().includes("expected") return `
${home}
${{ "Shots on Goal": "Shots on Target", "Shots off Goal": "Shots off Target", "Total Shots": "Total Shots", "Blocked Shots": "Blocked", "Shots insidebox": "Shots (Inside Box)", "Shots outsidebox": "Shots (Outside Box)", "Fouls": "Fouls", "Corner Kicks": "Corners", "Offsides": "Offsides", "Ball Possession": "Possession", "Yellow Cards": "Yellow Cards", "Red Cards": "Red Cards", "Goalkeeper Saves": "GK Saves", "Total passes": "Total Passes", "Passes accurate": "Accurate Passes", "Passes %": "Pass Accuracy", "expected_goals": "xG (Expected Goals)", "goals_prevented": "Goals Prevented" }[label] || label}
${away}
` } function parseStatValue(v) { if (v === "–") return { value: 0 } if (typeof v === "number") return { value: v } if (typeof v === "string" && v.endsWith("%")) return { value: parseFloat(v) } const n = parseFloat(v) return { value: isNaN(n) ? 0 : n } } /* =============================== MATCH MOMENTUM CHART ================================ */ function renderMomentum(fx) { if (!fx || !Array.isArray(fx.events) || !fx.teams) { return `

No momentum data available

` } const homeId = fx.teams.home.id const awayId = fx.teams.away.id const homeName = fx.teams.home.name const awayName = fx.teams.away.name const homeLogo = fx.teams.home.logo const awayLogo = fx.teams.away.logo const homeColour = fx.lineups?.[0]?.team?.colors?.player?.primary ? "#" + fx.lineups[0].team.colors.player.primary : "#e41e2c" const awayColour = fx.lineups?.[1]?.team?.colors?.player?.primary ? "#" + fx.lineups[1].team.colors.player.primary : "#1532c1" const status = fx.fixture?.status?.short || "NS" const elapsed = fx.fixture?.status?.elapsed || 0 const isLive = ["1H","2H","HT","ET","P"].includes(status) const isFinished = ["FT","AET","PEN"].includes(status) const homeGoals = fx.goals?.home ?? "–" const awayGoals = fx.goals?.away ?? "–" // ── Baseline from statistics ────────────────────────────── const stats = Array.isArray(fx.statistics) && fx.statistics.length === 2 ? fx.statistics : null function getStat(teamStats, label) { const entry = teamStats.statistics?.find(s => s.type === label) const v = entry?.value if (v === null || v === undefined) return 0 if (typeof v === "string" && v.endsWith("%")) return parseFloat(v) return parseFloat(v) || 0 } const BUCKET = 5 const NUM_BUCKETS = 19 const homeBuckets = new Array(NUM_BUCKETS).fill(0) const awayBuckets = new Array(NUM_BUCKETS).fill(0) // ── Step 1: place event spikes first ──────────────────────── const WEIGHTS = { "Goal": 6, "Card": 1, "Var": 2, "subst": 0.5 } const goalEvents = [] let homeScore = 0, awayScore = 0 const eventBuckets = new Set() fx.events.forEach(e => { const min = (e.time?.elapsed || 0) + (e.time?.extra || 0) * 0.1 const idx = Math.min(Math.floor(min / BUCKET), NUM_BUCKETS - 1) const w = WEIGHTS[e.type] || 0.3 const isHome = e.team?.id === homeId const isAway = e.team?.id === awayId if (isHome) homeBuckets[idx] += w if (isAway) awayBuckets[idx] += w eventBuckets.add(idx) if (e.type === "Goal") { if (isHome) homeScore++ if (isAway) awayScore++ goalEvents.push({ minute: Math.round(min), side: isHome ? "home" : "away", score: homeScore + "-" + awayScore, player: e.player?.name || "" }) } }) // ── Step 2: baseline ONLY on quiet buckets (no events) ─────── if (stats) { const hPoss = getStat(stats[0], "Ball Possession") const aPoss = getStat(stats[1], "Ball Possession") const hShots = getStat(stats[0], "Shots on Goal") const aShots = getStat(stats[1], "Shots on Goal") const hCorner = getStat(stats[0], "Corner Kicks") const aCorner = getStat(stats[1], "Corner Kicks") const hRaw = (hPoss * 0.5) + (hShots * 3) + (hCorner * 2) const aRaw = (aPoss * 0.5) + (aShots * 3) + (aCorner * 2) const total = hRaw + aRaw || 1 // Gentle baseline max 12 — only fills quiet periods const hBase = (hRaw / total) * 12 const aBase = (aRaw / total) * 12 for (let i = 0; i < NUM_BUCKETS; i++) { if (eventBuckets.has(i)) continue // skip buckets that already have events const variance = () => 0.60 + Math.random() * 0.80 // wide variance for natural look homeBuckets[i] += hBase * variance() awayBuckets[i] += aBase * variance() } } // Normalize const allVals = [...homeBuckets, ...awayBuckets] const maxVal = Math.max(...allVals, 1) const norm = v => Math.round((v / maxVal) * 100) const W_SVG = 340, H_SVG = 160, CHART_H = 70 const BAR_W = Math.floor(W_SVG / NUM_BUCKETS) - 1 const MID_Y = H_SVG / 2 let svgBars = "" for (let i = 0; i < NUM_BUCKETS; i++) { const x = i * (BAR_W + 1) + 2 const hH = Math.round((norm(homeBuckets[i]) / 100) * CHART_H) const aH = Math.round((norm(awayBuckets[i]) / 100) * CHART_H) if (hH > 0) svgBars += `` if (aH > 0) svgBars += `` } let goalMarkers = "" goalEvents.forEach(g => { const idx = Math.min(Math.floor(g.minute / BUCKET), NUM_BUCKETS - 1) const x = idx * (BAR_W + 1) + 2 + BAR_W / 2 goalMarkers += ` ` }) const htX = Math.round((45 / (BUCKET * NUM_BUCKETS)) * W_SVG) const liveX = isLive ? Math.round((elapsed / 90) * W_SVG) : 0 const svgChart = ` ${svgBars} ${goalMarkers} HT ${isLive ? `` : ""} 0' 45' 90' ` // Status badge let statusBadge = "" if (isFinished) { statusBadge = `
⬛ Full Time
` } else if (isLive) { statusBadge = `
LIVE · ${elapsed}'
` } // Score strip const scoreStrip = `
${homeName}
${homeGoals} – ${awayGoals}
${awayName}
` const goalLog = goalEvents.length > 0 ? `
${goalEvents.map(g => `
${g.minute}' ${g.player} ${g.score}
`).join("")}
` : "" return `
${scoreStrip}
${homeName} Match Momentum ${awayName}
${svgChart}
${statusBadge} ${goalLog}
` } // REPLACED_renderMomentum_end function renderH2H(h2h) { return `
${h2h.matches.map(m => { const result = m.home_goals > m.away_goals ? "home-win" : m.away_goals > m.home_goals ? "away-win" : "draw" return `
${m.date} ${m.league}
${m.home}
${m.home_goals} - ${m.away_goals}
${m.away}
` }).join("")}
` } function getDefaultTab(fx) { if (!fx || !fx.fixture) return "h2h" const status = fx.fixture.status?.short const matchTime = new Date(fx.fixture.date).getTime() const now = Date.now() const oneHour = 60 * 60 * 1000 // Live match → momentum if (["1H","2H","HT","ET","P"].includes(status)) return "momentum" // After match → lineups if (status === "FT") return "lineups" // 1 hour before kickoff → lineups if (matchTime - now <= oneHour) return "lineups" // Before that → H2H return "h2h" } function renderTopPolls(fx) { const home = fx.teams.home const away = fx.teams.away return `
${t("who_supporting")}
${home.name}
${away.name}
${t("who_will_win")}
${home.name}
${away.name}
${t("both_teams_score")}
YES
NO
` }