From 0cb7d3ca6537e27190af682379724fb5f19ef0b6 Mon Sep 17 00:00:00 2001 From: qcigano Date: Mon, 3 Aug 2026 15:03:48 +0200 Subject: [PATCH] =?UTF-8?q?Treuebonus-System:=2010=20abgeschlossene=20Best?= =?UTF-8?q?ellungen=20=3D=2020%=20Rabatt=20auf=20einen=20frei=20w=C3=A4hlb?= =?UTF-8?q?aren=20Artikel?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Neue eigene Kiste "Treuebonus" im Kundenkonto (5. Nav-Tab, grüner Akzent) zeigt den Fortschritt ("X von 10 Bestellungen") mit Fortschrittsbalken und Restanzahl-Hinweis, nach Freischaltung eine Erfolgsmeldung mit Link zum Warenkorb. Es zählen ausschließlich erfolgreich abgeschickte, bezahlte Bestellungen (in Phase 1: jede erfolgreich abgeschlossene Demo-Bestellung im Checkout) — abgebrochene Bestellungen zählen nie mit. Sobald der Bonus aktiv ist, erscheint im Warenkorb ein Hinweis-Banner und bei jedem Artikel eine Auswahl ("Für 20% Treuebonus auswählen") — es kann immer nur EIN Artikel gewählt werden (Radiobutton-Verhalten über alle Positionen hinweg). Der Rabatt fließt automatisch in Warenkorb- und Checkout-Summe ein (neue treuebonusDiscount()-Funktion in scripts/cart.ts, in totalDiscount() eingerechnet). Nach erfolgreichem Bestellabschluss wird die Belohnung als eingelöst markiert und der Fortschrittszähler beginnt wieder bei 0 — beides dauerhaft im Konto (localStorage, Phase 1) gespeichert und eindeutig diesem Kundenkonto zugeordnet. Neue Funktionen in scripts/account.ts: registriereAbgeschlosseneBestellung(), setTreuebonusArtikel(), treuebonusEinloesen(), treueFortschritt(). Beide Zustandsübergänge (Fortschritt zählen vs. Belohnung einlösen) end-to-end über den echten Checkout-Ablauf getestet und verifiziert. Co-Authored-By: Claude Sonnet 5 --- src/i18n/ui.ts | 60 +++++++++++++++++-- src/pages/ch/checkout/index.astro | 19 ++++-- src/pages/ch/konto/angemeldet.astro | 71 ++++++++++++++++++++-- src/pages/ch/warenkorb/index.astro | 56 +++++++++++++++-- src/pages/checkout/index.astro | 21 +++++-- src/pages/en/checkout/index.astro | 19 ++++-- src/pages/en/konto/angemeldet.astro | 71 ++++++++++++++++++++-- src/pages/en/warenkorb/index.astro | 56 +++++++++++++++-- src/pages/fr/checkout/index.astro | 19 ++++-- src/pages/fr/konto/angemeldet.astro | 71 ++++++++++++++++++++-- src/pages/fr/warenkorb/index.astro | 56 +++++++++++++++-- src/pages/konto/angemeldet.astro | 71 ++++++++++++++++++++-- src/pages/warenkorb/index.astro | 56 +++++++++++++++-- src/scripts/account.ts | 93 ++++++++++++++++++++++++++++- src/scripts/cart.ts | 32 ++++++++-- src/styles/global.css | 61 +++++++++++++++++++ 16 files changed, 769 insertions(+), 63 deletions(-) diff --git a/src/i18n/ui.ts b/src/i18n/ui.ts index 8f2bb38..3650954 100644 --- a/src/i18n/ui.ts +++ b/src/i18n/ui.ts @@ -175,6 +175,11 @@ export const ui = { partnerDiscount: "🤝 Partner-Rabatt", partnerCodeLabel: "🤝 Partner-Rabattcode", partnerCodePlaceholder: "Code eingeben", partnerCodeApply: "Einlösen", partnerCodeRemove: "Entfernen", partnerCodeInvalid: "Code ungültig oder abgelaufen.", partnerCodeApplied: (code: string) => `Partner-Code „${code}" eingelöst`, + loyaltyDiscount: "🎁 Treuebonus (-20 %)", + loyaltyBannerTitle: "🎁 Dein Treuebonus ist bereit!", + loyaltyBannerText: "Wähle unten den Artikel aus, auf den du 20 % Rabatt erhältst.", + loyaltySelectLabel: "Für 20 % Treuebonus auswählen", + loyaltySelectedLabel: "🎁 -20 % Treuebonus angewendet", }, checkout: { eyebrow: "Checkout", title: "Bestellung abschließen", @@ -224,7 +229,15 @@ export const ui = { notLoggedIn: "Du bist nicht angemeldet — du wirst zur Anmeldung weitergeleitet …", logout: "Abmelden", navLabel: "Kontobereich-Navigation", - statOrders: "Bestellungen", statWishlist: "Wunschliste", statOpen: "Offene Sendungen", statData: "Meine Daten", + statOrders: "Bestellungen", statWishlist: "Wunschliste", statOpen: "Offene Sendungen", statData: "Meine Daten", statLoyalty: "Treuebonus", + loyaltyTitle: "🎁 Mein Treuebonus", + loyaltyProgressTemplate: "Dein Bestellfortschritt: {cur} von {ziel} Bestellungen", + loyaltyRemainingOne: "Dir fehlt noch 1 Bestellung, bis du 20 % Rabatt auf einen Artikel deiner Wahl erhältst.", + loyaltyRemainingOther: "Dir fehlen noch {n} Bestellungen, bis du 20 % Rabatt auf einen Artikel deiner Wahl erhältst.", + loyaltyUnlockedTitle: "🎉 Treuebonus freigeschaltet", + loyaltyUnlockedText: "Du hast zehn Bestellungen abgeschlossen und erhältst bei deiner nächsten Bestellung 20 % Rabatt auf einen Artikel deiner Wahl.", + loyaltyGoToCart: "Artikel im Warenkorb auswählen →", + loyaltyExplainer: "Es zählen nur vollständig abgeschlossene und bezahlte Bestellungen. Nach der Einlösung beginnt dein Fortschritt wieder bei null.", noOpenOrders: "Aktuell keine offenen Sendungen — alle Bestellungen sind schon angekommen. 🎉", noArrivedOrders: "Noch keine angekommenen Bestellungen — hier erscheinen deine Bestellungen, sobald sie geliefert wurden.", ordersTitle: "📦 Meine Bestellungen", @@ -413,6 +426,11 @@ export const ui = { partnerDiscount: "🤝 Partner discount", partnerCodeLabel: "🤝 Partner discount code", partnerCodePlaceholder: "Enter code", partnerCodeApply: "Apply", partnerCodeRemove: "Remove", partnerCodeInvalid: "Code invalid or expired.", partnerCodeApplied: (code: string) => `Partner code "${code}" applied`, + loyaltyDiscount: "🎁 Loyalty reward (-20%)", + loyaltyBannerTitle: "🎁 Your loyalty reward is ready!", + loyaltyBannerText: "Pick the item below that you'd like 20% off.", + loyaltySelectLabel: "Select for 20% loyalty reward", + loyaltySelectedLabel: "🎁 -20% loyalty reward applied", }, checkout: { eyebrow: "Checkout", title: "Complete your order", @@ -462,7 +480,15 @@ export const ui = { notLoggedIn: "You're not signed in — redirecting you to sign in …", logout: "Sign out", navLabel: "Account section navigation", - statOrders: "Orders", statWishlist: "Wishlist", statOpen: "Open shipments", statData: "My details", + statOrders: "Orders", statWishlist: "Wishlist", statOpen: "Open shipments", statData: "My details", statLoyalty: "Loyalty reward", + loyaltyTitle: "🎁 My loyalty reward", + loyaltyProgressTemplate: "Your order progress: {cur} of {ziel} orders", + loyaltyRemainingOne: "You need 1 more order to get 20% off an item of your choice.", + loyaltyRemainingOther: "You need {n} more orders to get 20% off an item of your choice.", + loyaltyUnlockedTitle: "🎉 Loyalty reward unlocked", + loyaltyUnlockedText: "You've completed ten orders and get 20% off an item of your choice on your next order.", + loyaltyGoToCart: "Choose item in cart →", + loyaltyExplainer: "Only fully completed and paid orders count. After redemption your progress starts again from zero.", noOpenOrders: "No open shipments right now — every order has already arrived. 🎉", noArrivedOrders: "No delivered orders yet — your orders will appear here once they've arrived.", ordersTitle: "📦 My orders", @@ -651,6 +677,11 @@ export const ui = { partnerDiscount: "🤝 Partner-Rabatt", partnerCodeLabel: "🤝 Partner-Rabattcode", partnerCodePlaceholder: "Code igäh", partnerCodeApply: "Ilöse", partnerCodeRemove: "Ewägmache", partnerCodeInvalid: "Code ungültig oder abgloffe.", partnerCodeApplied: (code: string) => `Partner-Code „${code}" iglöst`, + loyaltyDiscount: "🎁 Treuebonus (-20 %)", + loyaltyBannerTitle: "🎁 Din Treuebonus isch parat!", + loyaltyBannerText: "Wähl unde dr Artikel us, uf dä du 20 % Rabatt übercho.", + loyaltySelectLabel: "Für 20 % Treuebonus uswähle", + loyaltySelectedLabel: "🎁 -20 % Treuebonus aagwändet", }, checkout: { eyebrow: "Checkout", title: "Bschtellig abschliesse", @@ -700,7 +731,15 @@ export const ui = { notLoggedIn: "Du bisch nid aagmäldet — du wirsch zur Aamäldig wytergleitet …", logout: "Abmälde", navLabel: "Kontobereich-Navigation", - statOrders: "Bstellige", statWishlist: "Wunschlischte", statOpen: "Offni Sändige", statData: "Mini Date", + statOrders: "Bstellige", statWishlist: "Wunschlischte", statOpen: "Offni Sändige", statData: "Mini Date", statLoyalty: "Treuebonus", + loyaltyTitle: "🎁 Mis Treuebonus", + loyaltyProgressTemplate: "Din Bstellfortschritt: {cur} vo {ziel} Bstellige", + loyaltyRemainingOne: "Dir fehlt no 1 Bstellig, bis du 20 % Rabatt uf en Artikel dinere Wahl übercho.", + loyaltyRemainingOther: "Dir fehle no {n} Bstellige, bis du 20 % Rabatt uf en Artikel dinere Wahl übercho.", + loyaltyUnlockedTitle: "🎉 Treuebonus freigschaltet", + loyaltyUnlockedText: "Du hesch zäh Bstellige abgschlosse und übercho bi dinere nächschte Bstellig 20 % Rabatt uf en Artikel dinere Wahl.", + loyaltyGoToCart: "Artikel im Warenchorb uswähle →", + loyaltyExplainer: "Es zelle nume vollständig abgschlosseni und bezahlti Bstellige. Nach dr Ilösig fangt din Fortschritt wieder bi null a.", noOpenOrders: "Momentan kei offni Sändige — alli Bstellige sind scho aacho. 🎉", noArrivedOrders: "No kei aacho Bstellige — da erschine dini Bstellige, sobald sie glifert worde sind.", ordersTitle: "📦 Mini Bstellige", @@ -889,6 +928,11 @@ export const ui = { partnerDiscount: "🤝 Remise partenaire", partnerCodeLabel: "🤝 Code partenaire", partnerCodePlaceholder: "Entrer le code", partnerCodeApply: "Valider", partnerCodeRemove: "Retirer", partnerCodeInvalid: "Code invalide ou expiré.", partnerCodeApplied: (code: string) => `Code partenaire « ${code} » appliqué`, + loyaltyDiscount: "🎁 Récompense fidélité (-20 %)", + loyaltyBannerTitle: "🎁 Votre récompense fidélité est prête !", + loyaltyBannerText: "Choisissez ci-dessous l'article sur lequel vous voulez -20 %.", + loyaltySelectLabel: "Choisir pour la récompense fidélité de 20 %", + loyaltySelectedLabel: "🎁 Récompense fidélité de -20 % appliquée", }, checkout: { eyebrow: "Paiement", title: "Finaliser la commande", @@ -938,7 +982,15 @@ export const ui = { notLoggedIn: "Vous n'êtes pas connecté(e) — redirection vers la connexion …", logout: "Se déconnecter", navLabel: "Navigation de l'espace client", - statOrders: "Commandes", statWishlist: "Liste de souhaits", statOpen: "Envois en cours", statData: "Mes données", + statOrders: "Commandes", statWishlist: "Liste de souhaits", statOpen: "Envois en cours", statData: "Mes données", statLoyalty: "Récompense", + loyaltyTitle: "🎁 Ma récompense fidélité", + loyaltyProgressTemplate: "Votre progression : {cur} sur {ziel} commandes", + loyaltyRemainingOne: "Il vous manque encore 1 commande pour obtenir 20 % de réduction sur l'article de votre choix.", + loyaltyRemainingOther: "Il vous manque encore {n} commandes pour obtenir 20 % de réduction sur l'article de votre choix.", + loyaltyUnlockedTitle: "🎉 Récompense fidélité débloquée", + loyaltyUnlockedText: "Vous avez validé dix commandes et bénéficiez de 20 % de réduction sur l'article de votre choix lors de votre prochaine commande.", + loyaltyGoToCart: "Choisir l'article dans le panier →", + loyaltyExplainer: "Seules les commandes entièrement finalisées et payées comptent. Après utilisation, votre progression repart de zéro.", noOpenOrders: "Aucun envoi en cours pour le moment — toutes les commandes sont déjà arrivées. 🎉", noArrivedOrders: "Aucune commande livrée pour le moment — vos commandes apparaîtront ici dès qu'elles seront arrivées.", ordersTitle: "📦 Mes commandes", diff --git a/src/pages/ch/checkout/index.astro b/src/pages/ch/checkout/index.astro index 96f913d..d57fcc9 100644 --- a/src/pages/ch/checkout/index.astro +++ b/src/pages/ch/checkout/index.astro @@ -173,19 +173,20 @@ const t = useTranslations(lang); - diff --git a/src/pages/ch/konto/angemeldet.astro b/src/pages/ch/konto/angemeldet.astro index e595ff0..465a993 100644 --- a/src/pages/ch/konto/angemeldet.astro +++ b/src/pages/ch/konto/angemeldet.astro @@ -109,6 +109,12 @@ const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status === + @@ -290,10 +296,28 @@ const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status === + + {/* Treuebonus: 10 gültige, abgeschlossene Bestellungen → 20% auf einen frei wählbaren Artikel + (siehe scripts/account.ts). Fortschritt/Status kommen komplett aus dem Konto und werden per + JS befüllt (siehe Skript unten) — hier nur das Gerüst mit sinnvollen Leerzuständen. */} +
+
+

{t.accountDash.loyaltyTitle}

+
+ + +
+

+

+ +

{t.accountDash.loyaltyExplainer}

+
+
+
- diff --git a/src/pages/checkout/index.astro b/src/pages/checkout/index.astro index 9d363c4..f73d759 100644 --- a/src/pages/checkout/index.astro +++ b/src/pages/checkout/index.astro @@ -173,22 +173,23 @@ const t = useTranslations(lang); - diff --git a/src/pages/en/checkout/index.astro b/src/pages/en/checkout/index.astro index c3e45e8..3aebaae 100644 --- a/src/pages/en/checkout/index.astro +++ b/src/pages/en/checkout/index.astro @@ -173,19 +173,20 @@ const t = useTranslations(lang); - diff --git a/src/pages/en/konto/angemeldet.astro b/src/pages/en/konto/angemeldet.astro index ed83a8c..a622035 100644 --- a/src/pages/en/konto/angemeldet.astro +++ b/src/pages/en/konto/angemeldet.astro @@ -109,6 +109,12 @@ const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status === + @@ -290,10 +296,28 @@ const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status === + + {/* Treuebonus: 10 gültige, abgeschlossene Bestellungen → 20% auf einen frei wählbaren Artikel + (siehe scripts/account.ts). Fortschritt/Status kommen komplett aus dem Konto und werden per + JS befüllt (siehe Skript unten) — hier nur das Gerüst mit sinnvollen Leerzuständen. */} +
+
+

{t.accountDash.loyaltyTitle}

+
+ + +
+

+

+ +

{t.accountDash.loyaltyExplainer}

+
+
+
- diff --git a/src/pages/fr/checkout/index.astro b/src/pages/fr/checkout/index.astro index 3fa5cc6..5b086a9 100644 --- a/src/pages/fr/checkout/index.astro +++ b/src/pages/fr/checkout/index.astro @@ -173,19 +173,20 @@ const t = useTranslations(lang); - diff --git a/src/pages/fr/konto/angemeldet.astro b/src/pages/fr/konto/angemeldet.astro index 3b42e15..f49e0c8 100644 --- a/src/pages/fr/konto/angemeldet.astro +++ b/src/pages/fr/konto/angemeldet.astro @@ -109,6 +109,12 @@ const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status === + @@ -290,10 +296,28 @@ const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status === + + {/* Treuebonus: 10 gültige, abgeschlossene Bestellungen → 20% auf einen frei wählbaren Artikel + (siehe scripts/account.ts). Fortschritt/Status kommen komplett aus dem Konto und werden per + JS befüllt (siehe Skript unten) — hier nur das Gerüst mit sinnvollen Leerzuständen. */} +
+
+

{t.accountDash.loyaltyTitle}

+
+ + +
+

+

+ +

{t.accountDash.loyaltyExplainer}

+
+
+
- diff --git a/src/pages/konto/angemeldet.astro b/src/pages/konto/angemeldet.astro index a415c2d..a436d9a 100644 --- a/src/pages/konto/angemeldet.astro +++ b/src/pages/konto/angemeldet.astro @@ -109,6 +109,12 @@ const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status === + @@ -290,10 +296,28 @@ const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status === + + {/* Treuebonus: 10 gültige, abgeschlossene Bestellungen → 20% auf einen frei wählbaren Artikel + (siehe scripts/account.ts). Fortschritt/Status kommen komplett aus dem Konto und werden per + JS befüllt (siehe Skript unten) — hier nur das Gerüst mit sinnvollen Leerzuständen. */} +
+
+

{t.accountDash.loyaltyTitle}

+
+ + +
+

+

+ +

{t.accountDash.loyaltyExplainer}

+
+
+
- diff --git a/src/scripts/account.ts b/src/scripts/account.ts index b585e43..db1c73d 100644 --- a/src/scripts/account.ts +++ b/src/scripts/account.ts @@ -18,12 +18,40 @@ export interface Account { photo: string; /** ":"-Paare, zu denen schon eine Rezension abgegeben wurde. */ bewerteteBestellungen: string[]; + /** Treuebonus (siehe TREUE_ZIEL weiter unten): Anzahl gültiger, vollständig abgeschlossener + * und bezahlter Bestellungen seit der letzten Einlösung — zählt NICHT weiter, sobald der + * Bonus aktiv ist (siehe treuebonusAktiv), damit "10 von 10" nicht einfach weiterwächst, + * während die Belohnung noch auf ihre Einlösung wartet. */ + treueBestellungen: number; + /** true, sobald treueBestellungen das Ziel erreicht hat UND die Belohnung noch nicht + * eingelöst wurde. Wird bei Einlösung wieder auf false gesetzt, gleichzeitig setzt + * treuebonusEinloesen() treueBestellungen zurück auf 0 — eindeutig diesem Konto zugeordnet, + * da alles im Konto-Objekt selbst liegt und nie kontoübergreifend gelesen wird. */ + treuebonusAktiv: boolean; + /** Slug des Artikels, den die Person im Warenkorb für die -20%-Belohnung ausgewählt hat + * (siehe scripts/cart.ts / warenkorb-Seite). Wird bei Einlösung wieder geleert. */ + treuebonusArtikelSlug: string | null; } const KEY = "vandiy_account_v1"; +/** Ab dieser Anzahl gültiger, abgeschlossener Bestellungen wird die Treuebelohnung freigeschaltet. */ +export const TREUE_ZIEL = 10; +/** Höhe des Treuebonus-Rabatts auf den frei gewählten Artikel. */ +export const TREUE_RABATT_PROZENT = 20; + function leeresKonto(): Account { - return { loggedIn: false, name: "", username: "", email: "", photo: "", bewerteteBestellungen: [] }; + return { + loggedIn: false, + name: "", + username: "", + email: "", + photo: "", + bewerteteBestellungen: [], + treueBestellungen: 0, + treuebonusAktiv: false, + treuebonusArtikelSlug: null, + }; } export function getAccount(): Account { @@ -100,3 +128,66 @@ export function markiereBestellungBewertet(bestellnummer: string, produktSlug: s saveAccount({ ...a, bewerteteBestellungen: [...a.bewerteteBestellungen, schluessel] }); } } + +// ─── Treuebonus (10 abgeschlossene Bestellungen → 20% auf einen frei wählbaren Artikel) ─── +// +// WICHTIG: Es zählt hier bewusst nur eine echte "Bestellung abgeschlossen UND bezahlt"-Aktion +// (siehe registriereAbgeschlosseneBestellung(), wird im Checkout nach erfolgreichem Absenden +// aufgerufen — nicht z.B. schon beim Warenkorb füllen). Stornierte/nicht bezahlte Bestellungen +// rufen diese Funktion nie auf und zählen dadurch automatisch nicht mit. In Phase 1 (kein +// echtes Zahlungsbackend) gilt jede erfolgreich abgeschickte Demo-Bestellung als "bezahlt"; +// TODO Phase 2: hier stattdessen auf ein echtes Zahlungs-Webhook-Ereignis reagieren. + +/** Zählt eine gültige, abgeschlossene Bestellung für den Treuebonus. Zählt NICHT weiter, wenn + * gerade schon eine Belohnung auf Einlösung wartet (erst einlösen, dann läuft der nächste + * Zähler wieder bei 0 los). Schaltet die Belohnung automatisch frei, sobald TREUE_ZIEL erreicht + * ist. */ +export function registriereAbgeschlosseneBestellung(): Account { + const a = getAccount(); + if (a.treuebonusAktiv) return a; + const naechsterStand = Math.min(TREUE_ZIEL, a.treueBestellungen + 1); + const updated: Account = { + ...a, + treueBestellungen: naechsterStand, + treuebonusAktiv: naechsterStand >= TREUE_ZIEL, + }; + saveAccount(updated); + return updated; +} + +/** Legt fest, auf welchen Artikel im Warenkorb der -20%-Treuebonus angewendet werden soll + * (oder hebt die Auswahl mit `null` wieder auf) — frei wählbar, siehe Pflichtenheft. */ +export function setTreuebonusArtikel(slug: string | null) { + saveAccount({ ...getAccount(), treuebonusArtikelSlug: slug }); +} + +/** Markiert die Belohnung als eingelöst: deaktiviert sie, setzt den Zähler für den nächsten + * Durchlauf auf 0 zurück und leert die Artikelauswahl. Wird im Checkout nach erfolgreichem + * Absenden einer Bestellung aufgerufen, die den Treuebonus tatsächlich genutzt hat. */ +export function treuebonusEinloesen(): Account { + const updated: Account = { + ...getAccount(), + treueBestellungen: 0, + treuebonusAktiv: false, + treuebonusArtikelSlug: null, + }; + saveAccount(updated); + return updated; +} + +export interface TreueFortschritt { + aktuelle: number; + ziel: number; + fehlende: number; + aktiv: boolean; +} + +/** Aufbereiteter Fortschritt fürs Anzeigen im Kundenkonto ("7 von 10", "Dir fehlen noch 3 …"). */ +export function treueFortschritt(a: Account): TreueFortschritt { + return { + aktuelle: a.treueBestellungen, + ziel: TREUE_ZIEL, + fehlende: Math.max(0, TREUE_ZIEL - a.treueBestellungen), + aktiv: a.treuebonusAktiv, + }; +} diff --git a/src/scripts/cart.ts b/src/scripts/cart.ts index 060cf43..370ef0d 100644 --- a/src/scripts/cart.ts +++ b/src/scripts/cart.ts @@ -2,9 +2,10 @@ // TODO Phase 2: durch echten Cloudflare-Worker-Warenkorb/Checkout ersetzen. import { getProduct } from "../data/products"; -import { effektiverZeilenpreis, istImSale } from "../data/rabatt"; +import { effektiverPreis, effektiverZeilenpreis, istImSale } from "../data/rabatt"; import { findeGutschein, gutscheinRabatt, type Gutschein } from "../data/gutscheine"; import { findePartnercode, partnercodeRabatt, type Partnercode } from "../data/partnercodes"; +import { getAccount, TREUE_RABATT_PROZENT } from "./account"; export interface CartItem { slug: string; @@ -186,9 +187,28 @@ export function partnerCodeDiscount(_subtotal?: number): number { return p ? partnercodeRabatt(p, partnerCodeEligibleSubtotal()) : 0; } -/** Gutschein und Partnercode können gleichzeitig eingelöst sein (zwei unabhängige Codes) — die - * Summe wird hier auf die Zwischensumme gedeckelt, damit der Gesamtpreis nie unter 0 € rutscht, - * egal wie großzügig die einzelnen Rabatte sind. */ -export function totalDiscount(subtotal: number): number { - return Math.min(subtotal, couponDiscount(subtotal) + partnerCodeDiscount(subtotal)); +// Treuebonus: 20% auf GENAU EINEN frei gewählten Artikel (siehe scripts/account.ts für die +// Fortschritts-/Freischalt-Logik). Die Auswahl (treuebonusArtikelSlug) liegt im Konto, nicht im +// Warenkorb selbst — bleibt so auch über einen geleerten/neu befüllten Warenkorb hinweg +// eindeutig diesem Kundenkonto zugeordnet. + +/** Rabatt in Euro, den der aktive Treuebonus auf den gewählten Artikel gibt — 0, wenn kein + * Bonus aktiv ist, kein Artikel gewählt wurde, oder der gewählte Artikel gerade nicht (mehr) + * zum Kauf ausgewählt im Warenkorb liegt. Gilt bewusst nur für EIN Stück der Position, nicht + * mehrfach bei größerer Menge — "ein frei wählbarer Artikel", kein Mengenrabatt-Ersatz. */ +export function treuebonusDiscount(): number { + const a = getAccount(); + if (!a.treuebonusAktiv || !a.treuebonusArtikelSlug) return 0; + const item = getCart().find((i) => i.slug === a.treuebonusArtikelSlug); + if (!item || !istAusgewaehlt(item)) return 0; + const produkt = getProduct(item.slug); + const einzelpreis = produkt ? effektiverPreis(produkt) : item.preis; + return einzelpreis * (TREUE_RABATT_PROZENT / 100); +} + +/** Gutschein, Partnercode und Treuebonus können gleichzeitig aktiv sein — die Summe wird hier + * auf die Zwischensumme gedeckelt, damit der Gesamtpreis nie unter 0 € rutscht, egal wie + * großzügig die einzelnen Rabatte zusammen sind. */ +export function totalDiscount(subtotal: number): number { + return Math.min(subtotal, couponDiscount(subtotal) + partnerCodeDiscount(subtotal) + treuebonusDiscount()); } diff --git a/src/styles/global.css b/src/styles/global.css index 5a65072..8b77134 100644 --- a/src/styles/global.css +++ b/src/styles/global.css @@ -1476,6 +1476,67 @@ a:focus-visible, button:focus-visible { .account-nav-open { --nav-color: var(--c-accent); --nav-delay: 0.4s; } .account-nav-wishlist { --nav-color: var(--c-sale); --nav-delay: 0.8s; } .account-nav-data { --nav-color: #e3b23c; --nav-delay: 1.2s; } +.account-nav-loyalty { --nav-color: #6ecb8f; --nav-delay: 1.6s; } +/* Sobald der Treuebonus wirklich freigeschaltet ist (10/10), bekommt sein Knopf zusätzlich zur + normalen "Premium"-Optik noch einen kräftigeren, schneller pulsierenden Rahmen — soll auf + keinen Fall übersehen werden, wenn die Belohnung bereit ist. */ +.account-nav-loyalty.is-ready { animation-duration: 1.4s, 1.1s; } +.account-nav-loyalty.is-ready::after { height: 4px; } + +/* Treuebonus-Fortschrittskarte im Inhaltsbereich — eigener grüner Farbton, sonst dieselbe + "besondere" Formsprache wie die anderen Konto-Karten (siehe .profile-card). */ +.loyalty-card { position: relative; overflow: hidden; display: flex; flex-direction: column; gap: 0.9rem; } +.loyalty-progress-bar { + width: 100%; + height: 12px; + border-radius: 999px; + background: rgba(244, 241, 247, 0.1); + overflow: hidden; + border: 1px solid rgba(244, 241, 247, 0.14); +} +.loyalty-progress-fill { + height: 100%; + width: 0%; + border-radius: 999px; + background: linear-gradient(90deg, #6ecb8f, #5fd3ec); + transition: width 0.5s var(--ease); +} +.loyalty-progress-text { font-weight: 700; font-size: 1.05rem; margin: 0; } +.loyalty-explainer { color: var(--c-text-muted); margin: 0.4rem 0 0; } +.loyalty-card-sparkle { position: absolute; font-size: 0.85rem; pointer-events: none; z-index: 1; } +.loyalty-card-sparkle-1 { top: 1.1rem; right: 1.3rem; animation: account-nav-sparkle-twinkle 2.7s ease-in-out infinite; } +.loyalty-card-sparkle-2 { bottom: 1.2rem; left: 1.3rem; font-size: 0.7rem; animation: account-nav-sparkle-twinkle 3.5s ease-in-out infinite 0.5s; } +@media (prefers-reduced-motion: reduce) { + .loyalty-card-sparkle-1, .loyalty-card-sparkle-2 { animation: none; } +} + +/* Warenkorb: Treuebonus-Banner + Auswahl direkt an der betroffenen Zeile. */ +.loyalty-banner { + display: flex; + align-items: center; + gap: 0.9rem; + border-radius: var(--radius-m); + border: 1.5px solid color-mix(in srgb, #6ecb8f 45%, transparent); + background: linear-gradient(120deg, color-mix(in srgb, #6ecb8f 20%, transparent), color-mix(in srgb, #5fd3ec 14%, transparent)); + padding: 1rem 1.2rem; + margin-bottom: 1rem; +} +.loyalty-banner-icon { font-size: 1.6rem; flex-shrink: 0; } +.loyalty-banner strong { display: block; margin-bottom: 0.15em; } +.loyalty-banner p { margin: 0; color: var(--c-text-muted); font-size: 0.9rem; } +.loyalty-select { + display: inline-flex; + align-items: center; + gap: 0.4em; + font-size: 0.82rem; + font-weight: 600; + color: var(--c-text-muted); + cursor: pointer; + white-space: nowrap; +} +.loyalty-select input { accent-color: #6ecb8f; width: auto; } +.cart-item.loyalty-selected { box-shadow: inset 0 0 0 1.5px color-mix(in srgb, #6ecb8f 60%, transparent); border-radius: var(--radius-s); } +.loyalty-selected-badge { color: #6ecb8f; font-weight: 700; font-size: 0.82rem; white-space: nowrap; } .account-nav-sparkle { position: absolute; top: 0.55rem;