// 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() { const filtered = getFilteredProjects(); const projects = getProjectsForPage(filtered, currentPage); portfolioGrid.innerHTML = projects.map(project => `
${getCategoryLabel(project.category)}

${project.title}

${project.desc.substring(0, 80)}...

`).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() { 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 : 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 += ` `; } gallerySlides.innerHTML = slidesHtml; // reset gallery state galleryIndex = 0; updateGallery(); updateGalleryDots(); if (modalOverlay) modalOverlay.classList.add('active'); if (window.feather && feather.replace) { try { feather.replace(); } catch (e) {} } } function closeProjectModal() { if (modalOverlay) modalOverlay.classList.remove('active'); } // 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 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 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) => `` ).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) => { 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); }); 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(); }); 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 ? '
' : ''; return `
${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(); });