Google Apps Script
Exception: Malformed HTML content: <!DOCTYPE html> <html lang="de"> <head> <base target="_top"> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>EG Webstore</title> <style> :root { --bg: #0f0c13; --card: #1a1625; --panel: #13101b; --line: #332d41; --primary: #9d4edd; --accent: #3c096c; --text: #e0e0e0; --muted: #9a91aa; --danger: #9b2226; --success: #2d6a4f; --warning: #ffcc00; --shadow: 0 10px 30px rgba(0, 0, 0, 0.5); } * { box-sizing: border-box; } body { margin: 0; min-height: 100vh; background: var(--bg); color: var(--text); font-family: "Segoe UI", system-ui, -apple-system, BlinkMacSystemFont, sans-serif; padding: 12px; } .app { width: min(1100px, 100%); margin: 0 auto; } .card { background: var(--card); border: 1px solid var(--line); border-radius: 8px; box-shadow: var(--shadow); padding: 20px; } #authArea { max-width: 440px; margin: 8vh auto 0; } #dashboard { display: none; } h1 { color: var(--primary); font-size: 24px; margin: 0 0 6px; text-align: center; } h2 { color: var(--primary); font-size: 13px; letter-spacing: 0; margin: 18px 0 8px; text-transform: uppercase; } h3 { margin: 0; font-size: 16px; line-height: 1.25; } p { margin: 0; } input, select, textarea { width: 100%; margin: 8px 0; padding: 12px; border: 1px solid var(--line); border-radius: 8px; outline: none; background: var(--bg); color: var(--text); font: inherit; } textarea { min-height: 96px; resize: vertical; } input:focus, select:focus, textarea:focus { border-color: var(--primary); box-shadow: 0 0 0 3px rgba(157, 78, 221, 0.18); } button { min-height: 40px; padding: 9px 12px; border: 0; border-radius: 8px; cursor: pointer; font: inherit; font-weight: 700; transition: opacity 160ms ease, transform 160ms ease; } button:hover:not(:disabled) { transform: translateY(-1px); } button:disabled { cursor: progress; opacity: 0.55; } .primary { background: var(--primary); color: white; } .muted { background: var(--line); color: var(--text); } .danger { background: var(--danger); color: white; } .success { background: var(--success); color: white; } .button-row { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 10px; } .button-row button { flex: 1 1 160px; } .subtle, .meta { color: var(--muted); font-size: 12px; line-height: 1.45; white-space: pre-wrap; } #message, #authMessage { min-height: 18px; color: var(--muted); font-size: 12px; text-align: right; white-space: pre-wrap; } #authMessage { color: #ff9ca7; text-align: center; } #message.error { color: #ff9ca7; } #message.info { color: var(--primary); } #message.success { color: #74c69d; } .topbar { display: flex; justify-content: space-between; gap: 12px; align-items: flex-start; margin-bottom: 16px; } .top-actions { display: flex; gap: 8px; justify-content: flex-end; flex-wrap: wrap; } .layout { display: grid; grid-template-columns: minmax(0, 1fr) minmax(300px, 360px); gap: 14px; align-items: start; } .toolbar { display: grid; grid-template-columns: minmax(0, 1fr) minmax(150px, 220px) minmax(150px, 220px); gap: 8px; margin: 10px 0; } .toolbar input, .toolbar select { margin: 0; } .grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 10px; } .product, .cart, .panel { background: var(--panel); border: 1px solid var(--line); border-radius: 8px; padding: 12px; } .product { position: relative; display: grid; gap: 8px; min-width: 0; } .product img, .placeholder { width: 100%; aspect-ratio: 4 / 3; object-fit: cover; border-radius: 8px; background: #2a2635; display: block; } .placeholder { display: grid; place-items: center; color: var(--muted); font-size: 12px; } .badge { position: absolute; top: 18px; left: 18px; border-radius: 4px; padding: 3px 7px; background: var(--primary); color: white; font-size: 10px; font-weight: 800; text-transform: uppercase; } .price { color: var(--primary); font-size: 18px; font-weight: 800; } .request-status { color: #74c69d; font-weight: 700; font-size: 13px; } .cart-item { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 8px; align-items: center; padding: 10px 0; border-bottom: 1px solid var(--line); } .cart-item:last-child { border-bottom: 0; } .cart-controls { display: flex; gap: 6px; align-items: center; } .cart-controls button { min-height: 32px; padding: 5px 9px; } .summary-line { display: flex; justify-content: space-between; gap: 12px; margin-top: 8px; } .summary-line.total { color: var(--primary); font-size: 18px; font-weight: 800; border-top: 1px solid var(--line); padding-top: 10px; margin-top: 10px; } .empty { margin: 0; padding: 14px; text-align: center; color: var(--muted); } .closed { display: none; margin: 12px 0; padding: 20px; border: 1px solid var(--primary); border-radius: 8px; background: rgba(60, 9, 108, 0.32); text-align: center; } .closed.active { display: block; } .webhook-box { display: none; margin-top: 10px; padding: 12px; border-radius: 8px; border: 1px solid var(--line); background: var(--panel); color: var(--muted); font-size: 12px; line-height: 1.45; white-space: pre-wrap; } .webhook-box.success { display: block; border-color: var(--success); color: #74c69d; } .webhook-box.error { display: block; border-color: var(--danger); color: #ff9ca7; } .webhook-box.info { display: block; border-color: var(--primary); color: var(--primary); } #plotIdWrapper, #giftFields { display: none; } .gift-toggle { display: flex; align-items: center; gap: 8px; color: var(--muted); font-size: 13px; margin: 8px 0; } .gift-toggle input { width: auto; margin: 0; } @media (max-width: 840px) { .layout { grid-template-columns: 1fr; } .toolbar { grid-template-columns: 1fr; } .topbar { display: block; } .top-actions { justify-content: flex-start; margin-top: 10px; } #message { text-align: left; margin-top: 8px; } } </style> </head> <body> <main class="app"> <section id="authArea" class="card"> <h1>EG Webstore</h1> <p class="subtle" style="text-align:center;">Login mit deinem normalen Account. Anfragen werden automatisch deinem Spielernamen zugeordnet.</p> <input id="usernameInput" type="text" autocomplete="username" placeholder="Name"> <input id="passwordInput" type="password" autocomplete="current-password" placeholder="Passwort"> <div class="button-row"> <button id="loginButton" class="primary" type="button">Login</button> <button id="registerButton" class="muted" type="button">Registrieren</button> </div> <p id="authMessage"></p> </section> <section id="dashboard" class="card"> <div class="topbar"> <div> <h1 style="text-align:left;">EG Webstore</h1> <p id="userInfo" class="subtle"></p> </div> <div> <div class="top-actions"> <button id="refreshButton" class="muted" type="button">Aktualisieren</button> <button id="logoutButton" class="muted" type="button">Logout</button> </div> <div id="message"></div> </div> </div> <p class="subtle"> Alle Preise beziehen sich auf die Ingame-Waehrung des JDBROS-Servers. Das Symbol $ steht fuer Servergeld und nicht fuer Echtgeld. </p> <div id="shopClosedBox" class="closed"></div> <div class="layout"> <div> <h2>Produkte auf Anfrage</h2> <div class="toolbar"> <input id="productSearch" type="search" placeholder="Produkte suchen..."> <select id="categoryFilter"> <option value="">Alle Kategorien</option> </select> <select id="sortSelect"> <option value="default">Sortierung</option> <option value="priceAsc">Preis aufsteigend</option> <option value="priceDesc">Preis absteigend</option> <option value="nameAsc">Name A-Z</option> <option value="nameDesc">Name Z-A</option> </select> </div> <div id="productGrid" class="grid"></div> </div> <aside class="cart"> <h2>Anfragekorb</h2> <div id="cartItems"></div> <div class="panel" style="margin-top:10px;"> <input id="discountCode" type="text" placeholder="Rabattcode"> <button id="discountButton" class="muted" type="button" style="width:100%;">Rabatt pruefen</button> <div id="discountInfo" class="meta"></div> </div> <div class="summary-line"><span>Zwischensumme</span><strong id="subtotalPrice">0,00 $</strong></div> <div class="summary-line"><span>Aufpreis</span><strong id="surchargePrice">0,00 $</strong></div> <div class="summary-line"><span>Rabatt</span><strong id="discountPrice">-0,00 $</strong></div> <div class="summary-line total"><span>Gesamt</span><strong id="cartTotal">0,00 $</strong></div> <button id="clearCartButton" class="danger" type="button" style="width:100%; margin-top:12px;">Anfragekorb leeren</button> <h2>Anfrage senden</h2> <form id="orderForm"> <input id="discordName" type="text" placeholder="Discord Name" required> <select id="deliveryType" required> <option value="">Lieferart waehlen</option> <option value="Abholung">Abholung</option> <option value="Lieferung zum Plot">Lieferung zum Plot</option> </select> <div id="plotIdWrapper"> <input id="plotId" type="text" placeholder="Plot ID"> </div> <textarea id="customerNote" placeholder="Nachricht / Hinweis zur Anfrage"></textarea> <label class="gift-toggle"> <input id="isGift" type="checkbox"> Als Geschenk anfragen </label> <div id="giftFields"> <input id="giftRecipientName" type="text" placeholder="Empfaenger Ingame Name"> <textarea id="giftMessage" placeholder="Geschenk Nachricht"></textarea> </div> <div id="orderPreview" class="panel meta">Anfragevorschau erscheint hier.</div> <button id="submitOrderButton" class="primary" type="submit" style="width:100%;">Anfrage senden</button> </form> <div id="discordFeedback" class="webhook-box"></div> <h2>Anfrage verfolgen</h2> <button id="trackOrderButton" class="muted" type="button" style="width:100%;">Meine offene Anfrage pruefen</button> <div id="trackingResult" class="meta"></div> </aside> </div> </section> </main> <script> const PRODUCT_SURCHARGE = 500; const state = { user: "", token: "", trust: {}, products: [], categories: [], cart: [], discount: null, shopClosed: false, surcharge: PRODUCT_SURCHARGE }; let locked = false; const $ = id => document.getElementById(id); function setLocked(value) { locked = value; document.querySelectorAll("button, input, select, textarea").forEach(control => { control.disabled = value; }); if (value) showMessage("Synchronisiere...", "info", true); if (!value && $("message").textContent === "Synchronisiere...") showMessage(""); } function showMessage(text, type = "", sticky = false) { const target = $("dashboard").style.display === "block" ? $("message") : $("authMessage"); target.textContent = text || ""; target.className = type || ""; if (text && !sticky && type !== "error") { window.setTimeout(() => { if (!locked && target.textContent === text) target.textContent = ""; }, 2600); } } function showDiscordFeedback(webhook) { const box = $("discordFeedback"); webhook = webhook || {}; box.className = "webhook-box"; box.textContent = ""; if (!webhook.status) return; if (webhook.status === "success") { box.classList.add("success"); box.textContent = "Discord: Nachricht wurde erfolgreich gesendet."; return; } if (webhook.status === "skipped") { box.classList.add("error"); box.textContent = "Discord: Nachricht wurde NICHT gesendet.\n" + "Grund: " + (webhook.message || "Webhook wurde uebersprungen.") + "\n" + "Pruefe im Settings-Sheet den Key webstoreOrderWebhookUrl."; return; } box.classList.add("error"); box.textContent = "Discord: Nachricht wurde NICHT gesendet.\n" + "Status: " + (webhook.status || "-") + "\n" + (webhook.code ? "HTTP-Code: " + webhook.code + "\n" : "") + (webhook.message ? "Meldung: " + webhook.message + "\n" : "") + (webhook.body ? "Antwort: " + webhook.body : ""); } function serverCall(method, args = []) { return new Promise((resolve, reject) => { if (!window.google || !google.script || !google.script.run) { reject(new Error("Google Apps Script ist lokal nicht verfuegbar.")); return; } google.script.run .withSuccessHandler(resolve) .withFailureHandler(reject)[method](...args); }); } async function call(method, args = []) { setLocked(true); try { const res = await serverCall(method, args); if (!res || !res.ok) { if (res && res.error) throw new Error(res.error); let details = ""; try { details = JSON.stringify(res); } catch (err) { details = String(res); } throw new Error("Unerwartete Server-Antwort von " + method + ": " + details); } return res; } finally { setLocked(false); } } async function authenticate(mode) { const username = $("usernameInput").value.trim(); const password = $("passwordInput").value; if (!username || !password) { showMessage("Bitte Name und Passwort eingeben.", "error"); return; } try { const auth = await call("getInitialData", [username, password, mode]); state.token = auth.token; const webstore = await call("getWebstoreInitialData", [state.token]); applyStore(webstore); $("authArea").style.display = "none"; $("dashboard").style.display = "block"; } catch (err) { showMessage(err.message || String(err), "error"); } } async function refreshStore() { try { const data = await call("getWebstoreInitialData", [state.token]); applyStore(data); showMessage("Aktualisiert.", "info"); } catch (err) { showMessage(err.message || String(err), "error"); } } function applyStore(data) { state.user = data.user || state.user; state.token = data.token || state.token; state.trust = data.trust || {}; state.products = Array.isArray(data.products) ? data.products : []; state.categories = Array.isArray(data.categories) ? data.categories : []; state.surcharge = Number(data.surcharge || PRODUCT_SURCHARGE); state.shopClosed = Boolean(data.shopStatus && data.shopStatus.closed); $("userInfo").textContent = `${state.user} | Vertrauen: ${(state.trust && state.trust.level) || "Neu"} | ${(state.trust && state.trust.shopOrders) || 0} Shop-Anfragen`; renderCategories(); renderShopStatus(data.shopStatus || {}); renderProducts(); renderCart(); renderTracking(data.activeOrder || null); } function renderShopStatus(status) { if (state.shopClosed) { $("shopClosedBox").classList.add("active"); $("shopClosedBox").textContent = status.reason || "Wegen Wartungsarbeiten voruebergehend geschlossen"; } else { $("shopClosedBox").classList.remove("active"); $("shopClosedBox").textContent = ""; } } function renderCategories() { $("categoryFilter").innerHTML = '<option value="">Alle Kategorien</option>' + state.categories.map(category => `<option value="${escapeHtml(category.name)}">${escapeHtml(category.name)} (${category.count})</option>` ).join(""); } function getFilteredProducts() { const query = $("productSearch").value.trim().toLowerCase(); const category = $("categoryFilter").value; const sort = $("sortSelect").value; const products = state.products.filter(product => { const text = `${product.name || ""} ${product.description || ""}`.toLowerCase(); return (!query || text.includes(query)) && (!category || product.category === category); }); if (sort === "priceAsc") products.sort((a, b) => Number(a.price) - Number(b.price)); else if (sort === "priceDesc") products.sort((a, b) => Number(b.price) - Number(a.price)); else if (sort === "nameAsc") products.sort((a, b) => String(a.name).localeCompare(String(b.name), "de")); else if (sort === "nameDesc") products.sort((a, b) => String(b.name).localeCompare(String(a.name), "de")); else products.sort((a, b) => Number(a.sortOrder || 9999) - Number(b.sortOrder || 9999)); return products; } function renderProducts() { const grid = $("productGrid"); if (state.shopClosed) { grid.innerHTML = ""; return; } const products = getFilteredProducts(); if (!products.length) { grid.innerHTML = '<p class="empty">Keine passenden Produkte gefunden.</p>'; return; } grid.innerHTML = products.map(product => { const image = product.image ? `<img src="${escapeHtml(product.image)}" alt="${escapeHtml(product.name)}" loading="lazy">` : '<div class="placeholder">Kein Bild</div>'; const badge = product.badge || "AUF ANFRAGE"; const status = product.availabilityText || product.status || "Auf Anfrage"; return ` <article class="product"> <span class="badge">${escapeHtml(badge)}</span> ${image} <h3>${escapeHtml(product.name)}</h3> <p class="meta">${escapeHtml(product.description || "")}</p> <div class="price">${money(product.price)}</div> <p class="meta">+ ${money(state.surcharge)} Aufpreis pro Stueck</p> <p class="request-status">${escapeHtml(status)}</p> <p class="meta">${escapeHtml(product.category || "Allgemein")}</p> <button class="primary" type="button" data-action="add" data-id="${escapeHtml(product.id)}">Zur Anfrage hinzufuegen</button> </article> `; }).join(""); } function addToCart(productId) { if (state.shopClosed) return; const product = state.products.find(item => String(item.id) === String(productId)); if (!product) return; const existing = state.cart.find(item => String(item.id) === String(productId)); if (existing) existing.quantity += 1; else { state.cart.push({ id: product.id, name: product.name, price: Number(product.price) || 0, quantity: 1 }); } clearDiscount("Anfragekorb geaendert. Bitte Rabattcode erneut pruefen."); renderCart(); } function changeQuantity(productId, delta) { const item = state.cart.find(entry => String(entry.id) === String(productId)); if (!item) return; item.quantity += delta; if (item.quantity <= 0) { state.cart = state.cart.filter(entry => String(entry.id) !== String(productId)); } clearDiscount("Anfragekorb geaendert. Bitte Rabattcode erneut pruefen."); renderCart(); } function clearCart() { state.cart = []; clearDiscount(""); renderCart(); } function clearDiscount(message) { state.discount = null; $("discountCode").value = ""; $("discountInfo").textContent = message || ""; } function getSubtotal() { return roundMoney(state.cart.reduce((sum, item) => sum + Number(item.price || 0) * Number(item.quantity || 0), 0 )); } function getItemCount() { return state.cart.reduce((sum, item) => sum + Number(item.quantity || 0), 0); } function getSurcharge() { return roundMoney(getItemCount() * state.surcharge); } function getDiscountAmount() { if (!state.discount) return 0; return Math.min(Number(state.discount.discountAmount || 0), getSubtotal()); } function getTotal() { return roundMoney(Math.max(0, getSubtotal() + getSurcharge() - getDiscountAmount())); } function renderCart() { if (!state.cart.length) { $("cartItems").innerHTML = '<p class="empty">Dein Anfragekorb ist leer.</p>'; } else { $("cartItems").innerHTML = state.cart.map(item => ` <div class="cart-item"> <div> <strong>${escapeHtml(item.name)}</strong><br> <span class="meta">${money(item.price)} | Menge: ${item.quantity}</span> </div> <div class="cart-controls"> <button type="button" data-action="minus" data-id="${escapeHtml(item.id)}">-</button> <span>${item.quantity}</span> <button type="button" data-action="plus" data-id="${escapeHtml(item.id)}">+</button> </div> </div> `).join(""); } $("subtotalPrice").textContent = money(getSubtotal()); $("surchargePrice").textContent = money(getSurcharge()); $("discountPrice").textContent = "-" + money(getDiscountAmount()); $("cartTotal").textContent = money(getTotal()); renderOrderPreview(); } async function applyDiscountCode() { const code = $("discountCode").value.trim(); if (!code) { $("discountInfo").textContent = "Bitte einen Rabattcode eingeben."; return; } if (!state.cart.length) { $("discountInfo").textContent = "Bitte zuerst Produkte in den Anfragekorb legen."; return; } try { const res = await call("validateWebstoreDiscount", [state.token, code, getSubtotal(), state.cart]); state.discount = res; $("discountInfo").textContent = `Rabattcode aktiv: ${res.code} (-${money(res.discountAmount)})`; renderCart(); } catch (err) { state.discount = null; $("discountInfo").textContent = err.message || String(err); renderCart(); } } function updateDeliveryFields() { const needsPlot = $("deliveryType").value === "Lieferung zum Plot"; $("plotIdWrapper").style.display = needsPlot ? "block" : "none"; $("plotId").required = needsPlot; if (!needsPlot) $("plotId").value = ""; renderOrderPreview(); } function updateGiftFields() { const enabled = $("isGift").checked; $("giftFields").style.display = enabled ? "block" : "none"; $("giftRecipientName").required = enabled; if (!enabled) { $("giftRecipientName").value = ""; $("giftMessage").value = ""; } renderOrderPreview(); } function renderOrderPreview() { if (!state.cart.length) { $("orderPreview").textContent = "Anfragevorschau erscheint hier."; return; } const lines = state.cart.map(item => `${item.name} x${item.quantity} = ${money(item.price * item.quantity)}` ); let text = "Anfragevorschau\n\n" + lines.join("\n") + "\n\nZwischensumme: " + money(getSubtotal()) + "\nAufpreis: " + money(getSurcharge()) + "\nRabatt: -" + money(getDiscountAmount()) + "\nGesamt: " + money(getTotal()) + "\nLieferart: " + ($("deliveryType").value || "Nicht gewaehlt"); if ($("deliveryType").value === "Lieferung zum Plot") { text += "\nPlot ID: " + ($("plotId").value.trim() || "Nicht angegeben"); } if ($("isGift").checked) { text += "\nGeschenk fuer: " + ($("giftRecipientName").value.trim() || "Nicht angegeben"); } $("orderPreview").textContent = text; } <script> async function submitOrder(event) { event.preventDefault(); if (state.shopClosed) { showMessage("Der Shop ist aktuell geschlossen.", "error"); return; } if (!state.cart.length) { showMessage("Bitte zuerst Produkte in den Anfragekorb legen.", "error"); return; } const payload = { discordName: $("discordName").value.trim(), deliveryType: $("deliveryType").value, plotId: $("plotId").value.trim(), customerNote: $("customerNote").value.trim(), items: state.cart, subtotal: getSubtotal(), itemCount: getItemCount(), productSurcharge: getSurcharge(), discountCode: state.discount ? state.discount.code : "", discountAmount: state.discount ? state.discount.discountAmount : 0, total: getTotal(), isGift: $("isGift").checked, giftRecipientName: $("giftRecipientName").value.trim(), giftMessage: $("giftMessage").value.trim() }; try { const res = await call("saveWebstoreOrder", [state.token, payload]); const webhook = res.webhook || {}; state.cart = []; state.discount = null; state.products = Array.isArray(res.products) ? res.products : state.products; state.trust = res.trust || state.trust; $("orderForm").reset(); updateDeliveryFields(); updateGiftFields(); renderProducts(); renderCart(); renderTracking(res.activeOrder || null); showDiscordFeedback(webhook); let discordText = "\nDiscord: Nachricht wurde NICHT gesendet."; discordText += "\nWebhook-Status: " + (webhook.status || "keine Rueckgabe"); if (webhook.code) discordText += "\nHTTP-Code: " + webhook.code; if (webhook.message) discordText += "\nMeldung: " + webhook.message; if (webhook.body) discordText += "\nAntwort: " + webhook.body; if (webhook.status === "success") { showMessage( "Anfrage erfolgreich gesendet.\nBestellnummer: " + res.orderNumber + "\nDiscord: Nachricht wurde gesendet.", "success", true ); } else { showMessage( "Anfrage wurde gespeichert.\nBestellnummer: " + res.orderNumber + discordText, "error", true ); } } catch (err) { showMessage(err.message || String(err), "error", true); } } </script> try { const res = await call("saveWebstoreOrder", [state.token, payload]); const webhook = res.webhook || {}; state.cart = []; state.discount = null; state.products = Array.isArray(res.products) ? res.products : state.products; state.trust = res.trust || state.trust; $("orderForm").reset(); updateDeliveryFields(); updateGiftFields(); renderProducts(); renderCart(); renderTracking(res.activeOrder || null); showDiscordFeedback(webhook); if (webhook.status === "success") { showMessage("Anfrage erfolgreich gesendet.\nBestellnummer: " + res.orderNumber + "\nDiscord: Nachricht wurde gesendet.", "success"); } else { showMessage("Anfrage wurde gespeichert.\nBestellnummer: " + res.orderNumber + "\nDiscord: Nachricht wurde NICHT gesendet.", "error", true); } } catch (err) { showMessage(err.message || String(err), "error"); } } async function trackOrder() { try { const res = await call("trackWebstoreOrder", [state.token]); renderTracking(res.order || null); } catch (err) { $("trackingResult").textContent = err.message || String(err); } } function renderTracking(order) { if (!order) { $("trackingResult").textContent = "Keine offene Anfrage gefunden."; return; } const handler = order.handler || order.worker || order.assignedTo || ""; const response = order.handlerResponse || order.response || order.adminNote || ""; $("trackingResult").textContent = `Bestellnummer: ${order.orderNumber || "-"}\n` + `Status: ${order.statusText || "-"}\n` + `Bearbeiter: ${handler || "Noch nicht zugewiesen"}\n` + (response ? `Rueckmeldung: ${response}\n` : "") + `Datum: ${order.date || "-"}\n` + `Produkte: ${order.products || "-"}\n` + `Lieferart: ${order.deliveryType || "-"}\n` + `Gesamt: ${order.total || "0,00 $"}`; } async function logout() { try { if (state.token) await call("logout", [state.token]); } catch (err) { // Reload reicht, falls der Server nicht antwortet. } window.location.reload(); } function money(value) { return `${roundMoney(value).toLocaleString("de-DE", { minimumFractionDigits: 2, maximumFractionDigits: 2 })} $`; } function roundMoney(value) { return Math.round((Number(value) || 0) * 100) / 100; } function escapeHtml(value) { return String(value === undefined || value === null ? "" : value) .replace(/&/g, "&amp;") .replace(/</g, "&lt;") .replace(/>/g, "&gt;") .replace(/"/g, "&quot;") .replace(/'/g, "&#039;"); } $("loginButton").addEventListener("click", () => authenticate("login")); $("registerButton").addEventListener("click", () => authenticate("register")); $("refreshButton").addEventListener("click", refreshStore); $("logoutButton").addEventListener("click", logout); $("productSearch").addEventListener("input", renderProducts); $("categoryFilter").addEventListener("change", renderProducts); $("sortSelect").addEventListener("change", renderProducts); $("discountButton").addEventListener("click", applyDiscountCode); $("clearCartButton").addEventListener("click", clearCart); $("deliveryType").addEventListener("change", updateDeliveryFields); $("plotId").addEventListener("input", renderOrderPreview); $("customerNote").addEventListener("input", renderOrderPreview); $("isGift").addEventListener("change", updateGiftFields); $("giftRecipientName").addEventListener("input", renderOrderPreview); $("giftMessage").addEventListener("input", renderOrderPreview); $("orderForm").addEventListener("submit", submitOrder); $("trackOrderButton").addEventListener("click", trackOrder); $("productGrid").addEventListener("click", event => { const button = event.target.closest("button[data-action='add']"); if (button) addToCart(button.dataset.id); }); $("cartItems").addEventListener("click", event => { const button = event.target.closest("button[data-action]"); if (!button) return; if (button.dataset.action === "plus") changeQuantity(button.dataset.id, 1); if (button.dataset.action === "minus") changeQuantity(button.dataset.id, -1); }); </script> </body> </html>. (line 140, file "Code")