// 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: '#', epicgames: 'https://www.fortnite.com/@omega_kube_team', email: 'contact@omegakube.fr' }; // SVGs des réseaux — utilisé partout dans les modals const SVG_ICONS = { discord: '', fortnite: '', epicgames: '', minecraft: '', gmod: '', hytale: '', twitter: '', instagram: '', youtube: '', tiktok: '', github: '', website: '', }; /** * 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 entries = [ { key: 'discord', url: S.discord || discordFallback }, { key: 'fortnite', url: S.fortnite }, { key: 'twitter', url: S.twitter }, { key: 'instagram', url: S.instagram }, { key: 'youtube', url: S.youtube }, { key: 'tiktok', url: S.tiktok }, { key: 'epicgames', url: S.epicgames }, { key: 'github', url: S.github }, { key: 'website', url: S.website }, ]; return entries .filter(e => e.url && e.url !== '#' && e.url !== '') .map(e => `${SVG_ICONS[e.key] || ''}`) .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'; function ensureFeather() { if (window.__featherLoading) return window.__featherLoading; if (window.feather && window.feather.replace) return Promise.resolve(window.feather); window.__featherLoading = new Promise((resolve, reject) => { const s = document.createElement('script'); s.src = FEATHER_URL; s.async = true; s.onload = () => resolve(window.feather); s.onerror = () => reject(new Error('Failed to load feather-icons')); document.head.appendChild(s); }); return window.__featherLoading; } window.ensureFeather = ensureFeather; document.addEventListener('DOMContentLoaded', async () => { // ensure feather icons are available and replace icons in document try { await ensureFeather(); if (window.feather && feather.replace) try { feather.replace(); } catch(e) {} } catch (e) {} // Load HTML includes (components header/footer) and fix relative paths when necessary async function loadIncludes() { const includeSelector = '[data-include]'; const nodes = Array.from(document.querySelectorAll(includeSelector)); if (!nodes.length) return; await Promise.all(nodes.map(async el => { const url = el.getAttribute('data-include'); try { const res = await fetch(url); if (!res.ok) throw new Error(res.statusText); const text = await res.text(); const template = document.createElement('template'); template.innerHTML = text; const fragment = template.content; // adjust relative paths for assets and page links when included from /pages/ fragment.querySelectorAll('[href], [src]').forEach(node => { ['href','src'].forEach(attr => { if (!node.hasAttribute(attr)) return; const original = node.getAttribute(attr); // leave absolute URLs and anchors untouched if (/^(https?:|\/|#)/.test(original)) return; // compute useful bases const includeUrl = new URL(url, document.baseURI); const includeDir = includeUrl.pathname.substring(0, includeUrl.pathname.lastIndexOf('/') + 1); const docUrl = new URL(document.location.href); const docDir = docUrl.pathname.substring(0, docUrl.pathname.lastIndexOf('/') + 1); // find common prefix between include location and current document to infer site root const includeParts = includeDir.split('/'); const docParts = docDir.split('/'); let i = 0; while (i < includeParts.length && i < docParts.length && includeParts[i] === docParts[i]) i++; const baseRoot = includeParts.slice(0, i).join('/') || '/'; let newVal; if (original.startsWith('./') || original.startsWith('../')) { // explicit relative paths should resolve relative to the include file const resolved = new URL(original, includeUrl); newVal = resolved.pathname + resolved.search + resolved.hash; } else { // treat plain paths (e.g. "assets/..." or "pages/...") as site-root relative newVal = (baseRoot.endsWith('/') ? baseRoot : baseRoot + '/') + original; } node.setAttribute(attr, newVal); }); }); el.replaceWith(fragment); } catch (err) { console.error('Failed to load include', url, err); } })); if (typeof ensureFeather === 'function') { ensureFeather().then(() => { try { if (window.feather && feather.replace) feather.replace(); } catch (e) {} }).catch(()=>{}); } else if (window.feather && feather.replace) { try { feather.replace(); } catch (e) {} } } // 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 : '#', 'epicgames': S.epicgames, }; 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 => { if (el.getBoundingClientRect().top < window.innerHeight - 50) { el.classList.add('active'); } }); }, 0); const html = document.documentElement; function getPreferredTheme() { const savedTheme = localStorage.getItem('theme'); if (savedTheme) return savedTheme; return window.matchMedia && window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark'; } // Apply preferred theme try { html.setAttribute('data-theme', getPreferredTheme()); } catch (e) {} // Theme toggle (robust) const themeToggle = document.getElementById('theme-toggle'); if (themeToggle) { // sync aria state themeToggle.setAttribute('aria-pressed', html.getAttribute('data-theme') === 'dark' ? 'false' : 'true'); themeToggle.addEventListener('click', () => { const currentTheme = html.getAttribute('data-theme'); const newTheme = currentTheme === 'dark' ? 'light' : 'dark'; html.setAttribute('data-theme', newTheme); localStorage.setItem('theme', newTheme); themeToggle.setAttribute('aria-pressed', newTheme === 'dark' ? 'false' : 'true'); }); } // Navbar scroll effect const navbar = document.getElementById('navbar'); function updateNavbarOnScroll() { if (!navbar) return; navbar.classList.toggle('scrolled', window.scrollY > 50); } window.addEventListener('scroll', updateNavbarOnScroll); window.addEventListener('load', updateNavbarOnScroll); // Mobile menu const mobileToggle = document.getElementById('mobile-toggle'); const navLinks = document.getElementById('nav-links'); // mobile menu initialization (debug logs removed) // create a toggle function that is safe even if elements are added later const toggleMenu = () => { const mt = document.getElementById('mobile-toggle'); const nl = document.getElementById('nav-links'); // console.log('[DEBUG] toggleMenu called', { mt: !!mt, nl: !!nl, mtClass: mt ? mt.className : null, nlClass: nl ? nl.className : null }); if (!mt || !nl) { // elements missing: abort toggle // console.log('[DEBUG] toggleMenu aborted: missing elements', { mt, nl }); return; } mt.classList.toggle('active'); nl.classList.toggle('active'); // console.log('[DEBUG] toggleMenu toggled', { mtClass: mt.className, nlClass: nl.className }); const expanded = nl.classList.contains('active') ? 'true' : 'false'; mt.setAttribute('aria-expanded', expanded); }; // |||||||||||||| ça fait buguer le menu mobile |||||||||||||| // direct binding if elements exist // if (mobileToggle && navLinks) { // if (!mobileToggle.hasAttribute('tabindex')) mobileToggle.setAttribute('tabindex', '0'); // mobileToggle.setAttribute('aria-expanded', navLinks.classList.contains('active') ? 'true' : 'false'); // mobileToggle.addEventListener('click', (e) => { // console.log('[DEBUG] mobileToggle click handler fired', e.target, e); // e.preventDefault(); // toggleMenu(); // }); // mobileToggle.addEventListener('keydown', (e) => { // if (e.key === 'Enter' || e.key === ' ') { // console.log('[DEBUG] mobileToggle keydown:', e.key); // e.preventDefault(); // toggleMenu(); // } // }); // navLinks.querySelectorAll('a').forEach(link => { // link.addEventListener('click', () => { // console.log('[DEBUG] nav link clicked in mobile menu', link.getAttribute('href')); // // collapse on link click // const mt = document.getElementById('mobile-toggle'); // const nl = document.getElementById('nav-links'); // if (mt && nl) { // mt.classList.remove('active'); // nl.classList.remove('active'); // mt.setAttribute('aria-expanded', 'false'); // } // }); // }); // } // Global delegation: catch clicks anywhere on page for '#mobile-toggle' (works even if element is replaced) document.addEventListener('click', (e) => { const mt = e.target.closest ? e.target.closest('#mobile-toggle') : null; if (mt) { // console.log('[DEBUG] delegated click caught for #mobile-toggle', e.target); e.preventDefault(); toggleMenu(); } }); // expose helper for console debugging window.toggleMobileMenu = toggleMenu; // Smooth scroll for anchor links document.querySelectorAll('a[href^="#"]').forEach(anchor => { anchor.addEventListener('click', function (e) { const href = this.getAttribute('href'); if (!href || href === '#') return; const target = document.querySelector(href); if (target) { e.preventDefault(); target.scrollIntoView({ behavior: 'smooth', block: 'start' }); } }); }); // Highlight active nav link based on current URL function normalizePath(path) { if (!path) return '/'; // treat both '/' and '/index.html' as root if (path.endsWith('index.html')) { const p = path.slice(0, -'index.html'.length); return p === '' ? '/' : p; } return path.endsWith('/') ? path : path; } function setActiveNavLink() { const links = document.querySelectorAll('#nav-links a[href]'); if (!links.length) return; const currentPath = normalizePath(window.location.pathname); const currentHash = window.location.hash || ''; let bestMatch = null; let bestScore = -1; links.forEach(link => { const href = link.getAttribute('href'); let linkUrl; try { linkUrl = new URL(href, document.location.href); } catch (e) { return; } const linkPath = normalizePath(linkUrl.pathname); const linkHash = linkUrl.hash || ''; let score = 0; if (linkPath === currentPath) score += 2; if (linkHash && linkHash === currentHash) score += 4; // Prefer exact hash matches, then exact path matches if (score > bestScore) { bestScore = score; bestMatch = link; } }); // Fallback: if on root (/) and there is an index link, pick it if (!bestMatch) { links.forEach(link => { if (!bestMatch && /index\.html$|\/$/.test(link.getAttribute('href') || '')) { bestMatch = link; } }); } links.forEach(l => { l.classList.remove('active'); l.removeAttribute('aria-current'); }); if (bestMatch) { bestMatch.classList.add('active'); bestMatch.setAttribute('aria-current', 'page'); } } // initialize and listen for navigation/hash changes setActiveNavLink(); window.addEventListener('hashchange', setActiveNavLink); window.addEventListener('popstate', setActiveNavLink); // Home page: observe sections so #apropos and #contact become active when visible, // and revert to "Accueil" when neither section is in view. let homeSectionObserver = null; let homeVisibility = {}; function initHomeSectionObserver() { // clean existing observer if (homeSectionObserver) { homeSectionObserver.disconnect(); homeSectionObserver = null; homeVisibility = {}; } // Instead of requiring a strict '/' path, detect whether the current document // actually contains any sections referenced by nav links (e.g. #apropos, #contact). const links = Array.from(document.querySelectorAll('#nav-links a[href]')); const sectionLinks = links.filter(link => { try { const u = new URL(link.getAttribute('href'), document.location.href); if (!u.hash) return false; return !!document.querySelector(u.hash); } catch (e) { return false; } }); if (!sectionLinks.length) return; const sections = sectionLinks.map(link => { const hash = new URL(link.getAttribute('href'), document.location.href).hash; const el = document.querySelector(hash); return el ? { id: el.id, el, link } : null; }).filter(Boolean); if (!sections.length) return; // setup observer homeSectionObserver = new IntersectionObserver((entries) => { entries.forEach(entry => { homeVisibility[entry.target.id] = entry.intersectionRatio || 0; }); // pick the section with highest intersection ratio let bestId = null; let bestRatio = 0; Object.entries(homeVisibility).forEach(([id, ratio]) => { if (ratio > bestRatio) { bestRatio = ratio; bestId = id; } }); const navLinks = Array.from(document.querySelectorAll('#nav-links a[href]')); // if a section is sufficiently visible, activate its link if (bestId && bestRatio > 0.15) { const targetLink = navLinks.find(l => { try { return new URL(l.getAttribute('href'), document.location.href).hash === '#' + bestId; } catch (e) { return false; } }); if (targetLink) { navLinks.forEach(l => { l.classList.remove('active'); l.removeAttribute('aria-current'); }); targetLink.classList.add('active'); targetLink.setAttribute('aria-current', 'page'); return; } } // fallback: set Accueil active when no section is visible const indexLink = Array.from(navLinks).find(l => /index\.html$|\/$/.test(l.getAttribute('href') || '')); navLinks.forEach(l => { l.classList.remove('active'); l.removeAttribute('aria-current'); }); if (indexLink) { indexLink.classList.add('active'); indexLink.setAttribute('aria-current', 'page'); } }, { root: null, threshold: [0, 0.15, 0.3, 0.6] }); sections.forEach(s => { homeVisibility[s.id] = 0; homeSectionObserver.observe(s.el); }); } // initialize observer on load and update when navigation changes initHomeSectionObserver(); window.addEventListener('resize', () => { if (homeSectionObserver) initHomeSectionObserver(); }); // re-init observer when URL path changes (i.e., leave/enter index) const originalSetActive = setActiveNavLink; function wrappedSetActive() { // if leaving index, disconnect observer const path = normalizePath(window.location.pathname); if (path !== '/') { if (homeSectionObserver) { homeSectionObserver.disconnect(); homeSectionObserver = null; homeVisibility = {}; } originalSetActive(); return; } // on index, ensure observer is active and let it control the highlight where possible initHomeSectionObserver(); originalSetActive(); } // replace handlers so navigation updates use our wrapped variant window.removeEventListener('hashchange', setActiveNavLink); window.removeEventListener('popstate', setActiveNavLink); window.addEventListener('hashchange', wrappedSetActive); window.addEventListener('popstate', wrappedSetActive); // Reveal on scroll — re-query après chargement des includes function revealOnScroll() { const els = document.querySelectorAll('.reveal:not(.active)'); els.forEach(el => { if (el.getBoundingClientRect().top < window.innerHeight - 50) { el.classList.add('active'); } }); } // Déclenche immédiatement après includes + au scroll + au load revealOnScroll(); window.addEventListener('scroll', revealOnScroll, { passive: true }); window.addEventListener('load', revealOnScroll); // Contact form (if exists) c'est en double // const contactForm = document.getElementById('contact-form'); // if (contactForm) { // contactForm.addEventListener('submit', function (e) { // e.preventDefault(); // alert('Merci pour votre message ! Nous vous répondrons très bientôt.'); // this.reset(); // }); // } });