1/* TOP 80 Radio â C'ÃTAIT QUOI ? AJAX-safe page controller */ 2(function () { 3 'use strict'; 4 5 function initCTKOI() { 6 7const page = document.querySelector('.top80-ctkoi-page'); 8 9 if (!page || page.dataset.ready === '1') { 10 return; 11 } 12 13 page.dataset.ready = '1'; 14 15 const searchForm = page.querySelector( 16 '#top80-ctkoi-search-form' 17 ); 18 const searchStatus = page.querySelector( 19 '#top80-ctkoi-search-status' 20 ); 21 const searchResults = page.querySelector( 22 '#top80-ctkoi-search-results' 23 ); 24 const fallbackCover = page.dataset.ctkoiFallback || ''; 25 const pageEndpoint = page.dataset.ctkoiEndpoint || window.location.href; 26 27 if (searchForm && searchStatus && searchResults) { 28 searchForm.addEventListener('submit', async function (event) { 29 event.preventDefault(); 30 31 const date = searchForm.querySelector( 32 '#top80-ctkoi-search-date' 33 ).value; 34 const time = searchForm.querySelector( 35 '#top80-ctkoi-search-time' 36 ).value; 37 38 if (!date || !time) { 39 return; 40 } 41 42 searchStatus.textContent = 'Recherche en coursâ¦'; 43 searchResults.replaceChildren(); 44 45 const endpoint = new URL(pageEndpoint, window.location.origin); 46 47 endpoint.searchParams.set('top80_ctkoi_history', '1'); 48 endpoint.searchParams.set('date', date); 49 endpoint.searchParams.set('time', time); 50 endpoint.searchParams.set('_', Date.now()); 51 52 try { 53 const response = await fetch(endpoint.toString(), { 54 cache: 'no-store', 55 credentials: 'same-origin' 56 }); 57 58 if (!response.ok) { 59 throw new Error('HTTP ' + response.status); 60 } 61 62 const json = await response.json(); 63 64 if (!json.success || !json.data) { 65 throw new Error( 66 json.data?.message || 'Réponse incorrecte.' 67 ); 68 } 69 70 if (!json.data.items.length) { 71 searchStatus.textContent = 72 'Aucun titre trouvé autour de cette heure.'; 73 return; 74 } 75 76 searchStatus.textContent = 77 json.data.items.length 78 + ' titre(s) trouvé(s) autour de ' 79 + json.data.time 80 + ' le ' 81 + json.data.date 82 + '.'; 83 84 json.data.items.forEach(function (item) { 85 const card = document.createElement('article'); 86 card.className = 87 'top80-ctkoi-item ' 88 + 'top80-ctkoi-search-result-item'; 89 90 const panelId = 91 'top80-ctkoi-search-details-' 92 + String(item.numero) 93 + '-' 94 + Math.random().toString(36).slice(2); 95 96 const summary = document.createElement('button'); 97 summary.type = 'button'; 98 summary.className = 'top80-ctkoi-summary'; 99 summary.setAttribute('aria-expanded', 'false'); 100 summary.setAttribute('aria-controls', panelId); 101 102 const timeBlock = document.createElement('span'); 103 timeBlock.className = 'top80-ctkoi-time'; 104 105 const clock = document.createElement('i'); 106 clock.className = 'fa-regular fa-clock'; 107 clock.setAttribute('aria-hidden', 'true'); 108 109 const timeText = document.createTextNode( 110 String(item.diffusion).slice(0, 5) 111 ); 112 113 timeBlock.append(clock, timeText); 114 115 const image = document.createElement('img'); 116 image.src = item.cover || fallbackCover; 117 image.alt = ''; 118 image.loading = 'lazy'; 119 image.addEventListener('error', function () { 120 image.src = fallbackCover; 121 }, { once: true }); 122 123 const cover = document.createElement('span'); 124 cover.className = 'top80-ctkoi-cover'; 125 cover.append(image); 126 127 const info = document.createElement('span'); 128 info.className = 'top80-ctkoi-track'; 129 const artist = document.createElement('strong'); 130 artist.textContent = item.artist; 131 const title = document.createElement('small'); 132 title.textContent = item.title; 133 info.append(artist, title); 134 135 const votes = document.createElement('span'); 136 votes.className = 'top80-ctkoi-votes'; 137 votes.innerHTML = 138 '<i class="fa-solid fa-heart" ' 139 + 'aria-hidden="true"></i>' 140 + '<b></b><small>voix</small>'; 141 votes.querySelector('b').textContent = 142 String(item.votes || 0); 143 144 const toggle = document.createElement('span'); 145 toggle.className = 'top80-ctkoi-toggle'; 146 toggle.setAttribute('aria-hidden', 'true'); 147 toggle.textContent = '+'; 148 149 summary.append( 150 timeBlock, 151 cover, 152 info, 153 votes, 154 toggle 155 ); 156 157 const details = document.createElement('div'); 158 details.id = panelId;
159 details.className = 'top80-ctkoi-details'; 160 details.hidden = true; 161 162 const detailsHeading = 163 document.createElement('div'); 164 detailsHeading.className = 165 'top80-ctkoi-details-heading'; 166 detailsHeading.innerHTML = 167 '<span aria-hidden="true">' 168 + '<i class="fa-solid fa-music"></i></span>' 169 + '<div><small>INFORMATIONS SUR LE TITRE</small>' 170 + '<h3></h3></div>'; 171 detailsHeading.querySelector('h3').textContent = 172 item.artist + ' â ' + item.title; 173 174 const biographies = 175 document.createElement('div'); 176 biographies.className = 177 'top80-ctkoi-biographies'; 178 biographies.dataset.biographies = ''; 179 biographies.dataset.artists = JSON.stringify( 180 item.biographyArtists || [item.artist] 181 ); 182 biographies.innerHTML = 183 '<p class="top80-ctkoi-bio-loading">' 184 + 'La biographie sera chargée ici.</p>'; 185 186 const actions = document.createElement('div'); 187 actions.className = 'top80-ctkoi-actions'; 188 189 function createVoteButton(type, label, iconClass) { 190 const button = document.createElement('button'); 191 button.type = 'button'; 192 button.className = 193 'top80-ctkoi-vote is-' + type; 194 button.dataset.vote = type; 195 button.dataset.numero = String(item.numero); 196 button.dataset.artiste = item.artist; 197 button.dataset.titre = item.title; 198 button.innerHTML = 199 '<i class="' + iconClass 200 + '" aria-hidden="true"></i>'; 201 button.append( 202 document.createTextNode(' ' + label) 203 ); 204 return button; 205 } 206 207 const plusButton = createVoteButton( 208 'plus', 209 'J\'AIME', 210 'fa-solid fa-heart' 211 ); 212 const minusButton = createVoteButton( 213 'minus', 214 'J\'AIME PAS', 215 'fa-solid fa-heart-crack' 216 ); 217 218 const buyLink = document.createElement('a'); 219 buyLink.className = 'top80-ctkoi-buy'; 220 buyLink.href = 221 'https://www.amazon.fr/s?k=' 222 + encodeURIComponent( 223 item.artist + ' ' + item.title 224 ).replace(/%20/g, '+') 225 + '&camp=1642&creative=6746'; 226 buyLink.target = '_blank'; 227 buyLink.rel = 'noopener'; 228 buyLink.innerHTML = 229 '<i class="fa-solid fa-cart-shopping" ' 230 + 'aria-hidden="true"></i> ACHETER'; 231 232 actions.append( 233 plusButton, 234 minusButton, 235 buyLink 236 ); 237 238 const notice = document.createElement('p'); 239 notice.className = 'top80-ctkoi-notice'; 240 notice.setAttribute('aria-live', 'polite'); 241 242 details.append( 243 detailsHeading, 244 biographies, 245 actions, 246 notice 247 ); 248 249 card.append(summary, details); 250 searchResults.append(card); 251 }); 252 } catch (error) { 253 searchStatus.textContent = 254 'Impossible d\'effectuer la recherche.'; 255 } 256 }); 257 } 258
259 async function loadBiographies(panel) { 260 const zone = panel.querySelector('[data-biographies]'); 261 262 if (!zone || zone.dataset.loaded === '1') { 263 return; 264 } 265 266 zone.dataset.loaded = '1'; 267 268 let artists = []; 269 270 try { 271 artists = JSON.parse(zone.dataset.artists || '[]'); 272 } catch (error) { 273 artists = []; 274 } 275 276 if (!artists.length) { 277 zone.replaceChildren(); 278 return; 279 } 280 281 const loading = document.createElement('p'); 282 loading.className = 'top80-ctkoi-bio-loading'; 283 loading.textContent = 'Chargement de la biographieâ¦'; 284 zone.replaceChildren(loading); 285 286 const requests = artists.map(async function (artist) { 287 const endpoint = new URL(pageEndpoint, window.location.origin); 288 289 endpoint.searchParams.set('top80_ctkoi_bio', '1'); 290 endpoint.searchParams.set('artist', artist); 291 endpoint.searchParams.set('_', Date.now()); 292 293 const response = await fetch(endpoint.toString(), { 294 cache: 'no-store', 295 credentials: 'same-origin' 296 }); 297 298 if (!response.ok) { 299 throw new Error('HTTP ' + response.status); 300 } 301 302 return response.json(); 303 }); 304 305 try { 306 const responses = await Promise.all(requests); 307 zone.replaceChildren(); 308 309 responses.forEach(function (response, index) { 310 const data = response.success 311 ? response.data 312 : null; 313 314 const card = document.createElement('article'); 315 card.className = 'top80-ctkoi-bio'; 316 317 const title = document.createElement('h4'); 318 title.textContent = data?.artist || artists[index]; 319 card.append(title); 320 321 const excerpt = document.createElement('p'); 322 323 if (data?.found) { 324 excerpt.textContent = data.excerpt; 325 card.append(excerpt); 326 327 const link = document.createElement('a'); 328 link.href = data.url; 329 link.textContent = 'LIRE LA BIOGRAPHIE âº'; 330 card.append(link); 331 } else { 332 excerpt.textContent = 333 'Biographie prochainement disponible.'; 334 card.append(excerpt); 335 } 336 337 zone.append(card); 338 }); 339 } catch (error) { 340 loading.textContent = 341 'La biographie est momentanément indisponible.'; 342 } 343 } 344 345 page.addEventListener('click', async function (event) { 346 const summary = event.target.closest('.top80-ctkoi-summary'); 347 348 if (summary) { 349 const panel = document.getElementById( 350 summary.getAttribute('aria-controls') 351 ); 352 353 if (!panel) { 354 return; 355 } 356 357 const isOpen = summary.getAttribute('aria-expanded') === 'true'; 358 summary.setAttribute('aria-expanded', String(!isOpen)); 359 360 if (isOpen) { 361 panel.classList.remove('is-open'); 362 363 window.setTimeout(function () { 364 if (!panel.classList.contains('is-open')) { 365 panel.hidden = true; 366 } 367 }, 360); 368 } else { 369 panel.hidden = false; 370 371 window.requestAnimationFrame(function () { 372 panel.classList.add('is-open'); 373 }); 374 375 loadBiographies(panel); 376 } 377 378 return; 379 } 380 381 const voteButton = event.target.closest('.top80-ctkoi-vote'); 382 383 if (!voteButton || voteButton.disabled) { 384 return; 385 } 386 387 const details = voteButton.closest('.top80-ctkoi-details'); 388 const notice = details?.querySelector('.top80-ctkoi-notice'); 389 390 voteButton.disabled = true; 391 392 const params = new URLSearchParams({ 393 type: voteButton.dataset.vote, 394 numero: voteButton.dataset.numero, 395 artiste: voteButton.dataset.artiste, 396 titre: voteButton.dataset.titre 397 }); 398 399 try { 400 const response = await fetch( 401 '/api/vote-proxy.php?' + params.toString(), 402 { 403 cache: 'no-store', 404 credentials: 'same-origin' 405 } 406 ); 407 408 const result = (await response.text()).trim(); 409 410 if (result === 'vote_ok') { 411 notice.textContent = 'Merci, votre vote est enregistré !'; 412 notice.className = 413 'top80-ctkoi-notice is-success'; 414 415 details 416 .closest('.top80-ctkoi-item') 417 ?.querySelectorAll('.top80-ctkoi-vote') 418 .forEach(function (button) { 419 button.disabled = true; 420 }); 421 } else if (result === 'already_voted') { 422 notice.textContent = 423 'Vous avez déjà voté aujourd\'hui pour ce titre.'; 424 notice.className = 425 'top80-ctkoi-notice is-error'; 426 voteButton.disabled = false;
427 } else { 428 throw new Error('Réponse incorrecte'); 429 } 430 } catch (error) { 431 notice.textContent = 432 'Impossible d\'enregistrer votre vote.'; 433 notice.className = 'top80-ctkoi-notice is-error'; 434 voteButton.disabled = false; 435 } 436 }); 437 } 438 439 document.addEventListener('DOMContentLoaded', initCTKOI); 440 document.addEventListener('top80:page-loaded', function (event) { 441 if (event && event.detail && event.detail.url) { 442 var main = document.querySelector('.top80-ctkoi-page'); 443 if (main) { 444 initCTKOI(); 445 } 446 } 447 }); 448})();
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.