Versandkosten-System (Pflichtenheft Punkt 9) + kritischen Warenkorb/Checkout-Bug behoben

Neu:
- Produkte können eigene Versandkosten je Land (DE/AT/CH/LU) bekommen, gelten nur bei
  Einzelkauf (genau 1 Stück von 1 Produkt)
- Versandkosten-Staffel nach Warenwert je Land, von VanVan im Adminbereich bearbeitbar
  (neuer Bereich "Versandkosten-Staffel"), greift bei mehreren Produkten/Stück
- Versandkostenfrei ab 75 € gilt immer, unabhängig von Einzelkauf/Staffel
- Checkout berechnet und zeigt jetzt die echten Versandkosten live beim Länderwechsel an

Kritischer Bugfix (unabhängig vom neuen Feature entdeckt):
- Warenkorb- und Checkout-Skripte haben "import" innerhalb von define:vars-Skripten benutzt,
  was Astro als IIFE rendert (kein ES-Modul) — das hat in der echten Live-Version die ganze
  Zeit zu "Cannot use import statement outside a module" geführt, wodurch Warenkorb und
  Checkout praktisch nie funktioniert haben. Behoben durch Trennung: define:vars-Skript setzt
  nur Werte auf window, separates <script type="module"> mit den echten Imports liest sie aus.
  Mit echtem End-to-End-Test verifiziert (Artikel in Warenkorb legen, Land wechseln, Summe
  prüfen) statt nur Screenshot.
