Files
ben.de-roo.org/domme-converters/assets/js/pages.js
T

576 lines
24 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"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,
};
})();