const slug = (s) => s.toLowerCase() .normalize("NFD").replace(/[̀-ͯ]/g, "") .replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, ""); const navEl = document.getElementById("nav"); const contentEl = document.getElementById("content"); const searchEl = document.getElementById("search"); const STORE_KEY = "frenchJourney.completed"; function loadDone() { try { return JSON.parse(localStorage.getItem(STORE_KEY)) || {}; } catch { return {}; } } function saveDone(done) { localStorage.setItem(STORE_KEY, JSON.stringify(done)); } let done = loadDone(); function chapterId(niveau, titre) { return slug(niveau.niveau + "-" + titre); } function render(filter = "") { const q = filter.trim().toLowerCase(); navEl.innerHTML = ""; contentEl.innerHTML = ""; let anyContent = false; GRAMMAIRE.forEach((niveau) => { const chapitres = niveau.chapitres.filter((c) => { if (!q) return true; const ex = c.exemples.map((e) => e.fr + " " + e.en).join(" "); const hay = (c.titre + " " + c.points.join(" ") + " " + ex).toLowerCase(); return hay.includes(q) || niveau.niveau.toLowerCase().includes(q); }); if (chapitres.length === 0) return; anyContent = true; const total = chapitres.length; const completed = chapitres.filter((c) => done[chapterId(niveau, c.titre)]).length; // --- Sidebar group --- const group = document.createElement("div"); group.className = "level-group"; group.innerHTML = `
${niveau.description}
${e.fr}${e.en}No results for “${filter}”.
`; } bindMarks(); setupScrollSpy(); } function bindMarks() { contentEl.querySelectorAll(".mark").forEach((btn) => { btn.addEventListener("click", () => { const id = btn.dataset.id; if (done[id]) delete done[id]; else done[id] = true; saveDone(done); render(searchEl.value); }); }); } function setupScrollSpy() { const links = [...navEl.querySelectorAll("a")]; const sections = links .map((a) => document.getElementById(a.dataset.id)) .filter(Boolean); if (!sections.length) return; const obs = new IntersectionObserver( (entries) => { entries.forEach((entry) => { if (entry.isIntersecting) { links.forEach((l) => l.classList.remove("active")); const active = links.find((l) => l.dataset.id === entry.target.id); if (active) active.classList.add("active"); } }); }, { rootMargin: "-60px 0px -70% 0px", threshold: 0 } ); sections.forEach((s) => obs.observe(s)); } searchEl.addEventListener("input", (e) => render(e.target.value)); render();