This commit is contained in:
qcigano
2026-08-02 17:56:21 +02:00
parent 49cf12fd1c
commit 094edaaf39
12 changed files with 282 additions and 40 deletions
+26 -10
View File
@@ -32,10 +32,10 @@ const t = useTranslations(lang);
<div>
<label for="land">{t.checkout.country}</label>
<select id="land">
<option selected>Schwiz</option>
<option>Dütschland</option>
<option>Öschterrych</option>
<option>Luxemburg</option>
<option value="ch" selected>Schwiz</option>
<option value="de">Dütschland</option>
<option value="at">Öschterrych</option>
<option value="lu">Luxemburg</option>
</select>
</div>
@@ -74,19 +74,35 @@ const t = useTranslations(lang);
</Layout>
<script define:vars={{ freeLabel: t.checkout.free, totalLabel: t.checkout.total, shippingLabel: t.cart.shipping, emptyCartAlert: t.checkout.emptyCartAlert, thankYouPath: "/ch/checkout/danke/", checkoutLang: lang }}>
// WICHTIG: define:vars-Skripte laufen als IIFE, keine "import"-Anweisungen möglich.
window.__checkoutVars = { freeLabel, totalLabel, shippingLabel, emptyCartAlert, thankYouPath, checkoutLang };
</script>
<script type="module">
import { getCart, cartTotal } from "../../../scripts/cart";
import { formatPrice } from "../../../i18n/format";
import { products } from "../../../data/products";
import { berechneVersandkosten } from "../../../data/versand";
const { freeLabel, totalLabel, shippingLabel, emptyCartAlert, thankYouPath, checkoutLang } = window.__checkoutVars;
const summary = document.getElementById("checkout-summary");
const landSelect = document.getElementById("land");
const cart = getCart();
const subtotal = cartTotal();
const shipping = cart.length === 0 ? 0 : subtotal >= 75 ? 0 : 4.95;
summary.innerHTML = `
${cart.map((i) => `<div class="row"><span>${i.menge}× ${i.name}</span><span>${formatPrice(i.preis * i.menge, checkoutLang)}</span></div>`).join("")}
<div class="row"><span>${shippingLabel}</span><span>${shipping === 0 ? freeLabel : formatPrice(shipping, checkoutLang)}</span></div>
<div class="row total"><span>${totalLabel}</span><span>${formatPrice(subtotal + shipping, checkoutLang)}</span></div>
`;
function renderSummary() {
const land = landSelect.value;
const shipping = cart.length === 0 ? 0 : berechneVersandkosten(land, cart.map((i) => ({ slug: i.slug, preis: i.preis, menge: i.menge })), products);
summary.innerHTML = `
${cart.map((i) => `<div class="row"><span>${i.menge}× ${i.name}</span><span>${formatPrice(i.preis * i.menge, checkoutLang)}</span></div>`).join("")}
<div class="row"><span>${shippingLabel}</span><span>${shipping === 0 ? freeLabel : formatPrice(shipping, checkoutLang)}</span></div>
<div class="row total"><span>${totalLabel}</span><span>${formatPrice(subtotal + shipping, checkoutLang)}</span></div>
`;
}
renderSummary();
landSelect.addEventListener("change", renderSummary);
document.getElementById("checkout-form").addEventListener("submit", (e) => {
e.preventDefault();
+6
View File
@@ -39,9 +39,15 @@ const t = useTranslations(lang);
</Layout>
<script define:vars={{ freeShipFrom: 75, removeLabel: t.cart.remove, perItemLabel: t.cart.perItem, remainingTemplate: t.cart.remaining("__V__"), freeShippingText: t.cart.freeShipping, cartLang: lang }}>
// WICHTIG: define:vars-Skripte laufen als IIFE, keine "import"-Anweisungen möglich.
window.__cartVars = { freeShipFrom, removeLabel, perItemLabel, remainingTemplate, freeShippingText, cartLang };
</script>
<script type="module">
import { getCart, updateQuantity, removeFromCart, cartTotal } from "../../../scripts/cart";
import { formatPrice } from "../../../i18n/format";
const { freeShipFrom, removeLabel, perItemLabel, remainingTemplate, freeShippingText, cartLang } = window.__cartVars;
function render() {
const cart = getCart();
const empty = document.getElementById("cart-empty");
+29 -10
View File
@@ -32,10 +32,10 @@ const t = useTranslations(lang);
<div>
<label for="land">{t.checkout.country}</label>
<select id="land">
<option>Deutschland</option>
<option>Österreich</option>
<option>Schweiz</option>
<option>Luxemburg</option>
<option value="de">Deutschland</option>
<option value="at">Österreich</option>
<option value="ch">Schweiz</option>
<option value="lu">Luxemburg</option>
</select>
</div>
@@ -74,19 +74,38 @@ const t = useTranslations(lang);
</Layout>
<script define:vars={{ freeLabel: t.checkout.free, totalLabel: t.checkout.total, shippingLabel: t.cart.shipping, emptyCartAlert: t.checkout.emptyCartAlert, thankYouPath: "/checkout/danke/", checkoutLang: lang }}>
// WICHTIG: define:vars-Skripte werden von Astro in ein IIFE gepackt (kein ES-Modul) — echte
// `import`-Anweisungen würden hier zur Laufzeit mit "Cannot use import statement outside a
// module" fehlschlagen. Deshalb hier nur die vom Server gerenderten Werte auf window ablegen,
// die eigentliche Logik läuft im separaten <script type="module"> darunter.
window.__checkoutVars = { freeLabel, totalLabel, shippingLabel, emptyCartAlert, thankYouPath, checkoutLang };
</script>
<script type="module">
import { getCart, cartTotal } from "../../scripts/cart";
import { formatPrice } from "../../i18n/format";
import { products } from "../../data/products";
import { berechneVersandkosten } from "../../data/versand";
const { freeLabel, totalLabel, shippingLabel, emptyCartAlert, thankYouPath, checkoutLang } = window.__checkoutVars;
const summary = document.getElementById("checkout-summary");
const landSelect = document.getElementById("land");
const cart = getCart();
const subtotal = cartTotal();
const shipping = cart.length === 0 ? 0 : subtotal >= 75 ? 0 : 4.95;
summary.innerHTML = `
${cart.map((i) => `<div class="row"><span>${i.menge}× ${i.name}</span><span>${formatPrice(i.preis * i.menge, checkoutLang)}</span></div>`).join("")}
<div class="row"><span>${shippingLabel}</span><span>${shipping === 0 ? freeLabel : formatPrice(shipping, checkoutLang)}</span></div>
<div class="row total"><span>${totalLabel}</span><span>${formatPrice(subtotal + shipping, checkoutLang)}</span></div>
`;
function renderSummary() {
const land = landSelect.value;
const shipping = cart.length === 0 ? 0 : berechneVersandkosten(land, cart.map((i) => ({ slug: i.slug, preis: i.preis, menge: i.menge })), products);
summary.innerHTML = `
${cart.map((i) => `<div class="row"><span>${i.menge}× ${i.name}</span><span>${formatPrice(i.preis * i.menge, checkoutLang)}</span></div>`).join("")}
<div class="row"><span>${shippingLabel}</span><span>${shipping === 0 ? freeLabel : formatPrice(shipping, checkoutLang)}</span></div>
<div class="row total"><span>${totalLabel}</span><span>${formatPrice(subtotal + shipping, checkoutLang)}</span></div>
`;
}
renderSummary();
landSelect.addEventListener("change", renderSummary);
document.getElementById("checkout-form").addEventListener("submit", (e) => {
e.preventDefault();
+26 -10
View File
@@ -32,10 +32,10 @@ const t = useTranslations(lang);
<div>
<label for="land">{t.checkout.country}</label>
<select id="land">
<option>Germany</option>
<option>Austria</option>
<option>Switzerland</option>
<option>Luxembourg</option>
<option value="de">Germany</option>
<option value="at">Austria</option>
<option value="ch">Switzerland</option>
<option value="lu">Luxembourg</option>
</select>
</div>
@@ -74,19 +74,35 @@ const t = useTranslations(lang);
</Layout>
<script define:vars={{ freeLabel: t.checkout.free, totalLabel: t.checkout.total, shippingLabel: t.cart.shipping, emptyCartAlert: t.checkout.emptyCartAlert, thankYouPath: "/en/checkout/danke/", checkoutLang: lang }}>
// WICHTIG: define:vars-Skripte laufen als IIFE, keine "import"-Anweisungen möglich.
window.__checkoutVars = { freeLabel, totalLabel, shippingLabel, emptyCartAlert, thankYouPath, checkoutLang };
</script>
<script type="module">
import { getCart, cartTotal } from "../../../scripts/cart";
import { formatPrice } from "../../../i18n/format";
import { products } from "../../../data/products";
import { berechneVersandkosten } from "../../../data/versand";
const { freeLabel, totalLabel, shippingLabel, emptyCartAlert, thankYouPath, checkoutLang } = window.__checkoutVars;
const summary = document.getElementById("checkout-summary");
const landSelect = document.getElementById("land");
const cart = getCart();
const subtotal = cartTotal();
const shipping = cart.length === 0 ? 0 : subtotal >= 75 ? 0 : 4.95;
summary.innerHTML = `
${cart.map((i) => `<div class="row"><span>${i.menge}× ${i.name}</span><span>${formatPrice(i.preis * i.menge, checkoutLang)}</span></div>`).join("")}
<div class="row"><span>${shippingLabel}</span><span>${shipping === 0 ? freeLabel : formatPrice(shipping, checkoutLang)}</span></div>
<div class="row total"><span>${totalLabel}</span><span>${formatPrice(subtotal + shipping, checkoutLang)}</span></div>
`;
function renderSummary() {
const land = landSelect.value;
const shipping = cart.length === 0 ? 0 : berechneVersandkosten(land, cart.map((i) => ({ slug: i.slug, preis: i.preis, menge: i.menge })), products);
summary.innerHTML = `
${cart.map((i) => `<div class="row"><span>${i.menge}× ${i.name}</span><span>${formatPrice(i.preis * i.menge, checkoutLang)}</span></div>`).join("")}
<div class="row"><span>${shippingLabel}</span><span>${shipping === 0 ? freeLabel : formatPrice(shipping, checkoutLang)}</span></div>
<div class="row total"><span>${totalLabel}</span><span>${formatPrice(subtotal + shipping, checkoutLang)}</span></div>
`;
}
renderSummary();
landSelect.addEventListener("change", renderSummary);
document.getElementById("checkout-form").addEventListener("submit", (e) => {
e.preventDefault();
+6
View File
@@ -39,9 +39,15 @@ const t = useTranslations(lang);
</Layout>
<script define:vars={{ freeShipFrom: 75, removeLabel: t.cart.remove, perItemLabel: t.cart.perItem, remainingTemplate: t.cart.remaining("__V__"), freeShippingText: t.cart.freeShipping, cartLang: lang }}>
// WICHTIG: define:vars-Skripte laufen als IIFE, keine "import"-Anweisungen möglich.
window.__cartVars = { freeShipFrom, removeLabel, perItemLabel, remainingTemplate, freeShippingText, cartLang };
</script>
<script type="module">
import { getCart, updateQuantity, removeFromCart, cartTotal } from "../../../scripts/cart";
import { formatPrice } from "../../../i18n/format";
const { freeShipFrom, removeLabel, perItemLabel, remainingTemplate, freeShippingText, cartLang } = window.__cartVars;
function render() {
const cart = getCart();
const empty = document.getElementById("cart-empty");
+26 -10
View File
@@ -32,10 +32,10 @@ const t = useTranslations(lang);
<div>
<label for="land">{t.checkout.country}</label>
<select id="land">
<option>Allemagne</option>
<option>Autriche</option>
<option>Suisse</option>
<option>Luxembourg</option>
<option value="de">Allemagne</option>
<option value="at">Autriche</option>
<option value="ch">Suisse</option>
<option value="lu">Luxembourg</option>
</select>
</div>
@@ -74,19 +74,35 @@ const t = useTranslations(lang);
</Layout>
<script define:vars={{ freeLabel: t.checkout.free, totalLabel: t.checkout.total, shippingLabel: t.cart.shipping, emptyCartAlert: t.checkout.emptyCartAlert, thankYouPath: "/fr/checkout/danke/", checkoutLang: lang }}>
// WICHTIG: define:vars-Skripte laufen als IIFE, keine "import"-Anweisungen möglich.
window.__checkoutVars = { freeLabel, totalLabel, shippingLabel, emptyCartAlert, thankYouPath, checkoutLang };
</script>
<script type="module">
import { getCart, cartTotal } from "../../../scripts/cart";
import { formatPrice } from "../../../i18n/format";
import { products } from "../../../data/products";
import { berechneVersandkosten } from "../../../data/versand";
const { freeLabel, totalLabel, shippingLabel, emptyCartAlert, thankYouPath, checkoutLang } = window.__checkoutVars;
const summary = document.getElementById("checkout-summary");
const landSelect = document.getElementById("land");
const cart = getCart();
const subtotal = cartTotal();
const shipping = cart.length === 0 ? 0 : subtotal >= 75 ? 0 : 4.95;
summary.innerHTML = `
${cart.map((i) => `<div class="row"><span>${i.menge}× ${i.name}</span><span>${formatPrice(i.preis * i.menge, checkoutLang)}</span></div>`).join("")}
<div class="row"><span>${shippingLabel}</span><span>${shipping === 0 ? freeLabel : formatPrice(shipping, checkoutLang)}</span></div>
<div class="row total"><span>${totalLabel}</span><span>${formatPrice(subtotal + shipping, checkoutLang)}</span></div>
`;
function renderSummary() {
const land = landSelect.value;
const shipping = cart.length === 0 ? 0 : berechneVersandkosten(land, cart.map((i) => ({ slug: i.slug, preis: i.preis, menge: i.menge })), products);
summary.innerHTML = `
${cart.map((i) => `<div class="row"><span>${i.menge}× ${i.name}</span><span>${formatPrice(i.preis * i.menge, checkoutLang)}</span></div>`).join("")}
<div class="row"><span>${shippingLabel}</span><span>${shipping === 0 ? freeLabel : formatPrice(shipping, checkoutLang)}</span></div>
<div class="row total"><span>${totalLabel}</span><span>${formatPrice(subtotal + shipping, checkoutLang)}</span></div>
`;
}
renderSummary();
landSelect.addEventListener("change", renderSummary);
document.getElementById("checkout-form").addEventListener("submit", (e) => {
e.preventDefault();
+6
View File
@@ -39,9 +39,15 @@ const t = useTranslations(lang);
</Layout>
<script define:vars={{ freeShipFrom: 75, removeLabel: t.cart.remove, perItemLabel: t.cart.perItem, remainingTemplate: t.cart.remaining("__V__"), freeShippingText: t.cart.freeShipping, cartLang: lang }}>
// WICHTIG: define:vars-Skripte laufen als IIFE, keine "import"-Anweisungen möglich.
window.__cartVars = { freeShipFrom, removeLabel, perItemLabel, remainingTemplate, freeShippingText, cartLang };
</script>
<script type="module">
import { getCart, updateQuantity, removeFromCart, cartTotal } from "../../../scripts/cart";
import { formatPrice } from "../../../i18n/format";
const { freeShipFrom, removeLabel, perItemLabel, remainingTemplate, freeShippingText, cartLang } = window.__cartVars;
function render() {
const cart = getCart();
const empty = document.getElementById("cart-empty");
+6
View File
@@ -39,9 +39,15 @@ const t = useTranslations(lang);
</Layout>
<script define:vars={{ freeShipFrom: 75, removeLabel: t.cart.remove, perItemLabel: t.cart.perItem, remainingTemplate: t.cart.remaining("__V__"), freeShippingText: t.cart.freeShipping, cartLang: lang }}>
// WICHTIG: define:vars-Skripte laufen als IIFE, keine "import"-Anweisungen möglich.
window.__cartVars = { freeShipFrom, removeLabel, perItemLabel, remainingTemplate, freeShippingText, cartLang };
</script>
<script type="module">
import { getCart, updateQuantity, removeFromCart, cartTotal } from "../../scripts/cart";
import { formatPrice } from "../../i18n/format";
const { freeShipFrom, removeLabel, perItemLabel, remainingTemplate, freeShippingText, cartLang } = window.__cartVars;
function render() {
const cart = getCart();
const empty = document.getElementById("cart-empty");