2026-02-06 18:36:03 +01:00
|
|
|
// OmégaKube - shared scripts
|
|
|
|
|
|
2026-04-07 01:51:50 +02:00
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
|
// 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: '#',
|
2026-04-13 23:05:47 +02:00
|
|
|
epicgames: 'https://www.fortnite.com/@omega_kube_team',
|
2026-04-07 01:51:50 +02:00
|
|
|
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>',
|
2026-04-13 23:05:47 +02:00
|
|
|
fortnite: '<svg viewBox="0 0 24 24" fill="currentColor" width="18" height="18"><path d="M2.234 0v24h6.522v-8.035h5.9v-5.64h-5.9V5.636h7.964V0zm11.13 10.329v5.636h5.9v2.382h-5.9V24H21.766V10.329z"/></svg>',
|
2026-04-14 00:03:19 +02:00
|
|
|
epicgames: '<svg viewBox="0 0 24 24" fill="currentColor" width="18" height="18"><path d="M3.537 0C2.165 0 1.66.506 1.66 1.879V18.44a4.262 4.262 0 00.02.433c.031.3.037.59.316.92.027.033.311.245.311.245.153.075.258.13.43.2l8.335 3.491c.433.199.614.276.928.27h.002c.314.006.495-.071.928-.27l8.335-3.492c.172-.07.277-.124.43-.2 0 0 .284-.211.311-.243.28-.33.285-.621.316-.92a4.261 4.261 0 00.02-.434V1.879c0-1.373-.506-1.88-1.878-1.88zm13.366 3.11h.68c1.138 0 1.688.553 1.688 1.696v1.88h-1.374v-1.8c0-.369-.17-.54-.523-.54h-.235c-.367 0-.537.17-.537.539v5.81c0 .369.17.54.537.54h.262c.353 0 .523-.171.523-.54V8.619h1.373v2.143c0 1.144-.562 1.71-1.7 1.71h-.694c-1.138 0-1.7-.566-1.7-1.71V4.82c0-1.144.562-1.709 1.7-1.709zm-12.186.08h3.114v1.274H6.117v2.603h1.648v1.275H6.117v2.774h1.74v1.275h-3.14zm3.816 0h2.198c1.138 0 1.7.564 1.7 1.708v2.445c0 1.144-.562 1.71-1.7 1.71h-.799v3.338h-1.4zm4.53 0h1.4v9.201h-1.4zm-3.13 1.235v3.392h.575c.354 0 .523-.171.523-.54V4.965c0-.368-.17-.54-.523-.54zm-3.74 10.147a1.708 1.708 0 01.591.108 1.745 1.745 0 01.49.299l-.452.546a1.247 1.247 0 00-.308-.195.91.91 0 00-.363-.068.658.658 0 00-.28.06.703.703 0 00-.224.163.783.783 0 00-.151.243.799.799 0 00-.056.299v.008a.852.852 0 00.056.31.7.7 0 00.157.245.736.736 0 00.238.16.774.774 0 00.303.058.79.79 0 00.445-.116v-.339h-.548v-.565H7.37v1.255a2.019 2.019 0 01-.524.307 1.789 1.789 0 01-.683.123 1.642 1.642 0 01-.602-.107 1.46 1.46 0 01-.478-.3 1.371 1.371 0 01-.318-.455 1.438 1.438 0 01-.115-.58v-.008a1.426 1.426 0 01.113-.57 1.449 1.449 0 01.312-.46 1.418 1.418 0 01.474-.309 1.58 1.58 0 01.598-.111 1.708 1.708 0 01.045 0zm11.963.008a2.006 2.006 0 01.612.094 1.61 1.61 0 01.507.277l-.386.546a1.562 1.562 0 00-.39-.205 1.178 1.178 0 00-.388-.07.347.347 0 00-.208.052.154.154 0 00-.07.127v.008a.158.158 0 00.022.084.198.198 0 00.076.066.831.831 0 00.147.06c.062.02.14.04.236.061a3.389 3.389 0 01.43.122 1.292 1.292 0 01.328.17.678.678 0 01.207.24.739.739 0 01.071.337v.008a.865.865 0 01-.081.382.82.82 0 01-.229.285 1.032 1.032 0 01-.353.18 1.606 1.606 0 01-.46.061 2.16 2.16 0 01-.71-.116 1.718 1.718 0 01-.593-.346l.43-.514c.277.223.578.335.9.335a.457.457 0 00.236-.05.157.157 0 00.082-.142v-.008a.15.15 0 00-.02-.077.204.204 0 00-.073-.066.753.753 0 00-.143-.062 2.45 2.45 0 00-.233-.062 5.036 5.036 0 01-.413-.113 1.26 1.26 0 01-.331-.16.72.72 0 01-.222-.243.73.73 0 01-.082-.36v-.008a.863.863 0 01.074-.359.794.794 0 01.214-.283 1.007 1.007 0 01.34-.185 1.423 1.423 0 01.448-.066 2.006 2.006 0 01.025 0zm-9.358.025h.742l1.183 2.81h-.825l-.203-.499H8.623l-.198.498h-.81zm2.197.02h.814l.663 1.08.663-1.08h.814v2.79h-.766v-1.602l-.711 1.091h-.016l-.707-1.083v1.593h-.754zm3.469 0h2.235v.658h-1.473v.422h1.334v.61h-1.334v.442h1.493v.658h-2.255zm-5.3.897l-.315.793h.624zm-1.145 5.19h8.014l-4.09 1.348z"/></svg>',
|
2026-04-13 23:05:47 +02:00
|
|
|
minecraft: '<svg viewBox="0 0 24 24" fill="currentColor" width="18" height="18"><path d="M3.261 0A3.261 3.261 0 0 0 0 3.261V20.74A3.261 3.261 0 0 0 3.261 24H20.74A3.261 3.261 0 0 0 24 20.739V3.261A3.261 3.261 0 0 0 20.739 0zm4.103 3.836h3.27v3.268h3.268V3.836h3.27v3.268h-3.27v3.27h3.27v3.267h-3.27v3.27h-3.268v-3.27H7.364v-3.268h3.268V7.104H7.364z"/></svg>',
|
|
|
|
|
gmod: '<svg viewBox="0 0 24 24" fill="currentColor" width="18" height="18"><path d="M12 0C5.373 0 0 5.373 0 12s5.373 12 12 12 12-5.373 12-12S18.627 0 12 0zm0 2c5.514 0 10 4.486 10 10s-4.486 10-10 10S2 17.514 2 12 6.486 2 12 2zm1 4h-2v6H7v2h4v4h2v-4h4v-2h-4z"/></svg>',
|
|
|
|
|
hytale: '<svg viewBox="0 0 24 24" fill="currentColor" width="18" height="18"><path d="M1.117 0v24h5.412v-9.386h10.942V24h5.412V0h-5.412v9.386H6.529V0z"/></svg>',
|
2026-04-07 01:51:50 +02:00
|
|
|
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 entries = [
|
2026-04-14 00:03:19 +02:00
|
|
|
{ key: 'discord', url: S.discord || discordFallback },
|
|
|
|
|
{ key: 'fortnite', url: S.fortnite },
|
2026-04-07 01:51:50 +02:00
|
|
|
{ key: 'twitter', url: S.twitter },
|
|
|
|
|
{ key: 'instagram', url: S.instagram },
|
|
|
|
|
{ key: 'youtube', url: S.youtube },
|
|
|
|
|
{ key: 'tiktok', url: S.tiktok },
|
2026-04-14 00:03:19 +02:00
|
|
|
{ key: 'epicgames', url: S.epicgames },
|
2026-04-07 01:51:50 +02:00
|
|
|
{ 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('');
|
|
|
|
|
};
|
|
|
|
|
|
2026-02-06 18:36:03 +01:00
|
|
|
// 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();
|
|
|
|
|
|
2026-04-07 01:51:50 +02:00
|
|
|
// 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 : '#',
|
2026-04-13 23:05:47 +02:00
|
|
|
'epicgames': S.epicgames,
|
2026-04-07 01:51:50 +02:00
|
|
|
};
|
|
|
|
|
document.querySelectorAll('[data-social]').forEach(el => {
|
|
|
|
|
const key = el.getAttribute('data-social');
|
|
|
|
|
if (map[key] && map[key] !== '#') {
|
|
|
|
|
el.setAttribute('href', map[key]);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
})();
|
|
|
|
|
|
2026-04-05 20:49:52 +02:00
|
|
|
// 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);
|
|
|
|
|
|
2026-02-06 18:36:03 +01:00
|
|
|
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);
|
|
|
|
|
|
2026-04-05 20:49:52 +02:00
|
|
|
// Reveal on scroll — re-query après chargement des includes
|
2026-02-06 18:36:03 +01:00
|
|
|
function revealOnScroll() {
|
2026-04-05 20:49:52 +02:00
|
|
|
const els = document.querySelectorAll('.reveal:not(.active)');
|
|
|
|
|
els.forEach(el => {
|
|
|
|
|
if (el.getBoundingClientRect().top < window.innerHeight - 50) {
|
2026-02-06 18:36:03 +01:00
|
|
|
el.classList.add('active');
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
2026-04-05 20:49:52 +02:00
|
|
|
// Déclenche immédiatement après includes + au scroll + au load
|
|
|
|
|
revealOnScroll();
|
|
|
|
|
window.addEventListener('scroll', revealOnScroll, { passive: true });
|
2026-02-06 18:36:03 +01:00
|
|
|
window.addEventListener('load', revealOnScroll);
|
|
|
|
|
|
2026-04-05 20:49:52 +02:00
|
|
|
// 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();
|
|
|
|
|
// });
|
|
|
|
|
// }
|
2026-02-06 18:36:03 +01:00
|
|
|
});
|