v1.5.2
This commit is contained in:
parent
7d26ffcee4
commit
25ea6cf6f6
19 changed files with 2270 additions and 467 deletions
|
|
@ -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 });
|
||||
}
|
||||
})();
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
// });
|
||||
// }
|
||||
});
|
||||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue