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