website/assets/js/home.js

427 lines
20 KiB
JavaScript
Raw Normal View History

2026-04-05 20:49:52 +02:00
// Home page specific JS: team carousel + modal + portfolio preview + partners + konami
2026-02-06 18:36:03 +01:00
document.addEventListener('DOMContentLoaded', () => {
2026-04-05 20:49:52 +02:00
// ── 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) => {
2026-04-07 01:51:50 +02:00
const firstFile = Array.isArray(p.files) && p.files.length > 0 ? p.files[0] : null;
const imgSrc = firstFile ? (p.images || '') + firstFile : null;
2026-04-05 20:49:52 +02:00
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};">
2026-04-07 01:51:50 +02:00
${imgSrc ? `<img src="${imgSrc}" alt="${p.title}" class="portfolio-cover-img" onerror="this.style.display='none'">` : ''}
2026-04-05 20:49:52 +02:00
</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);
2026-04-07 01:51:50 +02:00
// Délégation globale sur document — fonctionne même si realisations.js
// se charge après home.js (ordre des scripts defer non garanti)
2026-04-05 20:49:52 +02:00
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);
2026-04-07 01:51:50 +02:00
} 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);
2026-04-05 20:49:52 +02:00
}
});
})
.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) => {
2026-04-08 00:57:37 +02:00
const imgSrc = p.logo || `assets/images/partners/${p.name}.png`;
2026-04-05 20:49:52 +02:00
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;
2026-04-08 00:57:37 +02:00
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() {
2026-04-05 20:49:52 +02:00
this.style.display = 'none';
2026-04-08 00:57:37 +02:00
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();
2026-04-05 20:49:52 +02:00
};
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');
2026-04-07 01:51:50 +02:00
// Réseaux sociaux depuis le champ social du partenaire
links.innerHTML = typeof window.renderSocialLinks === 'function'
? window.renderSocialLinks(p.social)
: '';
2026-04-05 20:49:52 +02:00
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 ─────────────────────────────────────────────────────────────
2026-02-06 18:36:03 +01:00
fetch('assets/data/team.json')
.then(res => res.json())
2026-04-05 20:49:52 +02:00
.then(members => { initTeamSection(members); })
.catch(err => { console.error('Failed to load team data', err); });
2026-02-06 18:36:03 +01:00
2026-04-05 20:49:52 +02:00
// Konami + easter egg → définis au niveau top-level du script (voir bas de fichier)
// ── initTeamSection ──────────────────────────────────────────────────────────
2026-02-06 18:36:03 +01:00
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');
2026-04-05 20:49:52 +02:00
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"
2026-04-13 23:05:47 +02:00
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';}">
2026-04-05 20:49:52 +02:00
<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>
2026-02-06 18:36:03 +01:00
</div>
2026-04-05 20:49:52 +02:00
`;
}).join('');
let cards = Array.from(carousel.querySelectorAll('.team-card'));
2026-02-06 18:36:03 +01:00
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;
}
2026-04-05 20:49:52 +02:00
function getTotalDots() { return Math.max(1, cards.length - cardsPerView + 1); }
2026-02-06 18:36:03 +01:00
function createDots() {
dotsContainer.innerHTML = '';
2026-04-05 20:49:52 +02:00
for (let i = 0; i < getTotalDots(); i++) {
2026-02-06 18:36:03 +01:00
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() {
2026-04-05 20:49:52 +02:00
cards = Array.from(carousel.querySelectorAll('.team-card'));
if (!cards.length) return;
const gap = 32;
2026-02-06 18:36:03 +01:00
const cardWidth = cards[0].offsetWidth + gap;
carousel.style.transform = `translateX(-${currentIndex * cardWidth}px)`;
2026-04-05 20:49:52 +02:00
Array.from(dotsContainer.querySelectorAll('.carousel-dot')).forEach((d, i) => d.classList.toggle('active', i === currentIndex));
2026-02-06 18:36:03 +01:00
}
2026-04-05 20:49:52 +02:00
function goToSlide(index) {
currentIndex = Math.min(index, Math.max(0, cards.length - cardsPerView));
updateCarousel(); resetAutoPlay();
2026-02-06 18:36:03 +01:00
}
function nextSlide() {
2026-04-05 20:49:52 +02:00
const max = Math.max(0, cards.length - cardsPerView);
currentIndex = currentIndex >= max ? 0 : currentIndex + 1;
2026-02-06 18:36:03 +01:00
updateCarousel();
}
function prevSlide() {
2026-04-05 20:49:52 +02:00
const max = Math.max(0, cards.length - cardsPerView);
currentIndex = currentIndex <= 0 ? max : currentIndex - 1;
2026-02-06 18:36:03 +01:00
updateCarousel();
}
2026-04-05 20:49:52 +02:00
function startAutoPlay() { clearInterval(autoPlayInterval); autoPlayInterval = setInterval(nextSlide, 4000); }
function resetAutoPlay() { clearInterval(autoPlayInterval); startAutoPlay(); }
2026-02-06 18:36:03 +01:00
prevBtn && prevBtn.addEventListener('click', () => { prevSlide(); resetAutoPlay(); });
nextBtn && nextBtn.addEventListener('click', () => { nextSlide(); resetAutoPlay(); });
carousel.addEventListener('mouseenter', () => clearInterval(autoPlayInterval));
carousel.addEventListener('mouseleave', startAutoPlay);
2026-04-05 20:49:52 +02:00
let resizeTimer;
const ro = new ResizeObserver(() => {
clearTimeout(resizeTimer);
resizeTimer = setTimeout(() => { updateCardsPerView(); currentIndex = 0; createDots(); updateCarousel(); }, 100);
2026-02-06 18:36:03 +01:00
});
2026-04-05 20:49:52 +02:00
ro.observe(carousel.parentElement || document.body);
updateCardsPerView(); createDots(); updateCarousel(); startAutoPlay();
2026-02-06 18:36:03 +01:00
2026-04-05 20:49:52 +02:00
// Modal
2026-02-06 18:36:03 +01:00
const modalOverlay = document.getElementById('team-modal-overlay');
const modalClose = document.getElementById('team-modal-close');
2026-04-07 01:51:50 +02:00
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');
2026-02-06 18:36:03 +01:00
function openMemberModal(index) {
2026-04-05 20:49:52 +02:00
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"
2026-04-13 23:05:47 +02:00
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';}">
2026-04-05 20:49:52 +02:00
<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 => `
2026-02-06 18:36:03 +01:00
<div class="team-modal-stat">
2026-04-05 20:49:52 +02:00
<div class="team-modal-stat-value">${s.value}</div>
<div class="team-modal-stat-label">${s.label}</div>
2026-02-06 18:36:03 +01:00
</div>
`).join('');
2026-04-05 20:49:52 +02:00
modalSkills.innerHTML = (m.skills || []).map(s => `<span class="team-modal-skill">${s}</span>`).join('');
2026-04-07 01:51:50 +02:00
// 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;
2026-02-06 18:36:03 +01:00
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 => {
2026-04-05 20:49:52 +02:00
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(); } });
2026-02-06 18:36:03 +01:00
});
modalClose && modalClose.addEventListener('click', closeMemberModal);
2026-04-05 20:49:52 +02:00
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 });
2026-02-06 18:36:03 +01:00
}
2026-04-05 20:49:52 +02:00
})();