Add domme-converters/assets/js/app.js
This commit is contained in:
@@ -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 = `
|
||||||
|
<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)}"
|
||||||
|
onerror="this.remove(); this.parentNode.classList.add('thumb--fallback'); this.parentNode.insertAdjacentHTML('beforeend', this.parentNode.dataset.fallback);"
|
||||||
|
/>`;
|
||||||
|
}
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rendering.
|
||||||
|
function renderFilters() {
|
||||||
|
els.filters.innerHTML = CATEGORIES.map((c) =>
|
||||||
|
`<button class="filter${c.key === activeFilter ? " active" : ""}" data-filter="${c.key}">${escapeHtml(c.label)}</button>`
|
||||||
|
).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 = `<span class="conn">${escapeHtml(p.from)}</span><span class="arrow">→</span><span class="conn">${escapeHtml(p.to)}</span>`;
|
||||||
|
const thumb = p.image
|
||||||
|
? `<img src="${escapeHtml(p.image)}" alt="${escapeHtml(p.name)}" onerror="this.remove(); this.parentNode.innerHTML = this.parentNode.dataset.fallback;" />`
|
||||||
|
: fallback;
|
||||||
|
return `
|
||||||
|
<article class="card">
|
||||||
|
<div class="thumb" data-fallback="${escapeHtml(fallback)}">
|
||||||
|
${thumb}
|
||||||
|
</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 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>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
`;
|
||||||
|
}).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 = '<div class="cart-empty">Uw winkelmandje is leeg.</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rows = [];
|
||||||
|
for (const [id, qty] of cart) {
|
||||||
|
const p = findProduct(id);
|
||||||
|
if (!p) continue;
|
||||||
|
const tile = p.image
|
||||||
|
? `<img src="${escapeHtml(p.image)}" alt="" onerror="this.remove(); this.parentNode.textContent = this.parentNode.dataset.fallback;" />`
|
||||||
|
: escapeHtml(p.from);
|
||||||
|
rows.push(`
|
||||||
|
<div class="cart-row">
|
||||||
|
<div class="tile" data-fallback="${escapeHtml(p.from)}" aria-hidden="true">${tile}</div>
|
||||||
|
<div class="info">
|
||||||
|
<strong>${escapeHtml(p.name)}</strong>
|
||||||
|
<span class="unit">${formatPrice(p.price)} per stuk</span>
|
||||||
|
<div class="qty">
|
||||||
|
<button data-step="-1" data-id="${p.id}" aria-label="Aantal verlagen">−</button>
|
||||||
|
<span>${qty}</span>
|
||||||
|
<button data-step="1" data-id="${p.id}" aria-label="Aantal verhogen">+</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span class="line-total">${formatPrice(p.price * qty)}</span>
|
||||||
|
</div>
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
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 = `
|
||||||
|
<h2>Afrekenen</h2>
|
||||||
|
<p class="sub">Dit formulier verwerkt geen gegevens. Ingevoerde waarden worden niet opgeslagen of verzonden.</p>
|
||||||
|
<form id="payForm" novalidate>
|
||||||
|
<div class="field">
|
||||||
|
<label for="f-name">Naam</label>
|
||||||
|
<input id="f-name" name="name" required autocomplete="name" placeholder="Voor- en achternaam" />
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label for="f-email">E-mailadres</label>
|
||||||
|
<input id="f-email" name="email" type="email" required autocomplete="email" placeholder="naam@voorbeeld.nl" />
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label for="f-address">Bezorgadres</label>
|
||||||
|
<input id="f-address" name="address" required autocomplete="street-address" placeholder="Straat, huisnummer, plaats" />
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label for="f-card">Kaartnummer (voer geen echte gegevens in)</label>
|
||||||
|
<input id="f-card" name="card" inputmode="numeric" required placeholder="0000 0000 0000 0000" />
|
||||||
|
</div>
|
||||||
|
<div class="row2">
|
||||||
|
<div class="field">
|
||||||
|
<label for="f-exp">Vervaldatum</label>
|
||||||
|
<input id="f-exp" name="exp" required placeholder="MM/JJ" />
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label for="f-cvc">CVC</label>
|
||||||
|
<input id="f-cvc" name="cvc" inputmode="numeric" required placeholder="123" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button class="pay-btn" type="submit">Plaats bestelling (${formatPrice(total)})</button>
|
||||||
|
<p class="pay-note">Demonstratieformulier. Er wordt geen betaling uitgevoerd.</p>
|
||||||
|
</form>
|
||||||
|
`;
|
||||||
|
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 = `
|
||||||
|
<div class="success">
|
||||||
|
<div class="check" aria-hidden="true">✓</div>
|
||||||
|
<h2>Bestelling geregistreerd</h2>
|
||||||
|
<p>${count} fictief artikel(en) zijn vastgelegd in deze demonstratie.</p>
|
||||||
|
<div class="order-id">Referentie: ${escapeHtml(orderId)}</div>
|
||||||
|
<p>Verwachte levering: niet van toepassing.</p>
|
||||||
|
<button class="pay-btn" id="doneBtn" style="margin-top:18px">Verder winkelen</button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
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();
|
||||||
Reference in New Issue
Block a user