// Home page specific JS: team carousel + modal + portfolio preview + partners + konami document.addEventListener('DOMContentLoaded', () => { // ── Portfolio preview — même cartes que la page réalisations ────────────────── const homePortfolioGrid = document.getElementById('home-portfolio-grid'); if (homePortfolioGrid) { fetch('assets/data/projects.json') .then(res => { if (!res.ok) throw new Error(`HTTP ${res.status}: ${res.statusText}`); return res.json(); }) .then(projects => { // Garde-fou: vérifier que projects est un tableau if (!Array.isArray(projects)) { console.error('[Portfolio] Données invalides: projects n’est pas un tableau', projects); throw new Error('Invalid projects data format'); } const first3 = projects.slice(0, 3); const delayClasses = ['', ' reveal-delay-1', ' reveal-delay-2']; const categoryLabels = { bot: 'Bot Discord', map: 'Fortnite Map', graphisme: 'Graphisme' }; homePortfolioGrid.innerHTML = first3.map((p, i) => { const firstFile = Array.isArray(p.files) && p.files.length > 0 ? p.files[0] : null; const imgSrc = firstFile ? window.escapeHTML((p.images || '') + firstFile) : null; const safeTitle = window.escapeHTML(p.title); const safeCategory = window.escapeHTML(categoryLabels[p.category] || p.category); return `
${imgSrc ? `${safeTitle}` : ''}
${safeCategory}

${safeTitle}

