430fa718fa
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.
109 lines
5.1 KiB
JavaScript
109 lines
5.1 KiB
JavaScript
/* 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;
|
|
var popups;
|
|
try { popups = JSON.parse(root.getAttribute('data-popups') || '[]'); } catch (e) { return; }
|
|
if (!popups || !popups.length) return;
|
|
|
|
function seen(p) {
|
|
var k = 'hdc_popup_' + p.id;
|
|
var v = localStorage.getItem(k);
|
|
if (!v) return false;
|
|
if (p.freq === 'always') return false;
|
|
if (p.freq === 'session') return sessionStorage.getItem('hdc_ps_' + p.id) === '1';
|
|
if (p.freq === 'days7') { return (Date.now() - parseInt(v, 10)) < 7 * 864e5; }
|
|
return true;
|
|
}
|
|
function mark(p) {
|
|
localStorage.setItem('hdc_popup_' + p.id, String(Date.now()));
|
|
if (p.freq === 'session') sessionStorage.setItem('hdc_ps_' + p.id, '1');
|
|
}
|
|
function esc(s) { return String(s == null ? '' : s).replace(/[&<>"]/g, function (c) { return ({ '&': '&', '<': '<', '>': '>', '"': '"' })[c]; }); }
|
|
|
|
function innerHtml(p) {
|
|
var isNl = p.type === 'newsletter';
|
|
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">×</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">×</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);
|
|
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) {
|
|
e.preventDefault();
|
|
var email = form.querySelector('input').value;
|
|
var msg = form.querySelector('.nl-msg');
|
|
fetch('/api/subscribe', { method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ email: email, source: 'popup' }) })
|
|
.then(function (r) { return r.json(); })
|
|
.then(function () { msg.textContent = 'Danke! Schau in dein Postfach.'; setTimeout(close, 1600); })
|
|
.catch(function () { msg.textContent = 'Bitte später erneut versuchen.'; });
|
|
});
|
|
}
|
|
}
|
|
|
|
function arm(p) {
|
|
if (seen(p)) return;
|
|
var trig = p.trigger || 'delay';
|
|
var val = parseInt(p.trigger_value, 10) || 0;
|
|
if (trig === 'delay') { setTimeout(function () { build(p); }, Math.max(0, val) * 1000); }
|
|
else if (trig === 'scroll') {
|
|
var fn = function () {
|
|
var sc = (window.scrollY + window.innerHeight) / document.body.scrollHeight * 100;
|
|
if (sc >= (val || 50)) { window.removeEventListener('scroll', fn); build(p); }
|
|
};
|
|
window.addEventListener('scroll', fn, { passive: true });
|
|
} else if (trig === 'exit') {
|
|
var fired = false;
|
|
document.addEventListener('mouseout', function (e) {
|
|
if (!fired && e.clientY <= 0 && !e.relatedTarget) { fired = true; build(p); }
|
|
});
|
|
setTimeout(function () { if (!fired) { fired = true; build(p); } }, 25000); // mobile-fallback
|
|
}
|
|
}
|
|
popups.forEach(arm);
|
|
})();
|