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,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();