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:
qcigano
2026-08-02 23:41:27 +02:00
co-authored by Claude Sonnet 5
parent 6fc64e0972
commit e48700f982
13 changed files with 290 additions and 93 deletions
+103 -4
View File
@@ -23,9 +23,24 @@ const name = product.name[lang];
const desc = product.beschreibung[lang];
---
<a href={`${localePrefix(lang)}/produkt/${product.slug}/`} class="card product-card">
<div class="product-media">
<div class="product-media" data-carousel>
{product.bilder && product.bilder.length > 0 ? (
<img class="product-photo" src={product.bilder[0]} alt={name} loading="lazy" />
<>
{product.bilder.map((bild, i) => (
<img class="product-photo carousel-img" src={bild} alt={name} loading="lazy" data-idx={i} style={i === 0 ? undefined : "display:none;"} />
))}
{product.bilder.length > 1 && (
<>
<button type="button" class="carousel-btn carousel-prev" data-dir="-1" aria-label={t.common.prevPhoto}></button>
<button type="button" class="carousel-btn carousel-next" data-dir="1" aria-label={t.common.nextPhoto}></button>
<div class="carousel-dots">
{product.bilder.map((_, i) => (
<span class={`carousel-dot ${i === 0 ? "is-active" : ""}`} data-idx={i}></span>
))}
</div>
</>
)}
</>
) : (
<div class="img-placeholder" role="img" aria-label={`${t.common.photoSoon}: ${name}`}>
{t.common.photoSoon}<br /><span class="small">{name}</span>
@@ -48,11 +63,95 @@ const desc = product.beschreibung[lang];
</div>
</a>
<script>
// Mini-Bildergalerie direkt auf der Karte (Shop-Grid, Charakter-Kacheln, "Weitere Modelle" ...):
// sobald ein Produkt mehr als ein Foto hat, kann man hier sofort mit / durchblättern, ohne
// erst die Produktseite öffnen zu müssen. EIN einziger, delegierter Klick-Listener für alle
// Karten der Seite (Astro dedupliziert dieses <script> automatisch, egal wie oft ProductCard
// auf der Seite eingebunden wird — kein Mehrfach-Registrieren). preventDefault/stopPropagation
// ist nötig, weil die ganze Karte selbst ein <a>-Link ist — sonst würde ein Klick auf den Pfeil
// gleichzeitig zur Produktseite weiterspringen.
function karussellWechseln(media: Element, nextIdx: number) {
const imgs = [...media.querySelectorAll<HTMLElement>(".carousel-img")];
const dots = [...media.querySelectorAll<HTMLElement>(".carousel-dot")];
if (imgs.length === 0) return;
const clamped = ((nextIdx % imgs.length) + imgs.length) % imgs.length;
imgs.forEach((img, i) => { img.style.display = i === clamped ? "" : "none"; });
dots.forEach((dot, i) => dot.classList.toggle("is-active", i === clamped));
}
document.addEventListener("click", (e) => {
const target = e.target as HTMLElement;
const control = target.closest<HTMLElement>(".carousel-btn, .carousel-dot");
if (!control) return;
const media = control.closest("[data-carousel]");
if (!media) return;
e.preventDefault();
e.stopPropagation();
const imgs = [...media.querySelectorAll<HTMLElement>(".carousel-img")];
const current = Math.max(0, imgs.findIndex((img) => img.style.display !== "none"));
if (control.classList.contains("carousel-dot")) {
karussellWechseln(media, Number(control.dataset.idx));
} else {
karussellWechseln(media, current + Number(control.dataset.dir));
}
});
</script>
<style>
.product-card { display: flex; flex-direction: column; gap: 0.6rem; padding: 1rem; }
.product-media { position: relative; }
.product-media { position: relative; isolation: isolate; }
.product-media .img-placeholder { aspect-ratio: 1 / 1; min-height: 0; }
.product-badges { position: absolute; top: 0.6rem; left: 0.6rem; display: flex; gap: 0.4rem; flex-wrap: wrap; }
.product-badges { position: absolute; top: 0.6rem; left: 0.6rem; display: flex; gap: 0.4rem; flex-wrap: wrap; z-index: 2; }
/* Mini-Karussell: Pfeile links/rechts direkt auf der Karte, sobald ein Produkt mehr als ein
Foto hat. Dezent, aber immer sichtbar (nicht nur bei Hover) — sonst wären sie auf
Touch-Geräten (Handy/Tablet, kein Hover) gar nicht erreichbar. */
.carousel-btn {
position: absolute;
top: 50%;
transform: translateY(-50%);
z-index: 2;
width: 28px;
height: 28px;
border-radius: 50%;
border: 1px solid rgba(244, 241, 247, 0.18);
background: rgba(10, 9, 16, 0.55);
color: #fff;
font-size: 1.15rem;
line-height: 1;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
backdrop-filter: blur(3px);
transition: background 0.2s var(--ease), border-color 0.2s var(--ease), transform 0.2s var(--ease);
}
.carousel-btn:hover { background: rgba(10, 9, 16, 0.85); border-color: var(--c-accent); }
.carousel-prev { left: 0.5rem; }
.carousel-next { right: 0.5rem; }
.carousel-prev:hover { transform: translateY(-50%) translateX(-2px); }
.carousel-next:hover { transform: translateY(-50%) translateX(2px); }
.carousel-dots {
position: absolute;
bottom: 0.55rem;
left: 0;
right: 0;
z-index: 2;
display: flex;
justify-content: center;
gap: 0.35rem;
}
.carousel-dot {
width: 6px;
height: 6px;
border-radius: 50%;
background: rgba(255, 255, 255, 0.45);
border: 1px solid rgba(10, 9, 16, 0.4);
cursor: pointer;
transition: background 0.2s var(--ease), transform 0.2s var(--ease);
}
.carousel-dot.is-active { background: var(--c-accent); transform: scale(1.25); }
.product-card h3 { font-size: 1.05rem; margin: 0.2rem 0 0; transition: color 0.2s var(--ease); }
.product-card:hover h3 { color: var(--c-accent); }
.product-card p { margin: 0; }