// 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 ? `${project.title}` : ''; return `
${coverImg}
${getCategoryLabel(project.category)}

${project.title}

`; }).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 = ``; 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 += ``; if (startPage > 2) html += `...`; } for (let i = startPage; i <= endPage; i++) { html += ``; } if (endPage < totalPages) { if (endPage < totalPages - 1) html += `...`; html += ``; } html += ``; const startItem = (currentPage - 1) * ITEMS_PER_PAGE + 1; const endItem = Math.min(currentPage * ITEMS_PER_PAGE, filtered.length); html += `
Affichage ${startItem}-${endItem} sur ${filtered.length} projets
`; 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 = ` ${project.year}${project.duration ? ` ${project.duration}` : ''}`; modalFeatures.innerHTML = project.features.map(f => `
${f}
`).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 `
${name}
`; }).join(''); // Stats en grille 2×2 avec icônes modalStats.innerHTML = project.stats.map(s => `
${s.value}
${s.label}
`).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 ` `; } if (isLocalVideo) { return ` `; } return ` `; }).join(''); } else { // Fallback : une slide gradient unique gallerySlides.innerHTML = ` `; } // 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) => `` ).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) => `
${s.value}
${s.label}
`).join(''); const featuresHtml = (p.features || []).slice(0, 6).map(f => `${f}`).join(''); const divider = i < featured.length - 1 ? '
' : ''; const prefix = isRoot ? '' : '../'; const firstFile = Array.isArray(p.files) && p.files.length > 0 ? p.files[0] : null; const bentoCover = firstFile ? `${p.title}` : ''; return `
${bentoCover}
${categoryLabels[p.category] || p.category}

${p.title}

${p.desc.substring(0, 100)}${p.desc.length > 100 ? '…' : ''}

${statsHtml}

Description

${p.desc}

${featuresHtml}
${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(); });