diff --git a/app.js b/app.js index 059933a..ae4c3f2 100644 --- a/app.js +++ b/app.js @@ -1,5 +1,5 @@ const API_BASE = - "http://localhost:8000"; + "/api"; @@ -1175,4 +1175,4 @@ if(getToken()){ showLogin(); -} \ No newline at end of file +} diff --git a/calender/index.html b/calender/index.html deleted file mode 100644 index d39ae1c..0000000 --- a/calender/index.html +++ /dev/null @@ -1,146 +0,0 @@ - - - - - -Dynamic HTML Calendar - - - - -
-
- - - -
-
- Mon - Tue - Wed - Thu - Fri - Sat - Sun -
-
- - -
-
-
- - \ No newline at end of file diff --git a/domme-converters/assets/css/styles.css b/domme-converters/assets/css/styles.css deleted file mode 100644 index aec5bab..0000000 --- a/domme-converters/assets/css/styles.css +++ /dev/null @@ -1,575 +0,0 @@ -:root { - --bg: #f6f7f9; - --surface: #ffffff; - --surface-2: #f0f2f5; - --border: #e2e5ea; - --border-strong: #cdd2da; - --ink: #14181f; - --muted: #5b6470; - --muted-2: #828b97; - --accent: #1f4ed8; - --accent-hover: #1a42b8; - --accent-soft: #eaf0fe; - --ring: rgba(31, 78, 216, 0.28); - --radius: 10px; - --radius-sm: 7px; - --maxw: 1140px; - --font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; - --mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; -} - -* { box-sizing: border-box; } - -html { background: var(--bg); } - -body { - margin: 0; - font-family: var(--font); - background: var(--bg); - color: var(--ink); - line-height: 1.55; - -webkit-font-smoothing: antialiased; -} - -a { color: var(--accent); text-decoration: none; } -a:hover { text-decoration: underline; } - -.container { - max-width: var(--maxw); - margin: 0 auto; - padding: 0 24px; -} - -/* Notice bar */ -.notice { - background: var(--ink); - color: #f3f4f6; - font-size: 13px; - text-align: center; - padding: 8px 24px; -} - -/* Header */ -header { - position: sticky; - top: 0; - z-index: 40; - background: rgba(255, 255, 255, 0.9); - backdrop-filter: blur(8px); - border-bottom: 1px solid var(--border); -} -.nav { - display: flex; - align-items: center; - justify-content: space-between; - gap: 24px; - height: 68px; -} -.brand { - display: flex; - align-items: center; - gap: 12px; -} -.brand .mark { - width: 36px; - height: 36px; - border-radius: 8px; - background: var(--accent); - color: #fff; - display: grid; - place-items: center; - font-weight: 700; - font-size: 17px; - letter-spacing: -0.5px; -} -.brand .name { - font-weight: 700; - font-size: 18px; - letter-spacing: -0.3px; -} -.brand .name span { - display: block; - font-size: 11px; - font-weight: 500; - color: var(--muted-2); - letter-spacing: 0.2px; -} -.nav-links { - display: flex; - gap: 28px; - margin-left: auto; - margin-right: 8px; -} -.nav-links a { - color: var(--muted); - font-size: 14px; - font-weight: 500; -} -.nav-links a:hover { color: var(--ink); text-decoration: none; } - -.cart-btn { - position: relative; - border: 1px solid var(--border-strong); - background: var(--surface); - color: var(--ink); - padding: 9px 16px; - border-radius: var(--radius-sm); - font-weight: 600; - font-size: 14px; - cursor: pointer; - display: inline-flex; - align-items: center; - gap: 8px; - transition: border-color .15s, background .15s; -} -.cart-btn:hover { border-color: var(--accent); background: var(--accent-soft); } -.cart-count { - background: var(--accent); - color: #fff; - font-size: 12px; - font-weight: 700; - min-width: 20px; - height: 20px; - padding: 0 6px; - border-radius: 999px; - display: grid; - place-items: center; -} - -/* Hero */ -.hero { - border-bottom: 1px solid var(--border); - background: var(--surface); -} -.hero-inner { - display: grid; - grid-template-columns: 1.1fr 0.9fr; - gap: 48px; - align-items: center; - padding: 72px 24px; -} -.eyebrow { - display: inline-block; - font-size: 12px; - font-weight: 700; - letter-spacing: 1px; - text-transform: uppercase; - color: var(--accent); - margin-bottom: 16px; -} -.hero h1 { - font-size: clamp(30px, 4.5vw, 46px); - line-height: 1.08; - letter-spacing: -1px; - margin: 0 0 18px; -} -.hero p { - color: var(--muted); - font-size: 17px; - max-width: 520px; - margin: 0 0 28px; -} -.hero-actions { display: flex; gap: 12px; flex-wrap: wrap; } -.btn { - display: inline-flex; - align-items: center; - gap: 8px; - border-radius: var(--radius-sm); - font-weight: 600; - font-size: 15px; - padding: 12px 22px; - cursor: pointer; - border: 1px solid transparent; - transition: background .15s, border-color .15s, color .15s; -} -.btn-primary { background: var(--accent); color: #fff; } -.btn-primary:hover { background: var(--accent-hover); text-decoration: none; } -.btn-ghost { background: var(--surface); border-color: var(--border-strong); color: var(--ink); } -.btn-ghost:hover { border-color: var(--accent); color: var(--accent); text-decoration: none; } - -.hero-panel { - background: var(--surface-2); - border: 1px solid var(--border); - border-radius: var(--radius); - padding: 28px; -} -.spec-row { - display: flex; - justify-content: space-between; - align-items: baseline; - padding: 14px 0; - border-bottom: 1px solid var(--border); - font-size: 14px; -} -.spec-row:last-child { border-bottom: none; } -.spec-row .k { color: var(--muted); } -.spec-row .v { font-weight: 600; font-family: var(--mono); font-size: 13px; } - -/* Trust strip */ -.trust { - border-bottom: 1px solid var(--border); - background: var(--bg); -} -.trust-inner { - display: grid; - grid-template-columns: repeat(4, 1fr); - gap: 24px; - padding: 28px 24px; -} -.trust-item .t { font-size: 14px; font-weight: 600; } -.trust-item .d { font-size: 13px; color: var(--muted); margin-top: 2px; } - -/* Catalog */ -.catalog { padding: 56px 0 72px; } -.section-head { - display: flex; - align-items: flex-end; - justify-content: space-between; - margin-bottom: 28px; - gap: 16px; - flex-wrap: wrap; -} -.section-head h2 { font-size: 24px; letter-spacing: -0.5px; margin: 0 0 4px; } -.section-head p { color: var(--muted); font-size: 14px; margin: 0; } - -.filters { - display: flex; - gap: 8px; - flex-wrap: wrap; -} -.filter { - border: 1px solid var(--border-strong); - background: var(--surface); - color: var(--muted); - font-size: 13px; - font-weight: 600; - padding: 7px 14px; - border-radius: 999px; - cursor: pointer; - transition: all .15s; -} -.filter:hover { border-color: var(--accent); color: var(--accent); } -.filter.active { background: var(--ink); border-color: var(--ink); color: #fff; } - -.grid { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(264px, 1fr)); - gap: 20px; -} -.card { - background: var(--surface); - border: 1px solid var(--border); - border-radius: var(--radius); - overflow: hidden; - display: flex; - flex-direction: column; - transition: border-color .15s, box-shadow .15s, transform .15s; -} -.card:hover { - border-color: var(--border-strong); - box-shadow: 0 8px 24px rgba(20, 24, 31, 0.07); - transform: translateY(-2px); -} -.thumb { - aspect-ratio: 16 / 10; - background: var(--surface-2); - border-bottom: 1px solid var(--border); - display: flex; - align-items: center; - justify-content: center; - gap: 14px; - padding: 0 18px; - position: relative; - overflow: hidden; -} -.thumb img { - width: 100%; - height: 100%; - object-fit: cover; - display: block; -} -.conn { - background: var(--surface); - border: 1px solid var(--border-strong); - border-radius: var(--radius-sm); - padding: 8px 12px; - font-family: var(--mono); - font-size: 12px; - font-weight: 600; - color: var(--ink); - text-align: center; - min-width: 64px; - line-height: 1.2; -} -.arrow { - color: var(--accent); - font-weight: 700; - font-size: 18px; - font-family: var(--mono); -} -.card .body { - padding: 18px; - display: flex; - flex-direction: column; - gap: 8px; - flex: 1; -} -.card .meta { - display: flex; - align-items: center; - justify-content: space-between; - font-size: 12px; - color: var(--muted-2); -} -.sku { font-family: var(--mono); } -.stars { color: var(--accent); font-weight: 600; letter-spacing: 1px; } -.stars .reviews { color: var(--muted-2); font-weight: 400; letter-spacing: 0; margin-left: 4px; } -.card h3 { margin: 0; font-size: 16px; letter-spacing: -0.2px; } -.card .desc { color: var(--muted); font-size: 13.5px; flex: 1; } -.card .foot { - display: flex; - align-items: center; - justify-content: space-between; - margin-top: 8px; - padding-top: 14px; - border-top: 1px solid var(--border); -} -.price { font-weight: 700; font-size: 18px; } -.price small { - color: var(--muted-2); - font-weight: 500; - font-size: 12px; - text-decoration: line-through; - margin-left: 6px; -} -.add-btn { - border: 1px solid var(--accent); - background: var(--accent); - color: #fff; - font-weight: 600; - padding: 8px 14px; - border-radius: var(--radius-sm); - cursor: pointer; - font-size: 13.5px; - transition: background .15s; -} -.add-btn:hover { background: var(--accent-hover); } -.add-btn:active { transform: scale(0.97); } - -/* Cart drawer */ -.overlay { - position: fixed; inset: 0; - background: rgba(20, 24, 31, 0.5); - opacity: 0; pointer-events: none; - transition: opacity .2s; - z-index: 50; -} -.overlay.open { opacity: 1; pointer-events: auto; } -.drawer { - position: fixed; top: 0; right: 0; - height: 100%; width: 420px; max-width: 94vw; - background: var(--surface); - border-left: 1px solid var(--border); - transform: translateX(100%); - transition: transform .25s ease; - z-index: 60; - display: flex; - flex-direction: column; -} -.drawer.open { transform: translateX(0); } -.drawer-head { - padding: 20px 22px; - border-bottom: 1px solid var(--border); - display: flex; align-items: center; justify-content: space-between; -} -.drawer-head h2 { margin: 0; font-size: 17px; } -.icon-btn { - background: none; border: none; color: var(--muted); - font-size: 24px; cursor: pointer; line-height: 1; padding: 0 4px; -} -.icon-btn:hover { color: var(--ink); } -.cart-items { flex: 1; overflow-y: auto; padding: 8px 22px; } -.cart-empty { color: var(--muted); text-align: center; padding: 48px 0; font-size: 14px; } -.cart-row { - display: flex; gap: 14px; align-items: flex-start; - padding: 16px 0; - border-bottom: 1px solid var(--border); -} -.cart-row .tile { - width: 48px; height: 48px; border-radius: var(--radius-sm); - background: var(--surface-2); - border: 1px solid var(--border); - display: grid; place-items: center; - font-family: var(--mono); font-size: 11px; font-weight: 700; color: var(--accent); - flex-shrink: 0; - overflow: hidden; -} -.cart-row .tile img { width: 100%; height: 100%; object-fit: cover; } -.cart-row .info { flex: 1; min-width: 0; } -.cart-row .info strong { font-size: 14px; display: block; } -.cart-row .info .unit { font-size: 12px; color: var(--muted); } -.qty { display: inline-flex; align-items: center; gap: 0; margin-top: 8px; border: 1px solid var(--border-strong); border-radius: var(--radius-sm); overflow: hidden; } -.qty button { - width: 28px; height: 28px; - border: none; background: var(--surface-2); - color: var(--ink); cursor: pointer; font-weight: 700; font-size: 15px; -} -.qty button:hover { background: var(--border); } -.qty span { font-size: 13px; min-width: 32px; text-align: center; } -.line-total { font-weight: 700; font-size: 14px; white-space: nowrap; } -.drawer-foot { - padding: 20px 22px; - border-top: 1px solid var(--border); - background: var(--surface); -} -.total-row { - display: flex; justify-content: space-between; align-items: baseline; - font-size: 14px; color: var(--muted); margin-bottom: 4px; -} -.total-row.grand { color: var(--ink); margin: 10px 0 16px; } -.total-row.grand strong { font-size: 20px; } -.checkout-btn { - width: 100%; - border: none; - background: var(--accent); - color: #fff; - font-weight: 600; - padding: 14px; - border-radius: var(--radius-sm); - font-size: 15px; - cursor: pointer; - transition: background .15s; -} -.checkout-btn:hover { background: var(--accent-hover); } -.checkout-btn:disabled { background: var(--border-strong); color: var(--muted-2); cursor: not-allowed; } - -/* Modal */ -.modal-wrap { - position: fixed; inset: 0; - display: grid; place-items: center; - background: rgba(20, 24, 31, 0.6); - opacity: 0; pointer-events: none; - transition: opacity .2s; - z-index: 70; - padding: 20px; -} -.modal-wrap.open { opacity: 1; pointer-events: auto; } -.modal { - background: var(--surface); - border: 1px solid var(--border); - border-radius: 14px; - width: 480px; max-width: 100%; - max-height: 92vh; overflow-y: auto; - padding: 30px; - box-shadow: 0 24px 60px rgba(20, 24, 31, 0.25); -} -.modal h2 { margin: 0 0 4px; font-size: 22px; letter-spacing: -0.4px; } -.modal .sub { color: var(--muted); font-size: 14px; margin: 0 0 22px; } -.field { margin-bottom: 16px; } -.field label { display: block; font-size: 13px; font-weight: 600; margin-bottom: 6px; color: var(--ink); } -.field input { - width: 100%; - background: var(--surface); - border: 1px solid var(--border-strong); - border-radius: var(--radius-sm); - padding: 11px 13px; - color: var(--ink); - font-size: 14px; - font-family: var(--font); - transition: border-color .15s, box-shadow .15s; -} -.field input:focus { outline: none; border-color: var(--accent); box-shadow: 0 0 0 3px var(--ring); } -.row2 { display: flex; gap: 14px; } -.row2 .field { flex: 1; } -.pay-btn { - width: 100%; - border: none; - background: var(--accent); - color: #fff; - font-weight: 600; - padding: 14px; - border-radius: var(--radius-sm); - font-size: 15px; - cursor: pointer; - margin-top: 6px; - transition: background .15s; -} -.pay-btn:hover { background: var(--accent-hover); } -.pay-note { font-size: 12px; color: var(--muted); text-align: center; margin-top: 14px; } - -.success { text-align: center; padding: 8px 0; } -.success .check { - width: 64px; height: 64px; margin: 0 auto 18px; - border-radius: 50%; - background: var(--accent-soft); - border: 1px solid var(--accent); - display: grid; place-items: center; - color: var(--accent); font-size: 30px; font-weight: 700; -} -.success h2 { font-size: 22px; } -.success p { color: var(--muted); font-size: 14px; } -.order-id { - background: var(--surface-2); - border: 1px solid var(--border); - border-radius: var(--radius-sm); - padding: 12px; - font-family: var(--mono); - margin: 18px 0; - font-size: 14px; -} - -/* Toast */ -.toast { - position: fixed; bottom: 24px; left: 50%; - transform: translateX(-50%) translateY(16px); - background: var(--ink); - color: #fff; - padding: 12px 20px; - border-radius: var(--radius-sm); - font-size: 14px; font-weight: 500; - opacity: 0; pointer-events: none; - transition: opacity .2s, transform .2s; - z-index: 80; - box-shadow: 0 12px 32px rgba(20, 24, 31, 0.25); -} -.toast.show { opacity: 1; transform: translateX(-50%) translateY(0); } - -/* Footer */ -footer { - border-top: 1px solid var(--border); - background: var(--surface); - padding: 48px 0 32px; -} -.foot-grid { - display: grid; - grid-template-columns: 1.4fr 1fr 1fr 1fr; - gap: 32px; - margin-bottom: 36px; -} -.foot-grid h4 { font-size: 13px; text-transform: uppercase; letter-spacing: 0.6px; color: var(--muted-2); margin: 0 0 14px; } -.foot-grid ul { list-style: none; margin: 0; padding: 0; } -.foot-grid li { margin-bottom: 9px; } -.foot-grid a { color: var(--muted); font-size: 14px; } -.foot-grid a:hover { color: var(--ink); text-decoration: none; } -.foot-about p { color: var(--muted); font-size: 14px; margin: 12px 0 0; max-width: 320px; } -.foot-bottom { - border-top: 1px solid var(--border); - padding-top: 24px; - display: flex; - justify-content: space-between; - flex-wrap: wrap; - gap: 12px; - color: var(--muted-2); - font-size: 13px; -} - -@media (max-width: 900px) { - .hero-inner { grid-template-columns: 1fr; gap: 32px; padding: 48px 24px; } - .trust-inner { grid-template-columns: repeat(2, 1fr); } - .foot-grid { grid-template-columns: 1fr 1fr; } - .nav-links { display: none; } -} -@media (max-width: 560px) { - .trust-inner { grid-template-columns: 1fr; } - .foot-grid { grid-template-columns: 1fr; } -} diff --git a/domme-converters/assets/js/app.js b/domme-converters/assets/js/app.js deleted file mode 100644 index e4995a9..0000000 --- a/domme-converters/assets/js/app.js +++ /dev/null @@ -1,309 +0,0 @@ -"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 `${escapeHtml(p.name)}`; - } - 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 - ? `${escapeHtml(p.name)}` - : fallback; - return ` -
-
- ${thumb} -
-
-
- ${escapeHtml(p.id)} - ${stars(p.rating)}(${p.reviews}) -
-

${escapeHtml(p.name)}

-

${escapeHtml(p.desc)}

-
- ${formatPrice(p.price)}${formatPrice(p.old)} - -
-
-
- `; - }).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(` -
- -
- ${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.

-
-
- - -
-
- - -
-
- - -
-
- - -
-
-
- - -
-
- - -
-
- -

Demonstratieformulier. Er wordt geen betaling uitgevoerd.

-
- `; - 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(); diff --git a/domme-converters/assets/js/components.js b/domme-converters/assets/js/components.js deleted file mode 100644 index a3be363..0000000 --- a/domme-converters/assets/js/components.js +++ /dev/null @@ -1,187 +0,0 @@ -"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 = - `${escapeHtml(p.from)}` + - `` + - `${escapeHtml(p.to)}`; - if (p.image) { - return `${escapeHtml(p.name)}`; - } - return fallback; - } - - // ---- Productkaart (volledig klikbaar) ------------------------------------ - function productCard(p) { - const fallback = - `${escapeHtml(p.from)}` + - `` + - `${escapeHtml(p.to)}`; - return ` -
- -
- ${productThumb(p)} -
-
-
- ${escapeHtml(p.id)} - ${stars(p.rating)}(${p.reviews}) -
-

${escapeHtml(p.name)}

-

${escapeHtml(p.desc)}

-
-
-
- ${formatPrice(p.price)}${formatPrice(p.old)} - -
-
- `; - } - - function productGrid(list) { - if (!list.length) { - return `
Geen producten gevonden.
`; - } - return `
${list.map(productCard).join("")}
`; - } - - // ---- Paginakop ------------------------------------------------------------ - function pageHead({ eyebrow, title, intro, crumb }) { - return ` -
-
- ${crumb ? `` : ""} - ${eyebrow ? `${escapeHtml(eyebrow)}` : ""} -

${escapeHtml(title)}

- ${intro ? `

${escapeHtml(intro)}

` : ""} -
-
- `; - } - - 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 ? "" : `/`; - if (last || !part.path) return `${escapeHtml(part.label)}${sep}`; - return `${escapeHtml(part.label)}${sep}`; - }) - .join(""); - } - - // ---- Header --------------------------------------------------------------- - function header(activePath) { - const isActive = (path) => - path === activePath || (path !== "/" && activePath.startsWith(path + "/")) ? " active" : ""; - const links = NAV.map( - (item) => `${escapeHtml(item.label)}` - ).join(""); - return ` -
- -
- `; - } - - // ---- Footer --------------------------------------------------------------- - function footer() { - const column = (heading, items) => ` -
-

${escapeHtml(heading)}

- -
`; - return ` - - `; - } - - CL.fmt = { formatPrice, escapeHtml, stars }; - CL.nav = { NAV, FOOTER_LINKS, href }; - CL.components = { productThumb, productCard, productGrid, pageHead, crumb, header, footer }; -})(); diff --git a/domme-converters/assets/js/contents.js b/domme-converters/assets/js/contents.js deleted file mode 100644 index c3bcd99..0000000 --- a/domme-converters/assets/js/contents.js +++ /dev/null @@ -1,89 +0,0 @@ -"use strict"; - -/** - * Centrale inhoud voor de niet-product pagina's (bedrijf, vacatures, juridisch). - * Pagina's worden uit deze gegevens gegenereerd, zodat er geen losse, - * gedupliceerde HTML-bestanden nodig zijn. - */ -window.CONTENT = { - company: { - founded: "nooit", - employees: "0,5 fte", - headquarters: "Postbus 0, Nergenshuizen", - intro: - "Converterland is sinds onbepaalde tijd marktleider in signaalconversie tussen formaten die niet met elkaar verbonden kunnen worden. Wij geloven dat elk signaal het verdient om ergens anders terecht te komen, of dat nu zinvol is of niet.", - history: [ - { year: "Jaar 0", text: "Het idee voor de HDMI naar tuinslang ontstaat tijdens het water geven van een dode plant." }, - { year: "Jaar 1", text: "De eerste fictieve converter rolt van de niet-bestaande productielijn." }, - { year: "Jaar 3", text: "Uitbreiding naar de categorie 'Audio', met de baanbrekende AUX naar kaasplank." }, - { year: "Heden", text: "Een volledige catalogus aan onmogelijke converters, professioneel gepresenteerd en volledig onbruikbaar." }, - ], - values: [ - { title: "Compatibiliteit", text: "Wij verbinden wat niet verbonden hoort te worden, met onverstoorbaar vertrouwen." }, - { title: "Betrouwbaarheid", text: "Onze producten doen consequent niets, elke keer weer, zonder uitzondering." }, - { title: "Duurzaamheid", text: "Niets verzenden is de meest milieuvriendelijke logistiek die er bestaat." }, - ], - }, - - jobs: [ - { - id: "vac-01", title: "Senior Signaalfilosoof", type: "Voltijd (theoretisch)", location: "Op afstand, zeer ver", - summary: "U doordenkt of een signaal überhaupt ergens heen wil voordat wij beloven het om te zetten.", - details: "Als Senior Signaalfilosoof bent u verantwoordelijk voor de existentiële onderbouwing van onze converters. U beantwoordt vragen als 'wil dit signaal dit wel?' en 'wat is een poort eigenlijk?'. Ervaring met onmogelijke problemen is een pré.", - requirements: ["Aantoonbaar geen ervaring vereist", "Comfortabel met dubbelzinnigheid", "Eigen kabels meenemen"], - }, - { - id: "vac-02", title: "Hoofd Niet-Verzending", type: "Deeltijd", location: "Magazijn (leeg)", - summary: "U bewaakt dat er consequent niets de deur uitgaat. Punctualiteit niet vereist.", - details: "U leidt het team Niet-Verzending en zorgt dat bestellingen met grote zorg nergens naartoe worden gestuurd. U rapporteert aan niemand en beheert een magazijn dat opvallend leeg blijft.", - requirements: ["Talent voor stilstand", "Geen rijbewijs nodig", "Affiniteit met lege dozen"], - }, - { - id: "vac-03", title: "Klantbeloftecoördinator", type: "Stage", location: "Hybride (vooral afwezig)", - summary: "U beheert verwachtingen die wij toch niet waarmaken. Communicatief sterk gewenst.", - details: "U vertaalt onmogelijke productclaims naar geruststellende formuleringen. U beheert onze support 0/7 en zorgt dat de telefoon op het juiste moment niet wordt opgenomen.", - requirements: ["Engelengeduld", "Creatief met disclaimers", "Bereid om niet op te nemen"], - }, - ], - - contact: { - address: "Converterland B.V. (fictief)\nPostbus 0\n0000 AA Nergenshuizen", - email: "hallo@converterland.example", - phone: "+31 (0)0 000 00 00", - hours: "Bereikbaar: 0/7, het hele jaar door niet.", - }, - - legal: { - voorwaarden: { - title: "Algemene voorwaarden", - intro: "Deze voorwaarden zijn van toepassing op alle fictieve aankopen binnen deze demonstratie- en parodiewinkel.", - sections: [ - { h: "1. Toepasselijkheid", p: "Door deze website te gebruiken erkent u dat alle producten verzonnen zijn en dat er nooit een overeenkomst tot stand komt. Elke ogenschijnlijke bestelling is uitsluitend bedoeld als demonstratie." }, - { h: "2. Producten", p: "De aangeboden converters bestaan niet en zijn technisch onmogelijk. Specificaties zijn fictief en dienen uitsluitend ter vermaak. Aan afbeeldingen, prijzen en sterbeoordelingen kunnen geen rechten worden ontleend." }, - { h: "3. Bestellingen en betaling", p: "Er worden geen betalingen verwerkt. Ingevoerde gegevens in het afrekenformulier worden niet opgeslagen, niet verzonden en niet bewaard. Gebruik nooit echte betaalgegevens." }, - { h: "4. Levering", p: "Er vindt geen levering plaats. De vermelde levertijden ('nooit', 'niet van toepassing') zijn letterlijk bedoeld." }, - { h: "5. Aansprakelijkheid", p: "Aan deze website kunnen geen rechten worden ontleend. Converterland aanvaardt geen enkele aansprakelijkheid, onder andere omdat Converterland niet bestaat." }, - ], - }, - privacy: { - title: "Privacyverklaring", - intro: "Wij hechten waarde aan uw privacy, vooral omdat wij niets van u verzamelen.", - sections: [ - { h: "1. Gegevensverwerking", p: "Deze website verwerkt geen persoonsgegevens op een server. Er is geen backend, geen account en geen database. Het winkelmandje wordt uitsluitend lokaal in uw eigen browser bewaard." }, - { h: "2. Het afrekenformulier", p: "Gegevens die u in het demonstratie-afrekenformulier invult, verlaten uw apparaat niet en worden direct vergeten zodra u de pagina sluit. Voer hier nooit echte gegevens in." }, - { h: "3. Cookies", p: "Wij plaatsen geen tracking- of marketingcookies. De enige lokale opslag betreft uw winkelmandje, zodat dit bewaard blijft tijdens het navigeren." }, - { h: "4. Derden", p: "Wij delen niets met derden, omdat er niets te delen valt." }, - ], - }, - disclaimer: { - title: "Disclaimer", - intro: "Converterland is een parodieproject. Lees deze disclaimer als geheugensteun.", - sections: [ - { h: "1. Fictief karakter", p: "Alle producten, merknamen, beoordelingen en bedrijfsgegevens op deze website zijn verzonnen. Niets is te koop en niets wordt geleverd." }, - { h: "2. Geen handelsonderneming", p: "Converterland is geen bestaande onderneming. Adres, telefoonnummer en e-mailadres zijn placeholders en leiden nergens heen." }, - { h: "3. Gebruik op eigen risico", p: "Deze site is bedoeld ter vermaak en als demonstratie van een meerpagina-webshop. Neem geen technische claims serieus." }, - { h: "4. Vragen", p: "Heeft u vragen, dan kunt u die stellen via het contactformulier, dat ze vervolgens nergens naartoe stuurt." }, - ], - }, - }, -}; diff --git a/domme-converters/assets/js/pages.js b/domme-converters/assets/js/pages.js deleted file mode 100644 index 3652f9c..0000000 --- a/domme-converters/assets/js/pages.js +++ /dev/null @@ -1,575 +0,0 @@ -"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: ` -
-
-
- Universele signaalconversie -

Elk signaal naar elk medium. Theoretisch.

-

- 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. -

- -
-
-
Ondersteunde signalen
-
Gemiddelde latency3 werkdagen
-
Compatibiliteit0 / 0
-
Garantiegeen
-
Retourtermijnn.v.t.
-
-
-
- -
-
-
Niet op voorraad
Elk product, altijd.
-
Geen verzendkosten
Want geen verzending.
-
Veilig afrekenen
Er gebeurt niets.
-
Support 0/7
Wij nemen niet op.
-
-
- -
-
-
-

Uitgelicht

-

Een greep uit onze meest besproken converters.

-
- Alle producten → -
- ${productGrid(featured)} -
- `, - }; - } - - // ---- 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" }]), - })} -
-

Alle converters

${PRODUCTS.length} producten

- ${productGrid(PRODUCTS)} -
- `, - }; - } - - // ---- Catalogus (zoeken / sorteren / filteren) ----------------------------- - function catalogus() { - const filterButtons = CATEGORIES.map( - (c) => `` - ).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" }]), - })} -
-
-
- - -
-
-
${filterButtons}
- -
-
-

