diff --git a/domme-converters/assets/js/app.js b/domme-converters/assets/js/app.js
new file mode 100644
index 0000000..e4995a9
--- /dev/null
+++ b/domme-converters/assets/js/app.js
@@ -0,0 +1,309 @@
+"use strict";
+
+// Data komt uit products.js (window.PRODUCTS / window.CATEGORIES).
+const PRODUCTS = window.PRODUCTS || [];
+const CATEGORIES = window.CATEGORIES || [{ key: "all", label: "Alles" }];
+
+const TAX_RATE = 0.21;
+
+// Winkelmandje: Map van product-id -> aantal.
+const cart = new Map();
+let activeFilter = "all";
+
+const els = {
+ grid: document.getElementById("grid"),
+ filters: document.getElementById("filters"),
+ productCount: document.getElementById("productCount"),
+ overlay: document.getElementById("overlay"),
+ drawer: document.getElementById("drawer"),
+ cartItems: document.getElementById("cartItems"),
+ cartCount: document.getElementById("cartCount"),
+ cartSub: document.getElementById("cartSub"),
+ cartTax: document.getElementById("cartTax"),
+ cartTotal: document.getElementById("cartTotal"),
+ checkoutBtn: document.getElementById("checkoutBtn"),
+ modalWrap: document.getElementById("modalWrap"),
+ modalContent: document.getElementById("modalContent"),
+ toast: document.getElementById("toast"),
+ openCart: document.getElementById("openCart"),
+ closeCart: document.getElementById("closeCart"),
+};
+
+// Hulpfuncties voor opmaak.
+const formatPrice = (value) => "\u20AC" + value.toFixed(2).replace(".", ",");
+const escapeHtml = (str) =>
+ String(str).replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c]));
+const stars = (rating) => "\u2605".repeat(rating) + "\u2606".repeat(5 - rating);
+const findProduct = (id) => PRODUCTS.find((p) => p.id === id);
+
+// Tegelinhoud: toon de foto als die er is, val anders terug op de connector-tegel.
+// onerror verwijdert een gebroken afbeelding en toont alsnog de connectors.
+function thumbInner(p) {
+ const fallback = `
+ ${escapeHtml(p.from)}
+ →
+ ${escapeHtml(p.to)}
+ `;
+ if (p.image) {
+ return `
`;
+ }
+ return fallback;
+}
+
+// Rendering.
+function renderFilters() {
+ els.filters.innerHTML = CATEGORIES.map((c) =>
+ ``
+ ).join("");
+}
+
+function visibleProducts() {
+ return activeFilter === "all" ? PRODUCTS : PRODUCTS.filter((p) => p.category === activeFilter);
+}
+
+function renderCatalog() {
+ const list = visibleProducts();
+ els.productCount.textContent = list.length + " producten" + (activeFilter === "all" ? "" : " in deze categorie");
+ els.grid.innerHTML = list.map((p) => {
+ const fallback = `${escapeHtml(p.from)}→${escapeHtml(p.to)}`;
+ const thumb = p.image
+ ? `
`
+ : fallback;
+ return `
+
+
+ ${thumb}
+
+
+
+ ${escapeHtml(p.id)}
+ ${stars(p.rating)}(${p.reviews})
+
+
${escapeHtml(p.name)}
+
${escapeHtml(p.desc)}
+
+
+
+ `;
+ }).join("");
+}
+
+function cartTotals() {
+ let count = 0;
+ let subtotal = 0;
+ for (const [id, qty] of cart) {
+ const product = findProduct(id);
+ if (!product) continue;
+ count += qty;
+ subtotal += product.price * qty;
+ }
+ const tax = subtotal * TAX_RATE;
+ return { count, subtotal, tax, total: subtotal + tax };
+}
+
+function renderCart() {
+ const { count, subtotal, tax, total } = cartTotals();
+ els.cartCount.textContent = String(count);
+ els.cartSub.innerHTML = formatPrice(subtotal);
+ els.cartTax.innerHTML = formatPrice(tax);
+ els.cartTotal.innerHTML = formatPrice(total);
+ els.checkoutBtn.disabled = count === 0;
+
+ if (cart.size === 0) {
+ els.cartItems.innerHTML = '
Uw winkelmandje is leeg.
';
+ return;
+ }
+
+ const rows = [];
+ for (const [id, qty] of cart) {
+ const p = findProduct(id);
+ if (!p) continue;
+ const tile = p.image
+ ? `
`
+ : escapeHtml(p.from);
+ rows.push(`
+
+
${tile}
+
+
${escapeHtml(p.name)}
+
${formatPrice(p.price)} per stuk
+
+
+ ${qty}
+
+
+
+
${formatPrice(p.price * qty)}
+
+ `);
+ }
+ els.cartItems.innerHTML = rows.join("");
+}
+
+// Toast.
+let toastTimer;
+function showToast(message) {
+ els.toast.textContent = message;
+ els.toast.classList.add("show");
+ clearTimeout(toastTimer);
+ toastTimer = setTimeout(() => els.toast.classList.remove("show"), 2200);
+}
+
+// Mandje-operaties.
+function addToCart(id) {
+ const product = findProduct(id);
+ if (!product) return;
+ cart.set(id, (cart.get(id) || 0) + 1);
+ renderCart();
+ showToast(product.name + " toegevoegd aan het mandje.");
+}
+
+function stepQuantity(id, step) {
+ if (!cart.has(id)) return;
+ const next = cart.get(id) + step;
+ if (next <= 0) {
+ cart.delete(id);
+ } else {
+ cart.set(id, next);
+ }
+ renderCart();
+}
+
+function clearCart() {
+ cart.clear();
+ renderCart();
+}
+
+// Drawer.
+function openCart() {
+ els.overlay.classList.add("open");
+ els.drawer.classList.add("open");
+}
+function closeCart() {
+ els.overlay.classList.remove("open");
+ els.drawer.classList.remove("open");
+}
+
+// Modal.
+function openModal() { els.modalWrap.classList.add("open"); }
+function closeModal() { els.modalWrap.classList.remove("open"); }
+
+function renderCheckoutForm() {
+ const { total } = cartTotals();
+ els.modalContent.innerHTML = `
+ Afrekenen
+ Dit formulier verwerkt geen gegevens. Ingevoerde waarden worden niet opgeslagen of verzonden.
+
+ `;
+ document.getElementById("payForm").addEventListener("submit", (event) => {
+ event.preventDefault();
+ const form = event.currentTarget;
+ // Gebruik native validatie in plaats van ongeldige invoer te negeren.
+ if (!form.checkValidity()) {
+ form.reportValidity();
+ return;
+ }
+ renderSuccess();
+ });
+}
+
+function renderSuccess() {
+ const { count } = cartTotals();
+ const orderId = "CL-" + Math.floor(100000 + Math.random() * 900000);
+ els.modalContent.innerHTML = `
+
+
✓
+
Bestelling geregistreerd
+
${count} fictief artikel(en) zijn vastgelegd in deze demonstratie.
+
Referentie: ${escapeHtml(orderId)}
+
Verwachte levering: niet van toepassing.
+
+
+ `;
+ clearCart();
+ document.getElementById("doneBtn").addEventListener("click", () => {
+ closeModal();
+ showToast("Winkelmandje geleegd.");
+ });
+}
+
+// Event-koppeling (delegatie waar lijsten betrokken zijn).
+els.filters.addEventListener("click", (event) => {
+ const button = event.target.closest("[data-filter]");
+ if (!button) return;
+ activeFilter = button.dataset.filter;
+ renderFilters();
+ renderCatalog();
+});
+
+els.grid.addEventListener("click", (event) => {
+ const button = event.target.closest("[data-add]");
+ if (button) addToCart(button.dataset.add);
+});
+
+els.cartItems.addEventListener("click", (event) => {
+ const button = event.target.closest("[data-step]");
+ if (button) stepQuantity(button.dataset.id, Number(button.dataset.step));
+});
+
+els.openCart.addEventListener("click", openCart);
+els.closeCart.addEventListener("click", closeCart);
+els.overlay.addEventListener("click", closeCart);
+
+els.checkoutBtn.addEventListener("click", () => {
+ if (cart.size === 0) return;
+ closeCart();
+ renderCheckoutForm();
+ openModal();
+});
+
+els.modalWrap.addEventListener("click", (event) => {
+ if (event.target === els.modalWrap) closeModal();
+});
+
+document.addEventListener("keydown", (event) => {
+ if (event.key === "Escape") {
+ closeModal();
+ closeCart();
+ }
+});
+
+// Eerste render.
+document.getElementById("year").textContent = String(new Date().getFullYear());
+renderFilters();
+renderCatalog();
+renderCart();