590 lines
27 KiB
JavaScript
590 lines
27 KiB
JavaScript
|
|
// 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 `
|
|||
|
|
<div class="portfolio-item reveal${delayClasses[i]}" data-project-id="${window.escapeHTML(p.id)}" role="button" tabindex="0" aria-label="Voir le projet ${safeTitle}">
|
|||
|
|
<div class="portfolio-bg" style="background: ${window.escapeHTML(p.gradient)};">
|
|||
|
|
${imgSrc ? `<img src="${imgSrc}" alt="${safeTitle}" class="portfolio-cover-img" onerror="this.style.display='none'">` : ''}
|
|||
|
|
</div>
|
|||
|
|
<div class="portfolio-content">
|
|||
|
|
<span class="portfolio-tag">${safeCategory}</span>
|
|||
|
|
<h4>${safeTitle}</h4>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
`;
|
|||
|
|
}).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 = '
|
|||
|
|
<div class="alert alert-error" role="alert" style="grid-column: 1 / -1;">
|
|||
|
|
<p>⚠️ Impossible de charger les projets. Veuillez rafraîchir la page.</p>
|
|||
|
|
</div>
|
|||
|
|
';
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ── 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 `
|
|||
|
|
<div class="partner-slide" data-partner-idx="${i % partners.length}" role="button" tabindex="0" aria-label="Voir ${safeName}">
|
|||
|
|
<div class="partner-slide-inner">
|
|||
|
|
<img src="${imgSrc}" alt="${safeName}" class="partner-logo-img"
|
|||
|
|
onerror="this.style.display='none';this.nextElementSibling.style.display='flex';">
|
|||
|
|
<div class="partner-logo-fallback" style="background:${safeGradient};display:none;">
|
|||
|
|
${safeInitials}
|
|||
|
|
</div>
|
|||
|
|
<span class="partner-slide-name">${safeName}</span>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
`;
|
|||
|
|
}).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 => `<span class="team-modal-skill">${window.escapeHTML(s)}</span>`).join('');
|
|||
|
|
document.getElementById('partner-modal-stats').innerHTML = (p.stats || []).map(s => `
|
|||
|
|
<div class="team-modal-stat">
|
|||
|
|
<div class="team-modal-stat-value">${window.escapeHTML(s.value)}</div>
|
|||
|
|
<div class="team-modal-stat-label">${window.escapeHTML(s.label)}</div>
|
|||
|
|
</div>
|
|||
|
|
`).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 = '
|
|||
|
|
<div class="alert alert-error" role="alert">
|
|||
|
|
<p>⚠️ Impossible de charger les partenaires. Veuillez réessayer plus tard.</p>
|
|||
|
|
</div>
|
|||
|
|
';
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ── 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 = '
|
|||
|
|
<div class="alert alert-error" role="alert">
|
|||
|
|
<p>⚠️ Impossible de charger les informations de l\'équipe. Veuillez rafraîchir la page.</p>
|
|||
|
|
</div>
|
|||
|
|
';
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
.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 = '
|
|||
|
|
<div class="alert alert-error" role="alert">
|
|||
|
|
<p>⚠️ Impossible de charger les informations de l\'équipe. Veuillez réessayer plus tard.</p>
|
|||
|
|
</div>
|
|||
|
|
';
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
|
|||
|
|
// 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 `
|
|||
|
|
<div class="team-card" data-member="${i}" role="button" tabindex="0" aria-label="Voir le profil de ${safeName}">
|
|||
|
|
<div class="team-avatar">
|
|||
|
|
<div class="team-avatar-inner" id="team-avatar-inner-${i}">
|
|||
|
|
<img src="${imgSrc}" alt="${safeName}" class="team-avatar-photo"
|
|||
|
|
onerror="const s=this.src;if(s.endsWith('.png')){this.src=s.replace('.png','.jpg');}else if(s.endsWith('.jpg')){this.src=s.replace('.jpg','.gif');}else{this.style.display='none';document.getElementById('team-avatar-fallback-${i}').style.display='flex';}">
|
|||
|
|
<span class="team-avatar-fallback" id="team-avatar-fallback-${i}" style="display:none;">${safeFallback}</span>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
<h4>${safeName}</h4>
|
|||
|
|
<p class="team-role">${safeRole}</p>
|
|||
|
|
<p class="team-desc">${safeBio}</p>
|
|||
|
|
</div>
|
|||
|
|
`;
|
|||
|
|
}).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 = `
|
|||
|
|
<img src="${imgSrc}" alt="${safeName}" class="team-modal-avatar-photo"
|
|||
|
|
onerror="const s=this.src;if(s.endsWith('.png')){this.src=s.replace('.png','.jpg');}else if(s.endsWith('.jpg')){this.src=s.replace('.jpg','.gif');}else{this.style.display='none';this.nextElementSibling.style.display='flex';}">
|
|||
|
|
<span class="team-modal-avatar-fallback">${safeFallback}</span>
|
|||
|
|
`;
|
|||
|
|
modalName.textContent = m.name;
|
|||
|
|
modalRole.textContent = m.role;
|
|||
|
|
modalBio.textContent = m.bio;
|
|||
|
|
modalStats.innerHTML = (m.stats || []).map(s => `
|
|||
|
|
<div class="team-modal-stat">
|
|||
|
|
<div class="team-modal-stat-value">${window.escapeHTML(s.value)}</div>
|
|||
|
|
<div class="team-modal-stat-label">${window.escapeHTML(s.label)}</div>
|
|||
|
|
</div>
|
|||
|
|
`).join('');
|
|||
|
|
modalSkills.innerHTML = (m.skills || []).map(s => `<span class="team-modal-skill">${window.escapeHTML(s)}</span>`).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 = '<div class="easter-egg-box"><div class="easter-egg-icon">🎮</div><h2>GG\u00a0! Tu as trouvé l\u2019easter egg\u00a0!</h2><p>La vraie question c\u2019est\u00a0: combien de temps tu as mis\u00a0?</p><button id="easter-egg-close" class="btn btn-primary">Fermer</button></div>';
|
|||
|
|
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 });
|
|||
|
|
}
|
|||
|
|
})();
|