1(() => { 2 const header = document.querySelector('.site-header'); 3 const widget = document.querySelector('[data-ent-header-widget]'); 4 const desktopMq = window.matchMedia('(min-width: 1024px)'); 5 6 /* ========================================= 7 Nav overflow â collapse items that don't fit 8 into a "Plus â¾" dropdown (desktop only) 9 ========================================= */ 10 const menu = document.getElementById('primary-menu'); 11 let queueReflow = () => {}; 12 13 if (menu) { 14 const moreLi = document.createElement('li'); 15 moreLi.className = 'ent-nav-more'; 16 moreLi.hidden = true; 17 18 const moreBtn = document.createElement('button'); 19 moreBtn.type = 'button'; 20 moreBtn.setAttribute('aria-expanded', 'false'); 21 moreBtn.setAttribute('aria-haspopup', 'true'); 22 moreBtn.innerHTML = 'Plus <span aria-hidden="true">â¾</span>'; 23 24 const moreMenu = document.createElement('ul'); 25 moreMenu.className = 'ent-nav-more__menu'; 26 moreMenu.hidden = true; 27 28 moreLi.append(moreBtn, moreMenu); 29 menu.appendChild(moreLi); 30 31 const closeMore = () => { 32 moreLi.classList.remove('is-open'); 33 moreMenu.hidden = true; 34 moreBtn.setAttribute('aria-expanded', 'false'); 35 }; 36 37 moreBtn.addEventListener('click', () => { 38 const open = moreLi.classList.toggle('is-open'); 39 moreMenu.hidden = !open; 40 moreBtn.setAttribute('aria-expanded', String(open)); 41 }); 42 document.addEventListener('click', (event) => { 43 if (!moreLi.contains(event.target)) closeMore(); 44 }); 45 document.addEventListener('keydown', (event) => { 46 if (event.key === 'Escape') closeMore(); 47 }); 48 49 // In the sticky bar we always show at most 12 items, the rest go under "Plus" 50 const STICKY_MAX_ITEMS = 12; 51 52 const reflowNav = () => { 53 // restore every collapsed item to its original spot 54 while (moreMenu.firstElementChild) { 55 menu.insertBefore(moreMenu.firstElementChild, moreLi); 56 } 57 moreLi.hidden = true; 58 59 if (!desktopMq.matches) return; // mobile menu handles itself 60 61 // sticky: hard cap at 12 visible items 62 if (header && header.classList.contains('is-sticky')) { 63 const items = [...menu.children].filter( 64 (li) => li !== moreLi && !li.classList.contains('burger-close') 65 ); 66 for (let i = items.length - 1; i >= STICKY_MAX_ITEMS; i--) { 67 moreLi.hidden = false; 68 moreMenu.insertBefore(items[i], moreMenu.firstChild); 69 } 70 } 71 72 // then collapse trailing items until the row fits 73 let guard = menu.children.length; 74 while (guard-- > 0 && menu.scrollWidth > menu.clientWidth + 1) { 75 const item = moreLi.previousElementSibling; 76 if (!item || item.classList.contains('burger-close')) break; 77 moreLi.hidden = false; 78 moreMenu.insertBefore(item, moreMenu.firstChild); 79 } 80 81 if (!moreMenu.firstElementChild) moreLi.hidden = true; 82 }; 83 84 let reflowRaf = 0; 85 queueReflow = () => { 86 cancelAnimationFrame(reflowRaf); 87 reflowRaf = requestAnimationFrame(reflowNav); 88 }; 89 90 window.addEventListener('resize', queueReflow); 91 if (desktopMq.addEventListener) desktopMq.addEventListener('change', queueReflow); 92 if (document.fonts && document.fonts.ready) document.fonts.ready.then(queueReflow); 93 queueReflow(); 94 } 95 96 /* ========================================= 97 Sticky header 98 ========================================= */ 99 if (header) { 100 let ticking = false; 101 const root = document.documentElement; 102 const syncSticky = () => { 103 const wasSticky = header.classList.contains('is-sticky'); 104 // Hysteresis: collapse past 24px, expand again only near the top, so small 105 // wiggles around the threshold don't re-toggle the bar (each toggle re-lays 106 // out the header row, which is still a small layout shift). 107 const sticky = wasSticky ? window.scrollY > 8 : window.scrollY > 24; 108 if (wasSticky !== sticky) { 109 if (sticky) { 110 // CLS: on desktop the compact bar is position:fixed, so the page needs a
111 // spacer exactly as tall as the expanded header it replaces. Measure it 112 // while the header is still in flow, before the class switches. 113 root.style.setProperty('--ent-header-h', header.offsetHeight + 'px'); 114 } 115 // Hide the header's children for the one frame in which they re-flow 116 // into the other layout: Chrome ignores visibility:hidden boxes in the 117 // Layout Instability metric, so the row change stops counting as CLS. 118 header.classList.add('is-switching'); 119 header.classList.toggle('is-sticky', sticky); 120 root.classList.toggle('ent-header-fixed', sticky); 121 requestAnimationFrame(() => { 122 requestAnimationFrame(() => header.classList.remove('is-switching')); 123 }); 124 queueReflow(); // available nav width changes between states 125 } 126 ticking = false; 127 }; 128 window.addEventListener('scroll', () => { 129 if (!ticking) { 130 window.requestAnimationFrame(syncSticky); 131 ticking = true; 132 } 133 }, { passive: true }); 134 syncSticky(); 135 } 136 137 /* ========================================= 138 Header info widget (cities / weather / FX) 139 ========================================= */ 140 if (widget) { 141 const slides = [...widget.querySelectorAll('[data-ent-city-slide]')]; 142 const dots = [...widget.querySelectorAll('[data-ent-city-dot]')]; 143 const city = widget.querySelector('.ent-city'); 144 let active = 0; 145 let rotation; 146 147 // Current site language: GTranslate cookie ("/fr/en") â <html lang> â fr. 148 // JS-inserted text is not translated by GTranslate, so we localize it ourselves. 149 const siteLang = (() => { 150 const m = document.cookie.match(/(?:^|;\s*)googtrans=\/[a-z-]+\/([a-z-]+)/i); 151 const lang = (m && m[1]) || document.documentElement.lang || 'fr'; 152 return lang.toLowerCase().split('-')[0]; 153 })(); 154 155 const updateClocks = () => { 156 const now = new Date(); 157 slides.forEach((slide) => { 158 const timezone = slide.dataset.timezone || 'UTC'; 159 const time = slide.querySelector('[data-ent-city-time]'); 160 const date = slide.querySelector('[data-ent-city-date]'); 161 try { 162 time.textContent = new Intl.DateTimeFormat(siteLang, { 163 timeZone: timezone, hour: '2-digit', minute: '2-digit', hour12: false, 164 }).format(now); 165 date.textContent = new Intl.DateTimeFormat(siteLang, { 166 timeZone: timezone, day: '2-digit', month: 'long', 167 }).format(now).toUpperCase(); 168 } catch (_) { 169 time.textContent = now.toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit' }); 170 } 171 }); 172 }; 173 174 const show = (next) => { 175 if (!slides.length) return; 176 active = (next + slides.length) % slides.length; 177 slides.forEach((slide, index) => { 178 const selected = index === active; 179 slide.hidden = !selected; 180 slide.classList.toggle('is-active', selected); 181 }); 182 dots.forEach((dot, index) => { 183 dot.classList.toggle('is-active', index === active); 184 dot.setAttribute('aria-current', index === active ? 'true' : 'false'); 185 }); 186 }; 187 188 const start = () => { 189 window.clearInterval(rotation); 190 if (slides.length > 1 && !window.matchMedia('(prefers-reduced-motion: reduce)').matches) { 191 rotation = window.setInterval(() => show(active + 1), 10000); 192 } 193 }; 194 195 widget.querySelector('[data-ent-city-prev]')?.addEventListener('click', () => { show(active - 1); start(); }); 196 widget.querySelector('[data-ent-city-next]')?.addEventListener('click', () => { show(active + 1); start(); }); 197 dots.forEach((dot, index) => dot.addEventListener('click', () => { show(index); start(); })); 198 city?.addEventListener('mouseenter', () => window.clearInterval(rotation)); 199 city?.addEventListener('mouseleave', start); 200 city?.addEventListener('focusin', () => window.clearInterval(rotation)); 201 city?.addEventListener('focusout', start); 202 203 const fx = widget.querySelector('[data-ent-fx]'); 204 205 // FX auto-rotation (mobile shows one pair at a time; CSS decides visibility) 206 const initFxRotation = () => { 207 if (!fx) return; 208 const fxRates = [...fx.querySelectorAll('.ent-fx__rate')]; 209 if (!fxRates.length) return; 210 fx.querySelector('.ent-fx__rates')?.classList.add('is-js');
211 fxRates.forEach((r) => r.classList.remove('is-active')); 212 let fxActive = 0; 213 fxRates[0].classList.add('is-active'); 214 window.clearInterval(window.__entFxRot); 215 if (fxRates.length > 1 && !window.matchMedia('(prefers-reduced-motion: reduce)').matches) { 216 window.__entFxRot = window.setInterval(() => { 217 fxRates[fxActive].classList.remove('is-active'); 218 fxActive = (fxActive + 1) % fxRates.length; 219 fxRates[fxActive].classList.add('is-active'); 220 }, 5000); 221 } 222 }; 223 224 // Region-localised FX: when the visitor switched language, quote the region's 225 // own currency instead of the default euro pairs (e.g. UA â гÑн/USD). 226 // Values are "local currency per 1 foreign unit" so numbers stay readable. 227 const FX_REGION = { 228 uk: { base: 'UAH', quotes: ['USD', 'EUR', 'GBP', 'PLN'] }, 229 ru: { base: 'RUB', quotes: ['USD', 'EUR', 'GBP', 'CNY'] }, 230 ja: { base: 'JPY', quotes: ['USD', 'EUR', 'GBP', 'CNY'] }, 231 ar: { base: 'AED', quotes: ['USD', 'EUR', 'GBP', 'SAR'] }, 232 tr: { base: 'TRY', quotes: ['USD', 'EUR', 'GBP', 'RUB'] }, 233 pl: { base: 'PLN', quotes: ['USD', 'EUR', 'GBP', 'CZK'] }, 234 }; 235 236 // Daily baseline in localStorage â a real day-over-day % (API updates ~1Ã/day). 237 const fxChange = (pair, val) => { 238 try { 239 const today = new Date().toISOString().slice(0, 10); 240 const key = 'entFxBase:' + pair; 241 const rec = JSON.parse(localStorage.getItem(key) || 'null'); 242 if (!rec || !Number.isFinite(rec.val)) { 243 localStorage.setItem(key, JSON.stringify({ date: today, val })); 244 return 0; 245 } 246 const pct = rec.val ? ((val - rec.val) / rec.val) * 100 : 0; 247 if (rec.date !== today) localStorage.setItem(key, JSON.stringify({ date: today, val })); 248 return pct; 249 } catch (_) { return 0; } 250 }; 251 252 const applyRegionFx = async () => { 253 const region = FX_REGION[siteLang]; 254 const wrap = fx?.querySelector('.ent-fx__rates'); 255 if (!region || !wrap) { initFxRotation(); return; } 256 try { 257 const ckey = 'entFxRegion:' + region.base; 258 let rates = null; 259 try { 260 const c = JSON.parse(sessionStorage.getItem(ckey) || 'null'); 261 if (c && (Date.now() - c.t) < 6 * 3600 * 1000) rates = c.rates; 262 } catch (_) {} 263 if (!rates) { 264 const res = await fetch('https://open.er-api.com/v6/latest/' + region.base); 265 const j = res.ok ? await res.json() : null; 266 if (!j || j.result !== 'success' || !j.rates) { initFxRotation(); return; } 267 rates = j.rates; 268 try { sessionStorage.setItem(ckey, JSON.stringify({ t: Date.now(), rates })); } catch (_) {} 269 } 270 const html = region.quotes.map((q) => { 271 const r = rates[q]; 272 if (!r) return ''; 273 const val = 1 / r; // local currency per 1 foreign unit 274 const dec = val >= 100 ? 1 : (val >= 1 ? 2 : 4); 275 const pair = region.base + '/' + q; 276 const pct = fxChange(pair, val); 277 const dir = Math.abs(pct) < 0.005 ? 'flat' : (pct < 0 ? 'down' : 'up'); 278 const arrow = dir === 'down' ? 'â' : (dir === 'up' ? 'â' : ''); 279 return '<article class="ent-fx__rate"><span>' + pair + '</span>' + 280 '<strong>' + val.toFixed(dec) + (arrow ? ' <i class="is-' + dir + '">' + arrow + '</i>' : '') + '</strong>' + 281 '<small class="is-' + dir + '">' + (pct > 0 ? '+' : '') + pct.toFixed(2) + '%</small></article>'; 282 }).join(''); 283 if (html) wrap.innerHTML = html; 284 } catch (_) {} 285 initFxRotation(); 286 }; 287 applyRegionFx(); 288 289 updateClocks(); 290 window.setInterval(updateClocks, 30000); 291 show(0); 292 start(); 293 294 /* ----------------------------------------- 295 Local city slide â time & weather for the 296 visitor's own location. 297 No permission prompt: city is derived from 298 the browser IANA timezone (city-level only), 299 weather comes from the free Open-Meteo API. 300 ----------------------------------------- */ 301 // Correct romanization for cities whose old exonyms linger in data sources 302 const LATIN_CITY_FIXES = { kiev: 'Kyiv', kiew: 'Kyjiw' }; 303 const fixLatinCity = (name) => LATIN_CITY_FIXES[String(name).toLowerCase()] || name; 304 305 // Languages written in non-Latin scripts â take the city name from 306 // Wikipedia langlinks; Latin-script languages use the romanized name. 307 const NON_LATIN_LANGS = ['uk', 'ru', 'ar', 'ja', 'zh', 'ko', 'he', 'el', 'bg', 'sr']; 308 309 const YOUR_CITY_LABEL = { 310 fr: 'Votre ville', en: 'Your city', de: 'Ihre Stadt', es: 'Tu ciudad', 311 it: 'La tua città ', uk: 'ÐаÑе мÑÑÑо', ru: 'ÐÐ°Ñ Ð³Ð¾Ñод', 312 ar: '٠دÙÙØªÙ', ja: 'ããªãã®è¡', zh: 'æ¨çåå¸', 313 }; 314 315 // Localized weather condition from a WMO weather_code (so the visitor's 316 // local city matches the server-rendered world cities, which show a label). 317 const ent_wx_condition = (lang, code) => { 318 const cat = 319 code === 0 ? 'clear' 320 : [1, 2, 3].includes(code) ? 'cloud' 321 : [45, 48].includes(code) ? 'fog' 322 : ((code >= 51 && code <= 67) || (code >= 80 && code <= 82)) ? 'rain' 323 : ((code >= 71 && code <= 77) || [85, 86].includes(code)) ? 'snow' 324 : (code >= 95) ? 'storm' 325 : 'cloud'; 326 const T = { 327 fr: { clear: 'Soleil', cloud: 'Nuageux', fog: 'Brouillard', rain: 'Pluie', snow: 'Neige', storm: 'Orage' }, 328 en: { clear: 'Sunny', cloud: 'Cloudy', fog: 'Fog', rain: 'Rain', snow: 'Snow', storm: 'Storm' }, 329 de: { clear: 'Sonnig', cloud: 'Bewölkt', fog: 'Nebel', rain: 'Regen', snow: 'Schnee', storm: 'Gewitter' }, 330 it: { clear: 'Soleggiato', cloud: 'Nuvoloso', fog: 'Nebbia', rain: 'Pioggia', snow: 'Neve', storm: 'Temporale' }, 331 es: { clear: 'Soleado', cloud: 'Nublado', fog: 'Niebla', rain: 'Lluvia', snow: 'Nieve', storm: 'Tormenta' }, 332 ru: { clear: 'ЯÑно', cloud: 'ÐблаÑно', fog: 'ТÑман', rain: 'ÐождÑ', snow: 'Снег', storm: 'ÐÑоза' }, 333 uk: { clear: 'СонÑÑно', cloud: 'ХмаÑно', fog: 'ТÑман', rain: 'ÐоÑ', snow: 'СнÑг', storm: 'ÐÑоза' }, 334 ar: { clear: '٠ش٠س', cloud: 'غائ٠', fog: 'ضباب', rain: '٠٠طر', snow: 'Ø«ÙØ¬', storm: 'Ø¹Ø§ØµÙØ©' }, 335 ja: { clear: 'æ´ã', cloud: 'æã', fog: 'é§', rain: 'é¨', snow: 'éª', storm: 'é·é¨' }, 336 zh: { clear: 'æ´', cloud: 'å¤äº', fog: 'é¾', rain: 'é¨', snow: 'éª', storm: 'é·é¨' }, 337 }; 338 return (T[lang] || T.fr)[cat] || ''; 339 }; 340 341 // Localized "Feels like X° · High Y° / Low Z°" â omits missing values cleanly. 342 const ent_wx_details_line = (lang, feels, tmax, tmin) => { 343 const T = {
344 fr: { feel: 'Ressenti', max: 'Max', min: 'Min' }, 345 en: { feel: 'Feels like', max: 'High', min: 'Low' }, 346 de: { feel: 'Gefühlt', max: 'Max', min: 'Min' }, 347 es: { feel: 'Sensación', max: 'Máx', min: 'MÃn' }, 348 it: { feel: 'Percepiti', max: 'Max', min: 'Min' }, 349 uk: { feel: 'ÐÑдÑÑваÑÑÑÑÑ Ñк', max: 'ÐакÑ.', min: 'ÐÑн.' }, 350 ru: { feel: 'ÐÑÑÑаеÑÑÑ ÐºÐ°Ðº', max: 'ÐакÑ.', min: 'Ðин.' }, 351 ar: { feel: 'Ù ØØ³Ùس', max: 'Ø§ÙØ¹Ø¸Ù Ù', min: 'Ø§ÙØµØºØ±Ù' }, 352 ja: { feel: '使', max: 'æé«', min: 'æä½' }, 353 zh: { feel: '使', max: 'æé«', min: 'æä½' }, 354 }; 355 const t = T[lang] || T.fr; 356 const parts = []; 357 if (Number.isFinite(feels)) parts.push(t.feel + ' ' + feels + '°'); 358 const mm = []; 359 if (Number.isFinite(tmax)) mm.push(t.max + ' ' + tmax + '°'); 360 if (Number.isFinite(tmin)) mm.push(t.min + ' ' + tmin + '°'); 361 if (mm.length) parts.push(mm.join(' / ')); 362 return parts.join(' · '); 363 }; 364 365 const addLocalCitySlide = async () => { 366 const tz = (Intl.DateTimeFormat().resolvedOptions().timeZone || '').trim(); 367 if (!tz || !tz.includes('/')) return; 368 // already covered by a configured city? 369 if (slides.some((slide) => (slide.dataset.timezone || '') === tz)) return; 370 371 const cityRaw = fixLatinCity(tz.split('/').pop().replace(/_/g, ' ')); 372 if (!cityRaw) return; 373 374 const cacheKey = 'entLocalWx4:' + siteLang + ':' + tz; 375 let data = null; 376 try { 377 const cached = JSON.parse(sessionStorage.getItem(cacheKey) || 'null'); 378 if (cached && Date.now() - cached.t < 30 * 60 * 1000) data = cached; 379 } catch (_) {} 380 381 if (!data) { 382 const geoRes = await fetch( 383 'https://geocoding-api.open-meteo.com/v1/search?count=1&language=fr&name=' + encodeURIComponent(cityRaw) 384 ); 385 if (!geoRes.ok) return; 386 const geo = await geoRes.json(); 387 const hit = geo && geo.results && geo.results[0]; 388 if (!hit) return; 389 390 const wxRes = await fetch( 391 'https://api.open-meteo.com/v1/forecast?current=temperature_2m,apparent_temperature,weather_code&daily=temperature_2m_max,temperature_2m_min&forecast_days=1&timezone=auto&latitude=' + hit.latitude + '&longitude=' + hit.longitude 392 ); 393 if (!wxRes.ok) return; 394 const wx = await wxRes.json(); 395 const cur = (wx && wx.current) || {}; 396 const daily = (wx && wx.daily) || {}; 397 const temp = Math.round(cur.temperature_2m); 398 if (!Number.isFinite(temp)) return; 399 const feels = Number.isFinite(cur.apparent_temperature) ? Math.round(cur.apparent_temperature) : null; 400 const tmax = daily.temperature_2m_max ? Math.round(daily.temperature_2m_max[0]) : null; 401 const tmin = daily.temperature_2m_min ? Math.round(daily.temperature_2m_min[0]) : null; 402 const cond = Number.isFinite(cur.weather_code) ? ent_wx_condition(siteLang, cur.weather_code) : ''; 403 404 // Display name: romanized for Latin-script languages, localized via 405 // Wikipedia langlinks for non-Latin ones (e.g. ÐиÑв / ãã¼ã¦ / åºè¼). 406 let displayName = fixLatinCity(hit.name || cityRaw); 407 if (NON_LATIN_LANGS.includes(siteLang)) { 408 try { 409 const llRes = await fetch( 410 'https://en.wikipedia.org/w/api.php?action=query&prop=langlinks&redirects=1&format=json&origin=*&lllang=' + siteLang + '&titles=' + encodeURIComponent(cityRaw) 411 ); 412 if (llRes.ok) { 413 const ll = await llRes.json(); 414 const page = ll && ll.query && ll.query.pages && Object.values(ll.query.pages)[0]; 415 const localized = page && page.langlinks && page.langlinks[0] && page.langlinks[0]['*']; 416 if (localized) displayName = localized; 417 } 418 } catch (_) {} 419 } 420 421 data = { t: Date.now(), name: displayName, temp, feels, tmax, tmin, cond, photo: '' }; 422 try { sessionStorage.setItem(cacheKey, JSON.stringify(data)); } catch (_) {} 423 } 424 425 const slidesWrap = widget.querySelector('.ent-city__slides'); 426 if (!slidesWrap) return; 427 428 const sunSvg = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="4"></circle><path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M6.34 17.66l-1.41 1.41M19.07 4.93l-1.41 1.41"></path></svg>'; 429 430 const slide = document.createElement('article'); 431 slide.className = 'ent-city__slide'; 432 slide.setAttribute('data-ent-city-slide', ''); 433 slide.dataset.timezone = tz; 434 slide.hidden = true; 435 slide.innerHTML = 436 '<span class="ent-city__mark ent-city__mark--local" aria-hidden="true"><span></span></span>' + 437 '<span class="ent-city__place">
437' + 438 '<strong class="ent-city__name"></strong>' + 439 '<time class="ent-city__time" data-ent-city-time>--:--</time>' + 440 '<small class="ent-city__date" data-ent-city-date></small>' + 441 '</span>' + 442 '<span class="ent-city__weather">' + 443 '<span class="ent-city__wx">' + 444 '<span class="ent-weather-icon" aria-hidden="true">' + sunSvg + '</span>' + 445 '<span class="ent-city__wx-values">' + 446 '<strong class="ent-city__temp"></strong>' + 447 '<small class="ent-city__cond" hidden></small>' + 448 '</span>' + 449 '</span>' + 450 '</span>'; 451 452 const mark = slide.querySelector('.ent-city__mark'); 453 mark.querySelector('span').textContent = data.name.charAt(0).toUpperCase(); 454 455 const applyPhoto = (photo) => { 456 if (!photo) return; 457 mark.classList.add('ent-city__mark--photo'); 458 mark.style.backgroundImage = 'url("' + photo + '")'; 459 }; 460 461 // City photo â Wikipedia (Wikimedia Commons) page thumbnail. 462 // fr â en, REST summary first, PageImages API as a fallback; SVG 463 // flags/coats of arms are skipped. Runs in the background so a failed 464 // photo lookup never blocks the slide, and retries on the next page 465 // load when the cached entry has no photo yet. 466 const fetchCityPhoto = async () => { 467 const looksLikePhoto = (url) => !!url && !/\.svg|flag_of|coat_of_arms|blason|armoiries|escudo|wappen/i.test(url); 468 // Latin (romanized) title first â fr/en Wikipedia lookups need it even 469 // when the display name is non-Latin (ÐиÑв, ãã¼ã¦, â¦) 470 const titles = [...new Set([cityRaw, data.name])]; 471 472 for (const lang of ['fr', 'en']) { 473 for (const title of titles) { 474 try { 475 const res = await fetch( 476 'https://' + lang + '.wikipedia.org/api/rest_v1/page/summary/' + encodeURIComponent(title.replace(/ /g, '_')) 477 ); 478 if (!res.ok) continue; 479 const j = await res.json(); 480 const src = j && j.thumbnail && j.thumbnail.source; 481 if (looksLikePhoto(src)) return src; 482 } catch (_) {} 483 } 484 } 485 486 for (const lang of ['fr', 'en']) { 487 try { 488 const res = await fetch( 489 'https://' + lang + '.wikipedia.org/w/api.php?action=query&prop=pageimages&piprop=thumbnail&pithumbsize=330&redirects=1&format=json&origin=*&titles=' + encodeURIComponent(titles[0]) 490 ); 491 if (!res.ok) continue; 492 const j = await res.json(); 493 const pages = j && j.query && j.query.pages; 494 const first = pages && Object.values(pages)[0]; 495 const src = first && first.thumbnail && first.thumbnail.source; 496 if (looksLikePhoto(src)) return src; 497 } catch (_) {} 498 } 499 500 return ''; 501 }; 502 503 if (data.photo) { 504 applyPhoto(data.photo); 505 } else { 506 fetchCityPhoto().then((photo) => { 507 if (!photo) return; 508 applyPhoto(photo); 509 data.photo = photo; 510 try { sessionStorage.setItem(cacheKey, JSON.stringify(data)); } catch (_) {} 511 }).catch(() => {}); 512 } 513 514 slide.querySelector('.ent-city__name').textContent = data.name; 515 slide.querySelector('.ent-city__temp').textContent = data.temp + '°'; 516 517 const condEl = slide.querySelector('.ent-city__cond'); 518 if (condEl && data.cond) { 519 condEl.textContent = data.cond; 520 condEl.hidden = false; 521 } 522 523 slidesWrap.appendChild(slide); 524 slides.push(slide); 525 526 const dotsWrap = widget.querySelector('.ent-city__dots'); 527 if (dotsWrap) { 528 const dot = document.createElement('button'); 529 dot.type = 'button'; 530 dot.setAttribute('aria-label', data.name); 531 const dotIndex = dots.length; 532 dot.addEventListener('click', () => { show(dotIndex); start(); }); 533 dotsWrap.appendChild(dot); 534 dots.push(dot); 535 } 536 537 updateClocks(); 538 show(slides.length - 1); // lead with the visitor's city 539 start(); 540 }; 541 542 addLocalCitySlide().catch(() => {}); 543 } 544})();
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.