"3. Bestellübersicht" stand bisher als eigener Block über die volle Formularbreite unterhalb beider Spalten. Jetzt steht sie direkt in der rechten Spalte, gestapelt unter "2. Kontakt & Lieferadresse" — beide Spalten (Zahlungsart links, Kontakt + Bestellübersicht rechts) füllen dadurch die gemeinsame Karte deutlich ausgewogener. Verifiziert per Puppeteer-Screenshot gegen den echten Build.
286 lines
15 KiB
Plaintext
286 lines
15 KiB
Plaintext
---
|
||
import Layout from "../../../layouts/Layout.astro";
|
||
import type { Locale } from "../../../i18n/config";
|
||
import { useTranslations } from "../../../i18n/ui";
|
||
|
||
const lang: Locale = "ch";
|
||
const t = useTranslations(lang);
|
||
---
|
||
<Layout title="Checkout" description="Bschtellig abschliesse bi Van's DIY & Bastelbedarf." lang={lang} path="/checkout/">
|
||
<section class="section-tight">
|
||
<div class="container">
|
||
<span class="eyebrow">{t.checkout.eyebrow}</span>
|
||
<h1>{t.checkout.title}</h1>
|
||
<p class="todo-note">{t.checkout.todoNote}</p>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="section-tight">
|
||
<div class="container checkout-layout">
|
||
<form class="frm card" id="checkout-form">
|
||
<div class="checkout-split">
|
||
<div class="checkout-split-col">
|
||
<h3>{t.checkout.step2}</h3>
|
||
<div class="payment-options">
|
||
<div class="pay-card" style="--brand-color:#009cde; --brand-bg:#003087;">
|
||
<label class="pay-option">
|
||
<span class="pay-icon" style="background:linear-gradient(160deg, #0070ba, #003087); color:#fff;">P</span>
|
||
<span>PayPal</span>
|
||
<input type="radio" name="pay" value="paypal" checked />
|
||
</label>
|
||
<div class="pay-panel-inline">
|
||
<p>{t.checkout.payPalHint}</p>
|
||
<div id="paypal-button-container"></div>
|
||
</div>
|
||
</div>
|
||
<div class="pay-card" style="--brand-color:#ffb3c7; --brand-bg:#17120f;">
|
||
<label class="pay-option">
|
||
<span class="pay-icon" style="background:linear-gradient(160deg, #ffc9d8, #ffb3c7); color:#0a0a0a;">K</span>
|
||
<span>Klarna</span>
|
||
<input type="radio" name="pay" value="klarna" />
|
||
</label>
|
||
<div class="pay-panel-inline">
|
||
<p>{t.checkout.klarnaHint}</p>
|
||
<button type="button" class="btn btn-outline pay-connect-btn" id="klarna-connect">{t.checkout.klarnaConnect}</button>
|
||
<p class="pay-connect-status" id="klarna-status"></p>
|
||
</div>
|
||
</div>
|
||
<div class="pay-card" style="--brand-color:#ff9838; --brand-bg:#241f3d;">
|
||
<label class="pay-option">
|
||
<span class="pay-icon pay-icon-cc" style="background:linear-gradient(160deg, #2a2a3d, #14141f);">
|
||
<span class="cc-dot cc-dot-a"></span><span class="cc-dot cc-dot-b"></span>
|
||
</span>
|
||
<span>{t.checkout.creditCard}<span class="pay-sub">{t.checkout.creditCardSub}</span></span>
|
||
<input type="radio" name="pay" value="kreditkarte" />
|
||
</label>
|
||
<div class="pay-panel-inline">
|
||
<p>{t.checkout.cardHint}</p>
|
||
<div class="card-fields">
|
||
<div>
|
||
<label for="cc-number">{t.checkout.cardNumber}</label>
|
||
<input type="text" id="cc-number" inputmode="numeric" placeholder="1234 5678 9012 3456" maxlength="19" />
|
||
</div>
|
||
<div class="grid grid-2">
|
||
<div><label for="cc-expiry">{t.checkout.cardExpiry}</label><input type="text" id="cc-expiry" placeholder="MM/YY" maxlength="5" /></div>
|
||
<div><label for="cc-cvc">{t.checkout.cardCvc}</label><input type="text" id="cc-cvc" inputmode="numeric" placeholder="123" maxlength="4" /></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="pay-card" style="--brand-color:#7fa8c9; --brand-bg:#3b5a76;">
|
||
<label class="pay-option">
|
||
<span class="pay-icon pay-icon-emoji" style="background:linear-gradient(160deg, #4d7599, #2c4258);">🏦</span>
|
||
<span>{t.checkout.bankTransfer}</span>
|
||
<input type="radio" name="pay" value="ueberweisung" />
|
||
</label>
|
||
<div class="pay-panel-inline">
|
||
<p>{t.checkout.bankHintReal}</p>
|
||
<div class="bank-details" id="bank-details"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
</div>
|
||
<div class="checkout-split-divider" aria-hidden="true"><span>🧵</span></div>
|
||
<div class="checkout-split-col">
|
||
<h3>{t.checkout.step1}</h3>
|
||
<div><label for="email">{t.checkout.email}</label><input id="email" type="email" required /></div>
|
||
<div class="grid grid-2">
|
||
<div><label for="vorname">{t.checkout.firstName}</label><input id="vorname" type="text" required /></div>
|
||
<div><label for="nachname">{t.checkout.lastName}</label><input id="nachname" type="text" required /></div>
|
||
</div>
|
||
<div><label for="strasse">{t.checkout.street}</label><input id="strasse" type="text" placeholder="Bahnhofstrasse 1" required /></div>
|
||
<div class="grid grid-2">
|
||
<div><label for="plz">{t.checkout.zip}</label><input id="plz" type="text" inputmode="numeric" maxlength="4" placeholder="8000" required /></div>
|
||
<div><label for="ort">{t.checkout.city}</label><input id="ort" type="text" placeholder="Zürich" required /></div>
|
||
</div>
|
||
<div>
|
||
<label for="land">{t.checkout.country}</label>
|
||
<select id="land">
|
||
<option value="ch" selected>Schwiz</option>
|
||
<option value="de">Dütschland</option>
|
||
<option value="at">Öschterrych</option>
|
||
<option value="lu">Luxemburg</option>
|
||
</select>
|
||
</div>
|
||
|
||
<hr class="divider" />
|
||
<h3>{t.checkout.step3}</h3>
|
||
<div id="checkout-summary" class="checkout-summary"></div>
|
||
<p class="small">{t.common.vatNote}</p>
|
||
|
||
</div>
|
||
</div>
|
||
|
||
<div class="checkbox-row">
|
||
<input id="agb" type="checkbox" required />
|
||
<label for="agb">{t.checkout.agbPrefix} <a href="/agb/">{t.checkout.agbLink}</a> {t.checkout.agbSuffix}</label>
|
||
</div>
|
||
<div class="checkbox-row">
|
||
<input id="widerruf" type="checkbox" required />
|
||
<label for="widerruf">{t.checkout.revocationPrefix} <a href="/widerruf/">{t.checkout.revocationLink}</a> {t.checkout.revocationAnd} <a href="/datenschutz/">{t.checkout.privacyLink}</a> {t.checkout.revocationSuffix}</label>
|
||
</div>
|
||
|
||
<button class="btn btn-primary btn-block" type="submit">{t.checkout.orderButton}</button>
|
||
</form>
|
||
|
||
<aside class="card checkout-aside">
|
||
<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>
|
||
<hr class="divider stitched" />
|
||
<h3>🚚 {t.checkout.shippingHintTitle}</h3>
|
||
<p class="small">{t.checkout.shippingHint}</p>
|
||
<a class="small" href="/ch/versand-zahlung/">{t.checkout.shippingLink}</a>
|
||
</aside>
|
||
</div>
|
||
</section>
|
||
</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, discountLabel, couponInvalid, couponAppliedTemplate, couponRemoveLabel, klarnaConnectLabel, klarnaConnectingLabel, klarnaConnectedLabel, bankHolderLabel, bankIbanLabel, bankBicLabel, bankBankNameLabel, bankPlaceholderLabel };
|
||
</script>
|
||
<script type="module">
|
||
import { getCart, cartTotal, appliedCoupon, couponDiscount, setCouponCode, clearCoupon, istAusgewaehlt } from "../../../scripts/cart";
|
||
import { formatPrice } from "../../../i18n/format";
|
||
import { products } from "../../../data/products";
|
||
import { berechneVersandkosten } from "../../../data/versand";
|
||
import { effektiverZeilenpreis } from "../../../data/rabatt";
|
||
import { bankverbindung, bankverbindungVollstaendig } from "../../../data/bankverbindung";
|
||
|
||
const { freeLabel, totalLabel, shippingLabel, emptyCartAlert, thankYouPath, checkoutLang, discountLabel, couponInvalid, couponAppliedTemplate, couponRemoveLabel, klarnaConnectLabel, klarnaConnectingLabel, klarnaConnectedLabel, bankHolderLabel, bankIbanLabel, bankBicLabel, bankBankNameLabel, bankPlaceholderLabel } = window.__checkoutVars;
|
||
|
||
const summary = document.getElementById("checkout-summary");
|
||
const landSelect = document.getElementById("land");
|
||
// Nur die im Warenkorb ausgewählten Artikel werden hier bestellt — abgewählte bleiben im
|
||
// Warenkorb liegen und tauchen im Checkout gar nicht erst auf.
|
||
const cart = getCart().filter(istAusgewaehlt);
|
||
const subtotal = cartTotal();
|
||
let currentTotal = 0;
|
||
|
||
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);
|
||
const coupon = appliedCoupon();
|
||
const discount = couponDiscount(subtotal);
|
||
const total = Math.max(0, subtotal - discount) + shipping;
|
||
currentTotal = total;
|
||
|
||
summary.innerHTML = `
|
||
${cart.map((i) => {
|
||
const produkt = products.find((p) => p.slug === i.slug);
|
||
const zeilenpreis = produkt ? effektiverZeilenpreis(produkt, i.menge) : i.menge * i.preis;
|
||
return `<div class="row"><span>${i.menge}× ${i.name}</span><span>${formatPrice(zeilenpreis, checkoutLang)}</span></div>`;
|
||
}).join("")}
|
||
${coupon && discount > 0 ? `<div class="row"><span>${discountLabel}</span><span>-${formatPrice(discount, checkoutLang)}</span></div>` : ""}
|
||
<div class="row"><span>${shippingLabel}</span><span>${shipping === 0 ? freeLabel : formatPrice(shipping, checkoutLang)}</span></div>
|
||
<div class="row total"><span>${totalLabel}</span><span>${formatPrice(total, checkoutLang)}</span></div>
|
||
`;
|
||
|
||
const couponStatus = document.getElementById("coupon-status");
|
||
const couponInput = document.getElementById("coupon-input");
|
||
if (coupon && discount > 0) {
|
||
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(); renderSummary(); });
|
||
} else {
|
||
couponStatus.textContent = couponInput.dataset.invalid === "1" ? couponInvalid : "";
|
||
}
|
||
}
|
||
|
||
renderSummary();
|
||
landSelect.addEventListener("change", renderSummary);
|
||
|
||
// Zahlungsart-Auswahl: der "Verbinden"-Bereich jeder Karte klappt rein über CSS
|
||
// (:has(input:checked)) sofort auf, sobald man die Karte anklickt — kein JS, keine Verzögerung.
|
||
|
||
// PayPal: echte PayPal Smart Buttons über das offizielle JS-SDK (Sandbox-Client-ID "sb" —
|
||
// kostenlos, ohne eigenes Konto testbar). Klick öffnet den echten PayPal-Login-Popup. Für den
|
||
// Live-Betrieb muss hier nur "sb" durch VanVans echte (kostenlose) PayPal-Business-Client-ID
|
||
// ersetzt werden, siehe developer.paypal.com.
|
||
let paypalLoaded = false;
|
||
function loadPayPalButtons() {
|
||
if (paypalLoaded || !document.getElementById("paypal-button-container")) return;
|
||
paypalLoaded = true;
|
||
const script = document.createElement("script");
|
||
script.src = "https://www.paypal.com/sdk/js?client-id=sb¤cy=EUR&intent=capture&disable-funding=card,credit";
|
||
script.onload = () => {
|
||
if (!window.paypal) return;
|
||
window.paypal.Buttons({
|
||
style: { layout: "vertical", color: "blue", shape: "pill", label: "paypal" },
|
||
createOrder: (data, actions) => actions.order.create({
|
||
purchase_units: [{ amount: { value: Math.max(0.01, currentTotal).toFixed(2), currency_code: "EUR" } }],
|
||
}),
|
||
onApprove: (data, actions) => actions.order.capture().then(() => { location.href = thankYouPath; }),
|
||
}).render("#paypal-button-container");
|
||
};
|
||
document.head.appendChild(script);
|
||
}
|
||
loadPayPalButtons();
|
||
|
||
// Klarna: echtes "Verbinden" braucht eine serverseitige Session mit VanVans Klarna-Händlerkonto
|
||
// (Phase 2, Cloudflare Worker). Bis dahin ein sauber gestalteter Demo-Verbindungsablauf, damit
|
||
// die Oberfläche schon fertig aussieht und 1:1 austauschbar ist, sobald die Anbindung steht.
|
||
const klarnaBtn = document.getElementById("klarna-connect");
|
||
const klarnaStatus = document.getElementById("klarna-status");
|
||
function connectKlarna() {
|
||
if (!klarnaBtn || klarnaBtn.disabled) return;
|
||
klarnaBtn.disabled = true;
|
||
klarnaBtn.textContent = klarnaConnectingLabel;
|
||
setTimeout(() => {
|
||
klarnaBtn.textContent = klarnaConnectLabel;
|
||
klarnaBtn.disabled = false;
|
||
klarnaStatus.textContent = "✅ " + klarnaConnectedLabel;
|
||
}, 1100);
|
||
}
|
||
if (klarnaBtn) {
|
||
klarnaBtn.addEventListener("click", connectKlarna);
|
||
const klarnaRadio = document.querySelector('input[name="pay"][value="klarna"]');
|
||
// Verbindet automatisch, sobald die Klarna-Karte ausgewählt wird — kein separater Klick
|
||
// auf den Button nötig (der bleibt als "erneut verbinden" erhalten).
|
||
klarnaRadio?.addEventListener("change", () => { if (!klarnaStatus.textContent) connectKlarna(); });
|
||
}
|
||
|
||
// Banküberweisung (Vorkasse): braucht KEINE externe Anbindung — zeigt einfach VanVans echte,
|
||
// im Adminbereich hinterlegte Kontodaten an. Das ist die einzige der vier Zahlungsarten, die
|
||
// schon zu 100% "fertig" funktioniert.
|
||
const bankDetailsEl = document.getElementById("bank-details");
|
||
if (bankDetailsEl) {
|
||
if (bankverbindungVollstaendig(bankverbindung)) {
|
||
bankDetailsEl.innerHTML = `
|
||
<div class="bank-row"><span>${bankHolderLabel}</span><span>${bankverbindung.kontoinhaber}</span></div>
|
||
<div class="bank-row"><span>${bankIbanLabel}</span><span>${bankverbindung.iban}</span></div>
|
||
${bankverbindung.bic ? `<div class="bank-row"><span>${bankBicLabel}</span><span>${bankverbindung.bic}</span></div>` : ""}
|
||
${bankverbindung.bank ? `<div class="bank-row"><span>${bankBankNameLabel}</span><span>${bankverbindung.bank}</span></div>` : ""}
|
||
`;
|
||
} else {
|
||
bankDetailsEl.innerHTML = `<p class="small">${bankPlaceholderLabel}</p>`;
|
||
}
|
||
}
|
||
|
||
document.getElementById("coupon-apply").addEventListener("click", () => {
|
||
const input = document.getElementById("coupon-input");
|
||
const code = input.value.trim();
|
||
if (!code) { clearCoupon(); renderSummary(); return; }
|
||
setCouponCode(code);
|
||
input.dataset.invalid = appliedCoupon() ? "0" : "1";
|
||
renderSummary();
|
||
});
|
||
|
||
document.getElementById("checkout-form").addEventListener("submit", (e) => {
|
||
e.preventDefault();
|
||
if (cart.length === 0) {
|
||
alert(emptyCartAlert);
|
||
return;
|
||
}
|
||
location.href = thankYouPath;
|
||
});
|
||
</script>
|