-
${productGrid(PRODUCTS)}
-
- `, - 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) => ` - - ${escapeHtml(p.name)}${escapeHtml(p.id)} - ${escapeHtml(p.from)} → ${escapeHtml(p.to)} - ${escapeHtml(categoryLabel(p.category))} - ${escapeHtml(p.stock.label)} - ${formatPrice(p.price)} - ` - ).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" }]), - })} -
-
- - - - - ${rows} -
ProductConversieCategorieBeschikbaarheidPrijs
-
-
-

Algemene kenmerken

-
-
Ondersteunde signalen
-
Gemiddelde latency3 werkdagen
-
Compatibiliteit0 / 0
-
Garantiegeen
-
Retourtermijnn.v.t.
-
Certificeringzelfverklaard
-
-
-
- `, - }; - } - - // ---- Aanbiedingen --------------------------------------------------------- - function aanbiedingen() { - const list = [...PRODUCTS].sort((a, b) => discount(b) - discount(a)); - const cards = list - .map((p) => { - const fallback = - `${escapeHtml(p.from)}${escapeHtml(p.to)}`; - return ` -
- -
- -${discount(p)}% - ${productThumb(p)} -
-
-
${escapeHtml(p.id)}${stars(p.rating)}(${p.reviews})
-

${escapeHtml(p.name)}

