Charakter-Vorstellungen umgebaut: kompakte Karten nebeneinander statt großflächiger Text/Foto-Splits

Die großen abwechselnden Foto/Text-Boxen (voller Formularbreite je
Charakter, viel Leerraum bei kurzem Text) sind ersetzt durch kompakte
Karten im exakt gleichen Stil wie die Produktkarten im Shop-Grid: Foto
oben, Text darunter in voller Kartenbreite, alle Charaktere einer Familie
nebeneinander in einer Reihe statt einzeln untereinander.

Betrifft alle Tier-Familien (Hase, Kuh, Wal, Oktopus, Schildkröte,
Pinguin) — wächst weiterhin automatisch mit, sobald neue Charaktere
ergänzt werden. Die "Über die Kreative"-Seite nutzt dieselbe .intro-
CSS-Klasse für ihr eigenes Layout und ist unangetastet geblieben.

Verifiziert per Puppeteer: alle drei Hase-Karten korrekt (Foto, Text,
Link), "Über die Kreative" weiterhin fehlerfrei und unverändert.
This commit is contained in:
qcigano
2026-08-02 22:38:20 +02:00
parent ffddd33a38
commit bdb1b323c7
5 changed files with 83 additions and 112 deletions
@@ -21,16 +21,15 @@ const sub = getSubcategory(kategorie!, unterkategorie!)!;
// Alle Produkte dieser Unterkategorie (unabhängig davon, welchem Charakter sie zugeordnet sind). // Alle Produkte dieser Unterkategorie (unabhängig davon, welchem Charakter sie zugeordnet sind).
const alleProdukte = productsByUnterkategorie(category.slug, sub.slug); const alleProdukte = productsByUnterkategorie(category.slug, sub.slug);
// Jeder Charakter dieser Tier-Familie bekommt seine eigene Box, abwechselnd Foto links/rechts — // Jeder Charakter dieser Tier-Familie bekommt seine eigene kompakte Karte (wie die Produktkarten
// 1. Charakter links, 2. rechts, 3. wieder links, usw. Sobald VanVan im Adminbereich neue // im Shop-Grid), alle nebeneinander in einer Reihe. Sobald VanVan im Adminbereich neue
// Charaktere UND passende Produkte ergänzt, wächst diese Liste automatisch mit. Fehlt einem // Charaktere UND passende Produkte ergänzt, wächst diese Reihe automatisch mit. Fehlt einem
// Charakter noch ein echtes Produkt, zeigt seine Box trotzdem schon den Charaktertext (nur ohne // 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. // Foto) — die Seite muss nicht auf das erste Foto warten, um zu existieren.
const characters = sub.characters ?? []; const characters = sub.characters ?? [];
const characterEntries = characters.map((char, index) => ({ const characterEntries = characters.map((char) => ({
char, char,
product: alleProdukte.find((p) => p.charakter === char.slug), product: alleProdukte.find((p) => p.charakter === char.slug),
reverse: index % 2 === 0,
})); }));
// Produkte ohne (bekannte) Charakter-Zuordnung landen als "weitere Modelle" darunter. // Produkte ohne (bekannte) Charakter-Zuordnung landen als "weitere Modelle" darunter.
@@ -51,36 +50,27 @@ const weitere = alleProdukte.filter((p) => !zugeordneteSlugs.has(p.slug));
</div> </div>
{characterEntries.length > 0 ? ( {characterEntries.length > 0 ? (
characterEntries.map(({ char, product, reverse }) => ( <section class="section-tight">
<section class="section-tight"> <div class="container grid grid-3">
<div class={`container grid grid-2 intro ${reverse ? "intro-reverse" : ""}`}> {characterEntries.map(({ char, product }) => (
{product ? ( <div class="card character-card">
<a class="portrait-frame portrait" href={`/ch/produkt/${product.slug}/`} aria-label={product.name[lang]}> <div class="product-media">
{product.bilder && product.bilder.length > 0 ? ( {product && product.bilder && product.bilder.length > 0 ? (
<img class="product-photo" src={product.bilder[0]} alt={product.name[lang]} loading="lazy" /> <img class="product-photo" src={product.bilder[0]} alt={product.name[lang]} loading="lazy" />
) : ( ) : (
<div class="img-placeholder" role="img" aria-label={product.name[lang]}></div> <div class="img-placeholder" role="img" aria-label={product ? product.name[lang] : char.name[lang]}></div>
)} )}
</a>
) : (
<div class="portrait-frame portrait">
<div class="img-placeholder" role="img" aria-label={char.name[lang]}></div>
</div> </div>
)}
<div class="intro-text">
{char.vorstellung && <span class="eyebrow">{char.vorstellung[lang].split("\n")[0]}</span>} {char.vorstellung && <span class="eyebrow">{char.vorstellung[lang].split("\n")[0]}</span>}
<h2>{product ? <a href={`/ch/produkt/${product.slug}/`}>{product.name[lang]}</a> : char.name[lang]}</h2> <h3>{product ? <a href={`/ch/produkt/${product.slug}/`}>{product.name[lang]}</a> : char.name[lang]}</h3>
<p class="lead">{char.vorstellung ? char.vorstellung[lang].split("\n")[1] : (product ? product.beschreibung[lang] : t.shop.subcategoryEmpty)}</p> <p class="character-card-desc">{char.vorstellung ? char.vorstellung[lang].split("\n")[1] : (product ? product.beschreibung[lang] : t.shop.subcategoryEmpty)}</p>
{char.mehrLink && ( {char.mehrLink && (
<div class="character-more"> <a class="btn btn-outline btn-sm character-more-btn" href={char.mehrLink} target="_blank" rel="noopener">da →</a>
<p>Wenn du meh über mich wüsse witt, de klick</p>
<a class="btn btn-outline" href={char.mehrLink} target="_blank" rel="noopener">da →</a>
</div>
)} )}
</div> </div>
</div> ))}
</section> </div>
)) </section>
) : ( ) : (
<section class="section-tight"> <section class="section-tight">
<div class="container"> <div class="container">
@@ -21,16 +21,15 @@ const sub = getSubcategory(kategorie!, unterkategorie!)!;
// Alle Produkte dieser Unterkategorie (unabhängig davon, welchem Charakter sie zugeordnet sind). // Alle Produkte dieser Unterkategorie (unabhängig davon, welchem Charakter sie zugeordnet sind).
const alleProdukte = productsByUnterkategorie(category.slug, sub.slug); const alleProdukte = productsByUnterkategorie(category.slug, sub.slug);
// Jeder Charakter dieser Tier-Familie bekommt seine eigene Box, abwechselnd Foto links/rechts — // Jeder Charakter dieser Tier-Familie bekommt seine eigene kompakte Karte (wie die Produktkarten
// 1. Charakter links, 2. rechts, 3. wieder links, usw. Sobald VanVan im Adminbereich neue // im Shop-Grid), alle nebeneinander in einer Reihe. Sobald VanVan im Adminbereich neue
// Charaktere UND passende Produkte ergänzt, wächst diese Liste automatisch mit. Fehlt einem // Charaktere UND passende Produkte ergänzt, wächst diese Reihe automatisch mit. Fehlt einem
// Charakter noch ein echtes Produkt, zeigt seine Box trotzdem schon den Charaktertext (nur ohne // 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. // Foto) — die Seite muss nicht auf das erste Foto warten, um zu existieren.
const characters = sub.characters ?? []; const characters = sub.characters ?? [];
const characterEntries = characters.map((char, index) => ({ const characterEntries = characters.map((char) => ({
char, char,
product: alleProdukte.find((p) => p.charakter === char.slug), product: alleProdukte.find((p) => p.charakter === char.slug),
reverse: index % 2 === 0,
})); }));
// Produkte ohne (bekannte) Charakter-Zuordnung landen als "weitere Modelle" darunter. // Produkte ohne (bekannte) Charakter-Zuordnung landen als "weitere Modelle" darunter.
@@ -51,36 +50,27 @@ const weitere = alleProdukte.filter((p) => !zugeordneteSlugs.has(p.slug));
</div> </div>
{characterEntries.length > 0 ? ( {characterEntries.length > 0 ? (
characterEntries.map(({ char, product, reverse }) => ( <section class="section-tight">
<section class="section-tight"> <div class="container grid grid-3">
<div class={`container grid grid-2 intro ${reverse ? "intro-reverse" : ""}`}> {characterEntries.map(({ char, product }) => (
{product ? ( <div class="card character-card">
<a class="portrait-frame portrait" href={`/en/produkt/${product.slug}/`} aria-label={product.name[lang]}> <div class="product-media">
{product.bilder && product.bilder.length > 0 ? ( {product && product.bilder && product.bilder.length > 0 ? (
<img class="product-photo" src={product.bilder[0]} alt={product.name[lang]} loading="lazy" /> <img class="product-photo" src={product.bilder[0]} alt={product.name[lang]} loading="lazy" />
) : ( ) : (
<div class="img-placeholder" role="img" aria-label={product.name[lang]}></div> <div class="img-placeholder" role="img" aria-label={product ? product.name[lang] : char.name[lang]}></div>
)} )}
</a>
) : (
<div class="portrait-frame portrait">
<div class="img-placeholder" role="img" aria-label={char.name[lang]}></div>
</div> </div>
)}
<div class="intro-text">
{char.vorstellung && <span class="eyebrow">{char.vorstellung[lang].split("\n")[0]}</span>} {char.vorstellung && <span class="eyebrow">{char.vorstellung[lang].split("\n")[0]}</span>}
<h2>{product ? <a href={`/en/produkt/${product.slug}/`}>{product.name[lang]}</a> : char.name[lang]}</h2> <h3>{product ? <a href={`/en/produkt/${product.slug}/`}>{product.name[lang]}</a> : char.name[lang]}</h3>
<p class="lead">{char.vorstellung ? char.vorstellung[lang].split("\n")[1] : (product ? product.beschreibung[lang] : t.shop.subcategoryEmpty)}</p> <p class="character-card-desc">{char.vorstellung ? char.vorstellung[lang].split("\n")[1] : (product ? product.beschreibung[lang] : t.shop.subcategoryEmpty)}</p>
{char.mehrLink && ( {char.mehrLink && (
<div class="character-more"> <a class="btn btn-outline btn-sm character-more-btn" href={char.mehrLink} target="_blank" rel="noopener">here →</a>
<p>If you'd like to learn more about me, just click</p>
<a class="btn btn-outline" href={char.mehrLink} target="_blank" rel="noopener">here →</a>
</div>
)} )}
</div> </div>
</div> ))}
</section> </div>
)) </section>
) : ( ) : (
<section class="section-tight"> <section class="section-tight">
<div class="container"> <div class="container">
@@ -21,16 +21,15 @@ const sub = getSubcategory(kategorie!, unterkategorie!)!;
// Alle Produkte dieser Unterkategorie (unabhängig davon, welchem Charakter sie zugeordnet sind). // Alle Produkte dieser Unterkategorie (unabhängig davon, welchem Charakter sie zugeordnet sind).
const alleProdukte = productsByUnterkategorie(category.slug, sub.slug); const alleProdukte = productsByUnterkategorie(category.slug, sub.slug);
// Jeder Charakter dieser Tier-Familie bekommt seine eigene Box, abwechselnd Foto links/rechts — // Jeder Charakter dieser Tier-Familie bekommt seine eigene kompakte Karte (wie die Produktkarten
// 1. Charakter links, 2. rechts, 3. wieder links, usw. Sobald VanVan im Adminbereich neue // im Shop-Grid), alle nebeneinander in einer Reihe. Sobald VanVan im Adminbereich neue
// Charaktere UND passende Produkte ergänzt, wächst diese Liste automatisch mit. Fehlt einem // Charaktere UND passende Produkte ergänzt, wächst diese Reihe automatisch mit. Fehlt einem
// Charakter noch ein echtes Produkt, zeigt seine Box trotzdem schon den Charaktertext (nur ohne // 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. // Foto) — die Seite muss nicht auf das erste Foto warten, um zu existieren.
const characters = sub.characters ?? []; const characters = sub.characters ?? [];
const characterEntries = characters.map((char, index) => ({ const characterEntries = characters.map((char) => ({
char, char,
product: alleProdukte.find((p) => p.charakter === char.slug), product: alleProdukte.find((p) => p.charakter === char.slug),
reverse: index % 2 === 0,
})); }));
// Produkte ohne (bekannte) Charakter-Zuordnung landen als "weitere Modelle" darunter. // Produkte ohne (bekannte) Charakter-Zuordnung landen als "weitere Modelle" darunter.
@@ -51,36 +50,27 @@ const weitere = alleProdukte.filter((p) => !zugeordneteSlugs.has(p.slug));
</div> </div>
{characterEntries.length > 0 ? ( {characterEntries.length > 0 ? (
characterEntries.map(({ char, product, reverse }) => ( <section class="section-tight">
<section class="section-tight"> <div class="container grid grid-3">
<div class={`container grid grid-2 intro ${reverse ? "intro-reverse" : ""}`}> {characterEntries.map(({ char, product }) => (
{product ? ( <div class="card character-card">
<a class="portrait-frame portrait" href={`/fr/produkt/${product.slug}/`} aria-label={product.name[lang]}> <div class="product-media">
{product.bilder && product.bilder.length > 0 ? ( {product && product.bilder && product.bilder.length > 0 ? (
<img class="product-photo" src={product.bilder[0]} alt={product.name[lang]} loading="lazy" /> <img class="product-photo" src={product.bilder[0]} alt={product.name[lang]} loading="lazy" />
) : ( ) : (
<div class="img-placeholder" role="img" aria-label={product.name[lang]}></div> <div class="img-placeholder" role="img" aria-label={product ? product.name[lang] : char.name[lang]}></div>
)} )}
</a>
) : (
<div class="portrait-frame portrait">
<div class="img-placeholder" role="img" aria-label={char.name[lang]}></div>
</div> </div>
)}
<div class="intro-text">
{char.vorstellung && <span class="eyebrow">{char.vorstellung[lang].split("\n")[0]}</span>} {char.vorstellung && <span class="eyebrow">{char.vorstellung[lang].split("\n")[0]}</span>}
<h2>{product ? <a href={`/fr/produkt/${product.slug}/`}>{product.name[lang]}</a> : char.name[lang]}</h2> <h3>{product ? <a href={`/fr/produkt/${product.slug}/`}>{product.name[lang]}</a> : char.name[lang]}</h3>
<p class="lead">{char.vorstellung ? char.vorstellung[lang].split("\n")[1] : (product ? product.beschreibung[lang] : t.shop.subcategoryEmpty)}</p> <p class="character-card-desc">{char.vorstellung ? char.vorstellung[lang].split("\n")[1] : (product ? product.beschreibung[lang] : t.shop.subcategoryEmpty)}</p>
{char.mehrLink && ( {char.mehrLink && (
<div class="character-more"> <a class="btn btn-outline btn-sm character-more-btn" href={char.mehrLink} target="_blank" rel="noopener">ici →</a>
<p>Si tu veux en savoir plus sur moi, clique</p>
<a class="btn btn-outline" href={char.mehrLink} target="_blank" rel="noopener">ici →</a>
</div>
)} )}
</div> </div>
</div> ))}
</section> </div>
)) </section>
) : ( ) : (
<section class="section-tight"> <section class="section-tight">
<div class="container"> <div class="container">
@@ -21,16 +21,15 @@ const sub = getSubcategory(kategorie!, unterkategorie!)!;
// Alle Produkte dieser Unterkategorie (unabhängig davon, welchem Charakter sie zugeordnet sind). // Alle Produkte dieser Unterkategorie (unabhängig davon, welchem Charakter sie zugeordnet sind).
const alleProdukte = productsByUnterkategorie(category.slug, sub.slug); const alleProdukte = productsByUnterkategorie(category.slug, sub.slug);
// Jeder Charakter dieser Tier-Familie bekommt seine eigene Box, abwechselnd Foto links/rechts — // Jeder Charakter dieser Tier-Familie bekommt seine eigene kompakte Karte (wie die Produktkarten
// 1. Charakter links, 2. rechts, 3. wieder links, usw. Sobald VanVan im Adminbereich neue // im Shop-Grid), alle nebeneinander in einer Reihe. Sobald VanVan im Adminbereich neue
// Charaktere UND passende Produkte ergänzt, wächst diese Liste automatisch mit. Fehlt einem // Charaktere UND passende Produkte ergänzt, wächst diese Reihe automatisch mit. Fehlt einem
// Charakter noch ein echtes Produkt, zeigt seine Box trotzdem schon den Charaktertext (nur ohne // 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. // Foto) — die Seite muss nicht auf das erste Foto warten, um zu existieren.
const characters = sub.characters ?? []; const characters = sub.characters ?? [];
const characterEntries = characters.map((char, index) => ({ const characterEntries = characters.map((char) => ({
char, char,
product: alleProdukte.find((p) => p.charakter === char.slug), product: alleProdukte.find((p) => p.charakter === char.slug),
reverse: index % 2 === 0,
})); }));
// Produkte ohne (bekannte) Charakter-Zuordnung landen als "weitere Modelle" darunter. // Produkte ohne (bekannte) Charakter-Zuordnung landen als "weitere Modelle" darunter.
@@ -51,36 +50,27 @@ const weitere = alleProdukte.filter((p) => !zugeordneteSlugs.has(p.slug));
</div> </div>
{characterEntries.length > 0 ? ( {characterEntries.length > 0 ? (
characterEntries.map(({ char, product, reverse }) => ( <section class="section-tight">
<section class="section-tight"> <div class="container grid grid-3">
<div class={`container grid grid-2 intro ${reverse ? "intro-reverse" : ""}`}> {characterEntries.map(({ char, product }) => (
{product ? ( <div class="card character-card">
<a class="portrait-frame portrait" href={`/produkt/${product.slug}/`} aria-label={product.name[lang]}> <div class="product-media">
{product.bilder && product.bilder.length > 0 ? ( {product && product.bilder && product.bilder.length > 0 ? (
<img class="product-photo" src={product.bilder[0]} alt={product.name[lang]} loading="lazy" /> <img class="product-photo" src={product.bilder[0]} alt={product.name[lang]} loading="lazy" />
) : ( ) : (
<div class="img-placeholder" role="img" aria-label={product.name[lang]}></div> <div class="img-placeholder" role="img" aria-label={product ? product.name[lang] : char.name[lang]}></div>
)} )}
</a>
) : (
<div class="portrait-frame portrait">
<div class="img-placeholder" role="img" aria-label={char.name[lang]}></div>
</div> </div>
)}
<div class="intro-text">
{char.vorstellung && <span class="eyebrow">{char.vorstellung[lang].split("\n")[0]}</span>} {char.vorstellung && <span class="eyebrow">{char.vorstellung[lang].split("\n")[0]}</span>}
<h2>{product ? <a href={`/produkt/${product.slug}/`}>{product.name[lang]}</a> : char.name[lang]}</h2> <h3>{product ? <a href={`/produkt/${product.slug}/`}>{product.name[lang]}</a> : char.name[lang]}</h3>
<p class="lead">{char.vorstellung ? char.vorstellung[lang].split("\n")[1] : (product ? product.beschreibung[lang] : t.shop.subcategoryEmpty)}</p> <p class="character-card-desc">{char.vorstellung ? char.vorstellung[lang].split("\n")[1] : (product ? product.beschreibung[lang] : t.shop.subcategoryEmpty)}</p>
{char.mehrLink && ( {char.mehrLink && (
<div class="character-more"> <a class="btn btn-outline btn-sm character-more-btn" href={char.mehrLink} target="_blank" rel="noopener">hier →</a>
<p>Wenn du mehr über mich erfahren möchtest, dann klicke</p>
<a class="btn btn-outline" href={char.mehrLink} target="_blank" rel="noopener">hier →</a>
</div>
)} )}
</div> </div>
</div> ))}
</section> </div>
)) </section>
) : ( ) : (
<section class="section-tight"> <section class="section-tight">
<div class="container"> <div class="container">
+11
View File
@@ -804,6 +804,17 @@ a:focus-visible, button:focus-visible {
noch mehr Produkte derselben Reihe kommen (Charakter-Familie), egal wie viele es am Ende sind. */ noch mehr Produkte derselben Reihe kommen (Charakter-Familie), egal wie viele es am Ende sind. */
.more-variants-label { text-transform: uppercase; letter-spacing: 0.08em; color: var(--c-text-muted); margin-bottom: 0.8rem; } .more-variants-label { text-transform: uppercase; letter-spacing: 0.08em; color: var(--c-text-muted); margin-bottom: 0.8rem; }
/* Charakter-Karten auf den Tier-Familien-Seiten (z.B. HasiDog/HasiGott/Mama Hase unter "Hase") —
exakt dieselbe Karten-Optik wie die Produktkarten im Shop-Grid (Foto oben, Text darunter in
voller Breite), nur mit persönlichem Charaktertext statt Produktbeschreibung. */
.character-card { display: flex; flex-direction: column; gap: 0.6rem; padding: 1rem; }
.character-card .product-media { position: relative; }
.character-card h3 { font-size: 1.15rem; margin: 0.1rem 0 0; }
.character-card h3 a { color: inherit; transition: color 0.2s var(--ease); }
.character-card h3 a:hover { color: var(--c-accent); }
.character-card-desc { margin: 0; color: var(--c-text-muted); font-size: 0.92rem; }
.character-more-btn { margin-top: auto; align-self: flex-start; }
/* Hauptkategorie-Überschrift zwischen den Unterkategorie-Chips und der eigentlichen /* Hauptkategorie-Überschrift zwischen den Unterkategorie-Chips und der eigentlichen
Produktreihe auf der Kategorie-Seite — macht klar, welche Hauptkategorie man gerade Produktreihe auf der Kategorie-Seite — macht klar, welche Hauptkategorie man gerade
durchstöbert, fällt bewusst etwas mehr auf als die stille "Unterkategorien"-Beschriftung. */ durchstöbert, fällt bewusst etwas mehr auf als die stille "Unterkategorien"-Beschriftung. */