`; }).join(''); setTimeout(() => { homePortfolioGrid.querySelectorAll('.reveal').forEach(el => el.classList.add('active')); if (window.feather && feather.replace) try { feather.replace(); } catch(e) {} }, 80); // Délégation globale sur document — fonctionne même si realisations.js // se charge après home.js (ordre des scripts defer non garanti) homePortfolioGrid.addEventListener('click', e => { const item = e.target.closest('.portfolio-item[data-project-id]'); if (!item) return; const id = item.getAttribute('data-project-id'); if (typeof window.openProjectModal === 'function') { window.openProjectModal(id); } else { // Retry : realisations.js pas encore initialisé, on attend const wait = setInterval(() => { if (typeof window.openProjectModal === 'function') { clearInterval(wait); window.openProjectModal(id); } }, 50); // Abandon après 3 secondes setTimeout(() => clearInterval(wait), 3000); } }); }) .catch(err => { console.error('[Portfolio] Échec du chargement des projets:', err); // Fallback: afficher un message d'erreur homePortfolioGrid.innerHTML = ' '; }); } // ── Partners — carousel + modal ───────────────────────────────────────────── const partnersTrack = document.getElementById('partners-track'); const partnerModalOverlay = document.getElementById('partner-modal-overlay'); if (partnersTrack) { fetch('assets/data/partners.json') .then(res => { if (!res.ok) throw new Error(`HTTP ${res.status}: ${res.statusText}`); return res.json(); }) .then(partners => { // Garde-fou: vérifier que partners est un tableau if (!Array.isArray(partners)) { console.error('[Partners] Données invalides: partners n’est pas un tableau', partners); throw new Error('Invalid partners data format'); } if (!partners.length) { const section = document.getElementById('partenaires'); if (section) section.style.display = 'none'; return; } // Générer les slides (tripler pour garantir boucle sans trou) const copies = [...partners, ...partners, ...partners]; partnersTrack.innerHTML = copies.map((p, i) => { const imgSrc = window.escapeHTML(p.logo || `assets/images/partners/${p.name}.png`); const safeName = window.escapeHTML(p.displayName); const safeGradient = window.escapeHTML(p.gradient || ''); const safeInitials = window.escapeHTML(p.displayName.substring(0,2).toUpperCase()); return `
${safeName} ${safeName}
`; }).join(''); // Défilement infini — loopWidth mesuré une seule fois après rendu complet const track = partnersTrack; let isPaused = false; let scrollPos = 0; let loopWidth = 0; let animFrame; const SPEED = 0.6; function measureLoop() { // Mesure la largeur exacte d'un bloc (1/3 des slides) // On utilise offsetWidth de chaque slide réel + gap pour éviter les arrondis de scrollWidth const slides = Array.from(track.querySelectorAll('.partner-slide')); const perGroup = partners.length; if (slides.length < perGroup) return 0; // Largeur d'un groupe = somme des (offsetWidth + gap) des premiers slides const style = window.getComputedStyle(track); const gap = parseFloat(style.gap) || 24; let w = 0; for (let i = 0; i < perGroup; i++) { w += slides[i].offsetWidth + gap; } return Math.round(w); } function autoScroll() { if (!isPaused) { scrollPos += SPEED; if (loopWidth > 0 && scrollPos >= loopWidth) { scrollPos -= loopWidth; } track.style.transform = `translateX(-${scrollPos}px)`; } animFrame = requestAnimationFrame(autoScroll); } // Attendre que les slides soient rendues et mesurées requestAnimationFrame(() => { requestAnimationFrame(() => { loopWidth = measureLoop(); animFrame = requestAnimationFrame(autoScroll); }); }); // Re-mesurer au resize pour rester précis let resizeTO; window.addEventListener('resize', () => { clearTimeout(resizeTO); resizeTO = setTimeout(() => { loopWidth = measureLoop(); }, 150); }); track.parentElement.addEventListener('mouseenter', () => { isPaused = true; }); track.parentElement.addEventListener('mouseleave', () => { isPaused = false; }); // Click → modal track.querySelectorAll('.partner-slide').forEach(slide => { const open = () => { const idx = parseInt(slide.dataset.partnerIdx, 10); openPartnerModal(partners[idx]); }; slide.addEventListener('click', open); slide.addEventListener('keydown', e => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); open(); } }); }); function openPartnerModal(p) { // Garde-fou: vérifier que p est un objet valide if (!p || typeof p !== 'object') { console.error('[Partner Modal] Données partenaire invalides:', p); return; } try { isPaused = true; const imgSrc = window.escapeHTML(p.logo || `assets/images/partners/${p.name}.png`); const logoImg = document.getElementById('partner-modal-logo-img'); const logoInner = document.getElementById('partner-modal-logo-fallback'); if (!logoImg || !logoInner) { console.error('[Partner Modal] Éléments DOM manquants pour la modal'); return; } logoImg.src = imgSrc; logoImg.alt = window.escapeHTML(p.displayName || 'Partenaire'); logoImg.style.display = 'block'; logoInner.style.display = 'none'; logoInner.style.background = ''; logoInner.style.justifyContent = ''; logoInner.textContent = ''; // Si l'image échoue → fallback initiales dans l'octogone logoImg.onerror = function() { this.style.display = 'none'; logoInner.style.background = p.gradient; logoInner.style.display = 'flex'; logoInner.style.alignItems = 'center'; logoInner.style.justifyContent = 'center'; logoInner.style.fontSize = '2rem'; logoInner.style.fontFamily = "'OmegaLarge', sans-serif"; logoInner.style.color = 'white'; logoInner.textContent = p.displayName.substring(0,2).toUpperCase(); }; document.getElementById('partner-modal-name').textContent = p.displayName; document.getElementById('partner-modal-desc').textContent = p.longDesc; document.getElementById('partner-modal-since').textContent = p.since; document.getElementById('partner-modal-services').innerHTML = (p.services || []).map(s => `${window.escapeHTML(s)}`).join(''); document.getElementById('partner-modal-stats').innerHTML = (p.stats || []).map(s => `
${window.escapeHTML(s.value)}
${window.escapeHTML(s.label)}
`).join(''); const links = document.getElementById('partner-modal-links'); // Réseaux sociaux depuis le champ social du partenaire // Sécurité: fallback sûr sans injection HTML si renderSocialLinks n'existe pas ou est invalide let socialHtml = ''; try { if (typeof window.renderSocialLinks === 'function') { // Vérification supplémentaire: s'assurer que la fonction est celle attendue (définie dans main.js) if (window.renderSocialLinks.toString().includes('escapeHTML')) { socialHtml = window.renderSocialLinks(p.social); } else { console.warn('[Partner Modal] window.renderSocialLinks semble compromise, utilisation du fallback sécurisé'); } } } catch (e) { console.error('[Partner Modal] Erreur lors du rendu des liens sociaux:', e); } links.innerHTML = socialHtml; if (window.feather) feather.replace(); partnerModalOverlay.classList.add('active'); document.body.style.overflow = 'hidden'; } catch (err) { console.error('[Partner Modal] Erreur lors de l\'ouverture de la modal:', err); // Fallback: fermer la modal si elle a été ouverte if (partnerModalOverlay) { partnerModalOverlay.classList.remove('active'); document.body.style.overflow = ''; } } if (partnerModalOverlay) { const closeBtn = document.getElementById('partner-modal-close'); closeBtn && closeBtn.addEventListener('click', closePartnerModal); partnerModalOverlay.addEventListener('click', e => { if (e.target === partnerModalOverlay) closePartnerModal(); }); document.addEventListener('keydown', e => { if (e.key === 'Escape' && partnerModalOverlay.classList.contains('active')) closePartnerModal(); }); } function closePartnerModal() { partnerModalOverlay.classList.remove('active'); document.body.style.overflow = ''; isPaused = false; } }) .catch(err => { console.error('[Partners] Échec du chargement des partenaires:', err); const section = document.getElementById('partenaires'); if (section) { section.innerHTML = ' '; } }); } // ── Team section ───────────────────────────────────────────────────────────── fetch('assets/data/team.json') .then(res => { if (!res.ok) throw new Error(`HTTP ${res.status}: ${res.statusText}`); return res.json(); }) .then(members => { try { if (!Array.isArray(members)) { console.error('[Team] Données invalides: members n’est pas un tableau', members); throw new Error('Invalid team data format'); } initTeamSection(members); } catch (err) { console.error('[Team] Erreur lors de l’initialisation de la section créateurs:', err); // Fallback: afficher un message d'erreur dans la section si elle existe const teamSection = document.getElementById('les-createurs') || document.getElementById('team'); if (teamSection) { teamSection.innerHTML = ' '; } } }) .catch(err => { console.error('[Team] Échec du chargement des données de l’équipe:', err); // Fallback: afficher un message d'erreur dans la section si elle existe const teamSection = document.getElementById('les-createurs') || document.getElementById('team'); if (teamSection) { teamSection.innerHTML = ' '; } }); // Konami + easter egg → définis au niveau top-level du script (voir bas de fichier) // ── initTeamSection ────────────────────────────────────────────────────────── function initTeamSection(members) { // Sécurité: les garde-fous empêchent une erreur JS de casser tout le rendu. console.log('[Team] Initialisation de la section créateurs avec', members.length, 'membres'); if (!Array.isArray(members)) { console.error('[Team] initTeamSection: members n’est pas un tableau'); return; } const carousel = document.getElementById('team-carousel'); const dotsContainer = document.getElementById('carousel-dots'); const prevBtn = document.getElementById('carousel-prev'); const nextBtn = document.getElementById('carousel-next'); if (!carousel) { console.error('[Team] initTeamSection: #team-carousel introuvable'); return; } if (!dotsContainer) { console.warn('[Team] initTeamSection: #carousel-dots introuvable (dots désactivés)'); } // escapeHTML est utilisé pour éviter l’injection XSS via innerHTML. if (typeof window.escapeHTML !== 'function') { console.error('[Team] initTeamSection: window.escapeHTML introuvable (risque XSS)'); return; } carousel.innerHTML = members.map((m, i) => { const imgSrc = window.escapeHTML(`assets/images/team/${m.id}.png`); const safeName = window.escapeHTML(m.name); const safeRole = window.escapeHTML(m.role); const safeBio = window.escapeHTML((m.bio || '').split('.').shift() + '.'); const safeFallback = window.escapeHTML(m.avatar || ''); return `
${safeName}

