Files
vans-diy-bastelbedarf/src/pages/fr/warenkorb/index.astro
T
qciganoandClaude Sonnet 5 6fc64e0972 Neu: Partner-/Influencer-Rabattcodes (Warenkorb, Checkout, Admin)
Neben dem allgemeinen Gutscheincode gibt es jetzt einen zweiten,
unabhängigen "Partner-Rabattcode"-Slot — für Codes, die VanVan gezielt
mit einzelnen Streamer:innen/Content-Creator:innen aushandelt. Kund:innen
lösen ihn genauso ein wie einen Gutschein (eigene, optisch identische Box
direkt unter dem Gutschein im Warenkorb bzw. im Checkout), beide Codes
können unabhängig voneinander gleichzeitig aktiv sein und ihre Rabatte
werden addiert (auf die Zwischensumme gedeckelt, damit der Preis nie
unter 0 € fällt).

Admin (Sveltia CMS): neuer Bereich "🤝 Partner-Rabattcodes", getrennt von
den allgemeinen Gutscheinen geführt. Pro Code lässt sich hinterlegen, zu
welcher Partnerin/welchem Partner er gehört (nur für VanVans eigene
Übersicht, wird Kund:innen nirgends gezeigt), Prozent- ODER Betrags-
Rabatt, optionales Ablaufdatum, ein "Aktiv"-Schalter zum Pausieren ohne
Löschen, sowie ein freies Notizfeld für die vereinbarten Konditionen
(z.B. Provision) — echtes automatisches Auswerten, wer wie oft welchen
Code einlöst, bräuchte ein eigenes Backend (Phase 2) und ist bewusst
nicht Teil dieser rein statischen Umsetzung.

Nebenbei einen bestehenden Bug behoben: die define:vars-Variablenliste
im Checkout-Skript war bei EN/CH/FR unvollständig (discountLabel,
couponInvalid, Klarna-/Bank-Texte fehlten) — dadurch wären dort Teile
der Checkout-Seitenleiste als "undefined" gerendert worden. Jetzt an
die vollständige Liste der DE-Version angeglichen.

Neu erstellte Dateien lokal mit Test-Codes durchgespielt (Gutschein +
Partnercode gleichzeitig eingelöst, Rabatte korrekt addiert, Admin-
Formular mit allen Feldern geprüft) und wieder auf leere Listen
zurückgesetzt vor dem Commit.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-08-02 23:28:01 +02:00

237 lines
13 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
import Layout from "../../../layouts/Layout.astro";
import type { Locale } from "../../../i18n/config";
import { useTranslations } from "../../../i18n/ui";
const lang: Locale = "fr";
const t = useTranslations(lang);
---
<Layout title="Panier" description="Votre panier chez 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="/fr/shop/">{t.common.browseShop}</a>
</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="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">Allemagne</option>
<option value="at">Autriche</option>
<option value="ch">Suisse</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="/fr/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 }}>
// 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 };
</script>
<script type="module">
import { getCart, updateQuantity, removeFromCart, cartTotal, appliedCoupon, couponDiscount, setCouponCode, clearCoupon, appliedPartnerCode, partnerCodeDiscount, setPartnerCode, clearPartnerCode, totalDiscount, istAusgewaehlt, setAusgewaehlt } from "../../../scripts/cart";
import { formatPrice } from "../../../i18n/format";
import { getProduct, products } from "../../../data/products";
import { berechneVersandkosten } from "../../../data/versand";
import { effektiverZeilenpreis, mengenrabattProzent } from "../../../data/rabatt";
const { freeShipFrom, removeLabel, perItemLabel, remainingTemplate, freeShippingText, freeLabel, cartLang, mengenrabattTemplate, couponInvalid, couponAppliedTemplate, couponRemoveLabel, buyNowLabel, keptInCartLabel, partnerCodeInvalid, partnerCodeAppliedTemplate, partnerCodeRemoveLabel } = 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 = produkt ? effektiverZeilenpreis(produkt, i.menge) : i.menge * i.preis;
const mengenProzent = produkt ? mengenrabattProzent(produkt, i.menge) : 0;
return { item: i, produkt, zeilenpreis, mengenProzent };
});
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>` : "";
return `
<div class="cart-item ${ausgewaehlt ? "" : "deselected"}">
<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>
<div class="small">${formatPrice(zeilenpreis / i.menge, cartLang)} ${perItemLabel}</div>
${mengenBadge}
${keptBadge}
</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("");
// 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 : "";
}
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);
landSelect.addEventListener("change", render);
</script>