Teddy-Liste zeigt jetzt Mini-Foto + immer den echten Namen, auch bei alten Einlösungen

Auf ausdrücklichen Wunsch: statt der vagen Ersatzformulierung "Teddy aus einer früheren
Einlösung" zeigt die Liste "Diese Teddys hast du schon bekommen" jetzt fuer JEDEN Eintrag
den echten Produktnamen + eine kleine Vorschau — auch fuer Einloesungen von vor diesem
Feature, bei denen kein Name gespeichert war. Das geht zuverlaessig, weil es je Groesse
aktuell nur EIN konfiguriertes Teddy-Produkt gibt (aboEinstellungen.kleinTeddySlug/
grossTeddySlug) — es gibt also keine Mehrdeutigkeit, welcher Teddy es war.

Mini-Foto: echtes Produktfoto, sobald VanVan eins im Adminbereich hochlaedt (automatisch,
ohne Code-Aenderung) — bis dahin ein gestalteter Platzhalter statt eines kaputten Bildes,
da beide Teddy-Produkte aktuell noch kein Foto hinterlegt haben.

Dabei einen echten Datenfehler gefunden und behoben: der KLEINE Teddy trug auf Englisch
"Large Teddy" und auf Schweizerdeutsch "Grosse Teddy" (Kopierfehler vom grossen Teddy) —
war nicht nur in dieser Liste sichtbar, sondern auch auf der Produktseite selbst in allen
nicht-deutschen Sprachen. Jetzt korrekt "Small Teddy" / "Chliine Teddy" / "Petit ours en peluche".

