website/assets/js/pages/realisations.js

438 lines
18 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Page script for /pages/realisations.html et index.html
document.addEventListener('DOMContentLoaded', async () => {
// 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 {
const res = await fetch(dataUrl);
if (!res.ok) throw new Error(res.statusText);
allProjects = await res.json();
} catch (err) {
console.error('Failed to load projects data:', err);
return;
}
const ITEMS_PER_PAGE = 9;
let currentPage = 1;
let currentFilter = 'all';
const portfolioGrid = document.getElementById('portfolio-grid');
const paginationContainer = document.getElementById('pagination');
const filterTabs = document.querySelectorAll('.filter-tab');
const modalOverlay = document.getElementById('project-modal-overlay');
const modalClose = document.getElementById('project-modal-close');
const gallerySlides = document.getElementById('gallery-slides');
const galleryPrev = document.getElementById('gallery-prev');
const galleryNext = document.getElementById('gallery-next');
const modalTitle = document.getElementById('modal-title');
const modalCategory = document.getElementById('modal-category');
const modalDesc = document.getElementById('modal-desc');
const modalMeta = document.getElementById('modal-meta');
const modalFeatures = document.getElementById('modal-features');
const modalTech = document.getElementById('modal-tech');
const modalStats = document.getElementById('modal-stats');
function getFilteredProjects() {
if (currentFilter === 'all') return [...allProjects];
return allProjects.filter(p => p.category === currentFilter);
}
function getTotalPages(filtered) {
return Math.ceil(filtered.length / ITEMS_PER_PAGE) || 1;
}
function getProjectsForPage(filtered, page) {
const start = (page - 1) * ITEMS_PER_PAGE;
return filtered.slice(start, start + ITEMS_PER_PAGE);
}
function getCategoryLabel(category) {
const labels = { 'bot': 'Bot Discord', 'map': 'Fortnite Map', 'graphisme': 'Graphisme' };
return labels[category] || category;
}
function renderProjects() {
if (!portfolioGrid) return; // pas sur la page réalisations
const filtered = getFilteredProjects();
const projects = getProjectsForPage(filtered, currentPage);
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>
`;
}).join('');
// activate reveals & icons
setTimeout(() => {
document.querySelectorAll('.portfolio-item.reveal').forEach(el => el.classList.add('active'));
}, 80);
// ensure icons are replaced for newly injected content
if (window.feather && feather.replace) {
try { feather.replace(); } catch (e) {}
}
}
function renderPagination() {
if (!paginationContainer) return; // pas sur la page réalisations
const filtered = getFilteredProjects();
const totalPages = getTotalPages(filtered);
if (totalPages <= 1) {
paginationContainer.innerHTML = '';
return;
}
let html = `<button class="pagination-btn pagination-arrow" data-page="${Math.max(1, currentPage - 1)}" ${currentPage === 1 ? 'disabled' : ''}><i data-feather="chevron-left"></i></button>`;
const maxVisible = 5;
let startPage = Math.max(1, currentPage - Math.floor(maxVisible / 2));
let endPage = Math.min(totalPages, startPage + maxVisible - 1);
if (endPage - startPage < maxVisible - 1) startPage = Math.max(1, endPage - maxVisible + 1);
if (startPage > 1) {
html += `<button class="pagination-btn" data-page="1">1</button>`;
if (startPage > 2) html += `<span class="pagination-ellipsis">...</span>`;
}
for (let i = startPage; i <= endPage; i++) {
html += `<button class="pagination-btn ${i === currentPage ? 'active' : ''}" data-page="${i}">${i}</button>`;
}
if (endPage < totalPages) {
if (endPage < totalPages - 1) html += `<span class="pagination-ellipsis">...</span>`;
html += `<button class="pagination-btn" data-page="${totalPages}">${totalPages}</button>`;
}
html += `<button class="pagination-btn pagination-arrow" data-page="${Math.min(totalPages, currentPage + 1)}" ${currentPage === totalPages ? 'disabled' : ''}><i data-feather="chevron-right"></i></button>`;
const startItem = (currentPage - 1) * ITEMS_PER_PAGE + 1;
const endItem = Math.min(currentPage * ITEMS_PER_PAGE, filtered.length);
html += `<div class="pagination-info">Affichage ${startItem}-${endItem} sur ${filtered.length} projets</div>`;
paginationContainer.innerHTML = html;
if (window.feather && feather.replace) {
try { feather.replace(); } catch (e) {}
}
}
function goToPage(page) {
const filtered = getFilteredProjects();
const totalPages = getTotalPages(filtered);
if (page < 1 || page > totalPages) return;
currentPage = page;
renderProjects();
renderPagination();
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>${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('');
// 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 : utilise project.files (noms réels) avec fallback gradient si vide
const imgPath = (isRoot ? '' : '../') + (project.images || '');
const files = Array.isArray(project.files) ? project.files : [];
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) {}
}
}
function closeProjectModal() {
if (modalOverlay) modalOverlay.classList.remove('active');
}
// Gallery helpers
let galleryIndex = 0;
function getAllSlides() {
if (!gallerySlides) return [];
return Array.from(gallerySlides.children).filter(s => !s.classList.contains('no-image'));
}
function updateGallery() {
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');
}
});
}
function updateGalleryDots() {
const galleryDots = document.getElementById('gallery-dots');
if (!galleryDots) return;
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));
});
});
}
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() {
galleryIndex = galleryIndex <= 0 ? getAllSlides().length - 1 : galleryIndex - 1;
updateGallery();
syncDots();
}
function nextSlide() {
galleryIndex = galleryIndex >= getAllSlides().length - 1 ? 0 : galleryIndex + 1;
updateGallery();
syncDots();
}
// 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();
}));
}
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);
});
}
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 && modalOverlay.classList.contains('active')) closeProjectModal(); });
if (galleryPrev) galleryPrev.addEventListener('click', prevSlide);
if (galleryNext) galleryNext.addEventListener('click', nextSlide);
// Expose for compatibility
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>' : '';
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>
<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();
});