all updates + fix bug
This commit is contained in:
parent
78dc2e6e7f
commit
f17ca2527c
44 changed files with 1087 additions and 521 deletions
|
|
@ -57,19 +57,24 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|||
}
|
||||
|
||||
function renderProjects() {
|
||||
if (!portfolioGrid) return; // pas sur la page réalisations
|
||||
const filtered = getFilteredProjects();
|
||||
const projects = getProjectsForPage(filtered, currentPage);
|
||||
|
||||
portfolioGrid.innerHTML = projects.map(project => `
|
||||
<div class="portfolio-item reveal" data-project-id="${project.id}">
|
||||
<div class="portfolio-bg" style="background: ${project.gradient};"></div>
|
||||
<div class="portfolio-content">
|
||||
<span class="portfolio-tag">${getCategoryLabel(project.category)}</span>
|
||||
<h4>${project.title}</h4>
|
||||
<p>${project.desc.substring(0, 80)}...</p>
|
||||
portfolioGrid.innerHTML = projects.map(project => {
|
||||
const prefix = isRoot ? '' : '../';
|
||||
const firstFile = Array.isArray(project.files) && project.files.length > 0 ? project.files[0] : null;
|
||||
const coverImg = firstFile ? `<img src="${prefix}${project.images}${firstFile}" alt="${project.title}" class="portfolio-cover-img" onerror="this.style.display='none'">` : '';
|
||||
return `
|
||||
<div class="portfolio-item reveal" data-project-id="${project.id}">
|
||||
<div class="portfolio-bg" style="background: ${project.gradient};">${coverImg}</div>
|
||||
<div class="portfolio-content">
|
||||
<span class="portfolio-tag">${getCategoryLabel(project.category)}</span>
|
||||
<h4>${project.title}</h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
// activate reveals & icons
|
||||
setTimeout(() => {
|
||||
|
|
@ -83,6 +88,7 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|||
}
|
||||
|
||||
function renderPagination() {
|
||||
if (!paginationContainer) return; // pas sur la page réalisations
|
||||
const filtered = getFilteredProjects();
|
||||
const totalPages = getTotalPages(filtered);
|
||||
|
||||
|
|
@ -183,35 +189,75 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|||
</div>
|
||||
`).join('');
|
||||
|
||||
// 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;
|
||||
// Gallery : utilise project.files (noms réels) avec fallback gradient si vide
|
||||
const imgPath = (isRoot ? '' : '../') + (project.images || '');
|
||||
const files = Array.isArray(project.files) ? project.files : [];
|
||||
|
||||
// reset gallery state
|
||||
if (files.length > 0) {
|
||||
// Images réelles nommées explicitement dans le JSON
|
||||
// Supporte : fichier local, mp4 local, URL YouTube, shortcode yt:VIDEO_ID
|
||||
gallerySlides.innerHTML = files.map((filename, i) => {
|
||||
const isLocalVideo = /\.(mp4|webm|ogg)$/i.test(filename);
|
||||
|
||||
// Détecte YouTube : URL complète ou shortcode yt:ID
|
||||
const ytMatch = filename.match(/(?:yt:|youtube\.com\/watch\?v=|youtu\.be\/)([\w-]+)/);
|
||||
|
||||
if (ytMatch) {
|
||||
const videoId = ytMatch[1];
|
||||
return `
|
||||
<div class="project-gallery-slide slide-right gallery-slide-youtube" data-img-idx="${i}" style="background:${project.gradient};">
|
||||
<div class="yt-wrapper">
|
||||
<iframe
|
||||
src="https://www.youtube.com/embed/${videoId}?rel=0&modestbranding=1"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||
allowfullscreen
|
||||
class="gallery-slide-yt-iframe"
|
||||
title="Vidéo YouTube ${project.title}"
|
||||
loading="lazy">
|
||||
</iframe>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
if (isLocalVideo) {
|
||||
return `
|
||||
<div class="project-gallery-slide slide-right" data-img-idx="${i}" style="background:${project.gradient};">
|
||||
<video class="gallery-slide-img gallery-video" src="${imgPath}${filename}" muted loop autoplay playsinline></video>
|
||||
<button class="gallery-mute-btn" aria-label="Activer le son" title="Activer le son" onclick="(function(btn){var v=btn.closest('.project-gallery-slide').querySelector('video');v.muted=!v.muted;btn.classList.toggle('unmuted',!v.muted);btn.setAttribute('aria-label',v.muted?'Activer le son':'Couper le son');})(this)">
|
||||
<svg class="icon-muted" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="18" height="18"><polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"/><line x1="23" y1="9" x2="17" y2="15"/><line x1="17" y1="9" x2="23" y2="15"/></svg>
|
||||
<svg class="icon-unmuted" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="18" height="18"><polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"/><path d="M19.07 4.93a10 10 0 0 1 0 14.14"/><path d="M15.54 8.46a5 5 0 0 1 0 7.07"/></svg>
|
||||
</button>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
return `
|
||||
<div class="project-gallery-slide slide-right" data-img-idx="${i}" style="background:${project.gradient};">
|
||||
<img src="${imgPath}${filename}" alt="${project.title} ${i + 1}"
|
||||
class="gallery-slide-img"
|
||||
onerror="this.parentElement.classList.add('no-image');this.style.display='none'">
|
||||
</div>`;
|
||||
}).join('');
|
||||
} else {
|
||||
// Fallback : une slide gradient unique
|
||||
gallerySlides.innerHTML = `
|
||||
<div class="project-gallery-slide has-image" style="background:${project.gradient};"></div>`;
|
||||
}
|
||||
|
||||
// reset gallery + affichage immédiat
|
||||
galleryIndex = 0;
|
||||
updateGallery();
|
||||
updateGalleryDots();
|
||||
|
||||
if (modalOverlay) modalOverlay.classList.add('active');
|
||||
|
||||
// Réseaux sociaux du projet
|
||||
const projectSocialEl = document.getElementById('modal-project-social');
|
||||
if (projectSocialEl) {
|
||||
projectSocialEl.innerHTML = typeof window.renderSocialLinks === 'function'
|
||||
? window.renderSocialLinks(project.social)
|
||||
: '';
|
||||
}
|
||||
|
||||
if (window.feather && feather.replace) {
|
||||
try { feather.replace(); } catch (e) {}
|
||||
}
|
||||
|
|
@ -223,88 +269,100 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|||
|
||||
// Gallery helpers
|
||||
let galleryIndex = 0;
|
||||
function getVisibleSlides() {
|
||||
function getAllSlides() {
|
||||
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);
|
||||
return Array.from(gallerySlides.children).filter(s => !s.classList.contains('no-image'));
|
||||
}
|
||||
|
||||
function updateGallery() {
|
||||
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}%)`;
|
||||
const slides = getAllSlides();
|
||||
if (!slides.length) return;
|
||||
if (galleryIndex >= slides.length) galleryIndex = 0;
|
||||
if (galleryIndex < 0) galleryIndex = slides.length - 1;
|
||||
slides.forEach((s, i) => {
|
||||
// Le CSS gère tout via .active — on ne touche JAMAIS à style.transform
|
||||
s.classList.toggle('active', i === galleryIndex);
|
||||
// Juste la direction (gauche/droite) via une classe data
|
||||
if (i < galleryIndex) {
|
||||
s.classList.add('slide-left');
|
||||
s.classList.remove('slide-right');
|
||||
} else if (i > galleryIndex) {
|
||||
s.classList.add('slide-right');
|
||||
s.classList.remove('slide-left');
|
||||
} else {
|
||||
s.classList.remove('slide-left', 'slide-right');
|
||||
}
|
||||
});
|
||||
// Clamp index
|
||||
if (galleryIndex >= visible.length) galleryIndex = Math.max(0, visible.length - 1);
|
||||
}
|
||||
|
||||
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));
|
||||
});
|
||||
const slides = getAllSlides();
|
||||
galleryDots.innerHTML = slides.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 syncDots() {
|
||||
const galleryDots = document.getElementById('gallery-dots');
|
||||
if (!galleryDots) return;
|
||||
galleryDots.querySelectorAll('.gallery-dot').forEach((d, i) =>
|
||||
d.classList.toggle('active', i === galleryIndex));
|
||||
}
|
||||
|
||||
function prevSlide() {
|
||||
const visible = getVisibleSlides();
|
||||
galleryIndex = galleryIndex <= 0 ? visible.length - 1 : galleryIndex - 1;
|
||||
galleryIndex = galleryIndex <= 0 ? getAllSlides().length - 1 : galleryIndex - 1;
|
||||
updateGallery();
|
||||
syncDots();
|
||||
}
|
||||
function nextSlide() {
|
||||
const visible = getVisibleSlides();
|
||||
galleryIndex = galleryIndex >= visible.length - 1 ? 0 : galleryIndex + 1;
|
||||
galleryIndex = galleryIndex >= getAllSlides().length - 1 ? 0 : galleryIndex + 1;
|
||||
updateGallery();
|
||||
syncDots();
|
||||
}
|
||||
|
||||
// Event bindings
|
||||
filterTabs.forEach(tab => tab.addEventListener('click', (e) => {
|
||||
filterTabs.forEach(t => t.classList.remove('active'));
|
||||
tab.classList.add('active');
|
||||
currentFilter = tab.dataset.filter || 'all';
|
||||
currentPage = 1;
|
||||
renderProjects();
|
||||
renderPagination();
|
||||
}));
|
||||
// Event bindings — uniquement sur la page réalisations
|
||||
if (filterTabs.length) {
|
||||
filterTabs.forEach(tab => tab.addEventListener('click', (e) => {
|
||||
filterTabs.forEach(t => t.classList.remove('active'));
|
||||
tab.classList.add('active');
|
||||
currentFilter = tab.dataset.filter || 'all';
|
||||
currentPage = 1;
|
||||
renderProjects();
|
||||
renderPagination();
|
||||
}));
|
||||
}
|
||||
|
||||
paginationContainer.addEventListener('click', (e) => {
|
||||
const btn = e.target.closest('button[data-page]');
|
||||
if (!btn) return;
|
||||
const page = Number(btn.getAttribute('data-page'));
|
||||
goToPage(page);
|
||||
});
|
||||
if (paginationContainer) {
|
||||
paginationContainer.addEventListener('click', (e) => {
|
||||
const btn = e.target.closest('button[data-page]');
|
||||
if (!btn) return;
|
||||
const page = Number(btn.getAttribute('data-page'));
|
||||
goToPage(page);
|
||||
});
|
||||
}
|
||||
|
||||
portfolioGrid.addEventListener('click', (e) => {
|
||||
const item = e.target.closest('.portfolio-item');
|
||||
if (!item) return;
|
||||
const id = item.getAttribute('data-project-id');
|
||||
openProjectModal(id);
|
||||
});
|
||||
if (portfolioGrid) {
|
||||
portfolioGrid.addEventListener('click', (e) => {
|
||||
const item = e.target.closest('.portfolio-item');
|
||||
if (!item) return;
|
||||
const id = item.getAttribute('data-project-id');
|
||||
openProjectModal(id);
|
||||
});
|
||||
}
|
||||
|
||||
if (modalClose) modalClose.addEventListener('click', closeProjectModal);
|
||||
if (modalOverlay) modalOverlay.addEventListener('click', (e) => { if (e.target === modalOverlay) closeProjectModal(); });
|
||||
window.addEventListener('keydown', (e) => { if (e.key === 'Escape' && modalOverlay.classList.contains('active')) closeProjectModal(); });
|
||||
window.addEventListener('keydown', (e) => { if (e.key === 'Escape' && modalOverlay && modalOverlay.classList.contains('active')) closeProjectModal(); });
|
||||
|
||||
if (galleryPrev) galleryPrev.addEventListener('click', prevSlide);
|
||||
if (galleryNext) galleryNext.addEventListener('click', nextSlide);
|
||||
|
|
@ -331,9 +389,16 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|||
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>' : '';
|
||||
|
||||
const prefix = isRoot ? '' : '../';
|
||||
const firstFile = Array.isArray(p.files) && p.files.length > 0 ? p.files[0] : null;
|
||||
const bentoCover = firstFile
|
||||
? `<img src="${prefix}${p.images}${firstFile}" alt="${p.title}" class="bento-cover-img" onerror="this.style.display='none'">`
|
||||
: '';
|
||||
|
||||
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};">
|
||||
${bentoCover}
|
||||
<div class="bento-image-content">
|
||||
<span class="portfolio-tag">${categoryLabels[p.category] || p.category}</span>
|
||||
<h3>${p.title}</h3>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue