This commit is contained in:
KIRITO_KMA 2026-04-05 20:49:52 +02:00
parent 7d26ffcee4
commit 25ea6cf6f6
19 changed files with 2270 additions and 467 deletions

View file

@ -1,36 +1,238 @@
// Home page specific JS: team carousel + modal
// Home page specific JS: team carousel + modal + portfolio preview + partners + konami
document.addEventListener('DOMContentLoaded', () => {
// Fetch team data and render
// ── 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 imgSrc = (p.images || '') + '1.png';
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};">
<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>
<p>${p.desc.substring(0, 80)}${p.desc.length > 80 ? '…' : ''}</p>
</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 : attend que openProjectModal soit défini par realisations.js
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);
}
});
})
.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 = `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 = `assets/images/partners/${p.name}.png`;
document.getElementById('partner-modal-logo-img').src = imgSrc;
document.getElementById('partner-modal-logo-img').alt = p.displayName;
document.getElementById('partner-modal-logo-img').onerror = function() {
this.style.display = 'none';
document.getElementById('partner-modal-logo-fallback').style.display = 'flex';
document.getElementById('partner-modal-logo-fallback').style.background = p.gradient;
document.getElementById('partner-modal-logo-fallback').textContent = p.displayName.substring(0,2).toUpperCase();
};
document.getElementById('partner-modal-logo-fallback').style.display = 'none';
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');
links.innerHTML = '';
if (p.url && p.url !== '#') {
links.innerHTML += `<a href="${p.url}" target="_blank" rel="noopener noreferrer" class="btn btn-secondary"><i data-feather="external-link"></i> Site web</a>`;
}
if (p.discord && p.discord !== '#') {
links.innerHTML += `<a href="${p.discord}" target="_blank" rel="noopener noreferrer" class="btn btn-primary">
<svg viewBox="0 0 24 24" fill="currentColor" width="16" height="16"><path d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0 12.64 12.64 0 0 0-.617-1.25.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057 19.9 19.9 0 0 0 5.993 3.03.078.078 0 0 0 .084-.028c.462-.63.874-1.295 1.226-1.994a.076.076 0 0 0-.041-.106 13.107 13.107 0 0 1-1.872-.892.077.077 0 0 1-.008-.128 10.2 10.2 0 0 0 .372-.292.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127 12.299 12.299 0 0 1-1.873.892.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028 19.839 19.839 0 0 0 6.002-3.03.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03z"/></svg>
Discord
</a>`;
}
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);
});
.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');
// Render cards
carousel.innerHTML = members.map((m, i) => `
<div class="team-card" data-member="${i}">
<div class="team-avatar">
<div class="team-avatar-inner">${m.avatar}</div>
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="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>
<h4>${m.name}</h4>
<p class="team-role">${m.role}</p>
<p class="team-desc">${(m.bio || '').split('.').shift()}.</p>
</div>
`).join('');
`;
}).join('');
// Carousel logic
const cardsContainer = carousel; // the track
let cards = carousel.querySelectorAll('.team-card');
let cards = Array.from(carousel.querySelectorAll('.team-card'));
let currentIndex = 0;
let cardsPerView = 3;
let autoPlayInterval;
@ -41,10 +243,11 @@ document.addEventListener('DOMContentLoaded', () => {
else cardsPerView = 3;
}
function getTotalDots() { return Math.max(1, cards.length - cardsPerView + 1); }
function createDots() {
dotsContainer.innerHTML = '';
const totalDots = Math.max(1, Math.ceil(cards.length / cardsPerView));
for (let i = 0; i < totalDots; i++) {
for (let i = 0; i < getTotalDots(); i++) {
const dot = document.createElement('div');
dot.classList.add('carousel-dot');
if (i === 0) dot.classList.add('active');
@ -54,65 +257,49 @@ document.addEventListener('DOMContentLoaded', () => {
}
function updateCarousel() {
cards = carousel.querySelectorAll('.team-card');
if (cards.length === 0) return;
const cardStyle = window.getComputedStyle(cards[0]);
const gap = 32; // matches previous implementation
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)`;
const dots = dotsContainer.querySelectorAll('.carousel-dot');
const activeDotIndex = Math.floor(currentIndex / cardsPerView);
dots.forEach((dot, i) => dot.classList.toggle('active', i === activeDotIndex));
Array.from(dotsContainer.querySelectorAll('.carousel-dot')).forEach((d, i) => d.classList.toggle('active', i === currentIndex));
}
function goToSlide(dotIndex) {
currentIndex = dotIndex * cardsPerView;
if (currentIndex > cards.length - cardsPerView) currentIndex = Math.max(0, cards.length - cardsPerView);
updateCarousel();
resetAutoPlay();
function goToSlide(index) {
currentIndex = Math.min(index, Math.max(0, cards.length - cardsPerView));
updateCarousel(); resetAutoPlay();
}
function nextSlide() {
currentIndex++;
if (currentIndex > cards.length - cardsPerView) currentIndex = 0;
const max = Math.max(0, cards.length - cardsPerView);
currentIndex = currentIndex >= max ? 0 : currentIndex + 1;
updateCarousel();
}
function prevSlide() {
currentIndex--;
if (currentIndex < 0) currentIndex = Math.max(0, cards.length - cardsPerView);
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();
}
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);
updateCardsPerView();
createDots();
startAutoPlay();
window.addEventListener('resize', () => {
updateCardsPerView();
createDots();
currentIndex = 0;
updateCarousel();
let resizeTimer;
const ro = new ResizeObserver(() => {
clearTimeout(resizeTimer);
resizeTimer = setTimeout(() => { updateCardsPerView(); currentIndex = 0; createDots(); updateCarousel(); }, 100);
});
ro.observe(carousel.parentElement || document.body);
// Modal logic
updateCardsPerView(); createDots(); updateCarousel(); startAutoPlay();
// Modal
const modalOverlay = document.getElementById('team-modal-overlay');
const modalClose = document.getElementById('team-modal-close');
const modalAvatar = document.getElementById('modal-avatar');
@ -124,29 +311,33 @@ document.addEventListener('DOMContentLoaded', () => {
const modalSocial = document.getElementById('modal-social');
function openMemberModal(index) {
const member = members[index];
if (!member) return;
modalAvatar.textContent = member.avatar || member.name.charAt(0);
modalName.textContent = member.name;
modalRole.textContent = member.role;
modalBio.textContent = member.bio;
modalStats.innerHTML = (member.stats || []).map(stat => `
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="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">${stat.value}</div>
<div class="team-modal-stat-label">${stat.label}</div>
<div class="team-modal-stat-value">${s.value}</div>
<div class="team-modal-stat-label">${s.label}</div>
</div>
`).join('');
modalSkills.innerHTML = (member.skills || []).map(s => `<span class="team-modal-skill">${s}</span>`).join('');
modalSkills.innerHTML = (m.skills || []).map(s => `<span class="team-modal-skill">${s}</span>`).join('');
modalSocial.innerHTML = `
<a href="#" aria-label="Discord"><i data-feather="message-circle"></i></a>
<a href="#" aria-label="Twitter"><i data-feather="twitter"></i></a>
<a href="https://discord.gg/FwvNkvu5dy" target="_blank" rel="noopener noreferrer" aria-label="Discord">
<svg viewBox="0 0 24 24" fill="currentColor" width="18" height="18"><path d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0 12.64 12.64 0 0 0-.617-1.25.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057 19.9 19.9 0 0 0 5.993 3.03.078.078 0 0 0 .084-.028c.462-.63.874-1.295 1.226-1.994a.076.076 0 0 0-.041-.106 13.107 13.107 0 0 1-1.872-.892.077.077 0 0 1-.008-.128 10.2 10.2 0 0 0 .372-.292.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127 12.299 12.299 0 0 1-1.873.892.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028 19.839 19.839 0 0 0 6.002-3.03.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.956-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.955-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.946 2.418-2.157 2.418z"/></svg>
</a>
<a href="#" target="_blank" rel="noopener noreferrer" aria-label="X (Twitter)">
<svg viewBox="0 0 24 24" fill="currentColor" width="18" height="18"><path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-4.714-6.231-5.401 6.231H2.746l7.73-8.835L1.254 2.25H8.08l4.259 5.631 5.905-5.631zm-1.161 17.52h1.833L7.084 4.126H5.117z"/></svg>
</a>
`;
if (window.feather && feather.replace) feather.replace();
modalOverlay.classList.add('active');
document.body.style.overflow = 'hidden';
}
@ -156,16 +347,68 @@ document.addEventListener('DOMContentLoaded', () => {
document.body.style.overflow = '';
}
// Attach click listeners to generated cards
carousel.querySelectorAll('.team-card').forEach(card => {
card.addEventListener('click', () => {
const idx = parseInt(card.dataset.member, 10);
openMemberModal(idx);
});
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.classList.contains('active')) 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 });
}
})();

View file

@ -86,6 +86,15 @@ document.addEventListener('DOMContentLoaded', async () => {
// wait for includes to be loaded so subsequent initialization can bind to elements inside them
await loadIncludes();
// Déclenche les reveals maintenant que tous les includes sont dans le DOM
setTimeout(() => {
document.querySelectorAll('.reveal:not(.active)').forEach(el => {
if (el.getBoundingClientRect().top < window.innerHeight - 50) {
el.classList.add('active');
}
});
}, 0);
const html = document.documentElement;
function getPreferredTheme() {
@ -367,27 +376,27 @@ document.addEventListener('DOMContentLoaded', async () => {
window.addEventListener('hashchange', wrappedSetActive);
window.addEventListener('popstate', wrappedSetActive);
// Reveal on scroll
const revealElements = document.querySelectorAll('.reveal');
// Reveal on scroll — re-query après chargement des includes
function revealOnScroll() {
if (!revealElements.length) return;
revealElements.forEach(el => {
const elementTop = el.getBoundingClientRect().top;
if (elementTop < window.innerHeight - 100) {
const els = document.querySelectorAll('.reveal:not(.active)');
els.forEach(el => {
if (el.getBoundingClientRect().top < window.innerHeight - 50) {
el.classList.add('active');
}
});
}
window.addEventListener('scroll', revealOnScroll);
// Déclenche immédiatement après includes + au scroll + au load
revealOnScroll();
window.addEventListener('scroll', revealOnScroll, { passive: true });
window.addEventListener('load', revealOnScroll);
// Contact form (if exists)
const contactForm = document.getElementById('contact-form');
if (contactForm) {
contactForm.addEventListener('submit', function (e) {
e.preventDefault();
alert('Merci pour votre message ! Nous vous répondrons très bientôt.');
this.reset();
});
}
// Contact form (if exists) c'est en double
// const contactForm = document.getElementById('contact-form');
// if (contactForm) {
// contactForm.addEventListener('submit', function (e) {
// e.preventDefault();
// alert('Merci pour votre message ! Nous vous répondrons très bientôt.');
// this.reset();
// });
// }
});

View file

@ -1,7 +1,9 @@
// Page script for /pages/realisations.html
// Page script for /pages/realisations.html et index.html
document.addEventListener('DOMContentLoaded', async () => {
const dataUrl = '../assets/data/projects.json';
// Fonctionne depuis /pages/ et depuis / (index.html)
const isRoot = !window.location.pathname.includes('/pages/');
const dataUrl = isRoot ? 'assets/data/projects.json' : '../assets/data/projects.json';
let allProjects = [];
try {
@ -133,35 +135,80 @@ document.addEventListener('DOMContentLoaded', async () => {
portfolioGrid.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
// ── Modal tab switching ──────────────────────────────────────────────────────
const modalTabs = document.querySelectorAll('.project-modal-tab');
modalTabs.forEach(tab => {
tab.addEventListener('click', () => {
const tabId = tab.dataset.tab;
modalTabs.forEach(t => t.classList.remove('active'));
tab.classList.add('active');
document.querySelectorAll('.project-modal-tab-content').forEach(c => {
c.classList.toggle('active', c.id === 'tab-' + tabId);
});
});
});
function resetModalTabs() {
modalTabs.forEach((t, i) => t.classList.toggle('active', i === 0));
document.querySelectorAll('.project-modal-tab-content').forEach((c, i) => c.classList.toggle('active', i === 0));
}
function openProjectModal(projectId) {
const project = allProjects.find(p => p.id === Number(projectId));
if (!project) return;
resetModalTabs();
modalCategory.textContent = getCategoryLabel(project.category);
modalTitle.textContent = project.title;
modalDesc.textContent = project.desc;
modalMeta.innerHTML = `<span><i data-feather="calendar"></i> ${project.year}</span> <span><i data-feather="clock"></i> ${project.duration}</span>`;
modalMeta.innerHTML = `<span><i data-feather="calendar"></i> ${project.year}</span>${project.duration ? ` <span><i data-feather="clock"></i> ${project.duration}</span>` : ''}`;
modalFeatures.innerHTML = project.features.map(f => `<div class="project-feature-item">${f}</div>`).join('');
modalTech.innerHTML = project.tech.map(t => `<div class="project-tech-item">${t}</div>`).join('');
// Tech avec icônes (support ancien format string et nouveau format {name, icon})
modalTech.innerHTML = project.tech.map(t => {
const name = typeof t === 'object' ? t.name : t;
const icon = typeof t === 'object' ? (t.icon || 'code') : 'code';
return `<div class="project-tech-item"><i data-feather="${icon}"></i>${name}</div>`;
}).join('');
// Stats en grille 2×2 avec icônes
modalStats.innerHTML = project.stats.map(s => `
<div class="project-stat-card">
<div class="project-stat-card-icon"><i data-feather="${s.icon || 'star'}"></i></div>
<div class="project-stat-card-value">${s.value}</div>
<div class="project-stat-card-label">${s.label}</div>
</div>
`).join('');
// gallery slides (use gradient as placeholder slides like original)
gallerySlides.innerHTML = `
<div class="project-gallery-slide" style="background: ${project.gradient};"></div>
<div class="project-gallery-slide" style="background: ${project.gradient}; filter: hue-rotate(30deg);"></div>
<div class="project-gallery-slide" style="background: ${project.gradient}; filter: hue-rotate(-30deg);"></div>
`;
// Gallery : charger les images du dossier via convention de nommage
// images/projects/{slug}/1.png, 2.png... avec fallback gradient
const imgPath = project.images || '';
const imgExtensions = ['png', 'jpg', 'jpeg', 'webp'];
// On tente de charger jusqu'à 6 images numérotées depuis le dossier
const maxImages = 6;
let slidesHtml = '';
// Toujours créer au moins 1 slide gradient (fallback)
// Les images réelles se chargent avec onerror pour masquer si absentes
for (let i = 1; i <= maxImages; i++) {
slidesHtml += `
<div class="project-gallery-slide" data-img-idx="${i}" style="background: ${project.gradient};">
<img src="${imgPath}${i}.png" alt="${project.title} ${i}"
class="gallery-slide-img"
onerror="this.parentElement.classList.add('no-image')"
onload="this.parentElement.classList.add('has-image')">
</div>`;
}
gallerySlides.innerHTML = slidesHtml;
// reset gallery state
galleryIndex = 0;
updateGallery();
updateGalleryDots();
if (modalOverlay) modalOverlay.classList.add('active');
@ -176,16 +223,60 @@ document.addEventListener('DOMContentLoaded', async () => {
// Gallery helpers
let galleryIndex = 0;
function getVisibleSlides() {
if (!gallerySlides) return [];
// Retourne uniquement les slides qui ont une image chargée OU qui sont la première (fallback gradient)
const all = Array.from(gallerySlides.children);
const withImg = all.filter(s => s.classList.contains('has-image'));
return withImg.length > 0 ? withImg : [all[0]].filter(Boolean);
}
function updateGallery() {
const slides = gallerySlides ? Array.from(gallerySlides.children) : [];
slides.forEach((s, i) => {
const visible = getVisibleSlides();
const all = gallerySlides ? Array.from(gallerySlides.children) : [];
// Cache toutes les slides non visibles
all.forEach(s => {
if (!visible.includes(s)) { s.style.display = 'none'; }
else { s.style.display = ''; }
});
// Positionne les slides visibles
visible.forEach((s, i) => {
s.style.transform = `translateX(${(i - galleryIndex) * 100}%)`;
s.classList.toggle('active', i === galleryIndex);
});
// Clamp index
if (galleryIndex >= visible.length) galleryIndex = Math.max(0, visible.length - 1);
}
function prevSlide() { galleryIndex = Math.max(0, galleryIndex - 1); updateGallery(); }
function nextSlide() { const slides = gallerySlides ? gallerySlides.children.length : 0; galleryIndex = Math.min(slides - 1, galleryIndex + 1); updateGallery(); }
function updateGalleryDots() {
const galleryDots = document.getElementById('gallery-dots');
if (!galleryDots) return;
// Attend que les images se chargent avant de créer les dots
setTimeout(() => {
const visible = getVisibleSlides();
galleryDots.innerHTML = visible.map((_, i) =>
`<div class="gallery-dot${i === 0 ? ' active' : ''}" data-idx="${i}"></div>`
).join('');
galleryDots.querySelectorAll('.gallery-dot').forEach(dot => {
dot.addEventListener('click', () => {
galleryIndex = parseInt(dot.dataset.idx);
updateGallery();
galleryDots.querySelectorAll('.gallery-dot').forEach((d, i) => d.classList.toggle('active', i === galleryIndex));
});
});
}, 300);
}
function prevSlide() {
const visible = getVisibleSlides();
galleryIndex = galleryIndex <= 0 ? visible.length - 1 : galleryIndex - 1;
updateGallery();
}
function nextSlide() {
const visible = getVisibleSlides();
galleryIndex = galleryIndex >= visible.length - 1 ? 0 : galleryIndex + 1;
updateGallery();
}
// Event bindings
filterTabs.forEach(tab => tab.addEventListener('click', (e) => {
@ -222,6 +313,60 @@ document.addEventListener('DOMContentLoaded', async () => {
window.openProjectModal = openProjectModal;
window.goToPage = goToPage;
// ── Projets phares (bento grid) — 3 premiers projets du JSON ───────────────
const featuredBento = document.getElementById('featured-bento-grid');
if (featuredBento) {
const featured = allProjects.slice(0, 3);
const categoryLabels = { bot: 'Bot Discord', map: 'Fortnite Map', graphisme: 'Graphisme' };
featuredBento.innerHTML = featured.map((p, i) => {
const isReverse = i % 2 === 1 ? ' reverse' : '';
const statsHtml = (p.stats || []).slice(0, 4).map((s) => `
<div class="bento-stat">
<div class="bento-stat-icon"><i data-feather="${s.icon || 'star'}"></i></div>
<div class="bento-stat-value">${s.value}</div>
<div class="bento-stat-label">${s.label}</div>
</div>
`).join('');
const featuresHtml = (p.features || []).slice(0, 6).map(f => `<span class="bento-feature">${f}</span>`).join('');
const divider = i < featured.length - 1 ? '<div class="bento-divider"></div>' : '';
return `
<div class="bento-project${isReverse} reveal" data-project-id="${p.id}" style="cursor:pointer;" role="button" tabindex="0" aria-label="Voir le projet ${p.title}">
<div class="bento-image" style="background: ${p.gradient};">
<div class="bento-image-content">
<span class="portfolio-tag">${categoryLabels[p.category] || p.category}</span>
<h3>${p.title}</h3>
<p>${p.desc.substring(0, 100)}${p.desc.length > 100 ? '…' : ''}</p>
</div>
</div>
<div class="bento-stats">${statsHtml}</div>
<div class="bento-info">
<h4>Description</h4>
<p>${p.desc}</p>
<div class="bento-features">${featuresHtml}</div>
</div>
</div>
${divider}
`;
}).join('');
// Replace feather icons
if (window.feather && feather.replace) { try { feather.replace(); } catch(e) {} }
// Click to open modal
featuredBento.querySelectorAll('.bento-project').forEach(el => {
const open = () => openProjectModal(el.getAttribute('data-project-id'));
el.addEventListener('click', open);
el.addEventListener('keydown', e => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); open(); } });
});
// Reveal animation
setTimeout(() => {
featuredBento.querySelectorAll('.reveal').forEach(el => el.classList.add('active'));
}, 80);
}
// Initial render
renderProjects();
renderPagination();

View file

@ -68,6 +68,11 @@
</div>`).join('');
}
function renderNote(note) {
if (!note || !note.length) return '';
return `<p class="service-note">${note.map(n => `<span class="service-note-mark">${n.value}</span> ${n.label}<br>`).join(' ')}</p>`;
}
function renderCtas(ctas) {
return ctas.map(c => `
<a href="${c.href}" class="btn btn-${c.style}">
@ -97,6 +102,9 @@
<div class="service-tab-features">
${renderFeatures(service.features, accentFrom, accentTo)}
</div>
<div class="service-tab-note">
${renderNote(service.note)}
</div>
<div class="service-tab-cta">
${renderCtas(service.cta)}
</div>