all updates + fix bug

This commit is contained in:
KIRITO_KMA 2026-04-07 01:51:50 +02:00
parent 78dc2e6e7f
commit f17ca2527c
44 changed files with 1087 additions and 521 deletions

View file

@ -12,16 +12,16 @@ document.addEventListener('DOMContentLoaded', () => {
const categoryLabels = { bot: 'Bot Discord', map: 'Fortnite Map', graphisme: 'Graphisme' };
homePortfolioGrid.innerHTML = first3.map((p, i) => {
const imgSrc = (p.images || '') + '1.png';
const firstFile = Array.isArray(p.files) && p.files.length > 0 ? p.files[0] : null;
const imgSrc = firstFile ? (p.images || '') + firstFile : null;
return `
<div class="portfolio-item reveal${delayClasses[i]}" data-project-id="${p.id}" role="button" tabindex="0" aria-label="Voir le projet ${p.title}">
<div class="portfolio-bg" style="background: ${p.gradient};">
<img src="${imgSrc}" alt="${p.title}" class="portfolio-cover-img" onerror="this.style.display='none'">
${imgSrc ? `<img src="${imgSrc}" alt="${p.title}" class="portfolio-cover-img" onerror="this.style.display='none'">` : ''}
</div>
<div class="portfolio-content">
<span class="portfolio-tag">${categoryLabels[p.category] || p.category}</span>
<h4>${p.title}</h4>
<p>${p.desc.substring(0, 80)}${p.desc.length > 80 ? '…' : ''}</p>
</div>
</div>
`;
@ -32,13 +32,24 @@ document.addEventListener('DOMContentLoaded', () => {
if (window.feather && feather.replace) try { feather.replace(); } catch(e) {}
}, 80);
// Délégation : attend que openProjectModal soit défini par realisations.js
// Délégation globale sur document — fonctionne même si realisations.js
// se charge après home.js (ordre des scripts defer non garanti)
homePortfolioGrid.addEventListener('click', e => {
const item = e.target.closest('.portfolio-item[data-project-id]');
if (!item) return;
const id = item.getAttribute('data-project-id');
if (typeof window.openProjectModal === 'function') {
window.openProjectModal(id);
} else {
// Retry : realisations.js pas encore initialisé, on attend
const wait = setInterval(() => {
if (typeof window.openProjectModal === 'function') {
clearInterval(wait);
window.openProjectModal(id);
}
}, 50);
// Abandon après 3 secondes
setTimeout(() => clearInterval(wait), 3000);
}
});
})
@ -163,16 +174,10 @@ document.addEventListener('DOMContentLoaded', () => {
</div>
`).join('');
const links = document.getElementById('partner-modal-links');
links.innerHTML = '';
if (p.url && p.url !== '#') {
links.innerHTML += `<a href="${p.url}" target="_blank" rel="noopener noreferrer" class="btn btn-secondary"><i data-feather="external-link"></i> Site web</a>`;
}
if (p.discord && p.discord !== '#') {
links.innerHTML += `<a href="${p.discord}" target="_blank" rel="noopener noreferrer" class="btn btn-primary">
<svg viewBox="0 0 24 24" fill="currentColor" width="16" height="16"><path d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0 12.64 12.64 0 0 0-.617-1.25.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057 19.9 19.9 0 0 0 5.993 3.03.078.078 0 0 0 .084-.028c.462-.63.874-1.295 1.226-1.994a.076.076 0 0 0-.041-.106 13.107 13.107 0 0 1-1.872-.892.077.077 0 0 1-.008-.128 10.2 10.2 0 0 0 .372-.292.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127 12.299 12.299 0 0 1-1.873.892.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028 19.839 19.839 0 0 0 6.002-3.03.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03z"/></svg>
Discord
</a>`;
}
// Réseaux sociaux depuis le champ social du partenaire
links.innerHTML = typeof window.renderSocialLinks === 'function'
? window.renderSocialLinks(p.social)
: '';
if (window.feather) feather.replace();
partnerModalOverlay.classList.add('active');
document.body.style.overflow = 'hidden';
@ -302,13 +307,13 @@ document.addEventListener('DOMContentLoaded', () => {
// Modal
const modalOverlay = document.getElementById('team-modal-overlay');
const modalClose = document.getElementById('team-modal-close');
const modalAvatar = document.getElementById('modal-avatar');
const modalName = document.getElementById('modal-name');
const modalRole = document.getElementById('modal-role');
const modalBio = document.getElementById('modal-bio');
const modalStats = document.getElementById('modal-stats');
const modalSkills = document.getElementById('modal-skills');
const modalSocial = document.getElementById('modal-social');
const modalAvatar = document.getElementById('team-modal-avatar');
const modalName = document.getElementById('team-modal-name');
const modalRole = document.getElementById('team-modal-role');
const modalBio = document.getElementById('team-modal-bio');
const modalStats = document.getElementById('team-modal-stats');
const modalSkills = document.getElementById('team-modal-skills');
const modalSocial = document.getElementById('team-modal-social');
function openMemberModal(index) {
const m = members[index];
@ -330,14 +335,11 @@ document.addEventListener('DOMContentLoaded', () => {
</div>
`).join('');
modalSkills.innerHTML = (m.skills || []).map(s => `<span class="team-modal-skill">${s}</span>`).join('');
modalSocial.innerHTML = `
<a href="https://discord.gg/FwvNkvu5dy" target="_blank" rel="noopener noreferrer" aria-label="Discord">
<svg viewBox="0 0 24 24" fill="currentColor" width="18" height="18"><path d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0 12.64 12.64 0 0 0-.617-1.25.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057 19.9 19.9 0 0 0 5.993 3.03.078.078 0 0 0 .084-.028c.462-.63.874-1.295 1.226-1.994a.076.076 0 0 0-.041-.106 13.107 13.107 0 0 1-1.872-.892.077.077 0 0 1-.008-.128 10.2 10.2 0 0 0 .372-.292.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127 12.299 12.299 0 0 1-1.873.892.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028 19.839 19.839 0 0 0 6.002-3.03.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.956-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.955-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.946 2.418-2.157 2.418z"/></svg>
</a>
<a href="#" target="_blank" rel="noopener noreferrer" aria-label="X (Twitter)">
<svg viewBox="0 0 24 24" fill="currentColor" width="18" height="18"><path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-4.714-6.231-5.401 6.231H2.746l7.73-8.835L1.254 2.25H8.08l4.259 5.631 5.905-5.631zm-1.161 17.52h1.833L7.084 4.126H5.117z"/></svg>
</a>
`;
// Réseaux sociaux depuis le JSON du membre (+ fallback discord global)
const socialHtml = typeof window.renderSocialLinks === 'function'
? window.renderSocialLinks(m.social, (window.OK_SOCIAL || {}).discord)
: '';
modalSocial.innerHTML = socialHtml;
modalOverlay.classList.add('active');
document.body.style.overflow = 'hidden';
}

View file

@ -1,5 +1,58 @@
// OmégaKube - shared scripts
// ═══════════════════════════════════════════════════════════════════════════
// CONFIGURATION GLOBALE — liens réseaux sociaux
// Modifier ici pour mettre à jour partout sur le site
// ═══════════════════════════════════════════════════════════════════════════
window.OK_SOCIAL = {
discord: 'https://discord.gg/FwvNkvu5dy',
fortnite: 'https://www.fortnite.com/@omega_kube_team',
twitter: 'https://x.com/Omegakube',
instagram: 'https://www.instagram.com/omegakube/',
youtube: 'https://www.youtube.com/@OmegaKube',
tiktok: 'https://www.tiktok.com/@omegakube',
facebook: '#',
bluesky: '#',
email: 'contact@omegakube.fr'
};
// SVGs des réseaux — utilisé partout dans les modals
const SVG_ICONS = {
discord: '<svg viewBox="0 0 24 24" fill="currentColor" width="18" height="18"><path d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0 12.64 12.64 0 0 0-.617-1.25.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057 19.9 19.9 0 0 0 5.993 3.03.078.078 0 0 0 .084-.028c.462-.63.874-1.295 1.226-1.994a.076.076 0 0 0-.041-.106 13.107 13.107 0 0 1-1.872-.892.077.077 0 0 1-.008-.128 10.2 10.2 0 0 0 .372-.292.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127 12.299 12.299 0 0 1-1.873.892.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028 19.839 19.839 0 0 0 6.002-3.03.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.956-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.955-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.946 2.418-2.157 2.418z"/></svg>',
fortnite: '<svg viewBox="0 0 24 24" fill="currentColor" width="18" height="18"><path d="M6 3h12a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2zm0 2v8h12V5H6zm2 2h2v1h1V7h2v4h-2V9h-1v2H8V7zm7 1a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm2 2a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm-4 4l-2 4h2l1-2 1 2h2l-2-4h-2z"/></svg>',
twitter: '<svg viewBox="0 0 24 24" fill="currentColor" width="18" height="18"><path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-4.714-6.231-5.401 6.231H2.746l7.73-8.835L1.254 2.25H8.08l4.259 5.631 5.905-5.631zm-1.161 17.52h1.833L7.084 4.126H5.117z"/></svg>',
instagram: '<svg viewBox="0 0 24 24" fill="currentColor" width="18" height="18"><path d="M12 2.163c3.204 0 3.584.012 4.85.07 3.252.148 4.771 1.691 4.919 4.919.058 1.265.069 1.645.069 4.849 0 3.205-.012 3.584-.069 4.849-.149 3.225-1.664 4.771-4.919 4.919-1.266.058-1.644.07-4.85.07-3.204 0-3.584-.012-4.849-.07-3.26-.149-4.771-1.699-4.919-4.92-.058-1.265-.07-1.644-.07-4.849 0-3.204.013-3.583.07-4.849.149-3.227 1.664-4.771 4.919-4.919 1.266-.057 1.645-.069 4.849-.069zm0-2.163c-3.259 0-3.667.014-4.947.072-4.358.2-6.78 2.618-6.98 6.98-.059 1.281-.073 1.689-.073 4.948 0 3.259.014 3.668.072 4.948.2 4.358 2.618 6.78 6.98 6.98 1.281.058 1.689.072 4.948.072 3.259 0 3.668-.014 4.948-.072 4.354-.2 6.782-2.618 6.979-6.98.059-1.28.073-1.689.073-4.948 0-3.259-.014-3.667-.072-4.947-.196-4.354-2.617-6.78-6.979-6.98-1.281-.059-1.69-.073-4.949-.073zm0 5.838c-3.403 0-6.162 2.759-6.162 6.162s2.759 6.163 6.162 6.163 6.162-2.759 6.162-6.163c0-3.403-2.759-6.162-6.162-6.162zm0 10.162c-2.209 0-4-1.79-4-4 0-2.209 1.791-4 4-4s4 1.791 4 4c0 2.21-1.791 4-4 4zm6.406-11.845c-.796 0-1.441.645-1.441 1.44s.645 1.44 1.441 1.44c.795 0 1.439-.645 1.439-1.44s-.644-1.44-1.439-1.44z"/></svg>',
youtube: '<svg viewBox="0 0 24 24" fill="currentColor" width="18" height="18"><path d="M23.498 6.186a3.016 3.016 0 0 0-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.017 3.017 0 0 0 .502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.016 3.016 0 0 0 2.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.015 3.015 0 0 0 2.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814zM9.545 15.568V8.432L15.818 12l-6.273 3.568z"/></svg>',
tiktok: '<svg viewBox="0 0 24 24" fill="currentColor" width="18" height="18"><path d="M19.59 6.69a4.83 4.83 0 0 1-3.77-4.25V2h-3.45v13.67a2.89 2.89 0 0 1-2.88 2.5 2.89 2.89 0 0 1-2.89-2.89 2.89 2.89 0 0 1 2.89-2.89c.28 0 .54.04.79.1V9.01a6.32 6.32 0 0 0-.79-.05 6.34 6.34 0 0 0-6.34 6.34 6.34 6.34 0 0 0 6.34 6.34 6.34 6.34 0 0 0 6.33-6.34V8.69a8.17 8.17 0 0 0 4.77 1.52V6.75a4.85 4.85 0 0 1-1-.06z"/></svg>',
github: '<svg viewBox="0 0 24 24" fill="currentColor" width="18" height="18"><path d="M12 0C5.374 0 0 5.373 0 12c0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23A11.509 11.509 0 0 1 12 5.803c1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576C20.566 21.797 24 17.3 24 12c0-6.627-5.373-12-12-12z"/></svg>',
website: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="18" height="18"><circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg>',
};
/**
* Génère le HTML des liens sociaux pour les modals
* @param {Object} social - objet {discord, twitter, instagram, ...}
* @param {string} [discordFallback] - lien discord global si pas de lien perso
* @returns {string} HTML des icônes sociales
*/
window.renderSocialLinks = function(social, discordFallback) {
const S = social || {};
const G = window.OK_SOCIAL || {};
const entries = [
{ key: 'discord', url: S.discord || discordFallback || G.discord },
{ key: 'fortnite', url: S.fortnite || G.fortnite },
{ key: 'twitter', url: S.twitter },
{ key: 'instagram', url: S.instagram },
{ key: 'youtube', url: S.youtube },
{ key: 'tiktok', url: S.tiktok },
{ key: 'github', url: S.github },
{ key: 'website', url: S.website },
];
return entries
.filter(e => e.url && e.url !== '#' && e.url !== '')
.map(e => `<a href="${e.url}" target="_blank" rel="noopener noreferrer" aria-label="${e.key}">${SVG_ICONS[e.key] || ''}</a>`)
.join('');
};
// Feather icons loader (centralized)
// loads https://unpkg.com/feather-icons when needed and exposes window.ensureFeather()
const FEATHER_URL = 'https://unpkg.com/feather-icons';
@ -86,6 +139,27 @@ document.addEventListener('DOMContentLoaded', async () => {
// wait for includes to be loaded so subsequent initialization can bind to elements inside them
await loadIncludes();
// Hydrate les liens sociaux (data-social) avec les URLs de OK_SOCIAL
(function hydrateSocialLinks() {
const S = window.OK_SOCIAL || {};
const map = {
'discord': S.discord,
'twitter': S.twitter,
'instagram': S.instagram,
'youtube': S.youtube,
'tiktok': S.tiktok,
'facebook': S.facebook,
'bluesky': S.bluesky,
'email-link': S.email ? 'mailto:' + S.email : '#',
};
document.querySelectorAll('[data-social]').forEach(el => {
const key = el.getAttribute('data-social');
if (map[key] && map[key] !== '#') {
el.setAttribute('href', map[key]);
}
});
})();
// Déclenche les reveals maintenant que tous les includes sont dans le DOM
setTimeout(() => {
document.querySelectorAll('.reveal:not(.active)').forEach(el => {

View file

@ -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>