Drei Mitgliedschaftsstufen (Klein 4,99€/5%, Mittel 9,99€/10%, Groß 19,99€/20%) mit automatischem Bestellrabatt und Teddy-Prämien nach Meilensteinen (12-Monats- Rhythmus bzw. 6+12 bei Groß). Preise/Rabatte und alle Admin-Regeln (Kombinier- barkeit mit anderen Rabatten, Fortsetzung bei Wiederanmeldung, Monate über Stufen zusammenzählen, kostenloser Versand bei Prämie) sind über Decap CMS einstellbar (src/content/abo-einstellungen.json). Neuer "Mein Abo"-Bereich im Kundenkonto (6. Nav-Kachel) mit Plan-Vergleich, Live-Status (Laufzeit, nächste Zahlung, Rabatt, Fortschritt bis zur nächsten Prämie), Einlösen-Buttons für freigeschaltete Teddys, Kündigen/Wechseln sowie einer übersetzten Benachrichtigungsliste. Dazu ein extra hervorgehobener "Mein Abo verwalten"-Button neben Abmelden in der Kopfkarte. Warenkorb/Checkout rechnen den Abo-Rabatt korrekt in die Summe ein und stellen eingelöste Teddys als 0€-Gratisposition dar. Alles in 4 Sprachen (DE/EN/CH/FR). Unterwegs zwei echte Bugs gefunden und behoben: addMonateISO() vermischte lokale Zeit mit toISOString() (UTC) und verlor dadurch bei jeder Monats- verlängerung reproduzierbar einen Tag; die Checkout-Positionsliste zeigte für Gratis-Prämien fälschlich den vollen Preis statt 0€. Wie beim Treuebonus gilt: Speicherung aktuell im localStorage des Geräts (Phase 1) statt einer echten geräteübergreifenden Datenbank — folgt mit der Zahlungsanbindung in Phase 2. Co-Authored-By: Claude Sonnet 5 <[email protected]>
297 lines
16 KiB
Plaintext
297 lines
16 KiB
Plaintext
---
|
||
import Layout from "../../../layouts/Layout.astro";
|
||
import type { Locale } from "../../../i18n/config";
|
||
import { useTranslations } from "../../../i18n/ui";
|
||
|
||
const lang: Locale = "en";
|
||
const t = useTranslations(lang);
|
||
---
|
||
<Layout title="Cart" description="Your cart at Van's DIY & Bastelbedarf." lang={lang} path="/warenkorb/">
|
||
<section class="section-tight">
|
||
<div class="container">
|
||
<span class="eyebrow">{t.cart.eyebrow}</span>
|
||
<h1>{t.cart.title}</h1>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="section-tight">
|
||
<div class="container">
|
||
<div id="cart-empty" class="card" style="display:none;">
|
||
<p>{t.cart.empty}</p>
|
||
<a class="btn btn-primary" href="/en/shop/">{t.common.browseShop}</a>
|
||
</div>
|
||
|
||
{/* Treuebonus-Banner: nur sichtbar, wenn im Konto ein Bonus wartet (siehe scripts/account.ts)
|
||
— erklärt direkt hier, dass man unten bei einem Artikel "-20%" auswählen kann. */}
|
||
<div class="card loyalty-banner" id="loyalty-banner" style="display:none;">
|
||
<span class="loyalty-banner-icon" aria-hidden="true">🎁</span>
|
||
<div>
|
||
<strong>{t.cart.loyaltyBannerTitle}</strong>
|
||
<p>{t.cart.loyaltyBannerText}</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="cart-content" class="cart-layout" style="display:none;">
|
||
<div class="card cart-items" id="cart-items"></div>
|
||
<aside class="card cart-summary">
|
||
<h3>🧾 {t.cart.subtotal}</h3>
|
||
<div id="summary-items" class="checkout-summary"></div>
|
||
<div class="summary-row"><span>{t.cart.subtotal}</span><span id="sum-subtotal">0,00 €</span></div>
|
||
<div class="summary-row discount-row" id="discount-row" style="display:none;"><span>{t.cart.discount}</span><span id="sum-discount">-0,00 €</span></div>
|
||
<div class="summary-row discount-row" id="partner-discount-row" style="display:none;"><span>{t.cart.partnerDiscount}</span><span id="sum-partner-discount">-0,00 €</span></div>
|
||
<div class="summary-row discount-row" id="loyalty-discount-row" style="display:none;"><span>{t.cart.loyaltyDiscount}</span><span id="sum-loyalty-discount">-0,00 €</span></div>
|
||
<div class="summary-row discount-row" id="abo-discount-row" style="display:none;"><span>{t.cart.aboDiscount}</span><span id="sum-abo-discount">-0,00 €</span></div>
|
||
<div class="coupon-row">
|
||
<label for="coupon-input">{t.cart.couponLabel}</label>
|
||
<div class="coupon-input-group">
|
||
<input type="text" id="coupon-input" placeholder={t.cart.couponPlaceholder} />
|
||
<button type="button" id="coupon-apply" class="btn btn-outline btn-sm">{t.cart.couponApply}</button>
|
||
</div>
|
||
<p class="small" id="coupon-status"></p>
|
||
</div>
|
||
<div class="coupon-row">
|
||
<label for="partner-code-input">{t.cart.partnerCodeLabel}</label>
|
||
<div class="coupon-input-group">
|
||
<input type="text" id="partner-code-input" placeholder={t.cart.partnerCodePlaceholder} />
|
||
<button type="button" id="partner-code-apply" class="btn btn-outline btn-sm">{t.cart.partnerCodeApply}</button>
|
||
</div>
|
||
<p class="small" id="partner-code-status"></p>
|
||
</div>
|
||
<div class="ship-country-row">
|
||
<label for="cart-land">📦 {t.checkout.country}</label>
|
||
<select id="cart-land">
|
||
<option value="de">Germany</option>
|
||
<option value="at">Austria</option>
|
||
<option value="ch">Switzerland</option>
|
||
<option value="lu">Luxembourg</option>
|
||
</select>
|
||
</div>
|
||
<div class="summary-row"><span>{t.cart.shipping}</span><span id="sum-shipping">{t.cart.shippingCalculated}</span></div>
|
||
<p class="free-shipping-badge" id="shipping-hint"></p>
|
||
<hr class="divider" />
|
||
<div class="summary-row total"><span>{t.cart.total}</span><span id="sum-total">0,00 €</span></div>
|
||
<p class="small">{t.common.vatNote}</p>
|
||
<a class="btn btn-primary btn-block" href="/en/checkout/">{t.cart.toCheckout}</a>
|
||
</aside>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
</Layout>
|
||
|
||
<script define:vars={{ freeShipFrom: 75, removeLabel: t.cart.remove, perItemLabel: t.cart.perItem, remainingTemplate: t.cart.remaining("__V__"), freeShippingText: t.cart.freeShipping, freeLabel: t.checkout.free, cartLang: lang, mengenrabattTemplate: t.cart.mengenrabatt("__P__"), couponInvalid: t.cart.couponInvalid, couponAppliedTemplate: t.cart.couponApplied("__C__"), couponRemoveLabel: t.cart.couponRemove, buyNowLabel: t.cart.buyNow, keptInCartLabel: t.cart.keptInCart, partnerCodeInvalid: t.cart.partnerCodeInvalid, partnerCodeAppliedTemplate: t.cart.partnerCodeApplied("__C__"), partnerCodeRemoveLabel: t.cart.partnerCodeRemove, loyaltySelectLabel: t.cart.loyaltySelectLabel, loyaltySelectedLabel: t.cart.loyaltySelectedLabel, gratisLabel: t.cart.gratisPraemie }}>
|
||
// WICHTIG: define:vars-Skripte laufen als IIFE, keine "import"-Anweisungen möglich.
|
||
window.__cartVars = { freeShipFrom, removeLabel, perItemLabel, remainingTemplate, freeShippingText, freeLabel, cartLang, mengenrabattTemplate, couponInvalid, couponAppliedTemplate, couponRemoveLabel, buyNowLabel, keptInCartLabel, partnerCodeInvalid, partnerCodeAppliedTemplate, partnerCodeRemoveLabel, loyaltySelectLabel, loyaltySelectedLabel, gratisLabel };
|
||
</script>
|
||
<script type="module">
|
||
import { getCart, updateQuantity, removeFromCart, cartTotal, appliedCoupon, couponDiscount, setCouponCode, clearCoupon, appliedPartnerCode, partnerCodeDiscount, setPartnerCode, clearPartnerCode, totalDiscount, treuebonusDiscount, aboDiscount, istAusgewaehlt, setAusgewaehlt, zeilenpreisFuer } from "../../../scripts/cart";
|
||
import { formatPrice } from "../../../i18n/format";
|
||
import { getProduct, products } from "../../../data/products";
|
||
import { berechneVersandkosten } from "../../../data/versand";
|
||
import { mengenrabattProzent } from "../../../data/rabatt";
|
||
import { getAccount, treueFortschritt, setTreuebonusArtikel } from "../../../scripts/account";
|
||
|
||
const { freeShipFrom, removeLabel, perItemLabel, remainingTemplate, freeShippingText, freeLabel, cartLang, mengenrabattTemplate, couponInvalid, couponAppliedTemplate, couponRemoveLabel, buyNowLabel, keptInCartLabel, partnerCodeInvalid, partnerCodeAppliedTemplate, partnerCodeRemoveLabel, loyaltySelectLabel, loyaltySelectedLabel, gratisLabel } = window.__cartVars;
|
||
const landSelect = document.getElementById("cart-land");
|
||
|
||
function render() {
|
||
const cart = getCart();
|
||
const empty = document.getElementById("cart-empty");
|
||
const content = document.getElementById("cart-content");
|
||
const itemsEl = document.getElementById("cart-items");
|
||
|
||
if (cart.length === 0) {
|
||
empty.style.display = "block";
|
||
content.style.display = "none";
|
||
return;
|
||
}
|
||
empty.style.display = "none";
|
||
content.style.display = "grid";
|
||
|
||
const zeilen = cart.map((i) => {
|
||
const produkt = getProduct(i.slug);
|
||
const zeilenpreis = zeilenpreisFuer(i);
|
||
const mengenProzent = produkt ? mengenrabattProzent(produkt, i.menge) : 0;
|
||
return { item: i, produkt, zeilenpreis, mengenProzent };
|
||
});
|
||
|
||
// Treuebonus: nur relevant, wenn im Konto gerade eine Belohnung auf Einlösung wartet — dann
|
||
// darf hier EIN Artikel frei ausgewählt werden, auf den die -20% angewendet werden.
|
||
const konto = getAccount();
|
||
const treueAktiv = treueFortschritt(konto).aktiv;
|
||
const gewaehlterSlug = konto.treuebonusArtikelSlug;
|
||
const loyaltyBanner = document.getElementById("loyalty-banner");
|
||
if (loyaltyBanner) loyaltyBanner.style.display = treueAktiv ? "flex" : "none";
|
||
|
||
itemsEl.innerHTML = zeilen.map(({ item: i, produkt, zeilenpreis, mengenProzent }) => {
|
||
const foto = produkt?.bilder?.[0];
|
||
const thumb = foto
|
||
? `<img class="thumb product-photo" src="${foto}" alt="${i.name}" loading="lazy" />`
|
||
: `<div class="img-placeholder thumb" role="img" aria-label="${i.name}"></div>`;
|
||
const mengenBadge = mengenProzent > 0 ? `<div class="small mengenrabatt-badge">${mengenrabattTemplate.replace("__P__", mengenProzent)}</div>` : "";
|
||
const ausgewaehlt = istAusgewaehlt(i);
|
||
const keptBadge = !ausgewaehlt ? `<div class="small kept-in-cart-badge">🕓 ${keptInCartLabel}</div>` : "";
|
||
const istTreueartikel = gewaehlterSlug === i.slug;
|
||
const loyaltyControl = !treueAktiv ? "" : istTreueartikel
|
||
? `<span class="loyalty-selected-badge">${loyaltySelectedLabel}</span>`
|
||
: `<label class="loyalty-select"><input type="radio" name="loyalty-artikel" data-loyalty-slug="${i.slug}" /> ${loyaltySelectLabel}</label>`;
|
||
const gratisBadge = i.gratis ? `<div class="small loyalty-selected-badge">🧸 ${gratisLabel}</div>` : "";
|
||
return `
|
||
<div class="cart-item ${ausgewaehlt ? "" : "deselected"} ${istTreueartikel ? "loyalty-selected" : ""}">
|
||
<input type="checkbox" class="buy-toggle" data-slug="${i.slug}" ${ausgewaehlt ? "checked" : ""} aria-label="${buyNowLabel}" title="${buyNowLabel}" />
|
||
${thumb}
|
||
<div class="info">
|
||
<strong>${i.name}</strong>
|
||
${!i.gratis ? `<div class="small">${formatPrice(zeilenpreis / i.menge, cartLang)} ${perItemLabel}</div>` : ""}
|
||
${mengenBadge}
|
||
${keptBadge}
|
||
${gratisBadge}
|
||
${loyaltyControl}
|
||
</div>
|
||
<div class="qty-controls">
|
||
<button data-action="dec" data-slug="${i.slug}" aria-label="−">−</button>
|
||
<span>${i.menge}</span>
|
||
<button data-action="inc" data-slug="${i.slug}" aria-label="+">+</button>
|
||
</div>
|
||
<div class="line-total">${formatPrice(zeilenpreis, cartLang)}</div>
|
||
<a href="#" class="remove" data-action="remove" data-slug="${i.slug}">✕ ${removeLabel}</a>
|
||
</div>
|
||
`;
|
||
}).join("");
|
||
|
||
itemsEl.querySelectorAll("[data-loyalty-slug]").forEach((el) => {
|
||
el.addEventListener("change", (e) => {
|
||
setTreuebonusArtikel(e.currentTarget.dataset.loyaltySlug);
|
||
render();
|
||
});
|
||
});
|
||
|
||
// Nur ausgewählte Artikel fließen in Rechnung/Zwischensumme/Versand/Checkout ein — abgewählte
|
||
// bleiben sichtbar im Warenkorb liegen (leicht abgedunkelt), ohne mitgekauft zu werden.
|
||
const ausgewaehlteZeilen = zeilen.filter(({ item: i }) => istAusgewaehlt(i));
|
||
|
||
// Rechnungs-Übersicht in der Zusammenfassung — dieselbe Positionsliste wie im Warenkorb
|
||
// links, nur kompakt als "Beleg" (Menge × Name → Zeilenpreis), passend zum Checkout.
|
||
document.getElementById("summary-items").innerHTML = ausgewaehlteZeilen.map(({ item: i, zeilenpreis }) =>
|
||
`<div class="row"><span>${i.menge}× ${i.name}</span><span>${formatPrice(zeilenpreis, cartLang)}</span></div>`
|
||
).join("");
|
||
|
||
const subtotal = cartTotal();
|
||
const remaining = Math.max(0, freeShipFrom - subtotal);
|
||
const shipping = berechneVersandkosten(landSelect.value, ausgewaehlteZeilen.map(({ item: i }) => ({ slug: i.slug, preis: i.preis, menge: i.menge })), products);
|
||
const coupon = appliedCoupon();
|
||
const discount = couponDiscount(subtotal);
|
||
const partnerCode = appliedPartnerCode();
|
||
const partnerDiscount = partnerCodeDiscount(subtotal);
|
||
const total = Math.max(0, subtotal - totalDiscount(subtotal)) + shipping;
|
||
document.getElementById("sum-subtotal").textContent = formatPrice(subtotal, cartLang);
|
||
document.getElementById("sum-shipping").textContent = shipping === 0 ? freeLabel : formatPrice(shipping, cartLang);
|
||
document.getElementById("sum-total").textContent = formatPrice(total, cartLang);
|
||
document.getElementById("shipping-hint").textContent =
|
||
remaining > 0 ? remainingTemplate.replace("__V__", formatPrice(remaining, cartLang)) : `🩵 ${freeShippingText}`;
|
||
|
||
const discountRow = document.getElementById("discount-row");
|
||
const couponStatus = document.getElementById("coupon-status");
|
||
const couponInput = document.getElementById("coupon-input");
|
||
if (coupon && discount > 0) {
|
||
discountRow.style.display = "flex";
|
||
document.getElementById("sum-discount").textContent = "-" + formatPrice(discount, cartLang);
|
||
couponStatus.innerHTML = `✅ ${couponAppliedTemplate.replace("__C__", coupon.code)} <a href="#" id="coupon-clear">✕ ${couponRemoveLabel}</a>`;
|
||
couponInput.value = coupon.code;
|
||
const clearLink = document.getElementById("coupon-clear");
|
||
if (clearLink) clearLink.addEventListener("click", (e) => { e.preventDefault(); clearCoupon(); render(); });
|
||
} else {
|
||
discountRow.style.display = "none";
|
||
couponStatus.textContent = couponInput.dataset.invalid === "1" ? couponInvalid : "";
|
||
}
|
||
|
||
const partnerDiscountRow = document.getElementById("partner-discount-row");
|
||
const partnerStatus = document.getElementById("partner-code-status");
|
||
const partnerInput = document.getElementById("partner-code-input");
|
||
if (partnerCode && partnerDiscount > 0) {
|
||
partnerDiscountRow.style.display = "flex";
|
||
document.getElementById("sum-partner-discount").textContent = "-" + formatPrice(partnerDiscount, cartLang);
|
||
partnerStatus.innerHTML = `✅ ${partnerCodeAppliedTemplate.replace("__C__", partnerCode.code)} <a href="#" id="partner-code-clear">✕ ${partnerCodeRemoveLabel}</a>`;
|
||
partnerInput.value = partnerCode.code;
|
||
const partnerClearLink = document.getElementById("partner-code-clear");
|
||
if (partnerClearLink) partnerClearLink.addEventListener("click", (e) => { e.preventDefault(); clearPartnerCode(); render(); });
|
||
} else {
|
||
partnerDiscountRow.style.display = "none";
|
||
partnerStatus.textContent = partnerInput.dataset.invalid === "1" ? partnerCodeInvalid : "";
|
||
}
|
||
|
||
const loyaltyDiscountRow = document.getElementById("loyalty-discount-row");
|
||
const loyaltyDiscount = treuebonusDiscount();
|
||
if (loyaltyDiscountRow) {
|
||
if (loyaltyDiscount > 0) {
|
||
loyaltyDiscountRow.style.display = "flex";
|
||
document.getElementById("sum-loyalty-discount").textContent = "-" + formatPrice(loyaltyDiscount, cartLang);
|
||
} else {
|
||
loyaltyDiscountRow.style.display = "none";
|
||
}
|
||
}
|
||
|
||
const aboDiscountRow = document.getElementById("abo-discount-row");
|
||
const aboDiscountBetrag = aboDiscount(subtotal);
|
||
if (aboDiscountRow) {
|
||
if (aboDiscountBetrag > 0) {
|
||
aboDiscountRow.style.display = "flex";
|
||
document.getElementById("sum-abo-discount").textContent = "-" + formatPrice(aboDiscountBetrag, cartLang);
|
||
} else {
|
||
aboDiscountRow.style.display = "none";
|
||
}
|
||
}
|
||
|
||
itemsEl.querySelectorAll("button, a.remove").forEach((el) => {
|
||
el.addEventListener("click", (e) => {
|
||
e.preventDefault();
|
||
const target = e.currentTarget;
|
||
const slug = target.dataset.slug;
|
||
const action = target.dataset.action;
|
||
const item = getCart().find((i) => i.slug === slug);
|
||
if (!item) return;
|
||
// Nie mehr in den Warenkorb legen können, als tatsächlich auf Lager ist.
|
||
if (action === "inc") {
|
||
const produkt = getProduct(slug);
|
||
const max = produkt ? produkt.bestand : Infinity;
|
||
updateQuantity(slug, Math.min(item.menge + 1, max));
|
||
}
|
||
if (action === "dec") updateQuantity(slug, item.menge - 1);
|
||
if (action === "remove") removeFromCart(slug);
|
||
render();
|
||
});
|
||
});
|
||
|
||
itemsEl.querySelectorAll(".buy-toggle").forEach((el) => {
|
||
el.addEventListener("change", (e) => {
|
||
setAusgewaehlt(e.currentTarget.dataset.slug, e.currentTarget.checked);
|
||
render();
|
||
});
|
||
});
|
||
}
|
||
|
||
document.getElementById("coupon-apply").addEventListener("click", () => {
|
||
const input = document.getElementById("coupon-input");
|
||
const code = input.value.trim();
|
||
if (!code) { clearCoupon(); render(); return; }
|
||
setCouponCode(code);
|
||
input.dataset.invalid = appliedCoupon() ? "0" : "1";
|
||
render();
|
||
});
|
||
|
||
document.getElementById("partner-code-apply").addEventListener("click", () => {
|
||
const input = document.getElementById("partner-code-input");
|
||
const code = input.value.trim();
|
||
if (!code) { clearPartnerCode(); render(); return; }
|
||
setPartnerCode(code);
|
||
input.dataset.invalid = appliedPartnerCode() ? "0" : "1";
|
||
render();
|
||
});
|
||
|
||
render();
|
||
window.addEventListener("cart:changed", render);
|
||
// Treuebonus-Auswahl liegt im Konto, nicht im Warenkorb — eigenes Event, damit die Anzeige
|
||
// auch reagiert, wenn sich NUR das Konto ändert (z.B. Bonus wird woanders freigeschaltet).
|
||
window.addEventListener("account:changed", render);
|
||
landSelect.addEventListener("change", render);
|
||
</script>
|