${safeName}

${safeRole}

${safeBio}

`; }).join(''); let cards = Array.from(carousel.querySelectorAll('.team-card')); let currentIndex = 0; let cardsPerView = 3; let autoPlayInterval; function updateCardsPerView() { if (window.innerWidth <= 768) cardsPerView = 1; else if (window.innerWidth <= 1024) cardsPerView = 2; else cardsPerView = 3; } function getTotalDots() { return Math.max(1, cards.length - cardsPerView + 1); } function createDots() { if (!dotsContainer) return; dotsContainer.innerHTML = ''; for (let i = 0; i < getTotalDots(); i++) { const dot = document.createElement('div'); dot.classList.add('carousel-dot'); if (i === 0) dot.classList.add('active'); dot.addEventListener('click', () => goToSlide(i)); dotsContainer.appendChild(dot); } } function updateCarousel() { cards = Array.from(carousel.querySelectorAll('.team-card')); if (!cards.length) return; const gap = 32; const cardWidth = cards[0].offsetWidth + gap; carousel.style.transform = `translateX(-${currentIndex * cardWidth}px)`; if (dotsContainer) Array.from(dotsContainer.querySelectorAll('.carousel-dot')).forEach((d, i) => d.classList.toggle('active', i === currentIndex)); } function goToSlide(index) { currentIndex = Math.min(index, Math.max(0, cards.length - cardsPerView)); updateCarousel(); resetAutoPlay(); } function nextSlide() { const max = Math.max(0, cards.length - cardsPerView); currentIndex = currentIndex >= max ? 0 : currentIndex + 1; updateCarousel(); } function prevSlide() { const max = Math.max(0, cards.length - cardsPerView); currentIndex = currentIndex <= 0 ? max : currentIndex - 1; updateCarousel(); } function startAutoPlay() { clearInterval(autoPlayInterval); autoPlayInterval = setInterval(nextSlide, 4000); } function resetAutoPlay() { clearInterval(autoPlayInterval); startAutoPlay(); } prevBtn && prevBtn.addEventListener('click', () => { prevSlide(); resetAutoPlay(); }); nextBtn && nextBtn.addEventListener('click', () => { nextSlide(); resetAutoPlay(); }); carousel.addEventListener('mouseenter', () => clearInterval(autoPlayInterval)); carousel.addEventListener('mouseleave', startAutoPlay); let resizeTimer; const ro = new ResizeObserver(() => { clearTimeout(resizeTimer); resizeTimer = setTimeout(() => { updateCardsPerView(); currentIndex = 0; createDots(); updateCarousel(); }, 100); }); ro.observe(carousel.parentElement || document.body); updateCardsPerView(); createDots(); updateCarousel(); startAutoPlay(); // Modal const modalOverlay = document.getElementById('team-modal-overlay'); const modalClose = document.getElementById('team-modal-close'); const modalAvatar = document.getElementById('team-modal-avatar'); const modalName = document.getElementById('team-modal-name'); const modalRole = document.getElementById('team-modal-role'); const modalBio = document.getElementById('team-modal-bio'); const modalStats = document.getElementById('team-modal-stats'); const modalSkills = document.getElementById('team-modal-skills'); const modalSocial = document.getElementById('team-modal-social'); function openMemberModal(index) { const m = members[index]; // Garde-fou: vérifier que m est un objet valide if (!m || typeof m !== 'object') { console.error('[Team Modal] Données membre invalides pour l\'index', index, ':', m); return; } try { // Avatar avec image si dispo const imgSrc = window.escapeHTML(`assets/images/team/${m.id}.png`); const safeName = window.escapeHTML(m.name); const safeFallback = window.escapeHTML(m.avatar || (m.name ? m.name.charAt(0) : '?')); if (!modalAvatar) { console.error('[Team Modal] Éléments DOM manquants pour la modal'); return; } modalAvatar.innerHTML = ` ${safeName} ${safeFallback} `; modalName.textContent = m.name; modalRole.textContent = m.role; modalBio.textContent = m.bio; modalStats.innerHTML = (m.stats || []).map(s => `
${window.escapeHTML(s.value)}
${window.escapeHTML(s.label)}
`).join(''); modalSkills.innerHTML = (m.skills || []).map(s => `${window.escapeHTML(s)}`).join(''); // Réseaux sociaux depuis le JSON du membre (+ fallback discord global) // Sécurité : ne jamais injecter de HTML non maîtrisé si le renderer n'existe pas ou est compromis. let socialHtml = ''; try { if (typeof window.renderSocialLinks === 'function') { // Vérification supplémentaire: s'assurer que la fonction utilise escapeHTML if (window.renderSocialLinks.toString().includes('escapeHTML')) { socialHtml = window.renderSocialLinks(m.social, (window.OK_SOCIAL || {}).discord); } else { console.warn('[Team Modal] window.renderSocialLinks semble compromise, utilisation du fallback sécurisé'); } } else { console.warn('[Team Modal] window.renderSocialLinks non définie'); } } catch (e) { console.error('[Team Modal] Erreur lors du rendu des liens sociaux:', e); } modalSocial.innerHTML = socialHtml; modalOverlay.classList.add('active'); document.body.style.overflow = 'hidden'; } catch (err) { console.error('[Team Modal] Erreur lors de l\'ouverture de la modal:', err); // Fallback: fermer la modal si elle a été ouverte if (modalOverlay) { modalOverlay.classList.remove('active'); document.body.style.overflow = ''; } } function closeMemberModal() { modalOverlay.classList.remove('active'); document.body.style.overflow = ''; } carousel.querySelectorAll('.team-card').forEach(card => { const openCard = () => { const idx = parseInt(card.dataset.member, 10); openMemberModal(idx); }; card.addEventListener('click', openCard); card.addEventListener('keydown', e => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); openCard(); } }); }); modalClose && modalClose.addEventListener('click', closeMemberModal); modalOverlay && modalOverlay.addEventListener('click', e => { if (e.target === modalOverlay) closeMemberModal(); }); document.addEventListener('keydown', e => { if (e.key === 'Escape' && modalOverlay && modalOverlay.classList.contains('active')) closeMemberModal(); }); } }); // ═══════════════════════════════════════════════════════════════════════════ // EASTER EGG — top-level (hors DOMContentLoaded pour garantir l'attachement) // ═══════════════════════════════════════════════════════════════════════════ (function () { var KONAMI = ['ArrowUp','ArrowUp','ArrowDown','ArrowDown','ArrowLeft','ArrowRight','ArrowLeft','ArrowRight','b','a']; var progress = 0; // PC : Konami Code — capture:true pour passer avant tout autre listener window.addEventListener('keydown', function(e) { var key = e.key.length === 1 ? e.key.toLowerCase() : e.key; if (key === KONAMI[progress]) { progress++; if (progress === KONAMI.length) { progress = 0; triggerEasterEgg(); } } else { progress = (key === KONAMI[0]) ? 1 : 0; } }, true); // capture phase — reçu avant tout stopPropagation // Mobile : 10 taps sur le logo navbar OU logo footer var tapCount = 0; var tapTimer = null; document.addEventListener('click', function(e) { if (!e.target.closest('.logo') && !e.target.closest('.logo-mini') && !e.target.closest('.omega-symbol')) return; if (!('ontouchstart' in window) && navigator.maxTouchPoints === 0) return; tapCount++; clearTimeout(tapTimer); tapTimer = setTimeout(function() { tapCount = 0; }, 600); if (tapCount >= 10) { tapCount = 0; triggerEasterEgg(); } }); function triggerEasterEgg() { if (document.getElementById('easter-egg-overlay')) return; var egg = document.createElement('div'); egg.id = 'easter-egg-overlay'; egg.setAttribute('role', 'dialog'); egg.setAttribute('aria-modal', 'true'); egg.innerHTML = '
🎮

GG\u00a0! Tu as trouvé l\u2019easter egg\u00a0!

La vraie question c\u2019est\u00a0: combien de temps tu as mis\u00a0?

'; document.body.appendChild(egg); var closeBtn = document.getElementById('easter-egg-close'); closeBtn && closeBtn.focus(); var remove = function() { egg.remove(); }; closeBtn && closeBtn.addEventListener('click', remove); egg.addEventListener('click', function(e) { if (e.target === egg) remove(); }); window.addEventListener('keydown', function esc(e) { if (e.key === 'Escape') { remove(); window.removeEventListener('keydown', esc); } }, { once: true }); } })();