Tier-Familien-Seiten verfeinert, Foto-Karussell, Partnercode auf Sale-Ausschluss
Mehrere Feinschliff-Punkte auf den Tier-Familien-Seiten (z.B. "Hase"): - Abstand zwischen der Story-Box und der Kartenreihe verkleinert (vorher durch zwei separate Section-Umrandungen unnötig groß) — beide Bereiche teilen sich jetzt eine gemeinsame Section statt zwei gestapelte. - Platzhalter-Karten (Charaktere ohne Foto) bekommen jetzt exakt die gleiche Bildhöhe wie eine Karte mit echtem Foto (vorher griff nur die allgemeine Mindesthöhe, wirkte kleiner/anders proportioniert). - Neues optionales Feld `bannerName` an der Unterkategorie: überschreibt NUR die große Bannerüberschrift (Breadcrumb/Chips bleiben unverändert) — für "Hase" jetzt auf "HasiDog" gesetzt, den bekanntesten Charakter der Familie. - Unterkategorie-Seiten ganz ohne Produkte/Charaktere (z.B. Maschen- markierer, Holzknöpfe) zeigen jetzt schon 3 vorbereitete Platzhalter- Karten im selben Kartenstil statt nur einer nackten Textzeile — die Seite "sieht schon fertig aus", auch bevor echte Produkte da sind. Nebenbei einen Bug behoben: bei Unterkategorien ohne Charaktere, aber MIT echten Produkten, erschien vorher zusätzlich fälschlich der "noch keine Produkte"-Hinweis über der echten Produktreihe. Neu: Mini-Bildkarussell direkt auf jeder Produktkarte (Shop-Grid, Charakter-Kacheln, "Weitere Modelle", Sale-Seite, Startseite — überall wo ProductCard verwendet wird). Sobald ein Produkt mehrere Fotos hat (Admin-Bereich erlaubt das bereits über das Foto-Listenfeld), erscheinen dezente ‹/›-Pfeile plus Punkte-Indikator direkt auf der Karte, ganz ohne erst die Produktseite öffnen zu müssen. Neu: Partner-Rabattcodes können im Admin jetzt gezielt auf "gilt NICHT für Artikel, die schon im Sale sind" gestellt werden (giltAufSale: false) — der Rabatt berechnet sich dann nur noch auf den regulär bepreisten Teil des Warenkorbs, damit sich der Partner-Rabatt nicht zusätzlich auf einen bereits reduzierten Preis obendrauf summiert. Standardverhalten (Feld leer/true) bleibt wie bisher unverändert. Alle Änderungen lokal mit Test-Daten durchgespielt (mehrere Testfotos, Testprodukt im Sale + Partnercode mit/ohne giltAufSale, leere Unterkategorie-Seite) und vor dem Commit wieder zurückgesetzt. Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
6fc64e0972
commit
e48700f982
@@ -35,6 +35,9 @@ const characterEntries = characters.map((char) => ({
|
||||
// Produkte ohne (bekannte) Charakter-Zuordnung landen als "weitere Modelle" darunter.
|
||||
const zugeordneteSlugs = new Set(characterEntries.map((e) => e.product?.slug).filter(Boolean));
|
||||
const weitere = alleProdukte.filter((p) => !zugeordneteSlugs.has(p.slug));
|
||||
const hatCharaktere = characterEntries.length > 0;
|
||||
const hatWeitere = weitere.length > 0;
|
||||
const komplettLeer = !hatCharaktere && !hatWeitere;
|
||||
---
|
||||
<Layout title={`${sub.name[lang]} – ${category.name[lang]}`} description={`${sub.name[lang]} bi Van's DIY & Bastelbedarf.`} lang={lang} path={`/shop/${category.slug}/${sub.slug}/`}>
|
||||
<div class="container">
|
||||
@@ -44,18 +47,16 @@ const weitere = alleProdukte.filter((p) => !zugeordneteSlugs.has(p.slug));
|
||||
<section class="page-banner" style="background-image:url('/img/banner-shop.webp'); --banner-ratio: 1600 / 729;">
|
||||
<div class="page-banner-content">
|
||||
<span class="eyebrow" aria-hidden="true">{category.icon} {category.name[lang]}</span>
|
||||
<h1>{sub.name[lang]}</h1>
|
||||
<h1>{sub.bannerName ? sub.bannerName[lang] : sub.name[lang]}</h1>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
{characterEntries.length > 0 ? (
|
||||
<>
|
||||
{/* 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) && (
|
||||
<section class="section-tight">
|
||||
<div class="container character-stories">
|
||||
{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>}
|
||||
@@ -67,12 +68,9 @@ const weitere = alleProdukte.filter((p) => !zugeordneteSlugs.has(p.slug));
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
)}
|
||||
|
||||
{/* Kartenreihe wie im Shop-Grid — nur Foto, Preis und Kaufinfos, keine Geschichten. */}
|
||||
<section class="section-tight">
|
||||
<div class="container grid grid-3">
|
||||
<div class="grid grid-3">
|
||||
{characterEntries.map(({ char, product }) =>
|
||||
product ? (
|
||||
<ProductCard product={product} lang={lang} />
|
||||
@@ -87,20 +85,32 @@ const weitere = alleProdukte.filter((p) => !zugeordneteSlugs.has(p.slug));
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
</>
|
||||
) : (
|
||||
<section class="section-tight">
|
||||
<div class="container">
|
||||
<p class="small">{t.shop.subcategoryEmpty}</p>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{weitere.length > 0 && (
|
||||
{komplettLeer && (
|
||||
<section class="section-tight">
|
||||
<div class="container">
|
||||
<h2 class="text-center small more-variants-label">{t.shop.moreVariants}</h2>
|
||||
<div class="grid grid-3">
|
||||
{[0, 1, 2].map(() => (
|
||||
<div class="card character-card">
|
||||
<div class="product-media">
|
||||
<div class="img-placeholder" role="img" aria-label={sub.name[lang]}></div>
|
||||
</div>
|
||||
<h3>{sub.name[lang]}</h3>
|
||||
<p class="character-card-desc">{t.shop.subcategoryEmpty}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{hatWeitere && (
|
||||
<section class="section-tight">
|
||||
<div class="container">
|
||||
{hatCharaktere && <h2 class="text-center small more-variants-label">{t.shop.moreVariants}</h2>}
|
||||
<div class="grid grid-3">{weitere.map((p) => <ProductCard product={p} lang={lang} />)}</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -35,6 +35,9 @@ const characterEntries = characters.map((char) => ({
|
||||
// Produkte ohne (bekannte) Charakter-Zuordnung landen als "weitere Modelle" darunter.
|
||||
const zugeordneteSlugs = new Set(characterEntries.map((e) => e.product?.slug).filter(Boolean));
|
||||
const weitere = alleProdukte.filter((p) => !zugeordneteSlugs.has(p.slug));
|
||||
const hatCharaktere = characterEntries.length > 0;
|
||||
const hatWeitere = weitere.length > 0;
|
||||
const komplettLeer = !hatCharaktere && !hatWeitere;
|
||||
---
|
||||
<Layout title={`${sub.name[lang]} – ${category.name[lang]}`} description={`${sub.name[lang]} at Van's DIY & Bastelbedarf.`} lang={lang} path={`/shop/${category.slug}/${sub.slug}/`}>
|
||||
<div class="container">
|
||||
@@ -44,18 +47,16 @@ const weitere = alleProdukte.filter((p) => !zugeordneteSlugs.has(p.slug));
|
||||
<section class="page-banner" style="background-image:url('/img/banner-shop.webp'); --banner-ratio: 1600 / 729;">
|
||||
<div class="page-banner-content">
|
||||
<span class="eyebrow" aria-hidden="true">{category.icon} {category.name[lang]}</span>
|
||||
<h1>{sub.name[lang]}</h1>
|
||||
<h1>{sub.bannerName ? sub.bannerName[lang] : sub.name[lang]}</h1>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
{characterEntries.length > 0 ? (
|
||||
<>
|
||||
{/* 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) && (
|
||||
<section class="section-tight">
|
||||
<div class="container character-stories">
|
||||
{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>}
|
||||
@@ -67,12 +68,9 @@ const weitere = alleProdukte.filter((p) => !zugeordneteSlugs.has(p.slug));
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
)}
|
||||
|
||||
{/* Kartenreihe wie im Shop-Grid — nur Foto, Preis und Kaufinfos, keine Geschichten. */}
|
||||
<section class="section-tight">
|
||||
<div class="container grid grid-3">
|
||||
<div class="grid grid-3">
|
||||
{characterEntries.map(({ char, product }) =>
|
||||
product ? (
|
||||
<ProductCard product={product} lang={lang} />
|
||||
@@ -87,20 +85,32 @@ const weitere = alleProdukte.filter((p) => !zugeordneteSlugs.has(p.slug));
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
</>
|
||||
) : (
|
||||
<section class="section-tight">
|
||||
<div class="container">
|
||||
<p class="small">{t.shop.subcategoryEmpty}</p>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{weitere.length > 0 && (
|
||||
{komplettLeer && (
|
||||
<section class="section-tight">
|
||||
<div class="container">
|
||||
<h2 class="text-center small more-variants-label">{t.shop.moreVariants}</h2>
|
||||
<div class="grid grid-3">
|
||||
{[0, 1, 2].map(() => (
|
||||
<div class="card character-card">
|
||||
<div class="product-media">
|
||||
<div class="img-placeholder" role="img" aria-label={sub.name[lang]}></div>
|
||||
</div>
|
||||
<h3>{sub.name[lang]}</h3>
|
||||
<p class="character-card-desc">{t.shop.subcategoryEmpty}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{hatWeitere && (
|
||||
<section class="section-tight">
|
||||
<div class="container">
|
||||
{hatCharaktere && <h2 class="text-center small more-variants-label">{t.shop.moreVariants}</h2>}
|
||||
<div class="grid grid-3">{weitere.map((p) => <ProductCard product={p} lang={lang} />)}</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -35,6 +35,9 @@ const characterEntries = characters.map((char) => ({
|
||||
// Produkte ohne (bekannte) Charakter-Zuordnung landen als "weitere Modelle" darunter.
|
||||
const zugeordneteSlugs = new Set(characterEntries.map((e) => e.product?.slug).filter(Boolean));
|
||||
const weitere = alleProdukte.filter((p) => !zugeordneteSlugs.has(p.slug));
|
||||
const hatCharaktere = characterEntries.length > 0;
|
||||
const hatWeitere = weitere.length > 0;
|
||||
const komplettLeer = !hatCharaktere && !hatWeitere;
|
||||
---
|
||||
<Layout title={`${sub.name[lang]} – ${category.name[lang]}`} description={`${sub.name[lang]} chez Van's DIY & Bastelbedarf.`} lang={lang} path={`/shop/${category.slug}/${sub.slug}/`}>
|
||||
<div class="container">
|
||||
@@ -44,18 +47,16 @@ const weitere = alleProdukte.filter((p) => !zugeordneteSlugs.has(p.slug));
|
||||
<section class="page-banner" style="background-image:url('/img/banner-shop.webp'); --banner-ratio: 1600 / 729;">
|
||||
<div class="page-banner-content">
|
||||
<span class="eyebrow" aria-hidden="true">{category.icon} {category.name[lang]}</span>
|
||||
<h1>{sub.name[lang]}</h1>
|
||||
<h1>{sub.bannerName ? sub.bannerName[lang] : sub.name[lang]}</h1>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
{characterEntries.length > 0 ? (
|
||||
<>
|
||||
{/* 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) && (
|
||||
<section class="section-tight">
|
||||
<div class="container character-stories">
|
||||
{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>}
|
||||
@@ -67,12 +68,9 @@ const weitere = alleProdukte.filter((p) => !zugeordneteSlugs.has(p.slug));
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
)}
|
||||
|
||||
{/* Kartenreihe wie im Shop-Grid — nur Foto, Preis und Kaufinfos, keine Geschichten. */}
|
||||
<section class="section-tight">
|
||||
<div class="container grid grid-3">
|
||||
<div class="grid grid-3">
|
||||
{characterEntries.map(({ char, product }) =>
|
||||
product ? (
|
||||
<ProductCard product={product} lang={lang} />
|
||||
@@ -87,20 +85,32 @@ const weitere = alleProdukte.filter((p) => !zugeordneteSlugs.has(p.slug));
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
</>
|
||||
) : (
|
||||
<section class="section-tight">
|
||||
<div class="container">
|
||||
<p class="small">{t.shop.subcategoryEmpty}</p>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{weitere.length > 0 && (
|
||||
{komplettLeer && (
|
||||
<section class="section-tight">
|
||||
<div class="container">
|
||||
<h2 class="text-center small more-variants-label">{t.shop.moreVariants}</h2>
|
||||
<div class="grid grid-3">
|
||||
{[0, 1, 2].map(() => (
|
||||
<div class="card character-card">
|
||||
<div class="product-media">
|
||||
<div class="img-placeholder" role="img" aria-label={sub.name[lang]}></div>
|
||||
</div>
|
||||
<h3>{sub.name[lang]}</h3>
|
||||
<p class="character-card-desc">{t.shop.subcategoryEmpty}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{hatWeitere && (
|
||||
<section class="section-tight">
|
||||
<div class="container">
|
||||
{hatCharaktere && <h2 class="text-center small more-variants-label">{t.shop.moreVariants}</h2>}
|
||||
<div class="grid grid-3">{weitere.map((p) => <ProductCard product={p} lang={lang} />)}</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -35,6 +35,13 @@ const characterEntries = characters.map((char) => ({
|
||||
// Produkte ohne (bekannte) Charakter-Zuordnung landen als "weitere Modelle" darunter.
|
||||
const zugeordneteSlugs = new Set(characterEntries.map((e) => e.product?.slug).filter(Boolean));
|
||||
const weitere = alleProdukte.filter((p) => !zugeordneteSlugs.has(p.slug));
|
||||
const hatCharaktere = characterEntries.length > 0;
|
||||
const hatWeitere = weitere.length > 0;
|
||||
// Damit "schon vorbereitet aussieht", auch wenn VanVan hier noch gar keine Produkte/Charaktere
|
||||
// eingetragen hat: statt einer nackten Text-Zeile zeigen wir schon die spätere Karten-Optik,
|
||||
// nur eben mit Platzhaltern. Sobald echte Produkte/Charaktere dazukommen, verschwinden diese
|
||||
// automatisch (siehe hatCharaktere/hatWeitere oben).
|
||||
const komplettLeer = !hatCharaktere && !hatWeitere;
|
||||
---
|
||||
<Layout title={`${sub.name[lang]} – ${category.name[lang]}`} description={`${sub.name[lang]} bei Van's DIY & Bastelbedarf.`} lang={lang} path={`/shop/${category.slug}/${sub.slug}/`}>
|
||||
<div class="container">
|
||||
@@ -44,18 +51,18 @@ const weitere = alleProdukte.filter((p) => !zugeordneteSlugs.has(p.slug));
|
||||
<section class="page-banner" style="background-image:url('/img/banner-shop.webp'); --banner-ratio: 1600 / 729;">
|
||||
<div class="page-banner-content">
|
||||
<span class="eyebrow" aria-hidden="true">{category.icon} {category.name[lang]}</span>
|
||||
<h1>{sub.name[lang]}</h1>
|
||||
<h1>{sub.bannerName ? sub.bannerName[lang] : sub.name[lang]}</h1>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
{characterEntries.length > 0 ? (
|
||||
<>
|
||||
{/* 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) && (
|
||||
<section class="section-tight">
|
||||
<div class="container character-stories">
|
||||
{hatCharaktere && (
|
||||
<section class="section-tight">
|
||||
<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>}
|
||||
@@ -67,12 +74,10 @@ const weitere = alleProdukte.filter((p) => !zugeordneteSlugs.has(p.slug));
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
)}
|
||||
|
||||
{/* Kartenreihe wie im Shop-Grid — nur Foto, Preis und Kaufinfos, keine Geschichten. */}
|
||||
<section class="section-tight">
|
||||
<div class="container grid grid-3">
|
||||
{/* 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} />
|
||||
@@ -87,20 +92,32 @@ const weitere = alleProdukte.filter((p) => !zugeordneteSlugs.has(p.slug));
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
</>
|
||||
) : (
|
||||
<section class="section-tight">
|
||||
<div class="container">
|
||||
<p class="small">{t.shop.subcategoryEmpty}</p>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{weitere.length > 0 && (
|
||||
{komplettLeer && (
|
||||
<section class="section-tight">
|
||||
<div class="container">
|
||||
<h2 class="text-center small more-variants-label">{t.shop.moreVariants}</h2>
|
||||
<div class="grid grid-3">
|
||||
{[0, 1, 2].map(() => (
|
||||
<div class="card character-card">
|
||||
<div class="product-media">
|
||||
<div class="img-placeholder" role="img" aria-label={sub.name[lang]}></div>
|
||||
</div>
|
||||
<h3>{sub.name[lang]}</h3>
|
||||
<p class="character-card-desc">{t.shop.subcategoryEmpty}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{hatWeitere && (
|
||||
<section class="section-tight">
|
||||
<div class="container">
|
||||
{hatCharaktere && <h2 class="text-center small more-variants-label">{t.shop.moreVariants}</h2>}
|
||||
<div class="grid grid-3">{weitere.map((p) => <ProductCard product={p} lang={lang} />)}</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
Reference in New Issue
Block a user