diff --git a/domme-converters/assets/js/pages.js b/domme-converters/assets/js/pages.js
new file mode 100644
index 0000000..3652f9c
--- /dev/null
+++ b/domme-converters/assets/js/pages.js
@@ -0,0 +1,575 @@
+"use strict";
+
+/**
+ * Pagina-views. Elke view geeft een object terug:
+ * { title, html, onMount? }
+ * - title : documenttitel.
+ * - html : markup voor de #app-container.
+ * - onMount: optionele functie die na rendering events koppelt.
+ *
+ * Alle inhoud komt uit de centrale databronnen (PRODUCTS, CATEGORIES, CONTENT).
+ */
+(function () {
+ const CL = (window.CL = window.CL || {});
+ const { formatPrice, escapeHtml, stars } = CL.fmt;
+ const { productGrid, pageHead, crumb, productThumb } = CL.components;
+ const { href } = CL.nav;
+
+ const PRODUCTS = window.PRODUCTS || [];
+ const CATEGORIES = window.CATEGORIES || [{ key: "all", label: "Alles" }];
+ const CONTENT = window.CONTENT || {};
+
+ const bySlug = (slug) => PRODUCTS.find((p) => p.slug === slug);
+ const categoryLabel = (key) => (CATEGORIES.find((c) => c.key === key) || { label: key }).label;
+ const discount = (p) => Math.round((1 - p.price / p.old) * 100);
+
+ // ---- Home -----------------------------------------------------------------
+ function home() {
+ const featured = PRODUCTS.slice(0, 4);
+ return {
+ title: "Converterland — Adapters en signaalconverters",
+ html: `
+
+
+
+
Universele signaalconversie
+
Elk signaal naar elk medium. Theoretisch.
+
+ Converterland levert adapters voor verbindingen die niet bestaan en
+ problemen die u niet heeft. Een complete catalogus aan fictieve
+ converters, professioneel gepresenteerd en volledig onbruikbaar.
+
+
+
+
+
Ondersteunde signalen ∞
+
Gemiddelde latency 3 werkdagen
+
Compatibiliteit 0 / 0
+
Garantie geen
+
Retourtermijn n.v.t.
+
+
+
+
+
+
+
Niet op voorraad
Elk product, altijd.
+
Geen verzendkosten
Want geen verzending.
+
Veilig afrekenen
Er gebeurt niets.
+
Support 0/7
Wij nemen niet op.
+
+
+
+
+
+
+
Uitgelicht
+
Een greep uit onze meest besproken converters.
+
+
Alle producten →
+
+ ${productGrid(featured)}
+
+ `,
+ };
+ }
+
+ // ---- Producten ------------------------------------------------------------
+ function producten() {
+ return {
+ title: "Producten — Converterland",
+ html: `
+ ${pageHead({
+ eyebrow: "Assortiment",
+ title: "Producten",
+ intro: "Het volledige assortiment fictieve signaalconverters, overzichtelijk gepresenteerd.",
+ crumb: crumb([{ label: "Home", path: "/" }, { label: "Producten" }]),
+ })}
+
+ Alle converters ${PRODUCTS.length} producten
+ ${productGrid(PRODUCTS)}
+
+ `,
+ };
+ }
+
+ // ---- Catalogus (zoeken / sorteren / filteren) -----------------------------
+ function catalogus() {
+ const filterButtons = CATEGORIES.map(
+ (c) => `${escapeHtml(c.label)} `
+ ).join("");
+
+ const maxPrice = Math.ceil(Math.max(...PRODUCTS.map((p) => p.price)));
+
+ return {
+ title: "Catalogus — Converterland",
+ html: `
+ ${pageHead({
+ eyebrow: "Catalogus",
+ title: "Catalogus",
+ intro: "Doorzoek, sorteer en filter het volledige assortiment.",
+ crumb: crumb([{ label: "Home", path: "/" }, { label: "Catalogus" }]),
+ })}
+
+
+
+ ${productGrid(PRODUCTS)}
+
+ `,
+ onMount() {
+ const state = { q: "", sort: "featured", category: "all", maxPrice };
+ const gridEl = document.getElementById("catalogGrid");
+ const countEl = document.getElementById("resultCount");
+ const searchEl = document.getElementById("searchInput");
+ const sortEl = document.getElementById("sortSelect");
+ const rangeEl = document.getElementById("priceRange");
+ const priceOut = document.getElementById("priceOut");
+ const filtersEl = document.getElementById("filters");
+
+ function apply() {
+ let list = PRODUCTS.filter((p) => {
+ if (state.category !== "all" && p.category !== state.category) return false;
+ if (p.price > state.maxPrice) return false;
+ if (state.q) {
+ const hay = (p.name + " " + p.from + " " + p.to + " " + p.desc).toLowerCase();
+ if (!hay.includes(state.q)) return false;
+ }
+ return true;
+ });
+ switch (state.sort) {
+ case "price-asc": list.sort((a, b) => a.price - b.price); break;
+ case "price-desc": list.sort((a, b) => b.price - a.price); break;
+ case "rating": list.sort((a, b) => b.rating - a.rating || b.reviews - a.reviews); break;
+ case "name": list.sort((a, b) => a.name.localeCompare(b.name, "nl")); break;
+ }
+ gridEl.innerHTML = productGrid(list);
+ countEl.textContent = list.length + " van " + PRODUCTS.length + " producten";
+ }
+
+ searchEl.addEventListener("input", () => { state.q = searchEl.value.trim().toLowerCase(); apply(); });
+ sortEl.addEventListener("change", () => { state.sort = sortEl.value; apply(); });
+ rangeEl.addEventListener("input", () => {
+ state.maxPrice = Number(rangeEl.value);
+ priceOut.textContent = formatPrice(state.maxPrice);
+ apply();
+ });
+ filtersEl.addEventListener("click", (e) => {
+ const btn = e.target.closest("[data-filter]");
+ if (!btn) return;
+ state.category = btn.dataset.filter;
+ filtersEl.querySelectorAll(".filter").forEach((b) => b.classList.toggle("active", b === btn));
+ apply();
+ });
+ apply();
+ },
+ };
+ }
+
+ // ---- Specificaties --------------------------------------------------------
+ function specificaties() {
+ const rows = PRODUCTS.map(
+ (p) => `
+
+ ${escapeHtml(p.name)} ${escapeHtml(p.id)}
+ ${escapeHtml(p.from)} → ${escapeHtml(p.to)}
+ ${escapeHtml(categoryLabel(p.category))}
+ ${escapeHtml(p.stock.label)}
+ ${formatPrice(p.price)}
+ `
+ ).join("");
+
+ return {
+ title: "Specificaties — Converterland",
+ html: `
+ ${pageHead({
+ eyebrow: "Technische gegevens",
+ title: "Specificaties",
+ intro: "Een vergelijkend overzicht van het volledige assortiment. Alle waarden zijn fictief.",
+ crumb: crumb([{ label: "Home", path: "/" }, { label: "Specificaties" }]),
+ })}
+
+
+
+
+ Product Conversie Categorie Beschikbaarheid Prijs
+
+ ${rows}
+
+
+
+
Algemene kenmerken
+
+
Ondersteunde signalen ∞
+
Gemiddelde latency 3 werkdagen
+
Compatibiliteit 0 / 0
+
Garantie geen
+
Retourtermijn n.v.t.
+
Certificering zelfverklaard
+
+
+
+ `,
+ };
+ }
+
+ // ---- Aanbiedingen ---------------------------------------------------------
+ function aanbiedingen() {
+ const list = [...PRODUCTS].sort((a, b) => discount(b) - discount(a));
+ const cards = list
+ .map((p) => {
+ const fallback =
+ `${escapeHtml(p.from)} → ${escapeHtml(p.to)} `;
+ return `
+
+
+
+ -${discount(p)}%
+ ${productThumb(p)}
+
+
+
${escapeHtml(p.id)} ${stars(p.rating)}(${p.reviews})
+
${escapeHtml(p.name)}
+
${escapeHtml(p.desc)}
+
+
+
+ `;
+ })
+ .join("");
+
+ return {
+ title: "Aanbiedingen — Converterland",
+ html: `
+ ${pageHead({
+ eyebrow: "Tijdelijk voordeel",
+ title: "Aanbiedingen",
+ intro: "Alle converters zijn afgeprijsd, want niets is iets waard. Gesorteerd op korting.",
+ crumb: crumb([{ label: "Home", path: "/" }, { label: "Aanbiedingen" }]),
+ })}
+
+ ${cards}
+
+ `,
+ };
+ }
+
+ // ---- Bedrijf --------------------------------------------------------------
+ function bedrijf() {
+ const c = CONTENT.company;
+ const values = c.values
+ .map((v) => `${escapeHtml(v.title)} ${escapeHtml(v.text)}
`)
+ .join("");
+ return {
+ title: "Bedrijf — Converterland",
+ html: `
+ ${pageHead({
+ eyebrow: "Over de onderneming",
+ title: "Bedrijf",
+ intro: c.intro,
+ crumb: crumb([{ label: "Home", path: "/" }, { label: "Bedrijf" }]),
+ })}
+
+
+
${escapeHtml(c.founded)} Opgericht
+
${escapeHtml(c.employees)} Medewerkers
+
${PRODUCTS.length} Producten
+
0 Leveringen
+
+ ${values}
+
+
+ `,
+ };
+ }
+
+ // ---- Over ons -------------------------------------------------------------
+ function overOns() {
+ const c = CONTENT.company;
+ const timeline = c.history
+ .map((h) => `${escapeHtml(h.year)} ${escapeHtml(h.text)} `)
+ .join("");
+ return {
+ title: "Over ons — Converterland",
+ html: `
+ ${pageHead({
+ eyebrow: "Onze geschiedenis",
+ title: "Over ons",
+ intro: c.intro,
+ crumb: crumb([{ label: "Home", path: "/" }, { label: "Bedrijf", path: "/bedrijf" }, { label: "Over ons" }]),
+ })}
+
+
+
Hoofdkantoor
+
${escapeHtml(c.headquarters)}
+
+ Tijdlijn
+ ${timeline}
+
+ `,
+ };
+ }
+
+ // ---- Contact --------------------------------------------------------------
+ function contact() {
+ const c = CONTENT.contact;
+ return {
+ title: "Contact — Converterland",
+ html: `
+ ${pageHead({
+ eyebrow: "Neem contact op",
+ title: "Contact",
+ intro: "Stuur ons een bericht. Het formulier verwerkt geen gegevens en verstuurt niets.",
+ crumb: crumb([{ label: "Home", path: "/" }, { label: "Contact" }]),
+ })}
+
+
+
+ `,
+ onMount() {
+ const form = document.getElementById("contactForm");
+ const note = document.getElementById("contactNote");
+ form.addEventListener("submit", (e) => {
+ e.preventDefault();
+ if (!form.checkValidity()) { form.reportValidity(); return; }
+ form.reset();
+ note.textContent = "Bericht ontvangen in deze demonstratie. Er is niets verzonden of opgeslagen.";
+ });
+ },
+ };
+ }
+
+ // ---- Vacatures ------------------------------------------------------------
+ function vacatures() {
+ const items = CONTENT.jobs
+ .map(
+ (j) => `
+
+
+
+ ${escapeHtml(j.title)}
+ ${escapeHtml(j.type)} · ${escapeHtml(j.location)}
+
+ +
+
+
+
${escapeHtml(j.summary)}
+
${escapeHtml(j.details)}
+
Wat wij vragen
+
${j.requirements.map((r) => `${escapeHtml(r)} `).join("")}
+
Solliciteer (gaat naar contact)
+
+ `
+ )
+ .join("");
+ return {
+ title: "Vacatures — Converterland",
+ html: `
+ ${pageHead({
+ eyebrow: "Werken bij",
+ title: "Vacatures",
+ intro: "Wij groeien al jaren niet, maar er is altijd plek voor talent dat van niets houdt.",
+ crumb: crumb([{ label: "Home", path: "/" }, { label: "Bedrijf", path: "/bedrijf" }, { label: "Vacatures" }]),
+ })}
+
+ ${items}
+
+ `,
+ onMount() {
+ const jobs = document.querySelector(".jobs");
+ jobs.addEventListener("click", (e) => {
+ const head = e.target.closest("[data-job]");
+ if (!head) return;
+ const body = head.nextElementSibling;
+ const open = head.getAttribute("aria-expanded") === "true";
+ head.setAttribute("aria-expanded", String(!open));
+ head.querySelector(".job-toggle").textContent = open ? "+" : "\u2212";
+ body.hidden = open;
+ });
+ },
+ };
+ }
+
+ // ---- Juridische pagina's --------------------------------------------------
+ function legalPage(key) {
+ const data = CONTENT.legal[key];
+ const sections = data.sections
+ .map((s) => `${escapeHtml(s.h)} ${escapeHtml(s.p)}
`)
+ .join("");
+ return {
+ title: data.title + " — Converterland",
+ html: `
+ ${pageHead({
+ eyebrow: "Juridisch",
+ title: data.title,
+ intro: data.intro,
+ crumb: crumb([{ label: "Home", path: "/" }, { label: data.title }]),
+ })}
+
+ ${sections}
+
+ `,
+ };
+ }
+
+ // ---- Productdetail --------------------------------------------------------
+ function productDetail(slug) {
+ const p = bySlug(slug);
+ if (!p) return notFound();
+
+ const specRows = Object.entries(p.specs)
+ .map(([k, v]) => `${escapeHtml(k)} ${escapeHtml(v)}
`)
+ .join("");
+ const features = p.features.map((f) => `${escapeHtml(f)} `).join("");
+ const fallback =
+ `${escapeHtml(p.from)} → ${escapeHtml(p.to)} `;
+ const related = PRODUCTS.filter((x) => x.category === p.category && x.id !== p.id).slice(0, 3);
+ const relatedHtml = related.length ? `${CL.components.productGrid(related)}` : "";
+
+ return {
+ title: p.name + " — Converterland",
+ html: `
+ ${pageHead({
+ crumb: crumb([
+ { label: "Home", path: "/" },
+ { label: "Producten", path: "/producten" },
+ { label: p.name },
+ ]),
+ title: p.name,
+ })}
+
+
+
+
+
+ ${escapeHtml(p.id)}
+ ${stars(p.rating)}(${p.reviews} beoordelingen)
+
+
+ ${formatPrice(p.price)}${formatPrice(p.old)}
+ U bespaart ${formatPrice(p.old - p.price)}
+
+
${escapeHtml(p.stock.label)}
+
${escapeHtml(p.long)}
+
+
+
+
+
+
+ Technische specificaties
+ ${specRows}
+
+
+ ${
+ related.length
+ ? ``
+ : ""
+ }
+
+ `,
+ };
+ }
+
+ // ---- 404 ------------------------------------------------------------------
+ function notFound() {
+ return {
+ title: "Niet gevonden — Converterland",
+ html: `
+ ${pageHead({ eyebrow: "Fout 404", title: "Pagina niet gevonden", intro: "Deze pagina bestaat net zomin als onze producten." })}
+
+ Terug naar de startpagina
+
+ `,
+ };
+ }
+
+ CL.pages = {
+ home,
+ producten,
+ catalogus,
+ specificaties,
+ aanbiedingen,
+ bedrijf,
+ overOns,
+ contact,
+ vacatures,
+ voorwaarden: () => legalPage("voorwaarden"),
+ privacy: () => legalPage("privacy"),
+ disclaimer: () => legalPage("disclaimer"),
+ productDetail,
+ notFound,
+ };
+})();