v2.1: Gutschein-/Rabatt-Engine + editierbare gebrandete 404

Feature 1 — Rabatt-Engine (store-sqlite.js):
- Tabellen discounts + discount_redemptions; orders um discount_code/discount_cents erweitert.
- Helper: getDiscountByCode, listDiscounts, create/update/deleteDiscount,
  validateDiscount (Zeitplan/Mindestwert/Limits/pro-Kunde), bestAutoDiscount, redeemDiscount.
- Seed: WILLKOMMEN10, NAEHEN5, GRATISVERSAND (geplant), AUTO15AB75 (auto).
- Checkout: /api/discount (serverseitige Subtotal-Berechnung) + /api/checkout re-validiert,
  wendet Rabatt/Gratisversand an, speichert + redeemt, auto-Discount-Fallback, Stripe-Coupon.
- Cart/Checkout-UI mit Code-Feld + Einlösen; Rabattzeile in Order-Detail + Erfolgsseite.
- Admin "Rabatte" (owner+redaktion) mit Status-Badges + Editor (Zufallscode, Typ-abh. Wertfeld).
- Popups: Typ discount zeigt Code + Kopieren-Button; Stile modal/slidein/bar (CSS ergaenzt).

Feature 2 — 404:
- src/pages/404.astro nutzt Base + BlockRenderer, laedt System-Seite slug 404.
- ensureSystemPages() legt 404 idempotent bei jedem Boot an (INSERT OR IGNORE, flache Bloecke).
- 404/system erscheint in Admin "Inhalte" und oeffnet im Block-Editor.

API/MCP: discounts in /api/admin/* (CRUD), Manifest + ai-admin.txt ergaenzt
(inkl. Hinweis: Block-Objekte sind flach); MCP list/upsert/delete_discount.
README + Versionen (2.1.0) aktualisiert.
This commit is contained in:
2026-06-17 15:12:07 +00:00
parent aec179db36
commit 430fa718fa
21 changed files with 572 additions and 52 deletions
+48 -15
View File
@@ -1,4 +1,5 @@
/* hd-commerce — Popup-Engine (vanilla). Frequenz-Cap via localStorage. */
/* hd-commerce — Popup-Engine (vanilla). Frequenz-Cap via localStorage.
Stile: modal (zentral), slidein (unten rechts), bar (oben). Typ discount: Code + Kopieren. */
(function () {
var root = document.getElementById('popupRoot');
if (!root) return;
@@ -19,25 +20,56 @@
localStorage.setItem('hdc_popup_' + p.id, String(Date.now()));
if (p.freq === 'session') sessionStorage.setItem('hdc_ps_' + p.id, '1');
}
function build(p) {
var ov = document.createElement('div');
ov.className = 'hdc-popup-overlay';
function esc(s) { return String(s == null ? '' : s).replace(/[&<>"]/g, function (c) { return ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' })[c]; }); }
function innerHtml(p) {
var isNl = p.type === 'newsletter';
ov.innerHTML =
'<div class="hdc-popup">' +
'<button class="px" aria-label="Schließen">&times;</button>' +
(p.image ? '<img src="' + p.image + '" alt="" style="border-radius:10px;margin-bottom:16px;max-height:160px;width:100%;object-fit:cover">' : '') +
'<h3>' + (p.headline || '') + '</h3>' +
'<p>' + (p.body || '') + '</p>' +
(isNl
? '<form class="nl-form"><input type="email" required placeholder="deine@email.de"><button class="btn btn-primary btn-block" type="submit">' + (p.cta_text || 'Anmelden') + '</button><div class="nl-msg"></div></form>'
: (p.cta_url ? '<a class="btn btn-primary btn-lg" href="' + p.cta_url + '">' + (p.cta_text || 'Mehr') + '</a>' : '')) +
'</div>';
var isDisc = p.type === 'discount' && p.discount_code;
var html =
(p.image ? '<img src="' + esc(p.image) + '" alt="" style="border-radius:10px;margin-bottom:14px;max-height:160px;width:100%;object-fit:cover">' : '') +
'<h3>' + esc(p.headline) + '</h3>' +
'<p>' + esc(p.body) + '</p>';
if (isDisc) {
html += '<div class="hdc-code"><code>' + esc(p.discount_code) + '</code>' +
'<button type="button" class="hdc-copy" data-code="' + esc(p.discount_code) + '">Kopieren</button></div>';
if (p.cta_url) html += '<a class="btn btn-primary btn-block" href="' + esc(p.cta_url) + '">' + esc(p.cta_text || 'Zum Shop') + '</a>';
} else if (isNl) {
html += '<form class="nl-form"><input type="email" required placeholder="deine@email.de"><button class="btn btn-primary btn-block" type="submit">' + esc(p.cta_text || 'Anmelden') + '</button><div class="nl-msg"></div></form>';
} else if (p.cta_url) {
html += '<a class="btn btn-primary btn-lg" href="' + esc(p.cta_url) + '">' + esc(p.cta_text || 'Mehr') + '</a>';
}
return html;
}
function build(p) {
var style = p.style || 'modal';
var ov, card;
if (style === 'modal') {
ov = document.createElement('div');
ov.className = 'hdc-popup-overlay';
ov.innerHTML = '<div class="hdc-popup"><button class="px" aria-label="Schließen">&times;</button>' + innerHtml(p) + '</div>';
card = ov.querySelector('.hdc-popup');
} else {
ov = document.createElement('div');
ov.className = 'hdc-popup-' + (style === 'bar' ? 'bar' : 'slidein');
ov.innerHTML = '<div class="hdc-pp-inner"><button class="px" aria-label="Schließen">&times;</button>' + innerHtml(p) + '</div>';
card = ov;
}
document.body.appendChild(ov);
requestAnimationFrame(function () { ov.classList.add('show'); });
function close() { ov.classList.remove('show'); mark(p); setTimeout(function () { ov.remove(); }, 320); }
ov.querySelector('.px').addEventListener('click', close);
ov.addEventListener('click', function (e) { if (e.target === ov) close(); });
if (style === 'modal') ov.addEventListener('click', function (e) { if (e.target === ov) close(); });
var copyBtn = ov.querySelector('.hdc-copy');
if (copyBtn) copyBtn.addEventListener('click', function () {
var code = copyBtn.getAttribute('data-code');
try { navigator.clipboard.writeText(code); } catch (e) {}
var old = copyBtn.textContent; copyBtn.textContent = 'Kopiert!';
setTimeout(function () { copyBtn.textContent = old; }, 1600);
});
var form = ov.querySelector('form');
if (form) {
form.addEventListener('submit', function (e) {
@@ -52,6 +84,7 @@
});
}
}
function arm(p) {
if (seen(p)) return;
var trig = p.trigger || 'delay';