PageSourceSearch

https://www.weddenopwk.nl/wp-content/plugins/vgn-wk2026/templates/slider.js?ver=3.9.6

js weddenopwk.nl collected 2026-10-06 06:19:07 UTC 37,593 bytes, 728 lines download raw bytes

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.