Add domme-converters/assets/js/pages.js
This commit is contained in:
@@ -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">∞</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 →</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 (A–Z)</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)} → ${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">∞</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">→</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)} · ${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">→</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)} → ${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,
|
||||||
|
};
|
||||||
|
})();
|
||||||
Reference in New Issue
Block a user