Abo-System: VanVan kann jetzt wirklich alles im Admin einstellen

Bisher waren nur Preis, Rabatt-% und ein paar Verhaltensregeln im CMS
editierbar — Namen, Vorteilstexte und die Teddy-Vergabe-Intervalle steckten
fest im Code. Jetzt ist pro Abo-Stufe (Klein/Mittel/Groß) direkt im
Adminbereich einstellbar:

- Name & Vorteilstext der Stufe, in allen 4 Sprachen einzeln
- Ob die Stufe aktuell überhaupt neu buchbar ist (bestehende Abonnent:innen
  behalten ihre Vorteile trotzdem, im Kundenkonto wird die Stufe nur für neue
  Buchungen ausgegraut mit "Derzeit nicht buchbar")
- Nach wie vielen Monaten es den kleinen/großen Teddy gibt (ersetzt die vorher
  fest verankerten 6-/12-Monats-Abstände)
- Welches Produkt als "kleiner"/"großer" Teddy für die Prämien-Einlösung
  verwendet wird (Produktauswahl statt festem Slug im Code)

Alle Berechnungen (Meilensteine, Fortschrittsanzeige) laufen jetzt komplett
über diese Admin-Werte statt über Konstanten im Code. Konto-Plan-Karten in
allen 4 Sprachen zeigen die admin-gepflegten Texte live an.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
qcigano
2026-08-03 17:03:28 +02:00
co-authored by Claude Sonnet 5
parent 208d995941
commit a222d6c96e
10 changed files with 204 additions and 57 deletions
+38 -7
View File
@@ -367,21 +367,52 @@ collections:
- label: "Kleines Abo" - label: "Kleines Abo"
name: klein name: klein
widget: object widget: object
fields: fields: &abo-stufe-fields
- { label: "Preis pro Monat (€)", name: preis, widget: number, value_type: float, step: 0.01 } - { label: "Preis pro Monat (€)", name: preis, widget: number, value_type: float, step: 0.01 }
- { label: "Rabatt auf jede Bestellung (%)", name: rabattProzent, widget: number, value_type: int, min: 0, max: 100 } - { label: "Rabatt auf jede Bestellung (%)", name: rabattProzent, widget: number, value_type: int, min: 0, max: 100 }
- { label: "Aktuell neu buchbar?", name: aktiv, widget: boolean, default: true, hint: "Aus: Diese Stufe erscheint im Konto ausgegraut und kann nicht neu abgeschlossen werden. Bestehende Kund:innen dieser Stufe behalten trotzdem alle Vorteile." }
- { label: "Kleiner Teddy: alle wie viele Monate?", name: kleinIntervallMonate, widget: number, value_type: int, min: 0, required: false, hint: "0 oder leer lassen = diese Stufe vergibt gar keinen kleinen Teddy." }
- { label: "Großer Teddy: alle wie viele Monate?", name: grossIntervallMonate, widget: number, value_type: int, min: 0, required: false, hint: "0 oder leer lassen = diese Stufe vergibt gar keinen großen Teddy." }
- label: "Name der Stufe"
name: name
widget: object
fields:
- { label: Deutsch, name: de, widget: string }
- { label: Englisch, name: en, widget: string }
- { label: "Schweizerdeutsch", name: ch, widget: string }
- { label: Französisch, name: fr, widget: string }
- label: "Vorteilstext (erscheint auf der Plan-Karte im Kundenkonto)"
name: vorteil
widget: object
fields:
- { label: Deutsch, name: de, widget: text }
- { label: Englisch, name: en, widget: text }
- { label: "Schweizerdeutsch", name: ch, widget: text }
- { label: Französisch, name: fr, widget: text }
- label: "Mittleres Abo" - label: "Mittleres Abo"
name: mittel name: mittel
widget: object widget: object
fields: fields: *abo-stufe-fields
- { label: "Preis pro Monat (€)", name: preis, widget: number, value_type: float, step: 0.01 }
- { label: "Rabatt auf jede Bestellung (%)", name: rabattProzent, widget: number, value_type: int, min: 0, max: 100 }
- label: "Großes Abo" - label: "Großes Abo"
name: gross name: gross
widget: object widget: object
fields: fields: *abo-stufe-fields
- { label: "Preis pro Monat (€)", name: preis, widget: number, value_type: float, step: 0.01 } - label: "Welches Produkt ist der 'kleine Teddy'?"
- { label: "Rabatt auf jede Bestellung (%)", name: rabattProzent, widget: number, value_type: int, min: 0, max: 100 } name: kleinTeddySlug
widget: relation
collection: products
search_fields: ["name.de"]
value_field: "slug"
display_fields: ["name.de"]
hint: "Dieses Produkt wird kostenlos in den Warenkorb gelegt, sobald jemand eine 'kleiner Teddy'-Prämie einlöst."
- label: "Welches Produkt ist der 'große Teddy'?"
name: grossTeddySlug
widget: relation
collection: products
search_fields: ["name.de"]
value_field: "slug"
display_fields: ["name.de"]
hint: "Dieses Produkt wird kostenlos in den Warenkorb gelegt, sobald jemand eine 'großer Teddy'-Prämie einlöst."
- label: "Abo-Rabatt mit anderen Rabatten kombinierbar (Gutschein/Partnercode/Treuebonus)?" - label: "Abo-Rabatt mit anderen Rabatten kombinierbar (Gutschein/Partnercode/Treuebonus)?"
name: kombinierbarMitAnderenRabatten name: kombinierbarMitAnderenRabatten
widget: boolean widget: boolean
+59 -3
View File
@@ -1,7 +1,63 @@
{ {
"klein": { "preis": 4.99, "rabattProzent": 5 }, "klein": {
"mittel": { "preis": 9.99, "rabattProzent": 10 }, "preis": 4.99,
"gross": { "preis": 19.99, "rabattProzent": 20 }, "rabattProzent": 5,
"aktiv": true,
"kleinIntervallMonate": 12,
"grossIntervallMonate": 0,
"name": {
"de": "Kleines Abo",
"en": "Small plan",
"ch": "Chliises Abo",
"fr": "Petit abonnement"
},
"vorteil": {
"de": "5 % Rabatt auf jede Bestellung. Nach jedem vollen Abo-Jahr: 1 kleiner Teddy gratis.",
"en": "5% off every order. After each full subscription year: 1 small teddy for free.",
"ch": "5 % Rabatt uf jedi Bstellig. Nach jedem volle Abo-Jahr: 1 chliine Teddy gratis.",
"fr": "5 % de remise sur chaque commande. Après chaque année d'abonnement complète : 1 petit ours en peluche offert."
}
},
"mittel": {
"preis": 9.99,
"rabattProzent": 10,
"aktiv": true,
"kleinIntervallMonate": 0,
"grossIntervallMonate": 12,
"name": {
"de": "Mittleres Abo",
"en": "Medium plan",
"ch": "Mittleres Abo",
"fr": "Abonnement moyen"
},
"vorteil": {
"de": "10 % Rabatt auf jede Bestellung. Nach jedem vollen Abo-Jahr: 1 großer Teddy gratis.",
"en": "10% off every order. After each full subscription year: 1 large teddy for free.",
"ch": "10 % Rabatt uf jedi Bstellig. Nach jedem volle Abo-Jahr: 1 grosse Teddy gratis.",
"fr": "10 % de remise sur chaque commande. Après chaque année d'abonnement complète : 1 grand ours en peluche offert."
}
},
"gross": {
"preis": 19.99,
"rabattProzent": 20,
"aktiv": true,
"kleinIntervallMonate": 6,
"grossIntervallMonate": 12,
"name": {
"de": "Großes Abo",
"en": "Large plan",
"ch": "Grosses Abo",
"fr": "Grand abonnement"
},
"vorteil": {
"de": "20 % Rabatt auf jede Bestellung. Alle 6 Monate ein kleiner Teddy, nach jedem vollen Jahr zusätzlich ein großer Teddy.",
"en": "20% off every order. A small teddy every 6 months, plus a large teddy after every full year.",
"ch": "20 % Rabatt uf jedi Bstellig. Alli 6 Monet en chliine Teddy, nach jedem volle Jahr zuesätzlich en grosse Teddy.",
"fr": "20 % de remise sur chaque commande. Un petit ours tous les 6 mois, et un grand ours en plus après chaque année complète."
}
},
"kleinTeddySlug": "kleiner-teddy",
"grossTeddySlug": "grosser-teddy",
"kombinierbarMitAnderenRabatten": false, "kombinierbarMitAnderenRabatten": false,
"versandKostenlosBeiPraemie": false, "versandKostenlosBeiPraemie": false,
"fortsetzungBeiWiederanmeldung": false, "fortsetzungBeiWiederanmeldung": false,
+50 -12
View File
@@ -8,15 +8,38 @@ import raw from "../content/abo-einstellungen.json";
export type AboStufe = "klein" | "mittel" | "gross"; export type AboStufe = "klein" | "mittel" | "gross";
export interface MehrsprachigerText {
de: string;
en: string;
ch: string;
fr: string;
}
export interface AboStufenKonfig { export interface AboStufenKonfig {
preis: number; preis: number;
rabattProzent: number; rabattProzent: number;
/** Ob diese Stufe aktuell neu buchbar ist. Bestehende Abonnent:innen dieser Stufe behalten
* ihre Vorteile unabhängig davon — nur NEUE Buchungen werden im Kundenkonto gesperrt. */
aktiv: boolean;
/** Nach wie vielen durchgehend bezahlten Monaten es jeweils einen kleinen/großen Teddy gibt.
* 0 oder nicht gesetzt = diese Stufe kennt diese Prämienart gar nicht. Admin-editierbar,
* ersetzt die früher fest im Code verankerten 6-/12-Monats-Intervalle. */
kleinIntervallMonate: number;
grossIntervallMonate: number;
/** Anzeigename/Vorteilstext der Stufe, pro Sprache — ersetzt die früher fest in i18n/ui.ts
* verankerten Texte, damit VanVan Name und Beschreibung jeder Stufe selbst pflegen kann. */
name: MehrsprachigerText;
vorteil: MehrsprachigerText;
} }
export interface AboEinstellungen { export interface AboEinstellungen {
klein: AboStufenKonfig; klein: AboStufenKonfig;
mittel: AboStufenKonfig; mittel: AboStufenKonfig;
gross: AboStufenKonfig; gross: AboStufenKonfig;
/** Produkt-Slugs (siehe src/content/products/*.json), die als "kleiner"/"großer" Teddy für
* die Prämien-Einlösung verwendet werden — im Admin per Produkt-Auswahl änderbar. */
kleinTeddySlug: string;
grossTeddySlug: string;
kombinierbarMitAnderenRabatten: boolean; kombinierbarMitAnderenRabatten: boolean;
versandKostenlosBeiPraemie: boolean; versandKostenlosBeiPraemie: boolean;
fortsetzungBeiWiederanmeldung: boolean; fortsetzungBeiWiederanmeldung: boolean;
@@ -31,27 +54,42 @@ export function aboStufenKonfig(stufe: AboStufe): AboStufenKonfig {
export const ABO_STUFEN: AboStufe[] = ["klein", "mittel", "gross"]; export const ABO_STUFEN: AboStufe[] = ["klein", "mittel", "gross"];
/** Name/Vorteilstext der Stufe in der gewünschten Sprache, mit Fallback auf Deutsch, falls das
* Feld im Admin (noch) leer gelassen wurde. */
export function aboStufeName(stufe: AboStufe, lang: keyof MehrsprachigerText): string {
const cfg = aboStufenKonfig(stufe);
return cfg.name?.[lang] || cfg.name?.de || stufe;
}
export function aboStufeVorteil(stufe: AboStufe, lang: keyof MehrsprachigerText): string {
const cfg = aboStufenKonfig(stufe);
return cfg.vorteil?.[lang] || cfg.vorteil?.de || "";
}
/** Wie viele Teddy-Prämien welcher Art bei X durchgehend bezahlten Monaten in dieser Stufe /** Wie viele Teddy-Prämien welcher Art bei X durchgehend bezahlten Monaten in dieser Stufe
* INSGESAMT freigeschaltet sein müssten (kumulativ, nicht nur "neu diesen Monat"): * INSGESAMT freigeschaltet sein müssten (kumulativ, nicht nur "neu diesen Monat") — rechnet
* - Klein-Abo: 1 kleiner Teddy pro volles Jahr. * rein aus den admin-konfigurierten Intervallen (siehe AboStufenKonfig), keine fest im Code
* - Mittel-Abo: 1 großer Teddy pro volles Jahr. * verankerten Zahlen mehr. Ein Intervall von 0 (oder nicht gesetzt) bedeutet "diese Stufe
* - Groß-Abo: 1 kleiner Teddy alle 6 Monate UND zusätzlich 1 großer Teddy pro volles Jahr * kennt diese Prämienart nicht". */
* (nach 6 Monaten: 1 klein · nach 12: 2 klein + 1 groß · nach 18: 3 klein · nach 24: 4 klein + 2 groß). */
export function teddyMeilensteine(stufe: AboStufe, monate: number): { klein: number; gross: number } { export function teddyMeilensteine(stufe: AboStufe, monate: number): { klein: number; gross: number } {
if (monate <= 0) return { klein: 0, gross: 0 }; if (monate <= 0) return { klein: 0, gross: 0 };
if (stufe === "klein") return { klein: Math.floor(monate / 12), gross: 0 }; const cfg = aboStufenKonfig(stufe);
if (stufe === "mittel") return { klein: 0, gross: Math.floor(monate / 12) }; const kleinIntervall = cfg.kleinIntervallMonate || 0;
return { klein: Math.floor(monate / 6), gross: Math.floor(monate / 12) }; const grossIntervall = cfg.grossIntervallMonate || 0;
return {
klein: kleinIntervall > 0 ? Math.floor(monate / kleinIntervall) : 0,
gross: grossIntervall > 0 ? Math.floor(monate / grossIntervall) : 0,
};
} }
/** Wie viele Monate fehlen noch bis zum jeweils nächsten Meilenstein (für die /** Wie viele Monate fehlen noch bis zum jeweils nächsten Meilenstein (für die
* Fortschrittsanzeige im Kundenkonto), null wenn diese Stufe die Prämienart gar nicht kennt. */ * Fortschrittsanzeige im Kundenkonto), null wenn diese Stufe die Prämienart gar nicht kennt. */
export function naechsteMeilensteine(stufe: AboStufe, monate: number): { klein: number | null; gross: number | null } { export function naechsteMeilensteine(stufe: AboStufe, monate: number): { klein: number | null; gross: number | null } {
const kleinIntervall = stufe === "gross" ? 6 : stufe === "klein" ? 12 : null; const cfg = aboStufenKonfig(stufe);
const grossIntervall = stufe === "klein" ? null : 12; const kleinIntervall = cfg.kleinIntervallMonate || 0;
const grossIntervall = cfg.grossIntervallMonate || 0;
return { return {
klein: kleinIntervall ? kleinIntervall - (monate % kleinIntervall) : null, klein: kleinIntervall > 0 ? kleinIntervall - (monate % kleinIntervall) : null,
gross: grossIntervall ? grossIntervall - (monate % grossIntervall) : null, gross: grossIntervall > 0 ? grossIntervall - (monate % grossIntervall) : null,
}; };
} }
+4
View File
@@ -252,6 +252,7 @@ export const ui = {
aboAbschliessenBtn: "Abo abschließen", aboAbschliessenBtn: "Abo abschließen",
aboAktivesLabel: "✓ Dein aktuelles Abo", aboAktivesLabel: "✓ Dein aktuelles Abo",
aboWechselnBtn: "Zu diesem Abo wechseln", aboWechselnBtn: "Zu diesem Abo wechseln",
aboNichtBuchbar: "Derzeit nicht buchbar",
aboStatusTitle: "Dein Abo-Status", aboStatusTitle: "Dein Abo-Status",
aboFieldStufe: "Abo-Stufe", aboFieldPreis: "Monatlicher Preis", aboFieldRabatt: "Enthaltener Rabatt", aboFieldStufe: "Abo-Stufe", aboFieldPreis: "Monatlicher Preis", aboFieldRabatt: "Enthaltener Rabatt",
aboFieldStart: "Startdatum", aboFieldNaechsteZahlung: "Nächste Zahlung", aboFieldLaufzeit: "Bisherige Laufzeit", aboFieldStart: "Startdatum", aboFieldNaechsteZahlung: "Nächste Zahlung", aboFieldLaufzeit: "Bisherige Laufzeit",
@@ -548,6 +549,7 @@ export const ui = {
aboAbschliessenBtn: "Subscribe", aboAbschliessenBtn: "Subscribe",
aboAktivesLabel: "✓ Your current plan", aboAktivesLabel: "✓ Your current plan",
aboWechselnBtn: "Switch to this plan", aboWechselnBtn: "Switch to this plan",
aboNichtBuchbar: "Currently unavailable",
aboStatusTitle: "Your subscription status", aboStatusTitle: "Your subscription status",
aboFieldStufe: "Plan", aboFieldPreis: "Monthly price", aboFieldRabatt: "Included discount", aboFieldStufe: "Plan", aboFieldPreis: "Monthly price", aboFieldRabatt: "Included discount",
aboFieldStart: "Start date", aboFieldNaechsteZahlung: "Next payment", aboFieldLaufzeit: "Duration so far", aboFieldStart: "Start date", aboFieldNaechsteZahlung: "Next payment", aboFieldLaufzeit: "Duration so far",
@@ -844,6 +846,7 @@ export const ui = {
aboAbschliessenBtn: "Abo abschliesse", aboAbschliessenBtn: "Abo abschliesse",
aboAktivesLabel: "✓ Dis aktuells Abo", aboAktivesLabel: "✓ Dis aktuells Abo",
aboWechselnBtn: "Zu dem Abo wechsle", aboWechselnBtn: "Zu dem Abo wechsle",
aboNichtBuchbar: "Momentan nöd buechbar",
aboStatusTitle: "Din Abo-Status", aboStatusTitle: "Din Abo-Status",
aboFieldStufe: "Abo-Stufe", aboFieldPreis: "Monetliche Priis", aboFieldRabatt: "Enthaltene Rabatt", aboFieldStufe: "Abo-Stufe", aboFieldPreis: "Monetliche Priis", aboFieldRabatt: "Enthaltene Rabatt",
aboFieldStart: "Startdatum", aboFieldNaechsteZahlung: "Nächschti Zahlig", aboFieldLaufzeit: "Bisherigi Laufziit", aboFieldStart: "Startdatum", aboFieldNaechsteZahlung: "Nächschti Zahlig", aboFieldLaufzeit: "Bisherigi Laufziit",
@@ -1140,6 +1143,7 @@ export const ui = {
aboAbschliessenBtn: "S'abonner", aboAbschliessenBtn: "S'abonner",
aboAktivesLabel: "✓ Votre abonnement actuel", aboAktivesLabel: "✓ Votre abonnement actuel",
aboWechselnBtn: "Passer à cet abonnement", aboWechselnBtn: "Passer à cet abonnement",
aboNichtBuchbar: "Actuellement indisponible",
aboStatusTitle: "Statut de votre abonnement", aboStatusTitle: "Statut de votre abonnement",
aboFieldStufe: "Formule", aboFieldPreis: "Prix mensuel", aboFieldRabatt: "Remise incluse", aboFieldStufe: "Formule", aboFieldPreis: "Prix mensuel", aboFieldRabatt: "Remise incluse",
aboFieldStart: "Date de début", aboFieldNaechsteZahlung: "Prochain paiement", aboFieldLaufzeit: "Durée jusqu'ici", aboFieldStart: "Date de début", aboFieldNaechsteZahlung: "Prochain paiement", aboFieldLaufzeit: "Durée jusqu'ici",
+12 -8
View File
@@ -5,7 +5,7 @@ import { products, getProduct } from "../../../data/products";
import type { Locale } from "../../../i18n/config"; import type { Locale } from "../../../i18n/config";
import { useTranslations } from "../../../i18n/ui"; import { useTranslations } from "../../../i18n/ui";
import { formatPrice } from "../../../i18n/format"; import { formatPrice } from "../../../i18n/format";
import { ABO_STUFEN, aboStufenKonfig, type AboStufe } from "../../../data/abo"; import { ABO_STUFEN, aboStufenKonfig, aboStufeName, aboStufeVorteil, type AboStufe } from "../../../data/abo";
const lang: Locale = "ch"; const lang: Locale = "ch";
const t = useTranslations(lang); const t = useTranslations(lang);
@@ -53,9 +53,9 @@ const statusLabel = {
// Abo-Pläne: Namen/Vorteilstexte kommen aus i18n, Preis/Rabatt aus den admin-editierbaren // Abo-Pläne: Namen/Vorteilstexte kommen aus i18n, Preis/Rabatt aus den admin-editierbaren
// Einstellungen (src/content/abo-einstellungen.json). Reihenfolge fest klein → mittel → groß. // Einstellungen (src/content/abo-einstellungen.json). Reihenfolge fest klein → mittel → groß.
const aboPlanLabels: Record<AboStufe, { name: string; vorteil: string }> = { const aboPlanLabels: Record<AboStufe, { name: string; vorteil: string }> = {
klein: { name: t.accountDash.aboNameKlein, vorteil: t.accountDash.aboVorteilKlein }, klein: { name: aboStufeName("klein", lang), vorteil: aboStufeVorteil("klein", lang) },
mittel: { name: t.accountDash.aboNameMittel, vorteil: t.accountDash.aboVorteilMittel }, mittel: { name: aboStufeName("mittel", lang), vorteil: aboStufeVorteil("mittel", lang) },
gross: { name: t.accountDash.aboNameGross, vorteil: t.accountDash.aboVorteilGross }, gross: { name: aboStufeName("gross", lang), vorteil: aboStufeVorteil("gross", lang) },
}; };
// "Zuletzt angesehen" zeigt in dieser Vorschau einfach die ersten echten Produkte aus dem Shop — // "Zuletzt angesehen" zeigt in dieser Vorschau einfach die ersten echten Produkte aus dem Shop —
@@ -350,7 +350,7 @@ const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status ===
const cfg = aboStufenKonfig(stufe); const cfg = aboStufenKonfig(stufe);
const labels = aboPlanLabels[stufe]; const labels = aboPlanLabels[stufe];
return ( return (
<div class={`card abo-plan-card abo-plan-${stufe}`} data-abo-plan={stufe}> <div class={`card abo-plan-card abo-plan-${stufe}`} data-abo-plan={stufe} data-abo-plan-aktiv={cfg.aktiv ? "1" : "0"}>
<span class="abo-plan-badge" data-abo-active-badge style="display:none;">{t.accountDash.aboAktivesLabel}</span> <span class="abo-plan-badge" data-abo-active-badge style="display:none;">{t.accountDash.aboAktivesLabel}</span>
<h3>{labels.name}</h3> <h3>{labels.name}</h3>
<p class="abo-plan-price">{formatPrice(cfg.preis, lang)} <span class="small">{t.accountDash.aboPreisSuffix}</span></p> <p class="abo-plan-price">{formatPrice(cfg.preis, lang)} <span class="small">{t.accountDash.aboPreisSuffix}</span></p>
@@ -410,7 +410,7 @@ const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status ===
</section> </section>
</Layout> </Layout>
<script define:vars={{ loginPath: "/ch/konto/", fallbackName: "Kundin", greetingTemplate: t.accountDash.greeting("{name}"), reviewedBadgeText: t.accountDash.reviewedBadge , invoiceLang: lang, invoiceLabels: { title: t.accountDash.invoiceTitle, numberLabel: t.accountDash.invoiceNumberLabel, dateLabel: t.accountDash.invoiceDateLabel, sellerLabel: t.accountDash.invoiceSellerLabel, billToLabel: t.accountDash.invoiceBillToLabel, itemLabel: t.accountDash.invoiceItemLabel, qtyLabel: t.accountDash.invoiceQtyLabel, unitPriceLabel: t.accountDash.invoiceUnitPriceLabel, sumLabel: t.accountDash.invoiceSumLabel, totalLabel: t.accountDash.orderTotal, vatNote: t.common.vatNote }, loyaltyProgressTemplate: t.accountDash.loyaltyProgressTemplate, loyaltyRemainingOne: t.accountDash.loyaltyRemainingOne, loyaltyRemainingOther: t.accountDash.loyaltyRemainingOther, loyaltyUnlockedTitle: t.accountDash.loyaltyUnlockedTitle, loyaltyUnlockedText: t.accountDash.loyaltyUnlockedText, aboLang: lang, aboLabels: { nameKlein: t.accountDash.aboNameKlein, nameMittel: t.accountDash.aboNameMittel, nameGross: t.accountDash.aboNameGross, preisSuffix: t.accountDash.aboPreisSuffix, monateSuffix: t.accountDash.aboMonateSuffix, fortschrittKleinTemplate: t.accountDash.aboFortschrittKleinTemplate, fortschrittGrossTemplate: t.accountDash.aboFortschrittGrossTemplate, praemienErhaltenTemplate: t.accountDash.aboPraemienErhaltenTemplate, aktivBadge: t.accountDash.aboAktivesLabel, nichtAktivBadge: t.accountDash.aboNichtAktivBadge, gekuendigtHinweisTemplate: t.accountDash.aboGekuendigtHinweisTemplate, kuendigenConfirm: t.accountDash.aboKuendigenConfirm, abschliessenBtn: t.accountDash.aboAbschliessenBtn, wechselnBtn: t.accountDash.aboWechselnBtn, keineBenachrichtigungen: t.accountDash.aboKeineBenachrichtigungen, notifKeyLabels: { abo_abgeschlossen: t.accountDash.notifAboAbgeschlossen, abo_gewechselt: t.accountDash.notifAboGewechselt, abo_zahlung_erfolgreich: t.accountDash.notifAboZahlungErfolgreich, abo_teddy_klein_frei: t.accountDash.notifAboTeddyKleinFrei, abo_teddy_gross_frei: t.accountDash.notifAboTeddyGrossFrei, abo_bald_praemie: t.accountDash.notifAboBaldPraemie, abo_gekuendigt: t.accountDash.notifAboGekuendigt, abo_teddy_klein_eingeloest: t.accountDash.notifAboTeddyKleinEingeloest, abo_teddy_gross_eingeloest: t.accountDash.notifAboTeddyGrossEingeloest } } }}> <script define:vars={{ loginPath: "/ch/konto/", fallbackName: "Kundin", greetingTemplate: t.accountDash.greeting("{name}"), reviewedBadgeText: t.accountDash.reviewedBadge , invoiceLang: lang, invoiceLabels: { title: t.accountDash.invoiceTitle, numberLabel: t.accountDash.invoiceNumberLabel, dateLabel: t.accountDash.invoiceDateLabel, sellerLabel: t.accountDash.invoiceSellerLabel, billToLabel: t.accountDash.invoiceBillToLabel, itemLabel: t.accountDash.invoiceItemLabel, qtyLabel: t.accountDash.invoiceQtyLabel, unitPriceLabel: t.accountDash.invoiceUnitPriceLabel, sumLabel: t.accountDash.invoiceSumLabel, totalLabel: t.accountDash.orderTotal, vatNote: t.common.vatNote }, loyaltyProgressTemplate: t.accountDash.loyaltyProgressTemplate, loyaltyRemainingOne: t.accountDash.loyaltyRemainingOne, loyaltyRemainingOther: t.accountDash.loyaltyRemainingOther, loyaltyUnlockedTitle: t.accountDash.loyaltyUnlockedTitle, loyaltyUnlockedText: t.accountDash.loyaltyUnlockedText, aboLang: lang, aboLabels: { nameKlein: t.accountDash.aboNameKlein, nameMittel: t.accountDash.aboNameMittel, nameGross: t.accountDash.aboNameGross, preisSuffix: t.accountDash.aboPreisSuffix, monateSuffix: t.accountDash.aboMonateSuffix, fortschrittKleinTemplate: t.accountDash.aboFortschrittKleinTemplate, fortschrittGrossTemplate: t.accountDash.aboFortschrittGrossTemplate, praemienErhaltenTemplate: t.accountDash.aboPraemienErhaltenTemplate, aktivBadge: t.accountDash.aboAktivesLabel, nichtAktivBadge: t.accountDash.aboNichtAktivBadge, gekuendigtHinweisTemplate: t.accountDash.aboGekuendigtHinweisTemplate, kuendigenConfirm: t.accountDash.aboKuendigenConfirm, abschliessenBtn: t.accountDash.aboAbschliessenBtn, wechselnBtn: t.accountDash.aboWechselnBtn, nichtBuchbar: t.accountDash.aboNichtBuchbar, keineBenachrichtigungen: t.accountDash.aboKeineBenachrichtigungen, notifKeyLabels: { abo_abgeschlossen: t.accountDash.notifAboAbgeschlossen, abo_gewechselt: t.accountDash.notifAboGewechselt, abo_zahlung_erfolgreich: t.accountDash.notifAboZahlungErfolgreich, abo_teddy_klein_frei: t.accountDash.notifAboTeddyKleinFrei, abo_teddy_gross_frei: t.accountDash.notifAboTeddyGrossFrei, abo_bald_praemie: t.accountDash.notifAboBaldPraemie, abo_gekuendigt: t.accountDash.notifAboGekuendigt, abo_teddy_klein_eingeloest: t.accountDash.notifAboTeddyKleinEingeloest, abo_teddy_gross_eingeloest: t.accountDash.notifAboTeddyGrossEingeloest } } }}>
window.__accountDashVars = { loginPath, fallbackName, greetingTemplate, reviewedBadgeText, invoiceLang, invoiceLabels, loyaltyProgressTemplate, loyaltyRemainingOne, loyaltyRemainingOther, loyaltyUnlockedTitle, loyaltyUnlockedText, aboLang, aboLabels }; window.__accountDashVars = { loginPath, fallbackName, greetingTemplate, reviewedBadgeText, invoiceLang, invoiceLabels, loyaltyProgressTemplate, loyaltyRemainingOne, loyaltyRemainingOther, loyaltyUnlockedTitle, loyaltyUnlockedText, aboLang, aboLabels };
</script> </script>
<script> <script>
@@ -594,12 +594,16 @@ const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status ===
const badge = card.querySelector("[data-abo-active-badge]") as HTMLElement | null; const badge = card.querySelector("[data-abo-active-badge]") as HTMLElement | null;
if (badge) badge.style.display = isCurrent ? "" : "none"; if (badge) badge.style.display = isCurrent ? "" : "none";
card.classList.toggle("is-current-plan", isCurrent); card.classList.toggle("is-current-plan", isCurrent);
const buchbar = card.dataset.aboPlanAktiv !== "0";
card.classList.toggle("is-not-bookable", !buchbar && !isCurrent);
const btn = card.querySelector("[data-abo-choose]") as HTMLButtonElement | null; const btn = card.querySelector("[data-abo-choose]") as HTMLButtonElement | null;
if (btn) { if (btn) {
btn.disabled = isCurrent; btn.disabled = isCurrent || (!buchbar && !isCurrent);
btn.textContent = isCurrent btn.textContent = isCurrent
? aboLabels.aktivBadge ? aboLabels.aktivBadge
: (status.stufe && status.aktiv && !a.abo.gekuendigt ? aboLabels.wechselnBtn : aboLabels.abschliessenBtn); : !buchbar
? aboLabels.nichtBuchbar
: (status.stufe && status.aktiv && !a.abo.gekuendigt ? aboLabels.wechselnBtn : aboLabels.abschliessenBtn);
} }
}); });
+12 -8
View File
@@ -5,7 +5,7 @@ import { products, getProduct } from "../../../data/products";
import type { Locale } from "../../../i18n/config"; import type { Locale } from "../../../i18n/config";
import { useTranslations } from "../../../i18n/ui"; import { useTranslations } from "../../../i18n/ui";
import { formatPrice } from "../../../i18n/format"; import { formatPrice } from "../../../i18n/format";
import { ABO_STUFEN, aboStufenKonfig, type AboStufe } from "../../../data/abo"; import { ABO_STUFEN, aboStufenKonfig, aboStufeName, aboStufeVorteil, type AboStufe } from "../../../data/abo";
const lang: Locale = "en"; const lang: Locale = "en";
const t = useTranslations(lang); const t = useTranslations(lang);
@@ -53,9 +53,9 @@ const statusLabel = {
// Abo-Pläne: Namen/Vorteilstexte kommen aus i18n, Preis/Rabatt aus den admin-editierbaren // Abo-Pläne: Namen/Vorteilstexte kommen aus i18n, Preis/Rabatt aus den admin-editierbaren
// Einstellungen (src/content/abo-einstellungen.json). Reihenfolge fest klein → mittel → groß. // Einstellungen (src/content/abo-einstellungen.json). Reihenfolge fest klein → mittel → groß.
const aboPlanLabels: Record<AboStufe, { name: string; vorteil: string }> = { const aboPlanLabels: Record<AboStufe, { name: string; vorteil: string }> = {
klein: { name: t.accountDash.aboNameKlein, vorteil: t.accountDash.aboVorteilKlein }, klein: { name: aboStufeName("klein", lang), vorteil: aboStufeVorteil("klein", lang) },
mittel: { name: t.accountDash.aboNameMittel, vorteil: t.accountDash.aboVorteilMittel }, mittel: { name: aboStufeName("mittel", lang), vorteil: aboStufeVorteil("mittel", lang) },
gross: { name: t.accountDash.aboNameGross, vorteil: t.accountDash.aboVorteilGross }, gross: { name: aboStufeName("gross", lang), vorteil: aboStufeVorteil("gross", lang) },
}; };
// "Zuletzt angesehen" zeigt in dieser Vorschau einfach die ersten echten Produkte aus dem Shop — // "Zuletzt angesehen" zeigt in dieser Vorschau einfach die ersten echten Produkte aus dem Shop —
@@ -350,7 +350,7 @@ const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status ===
const cfg = aboStufenKonfig(stufe); const cfg = aboStufenKonfig(stufe);
const labels = aboPlanLabels[stufe]; const labels = aboPlanLabels[stufe];
return ( return (
<div class={`card abo-plan-card abo-plan-${stufe}`} data-abo-plan={stufe}> <div class={`card abo-plan-card abo-plan-${stufe}`} data-abo-plan={stufe} data-abo-plan-aktiv={cfg.aktiv ? "1" : "0"}>
<span class="abo-plan-badge" data-abo-active-badge style="display:none;">{t.accountDash.aboAktivesLabel}</span> <span class="abo-plan-badge" data-abo-active-badge style="display:none;">{t.accountDash.aboAktivesLabel}</span>
<h3>{labels.name}</h3> <h3>{labels.name}</h3>
<p class="abo-plan-price">{formatPrice(cfg.preis, lang)} <span class="small">{t.accountDash.aboPreisSuffix}</span></p> <p class="abo-plan-price">{formatPrice(cfg.preis, lang)} <span class="small">{t.accountDash.aboPreisSuffix}</span></p>
@@ -410,7 +410,7 @@ const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status ===
</section> </section>
</Layout> </Layout>
<script define:vars={{ loginPath: "/en/konto/", fallbackName: "Customer", greetingTemplate: t.accountDash.greeting("{name}"), reviewedBadgeText: t.accountDash.reviewedBadge , invoiceLang: lang, invoiceLabels: { title: t.accountDash.invoiceTitle, numberLabel: t.accountDash.invoiceNumberLabel, dateLabel: t.accountDash.invoiceDateLabel, sellerLabel: t.accountDash.invoiceSellerLabel, billToLabel: t.accountDash.invoiceBillToLabel, itemLabel: t.accountDash.invoiceItemLabel, qtyLabel: t.accountDash.invoiceQtyLabel, unitPriceLabel: t.accountDash.invoiceUnitPriceLabel, sumLabel: t.accountDash.invoiceSumLabel, totalLabel: t.accountDash.orderTotal, vatNote: t.common.vatNote }, loyaltyProgressTemplate: t.accountDash.loyaltyProgressTemplate, loyaltyRemainingOne: t.accountDash.loyaltyRemainingOne, loyaltyRemainingOther: t.accountDash.loyaltyRemainingOther, loyaltyUnlockedTitle: t.accountDash.loyaltyUnlockedTitle, loyaltyUnlockedText: t.accountDash.loyaltyUnlockedText, aboLang: lang, aboLabels: { nameKlein: t.accountDash.aboNameKlein, nameMittel: t.accountDash.aboNameMittel, nameGross: t.accountDash.aboNameGross, preisSuffix: t.accountDash.aboPreisSuffix, monateSuffix: t.accountDash.aboMonateSuffix, fortschrittKleinTemplate: t.accountDash.aboFortschrittKleinTemplate, fortschrittGrossTemplate: t.accountDash.aboFortschrittGrossTemplate, praemienErhaltenTemplate: t.accountDash.aboPraemienErhaltenTemplate, aktivBadge: t.accountDash.aboAktivesLabel, nichtAktivBadge: t.accountDash.aboNichtAktivBadge, gekuendigtHinweisTemplate: t.accountDash.aboGekuendigtHinweisTemplate, kuendigenConfirm: t.accountDash.aboKuendigenConfirm, abschliessenBtn: t.accountDash.aboAbschliessenBtn, wechselnBtn: t.accountDash.aboWechselnBtn, keineBenachrichtigungen: t.accountDash.aboKeineBenachrichtigungen, notifKeyLabels: { abo_abgeschlossen: t.accountDash.notifAboAbgeschlossen, abo_gewechselt: t.accountDash.notifAboGewechselt, abo_zahlung_erfolgreich: t.accountDash.notifAboZahlungErfolgreich, abo_teddy_klein_frei: t.accountDash.notifAboTeddyKleinFrei, abo_teddy_gross_frei: t.accountDash.notifAboTeddyGrossFrei, abo_bald_praemie: t.accountDash.notifAboBaldPraemie, abo_gekuendigt: t.accountDash.notifAboGekuendigt, abo_teddy_klein_eingeloest: t.accountDash.notifAboTeddyKleinEingeloest, abo_teddy_gross_eingeloest: t.accountDash.notifAboTeddyGrossEingeloest } } }}> <script define:vars={{ loginPath: "/en/konto/", fallbackName: "Customer", greetingTemplate: t.accountDash.greeting("{name}"), reviewedBadgeText: t.accountDash.reviewedBadge , invoiceLang: lang, invoiceLabels: { title: t.accountDash.invoiceTitle, numberLabel: t.accountDash.invoiceNumberLabel, dateLabel: t.accountDash.invoiceDateLabel, sellerLabel: t.accountDash.invoiceSellerLabel, billToLabel: t.accountDash.invoiceBillToLabel, itemLabel: t.accountDash.invoiceItemLabel, qtyLabel: t.accountDash.invoiceQtyLabel, unitPriceLabel: t.accountDash.invoiceUnitPriceLabel, sumLabel: t.accountDash.invoiceSumLabel, totalLabel: t.accountDash.orderTotal, vatNote: t.common.vatNote }, loyaltyProgressTemplate: t.accountDash.loyaltyProgressTemplate, loyaltyRemainingOne: t.accountDash.loyaltyRemainingOne, loyaltyRemainingOther: t.accountDash.loyaltyRemainingOther, loyaltyUnlockedTitle: t.accountDash.loyaltyUnlockedTitle, loyaltyUnlockedText: t.accountDash.loyaltyUnlockedText, aboLang: lang, aboLabels: { nameKlein: t.accountDash.aboNameKlein, nameMittel: t.accountDash.aboNameMittel, nameGross: t.accountDash.aboNameGross, preisSuffix: t.accountDash.aboPreisSuffix, monateSuffix: t.accountDash.aboMonateSuffix, fortschrittKleinTemplate: t.accountDash.aboFortschrittKleinTemplate, fortschrittGrossTemplate: t.accountDash.aboFortschrittGrossTemplate, praemienErhaltenTemplate: t.accountDash.aboPraemienErhaltenTemplate, aktivBadge: t.accountDash.aboAktivesLabel, nichtAktivBadge: t.accountDash.aboNichtAktivBadge, gekuendigtHinweisTemplate: t.accountDash.aboGekuendigtHinweisTemplate, kuendigenConfirm: t.accountDash.aboKuendigenConfirm, abschliessenBtn: t.accountDash.aboAbschliessenBtn, wechselnBtn: t.accountDash.aboWechselnBtn, nichtBuchbar: t.accountDash.aboNichtBuchbar, keineBenachrichtigungen: t.accountDash.aboKeineBenachrichtigungen, notifKeyLabels: { abo_abgeschlossen: t.accountDash.notifAboAbgeschlossen, abo_gewechselt: t.accountDash.notifAboGewechselt, abo_zahlung_erfolgreich: t.accountDash.notifAboZahlungErfolgreich, abo_teddy_klein_frei: t.accountDash.notifAboTeddyKleinFrei, abo_teddy_gross_frei: t.accountDash.notifAboTeddyGrossFrei, abo_bald_praemie: t.accountDash.notifAboBaldPraemie, abo_gekuendigt: t.accountDash.notifAboGekuendigt, abo_teddy_klein_eingeloest: t.accountDash.notifAboTeddyKleinEingeloest, abo_teddy_gross_eingeloest: t.accountDash.notifAboTeddyGrossEingeloest } } }}>
window.__accountDashVars = { loginPath, fallbackName, greetingTemplate, reviewedBadgeText, invoiceLang, invoiceLabels, loyaltyProgressTemplate, loyaltyRemainingOne, loyaltyRemainingOther, loyaltyUnlockedTitle, loyaltyUnlockedText, aboLang, aboLabels }; window.__accountDashVars = { loginPath, fallbackName, greetingTemplate, reviewedBadgeText, invoiceLang, invoiceLabels, loyaltyProgressTemplate, loyaltyRemainingOne, loyaltyRemainingOther, loyaltyUnlockedTitle, loyaltyUnlockedText, aboLang, aboLabels };
</script> </script>
<script> <script>
@@ -594,12 +594,16 @@ const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status ===
const badge = card.querySelector("[data-abo-active-badge]") as HTMLElement | null; const badge = card.querySelector("[data-abo-active-badge]") as HTMLElement | null;
if (badge) badge.style.display = isCurrent ? "" : "none"; if (badge) badge.style.display = isCurrent ? "" : "none";
card.classList.toggle("is-current-plan", isCurrent); card.classList.toggle("is-current-plan", isCurrent);
const buchbar = card.dataset.aboPlanAktiv !== "0";
card.classList.toggle("is-not-bookable", !buchbar && !isCurrent);
const btn = card.querySelector("[data-abo-choose]") as HTMLButtonElement | null; const btn = card.querySelector("[data-abo-choose]") as HTMLButtonElement | null;
if (btn) { if (btn) {
btn.disabled = isCurrent; btn.disabled = isCurrent || (!buchbar && !isCurrent);
btn.textContent = isCurrent btn.textContent = isCurrent
? aboLabels.aktivBadge ? aboLabels.aktivBadge
: (status.stufe && status.aktiv && !a.abo.gekuendigt ? aboLabels.wechselnBtn : aboLabels.abschliessenBtn); : !buchbar
? aboLabels.nichtBuchbar
: (status.stufe && status.aktiv && !a.abo.gekuendigt ? aboLabels.wechselnBtn : aboLabels.abschliessenBtn);
} }
}); });
+12 -8
View File
@@ -5,7 +5,7 @@ import { products, getProduct } from "../../../data/products";
import type { Locale } from "../../../i18n/config"; import type { Locale } from "../../../i18n/config";
import { useTranslations } from "../../../i18n/ui"; import { useTranslations } from "../../../i18n/ui";
import { formatPrice } from "../../../i18n/format"; import { formatPrice } from "../../../i18n/format";
import { ABO_STUFEN, aboStufenKonfig, type AboStufe } from "../../../data/abo"; import { ABO_STUFEN, aboStufenKonfig, aboStufeName, aboStufeVorteil, type AboStufe } from "../../../data/abo";
const lang: Locale = "fr"; const lang: Locale = "fr";
const t = useTranslations(lang); const t = useTranslations(lang);
@@ -53,9 +53,9 @@ const statusLabel = {
// Abo-Pläne: Namen/Vorteilstexte kommen aus i18n, Preis/Rabatt aus den admin-editierbaren // Abo-Pläne: Namen/Vorteilstexte kommen aus i18n, Preis/Rabatt aus den admin-editierbaren
// Einstellungen (src/content/abo-einstellungen.json). Reihenfolge fest klein → mittel → groß. // Einstellungen (src/content/abo-einstellungen.json). Reihenfolge fest klein → mittel → groß.
const aboPlanLabels: Record<AboStufe, { name: string; vorteil: string }> = { const aboPlanLabels: Record<AboStufe, { name: string; vorteil: string }> = {
klein: { name: t.accountDash.aboNameKlein, vorteil: t.accountDash.aboVorteilKlein }, klein: { name: aboStufeName("klein", lang), vorteil: aboStufeVorteil("klein", lang) },
mittel: { name: t.accountDash.aboNameMittel, vorteil: t.accountDash.aboVorteilMittel }, mittel: { name: aboStufeName("mittel", lang), vorteil: aboStufeVorteil("mittel", lang) },
gross: { name: t.accountDash.aboNameGross, vorteil: t.accountDash.aboVorteilGross }, gross: { name: aboStufeName("gross", lang), vorteil: aboStufeVorteil("gross", lang) },
}; };
// "Zuletzt angesehen" zeigt in dieser Vorschau einfach die ersten echten Produkte aus dem Shop — // "Zuletzt angesehen" zeigt in dieser Vorschau einfach die ersten echten Produkte aus dem Shop —
@@ -350,7 +350,7 @@ const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status ===
const cfg = aboStufenKonfig(stufe); const cfg = aboStufenKonfig(stufe);
const labels = aboPlanLabels[stufe]; const labels = aboPlanLabels[stufe];
return ( return (
<div class={`card abo-plan-card abo-plan-${stufe}`} data-abo-plan={stufe}> <div class={`card abo-plan-card abo-plan-${stufe}`} data-abo-plan={stufe} data-abo-plan-aktiv={cfg.aktiv ? "1" : "0"}>
<span class="abo-plan-badge" data-abo-active-badge style="display:none;">{t.accountDash.aboAktivesLabel}</span> <span class="abo-plan-badge" data-abo-active-badge style="display:none;">{t.accountDash.aboAktivesLabel}</span>
<h3>{labels.name}</h3> <h3>{labels.name}</h3>
<p class="abo-plan-price">{formatPrice(cfg.preis, lang)} <span class="small">{t.accountDash.aboPreisSuffix}</span></p> <p class="abo-plan-price">{formatPrice(cfg.preis, lang)} <span class="small">{t.accountDash.aboPreisSuffix}</span></p>
@@ -410,7 +410,7 @@ const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status ===
</section> </section>
</Layout> </Layout>
<script define:vars={{ loginPath: "/fr/konto/", fallbackName: "Cliente", greetingTemplate: t.accountDash.greeting("{name}"), reviewedBadgeText: t.accountDash.reviewedBadge , invoiceLang: lang, invoiceLabels: { title: t.accountDash.invoiceTitle, numberLabel: t.accountDash.invoiceNumberLabel, dateLabel: t.accountDash.invoiceDateLabel, sellerLabel: t.accountDash.invoiceSellerLabel, billToLabel: t.accountDash.invoiceBillToLabel, itemLabel: t.accountDash.invoiceItemLabel, qtyLabel: t.accountDash.invoiceQtyLabel, unitPriceLabel: t.accountDash.invoiceUnitPriceLabel, sumLabel: t.accountDash.invoiceSumLabel, totalLabel: t.accountDash.orderTotal, vatNote: t.common.vatNote }, loyaltyProgressTemplate: t.accountDash.loyaltyProgressTemplate, loyaltyRemainingOne: t.accountDash.loyaltyRemainingOne, loyaltyRemainingOther: t.accountDash.loyaltyRemainingOther, loyaltyUnlockedTitle: t.accountDash.loyaltyUnlockedTitle, loyaltyUnlockedText: t.accountDash.loyaltyUnlockedText, aboLang: lang, aboLabels: { nameKlein: t.accountDash.aboNameKlein, nameMittel: t.accountDash.aboNameMittel, nameGross: t.accountDash.aboNameGross, preisSuffix: t.accountDash.aboPreisSuffix, monateSuffix: t.accountDash.aboMonateSuffix, fortschrittKleinTemplate: t.accountDash.aboFortschrittKleinTemplate, fortschrittGrossTemplate: t.accountDash.aboFortschrittGrossTemplate, praemienErhaltenTemplate: t.accountDash.aboPraemienErhaltenTemplate, aktivBadge: t.accountDash.aboAktivesLabel, nichtAktivBadge: t.accountDash.aboNichtAktivBadge, gekuendigtHinweisTemplate: t.accountDash.aboGekuendigtHinweisTemplate, kuendigenConfirm: t.accountDash.aboKuendigenConfirm, abschliessenBtn: t.accountDash.aboAbschliessenBtn, wechselnBtn: t.accountDash.aboWechselnBtn, keineBenachrichtigungen: t.accountDash.aboKeineBenachrichtigungen, notifKeyLabels: { abo_abgeschlossen: t.accountDash.notifAboAbgeschlossen, abo_gewechselt: t.accountDash.notifAboGewechselt, abo_zahlung_erfolgreich: t.accountDash.notifAboZahlungErfolgreich, abo_teddy_klein_frei: t.accountDash.notifAboTeddyKleinFrei, abo_teddy_gross_frei: t.accountDash.notifAboTeddyGrossFrei, abo_bald_praemie: t.accountDash.notifAboBaldPraemie, abo_gekuendigt: t.accountDash.notifAboGekuendigt, abo_teddy_klein_eingeloest: t.accountDash.notifAboTeddyKleinEingeloest, abo_teddy_gross_eingeloest: t.accountDash.notifAboTeddyGrossEingeloest } } }}> <script define:vars={{ loginPath: "/fr/konto/", fallbackName: "Cliente", greetingTemplate: t.accountDash.greeting("{name}"), reviewedBadgeText: t.accountDash.reviewedBadge , invoiceLang: lang, invoiceLabels: { title: t.accountDash.invoiceTitle, numberLabel: t.accountDash.invoiceNumberLabel, dateLabel: t.accountDash.invoiceDateLabel, sellerLabel: t.accountDash.invoiceSellerLabel, billToLabel: t.accountDash.invoiceBillToLabel, itemLabel: t.accountDash.invoiceItemLabel, qtyLabel: t.accountDash.invoiceQtyLabel, unitPriceLabel: t.accountDash.invoiceUnitPriceLabel, sumLabel: t.accountDash.invoiceSumLabel, totalLabel: t.accountDash.orderTotal, vatNote: t.common.vatNote }, loyaltyProgressTemplate: t.accountDash.loyaltyProgressTemplate, loyaltyRemainingOne: t.accountDash.loyaltyRemainingOne, loyaltyRemainingOther: t.accountDash.loyaltyRemainingOther, loyaltyUnlockedTitle: t.accountDash.loyaltyUnlockedTitle, loyaltyUnlockedText: t.accountDash.loyaltyUnlockedText, aboLang: lang, aboLabels: { nameKlein: t.accountDash.aboNameKlein, nameMittel: t.accountDash.aboNameMittel, nameGross: t.accountDash.aboNameGross, preisSuffix: t.accountDash.aboPreisSuffix, monateSuffix: t.accountDash.aboMonateSuffix, fortschrittKleinTemplate: t.accountDash.aboFortschrittKleinTemplate, fortschrittGrossTemplate: t.accountDash.aboFortschrittGrossTemplate, praemienErhaltenTemplate: t.accountDash.aboPraemienErhaltenTemplate, aktivBadge: t.accountDash.aboAktivesLabel, nichtAktivBadge: t.accountDash.aboNichtAktivBadge, gekuendigtHinweisTemplate: t.accountDash.aboGekuendigtHinweisTemplate, kuendigenConfirm: t.accountDash.aboKuendigenConfirm, abschliessenBtn: t.accountDash.aboAbschliessenBtn, wechselnBtn: t.accountDash.aboWechselnBtn, nichtBuchbar: t.accountDash.aboNichtBuchbar, keineBenachrichtigungen: t.accountDash.aboKeineBenachrichtigungen, notifKeyLabels: { abo_abgeschlossen: t.accountDash.notifAboAbgeschlossen, abo_gewechselt: t.accountDash.notifAboGewechselt, abo_zahlung_erfolgreich: t.accountDash.notifAboZahlungErfolgreich, abo_teddy_klein_frei: t.accountDash.notifAboTeddyKleinFrei, abo_teddy_gross_frei: t.accountDash.notifAboTeddyGrossFrei, abo_bald_praemie: t.accountDash.notifAboBaldPraemie, abo_gekuendigt: t.accountDash.notifAboGekuendigt, abo_teddy_klein_eingeloest: t.accountDash.notifAboTeddyKleinEingeloest, abo_teddy_gross_eingeloest: t.accountDash.notifAboTeddyGrossEingeloest } } }}>
window.__accountDashVars = { loginPath, fallbackName, greetingTemplate, reviewedBadgeText, invoiceLang, invoiceLabels, loyaltyProgressTemplate, loyaltyRemainingOne, loyaltyRemainingOther, loyaltyUnlockedTitle, loyaltyUnlockedText, aboLang, aboLabels }; window.__accountDashVars = { loginPath, fallbackName, greetingTemplate, reviewedBadgeText, invoiceLang, invoiceLabels, loyaltyProgressTemplate, loyaltyRemainingOne, loyaltyRemainingOther, loyaltyUnlockedTitle, loyaltyUnlockedText, aboLang, aboLabels };
</script> </script>
<script> <script>
@@ -594,12 +594,16 @@ const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status ===
const badge = card.querySelector("[data-abo-active-badge]") as HTMLElement | null; const badge = card.querySelector("[data-abo-active-badge]") as HTMLElement | null;
if (badge) badge.style.display = isCurrent ? "" : "none"; if (badge) badge.style.display = isCurrent ? "" : "none";
card.classList.toggle("is-current-plan", isCurrent); card.classList.toggle("is-current-plan", isCurrent);
const buchbar = card.dataset.aboPlanAktiv !== "0";
card.classList.toggle("is-not-bookable", !buchbar && !isCurrent);
const btn = card.querySelector("[data-abo-choose]") as HTMLButtonElement | null; const btn = card.querySelector("[data-abo-choose]") as HTMLButtonElement | null;
if (btn) { if (btn) {
btn.disabled = isCurrent; btn.disabled = isCurrent || (!buchbar && !isCurrent);
btn.textContent = isCurrent btn.textContent = isCurrent
? aboLabels.aktivBadge ? aboLabels.aktivBadge
: (status.stufe && status.aktiv && !a.abo.gekuendigt ? aboLabels.wechselnBtn : aboLabels.abschliessenBtn); : !buchbar
? aboLabels.nichtBuchbar
: (status.stufe && status.aktiv && !a.abo.gekuendigt ? aboLabels.wechselnBtn : aboLabels.abschliessenBtn);
} }
}); });
+15 -10
View File
@@ -5,7 +5,7 @@ import { products, getProduct } from "../../data/products";
import type { Locale } from "../../i18n/config"; import type { Locale } from "../../i18n/config";
import { useTranslations } from "../../i18n/ui"; import { useTranslations } from "../../i18n/ui";
import { formatPrice } from "../../i18n/format"; import { formatPrice } from "../../i18n/format";
import { ABO_STUFEN, aboStufenKonfig, type AboStufe } from "../../data/abo"; import { ABO_STUFEN, aboStufenKonfig, aboStufeName, aboStufeVorteil, type AboStufe } from "../../data/abo";
const lang: Locale = "de"; const lang: Locale = "de";
const t = useTranslations(lang); const t = useTranslations(lang);
@@ -50,12 +50,13 @@ const statusLabel = {
abgeschlossen: t.accountDash.statusAbgeschlossen, abgeschlossen: t.accountDash.statusAbgeschlossen,
} as const; } as const;
// Abo-Pläne: Namen/Vorteilstexte kommen aus i18n, Preis/Rabatt aus den admin-editierbaren // Abo-Pläne: Namen/Vorteilstexte UND Preis/Rabatt kommen komplett aus den admin-editierbaren
// Einstellungen (src/content/abo-einstellungen.json). Reihenfolge fest klein → mittel → groß. // Einstellungen (src/content/abo-einstellungen.json) — VanVan kann im CMS alles zum Abo selbst
// pflegen. Reihenfolge fest klein → mittel → groß.
const aboPlanLabels: Record<AboStufe, { name: string; vorteil: string }> = { const aboPlanLabels: Record<AboStufe, { name: string; vorteil: string }> = {
klein: { name: t.accountDash.aboNameKlein, vorteil: t.accountDash.aboVorteilKlein }, klein: { name: aboStufeName("klein", lang), vorteil: aboStufeVorteil("klein", lang) },
mittel: { name: t.accountDash.aboNameMittel, vorteil: t.accountDash.aboVorteilMittel }, mittel: { name: aboStufeName("mittel", lang), vorteil: aboStufeVorteil("mittel", lang) },
gross: { name: t.accountDash.aboNameGross, vorteil: t.accountDash.aboVorteilGross }, gross: { name: aboStufeName("gross", lang), vorteil: aboStufeVorteil("gross", lang) },
}; };
// "Zuletzt angesehen" zeigt in dieser Vorschau einfach die ersten echten Produkte aus dem Shop — // "Zuletzt angesehen" zeigt in dieser Vorschau einfach die ersten echten Produkte aus dem Shop —
@@ -350,7 +351,7 @@ const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status ===
const cfg = aboStufenKonfig(stufe); const cfg = aboStufenKonfig(stufe);
const labels = aboPlanLabels[stufe]; const labels = aboPlanLabels[stufe];
return ( return (
<div class={`card abo-plan-card abo-plan-${stufe}`} data-abo-plan={stufe}> <div class={`card abo-plan-card abo-plan-${stufe}`} data-abo-plan={stufe} data-abo-plan-aktiv={cfg.aktiv ? "1" : "0"}>
<span class="abo-plan-badge" data-abo-active-badge style="display:none;">{t.accountDash.aboAktivesLabel}</span> <span class="abo-plan-badge" data-abo-active-badge style="display:none;">{t.accountDash.aboAktivesLabel}</span>
<h3>{labels.name}</h3> <h3>{labels.name}</h3>
<p class="abo-plan-price">{formatPrice(cfg.preis, lang)} <span class="small">{t.accountDash.aboPreisSuffix}</span></p> <p class="abo-plan-price">{formatPrice(cfg.preis, lang)} <span class="small">{t.accountDash.aboPreisSuffix}</span></p>
@@ -410,7 +411,7 @@ const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status ===
</section> </section>
</Layout> </Layout>
<script define:vars={{ loginPath: "/konto/", fallbackName: "Kundin", greetingTemplate: t.accountDash.greeting("{name}"), reviewedBadgeText: t.accountDash.reviewedBadge , invoiceLang: lang, invoiceLabels: { title: t.accountDash.invoiceTitle, numberLabel: t.accountDash.invoiceNumberLabel, dateLabel: t.accountDash.invoiceDateLabel, sellerLabel: t.accountDash.invoiceSellerLabel, billToLabel: t.accountDash.invoiceBillToLabel, itemLabel: t.accountDash.invoiceItemLabel, qtyLabel: t.accountDash.invoiceQtyLabel, unitPriceLabel: t.accountDash.invoiceUnitPriceLabel, sumLabel: t.accountDash.invoiceSumLabel, totalLabel: t.accountDash.orderTotal, vatNote: t.common.vatNote }, loyaltyProgressTemplate: t.accountDash.loyaltyProgressTemplate, loyaltyRemainingOne: t.accountDash.loyaltyRemainingOne, loyaltyRemainingOther: t.accountDash.loyaltyRemainingOther, loyaltyUnlockedTitle: t.accountDash.loyaltyUnlockedTitle, loyaltyUnlockedText: t.accountDash.loyaltyUnlockedText, aboLang: lang, aboLabels: { nameKlein: t.accountDash.aboNameKlein, nameMittel: t.accountDash.aboNameMittel, nameGross: t.accountDash.aboNameGross, preisSuffix: t.accountDash.aboPreisSuffix, monateSuffix: t.accountDash.aboMonateSuffix, fortschrittKleinTemplate: t.accountDash.aboFortschrittKleinTemplate, fortschrittGrossTemplate: t.accountDash.aboFortschrittGrossTemplate, praemienErhaltenTemplate: t.accountDash.aboPraemienErhaltenTemplate, aktivBadge: t.accountDash.aboAktivesLabel, nichtAktivBadge: t.accountDash.aboNichtAktivBadge, gekuendigtHinweisTemplate: t.accountDash.aboGekuendigtHinweisTemplate, kuendigenConfirm: t.accountDash.aboKuendigenConfirm, abschliessenBtn: t.accountDash.aboAbschliessenBtn, wechselnBtn: t.accountDash.aboWechselnBtn, keineBenachrichtigungen: t.accountDash.aboKeineBenachrichtigungen, notifKeyLabels: { abo_abgeschlossen: t.accountDash.notifAboAbgeschlossen, abo_gewechselt: t.accountDash.notifAboGewechselt, abo_zahlung_erfolgreich: t.accountDash.notifAboZahlungErfolgreich, abo_teddy_klein_frei: t.accountDash.notifAboTeddyKleinFrei, abo_teddy_gross_frei: t.accountDash.notifAboTeddyGrossFrei, abo_bald_praemie: t.accountDash.notifAboBaldPraemie, abo_gekuendigt: t.accountDash.notifAboGekuendigt, abo_teddy_klein_eingeloest: t.accountDash.notifAboTeddyKleinEingeloest, abo_teddy_gross_eingeloest: t.accountDash.notifAboTeddyGrossEingeloest } } }}> <script define:vars={{ loginPath: "/konto/", fallbackName: "Kundin", greetingTemplate: t.accountDash.greeting("{name}"), reviewedBadgeText: t.accountDash.reviewedBadge , invoiceLang: lang, invoiceLabels: { title: t.accountDash.invoiceTitle, numberLabel: t.accountDash.invoiceNumberLabel, dateLabel: t.accountDash.invoiceDateLabel, sellerLabel: t.accountDash.invoiceSellerLabel, billToLabel: t.accountDash.invoiceBillToLabel, itemLabel: t.accountDash.invoiceItemLabel, qtyLabel: t.accountDash.invoiceQtyLabel, unitPriceLabel: t.accountDash.invoiceUnitPriceLabel, sumLabel: t.accountDash.invoiceSumLabel, totalLabel: t.accountDash.orderTotal, vatNote: t.common.vatNote }, loyaltyProgressTemplate: t.accountDash.loyaltyProgressTemplate, loyaltyRemainingOne: t.accountDash.loyaltyRemainingOne, loyaltyRemainingOther: t.accountDash.loyaltyRemainingOther, loyaltyUnlockedTitle: t.accountDash.loyaltyUnlockedTitle, loyaltyUnlockedText: t.accountDash.loyaltyUnlockedText, aboLang: lang, aboLabels: { nameKlein: t.accountDash.aboNameKlein, nameMittel: t.accountDash.aboNameMittel, nameGross: t.accountDash.aboNameGross, preisSuffix: t.accountDash.aboPreisSuffix, monateSuffix: t.accountDash.aboMonateSuffix, fortschrittKleinTemplate: t.accountDash.aboFortschrittKleinTemplate, fortschrittGrossTemplate: t.accountDash.aboFortschrittGrossTemplate, praemienErhaltenTemplate: t.accountDash.aboPraemienErhaltenTemplate, aktivBadge: t.accountDash.aboAktivesLabel, nichtAktivBadge: t.accountDash.aboNichtAktivBadge, gekuendigtHinweisTemplate: t.accountDash.aboGekuendigtHinweisTemplate, kuendigenConfirm: t.accountDash.aboKuendigenConfirm, abschliessenBtn: t.accountDash.aboAbschliessenBtn, wechselnBtn: t.accountDash.aboWechselnBtn, nichtBuchbar: t.accountDash.aboNichtBuchbar, keineBenachrichtigungen: t.accountDash.aboKeineBenachrichtigungen, notifKeyLabels: { abo_abgeschlossen: t.accountDash.notifAboAbgeschlossen, abo_gewechselt: t.accountDash.notifAboGewechselt, abo_zahlung_erfolgreich: t.accountDash.notifAboZahlungErfolgreich, abo_teddy_klein_frei: t.accountDash.notifAboTeddyKleinFrei, abo_teddy_gross_frei: t.accountDash.notifAboTeddyGrossFrei, abo_bald_praemie: t.accountDash.notifAboBaldPraemie, abo_gekuendigt: t.accountDash.notifAboGekuendigt, abo_teddy_klein_eingeloest: t.accountDash.notifAboTeddyKleinEingeloest, abo_teddy_gross_eingeloest: t.accountDash.notifAboTeddyGrossEingeloest } } }}>
window.__accountDashVars = { loginPath, fallbackName, greetingTemplate, reviewedBadgeText, invoiceLang, invoiceLabels, loyaltyProgressTemplate, loyaltyRemainingOne, loyaltyRemainingOther, loyaltyUnlockedTitle, loyaltyUnlockedText, aboLang, aboLabels }; window.__accountDashVars = { loginPath, fallbackName, greetingTemplate, reviewedBadgeText, invoiceLang, invoiceLabels, loyaltyProgressTemplate, loyaltyRemainingOne, loyaltyRemainingOther, loyaltyUnlockedTitle, loyaltyUnlockedText, aboLang, aboLabels };
</script> </script>
<script> <script>
@@ -594,12 +595,16 @@ const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status ===
const badge = card.querySelector("[data-abo-active-badge]") as HTMLElement | null; const badge = card.querySelector("[data-abo-active-badge]") as HTMLElement | null;
if (badge) badge.style.display = isCurrent ? "" : "none"; if (badge) badge.style.display = isCurrent ? "" : "none";
card.classList.toggle("is-current-plan", isCurrent); card.classList.toggle("is-current-plan", isCurrent);
const buchbar = card.dataset.aboPlanAktiv !== "0";
card.classList.toggle("is-not-bookable", !buchbar && !isCurrent);
const btn = card.querySelector("[data-abo-choose]") as HTMLButtonElement | null; const btn = card.querySelector("[data-abo-choose]") as HTMLButtonElement | null;
if (btn) { if (btn) {
btn.disabled = isCurrent; btn.disabled = isCurrent || (!buchbar && !isCurrent);
btn.textContent = isCurrent btn.textContent = isCurrent
? aboLabels.aktivBadge ? aboLabels.aktivBadge
: (status.stufe && status.aktiv && !a.abo.gekuendigt ? aboLabels.wechselnBtn : aboLabels.abschliessenBtn); : !buchbar
? aboLabels.nichtBuchbar
: (status.stufe && status.aktiv && !a.abo.gekuendigt ? aboLabels.wechselnBtn : aboLabels.abschliessenBtn);
} }
}); });
+1 -1
View File
@@ -118,7 +118,7 @@ export function cartTotal(): number {
* aber kostenlosen Artikel zum Warenkorb hinzu — taucht ganz normal in der Liste auf, zählt * aber kostenlosen Artikel zum Warenkorb hinzu — taucht ganz normal in der Liste auf, zählt
* aber mit 0 € in die Summe (siehe zeilenpreisFuer()). */ * aber mit 0 € in die Summe (siehe zeilenpreisFuer()). */
export function fuegeTeddyGratisHinzu(art: "klein" | "gross") { export function fuegeTeddyGratisHinzu(art: "klein" | "gross") {
const slug = art === "klein" ? "kleiner-teddy" : "grosser-teddy"; const slug = art === "klein" ? aboEinstellungen.kleinTeddySlug : aboEinstellungen.grossTeddySlug;
const produkt = getProduct(slug); const produkt = getProduct(slug);
const name = produkt?.name.de ?? (art === "klein" ? "Kleiner Teddy" : "Großer Teddy"); const name = produkt?.name.de ?? (art === "klein" ? "Kleiner Teddy" : "Großer Teddy");
const cart = getCart(); const cart = getCart();
+1
View File
@@ -2421,6 +2421,7 @@ a:focus-visible, button:focus-visible {
.abo-plan-badge { align-self: flex-start; background: color-mix(in srgb, var(--plan-color, var(--c-accent)) 30%, transparent); border: 1px solid color-mix(in srgb, var(--plan-color, var(--c-accent)) 55%, transparent); border-radius: 999px; padding: 0.2rem 0.7rem; font-size: 0.78rem; font-weight: 700; } .abo-plan-badge { align-self: flex-start; background: color-mix(in srgb, var(--plan-color, var(--c-accent)) 30%, transparent); border: 1px solid color-mix(in srgb, var(--plan-color, var(--c-accent)) 55%, transparent); border-radius: 999px; padding: 0.2rem 0.7rem; font-size: 0.78rem; font-weight: 700; }
.abo-plan-btn { align-self: flex-start; } .abo-plan-btn { align-self: flex-start; }
.abo-plan-btn:disabled { opacity: 0.6; cursor: default; } .abo-plan-btn:disabled { opacity: 0.6; cursor: default; }
.abo-plan-card.is-not-bookable { opacity: 0.55; filter: grayscale(0.3); }
.abo-status-card { margin-bottom: 1.4rem; } .abo-status-card { margin-bottom: 1.4rem; }
.abo-status-card h3 { display: flex; align-items: center; gap: 0.6rem; flex-wrap: wrap; } .abo-status-card h3 { display: flex; align-items: center; gap: 0.6rem; flex-wrap: wrap; }