50dfca59e1
P1 Medien: eigener Admin-Bereich /admin/medien (Grid, Mehrfach-Upload, Drag&Drop, Alt-Text, URL kopieren, Loeschen). Upload konvertiert JPG/PNG via sharp zu WebP (Qualitaet 82, max 2000px), Original wird verworfen; WebP/SVG/GIF/AVIF unveraendert; Konvertierungsfehler -> Original behalten statt 500. media um alt/width/height erweitert. Wiederverwendbarer Medien-Picker (public/media-picker.js) ersetzt den URL-Prompt im Block-Editor, Produkt-Editor (Karte/Galerie/Varianten-Bild), Slides und Popups. JSON-Quelle /api/admin/media (session-gesichert). P2 Varianten: products.options_json + Tabelle product_variants. Produkt-Editor mit Options-Definition + Matrix-Generator (Preis-Override/Bestand/SKU/Bild/aktiv je Variante). PDP-Selektoren -> Variante; Cart/Checkout tragen sku+Options, Order-Item bekommt sku/variant, Variantenpreis serverseitig verifiziert. Produkte ohne Optionen unveraendert. P3 Litestream: Binary im Dockerfile, docker-entrypoint.sh (Restore+replicate nur bei LITESTREAM_REPLICA_URL, sonst reiner Node-Start), litestream.yml, Backup-Status unter Einstellungen, README + .env.example. P4 Analytics: Bestseller, Top-Suchbegriffe, Umsatz/Quelle, Umsatz-Zeitreihe, AOV, Wiederkaufrate, Lager-Warnungen. Neue Dep sharp. +19 Unit-Tests (49 gesamt gruen), Build + Smoke (P1-P4) gruen.
70 lines
3.8 KiB
JavaScript
70 lines
3.8 KiB
JavaScript
/* hd-commerce — Storefront Cart (vanilla, localStorage) */
|
|
(function () {
|
|
var KEY = 'hdc_cart';
|
|
function read() { try { return JSON.parse(localStorage.getItem(KEY) || '[]'); } catch (e) { return []; } }
|
|
function write(c) { localStorage.setItem(KEY, JSON.stringify(c)); updateBadge(); }
|
|
function count() { return read().reduce(function (s, i) { return s + (i.qty || 1); }, 0); }
|
|
function updateBadge() {
|
|
var b = document.getElementById('cartBadge');
|
|
if (!b) return;
|
|
var n = count();
|
|
b.textContent = n;
|
|
b.classList.toggle('show', n > 0);
|
|
}
|
|
function track(type, value, meta) {
|
|
try {
|
|
fetch('/api/track', { method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ type: type, path: location.pathname, value_cents: value || 0, meta: meta || {} }) });
|
|
} catch (e) {}
|
|
}
|
|
function add(item) {
|
|
var c = read();
|
|
var ex = c.find(function (i) { return i.slug === item.slug && (i.sku || '') === (item.sku || '') && i.size === item.size; });
|
|
if (ex) ex.qty += item.qty || 1; else c.push(item);
|
|
write(c);
|
|
track('add_to_cart', (item.priceCents || 0) * (item.qty || 1), { slug: item.slug });
|
|
toast(item.name + ' wurde hinzugefügt');
|
|
}
|
|
function toast(msg) {
|
|
var t = document.createElement('div');
|
|
t.textContent = msg;
|
|
t.style.cssText = 'position:fixed;bottom:24px;left:50%;transform:translateX(-50%);background:var(--ink);color:#fff;padding:12px 22px;border-radius:999px;font-size:14px;font-weight:600;z-index:200;box-shadow:0 8px 30px rgba(0,0,0,.2);transition:.3s;opacity:0';
|
|
document.body.appendChild(t);
|
|
requestAnimationFrame(function () { t.style.opacity = '1'; });
|
|
setTimeout(function () { t.style.opacity = '0'; setTimeout(function () { t.remove(); }, 320); }, 2000);
|
|
}
|
|
window.HDC = {
|
|
read: read, write: write, count: count, add: add, track: track,
|
|
remove: function (idx) { var c = read(); c.splice(idx, 1); write(c); },
|
|
setQty: function (idx, q) { var c = read(); if (c[idx]) { c[idx].qty = Math.max(1, q); write(c); } },
|
|
clear: function () { write([]); },
|
|
subtotal: function () { return read().reduce(function (s, i) { return s + (i.priceCents || 0) * (i.qty || 1); }, 0); }
|
|
};
|
|
document.addEventListener('DOMContentLoaded', function () {
|
|
updateBadge();
|
|
document.querySelectorAll('[data-add-to-cart]').forEach(function (btn) {
|
|
btn.addEventListener('click', function () {
|
|
if (btn.disabled) return;
|
|
var p = JSON.parse(btn.getAttribute('data-product') || '{}');
|
|
if (p.hasVariants) {
|
|
if (!p.variant || !p.variant.options) { var st = document.getElementById('variantStatus'); if (st) { st.textContent = 'Bitte zuerst eine Variante wählen.'; st.style.color = '#b3261e'; } return; }
|
|
var label = Object.keys(p.variant.options).map(function (k) { return p.variant.options[k]; }).join(' / ');
|
|
add({ slug: p.slug, name: p.name, size: label, priceCents: p.priceCents, image: p.image, qty: 1, sku: p.variant.sku || '', variant: p.variant.options, options: p.variant.options });
|
|
return;
|
|
}
|
|
var sizeSel = document.querySelector('.size-chip.active');
|
|
var size = sizeSel ? sizeSel.getAttribute('data-size') : (p.sizes && p.sizes[0]) || 'One Size';
|
|
add({ slug: p.slug, name: p.name, size: size, priceCents: p.priceCents, image: p.image, qty: 1 });
|
|
});
|
|
});
|
|
document.querySelectorAll('.size-chip').forEach(function (chip) {
|
|
chip.addEventListener('click', function () {
|
|
document.querySelectorAll('.size-chip').forEach(function (c) { c.classList.remove('active'); });
|
|
chip.classList.add('active');
|
|
});
|
|
});
|
|
var pv = document.getElementById('pdpData');
|
|
if (pv) track('product_view', 0, { slug: pv.getAttribute('data-slug') });
|
|
});
|
|
})();
|