Add domme-converters/assets/js/components.js
This commit is contained in:
@@ -0,0 +1,187 @@
|
||||
"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) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[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">→</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">→</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>© ${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 };
|
||||
})();
|
||||
Reference in New Issue
Block a user