1'use strict'; 2 3const homeThrottle = (fn, wait = 150) => { 4 let lastCall = 0; 5 let timeoutId = null; 6 return (...args) => { 7 const now = Date.now(); 8 if (now - lastCall >= wait) { 9 lastCall = now; 10 fn(...args); 11 } else { 12 // Trailing call: garantit que l'état final est toujours appliqué 13 clearTimeout(timeoutId); 14 timeoutId = setTimeout(() => { 15 lastCall = Date.now(); 16 fn(...args); 17 }, wait - (now - lastCall)); 18 } 19 }; 20}; 21 22const loadScriptOnce = (src) => new Promise((resolve, reject) => { 23 if (document.querySelector(`script[src="${src}"]`)) { 24 resolve(); 25 return; 26 } 27 const s = document.createElement('script'); 28 s.src = src; 29 s.async = true; 30 s.defer = true; 31 s.onload = () => resolve(); 32 s.onerror = (e) => reject(e); 33 document.head.appendChild(s); 34}); 35 36const initSwiperInstances = () => { 37 if (typeof Swiper === 'undefined') { 38 return {}; 39 } 40 41 const mainSliderEl = document.querySelector('.main-slider'); 42 const actionSliderEl = document.querySelector('.action-slider'); 43 const reviewsSliderEl = document.querySelector('.reviews-slider'); 44 const gallerySliderEl = document.querySelector('.gallery-slider'); 45 46 const mainSlider = mainSliderEl 47 ? new Swiper(mainSliderEl, { 48 loop: true, 49 autoplay: { delay: 3000, disableOnInteraction: false }, 50 pagination: { el: '.swiper-pagination', clickable: true }, 51 navigation: { 52 nextEl: '.swiper-button-next', 53 prevEl: '.swiper-button-prev', 54 }, 55 }) 56 : null; 57 58 const actionSlider = actionSliderEl 59 ? new Swiper(actionSliderEl, { 60 loop: true, 61 loopAdditionalSlides: 2, 62 slidesPerView: 1.1, 63 spaceBetween: 15, 64 autoplay: { delay: 3000, disableOnInteraction: false }, 65 pagination: { el: '.swiper-pagination', clickable: true }, 66 breakpoints: { 67 640: { slidesPerView: 2.2, spaceBetween: 20 }, 68 1024: { slidesPerView: 4, spaceBetween: 24 }, 69 }, 70 }) 71 : null; 72 73 const reviewsSlider = reviewsSliderEl 74 ? new Swiper(reviewsSliderEl, { 75 loop: true, 76 loopAdditionalSlides: 2, 77 watchOverflow: false, 78 slidesPerView: 1.05, 79 slidesPerGroup: 1, 80 spaceBetween: 18, 81 centeredSlides: false, 82 autoplay: { delay: 3000, disableOnInteraction: false }, 83 pagination: { 84 el: '.swiper-pagination', 85 clickable: true, 86 }, 87 breakpoints: { 88 768: { slidesPerView: 2.1, spaceBetween: 18 }, 89 1024: { slidesPerView: 3, spaceBetween: 20 }, 90 }, 91 }) 92 : null; 93 94 const gallerySlider = gallerySliderEl 95 ? new Swiper(gallerySliderEl, { 96 loop: true, 97 loopAdditionalSlides: 2, 98 slidesPerView: 1.05, 99 spaceBetween: 16, 100 autoplay: { delay: 3000, disableOnInteraction: false }, 101 pagination: { el: '.swiper-pagination', clickable: true }, 102 breakpoints: { 103 768: { slidesPerView: 2, spaceBetween: 20 }, 104 1024: { slidesPerView: 4, spaceBetween: 24 }, 105 }, 106 }) 107 : null; 108 109 return { mainSlider, actionSlider, reviewsSlider, gallerySlider }; 110}; 111 112const initActionTabs = (actionSlider) => { 113 const inlineTabBtns = document.querySelectorAll('[data-filter-tabs] .tab-btn'); 114 if (!inlineTabBtns.length) return; 115 116 const tabBtns = Array.from(document.querySelectorAll('.tab-btn')); 117 const filterWrapper = document.querySelector('[data-filter]'); 118 const filterViewport = document.querySelector('[data-filter-viewport]'); 119 const filterTabsContainer = document.querySelector('[data-filter-tabs]'); 120 const prevBtn = document.querySelector('[data-filter-prev]'); 121 const nextBtn = document.querySelector('[data-filter-next]'); 122 const tabMoreBtn = document.getElementById('tabMoreBtn');
123 const sliderEl = document.querySelector('.action-slider'); 124 const sliderPrevControl = document.querySelector('[data-action-prev]'); 125 const sliderNextControl = document.querySelector('[data-action-next]'); 126 const slideCards = document.querySelectorAll('.swiper-slide[data-link]'); 127 let extrasVisible = false; 128 const DESKTOP_WIDE_BREAKPOINT = 1760; 129 const MIN_VISIBLE_TABS_DESKTOP = 6; 130 const getExtraTabs = () => Array.from(document.querySelectorAll('.tab-btn.extra-tab')); 131 132 tabBtns.forEach((btn) => { 133 if (btn.classList.contains('extra-tab')) { 134 btn.dataset.permanentExtra = '1'; 135 } 136 }); 137 138 const autoHideCandidates = tabBtns.filter( 139 (btn) => !btn.dataset.permanentExtra && btn.dataset.target !== 'all' 140 ); 141 142 const tabTargetToCategory = { 143 mouton: 'mouton', 144 naissance: 'aqiqa', 145 puits: 'puits', 146 fidya: 'fidya', 147 arbre: 'arbre', 148 orphelin: 'orphelin', 149 colis: 'colis', 150 cheveux: 'cheveux', 151 aqiqa: 'aqiqa', 152 sadaqa: 'sadaqa', 153 }; 154 155 const scrollInlineButtonIntoView = (btn) => { 156 if (!filterViewport) return; 157 158 if (window.innerWidth < 1024) { 159 const tabRect = btn.getBoundingClientRect(); 160 const viewportRect = filterViewport.getBoundingClientRect(); 161 const offset = 162 filterViewport.scrollLeft + 163 tabRect.left - 164 viewportRect.left - 165 viewportRect.width / 2 + 166 tabRect.width / 2; 167 168 filterViewport.scrollTo({ 169 left: Math.max(offset, 0), 170 behavior: 'smooth', 171 }); 172 return; 173 } 174 175 const buttonLeft = btn.offsetLeft; 176 const buttonRight = buttonLeft + btn.offsetWidth; 177 const viewportLeft = filterViewport.scrollLeft; 178 const viewportRight = viewportLeft + filterViewport.clientWidth; 179 if (buttonLeft < viewportLeft || buttonRight > viewportRight) { 180 const offset = buttonLeft - filterViewport.clientWidth / 2 + btn.offsetWidth / 2; 181 filterViewport.scrollTo({ left: Math.max(offset, 0), behavior: 'smooth' }); 182 } 183 }; 184 185 const resetAutoHiddenTabs = () => { 186 autoHideCandidates.forEach((btn) => { 187 if (btn.dataset.autoManaged === '1') { 188 btn.classList.remove('extra-tab', 'auto-hidden', 'hidden'); 189 delete btn.dataset.autoManaged; 190 } 191 }); 192 }; 193 194 const hideAutoTab = (btn) => { 195 btn.dataset.autoManaged = '1'; 196 btn.classList.add('extra-tab', 'auto-hidden'); 197 if (!extrasVisible) { 198 btn.classList.add('hidden'); 199 } else { 200 btn.classList.remove('hidden'); 201 } 202 }; 203 204 const ensureTabsFit = () => { 205 if (!filterViewport || !filterTabsContainer || extrasVisible) return; 206 resetAutoHiddenTabs(); 207 const candidates = autoHideCandidates.filter((btn) => btn.dataset.autoManaged !== '1').reverse(); 208 let overflow = filterTabsContainer.scrollWidth - filterViewport.clientWidth; 209 while (overflow > 2 && candidates.length) { 210 const btn = candidates.shift(); 211 hideAutoTab(btn); 212 overflow = filterTabsContainer.scrollWidth - filterViewport.clientWidth; 213 } 214 }; 215 216 slideCards.forEach((slide) => { 217 slide.addEventListener('click', (event) => { 218 if (event.target.closest('a, button')) { 219 return; 220 } 221 const link = slide.dataset.link; 222 if (link) { 223 window.location.href = link; 224 } 225 }); 226 }); 227 228 let currentFilterTarget = 'all'; 229 230 const filterSlides = (target) => { 231 if (!actionSlider) return; 232 const slides = Array.from(actionSlider.slides || []); 233 let firstVisibleIndex = null; 234 const isAll = target === 'all'; 235 236 slides.forEach((slide, index) => { 237 const shouldShow = isAll || slide.dataset.category === target; 238 slide.classList.toggle('slide-hidden', !shouldShow); 239 if (shouldShow && firstVisibleIndex === null) { 240 firstVisibleIndex = index; 241 } 242 }); 243 244 sliderEl?.classList.toggle('is-filtered', !isAll); 245 actionSlider.params.allowTouchMove = true; 246 actionSlider.updateSlides(); 247 actionSlider.updateSlidesClasses(); 248 actionSlider.updateProgress(); 249 actionSlider.updateActiveIndex(); 250 actionSlider.loopFix(); 251 252 if (firstVisibleIndex !== null) { 253 actionSlider.slideToLoop(firstVisibleIndex, 500); 254 } else { 255 actionSlider.slideTo(0, 0); 256 } 257 }; 258 259 const setActiveTab = (btn) => { 260 tabBtns.forEach((b) => { 261 const isActive = b === btn; 262 b.classList.toggle('bg-primary', isActive); 263 b.classList.toggle('text-white', isActive); 264 b.classList.toggle('active', isActive); 265 b.classList.toggle('bg-secondary', !isActive); 266 b.classList.toggle('text-primary', !isActive); 267 }); 268 269 scrollInlineButtonIntoView(btn); 270 271 const rawTarget = btn.dataset.target || 'all'; 272 const filterTarget = tabTargetToCategory[rawTarget] || rawTarget; 273 currentFilterTarget = filterTarget; 274 filterSlides(filterTarget); 275 276 }; 277 278 tabBtns.forEach((btn) => { 279 btn.addEventListener('click', () => setActiveTab(btn)); 280 }); 281
282 const slideWithinFilter = (direction = 'next') => { 283 if (!actionSlider) return; 284 const slides = Array.from(actionSlider.slides || []); 285 if (!slides.length) return; 286 287 const total = slides.length; 288 const step = direction === 'prev' ? -1 : 1; 289 let index = actionSlider.activeIndex || 0; 290 291 for (let i = 0; i < total; i += 1) { 292 index = (index + step + total) % total; 293 const slide = slides[index]; 294 if (!slide || slide.classList.contains('slide-hidden')) continue; 295 296 const targetAttr = slide.getAttribute('data-swiper-slide-index'); 297 const targetIndex = Number.isNaN(Number(targetAttr)) ? index : Number(targetAttr); 298 actionSlider.slideToLoop(targetIndex, 500); 299 break; 300 } 301 }; 302 303 const applyExtraTabVisibility = () => { 304 getExtraTabs().forEach((tab) => { 305 const keepVisible = tab.dataset.forceVisible === '1'; 306 tab.classList.toggle('hidden', !extrasVisible && !keepVisible); 307 }); 308 }; 309 310 const updateWideScreenExtras = () => { 311 const isWide = window.matchMedia(`(min-width: ${DESKTOP_WIDE_BREAKPOINT}px)`).matches; 312 const baseTabsCount = tabBtns.filter((btn) => !btn.classList.contains('extra-tab')).length; 313 const extraNeeded = Math.max(0, MIN_VISIBLE_TABS_DESKTOP - baseTabsCount); 314 const extraTabs = getExtraTabs(); 315 extraTabs.forEach((tab, index) => { 316 if (isWide && index < extraNeeded) { 317 tab.dataset.forceVisible = '1'; 318 } else if (tab.dataset.forceVisible) { 319 delete tab.dataset.forceVisible; 320 } 321 }); 322 applyExtraTabVisibility(); 323 }; 324 325 const toggleExtras = (shouldShow) => { 326 extrasVisible = typeof shouldShow === 'boolean' ? shouldShow : !extrasVisible; 327 applyExtraTabVisibility(); 328 filterWrapper?.classList.toggle('filters-expanded', extrasVisible); 329 const icon = tabMoreBtn?.querySelector('i'); 330 if (icon) { 331 icon.classList.toggle('ph-plus', !extrasVisible); 332 icon.classList.toggle('ph-minus', extrasVisible); 333 } 334 if (extrasVisible && filterViewport) { 335 filterViewport.scrollTo({ left: 0, behavior: 'smooth' }); 336 } 337 if (!extrasVisible) { 338 ensureTabsFit(); 339 } 340 updateFilterScroll(); 341 }; 342 343 tabMoreBtn?.addEventListener('click', () => toggleExtras()); 344 345 const updateFilterScroll = () => { 346 if (!filterViewport || !filterWrapper) return; 347 const maxScroll = Math.max(filterViewport.scrollWidth - filterViewport.clientWidth, 0); 348 const hasOverflow = maxScroll > 1; 349 if (prevBtn) prevBtn.disabled = !hasOverflow || filterViewport.scrollLeft <= 1; 350 if (nextBtn) nextBtn.disabled = !hasOverflow || filterViewport.scrollLeft >= maxScroll - 1; 351 filterWrapper.classList.toggle('has-left-shadow', hasOverflow && filterViewport.scrollLeft > 2); 352 filterWrapper.classList.toggle('has-right-shadow', hasOverflow && filterViewport.scrollLeft < maxScroll - 2); 353 filterWrapper.classList.toggle('has-overflow', hasOverflow); 354 }; 355 356 if (filterViewport) { 357 filterViewport.addEventListener('scroll', homeThrottle(updateFilterScroll, 100)); 358 window.addEventListener( 359 'resize', 360 homeThrottle(() => { 361 ensureTabsFit(); 362 updateFilterScroll(); 363 updateWideScreenExtras(); 364 }, 200) 365 ); 366 filterViewport.addEventListener( 367 'wheel', 368 (event) => { 369 if (Math.abs(event.deltaY) <= Math.abs(event.deltaX) || event.deltaY === 0) return; 370 filterViewport.scrollBy({ left: event.deltaY, behavior: 'auto' }); 371 event.preventDefault(); 372 }, 373 { passive: false } 374 ); 375 ensureTabsFit(); 376 updateFilterScroll(); 377 updateWideScreenExtras(); 378 } else { 379 updateWideScreenExtras(); 380 } 381 382 prevBtn?.addEventListener('click', () => { 383 if (!filterViewport) return; 384 filterViewport.scrollBy({ left: -filterViewport.clientWidth * 0.7, behavior: 'smooth' }); 385 }); 386 nextBtn?.addEventListener('click', () => { 387 if (!filterViewport) return; 388 filterViewport.scrollBy({ left: filterViewport.clientWidth * 0.7, behavior: 'smooth' }); 389 }); 390 391 sliderPrevControl?.addEventListener('click', () => { 392 slideWithinFilter('prev'); 393 }); 394 sliderNextControl?.addEventListener('click', () => { 395 slideWithinFilter('next'); 396 }); 397 398 toggleExtras(false); 399 setActiveTab(tabBtns[0]); 400}; 401 402const initPieChart = () => { 403 const canvasEl = document.getElementById('myPieChart'); 404 if (!canvasEl) return; 405 406 const labels = ['Don libre', 'Arbre', 'Puit', 'Colis alimentaires', 'Soutien aux orphelins']; 407 const colors = ['#fbbf24', '#4ade80', '#3b82f6', '#f43f5e', '#f97316']; 408 409 const ranges = labels.map((_, i) => document.getElementById(`range${i}`)); 410 const txts = labels.map((_, i) => document.getElementById(`txt${i}`)); 411 const totalDisplay = document.getElementById('totalDisplay'); 412 413 if (!ranges.every(r => r)) return; 414 415 const startChart = () => { 416 const getValues = () => ranges.map(r => { 417 const v = parseInt(r.value, 10); 418 return Number.isNaN(v) ? 0 : Math.max(0, Math.min(100, v)); 419 }); 420 421 const ctx = canvasEl.getContext('2d'); 422 const myChart = new Chart(ctx, { 423 type: 'pie', 424 data: { 425 labels, 426 datasets: [{ 427 data: getValues(), 428 backgroundColor: colors, 429 borderWidth: 2, 430 borderColor: '#ffffff', 431 hoverOffset: 4 432 }] 433 }, 434 options: { 435 responsive: true, 436 maintainAspectRatio: true, 437 plugins: { 438 legend: { display: false }, 439 tooltip: { enabled: true } 440 }, 441 layout: { padding: 0 }, 442 animation: { duration: 300 } 443 } 444 }); 445
446 ranges.forEach((range, i) => { 447 range.addEventListener('input', () => { 448 const values = getValues(); 449 txts[i].innerText = values[i] + '%'; 450 451 const total = values.reduce((s, v) => s + v, 0); 452 if (totalDisplay) { 453 totalDisplay.innerText = 'Total : ' + total + '%'; 454 } 455 456 myChart.data.datasets[0].data = values; 457 myChart.update(); 458 }); 459 }); 460 }; 461 462 if (typeof Chart === 'undefined') { 463 loadScriptOnce('https://cdn.jsdelivr.net/npm/chart.js') 464 .then(startChart) 465 .catch(() => {}); 466 } else { 467 startChart(); 468 } 469}; 470 471const initDonationsTicker = () => { 472 const tickerBodies = document.querySelectorAll('[data-donations-ticker]'); 473 if (!tickerBodies.length) return; 474 if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return; 475 476 tickerBodies.forEach((tickerBody) => { 477 const scrollArea = tickerBody.closest('[data-donation-table]'); 478 if (!scrollArea || tickerBody.children.length < 2) return; 479 480 let intervalId = null; 481 let resetTimeout = null; 482 let isPaused = false; 483 let isAnimating = false; 484 const ROTATION_DELAY = 2600; 485 const ANIMATION_DURATION = 650; 486 487 const cleanupAnimation = () => { 488 if (resetTimeout) { 489 clearTimeout(resetTimeout); 490 resetTimeout = null; 491 } 492 tickerBody.style.transition = 'none'; 493 tickerBody.style.transform = 'translateY(0)'; 494 isAnimating = false; 495 }; 496 497 const cycleRows = () => { 498 if (isPaused || isAnimating) return; 499 const firstRow = tickerBody.querySelector('tr'); 500 if (!firstRow) return; 501 const rowHeight = firstRow.getBoundingClientRect().height || 0; 502 if (!rowHeight) return; 503 isAnimating = true; 504 tickerBody.style.transition = 'transform 0.6s ease'; 505 tickerBody.style.transform = `translateY(-${rowHeight}px)`; 506 507 resetTimeout = window.setTimeout(() => { 508 tickerBody.appendChild(firstRow); 509 cleanupAnimation(); 510 }, ANIMATION_DURATION); 511 }; 512 513 const start = () => { 514 if (intervalId || isPaused) return; 515 intervalId = window.setInterval(cycleRows, ROTATION_DELAY); 516 }; 517 518 const stop = () => { 519 isPaused = true; 520 if (intervalId) { 521 clearInterval(intervalId); 522 intervalId = null; 523 } 524 cleanupAnimation(); 525 }; 526 527 const resume = () => { 528 if (!isPaused) return; 529 isPaused = false; 530 start(); 531 }; 532 533 scrollArea.addEventListener('mouseenter', stop); 534 scrollArea.addEventListener('mouseleave', resume); 535 scrollArea.addEventListener('focusin', stop); 536 scrollArea.addEventListener('focusout', resume); 537 538 start(); 539 window.setTimeout(cycleRows, 1200); 540 }); 541}; 542 543const initFinalCta = () => { 544 const container = document.querySelector('[data-final-cta]'); 545 if (!container) return; 546 const tags = Array.from(container.querySelectorAll('.final-cta-tag')); 547 const ctaButton = container.querySelector('#finalCtaButton'); 548 if (!tags.length || !ctaButton) return; 549 550 const setActiveTag = (tag) => {
551 tags.forEach((btn) => { 552 const isActive = btn === tag; 553 btn.classList.toggle('final-cta-tag--active', isActive); 554 btn.setAttribute('aria-selected', String(isActive)); 555 }); 556 557 const newText = tag.dataset.ctaText; 558 const newLink = tag.dataset.ctaLink; 559 if (newText) { 560 ctaButton.textContent = newText; 561 } 562 if (newLink) { 563 ctaButton.setAttribute('href', newLink); 564 } 565 }; 566 567 tags.forEach((tag) => { 568 tag.addEventListener('click', () => setActiveTag(tag)); 569 }); 570 571 const defaultTag = 572 tags.find((btn) => btn.classList.contains('final-cta-tag--active')) || tags[0]; 573 if (defaultTag) { 574 setActiveTag(defaultTag); 575 } 576}; 577 578const initServicesGrid = () => { 579 const grid = document.querySelector('[data-services-grid]'); 580 if (!grid) return; 581 const toggle = grid.querySelector('[data-services-toggle]'); 582 if (!toggle) return; 583 const labelEl = toggle.querySelector('[data-toggle-label]'); 584 const descEl = toggle.querySelector('[data-toggle-desc]'); 585 586 const updateTexts = (expanded) => { 587 if (labelEl) { 588 labelEl.textContent = expanded ? 'Voir moins' : 'En savoir plus'; 589 } 590 if (descEl) { 591 descEl.textContent = expanded 592 ? 'Masquer les autres missions.' 593 : 'Explorez davantage nos services pour aider.'; 594 } 595 toggle.setAttribute('aria-expanded', expanded ? 'true' : 'false'); 596 }; 597 598 toggle.addEventListener('click', () => { 599 const isExpanded = grid.classList.toggle('services-grid--expanded'); 600 updateTexts(isExpanded); 601 }); 602}; 603 604// Header scroll effect for home page 605const initHomeHeaderScroll = () => { 606 // Only run on home page 607 if (!document.body.classList.contains('home-page')) return; 608 609 const header = document.getElementById('header'); 610 if (!header) return; 611 612 const logoImg = header.querySelector('.legacy-logo img'); 613 const logoWhite = 'https://cdn.kebchi.fr/site_kebchi/general/kebchi-logo.svg'; 614 const logoColor = 'https://cdn.kebchi.fr/site_kebchi/general/kebchi-logo-v.webp'; 615 let isScrolled = false; 616 617 const handleScroll = () => { 618 const shouldBeScrolled = window.scrollY > 10; 619 620 // Ãviter les updates inutiles 621 if (shouldBeScrolled === isScrolled) return; 622 isScrolled = shouldBeScrolled; 623 624 if (isScrolled) { 625 header.classList.add('is-scrolled'); 626 // Force inline styles for scrolled state (avec !important) 627 header.style.setProperty('background', '#ffffff', 'important'); 628 header.style.setProperty('background-color', '#ffffff', 'important'); 629 header.style.setProperty('box-shadow', '0 4px 6px -1px rgba(0,0,0,0.1)', 'important'); 630 // Logo coloré 631 if (logoImg) { 632 logoImg.src = logoColor; 633 logoImg.style.setProperty('filter', 'none', 'important'); 634 } 635 } else { 636 header.classList.remove('is-scrolled'); 637 // Force inline styles for transparent state (avec !important) 638 header.style.setProperty('background', 'transparent', 'important'); 639 header.style.setProperty('background-color', 'transparent', 'important'); 640 header.style.setProperty('box-shadow', 'none', 'important'); 641 // Logo blanc (SVG avec filtre) 642 if (logoImg) { 643 logoImg.src = logoWhite; 644 logoImg.style.setProperty('filter', 'brightness(0) invert(1)', 'important'); 645 } 646 } 647 }; 648 649 // Initial check 650 handleScroll(); 651 652 // Listen to scroll events (throttled) 653 window.addEventListener('scroll', homeThrottle(handleScroll, 50), { passive: true }); 654}; 655 656// Hero Fidya calculator interactivity 657const initHeroCalculator = () => { 658 const PRICE_PER_DAY = 7; 659 let days = 30; 660 661 const daysValueEl = document.getElementById('days-value'); 662 const totalEl = document.getElementById('fidya-total'); 663 const btnMinus = document.getElementById('btn-minus'); 664 const btnPlus = document.getElementById('btn-plus'); 665 666 if (!daysValueEl || !totalEl || !btnMinus || !btnPlus) return; 667 668 const updateTotal = () => { 669 const total = days * PRICE_PER_DAY; 670 daysValueEl.value = days; 671 totalEl.textContent = total + '\u20AC'; 672 }; 673 674 btnMinus.addEventListener('click', () => { 675 if (days > 1) { 676 days--; 677 updateTotal(); 678 } 679 }); 680 681 btnPlus.addEventListener('click', () => { 682 if (days < 365) { 683 days++; 684 updateTotal(); 685 } 686 }); 687 688 // Allow manual input 689 daysValueEl.addEventListener('input', () => { 690 let val = parseInt(daysValueEl.value) || 1; 691 if (val < 1) val = 1; 692 if (val > 365) val = 365; 693 days = val; 694 totalEl.textContent = (days * PRICE_PER_DAY) + '\u20AC'; 695 }); 696 697 daysValueEl.addEventListener('blur', () => { 698 daysValueEl.value = days; 699 }); 700}; 701 702document.addEventListener('DOMContentLoaded', () => { 703 const { actionSlider } = initSwiperInstances(); 704 initActionTabs(actionSlider); 705 initReveal(); 706 initBackToTop(); 707 initPieChart(); 708 initHomeHeaderScroll(); 709 initHeroCalculator(); 710 initDonationsTicker(); 711 initFinalCta(); 712 initServicesGrid(); 713 714 const rightHintBtn = document.querySelector('.mobile-scroll-hint--right'); 715 const leftHintBtn = document.querySelector('.mobile-scroll-hint--left'); 716 const filterViewport = document.querySelector('[data-filter-viewport]'); 717 const filterContainer = document.querySelector('.actions-filter'); 718 719 if (filterViewport && (rightHintBtn || leftHintBtn)) { 720 rightHintBtn?.addEventListener('click', () => { 721 filterViewport.scrollBy({ left: 160, behavior: 'smooth' }); 722 }); 723 leftHintBtn?.addEventListener('click', () => { 724 filterViewport.scrollBy({ left: -160, behavior: 'smooth' }); 725 }); 726 727 const SCROLL_TOLERANCE = 12; 728 729 const setHintState = (btn, visible) => { 730 if (!btn) return; 731 btn.style.opacity = visible ? '1' : '0'; 732 btn.style.pointerEvents = visible ? 'auto' : 'none'; 733 btn.hidden = !visible; 734 }; 735 736 const updateHintVisibility = () => { 737 const maxScroll = Math.max(filterViewport.scrollWidth - filterViewport.clientWidth - 10, 0); 738 if (maxScroll <= 0) { 739 setHintState(rightHintBtn, false); 740 setHintState(leftHintBtn, false); 741 if (filterContainer) { 742 filterContainer.classList.add('no-right-hint', 'no-left-hint'); 743 } 744 return; 745 }
746 const scrollLeft = filterViewport.scrollLeft; 747 const canScrollRight = scrollLeft < (maxScroll - SCROLL_TOLERANCE); 748 const canScrollLeft = scrollLeft > SCROLL_TOLERANCE; 749 750 setHintState(rightHintBtn, canScrollRight); 751 setHintState(leftHintBtn, canScrollLeft); 752 753 if (filterContainer) { 754 filterContainer.classList.toggle('no-right-hint', !canScrollRight); 755 filterContainer.classList.toggle('no-left-hint', !canScrollLeft); 756 } 757 }; 758 759 filterViewport.addEventListener('scroll', homeThrottle(updateHintVisibility, 100)); 760 window.addEventListener('resize', homeThrottle(updateHintVisibility, 150)); 761 updateHintVisibility(); 762 } 763});
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.