- Zahlungsart-Auswahl umgebaut: jede Karte (PayPal/Klarna/Kreditkarte/
Banküberweisung) klappt den passenden "Verbinden"-Bereich jetzt SOFORT
in sich selbst auf, sobald man draufklickt — kein Zwischenschritt, kein
separates Panel weiter unten mehr. Rein CSS-gesteuert (:has()), reagiert
augenblicklich auf den Klick.
- Unterkategorie-Vorstellungsbox (z.B. HasiDog) zeigt jetzt wieder den
persönlichen Charaktertext ("Ich bin klein, knuffig, wuschelig...")
statt der Produktbeschreibung, plus den Cliffhanger-Satz "Wenn du mehr
über mich erfahren möchtest, dann klicke" vor dem Button — für alle 6
Tier-Serien (Kuh, Hase, Wal, Oktopus, Schildkröte, Pinguin). Foto und
Überschrift sind jetzt anklickbar und führen zur echten Produktseite,
damit trotz des reinen Charaktertexts nichts an Kaufbarkeit verloren
geht. Textspalte gegenüber Fotospalte verbreitert.
Verifiziert per Puppeteer gegen den echten Build: PayPal-Karte zeigt beim
Laden sofort den echten SDK-Button, Kreditkarte/Banküberweisung klappen
unmittelbar beim Klick auf (ohne Wartezeit geprüft), Bankdaten und
Charaktertext korrekt, keine Konsolenfehler.
278 lines
14 KiB
Plaintext
278 lines
14 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="Checkout" description="Complete your order at 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="1 Example Street" required /></div>
|
||
<div class="grid grid-2">
|
||
<div><label for="plz">{t.checkout.zip}</label><input id="plz" type="text" inputmode="numeric" maxlength="8" placeholder="SW1A 1AA" required /></div>
|
||
<div><label for="ort">{t.checkout.city}</label><input id="ort" type="text" placeholder="London" required /></div>
|
||
</div>
|
||
<div>
|
||
<label for="land">{t.checkout.country}</label>
|
||
<select id="land">
|
||
<option value="de">Germany</option>
|
||
<option value="at">Austria</option>
|
||
<option value="ch">Switzerland</option>
|
||
<option value="lu">Luxembourg</option>
|
||
</select>
|
||
</div>
|
||
|
||
</div>
|
||
</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 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="/en/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: "/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, discountLabel, couponInvalid, couponAppliedTemplate, couponRemoveLabel, klarnaConnectLabel, klarnaConnectingLabel, klarnaConnectedLabel, bankHolderLabel, bankIbanLabel, bankBicLabel, bankBankNameLabel, bankPlaceholderLabel };
|
||
</script>
|
||
<script type="module">
|
||
import { getCart, cartTotal, appliedCoupon, couponDiscount, setCouponCode, clearCoupon } 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");
|
||
const cart = getCart();
|
||
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";
|
||
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");
|
||
if (klarnaBtn) {
|
||
klarnaBtn.addEventListener("click", () => {
|
||
klarnaBtn.disabled = true;
|
||
klarnaBtn.textContent = klarnaConnectingLabel;
|
||
setTimeout(() => {
|
||
klarnaBtn.textContent = klarnaConnectLabel;
|
||
klarnaBtn.disabled = false;
|
||
klarnaStatus.textContent = "✅ " + klarnaConnectedLabel;
|
||
}, 1100);
|
||
});
|
||
}
|
||
|
||
// 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>
|