From 6f850181f1e8c8918b6ecfc5a5a25fac4fcf50ff Mon Sep 17 00:00:00 2001 From: qcigano Date: Sun, 2 Aug 2026 18:59:11 +0200 Subject: [PATCH] =?UTF-8?q?Rabatt-System,=20gr=C3=B6=C3=9Ferer=20Mengen-St?= =?UTF-8?q?epper,=20Zahlungsart=20zuerst,=20echte=20HasiDog-Inhalte?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Neues Rabatt-System: Prozent-/Betrag-Rabatt mit optionalem Ablaufdatum, Mengenrabatt-Staffel pro Produkt, sowie site-weite Gutscheincodes. Wird live im Warenkorb/Checkout berechnet (src/data/rabatt.ts, src/data/gutscheine.ts, cart.ts) und ist im Adminbereich direkt unter dem Preis bearbeitbar (config.yml: "Rabatte" + neue Collection "Gutscheincodes"). Effektiver Preis + Rabatt-Badge auf Produktkarten und Produktseite sichtbar. - Mengen-Stepper auf der Produktseite vergrößert (bereits vorbereitet, jetzt mit Build/Test verifiziert). - Zahlungsart im Checkout auf Platz 1, mit markenfarbigen, fertig gestalteten Buttons (PayPal/Klarna/Banküberweisung). - Unterkategorie-Seiten zeigen jetzt VanVans eigene Produktbeschreibung und das echte Fotos des Produkts statt Platzhaltertext, mit kleinem Link zur HasiDog-Universum-Seite statt großem Button. Verifiziert per Puppeteer-Funktionstest gegen den echten Production-Build (Rabatt-Berechnung, Mengenrabatt, Gutschein-Einlösung, Checkout-Summary, keine Konsolenfehler). --- public/admin/config.yml | 39 +++++++ src/components/ProductCard.astro | 9 +- src/content/gutscheine.json | 3 + src/data/gutscheine.ts | 36 ++++++ src/data/products.ts | 13 +++ src/data/rabatt.ts | 71 ++++++++++++ src/i18n/ui.ts | 40 +++++-- src/pages/ch/checkout/index.astro | 88 ++++++++++----- src/pages/ch/produkt/[slug].astro | 28 ++++- .../shop/[kategorie]/[unterkategorie].astro | 63 +++++++++-- src/pages/ch/warenkorb/index.astro | 55 +++++++-- src/pages/checkout/index.astro | 90 ++++++++++----- src/pages/en/checkout/index.astro | 88 ++++++++++----- src/pages/en/produkt/[slug].astro | 28 ++++- .../shop/[kategorie]/[unterkategorie].astro | 61 ++++++++-- src/pages/en/warenkorb/index.astro | 55 +++++++-- src/pages/fr/checkout/index.astro | 88 ++++++++++----- src/pages/fr/produkt/[slug].astro | 28 ++++- .../shop/[kategorie]/[unterkategorie].astro | 61 ++++++++-- src/pages/fr/warenkorb/index.astro | 55 +++++++-- src/pages/produkt/[slug].astro | 28 ++++- .../shop/[kategorie]/[unterkategorie].astro | 64 +++++++++-- src/pages/warenkorb/index.astro | 55 +++++++-- src/scripts/cart.ts | 42 ++++++- src/styles/global.css | 104 ++++++++++++++++-- 25 files changed, 1098 insertions(+), 194 deletions(-) create mode 100644 src/content/gutscheine.json create mode 100644 src/data/gutscheine.ts create mode 100644 src/data/rabatt.ts diff --git a/public/admin/config.yml b/public/admin/config.yml index 815a493..b4c2d70 100644 --- a/public/admin/config.yml +++ b/public/admin/config.yml @@ -63,6 +63,24 @@ collections: step: 0.01 required: false hint: "Hier den ALTEN, höheren Preis eintragen — er wird dann durchgestrichen neben dem neuen Preis angezeigt. Leer lassen, wenn kein Sale." + - label: "🏷️ Rabatte (optional — alles leer lassen, wenn kein automatischer Rabatt gelten soll)" + name: rabatt + widget: object + required: false + hint: "Zusätzlich zum 'Reduzierten Preis' oben kannst du hier einen automatischen Rabatt einrichten, der von selbst wieder abläuft, sowie einen Mengenrabatt (z.B. ab 3 Stück günstiger)." + fields: + - { label: "Rabatt in Prozent (z.B. 20 für 20%)", name: prozent, widget: number, value_type: int, required: false, hint: "Nur ausfüllen ODER den Betrag unten — nicht beides." } + - { label: "ODER: Rabatt als fester Betrag (€)", name: betrag, widget: number, value_type: float, step: 0.01, required: false } + - { label: "Rabatt gültig bis (leer = läuft nicht automatisch ab)", name: bis, widget: date, format: "YYYY-MM-DD", required: false, hint: "Nach diesem Datum verschwindet der Rabatt automatisch — ohne dass du etwas tun musst." } + - label: "Mengenrabatt-Staffel (z.B. ab 3 Stück 10% Rabatt)" + name: mengenrabatt + widget: list + required: false + summary: "ab {{fields.abMenge}} Stück → -{{fields.prozent}}%" + hint: "Gilt automatisch im Warenkorb, wenn jemand mehrere Stück dieses Produkts kauft." + fields: + - { label: "Ab wie vielen Stück", name: abMenge, widget: number, value_type: int } + - { label: "Rabatt in Prozent", name: prozent, widget: number, value_type: int } - label: "Auf Lager" name: bestand widget: number @@ -214,3 +232,24 @@ collections: fields: - { label: "Ab Warenwert (€)", name: abWarenwert, widget: number, value_type: float, step: 0.01 } - { label: "Versandkosten (€)", name: kosten, widget: number, value_type: float, step: 0.01 } + + - name: gutscheine + label: "🎟️ Gutscheincodes" + icon: local_offer + files: + - name: codes + label: "Gutscheincodes" + file: src/content/gutscheine.json + description: > + Codes, die Kund:innen im Warenkorb/an der Kasse eingeben können, um Rabatt auf den + gesamten Warenwert zu bekommen (z.B. für Aktionen, Newsletter, Stammkund:innen). + fields: + - label: Gutscheincodes + name: codes + widget: list + summary: "{{fields.code}}" + fields: + - { label: "Code (z.B. WILLKOMMEN10)", name: code, widget: string, hint: "Groß-/Kleinschreibung ist egal — wird automatisch angepasst." } + - { label: "Rabatt in Prozent (z.B. 10 für 10%)", name: prozent, widget: number, value_type: int, required: false } + - { label: "ODER: Rabatt als fester Betrag (€)", name: betrag, widget: number, value_type: float, step: 0.01, required: false } + - { label: "Gültig bis (leer = unbegrenzt)", name: bis, widget: date, format: "YYYY-MM-DD", required: false } diff --git a/src/components/ProductCard.astro b/src/components/ProductCard.astro index df5c36a..5e460ff 100644 --- a/src/components/ProductCard.astro +++ b/src/components/ProductCard.astro @@ -4,10 +4,14 @@ import type { Locale } from "../i18n/config"; import { localePrefix } from "../i18n/config"; import { useTranslations } from "../i18n/ui"; import { formatPrice } from "../i18n/format"; +import { effektiverPreis, rabattLabel, hatAktivenRabatt } from "../data/rabatt"; interface Props { product: Product; lang: Locale } const { product, lang } = Astro.props; const t = useTranslations(lang); +const finalPrice = effektiverPreis(product); +const discountBadge = rabattLabel(product); +const showOldAsPreis = !product.preisAlt && hatAktivenRabatt(product); const badgeLabel: Record = { neu: t.badge.neu, @@ -32,13 +36,15 @@ const desc = product.beschreibung[lang]; {badgeLabel[b]} ))} {product.bestand === 0 && {t.badge.ausverkauft}} + {discountBadge && {discountBadge}}

