426 lines
20 KiB
JavaScript
426 lines
20 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 => res.json())
|
|
.then(projects => {
|
|
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 ? (p.images || '') + firstFile : null;
|
|
return `
|
|
<div class="portfolio-item reveal${delayClasses[i]}" data-project-id="${p.id}" role="button" tabindex="0" aria-label="Voir le projet ${p.title}">
|
|
<div class="portfolio-bg" style="background: ${p.gradient};">
|
|
${imgSrc ? `<img src="${imgSrc}" alt="${p.title}" class="portfolio-cover-img" onerror="this.style.display='none'">` : ''}
|
|
</div>
|
|
<div class="portfolio-content">
|
|
<span class="portfolio-tag">${categoryLabels[p.category] || p.category}</span>
|
|
<h4>${p.title}</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('Failed to load projects for home preview', err));
|
|
}
|
|
|
|
// ── 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 => res.json())
|
|
.then(partners => {
|
|
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 = p.logo || `assets/images/partners/${p.name}.png`;
|
|
return `
|
|
<div class="partner-slide" data-partner-idx="${i % partners.length}" role="button" tabindex="0" aria-label="Voir ${p.displayName}">
|
|
<div class="partner-slide-inner">
|
|
<img src="${imgSrc}" alt="${p.displayName}" class="partner-logo-img"
|
|
onerror="this.style.display='none';this.nextElementSibling.style.display='flex';">
|
|
<div class="partner-logo-fallback" style="background:${p.gradient};display:none;">
|
|
${p.displayName.substring(0,2).toUpperCase()}
|
|
</div>
|
|
<span class="partner-slide-name">${p.displayName}</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) {
|
|
isPaused = true;
|
|
const imgSrc = p.logo || `assets/images/partners/${p.name}.png`;
|
|
const logoImg = document.getElementById('partner-modal-logo-img');
|
|
const logoInner = document.getElementById('partner-modal-logo-fallback');
|
|
logoImg.src = imgSrc;
|
|
logoImg.alt = p.displayName;
|
|
logoImg.style.display = 'block';
|
|
logoInner.style.background = '';
|
|
logoInner.style.justifyContent = '';
|
|
// 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">${s}</span>`).join('');
|
|
document.getElementById('partner-modal-stats').innerHTML = (p.stats || []).map(s => `
|
|
<div class="team-modal-stat">
|
|
<div class="team-modal-stat-value">${s.value}</div>
|
|
<div class="team-modal-stat-label">${s.label}</div>
|
|
</div>
|
|
`).join('');
|
|
const links = document.getElementById('partner-modal-links');
|
|
// Réseaux sociaux depuis le champ social du partenaire
|
|
links.innerHTML = typeof window.renderSocialLinks === 'function'
|
|
? window.renderSocialLinks(p.social)
|
|
: '';
|
|
if (window.feather) feather.replace();
|
|
partnerModalOverlay.classList.add('active');
|
|
document.body.style.overflow = 'hidden';
|
|
}
|
|
|
|
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 => {
|
|
const section = document.getElementById('partenaires');
|
|
if (section) section.style.display = 'none';
|
|
});
|
|
}
|
|
|
|
// ── Team section ─────────────────────────────────────────────────────────────
|
|
fetch('assets/data/team.json')
|
|
.then(res => res.json())
|
|
.then(members => { initTeamSection(members); })
|
|
.catch(err => { console.error('Failed to load team data', err); });
|
|
|
|
// Konami + easter egg → définis au niveau top-level du script (voir bas de fichier)
|
|
|
|
// ── initTeamSection ──────────────────────────────────────────────────────────
|
|
function initTeamSection(members) {
|
|
const carousel = document.getElementById('team-carousel');
|
|
const dotsContainer = document.getElementById('carousel-dots');
|
|
const prevBtn = document.getElementById('carousel-prev');
|
|
const nextBtn = document.getElementById('carousel-next');
|
|
|
|
carousel.innerHTML = members.map((m, i) => {
|
|
const imgSrc = `assets/images/team/${m.id}.png`;
|
|
return `
|
|
<div class="team-card" data-member="${i}" role="button" tabindex="0" aria-label="Voir le profil de ${m.name}">
|
|
<div class="team-avatar">
|
|
<div class="team-avatar-inner" id="team-avatar-inner-${i}">
|
|
<img src="${imgSrc}" alt="${m.name}" 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;">${m.avatar}</span>
|
|
</div>
|
|
</div>
|
|
<h4>${m.name}</h4>
|
|
<p class="team-role">${m.role}</p>
|
|
<p class="team-desc">${(m.bio || '').split('.').shift()}.</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() {
|
|
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)`;
|
|
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];
|
|
if (!m) return;
|
|
// Avatar avec image si dispo
|
|
const imgSrc = `assets/images/team/${m.id}.png`;
|
|
modalAvatar.innerHTML = `
|
|
<img src="${imgSrc}" alt="${m.name}" 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">${m.avatar || m.name.charAt(0)}</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">${s.value}</div>
|
|
<div class="team-modal-stat-label">${s.label}</div>
|
|
</div>
|
|
`).join('');
|
|
modalSkills.innerHTML = (m.skills || []).map(s => `<span class="team-modal-skill">${s}</span>`).join('');
|
|
// Réseaux sociaux depuis le JSON du membre (+ fallback discord global)
|
|
const socialHtml = typeof window.renderSocialLinks === 'function'
|
|
? window.renderSocialLinks(m.social, (window.OK_SOCIAL || {}).discord)
|
|
: '';
|
|
modalSocial.innerHTML = socialHtml;
|
|
modalOverlay.classList.add('active');
|
|
document.body.style.overflow = 'hidden';
|
|
}
|
|
|
|
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 });
|
|
}
|
|
})();
|