-

${escapeHtml(p.desc)}

-
-
-
- ${formatPrice(p.price)}${formatPrice(p.old)} - -
-
`; - }) - .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" }]), - })} -
-
${cards}
-
- `, - }; - } - - // ---- Bedrijf -------------------------------------------------------------- - function bedrijf() { - const c = CONTENT.company; - const values = c.values - .map((v) => `

${escapeHtml(v.title)}

${escapeHtml(v.text)}

`) - .join(""); - return { - title: "Bedrijf — Converterland", - html: ` - ${pageHead({ - eyebrow: "Over de onderneming", - title: "Bedrijf", - intro: c.intro, - crumb: crumb([{ label: "Home", path: "/" }, { label: "Bedrijf" }]), - })} -
-
-
${escapeHtml(c.founded)}Opgericht
-
${escapeHtml(c.employees)}Medewerkers
-
${PRODUCTS.length}Producten
-
0Leveringen
-
-
${values}
-
- Over ons - Vacatures - Contact -
-
- `, - }; - } - - // ---- Over ons ------------------------------------------------------------- - function overOns() { - const c = CONTENT.company; - const timeline = c.history - .map((h) => `
  • ${escapeHtml(h.year)}${escapeHtml(h.text)}
  • `) - .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" }]), - })} -
    -
    -

    Hoofdkantoor

    -

    ${escapeHtml(c.headquarters)}

    -
    -

    Tijdlijn

    -
      ${timeline}
    -
    - `, - }; - } - - // ---- 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" }]), - })} -
    -
    -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    - - -
    - -

    -
    - -
    -
    - `, - 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) => ` -
    - - -
    ` - ) - .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" }]), - })} -
    -
    ${items}
    -
    - `, - 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) => `

    ${escapeHtml(s.h)}

    ${escapeHtml(s.p)}

    `) - .join(""); - return { - title: data.title + " — Converterland", - html: ` - ${pageHead({ - eyebrow: "Juridisch", - title: data.title, - intro: data.intro, - crumb: crumb([{ label: "Home", path: "/" }, { label: data.title }]), - })} -
    -
    ${sections}
    -
    - `, - }; - } - - // ---- Productdetail -------------------------------------------------------- - function productDetail(slug) { - const p = bySlug(slug); - if (!p) return notFound(); - - const specRows = Object.entries(p.specs) - .map(([k, v]) => `
    ${escapeHtml(k)}${escapeHtml(v)}
    `) - .join(""); - const features = p.features.map((f) => `
  • ${escapeHtml(f)}
  • `).join(""); - const fallback = - `${escapeHtml(p.from)}${escapeHtml(p.to)}`; - 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, - })} -
    -
    -
    -
    ${productThumb(p)}
    -
    - ${escapeHtml(categoryLabel(p.category))} - ${escapeHtml(p.from)} → ${escapeHtml(p.to)} -
    -
    -
    -
    - ${escapeHtml(p.id)} - ${stars(p.rating)}(${p.reviews} beoordelingen) -
    -
    - ${formatPrice(p.price)}${formatPrice(p.old)} - U bespaart ${formatPrice(p.old - p.price)} -
    -

    ${escapeHtml(p.stock.label)}

    -

    ${escapeHtml(p.long)}

    -
    - - Verder winkelen -
    -
    -

    Eigenschappen

    -
      ${features}
    -
    -
    -
    - -
    -

    Technische specificaties

    -
    ${specRows}
    -
    - - ${ - related.length - ? `` - : "" - } -
    - `, - }; - } - - // ---- 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." })} -
    - Terug naar de startpagina -
    - `, - }; - } - - CL.pages = { - home, - producten, - catalogus, - specificaties, - aanbiedingen, - bedrijf, - overOns, - contact, - vacatures, - voorwaarden: () => legalPage("voorwaarden"), - privacy: () => legalPage("privacy"), - disclaimer: () => legalPage("disclaimer"), - productDetail, - notFound, - }; -})(); diff --git a/domme-converters/assets/js/products.js b/domme-converters/assets/js/products.js deleted file mode 100644 index d7647da..0000000 --- a/domme-converters/assets/js/products.js +++ /dev/null @@ -1,149 +0,0 @@ -"use strict"; - -/** - * Centrale productcatalogus. - * Elk product is de enige bron van waarheid; alle pagina's en het mandje - * worden hieruit gegenereerd. - * - * Velden: - * - id : artikelnummer / SKU. - * - slug : semantisch URL-deel, bijv. /producten/hdmi-naar-tuinslang. - * - from / to : labels op de connector-tegel (fallback zonder foto). - * - category : sleutel uit CATEGORIES. - * - image : pad naar een foto in assets/images/ (leeg = connector-tegel). - * - stock : { label, available } voor de beschikbaarheidsweergave. - * - desc : korte omschrijving voor kaarten. - * - long : uitgebreide omschrijving voor de productpagina. - * - specs : technische specificaties (label -> waarde). - * - features : opsomming van eigenschappen. - */ -window.PRODUCTS = [ - { - id: "CL-1001", slug: "hdmi-naar-tuinslang", from: "HDMI", to: "Slang", category: "video", - name: "HDMI naar tuinslang", price: 24.99, old: 39.99, rating: 4, reviews: 1204, - image: "assets/images/cl-1001.png", - stock: { label: "Theoretisch leverbaar", available: true }, - desc: "Geleidt 4K-beeldsignaal rechtstreeks naar een standaard tuinslangkoppeling.", - long: "De HDMI naar tuinslang vormt de brug tussen uw mediacentrum en de besproeiing van de border. Het beeldsignaal wordt zonder noemenswaardig verlies omgezet naar een waterkolom die geschikt is voor de meeste Europese slangkoppelingen. Ideaal voor wie zijn vakantiefoto's letterlijk wil laten stromen.", - specs: { "Aansluiting in": "HDMI 2.1 (m)", "Aansluiting uit": "Gardena-compatibel 13 mm", "Maximale resolutie": "4K bij 60 Hz", "Maximale waterdruk": "6 bar", "Kabellengte": "1,5 m", "Behuizing": "Geborsteld aluminium" }, - features: ["Volledig waterdichte signaalkern", "Geschikt voor binnen- en buitengebruik", "Knikbestendige trekontlasting", "Geen driver vereist (ook niet beschikbaar)"] - }, - { - id: "CL-1002", slug: "ethernet-naar-midi", from: "RJ45", to: "MIDI", category: "audio", - name: "Ethernet naar MIDI", price: 18.50, old: 29.00, rating: 5, reviews: 893, - image: "assets/images/cl-1002.png", - stock: { label: "Op voorraad in theorie", available: true }, - desc: "Zet netwerkpakketten om in MIDI-noten. Gemeten latency rond drie werkdagen.", - long: "De Ethernet naar MIDI vertaalt binnenkomend netwerkverkeer naar muzikale noten. Elk TCP-pakket wordt een toon, elke timeout een rust. Uw firewall-logboek wordt een symfonie. De gemeten latency van circa drie werkdagen maakt het instrument bijzonder geschikt voor zeer geduldige componisten.", - specs: { "Aansluiting in": "RJ45 (1 Gbit/s)", "Aansluiting uit": "5-pins DIN MIDI", "Polyfonie": "Onbeperkt, theoretisch", "Latency": "≈ 3 werkdagen", "Voeding": "Power over Ethernet", "Protocol": "MIDI 1.0 over IP" }, - features: ["Realtime omzetting van pakketten naar noten", "Compatibel met de meeste DAW's die niet bestaan", "Automatische kwantisering naar werkdagen", "Galmvrije netwerkstilte"] - }, - { - id: "CL-1003", slug: "sata-naar-9v-analoog", from: "SATA", to: "9V", category: "power", - name: "SATA naar 9V analoog", price: 12.95, old: 19.95, rating: 3, reviews: 412, - image: "assets/images/cl-1003.png", - stock: { label: "Beperkt leverbaar", available: true }, - desc: "Leest een harde schijf uit en levert het resultaat als analoge 9V-spanning.", - long: "De SATA naar 9V analoog zet de inhoud van uw harde schijf om in een vloeiende analoge spanning tussen 0 en 9 volt. Documenten worden golfvormen, vakantiefoto's worden ruis. Sluit een oude effectpedaal aan en beluister uw back-up zoals die bedoeld was: volstrekt onbegrijpelijk.", - specs: { "Aansluiting in": "SATA III data + voeding", "Aansluiting uit": "9V mono jack", "Uitgangsspanning": "0–9 V analoog", "Bemonstering": "Niet van toepassing", "Ruisniveau": "Aangenaam hoog", "Behuizing": "Mat zwart ABS" }, - features: ["Directe omzetting van data naar spanning", "Geschikt voor gitaarversterkers", "Geen formattering nodig", "Volledig onleesbaar resultaat gegarandeerd"] - }, - { - id: "CL-1004", slug: "hdmi-naar-kippensoep", from: "HDMI", to: "Soep", category: "video", - name: "HDMI naar kippensoep", price: 32.00, old: 49.99, rating: 5, reviews: 2671, - image: "assets/images/cl-1004.png", - stock: { label: "Goed leverbaar", available: true }, - desc: "Converteert een beeldsignaal naar warme bouillon. Vegetarische variant op aanvraag.", - long: "Onze bestverkochte converter zet ieder beeldsignaal om in een portie warme kippensoep. Een actiefilm levert een stevige bouillon, een natuurdocumentaire iets met meer groente. De vegetarische variant vervangt de kip door een gelijkwaardig signaalverlies.", - specs: { "Aansluiting in": "HDMI 2.0", "Aansluiting uit": "Hittebestendige tuit", "Capaciteit": "250 ml per aflevering", "Temperatuur": "63 °C ± 2", "Smaakprofiel": "Afhankelijk van het beeld", "Reiniging": "Handwas aanbevolen" }, - features: ["Warm signaal binnen enkele minuten", "Vegetarische modus op aanvraag", "Geïntegreerde zeef", "Geschikt voor filmavonden"] - }, - { - id: "CL-1005", slug: "usb-c-naar-faxrook", from: "USB-C", to: "Rook", category: "data", - name: "USB-C naar faxrook", price: 27.75, old: 44.00, rating: 4, reviews: 556, - image: "assets/images/cl-1005.png", - stock: { label: "Theoretisch leverbaar", available: true }, - desc: "Verstuurt bestanden als rooksignaal. Compatibel met de meeste schoorstenen.", - long: "De USB-C naar faxrook moderniseert de prehistorie. Bestanden worden omgezet in nette rookwolken die door collega's op een nabijgelegen heuvel kunnen worden ontcijferd. Werkt het best bij windstil weer en een gedeelde afspraak over wat de wolken betekenen.", - specs: { "Aansluiting in": "USB-C 3.2", "Aansluiting uit": "Geïsoleerde rookpijp", "Doorvoersnelheid": "1 wolk per minuut", "Bereik": "Tot de horizon", "Brandstof": "Niet meegeleverd", "Veiligheid": "Rookmelder aanbevolen" }, - features: ["Compatibel met de meeste schoorstenen", "Werkt offline en off-grid", "Geen abonnement nodig", "Historisch verantwoord"] - }, - { - id: "CL-1006", slug: "vga-naar-sok", from: "VGA", to: "Sok", category: "video", - name: "VGA naar sok", price: 9.99, old: 14.99, rating: 2, reviews: 88, - image: "assets/images/cl-1006.png", - stock: { label: "Laatste exemplaren", available: true }, - desc: "Beeldscherm-uitvoer naar een enkele katoenen sok. Beschikbaar in maat 38 tot 46.", - long: "De VGA naar sok is ons instapmodel. Het analoge beeldsignaal wordt geweven in een enkele katoenen sok, waardoor uw desktop voortaan ook draagbaar is. Let op: de andere sok is, net als bij echte sokken, onvindbaar.", - specs: { "Aansluiting in": "VGA (15-pins D-sub)", "Aansluiting uit": "Katoenen boord", "Maatvoering": "EU 38–46", "Materiaal": "80% katoen, 20% mysterie", "Wasvoorschrift": "30 °C fijne was", "Inhoud": "1 sok" }, - features: ["Draagbare desktop", "Ademend materiaal", "Geschikt voor de wasmachine", "Eén sok per verpakking"] - }, - { - id: "CL-1007", slug: "bluetooth-naar-koffie", from: "BT", to: "Koffie", category: "audio", - name: "Bluetooth naar koffie", price: 21.40, old: 33.00, rating: 5, reviews: 1999, - image: "assets/images/cl-1007.png", - stock: { label: "Goed leverbaar", available: true }, - desc: "Draadloze koppeling met een verse espresso. Pairing-code is altijd 0000.", - long: "De Bluetooth naar koffie koppelt draadloos met een vers gezette espresso. Na het pairen (code 0000, altijd) ontvangt u binnen handbereik een kopje van gemiddelde kwaliteit. Ondersteunt meerdere apparaten, mits ze allemaal koffie willen.", - specs: { "Protocol": "Bluetooth 5.2", "Pairing-code": "0000", "Capaciteit": "1 espresso per koppeling", "Bereik": "10 m", "Bonen": "Niet inbegrepen", "Accu": "Ingebouwd, niet vervangbaar" }, - features: ["Volledig draadloze koffie", "Ondersteunt meerdere apparaten", "Energiezuinige sluimerstand", "Pairing-code nooit te vergeten"] - }, - { - id: "CL-1008", slug: "displayport-naar-wind", from: "DP", to: "Wind", category: "video", - name: "DisplayPort naar wind", price: 16.20, old: 25.00, rating: 4, reviews: 734, - image: "assets/images/cl-1008.png", - stock: { label: "Theoretisch leverbaar", available: true }, - desc: "Vertaalt framerate naar luchtverplaatsing. Bij 144Hz wordt orkaankracht bereikt.", - long: "De DisplayPort naar wind zet de framerate van uw monitor om in een verfrissende bries. Op 60 Hz blijft het bij een zacht briesje; vanaf 144 Hz raden wij aan losse voorwerpen vast te zetten. Inclusief windschaal-sticker voor op uw bureau.", - specs: { "Aansluiting in": "DisplayPort 1.4", "Aansluiting uit": "Gerichte luchtmond", "Windkracht": "1–11 Bft", "Drempel orkaan": "144 Hz", "Geluidsniveau": "Afhankelijk van scène", "Behuizing": "Geventileerd polycarbonaat" }, - features: ["Variabele windkracht via framerate", "Inclusief windschaal-sticker", "Geen ventilator met bewegende delen", "Verkoelend tijdens het gamen"] - }, - { - id: "CL-1009", slug: "glasvezel-naar-slak", from: "Fiber", to: "Slak", category: "data", - name: "Glasvezel naar slak", price: 45.00, old: 69.99, rating: 3, reviews: 203, - image: "assets/images/cl-1009.png", - stock: { label: "Beperkt leverbaar", available: true }, - desc: "Gigabit-doorvoer in biologisch afbreekbare, uiterst trage uitvoering.", - long: "De Glasvezel naar slak combineert de bandbreedte van glasvezel met de voortvarendheid van een tuinslak. Pakketten arriveren gegarandeerd, zij het wanneer ze daar zelf aan toe zijn. Volledig biologisch afbreekbaar en bijzonder milieuvriendelijk.", - specs: { "Aansluiting in": "SC/APC glasvezel", "Transport": "Gastropode", "Doorvoer": "Tot 1 Gbit/s, ooit", "Snelheid": "0,013 m/s", "Voeding": "Sla", "Levensduur": "Seizoensgebonden" }, - features: ["Biologisch afbreekbaar transport", "Geen elektriciteit nodig", "Volledig geruisloos", "Komt altijd aan, uiteindelijk"] - }, - { - id: "CL-1010", slug: "aux-naar-kaasplank", from: "AUX", to: "Kaas", category: "audio", - name: "AUX naar kaasplank", price: 14.49, old: 22.00, rating: 5, reviews: 1420, - image: "assets/images/cl-1010.png", - stock: { label: "Goed leverbaar", available: true }, - desc: "Audio uit de ene poort, kaas uit de andere. Geschikt voor borrelopstellingen.", - long: "De AUX naar kaasplank tilt elke borrel naar een hoger niveau. Steek de jack in uw telefoon en de converter serveert een passende kaas bij het nummer. Jazz levert iets gerijpts op, hardstyle iets met peper. Plankje en mesje inbegrepen.", - specs: { "Aansluiting in": "3,5 mm jack (stereo)", "Aansluiting uit": "Eikenhouten plank", "Capaciteit": "3 soorten kaas", "Koeling": "Passief", "Accessoires": "Mesje inbegrepen", "Onderhoud": "Afneembaar met doek" }, - features: ["Kaasselectie op basis van genre", "Plankje en mesje inbegrepen", "Geschikt voor borrelopstellingen", "Stille werking tussen de gangen"] - }, - { - id: "CL-1011", slug: "rj45-naar-buitenaards", from: "RJ45", to: "Ruimte", category: "data", - name: "RJ45 naar buitenaards", price: 99.99, old: 149.99, rating: 5, reviews: 42, - image: "assets/images/cl-1011.png", - stock: { label: "Zeer beperkt leverbaar", available: true }, - desc: "Koppelt de router aan het bredere melkwegstelsel. Ping naar Mars circa vier minuten.", - long: "Ons vlaggenschip. De RJ45 naar buitenaards verbindt uw thuisnetwerk met het bredere melkwegstelsel. De ping naar Mars bedraagt onder gunstige omstandigheden circa vier minuten. Eventuele antwoorden van buitenaardse beschavingen vallen buiten de garantie.", - specs: { "Aansluiting in": "RJ45 (10 Gbit/s)", "Aansluiting uit": "Parabolische zender", "Bereik": "Het waarneembare heelal", "Ping naar Mars": "≈ 4 min", "Vermogen": "Aanzienlijk", "Vergunning": "Niet geregeld" }, - features: ["Interplanetaire connectiviteit", "Geschikt voor first contact", "Richtbare antenne", "Indrukwekkend op verjaardagen"] - }, - { - id: "CL-1012", slug: "usb-c-naar-drumsolo", from: "USB-C", to: "Drum", category: "power", - name: "USB-C naar drumsolo", price: 19.95, old: 29.95, rating: 4, reviews: 612, - image: "assets/images/cl-1012.png", - stock: { label: "Goed leverbaar", available: true }, - desc: "Bij elke laadcyclus wordt een korte drumsolo afgespeeld, inclusief cimbaal.", - long: "De USB-C naar drumsolo maakt opladen weer een gebeurtenis. Elke laadcyclus begint met een korte, energieke drumsolo, compleet met afsluitende cimbaal. Het volume schaalt mee met het laadvermogen, dus snelladen wordt een waar spektakel.", - specs: { "Aansluiting in": "USB-C Power Delivery", "Uitvoer": "Geïntegreerde luidspreker", "Vermogen": "Tot 100 W", "Soloduur": "4–8 seconden", "Cimbaal": "Inbegrepen", "Volume": "Schaalt met laadsnelheid" }, - features: ["Drumsolo bij elke laadcyclus", "Volume schaalt met vermogen", "Afsluitende cimbaal", "Niet uit te schakelen"] - }, -]; - -window.CATEGORIES = [ - { key: "all", label: "Alles" }, - { key: "video", label: "Video" }, - { key: "audio", label: "Audio" }, - { key: "data", label: "Data" }, - { key: "power", label: "Voeding" }, -]; diff --git a/domme-converters/assets/js/router.js b/domme-converters/assets/js/router.js deleted file mode 100644 index 5a610d8..0000000 --- a/domme-converters/assets/js/router.js +++ /dev/null @@ -1,88 +0,0 @@ -"use strict"; - -/** - * Hash-router voor de single-page shell. Houdt semantische, leesbare paden aan - * (bijv. #/producten/hdmi-naar-tuinslang) en werkt zonder serverconfiguratie, - * ook bij direct openen van het bestand. - */ -(function () { - const CL = (window.CL = window.CL || {}); - const pages = CL.pages; - - const headerEl = document.getElementById("site-header"); - const appEl = document.getElementById("app"); - const footerEl = document.getElementById("site-footer"); - - // Statische routes -> view-functies. - const routes = { - "/": pages.home, - "/producten": pages.producten, - "/catalogus": pages.catalogus, - "/specificaties": pages.specificaties, - "/aanbiedingen": pages.aanbiedingen, - "/bedrijf": pages.bedrijf, - "/over-ons": pages.overOns, - "/contact": pages.contact, - "/vacatures": pages.vacatures, - "/voorwaarden": pages.voorwaarden, - "/privacy": pages.privacy, - "/disclaimer": pages.disclaimer, - }; - - function parsePath() { - const raw = location.hash.replace(/^#/, ""); - const path = raw.split("?")[0] || "/"; - return path === "" ? "/" : path; - } - - // Bepaalt welke view bij een pad hoort (incl. dynamische productroute). - function resolve(path) { - if (routes[path]) return routes[path](); - const productMatch = path.match(/^\/producten\/([^/]+)$/); - if (productMatch) return pages.productDetail(decodeURIComponent(productMatch[1])); - return pages.notFound(); - } - - // Het pad dat de header gebruikt voor de actieve-link-markering. - function navPathFor(path) { - if (/^\/producten\/[^/]+$/.test(path)) return "/producten"; - return path; - } - - function render() { - const path = parsePath(); - const view = resolve(path); - - headerEl.innerHTML = CL.components.header(navPathFor(path)); - appEl.innerHTML = view.html; - document.title = view.title; - - CL.ui.bindHeader(); - CL.ui.updateCartCount(); - if (typeof view.onMount === "function") view.onMount(); - - window.scrollTo({ top: 0, behavior: "auto" }); - } - - function boot() { - footerEl.innerHTML = CL.components.footer(); - CL.ui.init(); - - // Mandje-wijzigingen werken de teller en de lade overal bij. - CL.store.subscribe(() => { - CL.ui.updateCartCount(); - CL.ui.renderCart(); - }); - - window.addEventListener("hashchange", render); - if (!location.hash) location.replace("#/"); - render(); - CL.ui.renderCart(); - } - - if (document.readyState === "loading") { - document.addEventListener("DOMContentLoaded", boot); - } else { - boot(); - } -})(); diff --git a/domme-converters/assets/js/store.js b/domme-converters/assets/js/store.js deleted file mode 100644 index 666d8a6..0000000 --- a/domme-converters/assets/js/store.js +++ /dev/null @@ -1,110 +0,0 @@ -"use strict"; - -/** - * Winkelmandje-store met persistentie in localStorage en een eenvoudig - * publish/subscribe-mechanisme. Alle pagina's delen dezelfde toestand, zodat - * het mandje behouden blijft tijdens het navigeren. - */ -(function () { - const STORAGE_KEY = "converterland.cart.v1"; - const TAX_RATE = 0.21; - - const CL = (window.CL = window.CL || {}); - const products = window.PRODUCTS || []; - const findProduct = (id) => products.find((p) => p.id === id); - - /** @type {Map} product-id -> aantal */ - const cart = new Map(); - const listeners = new Set(); - - function load() { - try { - const raw = localStorage.getItem(STORAGE_KEY); - if (!raw) return; - const entries = JSON.parse(raw); - if (Array.isArray(entries)) { - for (const [id, qty] of entries) { - if (findProduct(id) && Number.isFinite(qty) && qty > 0) { - cart.set(id, Math.floor(qty)); - } - } - } - } catch (err) { - // Beschadigde opslag negeren en met een leeg mandje starten. - } - } - - function persist() { - try { - localStorage.setItem(STORAGE_KEY, JSON.stringify([...cart.entries()])); - } catch (err) { - // Opslag kan geblokkeerd zijn (privémodus); de store werkt dan in-memory. - } - } - - function emit() { - persist(); - for (const fn of listeners) fn(); - } - - const store = { - TAX_RATE, - - subscribe(fn) { - listeners.add(fn); - return () => listeners.delete(fn); - }, - - entries() { - return [...cart.entries()]; - }, - - size() { - return cart.size; - }, - - quantityOf(id) { - return cart.get(id) || 0; - }, - - add(id, amount = 1) { - const product = findProduct(id); - if (!product) return; - cart.set(id, (cart.get(id) || 0) + amount); - emit(); - }, - - setQuantity(id, qty) { - if (!findProduct(id)) return; - if (qty <= 0) cart.delete(id); - else cart.set(id, Math.floor(qty)); - emit(); - }, - - step(id, delta) { - if (!cart.has(id)) return; - this.setQuantity(id, cart.get(id) + delta); - }, - - clear() { - cart.clear(); - emit(); - }, - - totals() { - 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 }; - }, - }; - - load(); - CL.store = store; -})(); diff --git a/domme-converters/assets/js/ui.js b/domme-converters/assets/js/ui.js deleted file mode 100644 index fcc4723..0000000 --- a/domme-converters/assets/js/ui.js +++ /dev/null @@ -1,208 +0,0 @@ -"use strict"; - -/** - * Globale UI die op elke pagina aanwezig is: winkelmandje-lade, afrekenmodal - * en toast-meldingen. Deze module praat uitsluitend met CL.store, zodat de - * mandjestoestand consistent blijft tijdens het navigeren. - */ -(function () { - const CL = (window.CL = window.CL || {}); - const { formatPrice, escapeHtml } = CL.fmt; - const store = CL.store; - const PRODUCTS = window.PRODUCTS || []; - const findProduct = (id) => PRODUCTS.find((p) => p.id === id); - - const els = {}; - let toastTimer; - - function init() { - els.overlay = document.getElementById("overlay"); - els.drawer = document.getElementById("drawer"); - els.closeCart = document.getElementById("closeCart"); - els.cartItems = document.getElementById("cartItems"); - els.cartSub = document.getElementById("cartSub"); - els.cartTax = document.getElementById("cartTax"); - els.cartTotal = document.getElementById("cartTotal"); - els.checkoutBtn = document.getElementById("checkoutBtn"); - els.modalWrap = document.getElementById("modalWrap"); - els.modalContent = document.getElementById("modalContent"); - els.toast = document.getElementById("toast"); - - els.closeCart.addEventListener("click", closeCart); - els.overlay.addEventListener("click", closeCart); - els.checkoutBtn.addEventListener("click", () => { - if (store.size() === 0) return; - closeCart(); - renderCheckoutForm(); - openModal(); - }); - - // Aantal aanpassen in de lade (delegatie). - els.cartItems.addEventListener("click", (e) => { - const btn = e.target.closest("[data-step]"); - if (btn) store.step(btn.dataset.id, Number(btn.dataset.step)); - }); - - // "Toevoegen" werkt overal: catalogus, productpagina, aanbiedingen. - document.addEventListener("click", (e) => { - const add = e.target.closest("[data-add]"); - if (!add) return; - e.preventDefault(); - const product = findProduct(add.dataset.add); - if (!product) return; - store.add(product.id); - showToast(product.name + " toegevoegd aan het mandje."); - }); - - els.modalWrap.addEventListener("click", (e) => { - if (e.target === els.modalWrap) closeModal(); - }); - document.addEventListener("keydown", (e) => { - if (e.key === "Escape") { closeModal(); closeCart(); } - }); - } - - // Koppelt knoppen die per route opnieuw worden gerenderd (in de header). - function bindHeader() { - const openCartBtn = document.getElementById("openCart"); - if (openCartBtn) openCartBtn.addEventListener("click", openCart); - - const toggle = document.getElementById("navToggle"); - const links = document.getElementById("navLinks"); - if (toggle && links) { - toggle.addEventListener("click", () => { - const open = links.classList.toggle("open"); - toggle.setAttribute("aria-expanded", String(open)); - }); - // Sluit het mobiele menu na een keuze. - links.addEventListener("click", (e) => { - if (e.target.closest("a")) { - links.classList.remove("open"); - toggle.setAttribute("aria-expanded", "false"); - } - }); - } - } - - function updateCartCount() { - const { count } = store.totals(); - document.querySelectorAll("[data-cart-count]").forEach((el) => (el.textContent = String(count))); - } - - function renderCart() { - const { count, subtotal, tax, total } = store.totals(); - els.cartSub.innerHTML = formatPrice(subtotal); - els.cartTax.innerHTML = formatPrice(tax); - els.cartTotal.innerHTML = formatPrice(total); - els.checkoutBtn.disabled = count === 0; - - const entries = store.entries(); - if (entries.length === 0) { - els.cartItems.innerHTML = '
    Uw winkelmandje is leeg.
    '; - return; - } - - const rows = []; - for (const [id, qty] of entries) { - const p = findProduct(id); - if (!p) continue; - const tile = p.image - ? `` - : escapeHtml(p.from); - rows.push(` -
    - -
    - ${escapeHtml(p.name)} - ${formatPrice(p.price)} per stuk -
    - - ${qty} - -
    -
    - ${formatPrice(p.price * qty)} -
    - `); - } - els.cartItems.innerHTML = rows.join(""); - } - - function openCart() { els.overlay.classList.add("open"); els.drawer.classList.add("open"); } - function closeCart() { els.overlay.classList.remove("open"); els.drawer.classList.remove("open"); } - function openModal() { els.modalWrap.classList.add("open"); } - function closeModal() { els.modalWrap.classList.remove("open"); } - - function showToast(message) { - els.toast.textContent = message; - els.toast.classList.add("show"); - clearTimeout(toastTimer); - toastTimer = setTimeout(() => els.toast.classList.remove("show"), 2200); - } - - function renderCheckoutForm() { - const { total } = store.totals(); - els.modalContent.innerHTML = ` -

    Afrekenen

    -

    Dit formulier verwerkt geen gegevens. Ingevoerde waarden worden niet opgeslagen of verzonden.

    -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    -
    - - -
    -
    - - -
    -
    - -

    Demonstratieformulier. Er wordt geen betaling uitgevoerd.

    -
    - `; - document.getElementById("payForm").addEventListener("submit", (event) => { - event.preventDefault(); - const form = event.currentTarget; - if (!form.checkValidity()) { form.reportValidity(); return; } - renderSuccess(); - }); - } - - function renderSuccess() { - const { count } = store.totals(); - 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.

    - -
    - `; - store.clear(); - document.getElementById("doneBtn").addEventListener("click", () => { - closeModal(); - showToast("Winkelmandje geleegd."); - }); - } - - CL.ui = { init, bindHeader, updateCartCount, renderCart, openCart, closeCart, showToast }; -})(); diff --git a/domme-converters/index.html b/domme-converters/index.html deleted file mode 100644 index c89af7a..0000000 --- a/domme-converters/index.html +++ /dev/null @@ -1,59 +0,0 @@ - - - - - - -Converterland — Adapters en signaalconverters - - - - - -
    Demonstratie- en parodiewinkel. Producten zijn fictief. Er worden geen betalingen verwerkt en geen bestellingen verzonden.
    - - - - - -
    - - - - - -
    - - - - - - -
    - - - - - - - - - - - - - diff --git a/gab/PHO00009.JPG b/gab/PHO00009.JPG deleted file mode 100644 index b048d24..0000000 Binary files a/gab/PHO00009.JPG and /dev/null differ diff --git a/gab/PHO00012.JPG b/gab/PHO00012.JPG deleted file mode 100644 index df17cda..0000000 Binary files a/gab/PHO00012.JPG and /dev/null differ diff --git a/gab/PHO00013.JPG b/gab/PHO00013.JPG deleted file mode 100644 index 5a6e040..0000000 Binary files a/gab/PHO00013.JPG and /dev/null differ diff --git a/gab/PHO00015.JPG b/gab/PHO00015.JPG deleted file mode 100644 index 7670e3b..0000000 Binary files a/gab/PHO00015.JPG and /dev/null differ diff --git a/gab/PHO00016.JPG b/gab/PHO00016.JPG deleted file mode 100644 index bc7e743..0000000 Binary files a/gab/PHO00016.JPG and /dev/null differ diff --git a/gab/PHO00017.JPG b/gab/PHO00017.JPG deleted file mode 100644 index f9c4abc..0000000 Binary files a/gab/PHO00017.JPG and /dev/null differ diff --git a/gab/PHO00018.JPG b/gab/PHO00018.JPG deleted file mode 100644 index 257b95b..0000000 Binary files a/gab/PHO00018.JPG and /dev/null differ diff --git a/gab/PHO00019.JPG b/gab/PHO00019.JPG deleted file mode 100644 index 50364e9..0000000 Binary files a/gab/PHO00019.JPG and /dev/null differ diff --git a/gab/PHO00020.JPG b/gab/PHO00020.JPG deleted file mode 100644 index 19afedf..0000000 Binary files a/gab/PHO00020.JPG and /dev/null differ diff --git a/gab/PHO00021.JPG b/gab/PHO00021.JPG deleted file mode 100644 index 7e53f18..0000000 Binary files a/gab/PHO00021.JPG and /dev/null differ diff --git a/gab/PHO00022.JPG b/gab/PHO00022.JPG deleted file mode 100644 index b3aceba..0000000 Binary files a/gab/PHO00022.JPG and /dev/null differ diff --git a/gab/PHO00023.JPG b/gab/PHO00023.JPG deleted file mode 100644 index 152f897..0000000 Binary files a/gab/PHO00023.JPG and /dev/null differ diff --git a/gab/PHO00024.JPG b/gab/PHO00024.JPG deleted file mode 100644 index 6df6dab..0000000 Binary files a/gab/PHO00024.JPG and /dev/null differ diff --git a/gab/PHO00025.JPG b/gab/PHO00025.JPG deleted file mode 100644 index 12c4eb8..0000000 Binary files a/gab/PHO00025.JPG and /dev/null differ diff --git a/gab/PHO00026.JPG b/gab/PHO00026.JPG deleted file mode 100644 index 9abb487..0000000 Binary files a/gab/PHO00026.JPG and /dev/null differ diff --git a/gab/PHO00027.JPG b/gab/PHO00027.JPG deleted file mode 100644 index 6bfa692..0000000 Binary files a/gab/PHO00027.JPG and /dev/null differ diff --git a/gab/PHO00028.JPG b/gab/PHO00028.JPG deleted file mode 100644 index 476c566..0000000 Binary files a/gab/PHO00028.JPG and /dev/null differ diff --git a/gab/PHO00029.JPG b/gab/PHO00029.JPG deleted file mode 100644 index 8872048..0000000 Binary files a/gab/PHO00029.JPG and /dev/null differ diff --git a/gab/PHO00030.JPG b/gab/PHO00030.JPG deleted file mode 100644 index 218445a..0000000 Binary files a/gab/PHO00030.JPG and /dev/null differ diff --git a/gab/PHO00031.JPG b/gab/PHO00031.JPG deleted file mode 100644 index 25db13b..0000000 Binary files a/gab/PHO00031.JPG and /dev/null differ diff --git a/gab/PHO00032.JPG b/gab/PHO00032.JPG deleted file mode 100644 index 8619dd7..0000000 Binary files a/gab/PHO00032.JPG and /dev/null differ diff --git a/gab/PHO00033.JPG b/gab/PHO00033.JPG deleted file mode 100644 index 9941426..0000000 Binary files a/gab/PHO00033.JPG and /dev/null differ diff --git a/gab/PHO00034.JPG b/gab/PHO00034.JPG deleted file mode 100644 index ae91ffd..0000000 Binary files a/gab/PHO00034.JPG and /dev/null differ diff --git a/gab/PHO00035.JPG b/gab/PHO00035.JPG deleted file mode 100644 index 9dcdc04..0000000 Binary files a/gab/PHO00035.JPG and /dev/null differ diff --git a/gab/PHO00036.JPG b/gab/PHO00036.JPG deleted file mode 100644 index 84e7b97..0000000 Binary files a/gab/PHO00036.JPG and /dev/null differ diff --git a/gab/PHO00037.JPG b/gab/PHO00037.JPG deleted file mode 100644 index 54e74cf..0000000 Binary files a/gab/PHO00037.JPG and /dev/null differ diff --git a/gab/PHO00038.JPG b/gab/PHO00038.JPG deleted file mode 100644 index 897ef1c..0000000 Binary files a/gab/PHO00038.JPG and /dev/null differ diff --git a/gab/PHO00039.JPG b/gab/PHO00039.JPG deleted file mode 100644 index 83013ad..0000000 Binary files a/gab/PHO00039.JPG and /dev/null differ diff --git a/gab/PHO00040.JPG b/gab/PHO00040.JPG deleted file mode 100644 index 741a7c7..0000000 Binary files a/gab/PHO00040.JPG and /dev/null differ diff --git a/gab/PHO00041.JPG b/gab/PHO00041.JPG deleted file mode 100644 index 215d312..0000000 Binary files a/gab/PHO00041.JPG and /dev/null differ diff --git a/gab/PHO00042.JPG b/gab/PHO00042.JPG deleted file mode 100644 index 2ae3ba5..0000000 Binary files a/gab/PHO00042.JPG and /dev/null differ diff --git a/gab/PHO00043.JPG b/gab/PHO00043.JPG deleted file mode 100644 index a1d7c81..0000000 Binary files a/gab/PHO00043.JPG and /dev/null differ diff --git a/gab/PHO00044.JPG b/gab/PHO00044.JPG deleted file mode 100644 index ee0dfdc..0000000 Binary files a/gab/PHO00044.JPG and /dev/null differ diff --git a/gab/PHO00045.JPG b/gab/PHO00045.JPG deleted file mode 100644 index 13ed4dc..0000000 Binary files a/gab/PHO00045.JPG and /dev/null differ diff --git a/gab/PHO00046.JPG b/gab/PHO00046.JPG deleted file mode 100644 index 825310e..0000000 Binary files a/gab/PHO00046.JPG and /dev/null differ diff --git a/gab/PHO00047.JPG b/gab/PHO00047.JPG deleted file mode 100644 index d18c369..0000000 Binary files a/gab/PHO00047.JPG and /dev/null differ diff --git a/gab/PHO00048.JPG b/gab/PHO00048.JPG deleted file mode 100644 index ad64545..0000000 Binary files a/gab/PHO00048.JPG and /dev/null differ diff --git a/gab/PHO00056.JPG b/gab/PHO00056.JPG deleted file mode 100644 index 0a4a7dd..0000000 Binary files a/gab/PHO00056.JPG and /dev/null differ diff --git a/gab/PHO00062.JPG b/gab/PHO00062.JPG deleted file mode 100644 index 05f907a..0000000 Binary files a/gab/PHO00062.JPG and /dev/null differ diff --git a/gab/index.php b/gab/index.php deleted file mode 100644 index 8245a7a..0000000 --- a/gab/index.php +++ /dev/null @@ -1,95 +0,0 @@ - - - - - - - -Foto Viewer - - - - - - - - - - - - - \ No newline at end of file diff --git a/gpt/index.html b/gpt/index.html deleted file mode 100644 index 3f3734f..0000000 --- a/gpt/index.html +++ /dev/null @@ -1,1700 +0,0 @@ - - - - - - NEURAL-NET TERMINAL v3.0 - - - -
    -
    - -
    - - -
    - NEURAL-NET TERMINAL v3.0 -
    - NO MODEL -
    - - OFFLINE -
    - -
    -
    - - -
    - - - - -
    - - -
    - - -
    - -
    -
    -
    -
    -
    -
    -
    - - -
    -
    -
    -
    -
    - - -
    -
    - -
    -
    -
    -
    - - -
    -
    - -
    
    -      
    -
    - -
    - - -
    -
    - $ - - 0t -
    -
    - ↑↓ history  |  Tab autocomplete  |  F2 config  |  F1 help  |  Shift+Enter multi-line - 0 / ∞ -
    -
    -
    - - -
    - - - - - - - \ No newline at end of file diff --git a/gpt/indexoud.html b/gpt/indexoud.html deleted file mode 100644 index b7f968d..0000000 --- a/gpt/indexoud.html +++ /dev/null @@ -1,671 +0,0 @@ - - - - - - NEURAL-NET TERMINAL v2.1 - - - -
    - -
    -
    - NEURAL-NET TERMINAL v2.1 -
    -
    - - OFFLINE -
    -
    -
    - -
    -
    NEURAL-NET TERMINAL v2.1 initialized
    -
    Type 'help' for available commands
    -
    Type 'config' to configure AI backend
    -
    ─────────────────────────────────────
    -
    - -
    - $ - -
    - - -
    - - -
    -

    [ AI BACKEND CONFIGURATION ]

    - -
    - - -
    - -
    - - -
    - -
    - - -
    - -
    - - -
    - -
    - - - -
    -
    - - - - diff --git a/gpt/script.py b/gpt/script.py deleted file mode 100644 index b56c3bb..0000000 --- a/gpt/script.py +++ /dev/null @@ -1,3 +0,0 @@ -print("ben gpt") -kaas = input("wat is je vraag: ") -print("Hallo! Welkom bij ChatGPT, hoe kan ik je helpen?") \ No newline at end of file diff --git a/haans/index.html b/haans/index.html deleted file mode 100644 index f1f1d0d..0000000 --- a/haans/index.html +++ /dev/null @@ -1,67 +0,0 @@ - - - - -Offline - - - -
    -

    Pech.

    -

    De site is offline.

    -
    -

    Er wordt momenteel aan gewerkt.

    - - -
    - - - - - diff --git a/haans/pindex.html b/haans/pindex.html deleted file mode 100644 index 8b105e8..0000000 --- a/haans/pindex.html +++ /dev/null @@ -1,2146 +0,0 @@ - - - - - - - - - -LAUPAN LUCHCD PEILTJUS - - - - - -
    -
    🎯
    - -
    -
    — HIGH SCORE —
    -
    00000
    -
    -
    [ DRUK OP SPATIE / KNOP OM TE STARTEN ]
    -
    - - -
    -
    - - - - - - - - - - - - - - - - - - - - - - -
    -
    🎯
    -
    - LAUPAN LUCHCD
    - PEILTJUS -
    -
    - [ TIK OM TE STARTEN ] -
    -
    - VOLLEDIG SCHERM WORDT GEACTIVEERD -
    -
    - - - - diff --git a/index.html.old b/index.html.old deleted file mode 100644 index e2f02bb..0000000 --- a/index.html.old +++ /dev/null @@ -1,1424 +0,0 @@ - - - - - - Terminal Chat - - - - - - - - - -
    - - -
    - - - - - -
    -
    v1.0 — Open Source
    -

    Chat from your Terminal

    -

    A simple yet powerful terminal-based chat application written in Python. Realtime messaging via TCP sockets, straight from your command line.

    - - - -
    -
    -
    - bash - -
    -
    - $ - curl -s https://ben.de-roo.org/install/script.sh | bash - -
    -
    -
    - or -
    -
    -
    - - - Download ZIP from git.de-roo.org - - -
    -
    -
    - - -
    -
    -
    -
    -
    terminal-chat — python3
    -
    -
    -
    - - -
    -
    - $ python3 client.py -
    -
    -
    -
    - Terminal Chat -
    -
    -
    -
    - ========================================
    -    Welcome to The Terminal Chat
    - ======================================== -
    -
    -
    - 1. Login
    - 2. New account -
    -
    -
    > 1 -
    -
    -
    Username: max -
    -
    - Password: ******** -
    -
    -
    --- Available chats: ---

    - 1. General
    - 2. Create new chat -
    -
    -
    > 1 -
    -
    - - - - -
    -
    -
    - - -
    -
    - -

    Everything you need

    -

    Terminal Chat is built with a focus on simplicity and functionality. No unnecessary complexity, just chatting from your terminal.

    -
    -
    -
    - -
    -

    Login & Signup

    -

    Full authentication system with username and password. Default test account included.

    -
    -
    -
    - -
    -

    Terminal Interface

    -

    Colorful terminal interface with intuitive navigation. Works on any terminal that supports ANSI colors.

    -
    -
    -
    - -
    -

    Realtime Messaging

    -

    Messages are delivered instantly via TCP sockets. No delay, no polling. True realtime communication.

    -
    -
    -
    - -
    -

    Multiple Chats

    -

    Create multiple chat channels and easily switch between them. Each channel has its own message history.

    -
    -
    -
    - -
    -

    User Management

    -

    Add users to chats with a simple command. Manage who is in which channel.

    -
    -
    -
    - -
    -

    Open Source

    -

    Fully open source and free to use. View the code, contribute, or fork the project for your own use.

    -
    -
    -
    -
    - -
    -
    - -

    Platform support

    -

    Built primarily for Linux terminals, with growing support for other platforms.

    - -
    - - -
    -
    -
    -
    - -
    -

    Linux

    -
    - FULLY SUPPORTED -
    -

    First-class support. All features work out of the box including ANSI colors, readline, and autofill.

    -
    -
    - - Full ANSI color support -
    -
    - - Readline & autofill -
    -
    - - Tested & verified -
    -
    -
    - - -
    -
    -
    -
    - -
    -

    Windows

    -
    - PARTIAL -
    -

    Works with modifications. The readline module and autofill function need to be adjusted for Windows compatibility.

    -
    -
    - - Core chat functionality -
    -
    - - Readline needs pyreadline3 -
    -
    - - Autofill needs adjustment -
    -
    -
    - - -
    -
    -
    -
    - -
    -

    macOS

    -
    - UNTESTED -
    -

    Should work since macOS has a Unix-based terminal, but it hasn't been tested yet. Community feedback is welcome!

    -
    -
    - - Likely compatible (Unix-based) -
    -
    - - Needs community testing -
    -
    - - Open an issue to report results -
    -
    -
    - -
    -
    -
    - - - -
    -
    - -

    Get started quickly

    -

    Everything you need to install, configure, and use Terminal Chat.

    -
    - - -
    -
    - -

    Installation (for Linux)

    -
    -
    -

    Install Terminal Chat with a single command. The script downloads and configures everything automatically.

    - Windows installation and setup -

    -
    - # Automatic installation
    - $ curl -s https://ben.de-roo.org/install/script.sh | bash

    - # Or manually
    - $ git clone https://git.de-roo.org/ben/pychat.git
    - $ cd terminal-chat
    - $ pip install -r requirements.txt -
    -
    -
    - - -
    -
    - -

    Getting Started

    -
    -
    -

    Start the server first, then the client. Log in with the default test account or create a new one.

    -
    - # Start the server
    - $ python3 server.py

    - # Start the client (another terminal window)
    - $ python3 client.py

    - # Default test account
    - user: test  pass: test -
    -
    -
    - - -
    -
    - -

    Available Commands

    -
    -
    -

    Use these commands while chatting to perform additional actions.

    - - - - - - - - - - - - - - - - - - - - - -
    CommandDescription
    /add <username>Add a user to the current chat
    /breakLeave the current chat and return to chat selection
    /quitClose the program completely and disconnect
    obviously more commands and features comming on the way!
    -
    -
    -
    -
    -
    - - -
    -
    - -

    How it works

    -

    Terminal Chat uses a classic client-server model with TCP sockets for reliable, bidirectional communication.

    -
    -
    - Client - Python script that connects to the server. Handles input/output and displays messages with ANSI colors. -
    -
    ↓ TCP Socket ↓
    -
    - Server - Runs centrally and manages all connections, authentication, chat channels, and message routing. -
    -
    ↓ Threading ↓
    -
    - Storage - User and chat data are stored server-side. Each client gets its own thread for non-blocking I/O. -
    -
    -

    -
    - # Simplified overview

    - client.py Connects to server via socket
    -             Login / Signup prompt
    -             Chat selection menu
    -             Send & receive messages

    - server.py Listens on TCP port
    -             New thread per client
    -             Authentication handling
    -             Message broadcast to channel -
    -
    -
    - - -
    -
    - -

    Make it yours

    -

    Terminal Chat is open source and built to be extended. Whether you want to add new features, build mods, or fix bugs — you're welcome to join.

    - -
    - -
    -
    01
    -
    -
    - -
    -

    Build mods

    -
    -

    Add new commands, custom themes, message encryption, file sharing, or anything else you can think of. The codebase is simple and easy to extend.

    -
    - -
    -
    02
    -
    -
    - -
    -

    Improve the core

    -
    -

    Help fix bugs, improve Windows/macOS compatibility, optimize performance, or refactor the codebase. Every PR matters.

    -
    - -
    -
    03
    -
    -
    - -
    -

    Share ideas

    -
    -

    Open an issue to suggest features, report bugs, or discuss improvements. Even if you don't code, your input helps shape the project.

    -
    - -
    -
    04
    -
    -
    - -
    -

    Fork & experiment

    -
    -

    Fork the repo and go wild. Build your own version, add a GUI, make it work with Discord bots — the code is yours to play with.

    -
    - -
    - - -
    -

    Ready to contribute? Start here:

    -
    - # Clone and start hacking
    - $ git clone https://git.de-roo.org/ben/pychat.git
    - $ cd pychat
    - $ python3 server.py & python3 client.py -
    -

    No complex setup. No build tools. Just Python.

    -
    -
    -
    - - -
    -
    - -

    Updates & News

    -

    Latest releases, patches, and project news.

    -
    -
    Loading updates...
    -
    -
    -
    - - - - - - - \ No newline at end of file diff --git a/install/NEXT_447.zip b/install/NEXT_447.zip deleted file mode 100644 index 2672b52..0000000 Binary files a/install/NEXT_447.zip and /dev/null differ diff --git a/install/Screenshot_20260220_134634.png b/install/Screenshot_20260220_134634.png deleted file mode 100644 index 4eb4af2..0000000 Binary files a/install/Screenshot_20260220_134634.png and /dev/null differ diff --git a/install/Windows/index.html b/install/Windows/index.html deleted file mode 100644 index b382d1e..0000000 --- a/install/Windows/index.html +++ /dev/null @@ -1,62 +0,0 @@ - - - - - - GET OUT - - - -

    Windows? 🥀🥀🥀

    -

    windows in the big '26

    -

    enjoy your microslop ahh operating system bruh 😭😭

    - Terminal Chat git - Download .zip -

    succes man (you will need it)

    - - -

    wat ben je aan het doen met je leven?.

    - - \ No newline at end of file diff --git a/install/index.html b/install/index.html deleted file mode 100644 index 0415a9e..0000000 --- a/install/index.html +++ /dev/null @@ -1,1424 +0,0 @@ - - - - - - Terminal Chat - Python TCP Chat Application - - - - - - - - - - -
    - - -
    - - - - - -
    -
    v1.0 — Open Source
    -

    Chat from your Terminal

    -

    A simple yet powerful terminal-based chat application written in Python. Realtime messaging via TCP sockets, straight from your command line.

    - - - -
    -
    -
    - bash - -
    -
    - $ - curl -s https://ben.de-roo.org/install/script.sh | bash -
    -
    -
    - or -
    -
    -
    - - - Download ZIP from git.de-roo.org - - -
    -
    -
    - - -
    -
    -
    -
    -
    terminal-chat — python3
    -
    -
    -
    - - -
    -
    - $ python3 client.py -
    -
    -
    -
    - Terminal Chat -
    -
    -
    -
    - ========================================
    -    Welcome to The Terminal Chat
    - ======================================== -
    -
    -
    - 1. Login
    - 2. New account -
    -
    -
    > 1 -
    -
    -
    Username: max -
    -
    - Password: ******** -
    -
    -
    --- Available chats: ---

    - 1. General
    - 2. Create new chat -
    -
    -
    > 1 -
    -
    - - - - -
    -
    -
    - - -
    -
    - -

    Everything you need

    -

    Terminal Chat is built with a focus on simplicity and functionality. No unnecessary complexity, just chatting from your terminal.

    -
    -
    -
    - -
    -

    Login & Signup

    -

    Full authentication system with username and password. Default test account included.

    -
    -
    -
    - -
    -

    Terminal Interface

    -

    Colorful terminal interface with intuitive navigation. Works on any terminal that supports ANSI colors.

    -
    -
    -
    - -
    -

    Realtime Messaging

    -

    Messages are delivered instantly via TCP sockets. No delay, no polling. True realtime communication.

    -
    -
    -
    - -
    -

    Multiple Chats

    -

    Create multiple chat channels and easily switch between them. Each channel has its own message history.

    -
    -
    -
    - -
    -

    User Management

    -

    Add users to chats with a simple command. Manage who is in which channel.

    -
    -
    -
    - -
    -

    Open Source

    -

    Fully open source and free to use. View the code, contribute, or fork the project for your own use.

    -
    -
    -
    -
    - -
    -
    - -

    Platform support

    -

    Built primarily for Linux terminals, with growing support for other platforms.

    - -
    - - -
    -
    -
    -
    - -
    -

    Linux

    -
    - FULLY SUPPORTED -
    -

    First-class support. All features work out of the box including ANSI colors, readline, and autofill.

    -
    -
    - - Full ANSI color support -
    -
    - - Readline & autofill -
    -
    - - Tested & verified -
    -
    -
    - - -
    -
    -
    -
    - -
    -

    Windows

    -
    - PARTIAL -
    -

    Works with modifications. The readline module and autofill function need to be adjusted for Windows compatibility.

    -
    -
    - - Core chat functionality -
    -
    - - Readline needs pyreadline3 -
    -
    - - Autofill needs adjustment -
    -
    -
    - - -
    -
    -
    -
    - -
    -

    macOS

    -
    - UNTESTED -
    -

    Should work since macOS has a Unix-based terminal, but it hasn't been tested yet. Community feedback is welcome!

    -
    -
    - - Likely compatible (Unix-based) -
    -
    - - Needs community testing -
    -
    - - Open an issue to report results -
    -
    -
    - -
    -
    -
    - - - -
    -
    - -

    Get started quickly

    -

    Everything you need to install, configure, and use Terminal Chat.

    -
    - - -
    -
    - -

    Installation (for Linux)

    -
    -
    -

    Install Terminal Chat with a single command. The script downloads and configures everything automatically.

    - Windows installation and setup -

    -
    - # Automatic installation
    - $ curl -s https://ben.de-roo.org/install/script.sh | bash

    - # Or manually
    - $ git clone https://git.de-roo.org/ben/pychat.git
    - $ cd terminal-chat
    - $ pip install -r requirements.txt -
    -
    -
    - - -
    -
    - -

    Getting Started

    -
    -
    -

    Start the server first, then the client. Log in with the default test account or create a new one.

    -
    - # Start the server
    - $ python3 server.py

    - # Start the client (another terminal window)
    - $ python3 client.py

    - # Default test account
    - user: test  pass: test -
    -
    -
    - - -
    -
    - -

    Available Commands

    -
    -
    -

    Use these commands while chatting to perform additional actions.

    - - - - - - - - - - - - - - - - - - - - - -
    CommandDescription
    /add <username>Add a user to the current chat
    /breakLeave the current chat and return to chat selection
    /quitClose the program completely and disconnect
    obviously more commands and features comming on the way!
    -
    -
    -
    -
    -
    - - -
    -
    - -

    How it works

    -

    Terminal Chat uses a classic client-server model with TCP sockets for reliable, bidirectional communication.

    -
    -
    - Client - Python script that connects to the server. Handles input/output and displays messages with ANSI colors. -
    -
    ↓ TCP Socket ↓
    -
    - Server - Runs centrally and manages all connections, authentication, chat channels, and message routing. -
    -
    ↓ Threading ↓
    -
    - Storage - User and chat data are stored server-side. Each client gets its own thread for non-blocking I/O. -
    -
    -

    -
    - # Simplified overview

    - client.py Connects to server via socket
    -             Login / Signup prompt
    -             Chat selection menu
    -             Send & receive messages

    - server.py Listens on TCP port
    -             New thread per client
    -             Authentication handling
    -             Message broadcast to channel -
    -
    -
    - - -
    -
    - -

    Make it yours

    -

    Terminal Chat is open source and built to be extended. Whether you want to add new features, build mods, or fix bugs — you're welcome to join.

    - -
    - -
    -
    01
    -
    -
    - -
    -

    Build mods

    -
    -

    Add new commands, custom themes, message encryption, file sharing, or anything else you can think of. The codebase is simple and easy to extend.

    -
    - -
    -
    02
    -
    -
    - -
    -

    Improve the core

    -
    -

    Help fix bugs, improve Windows/macOS compatibility, optimize performance, or refactor the codebase. Every PR matters.

    -
    - -
    -
    03
    -
    -
    - -
    -

    Share ideas

    -
    -

    Open an issue to suggest features, report bugs, or discuss improvements. Even if you don't code, your input helps shape the project.

    -
    - -
    -
    04
    -
    -
    - -
    -

    Fork & experiment

    -
    -

    Fork the repo and go wild. Build your own version, add a GUI, make it work with Discord bots — the code is yours to play with.

    -
    - -
    - - -
    -

    Ready to contribute? Start here:

    -
    - # Clone and start hacking
    - $ git clone https://git.de-roo.org/ben/pychat.git
    - $ cd pychat
    - $ python3 server.py & python3 client.py -
    -

    No complex setup. No build tools. Just Python.

    -
    -
    -
    - - -
    -
    - -

    Updates & News

    -

    Latest releases, patches, and project news.

    -
    -
    Loading updates...
    -
    -
    -
    - - - - - - - diff --git a/install/script.sh b/install/script.sh deleted file mode 100644 index 6ddc95b..0000000 --- a/install/script.sh +++ /dev/null @@ -1,40 +0,0 @@ -#!/bin/bash - -REPO_DIR="install" -INSTALL_DIR="pychat" -BASE_URL="https://git.de-roo.org/ben/pychat/raw/branch/main" - -echo "Thanks for using PyChat!" - -# python check -if ! command -v python3 &> /dev/null; then - echo "Error: python3 is not installed." - exit 1 -fi - -# downloader check -if command -v curl &> /dev/null; then - DL="curl -L -o" -elif command -v wget &> /dev/null; then - DL="wget -O" -else - echo "Error: curl or wget required." - exit 1 -fi - -mkdir -p "$INSTALL_DIR" -cd "$INSTALL_DIR" || exit 1 - -# bestanden downloaden -$DL server.py "$BASE_URL/server.py" -$DL client.py "$BASE_URL/client.py" -$DL clientBeta.py "$BASE_URL/clientBeta.py" -$DL clientWin.py "$BASE_URL/clientWin.py" -$DL passwd.json "$BASE_URL/passwd.json" - -echo -echo "Don't forget to change IP/port in client.py." -echo "Installation complete." -echo "Start server: python3 server.py" -echo "Start client: python3 client.py" -echo "Start beta: python3 clientBeta.py" \ No newline at end of file diff --git a/install/updates.json b/install/updates.json deleted file mode 100644 index 4930ce5..0000000 --- a/install/updates.json +++ /dev/null @@ -1,17 +0,0 @@ -[ - { - "type": "release", - "version": "v1.0.0", - "date": "2026-02-22", - "title": "First release of the Terminal PyChat", - "description": "First release of the basic version of the Terminal PyChat" - - }, - { - "type": "release", - "version": "v1.0.1", - "date": "2026-03-01", - "title": "Improved Chat privacy", - "description": "Added a server-side filter so clients only see chats they are members of" - } -] \ No newline at end of file diff --git a/rekenmachine/index.html b/rekenmachine/index.html deleted file mode 100644 index 66a51de..0000000 --- a/rekenmachine/index.html +++ /dev/null @@ -1,179 +0,0 @@ - - - - - -Calculator - - - -
    -
    -
    -
    0
    -
    -
    - - - - - - - - - - - - - - - - - - - - - -
    -
    - -
    -
    -
    :(
    -

    Your PC ran into a problem and needs to restart.

    -

    We're just collecting some error info, and then we'll restart for you.

    -
    0% complete
    -
    -

    If you'd like to know more, you can search online later for this error: pech.

    -

    Stop code: 0x0000007B (0x00000034, 0xC0000034, 0x00000000, 0x00000000)

    -

    What failed: your calculates were so ass it crashed

    -
    - -
    -
    - - - - diff --git a/style.css b/style.css index ae9b186..8a42936 100644 --- a/style.css +++ b/style.css @@ -1,408 +1,378 @@ +/* ============================================================ + Design tokens + ------------------------------------------------------------ + Palette: warm paper background, ink-dark sidebar "spine", + deep pine accent (not the generic AI-blue). + Type: Source Serif 4 for headings (editorial, planner-like), + Inter for body copy, IBM Plex Mono for time/data. + ============================================================ */ + +@import url('https://fonts.googleapis.com/css2?family=Source+Serif+4:opsz,wght@8..60,500;8..60,600&family=Inter:wght@400;500;600&family=IBM+Plex+Mono:wght@400;500&display=swap'); + :root { + /* Surfaces */ + --bg: #f7f6f3; + --panel: #ffffff; + --sidebar: #15171c; + --sidebar-hover: #1f222a; - --bg: #f4f5f7; - --panel: white; - --sidebar: #ffffff; - --text: #111; - --muted: #666; - --border: #ddd; - --accent: #2563eb; + /* Text */ + --text: #16181d; + --text-inverse: #eceae4; + --muted: #74716a; + --muted-inverse: #8b8e97; + /* Structure */ + --border: #e5e2da; + --border-strong: #d3cfc3; + + /* Accent */ + --accent: #1f5c52; + --accent-hover: #164941; + --accent-tint: #e8efec; + + /* Status (used as tint + left-bar, not solid fills) */ + --status-confirmed: #1f5c52; + --status-confirmed-tint: #e8efec; + --status-busy: #a15c14; + --status-busy-tint: #f6ede1; + --status-private: #4b4d57; + --status-private-tint: #eceae7; + + --danger: #ad2b38; + + /* Scale */ + --space-1: 4px; + --space-2: 8px; + --space-3: 12px; + --space-4: 16px; + --space-5: 24px; + --space-6: 32px; + --space-7: 40px; + + --radius-sm: 6px; + --radius-md: 8px; + --radius-lg: 12px; + + --shadow-sm: 0 1px 2px rgba(21, 23, 28, 0.06); + --shadow-md: 0 4px 16px rgba(21, 23, 28, 0.08); + + --ease: cubic-bezier(0.4, 0, 0.2, 1); + + --font-display: "Source Serif 4", Georgia, serif; + --font-body: Inter, system-ui, sans-serif; + --font-mono: "IBM Plex Mono", ui-monospace, monospace; } - - .dark { + --bg: #0e0f12; + --panel: #17191d; + --sidebar: #0a0b0d; + --sidebar-hover: #161821; - --bg: #080808; - --panel: #151515; - --sidebar: #101010; - --text: #eee; - --muted: #aaa; - --border: #333; - --accent: #38bdf8; + --text: #eceae4; + --text-inverse: #16181d; + --muted: #91908a; + --muted-inverse: #6b6d75; + --border: #2a2c31; + --border-strong: #3a3d43; + + --accent: #4fa393; + --accent-hover: #63b6a5; + --accent-tint: #16221f; + + --status-confirmed: #4fa393; + --status-confirmed-tint: #16221f; + --status-busy: #d1934f; + --status-busy-tint: #26200f; + --status-private: #9294a0; + --status-private-tint: #1c1d21; + + --danger: #e2707c; + + --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.3); + --shadow-md: 0 8px 24px rgba(0, 0, 0, 0.4); } - - * { box-sizing: border-box; } - - body { - margin: 0; - background: var(--bg); - color: var(--text); - - font-family: - Inter, - system-ui, - sans-serif; - - transition: 0.2s; - + font-family: var(--font-body); + font-size: 14px; + line-height: 1.5; + -webkit-font-smoothing: antialiased; + transition: background 0.2s var(--ease), color 0.2s var(--ease); } - +/* ============================================================ + Layout + ============================================================ */ .layout { - display: grid; - - grid-template-columns: 260px 1fr; - + grid-template-columns: 248px 1fr; height: 100vh; - - display: none; - } - .sidebar { - background: var(--sidebar); - - border-right: 1px solid var(--border); - - padding: 25px; - + color: var(--text-inverse); + border-right: 1px solid rgba(255, 255, 255, 0.06); + padding: var(--space-5) var(--space-4); display: flex; - flex-direction: column; - } - - .logo { - display: flex; - align-items: center; - - gap: 10px; - - margin-bottom: 40px; - + gap: var(--space-2); + margin-bottom: var(--space-6); + padding: 0 var(--space-2); } - - .logo h2 { - margin: 0; - + font-family: var(--font-display); + font-weight: 600; + font-size: 19px; + letter-spacing: -0.01em; } - - nav { - display: flex; - flex-direction: column; - - gap: 8px; - + gap: 2px; } - - nav a { - cursor: pointer; - - padding: 12px 15px; - - border-radius: 10px; - - color: var(--muted); - - transition: 0.2s; - + padding: 10px 14px; + border-radius: var(--radius-sm); + color: var(--muted-inverse); + font-size: 13.5px; + font-weight: 500; + transition: background 0.15s var(--ease), color 0.15s var(--ease); } +nav a:hover { + background: var(--sidebar-hover); + color: var(--text-inverse); +} - -nav a:hover, nav .active { - background: var(--accent); - - color: white; - + color: #ffffff; } - - .sidebar-bottom { - margin-top: auto; - + padding-top: var(--space-4); + border-top: 1px solid rgba(255, 255, 255, 0.08); } - - button { - width: 100%; - - padding: 12px; - - border-radius: 10px; - + padding: 10px 14px; + border-radius: var(--radius-sm); border: 1px solid var(--border); - background: var(--panel); - color: var(--text); - + font-family: var(--font-body); + font-size: 13.5px; + font-weight: 500; cursor: pointer; - - margin-top: 8px; - + margin-top: var(--space-2); + transition: border-color 0.15s var(--ease), background 0.15s var(--ease); } +button:hover { + border-color: var(--border-strong); + background: var(--bg); +} +button:focus-visible, +a:focus-visible, +input:focus-visible { + outline: 2px solid var(--accent); + outline-offset: 2px; +} + +.sidebar button { + background: transparent; + border-color: rgba(255, 255, 255, 0.12); + color: var(--text-inverse); +} + +.sidebar button:hover { + background: var(--sidebar-hover); + border-color: rgba(255, 255, 255, 0.2); +} /* Content */ .content { - - padding: 40px; - + padding: var(--space-6) var(--space-7); overflow-y: auto; - } - - h1 { - margin-top: 0; - + font-family: var(--font-display); + font-weight: 600; + font-size: 26px; + letter-spacing: -0.01em; } - - .card { - background: var(--panel); - border: 1px solid var(--border); - - border-radius: 16px; - - padding: 25px; - - margin-top: 20px; - - box-shadow: - 0 10px 30px rgba(0,0,0,0.05); - + border-radius: var(--radius-lg); + padding: var(--space-5); + margin-top: var(--space-4); + box-shadow: var(--shadow-sm); } - - /* forms */ input { - width: 100%; - - padding: 12px; - - border-radius: 10px; - + padding: 10px 12px; + border-radius: var(--radius-sm); border: 1px solid var(--border); - background: var(--bg); - color: var(--text); - - margin-top: 8px; - + font-family: var(--font-body); + font-size: 13.5px; + margin-top: var(--space-2); + transition: border-color 0.15s var(--ease); } - +input:focus { + border-color: var(--accent); +} form { - display: flex; - flex-direction: column; - } - - .muted { - color: var(--muted); - } - - .error { - - color: #e11d48; - + color: var(--danger); } - - /* list items */ .list-item { - - padding: 10px 15px; - + padding: 10px 14px; border: 1px solid var(--border); - - border-radius: 10px; - - margin-top: 10px; - + border-radius: var(--radius-md); + margin-top: var(--space-2); + transition: border-color 0.15s var(--ease); } - +.list-item:hover { + border-color: var(--border-strong); +} /* event actions */ .event-actions { - display: flex; - - gap: 10px; - - margin-top: 15px; - + gap: var(--space-2); + margin-top: var(--space-4); } - .event-actions button { - margin-top: 0; - } - - /* calendar */ .calendar-header { - display: flex; - align-items: center; - justify-content: space-between; - } - .calendar-header h3 { - margin: 0; - + font-family: var(--font-display); + font-weight: 600; + font-size: 17px; text-transform: capitalize; - } - .calendar-nav-btn { - width: auto; - - padding: 8px 14px; - + padding: 6px 12px; margin-top: 0; - } - .calendar-legend { - display: flex; - - gap: 18px; - - margin-top: 14px; - + gap: var(--space-5); + margin-top: var(--space-4); font-size: 12px; - color: var(--muted); - } - .legend-item { - display: flex; - align-items: center; - gap: 6px; - } - .legend-dot { - - width: 10px; - - height: 10px; - - border-radius: 50%; - + width: 8px; + height: 8px; + border-radius: 2px; display: inline-block; - } - /* week grid */ .calendar-week { display: flex; - margin-top: 16px; + margin-top: var(--space-4); overflow-x: auto; } - .calendar-gutter { - width: 55px; + width: 52px; flex-shrink: 0; padding-top: 0px; } - .calendar-hour-label { - height: 48px; /* 2 uur = 48px */ - font-size: 11px; + height: 48px; + font-family: var(--font-mono); + font-size: 10.5px; color: var(--muted); text-align: right; - padding-right: 8px; + padding-right: var(--space-2); display: flex; justify-content: flex-end; align-items: flex-start; transform: translateY(-6px); } - .calendar-days { display: flex; flex: 1; min-width: 700px; } - .calendar-day-column { flex: 1; display: flex; @@ -410,138 +380,110 @@ form { border-left: 1px solid var(--border); } - .calendar-day-header { height: 32px; text-align: center; padding: 6px 0; - font-size: 12px; + font-family: var(--font-mono); + font-size: 11px; + letter-spacing: 0.02em; + text-transform: uppercase; color: var(--muted); } - .calendar-day-track { position: relative; - - height: 576px; /* 12 uur zichtbaar × 48px */ - - background-image: - repeating-linear-gradient( - to bottom, - var(--border) 0, - var(--border) 1px, - transparent 1px, - transparent 48px - ); + height: 576px; /* 12 uur zichtbaar x 48px */ + background-image: repeating-linear-gradient( + to bottom, + var(--border) 0, + var(--border) 1px, + transparent 1px, + transparent 48px + ); } - .calendar-event { position: absolute; - left: 4px; right: 4px; - - border-radius: 6px; - - padding: 3px 6px; - + border-radius: var(--radius-sm); + padding: 4px 8px 4px 9px; font-size: 11px; - - color: white; - + color: var(--text); cursor: pointer; - overflow: hidden; - - box-shadow: - 0 2px 6px rgba(0,0,0,0.25); - - opacity: 0.92; - - border: 1px solid rgba(255,255,255,0.4); + border-left: 3px solid transparent; + background: var(--panel); + box-shadow: var(--shadow-sm); + transition: box-shadow 0.15s var(--ease); } +.calendar-event:hover { + box-shadow: var(--shadow-md); +} .calendar-event-time { display: block; - font-size: 10px; - opacity: 0.85; + font-family: var(--font-mono); + font-size: 9.5px; + color: var(--muted); } - .calendar-event-title { display: block; font-weight: 600; - white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .event-confirmed { - - background: #16a34a; - + background: var(--status-confirmed-tint); + border-left-color: var(--status-confirmed); } - .event-busy { - - background: #f97316; - + background: var(--status-busy-tint); + border-left-color: var(--status-busy); } - .event-private { - - background: #6b7280; - + background: var(--status-private-tint); + border-left-color: var(--status-private); } - +.legend-dot.event-confirmed, +.legend-dot.event-busy, +.legend-dot.event-private { + border-radius: 2px; + border-left: none; +} /* login screen */ .login-screen { - height: 100vh; - width: 100%; - display: flex; - align-items: center; - justify-content: center; - + background: var(--bg); } - - .login-box { - background: var(--panel); - border: 1px solid var(--border); - - border-radius: 16px; - - padding: 40px; - + border-radius: var(--radius-lg); + padding: var(--space-6); width: 320px; - - box-shadow: - 0 10px 30px rgba(0,0,0,0.05); - + box-shadow: var(--shadow-md); } - - .login-box h2 { - margin-top: 0; - + margin-bottom: var(--space-5); + font-family: var(--font-display); + font-weight: 600; text-align: center; - -} \ No newline at end of file +} diff --git a/updates.json b/updates.json deleted file mode 100644 index e102d73..0000000 --- a/updates.json +++ /dev/null @@ -1,17 +0,0 @@ -[ - { - "type": "release", - "version": "v1.0.0", - "date": "2026-02-22", - "title": "First release of the Terminal PyChat", - "description": "First release of the basic version of the Terminal PyChat" - - }, - { - "type": "release", - "version": "v1.0.1", - "date": "2026-03-01", - "title": "chat privacy", - "description": "Added a server-side filter so clients only see chats they are members of" - } -] \ No newline at end of file