Getestet (Browser): genau der Fall aus der Rückmeldung nachgestellt (2 alte Einlösungen ohne
Namensverlauf) — zeigt jetzt Name + Link statt "unbekannt". Gemischter Fall (echter + alter
Eintrag) korrekt sortiert. Vorhandenes Foto ersetzt den Platzhalter automatisch. Korrigierte
Produktnamen in allen 4 Sprachen bestätigt.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
qcigano
2026-08-04 11:37:02 +02:00
co-authored by Claude Sonnet 5
parent bcd80f12fe
commit 7e02879f66
6 changed files with 237 additions and 121 deletions
+3 -3
View File
@@ -2,9 +2,9 @@
"bilder": [], "bilder": [],
"name": { "name": {
"de": "Kleiner Teddy", "de": "Kleiner Teddy",
"en": "Large Teddy", "en": "Small Teddy",
"ch": "Grosse Teddy", "ch": "Chliine Teddy",
"fr": "Grand ours en peluche" "fr": "Petit ours en peluche"
}, },
"beschreibung": { "beschreibung": {
"de": "Unser handgemachter kleiner Teddy — als Dankeschön-Prämie im Abo-System kostenlos einlösbar, sobald du den passenden Meilenstein erreicht hast.", "de": "Unser handgemachter kleiner Teddy — als Dankeschön-Prämie im Abo-System kostenlos einlösbar, sobald du den passenden Meilenstein erreicht hast.",
+53 -27
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, aboStufeName, aboStufeVorteil, type AboStufe } from "../../../data/abo"; import { ABO_STUFEN, aboStufenKonfig, aboStufeName, aboStufeVorteil, aboEinstellungen, type AboStufe } from "../../../data/abo";
import { TREUE_ZIEL, TREUE_RABATT_PROZENT } from "../../../scripts/account"; import { TREUE_ZIEL, TREUE_RABATT_PROZENT } from "../../../scripts/account";
const lang: Locale = "ch"; const lang: Locale = "ch";
@@ -613,7 +613,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, loyaltyStatRedeemedNever: t.accountDash.loyaltyStatRedeemedNever, loyaltyStatRedeemedSuffix: t.accountDash.loyaltyStatRedeemedSuffix, loyaltyLastRedeemed: t.accountDash.loyaltyLastRedeemed, 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, praemienErhaltenTitel: t.accountDash.aboPraemienErhaltenTitel, praemienKeine: t.accountDash.aboPraemienKeine, praemienAmTemplate: t.accountDash.aboPraemienAmTemplate, praemienAnsehen: t.accountDash.aboPraemienAnsehen, praemienUnbekannt: t.accountDash.aboPraemienUnbekannt, teddyArtKlein: t.accountDash.aboTeddyArtKlein, teddyArtGross: t.accountDash.aboTeddyArtGross, 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 } }, deleteAccountConfirm: t.accountDash.deleteAccountConfirm, deleteAccountError: t.accountDash.deleteAccountError }}> <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, loyaltyStatRedeemedNever: t.accountDash.loyaltyStatRedeemedNever, loyaltyStatRedeemedSuffix: t.accountDash.loyaltyStatRedeemedSuffix, loyaltyLastRedeemed: t.accountDash.loyaltyLastRedeemed, 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, praemienErhaltenTitel: t.accountDash.aboPraemienErhaltenTitel, praemienKeine: t.accountDash.aboPraemienKeine, praemienAmTemplate: t.accountDash.aboPraemienAmTemplate, praemienAnsehen: t.accountDash.aboPraemienAnsehen, praemienUnbekannt: t.accountDash.aboPraemienUnbekannt, teddyArtKlein: t.accountDash.aboTeddyArtKlein, teddyArtGross: t.accountDash.aboTeddyArtGross, kleinTeddySlug: aboEinstellungen.kleinTeddySlug, grossTeddySlug: aboEinstellungen.grossTeddySlug, 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 } }, deleteAccountConfirm: t.accountDash.deleteAccountConfirm, deleteAccountError: t.accountDash.deleteAccountError }}>
window.__accountDashVars = { loginPath, fallbackName, greetingTemplate, reviewedBadgeText, invoiceLang, invoiceLabels, loyaltyProgressTemplate, loyaltyRemainingOne, loyaltyRemainingOther, loyaltyUnlockedTitle, loyaltyUnlockedText, loyaltyStatRedeemedNever, loyaltyStatRedeemedSuffix, loyaltyLastRedeemed, aboLang, aboLabels, deleteAccountConfirm, deleteAccountError }; window.__accountDashVars = { loginPath, fallbackName, greetingTemplate, reviewedBadgeText, invoiceLang, invoiceLabels, loyaltyProgressTemplate, loyaltyRemainingOne, loyaltyRemainingOther, loyaltyUnlockedTitle, loyaltyUnlockedText, loyaltyStatRedeemedNever, loyaltyStatRedeemedSuffix, loyaltyLastRedeemed, aboLang, aboLabels, deleteAccountConfirm, deleteAccountError };
</script> </script>
<script> <script>
@@ -1025,48 +1025,74 @@ const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status ===
if (erhaltenBox) erhaltenBox.classList.toggle("is-leer", gesamtEingeloest === 0); if (erhaltenBox) erhaltenBox.classList.toggle("is-leer", gesamtEingeloest === 0);
if (erhaltenListe) { if (erhaltenListe) {
erhaltenListe.innerHTML = ""; erhaltenListe.innerHTML = "";
const eintraege = [...a.abo.erhalteneTeddys].reverse(); // Baut EINEN Listeneintrag samt Mini-Foto — genutzt sowohl fuer echt aufgezeichnete
eintraege.forEach((teddy) => { // Einloesungen als auch fuer nachtraeglich aufgefuellte (siehe unten). Zeigt IMMER Name +
// Aktueller, uebersetzter Produktname bevorzugt; der beim Einloesen gespeicherte Name // Foto, nie mehr nur "unbekannt": es gibt je Groesse aktuell genau EIN konfiguriertes
// ist nur die Rueckfallebene, falls das Produkt inzwischen umbenannt/entfernt wurde. // Teddy-Produkt (siehe aboEinstellungen.kleinTeddySlug/grossTeddySlug), also ist auch bei
const produkt = getProduct(teddy.slug); // aelteren, vor dieser Funktion eingeloesten Praemien klar, welcher Teddy es war.
const name = produkt?.name[aboLang] ?? teddy.name; function baueTeddyEintrag(art, slug, gespeicherterName, datumIso) {
const artLabel = teddy.art === "klein" ? aboLabels.teddyArtKlein : aboLabels.teddyArtGross; const produkt = getProduct(slug);
const name = produkt?.name[aboLang] ?? gespeicherterName ?? "";
const artLabel = art === "klein" ? aboLabels.teddyArtKlein : aboLabels.teddyArtGross;
const li = document.createElement("li"); const li = document.createElement("li");
li.className = "abo-erhalten-eintrag"; li.className = "abo-erhalten-eintrag";
// Mini-Foto: echtes Produktfoto, sobald VanVan eins hochgeladen hat — bis dahin ein
// gestalteter Platzhalter statt eines kaputten Bildes.
if (produkt?.bilder?.[0]) {
const img = document.createElement("img");
img.className = "abo-erhalten-foto";
img.src = produkt.bilder[0];
img.alt = name || artLabel;
img.loading = "lazy";
li.appendChild(img);
} else {
const platzhalter = document.createElement("span");
platzhalter.className = "abo-erhalten-foto abo-erhalten-foto-platzhalter";
platzhalter.textContent = "🧸";
platzhalter.setAttribute("aria-hidden", "true");
li.appendChild(platzhalter);
}
const textWrap = document.createElement("span");
textWrap.className = "abo-erhalten-text";
const bez = document.createElement("span"); const bez = document.createElement("span");
bez.className = "abo-erhalten-name"; bez.className = "abo-erhalten-name";
// Heisst das Produkt selbst schon "Kleiner Teddy", nicht "Kleiner Teddy: Kleiner Teddy" // Heisst das Produkt selbst schon "Kleiner Teddy", nicht "Kleiner Teddy: Kleiner Teddy"
// schreiben. Erst wenn VanVan echte Teddy-Namen hinterlegt, steht beides nebeneinander. // schreiben. Erst wenn VanVan mehrere, unterschiedlich benannte Teddys hinterlegt,
// stehen Art und Name nebeneinander.
const nameGleichArt = name.trim().toLowerCase() === artLabel.trim().toLowerCase(); const nameGleichArt = name.trim().toLowerCase() === artLabel.trim().toLowerCase();
bez.textContent = name && !nameGleichArt ? `${artLabel}: ${name}` : (name || artLabel); bez.textContent = name && !nameGleichArt ? `${artLabel}: ${name}` : (name || artLabel);
li.appendChild(bez); textWrap.appendChild(bez);
if (teddy.datum) { if (datumIso) {
const dat = document.createElement("span"); const dat = document.createElement("span");
dat.className = "abo-erhalten-datum"; dat.className = "abo-erhalten-datum";
const [j, m, tg] = teddy.datum.split("-"); const [j, m, tg] = datumIso.split("-");
dat.textContent = aboLabels.praemienAmTemplate.replace("{datum}", `${tg}.${m}.${j}`); dat.textContent = aboLabels.praemienAmTemplate.replace("{datum}", `${tg}.${m}.${j}`);
li.appendChild(dat); textWrap.appendChild(dat);
} }
// Direkt zum Produkt springen, damit man sofort nachsehen kann, welcher Teddy das war.
if (produkt) { if (produkt) {
const link = document.createElement("a"); const link = document.createElement("a");
link.className = "abo-erhalten-link"; link.className = "abo-erhalten-link";
link.href = `${aboLang === "de" ? "" : "/" + aboLang}/produkt/${teddy.slug}/`; link.href = `${aboLang === "de" ? "" : "/" + aboLang}/produkt/${slug}/`;
link.textContent = aboLabels.praemienAnsehen; link.textContent = aboLabels.praemienAnsehen;
li.appendChild(link); textWrap.appendChild(link);
} }
erhaltenListe.appendChild(li); li.appendChild(textWrap);
return li;
}
const eintraege = [...a.abo.erhalteneTeddys].reverse();
eintraege.forEach((teddy) => {
erhaltenListe.appendChild(baueTeddyEintrag(teddy.art, teddy.slug, teddy.name, teddy.datum));
}); });
// Aeltere Konten kennen die namentliche Liste noch nicht: fehlende Eintraege werden // Aeltere Konten kennen die namentliche Liste noch nicht (die Einloesung liegt vor
// ehrlich ohne Namen ergaenzt, statt sie einfach zu verschweigen. // diesem Feature) — es gibt aber je Groesse nur EIN konfiguriertes Teddy-Produkt, daher
const fehlend = gesamtEingeloest - eintraege.length; // laesst sich auch dafuer zweifelsfrei Name + Foto zeigen statt nur "unbekannt".
for (let i = 0; i < fehlend; i++) { const kleinFehlend = Math.max(0, a.abo.kleinTeddyEingeloest - eintraege.filter((t) => t.art === "klein").length);
const li = document.createElement("li"); const grossFehlend = Math.max(0, a.abo.grossTeddyEingeloest - eintraege.filter((t) => t.art === "gross").length);
li.className = "abo-erhalten-eintrag abo-erhalten-eintrag-unbekannt"; for (let i = 0; i < kleinFehlend; i++) erhaltenListe.appendChild(baueTeddyEintrag("klein", aboLabels.kleinTeddySlug, null, null));
li.textContent = aboLabels.praemienUnbekannt; for (let i = 0; i < grossFehlend; i++) erhaltenListe.appendChild(baueTeddyEintrag("gross", aboLabels.grossTeddySlug, null, null));
erhaltenListe.appendChild(li);
}
} }
const gekuendigtHinweis = document.getElementById("abo-gekuendigt-hinweis"); const gekuendigtHinweis = document.getElementById("abo-gekuendigt-hinweis");
+53 -27
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, aboStufeName, aboStufeVorteil, type AboStufe } from "../../../data/abo"; import { ABO_STUFEN, aboStufenKonfig, aboStufeName, aboStufeVorteil, aboEinstellungen, type AboStufe } from "../../../data/abo";
import { TREUE_ZIEL, TREUE_RABATT_PROZENT } from "../../../scripts/account"; import { TREUE_ZIEL, TREUE_RABATT_PROZENT } from "../../../scripts/account";
const lang: Locale = "en"; const lang: Locale = "en";
@@ -613,7 +613,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, loyaltyStatRedeemedNever: t.accountDash.loyaltyStatRedeemedNever, loyaltyStatRedeemedSuffix: t.accountDash.loyaltyStatRedeemedSuffix, loyaltyLastRedeemed: t.accountDash.loyaltyLastRedeemed, 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, praemienErhaltenTitel: t.accountDash.aboPraemienErhaltenTitel, praemienKeine: t.accountDash.aboPraemienKeine, praemienAmTemplate: t.accountDash.aboPraemienAmTemplate, praemienAnsehen: t.accountDash.aboPraemienAnsehen, praemienUnbekannt: t.accountDash.aboPraemienUnbekannt, teddyArtKlein: t.accountDash.aboTeddyArtKlein, teddyArtGross: t.accountDash.aboTeddyArtGross, 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 } }, deleteAccountConfirm: t.accountDash.deleteAccountConfirm, deleteAccountError: t.accountDash.deleteAccountError }}> <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, loyaltyStatRedeemedNever: t.accountDash.loyaltyStatRedeemedNever, loyaltyStatRedeemedSuffix: t.accountDash.loyaltyStatRedeemedSuffix, loyaltyLastRedeemed: t.accountDash.loyaltyLastRedeemed, 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, praemienErhaltenTitel: t.accountDash.aboPraemienErhaltenTitel, praemienKeine: t.accountDash.aboPraemienKeine, praemienAmTemplate: t.accountDash.aboPraemienAmTemplate, praemienAnsehen: t.accountDash.aboPraemienAnsehen, praemienUnbekannt: t.accountDash.aboPraemienUnbekannt, teddyArtKlein: t.accountDash.aboTeddyArtKlein, teddyArtGross: t.accountDash.aboTeddyArtGross, kleinTeddySlug: aboEinstellungen.kleinTeddySlug, grossTeddySlug: aboEinstellungen.grossTeddySlug, 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 } }, deleteAccountConfirm: t.accountDash.deleteAccountConfirm, deleteAccountError: t.accountDash.deleteAccountError }}>
window.__accountDashVars = { loginPath, fallbackName, greetingTemplate, reviewedBadgeText, invoiceLang, invoiceLabels, loyaltyProgressTemplate, loyaltyRemainingOne, loyaltyRemainingOther, loyaltyUnlockedTitle, loyaltyUnlockedText, loyaltyStatRedeemedNever, loyaltyStatRedeemedSuffix, loyaltyLastRedeemed, aboLang, aboLabels, deleteAccountConfirm, deleteAccountError }; window.__accountDashVars = { loginPath, fallbackName, greetingTemplate, reviewedBadgeText, invoiceLang, invoiceLabels, loyaltyProgressTemplate, loyaltyRemainingOne, loyaltyRemainingOther, loyaltyUnlockedTitle, loyaltyUnlockedText, loyaltyStatRedeemedNever, loyaltyStatRedeemedSuffix, loyaltyLastRedeemed, aboLang, aboLabels, deleteAccountConfirm, deleteAccountError };
</script> </script>
<script> <script>
@@ -1025,48 +1025,74 @@ const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status ===
if (erhaltenBox) erhaltenBox.classList.toggle("is-leer", gesamtEingeloest === 0); if (erhaltenBox) erhaltenBox.classList.toggle("is-leer", gesamtEingeloest === 0);
if (erhaltenListe) { if (erhaltenListe) {
erhaltenListe.innerHTML = ""; erhaltenListe.innerHTML = "";
const eintraege = [...a.abo.erhalteneTeddys].reverse(); // Baut EINEN Listeneintrag samt Mini-Foto — genutzt sowohl fuer echt aufgezeichnete
eintraege.forEach((teddy) => { // Einloesungen als auch fuer nachtraeglich aufgefuellte (siehe unten). Zeigt IMMER Name +
// Aktueller, uebersetzter Produktname bevorzugt; der beim Einloesen gespeicherte Name // Foto, nie mehr nur "unbekannt": es gibt je Groesse aktuell genau EIN konfiguriertes
// ist nur die Rueckfallebene, falls das Produkt inzwischen umbenannt/entfernt wurde. // Teddy-Produkt (siehe aboEinstellungen.kleinTeddySlug/grossTeddySlug), also ist auch bei
const produkt = getProduct(teddy.slug); // aelteren, vor dieser Funktion eingeloesten Praemien klar, welcher Teddy es war.
const name = produkt?.name[aboLang] ?? teddy.name; function baueTeddyEintrag(art, slug, gespeicherterName, datumIso) {
const artLabel = teddy.art === "klein" ? aboLabels.teddyArtKlein : aboLabels.teddyArtGross; const produkt = getProduct(slug);
const name = produkt?.name[aboLang] ?? gespeicherterName ?? "";
const artLabel = art === "klein" ? aboLabels.teddyArtKlein : aboLabels.teddyArtGross;
const li = document.createElement("li"); const li = document.createElement("li");
li.className = "abo-erhalten-eintrag"; li.className = "abo-erhalten-eintrag";
// Mini-Foto: echtes Produktfoto, sobald VanVan eins hochgeladen hat — bis dahin ein
// gestalteter Platzhalter statt eines kaputten Bildes.
if (produkt?.bilder?.[0]) {
const img = document.createElement("img");
img.className = "abo-erhalten-foto";
img.src = produkt.bilder[0];
img.alt = name || artLabel;
img.loading = "lazy";
li.appendChild(img);
} else {
const platzhalter = document.createElement("span");
platzhalter.className = "abo-erhalten-foto abo-erhalten-foto-platzhalter";
platzhalter.textContent = "🧸";
platzhalter.setAttribute("aria-hidden", "true");
li.appendChild(platzhalter);
}
const textWrap = document.createElement("span");
textWrap.className = "abo-erhalten-text";
const bez = document.createElement("span"); const bez = document.createElement("span");
bez.className = "abo-erhalten-name"; bez.className = "abo-erhalten-name";
// Heisst das Produkt selbst schon "Kleiner Teddy", nicht "Kleiner Teddy: Kleiner Teddy" // Heisst das Produkt selbst schon "Kleiner Teddy", nicht "Kleiner Teddy: Kleiner Teddy"
// schreiben. Erst wenn VanVan echte Teddy-Namen hinterlegt, steht beides nebeneinander. // schreiben. Erst wenn VanVan mehrere, unterschiedlich benannte Teddys hinterlegt,
// stehen Art und Name nebeneinander.
const nameGleichArt = name.trim().toLowerCase() === artLabel.trim().toLowerCase(); const nameGleichArt = name.trim().toLowerCase() === artLabel.trim().toLowerCase();
bez.textContent = name && !nameGleichArt ? `${artLabel}: ${name}` : (name || artLabel); bez.textContent = name && !nameGleichArt ? `${artLabel}: ${name}` : (name || artLabel);
li.appendChild(bez); textWrap.appendChild(bez);
if (teddy.datum) { if (datumIso) {
const dat = document.createElement("span"); const dat = document.createElement("span");
dat.className = "abo-erhalten-datum"; dat.className = "abo-erhalten-datum";
const [j, m, tg] = teddy.datum.split("-"); const [j, m, tg] = datumIso.split("-");
dat.textContent = aboLabels.praemienAmTemplate.replace("{datum}", `${tg}.${m}.${j}`); dat.textContent = aboLabels.praemienAmTemplate.replace("{datum}", `${tg}.${m}.${j}`);
li.appendChild(dat); textWrap.appendChild(dat);
} }
// Direkt zum Produkt springen, damit man sofort nachsehen kann, welcher Teddy das war.
if (produkt) { if (produkt) {
const link = document.createElement("a"); const link = document.createElement("a");
link.className = "abo-erhalten-link"; link.className = "abo-erhalten-link";
link.href = `${aboLang === "de" ? "" : "/" + aboLang}/produkt/${teddy.slug}/`; link.href = `${aboLang === "de" ? "" : "/" + aboLang}/produkt/${slug}/`;
link.textContent = aboLabels.praemienAnsehen; link.textContent = aboLabels.praemienAnsehen;
li.appendChild(link); textWrap.appendChild(link);
} }
erhaltenListe.appendChild(li); li.appendChild(textWrap);
return li;
}
const eintraege = [...a.abo.erhalteneTeddys].reverse();
eintraege.forEach((teddy) => {
erhaltenListe.appendChild(baueTeddyEintrag(teddy.art, teddy.slug, teddy.name, teddy.datum));
}); });
// Aeltere Konten kennen die namentliche Liste noch nicht: fehlende Eintraege werden // Aeltere Konten kennen die namentliche Liste noch nicht (die Einloesung liegt vor
// ehrlich ohne Namen ergaenzt, statt sie einfach zu verschweigen. // diesem Feature) — es gibt aber je Groesse nur EIN konfiguriertes Teddy-Produkt, daher
const fehlend = gesamtEingeloest - eintraege.length; // laesst sich auch dafuer zweifelsfrei Name + Foto zeigen statt nur "unbekannt".
for (let i = 0; i < fehlend; i++) { const kleinFehlend = Math.max(0, a.abo.kleinTeddyEingeloest - eintraege.filter((t) => t.art === "klein").length);
const li = document.createElement("li"); const grossFehlend = Math.max(0, a.abo.grossTeddyEingeloest - eintraege.filter((t) => t.art === "gross").length);
li.className = "abo-erhalten-eintrag abo-erhalten-eintrag-unbekannt"; for (let i = 0; i < kleinFehlend; i++) erhaltenListe.appendChild(baueTeddyEintrag("klein", aboLabels.kleinTeddySlug, null, null));
li.textContent = aboLabels.praemienUnbekannt; for (let i = 0; i < grossFehlend; i++) erhaltenListe.appendChild(baueTeddyEintrag("gross", aboLabels.grossTeddySlug, null, null));
erhaltenListe.appendChild(li);
}
} }
const gekuendigtHinweis = document.getElementById("abo-gekuendigt-hinweis"); const gekuendigtHinweis = document.getElementById("abo-gekuendigt-hinweis");
+53 -27
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, aboStufeName, aboStufeVorteil, type AboStufe } from "../../../data/abo"; import { ABO_STUFEN, aboStufenKonfig, aboStufeName, aboStufeVorteil, aboEinstellungen, type AboStufe } from "../../../data/abo";
import { TREUE_ZIEL, TREUE_RABATT_PROZENT } from "../../../scripts/account"; import { TREUE_ZIEL, TREUE_RABATT_PROZENT } from "../../../scripts/account";
const lang: Locale = "fr"; const lang: Locale = "fr";
@@ -613,7 +613,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, loyaltyStatRedeemedNever: t.accountDash.loyaltyStatRedeemedNever, loyaltyStatRedeemedSuffix: t.accountDash.loyaltyStatRedeemedSuffix, loyaltyLastRedeemed: t.accountDash.loyaltyLastRedeemed, 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, praemienErhaltenTitel: t.accountDash.aboPraemienErhaltenTitel, praemienKeine: t.accountDash.aboPraemienKeine, praemienAmTemplate: t.accountDash.aboPraemienAmTemplate, praemienAnsehen: t.accountDash.aboPraemienAnsehen, praemienUnbekannt: t.accountDash.aboPraemienUnbekannt, teddyArtKlein: t.accountDash.aboTeddyArtKlein, teddyArtGross: t.accountDash.aboTeddyArtGross, 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 } }, deleteAccountConfirm: t.accountDash.deleteAccountConfirm, deleteAccountError: t.accountDash.deleteAccountError }}> <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, loyaltyStatRedeemedNever: t.accountDash.loyaltyStatRedeemedNever, loyaltyStatRedeemedSuffix: t.accountDash.loyaltyStatRedeemedSuffix, loyaltyLastRedeemed: t.accountDash.loyaltyLastRedeemed, 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, praemienErhaltenTitel: t.accountDash.aboPraemienErhaltenTitel, praemienKeine: t.accountDash.aboPraemienKeine, praemienAmTemplate: t.accountDash.aboPraemienAmTemplate, praemienAnsehen: t.accountDash.aboPraemienAnsehen, praemienUnbekannt: t.accountDash.aboPraemienUnbekannt, teddyArtKlein: t.accountDash.aboTeddyArtKlein, teddyArtGross: t.accountDash.aboTeddyArtGross, kleinTeddySlug: aboEinstellungen.kleinTeddySlug, grossTeddySlug: aboEinstellungen.grossTeddySlug, 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 } }, deleteAccountConfirm: t.accountDash.deleteAccountConfirm, deleteAccountError: t.accountDash.deleteAccountError }}>
window.__accountDashVars = { loginPath, fallbackName, greetingTemplate, reviewedBadgeText, invoiceLang, invoiceLabels, loyaltyProgressTemplate, loyaltyRemainingOne, loyaltyRemainingOther, loyaltyUnlockedTitle, loyaltyUnlockedText, loyaltyStatRedeemedNever, loyaltyStatRedeemedSuffix, loyaltyLastRedeemed, aboLang, aboLabels, deleteAccountConfirm, deleteAccountError }; window.__accountDashVars = { loginPath, fallbackName, greetingTemplate, reviewedBadgeText, invoiceLang, invoiceLabels, loyaltyProgressTemplate, loyaltyRemainingOne, loyaltyRemainingOther, loyaltyUnlockedTitle, loyaltyUnlockedText, loyaltyStatRedeemedNever, loyaltyStatRedeemedSuffix, loyaltyLastRedeemed, aboLang, aboLabels, deleteAccountConfirm, deleteAccountError };
</script> </script>
<script> <script>
@@ -1025,48 +1025,74 @@ const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status ===
if (erhaltenBox) erhaltenBox.classList.toggle("is-leer", gesamtEingeloest === 0); if (erhaltenBox) erhaltenBox.classList.toggle("is-leer", gesamtEingeloest === 0);
if (erhaltenListe) { if (erhaltenListe) {
erhaltenListe.innerHTML = ""; erhaltenListe.innerHTML = "";
const eintraege = [...a.abo.erhalteneTeddys].reverse(); // Baut EINEN Listeneintrag samt Mini-Foto — genutzt sowohl fuer echt aufgezeichnete
eintraege.forEach((teddy) => { // Einloesungen als auch fuer nachtraeglich aufgefuellte (siehe unten). Zeigt IMMER Name +
// Aktueller, uebersetzter Produktname bevorzugt; der beim Einloesen gespeicherte Name // Foto, nie mehr nur "unbekannt": es gibt je Groesse aktuell genau EIN konfiguriertes
// ist nur die Rueckfallebene, falls das Produkt inzwischen umbenannt/entfernt wurde. // Teddy-Produkt (siehe aboEinstellungen.kleinTeddySlug/grossTeddySlug), also ist auch bei
const produkt = getProduct(teddy.slug); // aelteren, vor dieser Funktion eingeloesten Praemien klar, welcher Teddy es war.
const name = produkt?.name[aboLang] ?? teddy.name; function baueTeddyEintrag(art, slug, gespeicherterName, datumIso) {
const artLabel = teddy.art === "klein" ? aboLabels.teddyArtKlein : aboLabels.teddyArtGross; const produkt = getProduct(slug);
const name = produkt?.name[aboLang] ?? gespeicherterName ?? "";
const artLabel = art === "klein" ? aboLabels.teddyArtKlein : aboLabels.teddyArtGross;
const li = document.createElement("li"); const li = document.createElement("li");
li.className = "abo-erhalten-eintrag"; li.className = "abo-erhalten-eintrag";
// Mini-Foto: echtes Produktfoto, sobald VanVan eins hochgeladen hat — bis dahin ein
// gestalteter Platzhalter statt eines kaputten Bildes.
if (produkt?.bilder?.[0]) {
const img = document.createElement("img");
img.className = "abo-erhalten-foto";
img.src = produkt.bilder[0];
img.alt = name || artLabel;
img.loading = "lazy";
li.appendChild(img);
} else {
const platzhalter = document.createElement("span");
platzhalter.className = "abo-erhalten-foto abo-erhalten-foto-platzhalter";
platzhalter.textContent = "🧸";
platzhalter.setAttribute("aria-hidden", "true");
li.appendChild(platzhalter);
}
const textWrap = document.createElement("span");
textWrap.className = "abo-erhalten-text";
const bez = document.createElement("span"); const bez = document.createElement("span");
bez.className = "abo-erhalten-name"; bez.className = "abo-erhalten-name";
// Heisst das Produkt selbst schon "Kleiner Teddy", nicht "Kleiner Teddy: Kleiner Teddy" // Heisst das Produkt selbst schon "Kleiner Teddy", nicht "Kleiner Teddy: Kleiner Teddy"
// schreiben. Erst wenn VanVan echte Teddy-Namen hinterlegt, steht beides nebeneinander. // schreiben. Erst wenn VanVan mehrere, unterschiedlich benannte Teddys hinterlegt,
// stehen Art und Name nebeneinander.
const nameGleichArt = name.trim().toLowerCase() === artLabel.trim().toLowerCase(); const nameGleichArt = name.trim().toLowerCase() === artLabel.trim().toLowerCase();
bez.textContent = name && !nameGleichArt ? `${artLabel}: ${name}` : (name || artLabel); bez.textContent = name && !nameGleichArt ? `${artLabel}: ${name}` : (name || artLabel);
li.appendChild(bez); textWrap.appendChild(bez);
if (teddy.datum) { if (datumIso) {
const dat = document.createElement("span"); const dat = document.createElement("span");
dat.className = "abo-erhalten-datum"; dat.className = "abo-erhalten-datum";
const [j, m, tg] = teddy.datum.split("-"); const [j, m, tg] = datumIso.split("-");
dat.textContent = aboLabels.praemienAmTemplate.replace("{datum}", `${tg}.${m}.${j}`); dat.textContent = aboLabels.praemienAmTemplate.replace("{datum}", `${tg}.${m}.${j}`);
li.appendChild(dat); textWrap.appendChild(dat);
} }
// Direkt zum Produkt springen, damit man sofort nachsehen kann, welcher Teddy das war.
if (produkt) { if (produkt) {
const link = document.createElement("a"); const link = document.createElement("a");
link.className = "abo-erhalten-link"; link.className = "abo-erhalten-link";
link.href = `${aboLang === "de" ? "" : "/" + aboLang}/produkt/${teddy.slug}/`; link.href = `${aboLang === "de" ? "" : "/" + aboLang}/produkt/${slug}/`;
link.textContent = aboLabels.praemienAnsehen; link.textContent = aboLabels.praemienAnsehen;
li.appendChild(link); textWrap.appendChild(link);
} }
erhaltenListe.appendChild(li); li.appendChild(textWrap);
return li;
}
const eintraege = [...a.abo.erhalteneTeddys].reverse();
eintraege.forEach((teddy) => {
erhaltenListe.appendChild(baueTeddyEintrag(teddy.art, teddy.slug, teddy.name, teddy.datum));
}); });
// Aeltere Konten kennen die namentliche Liste noch nicht: fehlende Eintraege werden // Aeltere Konten kennen die namentliche Liste noch nicht (die Einloesung liegt vor
// ehrlich ohne Namen ergaenzt, statt sie einfach zu verschweigen. // diesem Feature) — es gibt aber je Groesse nur EIN konfiguriertes Teddy-Produkt, daher
const fehlend = gesamtEingeloest - eintraege.length; // laesst sich auch dafuer zweifelsfrei Name + Foto zeigen statt nur "unbekannt".
for (let i = 0; i < fehlend; i++) { const kleinFehlend = Math.max(0, a.abo.kleinTeddyEingeloest - eintraege.filter((t) => t.art === "klein").length);
const li = document.createElement("li"); const grossFehlend = Math.max(0, a.abo.grossTeddyEingeloest - eintraege.filter((t) => t.art === "gross").length);
li.className = "abo-erhalten-eintrag abo-erhalten-eintrag-unbekannt"; for (let i = 0; i < kleinFehlend; i++) erhaltenListe.appendChild(baueTeddyEintrag("klein", aboLabels.kleinTeddySlug, null, null));
li.textContent = aboLabels.praemienUnbekannt; for (let i = 0; i < grossFehlend; i++) erhaltenListe.appendChild(baueTeddyEintrag("gross", aboLabels.grossTeddySlug, null, null));
erhaltenListe.appendChild(li);
}
} }
const gekuendigtHinweis = document.getElementById("abo-gekuendigt-hinweis"); const gekuendigtHinweis = document.getElementById("abo-gekuendigt-hinweis");
+53 -27
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, aboStufeName, aboStufeVorteil, type AboStufe } from "../../data/abo"; import { ABO_STUFEN, aboStufenKonfig, aboStufeName, aboStufeVorteil, aboEinstellungen, type AboStufe } from "../../data/abo";
import { TREUE_ZIEL, TREUE_RABATT_PROZENT } from "../../scripts/account"; import { TREUE_ZIEL, TREUE_RABATT_PROZENT } from "../../scripts/account";
const lang: Locale = "de"; const lang: Locale = "de";
@@ -622,7 +622,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, loyaltyStatRedeemedNever: t.accountDash.loyaltyStatRedeemedNever, loyaltyStatRedeemedSuffix: t.accountDash.loyaltyStatRedeemedSuffix, loyaltyLastRedeemed: t.accountDash.loyaltyLastRedeemed, 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, praemienErhaltenTitel: t.accountDash.aboPraemienErhaltenTitel, praemienKeine: t.accountDash.aboPraemienKeine, praemienAmTemplate: t.accountDash.aboPraemienAmTemplate, praemienAnsehen: t.accountDash.aboPraemienAnsehen, praemienUnbekannt: t.accountDash.aboPraemienUnbekannt, teddyArtKlein: t.accountDash.aboTeddyArtKlein, teddyArtGross: t.accountDash.aboTeddyArtGross, 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 } }, deleteAccountConfirm: t.accountDash.deleteAccountConfirm, deleteAccountError: t.accountDash.deleteAccountError }}> <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, loyaltyStatRedeemedNever: t.accountDash.loyaltyStatRedeemedNever, loyaltyStatRedeemedSuffix: t.accountDash.loyaltyStatRedeemedSuffix, loyaltyLastRedeemed: t.accountDash.loyaltyLastRedeemed, 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, praemienErhaltenTitel: t.accountDash.aboPraemienErhaltenTitel, praemienKeine: t.accountDash.aboPraemienKeine, praemienAmTemplate: t.accountDash.aboPraemienAmTemplate, praemienAnsehen: t.accountDash.aboPraemienAnsehen, praemienUnbekannt: t.accountDash.aboPraemienUnbekannt, teddyArtKlein: t.accountDash.aboTeddyArtKlein, teddyArtGross: t.accountDash.aboTeddyArtGross, kleinTeddySlug: aboEinstellungen.kleinTeddySlug, grossTeddySlug: aboEinstellungen.grossTeddySlug, 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 } }, deleteAccountConfirm: t.accountDash.deleteAccountConfirm, deleteAccountError: t.accountDash.deleteAccountError }}>
window.__accountDashVars = { loginPath, fallbackName, greetingTemplate, reviewedBadgeText, invoiceLang, invoiceLabels, loyaltyProgressTemplate, loyaltyRemainingOne, loyaltyRemainingOther, loyaltyUnlockedTitle, loyaltyUnlockedText, loyaltyStatRedeemedNever, loyaltyStatRedeemedSuffix, loyaltyLastRedeemed, aboLang, aboLabels, deleteAccountConfirm, deleteAccountError }; window.__accountDashVars = { loginPath, fallbackName, greetingTemplate, reviewedBadgeText, invoiceLang, invoiceLabels, loyaltyProgressTemplate, loyaltyRemainingOne, loyaltyRemainingOther, loyaltyUnlockedTitle, loyaltyUnlockedText, loyaltyStatRedeemedNever, loyaltyStatRedeemedSuffix, loyaltyLastRedeemed, aboLang, aboLabels, deleteAccountConfirm, deleteAccountError };
</script> </script>
<script> <script>
@@ -1041,48 +1041,74 @@ const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status ===
if (erhaltenBox) erhaltenBox.classList.toggle("is-leer", gesamtEingeloest === 0); if (erhaltenBox) erhaltenBox.classList.toggle("is-leer", gesamtEingeloest === 0);
if (erhaltenListe) { if (erhaltenListe) {
erhaltenListe.innerHTML = ""; erhaltenListe.innerHTML = "";
const eintraege = [...a.abo.erhalteneTeddys].reverse(); // Baut EINEN Listeneintrag samt Mini-Foto — genutzt sowohl fuer echt aufgezeichnete
eintraege.forEach((teddy) => { // Einloesungen als auch fuer nachtraeglich aufgefuellte (siehe unten). Zeigt IMMER Name +
// Aktueller, uebersetzter Produktname bevorzugt; der beim Einloesen gespeicherte Name // Foto, nie mehr nur "unbekannt": es gibt je Groesse aktuell genau EIN konfiguriertes
// ist nur die Rueckfallebene, falls das Produkt inzwischen umbenannt/entfernt wurde. // Teddy-Produkt (siehe aboEinstellungen.kleinTeddySlug/grossTeddySlug), also ist auch bei
const produkt = getProduct(teddy.slug); // aelteren, vor dieser Funktion eingeloesten Praemien klar, welcher Teddy es war.
const name = produkt?.name[aboLang] ?? teddy.name; function baueTeddyEintrag(art, slug, gespeicherterName, datumIso) {
const artLabel = teddy.art === "klein" ? aboLabels.teddyArtKlein : aboLabels.teddyArtGross; const produkt = getProduct(slug);
const name = produkt?.name[aboLang] ?? gespeicherterName ?? "";
const artLabel = art === "klein" ? aboLabels.teddyArtKlein : aboLabels.teddyArtGross;
const li = document.createElement("li"); const li = document.createElement("li");
li.className = "abo-erhalten-eintrag"; li.className = "abo-erhalten-eintrag";
// Mini-Foto: echtes Produktfoto, sobald VanVan eins hochgeladen hat — bis dahin ein
// gestalteter Platzhalter statt eines kaputten Bildes.
if (produkt?.bilder?.[0]) {
const img = document.createElement("img");
img.className = "abo-erhalten-foto";
img.src = produkt.bilder[0];
img.alt = name || artLabel;
img.loading = "lazy";
li.appendChild(img);
} else {
const platzhalter = document.createElement("span");
platzhalter.className = "abo-erhalten-foto abo-erhalten-foto-platzhalter";
platzhalter.textContent = "🧸";
platzhalter.setAttribute("aria-hidden", "true");
li.appendChild(platzhalter);
}
const textWrap = document.createElement("span");
textWrap.className = "abo-erhalten-text";
const bez = document.createElement("span"); const bez = document.createElement("span");
bez.className = "abo-erhalten-name"; bez.className = "abo-erhalten-name";
// Heisst das Produkt selbst schon "Kleiner Teddy", nicht "Kleiner Teddy: Kleiner Teddy" // Heisst das Produkt selbst schon "Kleiner Teddy", nicht "Kleiner Teddy: Kleiner Teddy"
// schreiben. Erst wenn VanVan echte Teddy-Namen hinterlegt, steht beides nebeneinander. // schreiben. Erst wenn VanVan mehrere, unterschiedlich benannte Teddys hinterlegt,
// stehen Art und Name nebeneinander.
const nameGleichArt = name.trim().toLowerCase() === artLabel.trim().toLowerCase(); const nameGleichArt = name.trim().toLowerCase() === artLabel.trim().toLowerCase();
bez.textContent = name && !nameGleichArt ? `${artLabel}: ${name}` : (name || artLabel); bez.textContent = name && !nameGleichArt ? `${artLabel}: ${name}` : (name || artLabel);
li.appendChild(bez); textWrap.appendChild(bez);
if (teddy.datum) { if (datumIso) {
const dat = document.createElement("span"); const dat = document.createElement("span");
dat.className = "abo-erhalten-datum"; dat.className = "abo-erhalten-datum";
const [j, m, tg] = teddy.datum.split("-"); const [j, m, tg] = datumIso.split("-");
dat.textContent = aboLabels.praemienAmTemplate.replace("{datum}", `${tg}.${m}.${j}`); dat.textContent = aboLabels.praemienAmTemplate.replace("{datum}", `${tg}.${m}.${j}`);
li.appendChild(dat); textWrap.appendChild(dat);
} }
// Direkt zum Produkt springen, damit man sofort nachsehen kann, welcher Teddy das war.
if (produkt) { if (produkt) {
const link = document.createElement("a"); const link = document.createElement("a");
link.className = "abo-erhalten-link"; link.className = "abo-erhalten-link";
link.href = `${aboLang === "de" ? "" : "/" + aboLang}/produkt/${teddy.slug}/`; link.href = `${aboLang === "de" ? "" : "/" + aboLang}/produkt/${slug}/`;
link.textContent = aboLabels.praemienAnsehen; link.textContent = aboLabels.praemienAnsehen;
li.appendChild(link); textWrap.appendChild(link);
} }
erhaltenListe.appendChild(li); li.appendChild(textWrap);
return li;
}
const eintraege = [...a.abo.erhalteneTeddys].reverse();
eintraege.forEach((teddy) => {
erhaltenListe.appendChild(baueTeddyEintrag(teddy.art, teddy.slug, teddy.name, teddy.datum));
}); });
// Aeltere Konten kennen die namentliche Liste noch nicht: fehlende Eintraege werden // Aeltere Konten kennen die namentliche Liste noch nicht (die Einloesung liegt vor
// ehrlich ohne Namen ergaenzt, statt sie einfach zu verschweigen. // diesem Feature) — es gibt aber je Groesse nur EIN konfiguriertes Teddy-Produkt, daher
const fehlend = gesamtEingeloest - eintraege.length; // laesst sich auch dafuer zweifelsfrei Name + Foto zeigen statt nur "unbekannt".
for (let i = 0; i < fehlend; i++) { const kleinFehlend = Math.max(0, a.abo.kleinTeddyEingeloest - eintraege.filter((t) => t.art === "klein").length);
const li = document.createElement("li"); const grossFehlend = Math.max(0, a.abo.grossTeddyEingeloest - eintraege.filter((t) => t.art === "gross").length);
li.className = "abo-erhalten-eintrag abo-erhalten-eintrag-unbekannt"; for (let i = 0; i < kleinFehlend; i++) erhaltenListe.appendChild(baueTeddyEintrag("klein", aboLabels.kleinTeddySlug, null, null));
li.textContent = aboLabels.praemienUnbekannt; for (let i = 0; i < grossFehlend; i++) erhaltenListe.appendChild(baueTeddyEintrag("gross", aboLabels.grossTeddySlug, null, null));
erhaltenListe.appendChild(li);
}
} }
const gekuendigtHinweis = document.getElementById("abo-gekuendigt-hinweis"); const gekuendigtHinweis = document.getElementById("abo-gekuendigt-hinweis");
+22 -10
View File
@@ -2914,21 +2914,33 @@ a:focus-visible, button:focus-visible {
} }
.abo-erhalten-box.is-leer { background: rgba(244, 241, 247, 0.05); border-color: rgba(244, 241, 247, 0.14); } .abo-erhalten-box.is-leer { background: rgba(244, 241, 247, 0.05); border-color: rgba(244, 241, 247, 0.14); }
.abo-erhalten-titel { margin: 0; display: flex; align-items: center; gap: 0.5rem; font-weight: 600; } .abo-erhalten-titel { margin: 0; display: flex; align-items: center; gap: 0.5rem; font-weight: 600; }
.abo-erhalten-liste { list-style: none; margin: 0.55rem 0 0; padding: 0; display: flex; flex-direction: column; gap: 0.35rem; } .abo-erhalten-liste { list-style: none; margin: 0.55rem 0 0; padding: 0; display: flex; flex-direction: column; gap: 0.55rem; }
.abo-erhalten-liste:empty { display: none; } .abo-erhalten-liste:empty { display: none; }
.abo-erhalten-eintrag { /* Mini-Foto + Text nebeneinander statt nur einem Emoji-Aufzähler davor — man soll auf einen
display: flex; Blick sehen, WELCHER Teddy das war, nicht nur, dass es "irgendein" Teddy war. */
flex-wrap: wrap; .abo-erhalten-eintrag { display: flex; align-items: center; gap: 0.7rem; }
align-items: baseline; .abo-erhalten-foto {
gap: 0.15rem 0.5rem; width: 40px;
padding-left: 1.4rem; height: 40px;
position: relative; border-radius: 10px;
flex-shrink: 0;
object-fit: cover;
box-shadow: 0 3px 10px -3px rgba(0, 0, 0, 0.5), inset 0 1px 0 rgba(255, 255, 255, 0.2);
} }
.abo-erhalten-eintrag::before { content: "🧸"; position: absolute; left: 0; font-size: 0.9em; } /* Solange VanVan noch kein echtes Foto für den Teddy hochgeladen hat (siehe product.bilder im
Adminbereich): ein gestalteter Platzhalter statt eines kaputten Bild-Symbols — sobald ein
Foto hochgeladen wird, erscheint es hier automatisch, ohne Code-Änderung. */
.abo-erhalten-foto-platzhalter {
display: flex;
align-items: center;
justify-content: center;
font-size: 1.2rem;
background: linear-gradient(160deg, rgba(126, 200, 160, 0.35), rgba(126, 200, 160, 0.12));
}
.abo-erhalten-text { display: flex; flex-wrap: wrap; align-items: baseline; gap: 0.15rem 0.5rem; min-width: 0; }
.abo-erhalten-name { font-weight: 600; } .abo-erhalten-name { font-weight: 600; }
.abo-erhalten-datum { color: var(--c-text-muted); font-size: 0.85em; } .abo-erhalten-datum { color: var(--c-text-muted); font-size: 0.85em; }
.abo-erhalten-link { font-size: 0.85em; text-decoration: underline; } .abo-erhalten-link { font-size: 0.85em; text-decoration: underline; }
.abo-erhalten-eintrag-unbekannt { color: var(--c-text-muted); font-style: italic; }
.abo-praemien-block { border-top: 1px dashed rgba(244, 241, 247, 0.14); padding-top: 0.9rem; margin-top: 0.3rem; } .abo-praemien-block { border-top: 1px dashed rgba(244, 241, 247, 0.14); padding-top: 0.9rem; margin-top: 0.3rem; }
.abo-praemien-block h4 { margin: 0 0 0.4rem; } .abo-praemien-block h4 { margin: 0 0 0.4rem; }