{name}

{desc.slice(0, 78)}{desc.length > 78 ? "…" : ""}

{product.preisAlt && {formatPrice(product.preisAlt, lang)}} - {formatPrice(product.preis, lang)} + {showOldAsPreis && {formatPrice(product.preis, lang)}} + {formatPrice(finalPrice, lang)}
@@ -53,4 +59,5 @@ const desc = product.beschreibung[lang]; .price-row { margin-top: auto; display: flex; align-items: baseline; gap: 0.6rem; } .price { color: var(--c-accent); font-weight: 700; font-size: 1.15rem; } .price-old { color: var(--c-text-muted); text-decoration: line-through; font-size: 0.9rem; } + .badge-rabatt { background: #ff5f7e; color: #fff; border-color: #ff5f7e; } diff --git a/src/content/gutscheine.json b/src/content/gutscheine.json new file mode 100644 index 0000000..dec17ab --- /dev/null +++ b/src/content/gutscheine.json @@ -0,0 +1,3 @@ +{ + "codes": [] +} diff --git a/src/data/gutscheine.ts b/src/data/gutscheine.ts new file mode 100644 index 0000000..bbdb625 --- /dev/null +++ b/src/data/gutscheine.ts @@ -0,0 +1,36 @@ +// Gutscheincodes — site-weit gültig, von VanVan im Adminbereich unter "🎟️ Gutscheincodes" +// verwaltet. Werden im Warenkorb/Checkout eingelöst und auf den Gesamt-Warenwert angewendet. + +// @ts-ignore -- JSON-Import, zur Build-Zeit von Astro/Vite aufgelöst +import gutscheineRaw from "../content/gutscheine.json"; + +export interface Gutschein { + code: string; + prozent?: number; + betrag?: number; + bis?: string; // ISO-Datum, optional +} + +const gutscheine = (gutscheineRaw as { codes: Gutschein[] }).codes ?? []; + +function istGueltig(bis?: string): boolean { + if (!bis) return true; + return new Date() <= new Date(`${bis}T23:59:59`); +} + +/** Sucht einen Gutschein anhand des eingegebenen Codes (Groß-/Kleinschreibung egal). */ +export function findeGutschein(code: string): Gutschein | undefined { + const normalisiert = code.trim().toUpperCase(); + return gutscheine.find((g) => g.code.toUpperCase() === normalisiert && istGueltig(g.bis)); +} + +/** Berechnet den Rabatt-Betrag in Euro für einen gegebenen Warenwert. */ +export function gutscheinRabatt(gutschein: Gutschein, warenwert: number): number { + if (typeof gutschein.prozent === "number" && gutschein.prozent > 0) { + return Math.min(warenwert, warenwert * (gutschein.prozent / 100)); + } + if (typeof gutschein.betrag === "number" && gutschein.betrag > 0) { + return Math.min(warenwert, gutschein.betrag); + } + return 0; +} diff --git a/src/data/products.ts b/src/data/products.ts index 7887e47..34b1762 100644 --- a/src/data/products.ts +++ b/src/data/products.ts @@ -34,6 +34,15 @@ export interface Product { // Versandkosten-Staffel (siehe src/data/versand.ts). Optional, damit Bestandsprodukte ohne // diese Angabe nicht kaputtgehen — dann greift automatisch die Staffel als Rückfalllösung. versand?: { de?: number; at?: number; ch?: number; lu?: number }; + // Rabatte — mehrere Arten gleichzeitig möglich (siehe src/data/rabatt.ts für die Logik): + // 1. Prozent/Betrag-Rabatt, optional zeitlich befristet (läuft automatisch ab). + // 2. Mengenrabatt-Staffel (ab X Stück Y% Rabatt), wird im Warenkorb automatisch angewendet. + rabatt?: { + prozent?: number; // z.B. 20 = 20% Rabatt auf den Preis + betrag?: number; // Alternative zu prozent: fester Euro-Betrag Rabatt + bis?: string; // ISO-Datum, z.B. "2026-12-31" — Rabatt gilt nur bis einschließlich diesem Tag + mengenrabatt?: { abMenge: number; prozent: number }[]; + }; } const modules = import.meta.glob("/src/content/products/*.json", { eager: true }) as Record< @@ -53,6 +62,10 @@ export function productsByCategory(kategorie: string): Product[] { return products.filter((p) => p.kategorie === kategorie); } +export function productsByUnterkategorie(kategorie: string, unterkategorie: string): Product[] { + return products.filter((p) => p.kategorie === kategorie && p.unterkategorie === unterkategorie); +} + export function saleProducts(): Product[] { return products.filter((p) => p.badges.includes("sale") || p.preisAlt); } diff --git a/src/data/rabatt.ts b/src/data/rabatt.ts new file mode 100644 index 0000000..9010725 --- /dev/null +++ b/src/data/rabatt.ts @@ -0,0 +1,71 @@ +// Rabatt-Logik. Unterstützt "alle Art von Rabatten" (Wunsch vom 02.08.2026): +// 1. Klassischer Sale-Preis (bereits vorhanden: `preis`/`preisAlt`, manuell gesetzt). +// 2. Prozentualer oder fester Euro-Rabatt am Produkt, automatisch berechnet, optional +// zeitlich befristet (läuft am Stichtag von selbst wieder ab, ohne dass VanVan etwas +// manuell zurückstellen muss). +// 3. Mengenrabatt-Staffel (z.B. "ab 3 Stück 10% Rabatt") — wird im Warenkorb automatisch +// auf die Zeile angewendet. +// 4. Gutscheincodes (site-weit, siehe src/data/gutscheine.ts) — im Warenkorb/Checkout einlösbar. + +import type { Product } from "./products"; + +/** Prüft, ob ein befristeter Rabatt heute noch gültig ist (Vergleich läuft im Browser zur + * echten aktuellen Uhrzeit, nicht nur zur Build-Zeit — der Rabatt läuft also wirklich + * automatisch ab, auch ohne neuen Deploy). */ +export function istRabattAktiv(bis?: string): boolean { + if (!bis) return true; + const heute = new Date(); + const ende = new Date(`${bis}T23:59:59`); + return heute <= ende; +} + +/** Berechnet den effektiven Verkaufspreis eines Produkts unter Berücksichtigung von + * Prozent-/Betrag-Rabatt (falls aktiv). Reiner Sale-Preis über `preisAlt` bleibt unabhängig + * davon bestehen und wird hier NICHT verändert — beide Mechanismen sind unabhängig + * kombinierbar (z.B. Sale-Preis als Basis, zusätzlich zeitlich befristete Rabattaktion oben drauf). */ +export function effektiverPreis(product: Product): number { + const basis = product.preis; + const r = product.rabatt; + if (!r || !istRabattAktiv(r.bis)) return basis; + if (typeof r.prozent === "number" && r.prozent > 0) { + return Math.max(0, basis * (1 - r.prozent / 100)); + } + if (typeof r.betrag === "number" && r.betrag > 0) { + return Math.max(0, basis - r.betrag); + } + return basis; +} + +/** Gibt an, ob gerade ein aktiver Rabatt greift (für Badge-Anzeige "-20%" o.ä.). */ +export function hatAktivenRabatt(product: Product): boolean { + return effektiverPreis(product) < product.preis; +} + +/** Für die Badge-Anzeige: "-20%" oder "-5 €", je nachdem was hinterlegt ist. */ +export function rabattLabel(product: Product): string | null { + const r = product.rabatt; + if (!r || !istRabattAktiv(r.bis)) return null; + if (typeof r.prozent === "number" && r.prozent > 0) return `-${r.prozent}%`; + if (typeof r.betrag === "number" && r.betrag > 0) return `-${r.betrag.toFixed(2).replace(".", ",")} €`; + return null; +} + +/** Mengenrabatt: findet den besten (höchsten) Prozentsatz, der bei der gegebenen Stückzahl + * greift. Gibt 0 zurück, wenn kein Mengenrabatt hinterlegt ist oder die Menge zu klein ist. */ +export function mengenrabattProzent(product: Product, menge: number): number { + const stufen = product.rabatt?.mengenrabatt ?? []; + let bester = 0; + for (const stufe of stufen) { + if (menge >= stufe.abMenge && stufe.prozent > bester) bester = stufe.prozent; + } + return bester; +} + +/** Effektiver Zeilenpreis (Einzelpreis inkl. Rabatt × Menge, zusätzlich mit Mengenrabatt + * verrechnet) — das ist die Zahl, die im Warenkorb pro Position angezeigt/summiert wird. */ +export function effektiverZeilenpreis(product: Product, menge: number): number { + const einzelpreis = effektiverPreis(product); + const mengenProzent = mengenrabattProzent(product, menge); + const zeile = einzelpreis * menge; + return mengenProzent > 0 ? zeile * (1 - mengenProzent / 100) : zeile; +} diff --git a/src/i18n/ui.ts b/src/i18n/ui.ts index 9ef997b..7bf4ee5 100644 --- a/src/i18n/ui.ts +++ b/src/i18n/ui.ts @@ -21,7 +21,7 @@ export const ui = { common: { addToCart: "In den Warenkorb", soldOut: "Ausverkauft", addedToCart: "Zum Warenkorb hinzugefügt ✓", browseShop: "Jetzt stöbern", viewAll: "Alle ansehen →", viewAllSale: "Alle Angebote →", - toShop: "Zum Shop →", moreAboutMe: "Mehr über mich →", + toShop: "Zum Shop →", moreAboutMe: "Mehr über mich →", discoverNow: "Jetzt entdecken", vatNote: "Gemäß §19 UStG wird keine Umsatzsteuer berechnet und ausgewiesen.", allPricesPlusShipping: "Alle Preise zzgl. Versand.", plusShipping: "zzgl. Versand", quantity: "Anzahl", photoSoon: "Produktfoto folgt", portraitSoon: "Porträtfoto von VanVan folgt", @@ -149,13 +149,17 @@ export const ui = { subtotal: "Zwischensumme", shipping: "Versand", shippingCalculated: "wird im Checkout berechnet", total: "Gesamt", remaining: (v: string) => `Noch ${v} bis zum kostenlosen Versand.`, freeShipping: "🩵 Du bekommst kostenlosen Versand!", toCheckout: "Weiter zum Checkout", remove: "Entfernen", perItem: "/ Stück", + discount: "Rabatt", mengenrabatt: (p: string) => `Mengenrabatt -${p}%`, + mengenrabattTier: (ab: string, p: string) => `ab ${ab} Stück -${p}%`, + couponLabel: "🎟️ Gutscheincode", couponPlaceholder: "Code eingeben", couponApply: "Einlösen", couponRemove: "Entfernen", + couponInvalid: "Code ungültig oder abgelaufen.", couponApplied: (code: string) => `Gutschein „${code}" eingelöst`, }, checkout: { eyebrow: "Checkout", title: "Bestellung abschließen", todoNote: "Phase 1 – Demo-Checkout. Es findet noch keine echte Zahlung statt. Die vollständige Anbindung an PayPal, Klarna und Banküberweisung sowie die Bestellverwaltung folgen in Phase 2 (Cloudflare Worker + D1, siehe Tech-Stack-Entscheidung im Projekt-Vault).", - step1: "1. Kontakt & Lieferadresse", email: "E-Mail", firstName: "Vorname", lastName: "Nachname", + step1: "2. Kontakt & Lieferadresse", email: "E-Mail", firstName: "Vorname", lastName: "Nachname", street: "Straße & Hausnummer", zip: "PLZ", city: "Ort", country: "Land", - step2: "2. Zahlungsart", + step2: "1. Zahlungsart", step3: "3. Bestellübersicht", agbPrefix: "Ich habe die", agbLink: "AGB", agbSuffix: "gelesen und akzeptiere sie.", revocationPrefix: "Ich habe die", revocationLink: "Widerrufsbelehrung", revocationAnd: "und", privacyLink: "Datenschutzerklärung", revocationSuffix: "zur Kenntnis genommen.", @@ -190,7 +194,7 @@ export const ui = { common: { addToCart: "Add to cart", soldOut: "Sold out", addedToCart: "Added to cart ✓", browseShop: "Browse now", viewAll: "View all →", viewAllSale: "View all offers →", - toShop: "To the shop →", moreAboutMe: "More about me →", + toShop: "To the shop →", moreAboutMe: "More about me →", discoverNow: "Discover now", vatNote: "Under §19 UStG (German small-business rule) no VAT is charged or shown.", allPricesPlusShipping: "All prices plus shipping.", plusShipping: "plus shipping", quantity: "Quantity", photoSoon: "Product photo coming soon", portraitSoon: "VanVan's portrait photo coming soon", @@ -318,13 +322,17 @@ export const ui = { subtotal: "Subtotal", shipping: "Shipping", shippingCalculated: "calculated at checkout", total: "Total", remaining: (v: string) => `${v} more for free shipping.`, freeShipping: "🩵 You qualify for free shipping!", toCheckout: "Continue to checkout", remove: "Remove", perItem: "/ item", + discount: "Discount", mengenrabatt: (p: string) => `Bulk discount -${p}%`, + mengenrabattTier: (ab: string, p: string) => `from ${ab} pcs -${p}%`, + couponLabel: "🎟️ Coupon code", couponPlaceholder: "Enter code", couponApply: "Apply", couponRemove: "Remove", + couponInvalid: "Code invalid or expired.", couponApplied: (code: string) => `Coupon "${code}" applied`, }, checkout: { eyebrow: "Checkout", title: "Complete your order", todoNote: "Phase 1 – demo checkout. No real payment is processed yet. Full integration with PayPal, Klarna and bank transfer, plus order management, follow in Phase 2 (Cloudflare Worker + D1, see the tech-stack decision in the project vault).", - step1: "1. Contact & shipping address", email: "Email", firstName: "First name", lastName: "Last name", + step1: "2. Contact & shipping address", email: "Email", firstName: "First name", lastName: "Last name", street: "Street & house number", zip: "ZIP code", city: "City", country: "Country", - step2: "2. Payment method", + step2: "1. Payment method", step3: "3. Order summary", agbPrefix: "I have read the", agbLink: "terms & conditions", agbSuffix: "and accept them.", revocationPrefix: "I have taken note of the", revocationLink: "withdrawal notice", revocationAnd: "and", privacyLink: "privacy policy", revocationSuffix: ".", @@ -359,7 +367,7 @@ export const ui = { common: { addToCart: "In Warenchorb", soldOut: "Usverkauft", addedToCart: "Zum Warenchorb dezuegfüegt ✓", browseShop: "Jetzt use luege", viewAll: "Alles aluege →", viewAllSale: "Alli Aktione →", - toShop: "Zum Shop →", moreAboutMe: "Meh über mich →", + toShop: "Zum Shop →", moreAboutMe: "Meh über mich →", discoverNow: "Jetzt aluege", vatNote: "Gemäss §19 UStG (dütsches Rächt) wird kei Umsatzsteuer berechnet und usgwiese.", allPricesPlusShipping: "Alli Pryse zzgl. Versand.", plusShipping: "zzgl. Versand", quantity: "Aazahl", photoSoon: "Produktfoto chunnt bald", portraitSoon: "Porträtfoto vo VanVan chunnt bald", @@ -487,13 +495,17 @@ export const ui = { subtotal: "Zwüschesumme", shipping: "Versand", shippingCalculated: "wird im Checkout berechnet", total: "Total", remaining: (v: string) => `No ${v} bis zum Gratisversand.`, freeShipping: "🩵 Du übercho't Gratisversand!", toCheckout: "Wyter zum Checkout", remove: "Ewägmache", perItem: "/ Stuck", + discount: "Rabatt", mengenrabatt: (p: string) => `Mengerabatt -${p}%`, + mengenrabattTier: (ab: string, p: string) => `ab ${ab} Stuck -${p}%`, + couponLabel: "🎟️ Gutschy-Code", couponPlaceholder: "Code igäh", couponApply: "Ilöse", couponRemove: "Ewägmache", + couponInvalid: "Code ungültig oder abgloffe.", couponApplied: (code: string) => `Gutschy „${code}" iglöst`, }, checkout: { eyebrow: "Checkout", title: "Bschtellig abschliesse", todoNote: "Phase 1 – Demo-Checkout. Es git no kei echti Zahlig. D'vollständigi Aabindig a PayPal, Klarna und Banküberwysig sowie d'Bstellverwaltig chöme i Phase 2.", - step1: "1. Kontakt & Lieferadrässe", email: "E-Mail", firstName: "Vorname", lastName: "Nachname", + step1: "2. Kontakt & Lieferadrässe", email: "E-Mail", firstName: "Vorname", lastName: "Nachname", street: "Strasse & Huusnummere", zip: "PLZ", city: "Ort", country: "Land", - step2: "2. Zahligsart", + step2: "1. Zahligsart", step3: "3. Bschtellübersicht", agbPrefix: "Ich ha d'", agbLink: "AGB", agbSuffix: "gläse und akzeptiere si.", revocationPrefix: "Ich ha d'", revocationLink: "Widerrufsbelehrig", revocationAnd: "und d'", privacyLink: "Datenschutzerklärig", revocationSuffix: "zur Kenntnis gnoh.", @@ -528,7 +540,7 @@ export const ui = { common: { addToCart: "Ajouter au panier", soldOut: "Épuisé", addedToCart: "Ajouté au panier ✓", browseShop: "Découvrir la boutique", viewAll: "Tout voir →", viewAllSale: "Toutes les offres →", - toShop: "Vers la boutique →", moreAboutMe: "En savoir plus sur moi →", + toShop: "Vers la boutique →", moreAboutMe: "En savoir plus sur moi →", discoverNow: "Découvrir maintenant", vatNote: "Conformément à l'§19 UStG (droit allemand), aucune TVA n'est calculée ni indiquée.", allPricesPlusShipping: "Tous les prix hors frais de port.", plusShipping: "hors frais de port", quantity: "Quantité", photoSoon: "Photo du produit à venir", portraitSoon: "Photo de VanVan à venir", @@ -656,13 +668,17 @@ export const ui = { subtotal: "Sous-total", shipping: "Livraison", shippingCalculated: "calculée au moment du paiement", total: "Total", remaining: (v: string) => `Encore ${v} avant la livraison gratuite.`, freeShipping: "🩵 Vous bénéficiez de la livraison gratuite !", toCheckout: "Passer au paiement", remove: "Retirer", perItem: "/ pièce", + discount: "Remise", mengenrabatt: (p: string) => `Remise de quantité -${p}%`, + mengenrabattTier: (ab: string, p: string) => `dès ${ab} pièces -${p}%`, + couponLabel: "🎟️ Code promo", couponPlaceholder: "Entrer le code", couponApply: "Valider", couponRemove: "Retirer", + couponInvalid: "Code invalide ou expiré.", couponApplied: (code: string) => `Code promo « ${code} » appliqué`, }, checkout: { eyebrow: "Paiement", title: "Finaliser la commande", todoNote: "Phase 1 – paiement de démonstration. Aucun paiement réel n'est encore traité. L'intégration complète avec PayPal, Klarna et le virement bancaire, ainsi que la gestion des commandes, suivront en phase 2.", - step1: "1. Contact & adresse de livraison", email: "E-mail", firstName: "Prénom", lastName: "Nom", + step1: "2. Contact & adresse de livraison", email: "E-mail", firstName: "Prénom", lastName: "Nom", street: "Rue & numéro", zip: "Code postal", city: "Ville", country: "Pays", - step2: "2. Moyen de paiement", + step2: "1. Moyen de paiement", step3: "3. Récapitulatif de commande", agbPrefix: "J'ai lu les", agbLink: "CGV", agbSuffix: "et je les accepte.", revocationPrefix: "J'ai pris connaissance de la", revocationLink: "notice de rétractation", revocationAnd: "et de la", privacyLink: "politique de confidentialité", revocationSuffix: ".", diff --git a/src/pages/ch/checkout/index.astro b/src/pages/ch/checkout/index.astro index 8815e67..2f24fba 100644 --- a/src/pages/ch/checkout/index.astro +++ b/src/pages/ch/checkout/index.astro @@ -18,6 +18,26 @@ const t = useTranslations(lang);
+

{t.checkout.step2}

+
+ + + +
+ +

{t.checkout.step1}

@@ -39,26 +59,6 @@ const t = useTranslations(lang);
-
-

{t.checkout.step2}

-
- - - -
-

{t.checkout.step3}

@@ -77,6 +77,15 @@ const t = useTranslations(lang);