PageSourceSearch

https://kebchi.fr/assets/js/accueil.js?v=20260703b

js kebchi.fr collected 2026-09-25 07:57:54 UTC 27,060 bytes, 763 lines download raw bytes

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.