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.niveau} ${niveau.titre} ${completed}/${total}
`; chapitres.forEach((c) => { const id = chapterId(niveau, c.titre); const a = document.createElement("a"); a.href = "#" + id; a.dataset.id = id; a.innerHTML = `${c.titre.split(" (")[0]}`; group.appendChild(a); }); navEl.appendChild(group); // --- Content header --- const header = document.createElement("div"); header.className = "level-header"; header.innerHTML = ` ${niveau.niveau}

${niveau.titre}

${niveau.description}

${completed} of ${total} completed
`; contentEl.appendChild(header); // --- Chapters --- chapitres.forEach((c) => { const id = chapterId(niveau, c.titre); const isDone = !!done[id]; const sec = document.createElement("section"); sec.className = "chapter" + (isDone ? " done" : ""); sec.id = id; const points = c.points.map((p) => `
  • ${p}
  • `).join(""); const ex = c.exemples .map((e) => `
    ${e.fr}${e.en}
    `) .join(""); sec.innerHTML = `

    ${c.titre}

    Examples
    ${ex}
    `; contentEl.appendChild(sec); }); }); if (!anyContent) { contentEl.innerHTML = `

    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();