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