Tier-Familien-Seiten: nur noch Charaktere mit echtem Admin-Produkt sichtbar

Wichtige Verhaltensänderung: ein Charakter (aus dem Kategorien-Bereich im
Admin) ist ab jetzt nur noch eine VORBEREITETE Definition. Er erscheint
auf der Seite NUR, wenn ihm im Admin tatsächlich ein Produkt mit genau
diesem Charakter-Code zugeordnet wurde (das Feld "Charakter" am
Produkt = seine "Rolle"). Ohne zugeordnetes Produkt taucht er nirgends
mehr auf — auch nicht mehr als Platzhalter-Karte mit seinem Namen.
Konkret: HasiGott und Mama Hase verschwinden von der Hase-Seite, weil
für sie (noch) keine Produkte im Admin angelegt wurden. Sobald VanVan
ein Produkt mit charakter: hasigott anlegt, erscheint er automatisch.

Familien ganz ohne Produkte (Kuh, Wal, Oktopus, Schildkröte, Pinguin)
zeigen weiterhin die bereits vorbereiteten, namenlosen "Kommt bald"-
Karten (unverändert) — nur die NAMENTLICHE Vorab-Anzeige einzelner
Charaktere ohne Produkt entfällt.

Zusätzlich: der "Mehr über mich"-Button in der Story-Box hatte keinen
erklärenden Text — man wusste nicht, wohin/wozu er führt. Jetzt steht
davor "Wenn du mehr über mich erfahren möchtest, dann klicke [hier →]"
in allen 4 Sprachen.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
qcigano
2026-08-03 00:47:25 +02:00
co-authored by Claude Sonnet 5
parent b28475c26a
commit 8700e2c588
6 changed files with 103 additions and 140 deletions
+4
View File
@@ -45,6 +45,7 @@ export const ui = {
resultCount: (n: number) => `${n} Produkt${n === 1 ? "" : "e"}`, noResults: "Keine Produkte gefunden bitte Filter anpassen.",
breadcrumbShop: "Shop", categoryEmpty: "In dieser Kategorie sind aktuell noch keine Produkte hinterlegt schau bald wieder vorbei.",
categoryLabel: "Kategorie", productSingular: "Produkt", productPlural: "Produkte", sortLabel: "Sortierung", subcategoriesLabel: "Unterkategorien", mainCategoryLabel: "Hauptkategorie", subcategoryEmpty: "Für diesen Bereich stellt VanVan bald die ersten Produkte ein schau bald wieder vorbei.", moreAbout: "Mehr über", moreVariants: "Weitere Modelle",
moreAboutCharacterLead: "Wenn du mehr über mich erfahren möchtest, dann klicke",
},
sale: {
eyebrow: "% Sale", title: "Aktuelle Angebote", lead: "Zeitlich begrenzte Rabatte auf ausgewählte handgemachte Stücke.",
@@ -233,6 +234,7 @@ export const ui = {
resultCount: (n: number) => `${n} product${n === 1 ? "" : "s"}`, noResults: "No products found please adjust your filters.",
breadcrumbShop: "Shop", categoryEmpty: "No products in this category yet check back soon.",
categoryLabel: "Category", productSingular: "product", productPlural: "products", sortLabel: "Sort by", subcategoriesLabel: "Subcategories", mainCategoryLabel: "Main category", subcategoryEmpty: "VanVan will be adding the first products here soon check back shortly.", moreAbout: "More about", moreVariants: "More designs",
moreAboutCharacterLead: "If you'd like to learn more about me, click",
},
sale: {
eyebrow: "% Sale", title: "Current offers", lead: "Time-limited discounts on selected handmade pieces.",
@@ -421,6 +423,7 @@ export const ui = {
resultCount: (n: number) => `${n} Produkt`, noResults: "Kei Produkt gfunde bitte Filter apasse.",
breadcrumbShop: "Shop", categoryEmpty: "In däre Kategorie sind grad no kei Produkt hinterlegt lueg spöter nomal verbi.",
categoryLabel: "Kategorie", productSingular: "Produkt", productPlural: "Produkt", sortLabel: "Sortiere", subcategoriesLabel: "Unterkategorie", mainCategoryLabel: "Hauptkategorie", subcategoryEmpty: "Für das da stellt VanVan bald die erschte Produkt i lueg bald wieder verbi.", moreAbout: "Meh über", moreVariants: "Witeri Modell",
moreAboutCharacterLead: "Wenn du meh über mich erfahre witsch, dänn klick",
},
sale: {
eyebrow: "% Aktion", title: "Aktuelli Aktion", lead: "Zytlich begrenzti Rabatt uf uswählti, vo Hang gmachti Sächeli.",
@@ -609,6 +612,7 @@ export const ui = {
resultCount: (n: number) => `${n} produit${n === 1 ? "" : "s"}`, noResults: "Aucun produit trouvé veuillez ajuster les filtres.",
breadcrumbShop: "Boutique", categoryEmpty: "Aucun produit dans cette catégorie pour le moment repassez bientôt.",
categoryLabel: "Catégorie", productSingular: "produit", productPlural: "produits", sortLabel: "Trier par", subcategoriesLabel: "Sous-catégories", mainCategoryLabel: "Catégorie principale", subcategoryEmpty: "VanVan ajoutera bientôt les premiers produits ici repassez bientôt.", moreAbout: "En savoir plus sur", moreVariants: "Autres modèles",
moreAboutCharacterLead: "Si tu veux en savoir plus sur moi, clique",
},
sale: {
eyebrow: "% Soldes", title: "Offres actuelles", lead: "Réductions limitées dans le temps sur une sélection de pièces faites main.",
@@ -21,19 +21,18 @@ const sub = getSubcategory(kategorie!, unterkategorie!)!;
// Alle Produkte dieser Unterkategorie (unabhängig davon, welchem Charakter sie zugeordnet sind).
const alleProdukte = productsByUnterkategorie(category.slug, sub.slug);
// Jeder Charakter dieser Tier-Familie bekommt seine eigene kompakte Karte (wie die Produktkarten
// im Shop-Grid), alle nebeneinander in einer Reihe. Sobald VanVan im Adminbereich neue
// Charaktere UND passende Produkte ergänzt, wächst diese Reihe automatisch mit. Fehlt einem
// Charakter noch ein echtes Produkt, zeigt seine Karte trotzdem schon den Charaktertext (nur ohne
// Foto) — die Seite muss nicht auf das erste Foto warten, um zu existieren.
// Ein Charakter (aus dem Kategorien-Bereich im Admin) ist erstmal nur eine VORBEREITETE
// Definition — er erscheint auf der Seite NUR, wenn ihm im Admin auch wirklich ein Produkt mit
// genau diesem Charakter-Code zugeordnet wurde (das Feld "Charakter" am Produkt = seine "Rolle").
// Ohne zugeordnetes Produkt taucht er hier NICHT auf, auch nicht als Platzhalter — nur echte,
// im Admin angelegte Artikel dürfen auf der Seite erscheinen.
const characters = sub.characters ?? [];
const characterEntries = characters.map((char) => ({
char,
product: alleProdukte.find((p) => p.charakter === char.slug),
}));
const characterEntries = characters
.map((char) => ({ char, product: alleProdukte.find((p) => p.charakter === char.slug) }))
.filter((e) => e.product);
// Produkte ohne (bekannte) Charakter-Zuordnung landen als "weitere Modelle" darunter.
const zugeordneteSlugs = new Set(characterEntries.map((e) => e.product?.slug).filter(Boolean));
const zugeordneteSlugs = new Set(characterEntries.map((e) => e.product!.slug));
const weitere = alleProdukte.filter((p) => !zugeordneteSlugs.has(p.slug));
const hatCharaktere = characterEntries.length > 0;
const hatWeitere = weitere.length > 0;
@@ -55,34 +54,23 @@ const komplettLeer = !hatCharaktere && !hatWeitere;
{hatCharaktere && (
<section class="section-tight">
<div class="container">
{characterEntries.some(({ product }) => product) && (
<div class="character-stories">
{characterEntries.filter(({ product }) => product).map(({ char }) => (
<div class="card character-story">
{char.vorstellung && <span class="eyebrow">{char.vorstellung[lang].split("\n")[0]}</span>}
<p>{char.vorstellung ? char.vorstellung[lang].split("\n")[1] : ""}</p>
{char.mehrLink && (
<div class="character-stories">
{characterEntries.map(({ char }) => (
<div class="card character-story">
{char.vorstellung && <span class="eyebrow">{char.vorstellung[lang].split("\n")[0]}</span>}
<p>{char.vorstellung ? char.vorstellung[lang].split("\n")[1] : ""}</p>
{char.mehrLink && (
<p class="character-more-lead">
{t.shop.moreAboutCharacterLead}{" "}
<a class="btn btn-outline btn-sm" href={char.mehrLink} target="_blank" rel="noopener">da →</a>
)}
</div>
))}
</div>
)}
</p>
)}
</div>
))}
</div>
<div class="grid grid-3">
{characterEntries.map(({ char, product }) =>
product ? (
<ProductCard product={product} lang={lang} />
) : (
<div class="card character-card">
<div class="product-media">
<div class="img-placeholder" role="img" aria-label={char.name[lang]}></div>
</div>
<h3>{char.name[lang]}</h3>
<p class="character-card-desc">{t.shop.subcategoryEmpty}</p>
</div>
)
)}
{characterEntries.map(({ product }) => <ProductCard product={product!} lang={lang} />)}
</div>
</div>
</section>
@@ -21,19 +21,18 @@ const sub = getSubcategory(kategorie!, unterkategorie!)!;
// Alle Produkte dieser Unterkategorie (unabhängig davon, welchem Charakter sie zugeordnet sind).
const alleProdukte = productsByUnterkategorie(category.slug, sub.slug);
// Jeder Charakter dieser Tier-Familie bekommt seine eigene kompakte Karte (wie die Produktkarten
// im Shop-Grid), alle nebeneinander in einer Reihe. Sobald VanVan im Adminbereich neue
// Charaktere UND passende Produkte ergänzt, wächst diese Reihe automatisch mit. Fehlt einem
// Charakter noch ein echtes Produkt, zeigt seine Karte trotzdem schon den Charaktertext (nur ohne
// Foto) — die Seite muss nicht auf das erste Foto warten, um zu existieren.
// Ein Charakter (aus dem Kategorien-Bereich im Admin) ist erstmal nur eine VORBEREITETE
// Definition — er erscheint auf der Seite NUR, wenn ihm im Admin auch wirklich ein Produkt mit
// genau diesem Charakter-Code zugeordnet wurde (das Feld "Charakter" am Produkt = seine "Rolle").
// Ohne zugeordnetes Produkt taucht er hier NICHT auf, auch nicht als Platzhalter — nur echte,
// im Admin angelegte Artikel dürfen auf der Seite erscheinen.
const characters = sub.characters ?? [];
const characterEntries = characters.map((char) => ({
char,
product: alleProdukte.find((p) => p.charakter === char.slug),
}));
const characterEntries = characters
.map((char) => ({ char, product: alleProdukte.find((p) => p.charakter === char.slug) }))
.filter((e) => e.product);
// Produkte ohne (bekannte) Charakter-Zuordnung landen als "weitere Modelle" darunter.
const zugeordneteSlugs = new Set(characterEntries.map((e) => e.product?.slug).filter(Boolean));
const zugeordneteSlugs = new Set(characterEntries.map((e) => e.product!.slug));
const weitere = alleProdukte.filter((p) => !zugeordneteSlugs.has(p.slug));
const hatCharaktere = characterEntries.length > 0;
const hatWeitere = weitere.length > 0;
@@ -55,34 +54,23 @@ const komplettLeer = !hatCharaktere && !hatWeitere;
{hatCharaktere && (
<section class="section-tight">
<div class="container">
{characterEntries.some(({ product }) => product) && (
<div class="character-stories">
{characterEntries.filter(({ product }) => product).map(({ char }) => (
<div class="card character-story">
{char.vorstellung && <span class="eyebrow">{char.vorstellung[lang].split("\n")[0]}</span>}
<p>{char.vorstellung ? char.vorstellung[lang].split("\n")[1] : ""}</p>
{char.mehrLink && (
<div class="character-stories">
{characterEntries.map(({ char }) => (
<div class="card character-story">
{char.vorstellung && <span class="eyebrow">{char.vorstellung[lang].split("\n")[0]}</span>}
<p>{char.vorstellung ? char.vorstellung[lang].split("\n")[1] : ""}</p>
{char.mehrLink && (
<p class="character-more-lead">
{t.shop.moreAboutCharacterLead}{" "}
<a class="btn btn-outline btn-sm" href={char.mehrLink} target="_blank" rel="noopener">here →</a>
)}
</div>
))}
</div>
)}
</p>
)}
</div>
))}
</div>
<div class="grid grid-3">
{characterEntries.map(({ char, product }) =>
product ? (
<ProductCard product={product} lang={lang} />
) : (
<div class="card character-card">
<div class="product-media">
<div class="img-placeholder" role="img" aria-label={char.name[lang]}></div>
</div>
<h3>{char.name[lang]}</h3>
<p class="character-card-desc">{t.shop.subcategoryEmpty}</p>
</div>
)
)}
{characterEntries.map(({ product }) => <ProductCard product={product!} lang={lang} />)}
</div>
</div>
</section>
@@ -21,19 +21,18 @@ const sub = getSubcategory(kategorie!, unterkategorie!)!;
// Alle Produkte dieser Unterkategorie (unabhängig davon, welchem Charakter sie zugeordnet sind).
const alleProdukte = productsByUnterkategorie(category.slug, sub.slug);
// Jeder Charakter dieser Tier-Familie bekommt seine eigene kompakte Karte (wie die Produktkarten
// im Shop-Grid), alle nebeneinander in einer Reihe. Sobald VanVan im Adminbereich neue
// Charaktere UND passende Produkte ergänzt, wächst diese Reihe automatisch mit. Fehlt einem
// Charakter noch ein echtes Produkt, zeigt seine Karte trotzdem schon den Charaktertext (nur ohne
// Foto) — die Seite muss nicht auf das erste Foto warten, um zu existieren.
// Ein Charakter (aus dem Kategorien-Bereich im Admin) ist erstmal nur eine VORBEREITETE
// Definition — er erscheint auf der Seite NUR, wenn ihm im Admin auch wirklich ein Produkt mit
// genau diesem Charakter-Code zugeordnet wurde (das Feld "Charakter" am Produkt = seine "Rolle").
// Ohne zugeordnetes Produkt taucht er hier NICHT auf, auch nicht als Platzhalter — nur echte,
// im Admin angelegte Artikel dürfen auf der Seite erscheinen.
const characters = sub.characters ?? [];
const characterEntries = characters.map((char) => ({
char,
product: alleProdukte.find((p) => p.charakter === char.slug),
}));
const characterEntries = characters
.map((char) => ({ char, product: alleProdukte.find((p) => p.charakter === char.slug) }))
.filter((e) => e.product);
// Produkte ohne (bekannte) Charakter-Zuordnung landen als "weitere Modelle" darunter.
const zugeordneteSlugs = new Set(characterEntries.map((e) => e.product?.slug).filter(Boolean));
const zugeordneteSlugs = new Set(characterEntries.map((e) => e.product!.slug));
const weitere = alleProdukte.filter((p) => !zugeordneteSlugs.has(p.slug));
const hatCharaktere = characterEntries.length > 0;
const hatWeitere = weitere.length > 0;
@@ -55,34 +54,23 @@ const komplettLeer = !hatCharaktere && !hatWeitere;
{hatCharaktere && (
<section class="section-tight">
<div class="container">
{characterEntries.some(({ product }) => product) && (
<div class="character-stories">
{characterEntries.filter(({ product }) => product).map(({ char }) => (
<div class="card character-story">
{char.vorstellung && <span class="eyebrow">{char.vorstellung[lang].split("\n")[0]}</span>}
<p>{char.vorstellung ? char.vorstellung[lang].split("\n")[1] : ""}</p>
{char.mehrLink && (
<div class="character-stories">
{characterEntries.map(({ char }) => (
<div class="card character-story">
{char.vorstellung && <span class="eyebrow">{char.vorstellung[lang].split("\n")[0]}</span>}
<p>{char.vorstellung ? char.vorstellung[lang].split("\n")[1] : ""}</p>
{char.mehrLink && (
<p class="character-more-lead">
{t.shop.moreAboutCharacterLead}{" "}
<a class="btn btn-outline btn-sm" href={char.mehrLink} target="_blank" rel="noopener">ici →</a>
)}
</div>
))}
</div>
)}
</p>
)}
</div>
))}
</div>
<div class="grid grid-3">
{characterEntries.map(({ char, product }) =>
product ? (
<ProductCard product={product} lang={lang} />
) : (
<div class="card character-card">
<div class="product-media">
<div class="img-placeholder" role="img" aria-label={char.name[lang]}></div>
</div>
<h3>{char.name[lang]}</h3>
<p class="character-card-desc">{t.shop.subcategoryEmpty}</p>
</div>
)
)}
{characterEntries.map(({ product }) => <ProductCard product={product!} lang={lang} />)}
</div>
</div>
</section>
@@ -21,19 +21,18 @@ const sub = getSubcategory(kategorie!, unterkategorie!)!;
// Alle Produkte dieser Unterkategorie (unabhängig davon, welchem Charakter sie zugeordnet sind).
const alleProdukte = productsByUnterkategorie(category.slug, sub.slug);
// Jeder Charakter dieser Tier-Familie bekommt seine eigene kompakte Karte (wie die Produktkarten
// im Shop-Grid), alle nebeneinander in einer Reihe. Sobald VanVan im Adminbereich neue
// Charaktere UND passende Produkte ergänzt, wächst diese Reihe automatisch mit. Fehlt einem
// Charakter noch ein echtes Produkt, zeigt seine Karte trotzdem schon den Charaktertext (nur ohne
// Foto) — die Seite muss nicht auf das erste Foto warten, um zu existieren.
// Ein Charakter (aus dem Kategorien-Bereich im Admin) ist erstmal nur eine VORBEREITETE
// Definition — er erscheint auf der Seite NUR, wenn ihm im Admin auch wirklich ein Produkt mit
// genau diesem Charakter-Code zugeordnet wurde (das Feld "Charakter" am Produkt = seine "Rolle").
// Ohne zugeordnetes Produkt taucht er hier NICHT auf, auch nicht als Platzhalter — nur echte,
// im Admin angelegte Artikel dürfen auf der Seite erscheinen.
const characters = sub.characters ?? [];
const characterEntries = characters.map((char) => ({
char,
product: alleProdukte.find((p) => p.charakter === char.slug),
}));
const characterEntries = characters
.map((char) => ({ char, product: alleProdukte.find((p) => p.charakter === char.slug) }))
.filter((e) => e.product);
// Produkte ohne (bekannte) Charakter-Zuordnung landen als "weitere Modelle" darunter.
const zugeordneteSlugs = new Set(characterEntries.map((e) => e.product?.slug).filter(Boolean));
const zugeordneteSlugs = new Set(characterEntries.map((e) => e.product!.slug));
const weitere = alleProdukte.filter((p) => !zugeordneteSlugs.has(p.slug));
const hatCharaktere = characterEntries.length > 0;
const hatWeitere = weitere.length > 0;
@@ -61,35 +60,24 @@ const komplettLeer = !hatCharaktere && !hatWeitere;
<div class="container">
{/* Die persönlichen Charaktergeschichten stehen als eigene, volle Breite Boxen ÜBER der
Kartenreihe — getrennt von den "Shop"-Karten darunter, die nur Foto/Preis/Infos zeigen. */}
{characterEntries.some(({ product }) => product) && (
<div class="character-stories">
{characterEntries.filter(({ product }) => product).map(({ char }) => (
<div class="card character-story">
{char.vorstellung && <span class="eyebrow">{char.vorstellung[lang].split("\n")[0]}</span>}
<p>{char.vorstellung ? char.vorstellung[lang].split("\n")[1] : ""}</p>
{char.mehrLink && (
<div class="character-stories">
{characterEntries.map(({ char }) => (
<div class="card character-story">
{char.vorstellung && <span class="eyebrow">{char.vorstellung[lang].split("\n")[0]}</span>}
<p>{char.vorstellung ? char.vorstellung[lang].split("\n")[1] : ""}</p>
{char.mehrLink && (
<p class="character-more-lead">
{t.shop.moreAboutCharacterLead}{" "}
<a class="btn btn-outline btn-sm" href={char.mehrLink} target="_blank" rel="noopener">hier →</a>
)}
</div>
))}
</div>
)}
</p>
)}
</div>
))}
</div>
{/* Kartenreihe wie im Shop-Grid — nur Foto, Preis und Kaufinfos, keine Geschichten. */}
<div class="grid grid-3">
{characterEntries.map(({ char, product }) =>
product ? (
<ProductCard product={product} lang={lang} />
) : (
<div class="card character-card">
<div class="product-media">
<div class="img-placeholder" role="img" aria-label={char.name[lang]}></div>
</div>
<h3>{char.name[lang]}</h3>
<p class="character-card-desc">{t.shop.subcategoryEmpty}</p>
</div>
)
)}
{characterEntries.map(({ product }) => <ProductCard product={product!} lang={lang} />)}
</div>
</div>
</section>
+7
View File
@@ -868,6 +868,13 @@ a:focus-visible, button:focus-visible {
color: transparent;
}
.character-story p { margin: 0 0 1.1rem; max-width: 62ch; font-size: 1.02rem; color: var(--c-text-muted); }
.character-story .character-more-lead {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 0.55em;
margin-bottom: 0;
}
.character-story .btn {
background: linear-gradient(135deg, var(--c-accent), var(--c-purple-glow));
color: #08181c;