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

188 lines
7.3 KiB
JavaScript

"use strict";
/**
* Herbruikbare helpers en UI-componenten. Alles wordt onder de globale
* CL-namespace gehangen zodat router en pagina's het kunnen gebruiken.
*/
(function () {
const CL = (window.CL = window.CL || {});
// ---- Opmaak-helpers -------------------------------------------------------
const formatPrice = (value) => "\u20AC" + Number(value).toFixed(2).replace(".", ",");
const escapeHtml = (str) =>
String(str).replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" }[c]));
const stars = (rating) => "\u2605".repeat(rating) + "\u2606".repeat(5 - rating);
// ---- Navigatiestructuur ---------------------------------------------------
// Eén bron voor de hoofdnavigatie; de footer vult dit aan met extra links.
const NAV = [
{ path: "/producten", label: "Producten" },
{ path: "/catalogus", label: "Catalogus" },
{ path: "/specificaties", label: "Specificaties" },
{ path: "/aanbiedingen", label: "Aanbiedingen" },
{ path: "/bedrijf", label: "Bedrijf" },
{ path: "/contact", label: "Contact" },
];
const FOOTER_LINKS = {
Producten: [
{ path: "/producten", label: "Producten" },
{ path: "/catalogus", label: "Catalogus" },
{ path: "/specificaties", label: "Specificaties" },
{ path: "/aanbiedingen", label: "Aanbiedingen" },
],
Bedrijf: [
{ path: "/bedrijf", label: "Bedrijf" },
{ path: "/over-ons", label: "Over ons" },
{ path: "/vacatures", label: "Vacatures" },
{ path: "/contact", label: "Contact" },
],
Juridisch: [
{ path: "/voorwaarden", label: "Voorwaarden" },
{ path: "/privacy", label: "Privacy" },
{ path: "/disclaimer", label: "Disclaimer" },
],
};
// Bouwt een interne hash-link op die door de router wordt afgevangen.
const href = (path) => "#" + path;
// ---- Product-thumbnail ----------------------------------------------------
// Toont de foto als die er is, valt anders terug op de connector-tegel.
function productThumb(p) {
const fallback =
`<span class="conn">${escapeHtml(p.from)}</span>` +
`<span class="arrow">&rarr;</span>` +
`<span class="conn">${escapeHtml(p.to)}</span>`;
if (p.image) {
return `<img src="${escapeHtml(p.image)}" alt="${escapeHtml(p.name)}" loading="lazy"
onerror="this.remove(); this.parentNode.innerHTML = this.parentNode.dataset.fallback;" />`;
}
return fallback;
}
// ---- Productkaart (volledig klikbaar) ------------------------------------
function productCard(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)}">
${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>
`;
}
function productGrid(list) {
if (!list.length) {
return `<div class="empty-state">Geen producten gevonden.</div>`;
}
return `<div class="grid">${list.map(productCard).join("")}</div>`;
}
// ---- Paginakop ------------------------------------------------------------
function pageHead({ eyebrow, title, intro, crumb }) {
return `
<section class="page-head">
<div class="container">
${crumb ? `<nav class="crumb" aria-label="Kruimelpad">${crumb}</nav>` : ""}
${eyebrow ? `<span class="eyebrow">${escapeHtml(eyebrow)}</span>` : ""}
<h1>${escapeHtml(title)}</h1>
${intro ? `<p>${escapeHtml(intro)}</p>` : ""}
</div>
</section>
`;
}
function crumb(parts) {
// parts: [{label, path?}] — laatste item is de huidige pagina.
return parts
.map((part, i) => {
const last = i === parts.length - 1;
const sep = last ? "" : `<span class="crumb-sep">/</span>`;
if (last || !part.path) return `<span aria-current="page">${escapeHtml(part.label)}</span>${sep}`;
return `<a href="${href(part.path)}">${escapeHtml(part.label)}</a>${sep}`;
})
.join("");
}
// ---- Header ---------------------------------------------------------------
function header(activePath) {
const isActive = (path) =>
path === activePath || (path !== "/" && activePath.startsWith(path + "/")) ? " active" : "";
const links = NAV.map(
(item) => `<a href="${href(item.path)}" class="${isActive(item.path).trim()}">${escapeHtml(item.label)}</a>`
).join("");
return `
<header>
<div class="container nav">
<a class="brand" href="${href("/")}" aria-label="Naar de startpagina">
<span class="mark">CL</span>
<span class="name">Converterland<span>Signaalconversie sinds nooit</span></span>
</a>
<button class="nav-toggle" id="navToggle" aria-label="Menu" aria-expanded="false">
<span></span><span></span><span></span>
</button>
<nav class="nav-links" id="navLinks">${links}</nav>
<button class="cart-btn" id="openCart" aria-label="Winkelmandje openen">
Mandje
<span class="cart-count" data-cart-count>0</span>
</button>
</div>
</header>
`;
}
// ---- Footer ---------------------------------------------------------------
function footer() {
const column = (heading, items) => `
<div>
<h4>${escapeHtml(heading)}</h4>
<ul>${items.map((i) => `<li><a href="${href(i.path)}">${escapeHtml(i.label)}</a></li>`).join("")}</ul>
</div>`;
return `
<footer>
<div class="container">
<div class="foot-grid">
<div class="foot-about">
<a class="brand" href="${href("/")}">
<span class="mark">CL</span>
<span class="name">Converterland</span>
</a>
<p>Fictieve signaalconverters voor de moderne onzin-infrastructuur. Een parodieproject, geen echte handelsonderneming.</p>
</div>
${column("Producten", FOOTER_LINKS.Producten)}
${column("Bedrijf", FOOTER_LINKS.Bedrijf)}
${column("Juridisch", FOOTER_LINKS.Juridisch)}
</div>
<div class="foot-bottom">
<span>&copy; ${new Date().getFullYear()} Converterland. Parodie. Alle merknamen zijn fictief.</span>
<span>Geen rechten te ontlenen aan deze website.</span>
</div>
</div>
</footer>
`;
}
CL.fmt = { formatPrice, escapeHtml, stars };
CL.nav = { NAV, FOOTER_LINKS, href };
CL.components = { productThumb, productCard, productGrid, pageHead, crumb, header, footer };
})();