1/** 2 * WK 2026 Wedstrijd Slider â [wk_slider] 3 * 4 * Haalt live data op van de VGN API, toont de beste odds 5 * per uitkomst (1, X, 2) over ALLE beschikbare bookmakers. 6 * Onbekende bookmakers worden automatisch ondersteund. 7 * 8 * Config wordt doorgegeven via wp_localize_script â wkSliderConfig 9 */ 10 11(function () { 12 13 /* âââââââââââââââââââââââââââââââââââââââââââ 14 CONFIGURATIE (vanuit PHP) 15 âââââââââââââââââââââââââââââââââââââââââââ */ 16 17 var cfg = window.wkSliderConfig || {}; 18 19 var API_BASE = cfg.apiBase || 'https://vgn.vg/api/data.php'; 20 var API_KEY = cfg.apiKey || ''; 21 var FLAG_BASE = cfg.flagBase || '/media/ico/vlag/hor/'; 22 var SHIRT_BASE = cfg.shirtBase || '/media/shirts/'; 23 var LOGO_BASE = cfg.logoBase || '/media/logo/'; 24 var WK_LOGO = cfg.wkLogo || '/media/logo/wk-2026-logo-2.svg'; 25 var MAX_SLIDES = cfg.maxSlides || 12; 26 var THEME = cfg.theme || 'light'; 27 var IS_DARK = THEME === 'dark'; 28 29 /* Zoek het root element â ondersteunt meerdere instanties op dezelfde pagina */ 30 var root = document.querySelector('.wk2026-vgn-slider-root'); 31 if (!root) return; 32 33 /* âââââââââââââââââââââââââââââââââââââââââââ 34 DARK MODE: kleur helpers 35 Zorgt dat teamkleuren leesbaar zijn op 36 donkere achtergronden door te donkere 37 kleuren lichter te maken. 38 âââââââââââââââââââââââââââââââââââââââââââ */ 39 40 function hexToHSL(hex) { 41 hex = hex.replace('#', ''); 42 var r = parseInt(hex.substring(0, 2), 16) / 255; 43 var g = parseInt(hex.substring(2, 4), 16) / 255;
44 var b = parseInt(hex.substring(4, 6), 16) / 255; 45 var max = Math.max(r, g, b), min = Math.min(r, g, b); 46 var h, s, l = (max + min) / 2; 47 if (max === min) { h = s = 0; } 48 else { 49 var d = max - min; 50 s = l > 0.5 ? d / (2 - max - min) : d / (max + min); 51 switch (max) { 52 case r: h = ((g - b) / d + (g < b ? 6 : 0)) / 6; break; 53 case g: h = ((b - r) / d + 2) / 6; break; 54 case b: h = ((r - g) / d + 4) / 6; break; 55 } 56 } 57 return { h: Math.round(h * 360), s: Math.round(s * 100), l: Math.round(l * 100) }; 58 } 59 60 function darkTeamColor(hex) { 61 if (!IS_DARK) return hex; 62 var hsl = hexToHSL(hex); 63 /* Als lightness < 45%, maak het lichter zodat het op donker leesbaar is */ 64 if (hsl.l < 45) { 65 hsl.l = Math.max(55, Math.min(75, hsl.l + 30)); 66 hsl.s = Math.min(90, hsl.s + 10); 67 } 68 return 'hsl(' + hsl.h + ',' + hsl.s + '%,' + hsl.l + '%)'; 69 } 70 71 /* âââââââââââââââââââââââââââââââââââââââââââ 72 TEAM DATABASE 73 Mapt API-teamnaam â slug, afkorting, vlagcode, kleur 74 Onbekende teams krijgen automatisch een fallback 75 âââââââââââââââââââââââââââââââââââââââââââ */ 76 77 var TEAMS = { 78 'Nederland': { slug: 'nederland', abbrev: 'NED', flagCode: 'nl', color: '#FF6600' }, 79 'Duitsland': { slug: 'duitsland', abbrev: 'DUI', flagCode: 'de', color: '#1a1a1a' }, 80 'Brazilië': { slug: 'brazilie', abbrev: 'BRA', flagCode: 'br', color: '#009C3B' }, 81 'Frankrijk': { slug: 'frankrijk', abbrev: 'FRA', flagCode: 'fr', color: '#002395' }, 82 'Engeland': { slug: 'engeland', abbrev: 'ENG', flagCode: 'gb-eng', color: '#C8102E' }, 83 'Argentinië': { slug: 'argentinie', abbrev: 'ARG', flagCode: 'ar', color: '#74ACDF', darkColor: '#1A8FD1' }, 84 'Spanje': { slug: 'spanje', abbrev: 'ESP', flagCode: 'es', color: '#AA151B' }, 85 'Portugal': { slug: 'portugal', abbrev: 'POR', flagCode: 'pt', color: '#006600' }, 86 'België': { slug: 'belgie', abbrev: 'BEL', flagCode: 'be', color: '#EF3340' }, 87 'Italië': { slug: 'italie', abbrev: 'ITA', flagCode: 'it', color: '#003399' }, 88 'Kroatië': { slug: 'kroatie', abbrev: 'KRO', flagCode: 'hr', color: '#FF0000' }, 89 'Marokko': { slug: 'marokko', abbrev: 'MAR', flagCode: 'ma', color: '#C1272D' }, 90 'Senegal': { slug: 'senegal', abbrev: 'SEN', flagCode: 'sn', color: '#00853F' }, 91 'Japan': { slug: 'japan', abbrev: 'JPN', flagCode: 'jp', color: '#BC002D' }, 92 'Mexico': { slug: 'mexico', abbrev: 'MEX', flagCode: 'mx', color: '#006847' }, 93 'Canada': { slug: 'canada', abbrev: 'CAN', flagCode: 'ca', color: '#FF0000' }, 94 'Verenigde Staten': { slug: 'verenigde-staten', abbrev: 'USA', flagCode: 'us', color: '#B22234', displayName: 'USA' }, 95 'Uruguay': { slug: 'uruguay', abbrev: 'URU', flagCode: 'uy', color: '#5AACDF', darkColor: '#1392E1' }, 96 'Colombia': { slug: 'colombia', abbrev: 'COL', flagCode: 'co', color: '#FCD116', darkColor: '#D4A800' }, 97 'Ecuador': { slug: 'ecuador', abbrev: 'ECU', flagCode: 'ec', color: '#FFD100', darkColor: '#D4A800' }, 98 'Ghana': { slug: 'ghana', abbrev: 'GHA', flagCode: 'gh', color: '#006B3F' }, 99 'Nigeria': { slug: 'nigeria', abbrev: 'NIG', flagCode: 'ng', color: '#008751' }, 100 'Ivoorkust': { slug: 'ivoorkust', abbrev: 'IVO', flagCode: 'ci', color: '#F77F00' }, 101 'Kameroen': { slug: 'kameroen', abbrev: 'KAM', flagCode: 'cm', color: '#007A5E' }, 102 'Saoedi-Arabië': { slug: 'saoedi-arabie', abbrev: 'KSA', flagCode: 'sa', color: '#006C35' }, 103 'Iran': { slug: 'iran', abbrev: 'IRN', flagCode: 'ir', color: '#239F40' }, 104 'Australië': { slug: 'australie', abbrev: 'AUS', flagCode: 'au', color: '#00008B' }, 105 'Zuid-Korea': { slug: 'zuid-korea', abbrev: 'KOR', flagCode: 'kr', color: '#CD2E3A' }, 106 'Zwitserland': { slug: 'zwitserland', abbrev: 'ZWI', flagCode: 'ch', color: '#F
106F0000' }, 107 'Oostenrijk': { slug: 'oostenrijk', abbrev: 'OOS', flagCode: 'at', color: '#ED2939' }, 108 'Denemarken': { slug: 'denemarken', abbrev: 'DEN', flagCode: 'dk', color: '#C60C30' }, 109 'Hongarije': { slug: 'hongarije', abbrev: 'HON', flagCode: 'hu', color: '#CE2939' }, 110 'Roemenië': { slug: 'roemenie', abbrev: 'ROE', flagCode: 'ro', color: '#002B7F' }, 111 'Oekraïne': { slug: 'oekraine', abbrev: 'OEK', flagCode: 'ua', color: '#005BBB' }, 112 'Schotland': { slug: 'schotland', abbrev: 'SCH', flagCode: 'gb-sct', color: '#003F87' }, 113 'Servië': { slug: 'servie', abbrev: 'SER', flagCode: 'rs', color: '#C6363C' }, 114 'Turkije': { slug: 'turkije', abbrev: 'TUR', flagCode: 'tr', color: '#E30A17' }, 115 'Venezuela': { slug: 'venezuela', abbrev: 'VEN', flagCode: 've', color: '#CF142B' }, 116 'Panama': { slug: 'panama', abbrev: 'PAN', flagCode: 'pa', color: '#DA121A' }, 117 'Honduras': { slug: 'honduras', abbrev: 'HON', flagCode: 'hn', color: '#0073CF' }, 118 'Zuid-Afrika': { slug: 'zuid-afrika', abbrev: 'ZAF', flagCode: 'za', color: '#007A4D' }, 119 'Paraguay': { slug: 'paraguay', abbrev: 'PAR', flagCode: 'py', color: '#D52B1E' }, 120 'Peru': { slug: 'peru', abbrev: 'PER', flagCode: 'pe', color: '#D91023' }, 121 'Chili': { slug: 'chili', abbrev: 'CHI', flagCode: 'cl', color: '#D52B1E' }, 122 'Bolivia': { slug: 'bolivia', abbrev: 'BOL', flagCode: 'bo', color: '#007934' }, 123 'Costa Rica': { slug: 'costa-rica', abbrev: 'CRC', flagCode: 'cr', color: '#002B7F' }, 124 'Jamaica': { slug: 'jamaica', abbrev: 'JAM', flagCode: 'jm', color: '#009B3A' }, 125 'Wales': { slug: 'wales', abbrev: 'WAL', flagCode: 'gb-wls', color: '#D4003A' }, 126 'Noorwegen': { slug: 'noorwegen', abbrev: 'NOR', flagCode: 'no', color: '#BA0C2F' }, 127 'Zweden': { slug: 'zweden', abbrev: 'ZWE', flagCode: 'se', color: '#004B87' }, 128 'Polen': { slug: 'polen', abbrev: 'POL', flagCode: 'pl', color: '#DC143C' }, 129 'Tsjechië': { slug: 'tsjechie', abbrev: 'TSJ', flagCode: 'cz', color: '#11457E' }, 130 'Griekenland': { slug: 'griekenland', abbrev: 'GRI', flagCode: 'gr', color: '#001489' }, 131 'Slovenië': { slug: 'slovenie', abbrev: 'SLO', flagCode: 'si', color: '#003DA5' }, 132 'Slowakije': { slug: 'slowakije', abbrev: 'SVK', flagCode: 'sk', color: '#0B4EA2' }, 133 'Albanië': { slug: 'albanie', abbrev: 'ALB', flagCode: 'al', color: '#E41E20' }, 134 'Georgië': { slug: 'georgie', abbrev: 'GEO', flagCode: 'ge', color: '#DA291C' }, 135 'Tunesië': { slug: 'tunesie', abbrev: 'TUN', flagCode: 'tn', color: '#E70013' }, 136 'Egypte': { slug: 'egypte', abbrev: 'EGY', flagCode: 'eg', color: '#C8102E' }, 137 'Algerije': { slug: 'algerije', abbrev: 'ALG', flagCode: 'dz', color: '#006233' }, 138 'Nieuw-Zeeland': { slug: 'nieuw-zeeland', abbrev: 'NZL', flagCode: 'nz', color: '#00247D' }, 139 'Qatar': { slug: 'qatar', abbrev: 'QAT', flagCode: 'qa', color: '#8D1B3D' }, 140 'Oezbekistan': { slug: 'oezbekistan', abbrev: 'OEZ', flagCode: 'uz', color: '#1EB53A' }, 141 'Indonesië': { slug: 'indonesie', abbrev: 'IDN', flagCode: 'id', color: '#CE1126' }, 142 'Bahrein': { slug: 'bahrein', abbrev: 'BHR', flagCode: 'bh', color: '#CE1126' }, 143 'Haïti': { slug: 'haiti', abbrev: 'HAÃ', flagCode: 'ht', color: '#00209F' }, 144 'Trinidad en Tobago': { slug: 'trinidad-en-tobago', abbrev: 'TRI', flagCode: 'tt', color: '#CE1126' }, 145 'Curaçao': { slug: 'curacao', abbrev: 'CUR', flagCode: 'cw', color: '#002B7F' }, 146 'El Salvador': { slug: 'el-salvador', abbrev: 'SLV', flagCode: 'sv', color: '#0F47AF' }, 147 'Guatemala': { slug: 'guatemala', abbrev: 'GUA', flagCode: 'gt', color: '#4997D0', darkColor: '#1A7BBD' }, 148 'Cuba': { slug: 'cuba', abbrev: 'CUB', flagCode: 'cu', color: '#002A8F' }, 149 'Suriname': { slug: 'suriname', abbrev: 'SUR', flagCode: 'sr', color: '#377E3F' }, 150 'Fiji': { slug: 'fiji', abbrev: 'FIJ', flagCode: 'fj', color: '#002868' }, 151 'Irak': { slug: 'irak', abbrev: 'IRQ', flagCode: 'iq', color: '#CE1126' }, 152 'Jordanië': { slug: 'jordanie', abbrev: 'JOR', flagCode: 'jo', color: '#007A3D' }, 153 'China': { slug: 'china', abbrev: 'CHN', flagCode: 'cn', color: '#DE2910' }, 154 'Noord-Korea': { slug: 'noord-korea', abbrev: 'PRK', flagCode: 'kp', color: '#ED1C27' }, 155 'Palestina': { slug: 'palestina', abbrev: 'PLE', flagCode: 'ps', color: '#007A3D' }, 156 'India': { slug: 'india', abbrev: 'IND', flagCode: 'in', color: '#F
156F9933', darkColor: '#E07A00' }, 157 'Kenia': { slug: 'kenia', abbrev: 'KEN', flagCode: 'ke', color: '#006600' }, 158 'Tanzania': { slug: 'tanzania', abbrev: 'TAN', flagCode: 'tz', color: '#1EB53A' }, 159 'Oeganda': { slug: 'oeganda', abbrev: 'OEG', flagCode: 'ug', color: '#FCDC04', darkColor: '#D4A800' }, 160 'Mali': { slug: 'mali', abbrev: 'MLI', flagCode: 'ml', color: '#009A00' }, 161 'Mozambique': { slug: 'mozambique', abbrev: 'MOZ', flagCode: 'mz', color: '#009A44' }, 162 'Congo-Brazzaville': { slug: 'congo-brazzaville', abbrev: 'CGO', flagCode: 'cg', color: '#009543' }, 163 'DR Congo': { slug: 'dr-congo', abbrev: 'COD', flagCode: 'cd', color: '#007FFF', darkColor: '#0060CC' }, 164 'Burkina Faso': { slug: 'burkina-faso', abbrev: 'BFA', flagCode: 'bf', color: '#009E49' }, 165 'Kaapverdië': { slug: 'kaapverdie', abbrev: 'CPV', flagCode: 'cv', color: '#003893' }, 166 'Benin': { slug: 'benin', abbrev: 'BEN', flagCode: 'bj', color: '#FCD116', darkColor: '#D4A800' }, 167 'Zambia': { slug: 'zambia', abbrev: 'ZAM', flagCode: 'zm', color: '#198A00' }, 168 'Zimbabwe': { slug: 'zimbabwe', abbrev: 'ZIM', flagCode: 'zw', color: '#319208' }, 169 'Gabon': { slug: 'gabon', abbrev: 'GAB', flagCode: 'ga', color: '#009E60' }, 170 'Guinee': { slug: 'guinee', abbrev: 'GUI', flagCode: 'gn', color: '#CE1126' }, 171 'Comoren': { slug: 'comoren', abbrev: 'COM', flagCode: 'km', color: '#3A75C4', darkColor: '#1A5AAF' }, 172 'Namibië': { slug: 'namibie', abbrev: 'NAM', flagCode: 'na', color: '#003580' }, 173 'Botswana': { slug: 'botswana', abbrev: 'BOT', flagCode: 'bw', color: '#75AADB', darkColor: '#1A7BBD' }, 174 'Equatoriaal-Guinea': { slug: 'equatoriaal-guinea', abbrev: 'EQG', flagCode: 'gq', color: '#3E9A00' }, 175 'Centraal-Afrikaanse Republiek': { slug: 'centraal-afrikaanse-republiek', abbrev: 'CTA', flagCode: 'cf', color: '#003082' }, 176 'Libië': { slug: 'libie', abbrev: 'LBY', flagCode: 'ly', color: '#239E46' }, 177 'Soedan': { slug: 'soedan', abbrev: 'SDN', flagCode: 'sd', color: '#007229' }, 178 'Angola': { slug: 'angola', abbrev: 'ANG', flagCode: 'ao', color: '#CE1126' }, 179 'Bosnië en Herzegovina': { slug: 'bosnie-herzegovina', abbrev: 'BOS', flagCode: 'ba', color: '#002395', displayName: 'Bosnië & H.' }, 180 'Bosnië-Herzegovina': { slug: 'bosnie-herzegovina', abbrev: 'BOS', flagCode: 'ba', color: '#002395', displayName: 'Bosnië & H.' } 181 }; 182 183 function getTeamData(name) { 184 if (TEAMS[name]) return TEAMS[name]; 185 var slug = name.toLowerCase() 186 .replace(/[ëé]/g, 'e').replace(/[ïÃ]/g, 'i').replace(/[üú]/g, 'u') 187 .replace(/[öó]/g, 'o').replace(/[äá]/g, 'a') 188 .replace(/\s+/g, '-').replace(/[^a-z0-9-]/g, ''); 189 return { 190 slug: slug, 191 abbrev: name.substring(0, 3).toUpperCase(), 192 flagCode: slug.substring(0, 2), 193 color: '#EE6301' 194 }; 195 } 196 197 /* âââââââââââââââââââââââââââââââââââââââââââ 198 BOOKMAKER DATABASE 199 Bekende bookmakers met brandkleuren. 200 Onbekende bookmakers krijgen automatisch 201 een fallback â schaalt met elke nieuwe 202 bookmaker die de API teruggeeft. 203 âââââââââââââââââââââââââââââââââââââââââââ */ 204 205 var BM_DB = { 206 'BetMGM': { slug: 'betmgm', bg: '#0a0a0a', label: 'BetMGM' }, 207 'LeoVegas': { slug: 'leovegas', bg: '#ff7e40', label: 'LeoVegas' }, 208 'Toto': { slug: 'toto', bg: '#249448', label: 'Toto' }, 209 'bet365': { slug: 'bet365', bg: '#005440', label: 'bet365' }, 210 'Bet365': { slug: 'bet365', bg: '#005440', label: 'Bet365' }, 211 'Unibet': { slug: 'unibet', bg: '#000000', label: 'Unibet' }, 212 "Jack's": { slug: 'jacks', bg: '#dc0812', label: "Jack's" }, 213 'Jacks': { slug: 'jacks', bg: '#dc0812', label: "Jack's" }, 214 '711': { slug: '711', bg: '#211b1b', label: '711' }, 215 'Circus': { slug: 'circus', bg: '#e02425', label: 'Circus' }, 216 'OneCasino': { slug: 'onecasino', bg: '#2a1a71', label: 'OneCasino' }, 217 'TonyBet': { slug: 'tonybet', bg: '#481f8e', label: 'TonyBet' }, 218 '888sport': { slug: '888sport', bg: '#f35e00', label: '888sport' }, 219 '888Sport': { slug: '888sport', bg: '#f35e00', label: '888Sport' } 220 }; 221 222 function getBmMeta(name) { 223 if (BM_DB[name]) return BM_DB[name]; 224 var slug = name.toLowerCase().replace(/[^a-z0-9]/g, ''); 225 return { slug: slug, bg: '#1a1a2e', label: name }; 226 } 227 228 /* âââââââââââââââââââââââââââââââââââââââââââ 229 HELPER FUNCTIES 230 âââââââââââââââââââââââââââââââââââââââââââ */ 231 232 /* Hex â rgba string */ 233 function hexToRgba(hex, alpha) { 234 var r = parseInt(hex.slice(1, 3), 16); 235 var g = parseInt(hex.slice(3, 5), 16); 236 var b = parseInt(hex.slice(5, 7), 16); 237 return 'rgba(' + r + ', ' + g + ', ' + b + ', ' + alpha + ')'; 238 } 239 240 function chevron(clr) { 241 return '<svg viewBox="0 0 12 12" fill="none" stroke="' + clr + '" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round">' + 242 '<path d="M2.5 4.5L6 8L9.5 4.5"/></svg>'; 243 } 244 245 var MAANDEN = ['Jan','Feb','Mrt','Apr','Mei','Jun','Jul','Aug','Sep','Okt','N
245ov','Dec']; 246 function formatDateTime(datum, tijd) { 247 var parts = datum.split('-'); 248 var dag = parseInt(parts[2], 10); 249 var maand = MAANDEN[parseInt(parts[1], 10) - 1] || parts[1]; 250 return dag + ' ' + maand + ' ' + tijd; 251 } 252 253 function statusBadgeClass(status) { 254 if (status === 'Live') return 'wk2026-vgn-slider-badge wk2026-vgn-slider-badge--live'; 255 return 'wk2026-vgn-slider-badge'; 256 } 257 258 /* âââââââââââââââââââââââââââââââââââââââââââ 259 BESTE ODDS LOGICA 260 âââââââââââââââââââââââââââââââââââââââââââ */ 261 262 function getBestOdds(market1x2) { 263 var outcomes = ['1', 'X', '2']; 264 var result = []; 265 266 outcomes.forEach(function(outcome) { 267 var bestVal = 0; 268 var bestBms = []; 269 270 Object.keys(market1x2).forEach(function(bmName) { 271 var val = parseFloat(market1x2[bmName][outcome]); 272 if (isNaN(val) || val <= 0) return; 273 if (val > bestVal) { 274 bestVal = val; 275 bestBms = [bmName]; 276 } else if (val === bestVal) { 277 bestBms.push(bmName); 278 } 279 }); 280 281 if (bestBms.length > 0) { 282 var picked = bestBms[Math.floor(Math.random() * bestBms.length)]; 283 result.push({ 284 val: bestVal.toFixed(2), 285 bm: getBmMeta(picked), 286 label: outcome 287 }); 288 } 289 }); 290 291 return result; 292 } 293 294 /* âââââââââââââââââââââââââââââââââââââââââââ 295 SLIDE BOUWEN 296 âââââââââââââââââââââââââââââââââââââââââââ */ 297 298 /* âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 299 KSA: zodra een wedstrijd is begonnen worden de odds verborgen. 300 Bepaling puur op STARTTIJD: vanaf kickoff blijft de kaart in de 301 'verborgen' staat staan TOT de wedstrijd als 'finished' is 302 gemarkeerd (dan valt-ie sowieso uit de slider). Geen bovengrens, 303 zodat we tijdens én vlak na de wedstrijd nooit (live/stale) odds 304 tonen â bookmakers leveren tijdens een wedstrijd vaak rommel-odds. 305 âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ */ 306 function minSinceKick(m) { 307 var t = (m.matchDate && !isNaN(m.matchDate.getTime())) ? m.matchDate.getTime() : NaN; 308 if (isNaN(t)) return null; 309 return (Date.now() - t) / 60000; 310 } 311 /* Odds verbergen? Vanaf kickoff tot 'finished'. */ 312 function isLiveWindow(m) { 313 if (!m || m.isFinished) return false; 314 var mins = minSinceKick(m); 315 return mins !== null && mins >= 0; 316 } 317 /* 'Live'-label tonen? Alleen tijdens een realistische wedstrijdduur, 318 daarna valt het label terug op de normale status (odds blijven verborgen). */ 319 var LIVE_BADGE_MAX_MIN = 165; // 2u45 320 function isLiveLabel(m) { 321 if (!m || m.isFinished) return false; 322 var mins = minSinceKick(m); 323 return mins !== null && mins >= 0 && mins < LIVE_BADGE_MAX_MIN; 324 } 325 326 var LIVE_LOCK_SVG = '<svg width="13" height="13" viewBox="0 0 24 24" fill="none" aria-hidden="true"><rect x="5" y="11" width="14" height="10" rx="2" stroke="currentColor" stroke-width="2"/><path d="M8 11V8a4 4 0 0 1 8 0v3" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>'; 327 328 /* Bouwt de odds-sectie. Tijdens het live-venster worden de odds-waarden 329 NIET in de DOM gezet; in plaats daarvan een grijze 'verborgen'-staat. */ 330 function oddsSectionHtml(m) { 331 if (isLiveWindow(m)) { 332 var n = (m.odds && m.odds.length) ? m.odds.length : 3; 333 var pills = ''; 334 for (var i = 0; i < n; i++) pills += '<span class="wk2026-vgn-slider-odd-hidden"></span>'; 335 return '<div class="wk2026-vgn-slider-odds-section wk2026-vgn-slider-odds-section--live">' + 336 '<div class="wk2026-vgn-slider-odds-row wk2026-vgn-slider-odds-row--live">' + pills + '</div>' + 337 '<div class="wk2026-vgn-slider-odds-livemsg">' + LIVE_LOCK_SVG + '<span>Odds tijdelijk verborgen na aftrap</span></div>' + 338 '</div>'; 339 } 340 var labelsHtml = m.odds.map(function(o) { 341 return '<div class="wk2026-vgn-slider-odds-label">' + o.label + '</div>'; 342 }).join(''); 343 var badgesHtml = m.odds.map(function(o) { 344 var bgNormal = hexToRgba(o.bm.bg, 0.90);
345 var bgHover = hexToRgba(o.bm.bg, 1); 346 return '<a href="/go/' + o.bm.slug + '/" target="_blank" rel="noopener" class="wk2026-vgn-slider-odd-badge" style="--wk-logo-bg:' + bgNormal + '; --wk-logo-bg-hover:' + bgHover + ';">' + 347 '<span class="wk2026-vgn-slider-odd-logo">' + 348 '<img src="' + LOGO_BASE + o.bm.slug + '-sq-wit.svg" alt="' + o.bm.label + '" loading="lazy">' + 349 '</span>' + 350 '<span class="wk2026-vgn-slider-odd-value">' + o.val + '</span>' + 351 '<span class="wk2026-vgn-slider-odd-chevron">' + chevron('#93b4f0') + '</span>' + 352 '</a>'; 353 }).join(''); 354 return '<div class="wk2026-vgn-slider-odds-section">' + 355 '<div class="wk2026-vgn-slider-odds-labels">' + labelsHtml + '</div>' + 356 '<div class="wk2026-vgn-slider-odds-row">' + badgesHtml + '</div>' + 357 '</div>'; 358 } 359 360 function buildSlide(m) { 361 function flagWrap(flagCode, name) { 362 return '<span class="wk2026-vgn-slider-name-flag-wrap">' + 363 '<img src="' + FLAG_BASE + flagCode + '.svg" alt="' + name + '" loading="lazy">' + 364 '</span>'; 365 } 366 367 function nameSpans(team) { 368 return '<span class="wk2026-vgn-slider-name-full">' + team.name + '</span>' + 369 '<span class="wk2026-vgn-slider-name-abbrev">' + team.abbrev + '</span>'; 370 } 371 372 var statusLabel = isLiveLabel(m) ? 'Live' : m.status; 373 var badgeClass = statusBadgeClass(statusLabel); 374 var keyAttr = (m.match_key || '').toString().replace(/"/g, ''); 375 376 return '' + 377 '<div class="wk2026-vgn-slider-card" data-match-key="' + keyAttr + '">' + 378 '<div class="wk2026-vgn-slider-card-body">' + 379 '<div class="wk2026-vgn-slider-topbar">' + 380 '<div class="wk2026-vgn-slider-topbar-left">' + 381 '<span class="' + badgeClass + '">' + statusLabel + '</span>' + 382 '<span class="wk2026-vgn-slider-badge-time">' + m.timeLabel + '</span>' + 383 '</div>' + 384 '<div class="wk2026-vgn-slider-league-info">' + 385 'WK 2026' + 386 '<img src="' + WK_LOGO + '" alt="WK 2026" class="wk2026-vgn-slider-wk-logo" loading="lazy">' + 387 '</div>' + 388 '</div>' + 389 '<div class="wk2026-vgn-slider-teams-row">' + 390 '<div class="wk2026-vgn-slider-team-col wk2026-vgn-slider-team-col--home">' + 391 '<div class="wk2026-vgn-slider-name-row">' + 392 flagWrap(m.home.flagCode, m.home.name) + 393 nameSpans(m.home) + 394 '</div>' + 395 '<div class="wk2026-vgn-slider-shirt-wrap">' + 396 '<img src="' + SHIRT_BASE + m.home.slug + '.svg" alt="' + m.home.name + '" loading="lazy">' + 397 '</div>' + 398 '</div>' + 399 '<div class="wk2026-vgn-slider-score-col">' + 400 '<span class="wk2026-vgn-slider-score-sep">:</span>' + 401 '</div>' + 402 '<div class="wk2026-vgn-slider-team-col wk2026-vgn-slider-team-col--away">' + 403 '<div class="wk2026-vgn-slider-shirt-wrap">' + 404 '<img src="' + SHIRT_BASE + m.away.slug + '.svg" alt="' + m.away.name + '" loading="lazy">' + 405 '</div>' + 406 '<div class="wk2026-vgn-slider-name-row">' + 407 nameSpans(m.away) + 408 flagWrap(m.away.flagCode, m.away.name) + 409 '</div>' + 410 '</div>' + 411 '</div>' + 412 '</div>' + 413 oddsSectionHtml(m) + 414 '</div>'; 415 } 416 417 /* âââââââââââââââââââââââââââââââââââââââââââ 418 TYPEWRITER EFFECT 419 âââââââââââââââââââââââââââââââââââââââââââ */ 420 421 var twHomeEl, twAwayEl, twTimer = null, twDebounce = null; 422 var twIndex1 = 0, twIndex2 = 0; 423 var twTarget1 = '', twTarget2 = ''; 424 var TYPE_SPEED = 38, BACK_SPEED = 12; 425 var twLastIndex = -1; 426 427 function twType() { 428 if (twIndex1 < twTarget1.length) { 429 twIndex1++; 430 twHomeEl.textContent = twTarget1.substring(0, twIndex1); 431 if (twIndex1 === 1) twHomeEl.classList.add('tw-loaded'); 432 twTimer = setTimeout(twType, TYPE_SPEED); 433 } else if (twIndex2 < twTarget2.length) { 434 twIndex2++; 435 twAwayEl.textContent = twTarget2.substring(0, twIndex2); 436 if (twIndex2 === 1) twAwayEl.classList.add('tw-loaded'); 437 twTimer = setTimeout(twType, TYPE_SPEED); 438 } 439 } 440 441 function twBack(onDone) { 442 if (twIndex1 > 0 || twIndex2 > 0) { 443 if (twIndex1 > 0) { twIndex1--; twHomeEl.textContent = twTarget1.substring(0, twIndex1); } 444 if (twIndex2 > 0) { twIndex2--; twAwayEl.textContent = twTarget2.substring(0, twIndex2); } 445 twTimer = setTimeout(function() { twBack(onDone); }, BACK_SPEED); 446 } else { 447 onDone(); 448 } 449 } 450 451 function updateTitle(realIndex, matches) { 452 /* Debounce: Swiper loop triggert meerdere slideChange events snel achter elkaar */ 453 if (realIndex === twLastIndex) return; 454 twLastIndex = realIndex; 455 456 if (twTimer) { clearTimeout(twTimer); twTimer = null; } 457 if (twDebounce) { clearTimeout(twDebounce); twDebounce = null; } 458 459 twDebounce = setTimeout(function() { 460 var m = matches[realIndex]; 461 var newTarget1 = m.home.name; 462 var newTarget2 = m.away.name; 463 var newColor1 = m.home.color; 464 var newColor2 = m.away.color; 465 466 if (twTimer) { clearTimeout(twTimer); twTimer = null; } 467 468 function startTyping() { 469 twTarget1 = newTarget1; 470 twTarget2 = newTarget2; 471 twIndex1 = 0; 472 twIndex2 = 0; 473 twHomeEl.textContent = ''; 474 twAwayEl.textContent = ''; 475 twHomeEl.classList.remove('tw-loaded'); 476 twAwayEl.classList.remove('tw-loaded'); 477 twHomeEl.style.color = newColor1; 478 twAwayEl.style.color = newColor2; 479 twTimer = setTimeout(twType, 80); 480 } 481 482 if (twIndex1 > 0 || twIndex2 > 0) { 483 twBack(startTyping); 484 } else { 485 startTyping(); 486 } 487 }, 50); 488 } 489 490 /* âââââââââââââââââââââââââââââââââââââââââââ 491 SLIDER RENDEREN 492 âââââââââââââââââââââââââââââââââââââââââââ */ 493 494 var liveTimer = null; 495
496 function renderSlider(matches) { 497 var loadingEl = root.querySelector('.wk2026-vgn-slider-loading'); 498 var swiperEl = root.querySelector('.wk2026-vgn-slider-swiper'); 499 var paginEl = root.querySelector('.wk2026-vgn-slider-pagination'); 500 501 if (loadingEl) loadingEl.style.display = 'none'; 502 swiperEl.style.display = ''; 503 paginEl.style.display = ''; 504 505 twHomeEl = root.querySelector('[data-role="home"]'); 506 twAwayEl = root.querySelector('[data-role="away"]'); 507 var wrapper = swiperEl.querySelector('.swiper-wrapper'); 508 var dotsEl = root.querySelector('.wk2026-vgn-slider-dots'); 509 510 matches.forEach(function(m) { 511 var s = document.createElement('div'); 512 s.className = 'swiper-slide'; 513 s.innerHTML = buildSlide(m); 514 wrapper.appendChild(s); 515 }); 516 517 var MAX_DOTS = 7; 518 var SIDE = Math.floor(MAX_DOTS / 2); // 3 519 520 function renderDots(realIndex) { 521 var total = matches.length; 522 var html = ''; 523 524 if (total <= MAX_DOTS) { 525 /* Weinig wedstrijden: toon alles */ 526 matches.forEach(function(_, i) { 527 html += '<button class="wk2026-vgn-slider-dot' + 528 (i === realIndex ? ' wk2026-vgn-slider-dot-active' : '') + 529 '" aria-label="Wedstrijd ' + (i + 1) + '" data-index="' + i + '"></button>'; 530 }); 531 } else { 532 /* Veel wedstrijden: toon sliding window van max 7 dots */ 533 var start = realIndex - SIDE; 534 var end = realIndex + SIDE; 535 536 if (start < 0) { start = 0; end = MAX_DOTS - 1; } 537 if (end >= total) { end = total - 1; start = total - MAX_DOTS; } 538 539 for (var i = start; i <= end; i++) { 540 var isEdge = (i === start && start > 0) || (i === end && end < total - 1); 541 var cls = 'wk2026-vgn-slider-dot'; 542 if (i === realIndex) cls += ' wk2026-vgn-slider-dot-active'; 543 if (isEdge) cls += ' wk2026-vgn-slider-dot-small'; 544 html += '<button class="' + cls + '" aria-label="Wedstrijd ' + (i + 1) + '" data-index="' + i + '"></button>'; 545 } 546 } 547 548 dotsEl.innerHTML = html; 549 dotsEl.querySelectorAll('.wk2026-vgn-slider-dot').forEach(function(btn) { 550 btn.addEventListener('click', function() { 551 swiper.slideToLoop(parseInt(this.getAttribute('data-index'))); 552 }); 553 }); 554 } 555 556 var swiper = new Swiper(swiperEl, { 557 centeredSlides: true, 558 slidesPerView: 'auto', 559 spaceBetween: 28, 560 loop: matches.length > 2, 561 grabCursor: true, 562 speed: 480, 563 on: { 564 init: function() { 565 updateTitle(this.realIndex, matches); 566 renderDots(this.realIndex); 567 }, 568 slideChange: function() { 569 updateTitle(this.realIndex, matches); 570 renderDots(this.realIndex); 571 } 572 } 573 }); 574 575 /* KSA live-venster: houd de odds-staat in sync terwijl de pagina openstaat. 576 Verbergt odds zodra een wedstrijd live gaat en zet ze terug na het venster. 577 Werkt ook op de Swiper-loop-clones (query op data-match-key in root). */ 578 function syncLiveStates() { 579 matches.forEach(function(m) { 580 var keySel = (m.match_key || '').toString().replace(/"/g, ''); 581 if (!keySel) return; 582 var want = isLiveWindow(m); 583 var cards = root.querySelectorAll('.wk2026-vgn-slider-card[data-match-key="' + keySel + '"]'); 584 cards.forEach(function(card) { 585 var sec = card.querySelector('.wk2026-vgn-slider-odds-section'); 586 if (sec) { 587 var isLiveNow = sec.className.indexOf('wk2026-vgn-slider-odds-section--live') !== -1; 588 if (want !== isLiveNow) { 589 var tmp = document.createElement('div'); 590 tmp.innerHTML = oddsSectionHtml(m); 591 if (tmp.firstChild) sec.parentNode.replaceChild(tmp.firstChild, sec); 592 } 593 } 594 var badge = card.querySelector('.wk2026-vgn-slider-topbar-left > span'); 595 if (badge) { 596 var label = isLiveLabel(m) ? 'Live' : m.status; 597 if (badge.textContent !== label) { 598 badge.textContent = label; 599 badge.className = statusBadgeClass(label); 600 } 601 } 602 }); 603 }); 604 } 605 syncLiveStates(); 606 if (liveTimer) clearInterval(liveTimer); 607 liveTimer = setInterval(syncLiveStates, 30000); 608 } 609 610 /* âââââââââââââââââââââââââââââââââââââââââââ 611 DATA OPHALEN & VERWERKEN 612 âââââââââââââââââââââââââââââââââââââââââââ */ 613 614 function fetchJSON(file) { 615 return fetch(API_BASE + '?key=' + API_KEY + '&file=' + file) 616 .then(function(r) { 617 if (!r.ok) throw new Error('API fout: ' + r.status); 618 return r.json(); 619 }); 620 } 621 622 /* Data ophalen: gebruik inline PHP data als beschikbaar, anders fallback naar API */ 623 function getData() { 624 var inlineSchedule = window.wkSliderInlineSchedule; 625 var inlineOdds = window.wkSliderInlineOdds; 626 627 if (inlineSchedule && inlineSchedule.wedstrijden && inlineOdds) { 628 /* Server-side gecachte data beschikbaar â geen API calls nodig */ 629 return Promise.resolve([inlineSchedule, inlineOdds]); 630 } 631 /* Fallback: client-side API calls */ 632 return Promise.all([ 633 fetchJSON('wedstrijden-wk2026'), 634 fetchJSON('wedstrijden-odds-wk2026') 635 ]); 636 } 637 638 getData().then(function(results) { 639 var scheduleData = results[0]; 640 var oddsData = results[1]; 641 642 var oddsMap = {}; 643 if (oddsData.wedstrijden) {
644 oddsData.wedstrijden.forEach(function(w) { 645 // Index op genormaliseerde teamnamen (thuisteam|uitteam) 646 var teamKey = ((w.thuisteam || '') + '|' + (w.uitteam || '')).toLowerCase(); 647 oddsMap[teamKey] = w.odds; 648 }); 649 } 650 651 var upcoming = []; 652 var completed = []; 653 654 scheduleData.wedstrijden.forEach(function(w) { 655 // Join op teamnamen (schedule team_1/team_2 â odds thuisteam/uitteam) 656 var teamKey = ((w.team_1 || '') + '|' + (w.team_2 || '')).toLowerCase(); 657 var odds = oddsMap[teamKey]; 658 if (!odds || Object.keys(odds).length === 0) return; 659 660 var bmKeys = Object.keys(odds); 661 if (bmKeys.length === 0) return; 662 663 var homeData = getTeamData(w.team_1); 664 var awayData = getTeamData(w.team_2); 665 666 var match = { 667 match_key: w.match_key, 668 status: w.Is_Live ? 'Live' : (w.Is_Upcoming !== false ? 'Binnenkort' : 'Afgelopen'), 669 timeLabel: formatDateTime(w.datum_nl, w.tijd_nl), 670 home: { 671 name: homeData.displayName || w.team_1, 672 slug: homeData.slug, 673 abbrev: homeData.abbrev, 674 flagCode: homeData.flagCode, 675 color: (IS_DARK && homeData.darkColor) ? homeData.darkColor : homeData.color 676 }, 677 away: { 678 name: awayData.displayName || w.team_2, 679 slug: awayData.slug, 680 abbrev: awayData.abbrev, 681 flagCode: awayData.flagCode, 682 color: (IS_DARK && awayData.darkColor) ? awayData.darkColor : awayData.color 683 }, 684 href: w.wedstrijd_url || '#', 685 odds: getBestOdds(odds), 686 matchDate: new Date(w.datetime_nl_iso || w.datum_nl + 'T' + w.tijd_nl), 687 isLive: w.Is_Live === true, 688 isFinished: (function(){ var s = String(w.Match_Status || '').toLowerCase(); return s === 'finished' || s === 'completed'; })(), 689 isKnockout: String(w.toernooifase || '').toLowerCase().indexOf('groep') === -1 690 }; 691 692 if (w.Is_Live === true || w.Match_Status === 'scheduled' || w.Is_Upcoming === true) { 693 upcoming.push(match); 694 } else { 695 completed.push(match); 696 } 697 }); 698 699 upcoming.sort(function(a, b) { return a.matchDate - b.matchDate; }); 700 completed.sort(function(a, b) { return a.matchDate - b.matchDate; }); 701 702 var live = upcoming.filter(function(m) { return m.isLive; }); 703 var notLive = upcoming.filter(function(m) { return !m.isLive; }); 704 upcoming = live.concat(notLive); 705 706 var matches; 707 if (upcoming.length > 0) { 708 matches = upcoming.slice(0, MAX_SLIDES); 709 } else if (completed.length > 0) { 710 matches = completed.slice(-MAX_SLIDES); 711 matches.forEach(function(m) { m.status = 'Gespeeld'; }); 712 } 713 714 if (!matches || matches.length === 0) { 715 var loadingEl = root.querySelector('.wk2026-vgn-slider-loading'); 716 if (loadingEl) loadingEl.innerHTML = '<p style="color:#999;">Nog geen wedstrijden beschikbaar.</p>'; 717 return; 718 } 719 720 renderSlider(matches); 721 722 }).catch(function(err) { 723 console.error('[WK2026 Slider] Fout bij laden:', err); 724 var loadingEl = root.querySelector('.wk2026-vgn-slider-loading'); 725 if (loadingEl) loadingEl.innerHTML = '<p style="color:#999;">Kan wedstrijden niet laden.</p>'; 726 }); 727 728})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.