Add domme-converters/assets/js/pages.js

This commit is contained in:
2026-06-24 21:13:21 +02:00
parent d2443e41d0
commit 17a247127b
+575
View File
@@ -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: `
<section class="hero">
<div class="container hero-inner">
<div>
<span class="eyebrow">Universele signaalconversie</span>
<h1>Elk signaal naar elk medium. Theoretisch.</h1>
<p>
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.
</p>
<div class="hero-actions">
<a href="${href("/catalogus")}" class="btn btn-primary">Bekijk catalogus</a>
<a href="${href("/specificaties")}" class="btn btn-ghost">Technische specificaties</a>
</div>
</div>
<div class="hero-panel">
<div class="spec-row"><span class="k">Ondersteunde signalen</span><span class="v">&infin;</span></div>
<div class="spec-row"><span class="k">Gemiddelde latency</span><span class="v">3 werkdagen</span></div>
<div class="spec-row"><span class="k">Compatibiliteit</span><span class="v">0 / 0</span></div>
<div class="spec-row"><span class="k">Garantie</span><span class="v">geen</span></div>
<div class="spec-row"><span class="k">Retourtermijn</span><span class="v">n.v.t.</span></div>
</div>
</div>
</section>
<section class="trust">
<div class="container trust-inner">
<div class="trust-item"><div class="t">Niet op voorraad</div><div class="d">Elk product, altijd.</div></div>
<div class="trust-item"><div class="t">Geen verzendkosten</div><div class="d">Want geen verzending.</div></div>
<div class="trust-item"><div class="t">Veilig afrekenen</div><div class="d">Er gebeurt niets.</div></div>
<div class="trust-item"><div class="t">Support 0/7</div><div class="d">Wij nemen niet op.</div></div>
</div>
</section>
<main class="container section">
<div class="section-head">
<div>
<h2>Uitgelicht</h2>
<p>Een greep uit onze meest besproken converters.</p>
</div>
<a href="${href("/producten")}" class="link-more">Alle producten &rarr;</a>
</div>
${productGrid(featured)}
</main>
`,
};
}
// ---- 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" }]),
})}
<main class="container section">
<div class="section-head"><div><h2>Alle converters</h2><p>${PRODUCTS.length} producten</p></div></div>
${productGrid(PRODUCTS)}
</main>
`,
};
}
// ---- Catalogus (zoeken / sorteren / filteren) -----------------------------
function catalogus() {
const filterButtons = CATEGORIES.map(
(c) => `<button class="filter${c.key === "all" ? " active" : ""}" data-filter="${c.key}">${escapeHtml(c.label)}</button>`
).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" }]),
})}
<main class="container section">
<div class="toolbar">
<div class="toolbar-row">
<input type="search" id="searchInput" class="search" placeholder="Zoek op naam, aansluiting of omschrijving" aria-label="Zoeken" />
<select id="sortSelect" class="select" aria-label="Sorteren">
<option value="featured">Aanbevolen</option>
<option value="price-asc">Prijs oplopend</option>
<option value="price-desc">Prijs aflopend</option>
<option value="rating">Best beoordeeld</option>
<option value="name">Naam (AZ)</option>
</select>
</div>
<div class="toolbar-row">
<div class="filters" id="filters">${filterButtons}</div>
<label class="price-filter">
Max. prijs: <output id="priceOut">${formatPrice(maxPrice)}</output>
<input type="range" id="priceRange" min="0" max="${maxPrice}" value="${maxPrice}" step="1" />
</label>
</div>
</div>
<p class="result-count" id="resultCount"></p>
<div id="catalogGrid">${productGrid(PRODUCTS)}</div>
</main>
`,
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) => `
<tr>
<td><a href="${href("/producten/" + p.slug)}">${escapeHtml(p.name)}</a><span class="sku">${escapeHtml(p.id)}</span></td>
<td><span class="conn-inline">${escapeHtml(p.from)} &rarr; ${escapeHtml(p.to)}</span></td>
<td>${escapeHtml(categoryLabel(p.category))}</td>
<td>${escapeHtml(p.stock.label)}</td>
<td class="num">${formatPrice(p.price)}</td>
</tr>`
).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" }]),
})}
<main class="container section">
<div class="table-wrap">
<table class="spec-table">
<thead>
<tr><th>Product</th><th>Conversie</th><th>Categorie</th><th>Beschikbaarheid</th><th class="num">Prijs</th></tr>
</thead>
<tbody>${rows}</tbody>
</table>
</div>
<div class="info-panel">
<h3>Algemene kenmerken</h3>
<div class="spec-grid">
<div class="spec-row"><span class="k">Ondersteunde signalen</span><span class="v">&infin;</span></div>
<div class="spec-row"><span class="k">Gemiddelde latency</span><span class="v">3 werkdagen</span></div>
<div class="spec-row"><span class="k">Compatibiliteit</span><span class="v">0 / 0</span></div>
<div class="spec-row"><span class="k">Garantie</span><span class="v">geen</span></div>
<div class="spec-row"><span class="k">Retourtermijn</span><span class="v">n.v.t.</span></div>
<div class="spec-row"><span class="k">Certificering</span><span class="v">zelfverklaard</span></div>
</div>
</div>
</main>
`,
};
}
// ---- Aanbiedingen ---------------------------------------------------------
function aanbiedingen() {
const list = [...PRODUCTS].sort((a, b) => discount(b) - discount(a));
const cards = list
.map((p) => {
const fallback =
`<span class="conn">${escapeHtml(p.from)}</span><span class="arrow">&rarr;</span><span class="conn">${escapeHtml(p.to)}</span>`;
return `
<article class="card">
<a class="card-link" href="${href("/producten/" + p.slug)}" aria-label="${escapeHtml(p.name)} bekijken">
<div class="thumb" data-fallback="${escapeHtml(fallback)}">
<span class="badge">-${discount(p)}%</span>
${productThumb(p)}
</div>
<div class="body">
<div class="meta"><span class="sku">${escapeHtml(p.id)}</span><span class="stars">${stars(p.rating)}<span class="reviews">(${p.reviews})</span></span></div>
<h3>${escapeHtml(p.name)}</h3>
<p class="desc">${escapeHtml(p.desc)}</p>
</div>
</a>
<div class="foot">
<span class="price">${formatPrice(p.price)}<small>${formatPrice(p.old)}</small></span>
<button class="add-btn" data-add="${p.id}">Toevoegen</button>
</div>
</article>`;
})
.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" }]),
})}
<main class="container section">
<div class="grid">${cards}</div>
</main>
`,
};
}
// ---- Bedrijf --------------------------------------------------------------
function bedrijf() {
const c = CONTENT.company;
const values = c.values
.map((v) => `<div class="value-card"><h3>${escapeHtml(v.title)}</h3><p>${escapeHtml(v.text)}</p></div>`)
.join("");
return {
title: "Bedrijf — Converterland",
html: `
${pageHead({
eyebrow: "Over de onderneming",
title: "Bedrijf",
intro: c.intro,
crumb: crumb([{ label: "Home", path: "/" }, { label: "Bedrijf" }]),
})}
<main class="container section">
<div class="stat-row">
<div class="stat"><span class="stat-v">${escapeHtml(c.founded)}</span><span class="stat-k">Opgericht</span></div>
<div class="stat"><span class="stat-v">${escapeHtml(c.employees)}</span><span class="stat-k">Medewerkers</span></div>
<div class="stat"><span class="stat-v">${PRODUCTS.length}</span><span class="stat-k">Producten</span></div>
<div class="stat"><span class="stat-v">0</span><span class="stat-k">Leveringen</span></div>
</div>
<div class="cols-2">${values}</div>
<div class="cta-row">
<a class="btn btn-primary" href="${href("/over-ons")}">Over ons</a>
<a class="btn btn-ghost" href="${href("/vacatures")}">Vacatures</a>
<a class="btn btn-ghost" href="${href("/contact")}">Contact</a>
</div>
</main>
`,
};
}
// ---- Over ons -------------------------------------------------------------
function overOns() {
const c = CONTENT.company;
const timeline = c.history
.map((h) => `<li><span class="t-year">${escapeHtml(h.year)}</span><span class="t-text">${escapeHtml(h.text)}</span></li>`)
.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" }]),
})}
<main class="container section narrow">
<div class="info-panel">
<h3>Hoofdkantoor</h3>
<p class="muted">${escapeHtml(c.headquarters)}</p>
</div>
<h2 class="block-title">Tijdlijn</h2>
<ol class="timeline">${timeline}</ol>
</main>
`,
};
}
// ---- 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" }]),
})}
<main class="container section">
<div class="contact-layout">
<form id="contactForm" class="contact-form" novalidate>
<div class="field">
<label for="c-name">Naam</label>
<input id="c-name" name="name" required placeholder="Voor- en achternaam" />
</div>
<div class="field">
<label for="c-email">E-mailadres</label>
<input id="c-email" name="email" type="email" required placeholder="naam@voorbeeld.nl" />
</div>
<div class="field">
<label for="c-subject">Onderwerp</label>
<input id="c-subject" name="subject" required placeholder="Waar gaat het over?" />
</div>
<div class="field">
<label for="c-message">Bericht</label>
<textarea id="c-message" name="message" rows="5" required placeholder="Uw bericht"></textarea>
</div>
<button class="btn btn-primary" type="submit">Versturen</button>
<p class="form-note" id="contactNote" role="status" aria-live="polite"></p>
</form>
<aside class="contact-aside">
<h3>Gegevens</h3>
<p class="muted">${escapeHtml(c.address).replace(/\n/g, "<br>")}</p>
<dl class="contact-dl">
<dt>E-mail</dt><dd>${escapeHtml(c.email)}</dd>
<dt>Telefoon</dt><dd>${escapeHtml(c.phone)}</dd>
<dt>Bereikbaarheid</dt><dd>${escapeHtml(c.hours)}</dd>
</dl>
</aside>
</div>
</main>
`,
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) => `
<article class="job">
<button class="job-head" data-job="${escapeHtml(j.id)}" aria-expanded="false">
<span>
<span class="job-title">${escapeHtml(j.title)}</span>
<span class="job-meta">${escapeHtml(j.type)} &middot; ${escapeHtml(j.location)}</span>
</span>
<span class="job-toggle" aria-hidden="true">+</span>
</button>
<div class="job-body" hidden>
<p>${escapeHtml(j.summary)}</p>
<p>${escapeHtml(j.details)}</p>
<h4>Wat wij vragen</h4>
<ul>${j.requirements.map((r) => `<li>${escapeHtml(r)}</li>`).join("")}</ul>
<a class="btn btn-primary btn-sm" href="${href("/contact")}">Solliciteer (gaat naar contact)</a>
</div>
</article>`
)
.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" }]),
})}
<main class="container section narrow">
<div class="jobs">${items}</div>
</main>
`,
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) => `<section class="prose-block"><h2>${escapeHtml(s.h)}</h2><p>${escapeHtml(s.p)}</p></section>`)
.join("");
return {
title: data.title + " — Converterland",
html: `
${pageHead({
eyebrow: "Juridisch",
title: data.title,
intro: data.intro,
crumb: crumb([{ label: "Home", path: "/" }, { label: data.title }]),
})}
<main class="container section narrow">
<div class="prose">${sections}</div>
</main>
`,
};
}
// ---- Productdetail --------------------------------------------------------
function productDetail(slug) {
const p = bySlug(slug);
if (!p) return notFound();
const specRows = Object.entries(p.specs)
.map(([k, v]) => `<div class="pdp-spec"><span class="k">${escapeHtml(k)}</span><span class="v">${escapeHtml(v)}</span></div>`)
.join("");
const features = p.features.map((f) => `<li>${escapeHtml(f)}</li>`).join("");
const fallback =
`<span class="conn">${escapeHtml(p.from)}</span><span class="arrow">&rarr;</span><span class="conn">${escapeHtml(p.to)}</span>`;
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,
})}
<main class="container section">
<div class="pdp">
<div class="pdp-media">
<div class="thumb pdp-thumb" data-fallback="${escapeHtml(fallback)}">${productThumb(p)}</div>
<div class="pdp-tags">
<span class="tag">${escapeHtml(categoryLabel(p.category))}</span>
<span class="tag">${escapeHtml(p.from)} &rarr; ${escapeHtml(p.to)}</span>
</div>
</div>
<div class="pdp-info">
<div class="meta">
<span class="sku">${escapeHtml(p.id)}</span>
<span class="stars">${stars(p.rating)}<span class="reviews">(${p.reviews} beoordelingen)</span></span>
</div>
<div class="pdp-price">
<span class="price">${formatPrice(p.price)}<small>${formatPrice(p.old)}</small></span>
<span class="save">U bespaart ${formatPrice(p.old - p.price)}</span>
</div>
<p class="pdp-avail ${p.stock.available ? "in" : "out"}">${escapeHtml(p.stock.label)}</p>
<p class="pdp-long">${escapeHtml(p.long)}</p>
<div class="pdp-actions">
<button class="btn btn-primary" data-add="${p.id}">In winkelmandje</button>
<a class="btn btn-ghost" href="${href("/catalogus")}">Verder winkelen</a>
</div>
<div class="pdp-features">
<h3>Eigenschappen</h3>
<ul>${features}</ul>
</div>
</div>
</div>
<section class="pdp-specs">
<h2 class="block-title">Technische specificaties</h2>
<div class="pdp-spec-grid">${specRows}</div>
</section>
${
related.length
? `<section class="related">
<div class="section-head"><div><h2>Gerelateerde producten</h2><p>Andere converters in de categorie ${escapeHtml(categoryLabel(p.category))}.</p></div></div>
${relatedHtml}
</section>`
: ""
}
</main>
`,
};
}
// ---- 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." })}
<main class="container section narrow">
<a class="btn btn-primary" href="${href("/")}">Terug naar de startpagina</a>
</main>
`,
};
}
CL.pages = {
home,
producten,
catalogus,
specificaties,
aanbiedingen,
bedrijf,
overOns,
contact,
vacatures,
voorwaarden: () => legalPage("voorwaarden"),
privacy: () => legalPage("privacy"),
disclaimer: () => legalPage("disclaimer"),
productDetail,
notFound,
};
})();