Neu: Kundenrezensionen mit Herzen in Lila-Babyblau, ueberall im Shop integriert

Neue HeartRating-Komponente (Herzen statt Sterne, Lila-Babyblau-Verlauf wie
der Rest der Markenoptik) + komplettes Rezensions-System:

- Produktkarten: Bewertungszeile (Herzen + Anzahl) unter dem Titel, nur wenn
  Bewertungen vorhanden sind.
- Produktseite: volle "Kundenrezensionen"-Sektion mit Durchschnitt und
  einzelnen Rezensionen (Name, Herzen, Datum, Text) bzw. "Noch keine
  Bewertungen"-Hinweis.
- Shop-Filter: neuer "Mindestbewertung"-Filter im Filter-Sidebar + neue
  Sortierung "Beste Bewertung".
- Startseite: "Was unsere Kund:innen sagen"-Sektion mit Gesamtbewertung und
  den 3 neuesten Rezensionen - erscheint automatisch, sobald die erste echte
  Bewertung existiert.
- Adminbereich: neues Feld "Bewertungen" pro Produkt (Name/Herzen/Text/Datum),
  mit deutlichem Hinweis, dass hier nur echte Kundenrueckmeldungen eingetragen
  werden duerfen - erfundene Bewertungen sind in Deutschland/der EU gesetzlich
  verboten (UWG). Deshalb bewusst OHNE Beispiel-Bewertungen im Code: alle
  Bereiche starten leer und blenden sich automatisch ein, sobald VanVan die
  erste echte Rezension eintraegt.

Mit temporaeren Testdaten end-to-end durchgetestet (Herzen-Darstellung,
Rezensions-Section, Filter, Startseiten-Sektion) und vor dem Commit wieder
aus den echten Produktdaten entfernt. Gilt fuer alle 4 Sprachen.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
qcigano
2026-08-03 03:15:00 +02:00
co-authored by Claude Sonnet 5
parent 1add9a42a1
commit b384938b92
18 changed files with 534 additions and 24 deletions
+42
View File
@@ -11,6 +11,18 @@ export type Badge = "neu" | "bestseller" | "sale" | "handgemacht";
type LocalizedText = Record<Locale, string>;
// Eine einzelne Kundenrezension zu einem Produkt — von VanVan im Adminbereich gepflegt (echte
// Kunden-Rückmeldungen, KEINE automatisch erzeugten/erfundenen Bewertungen: Fake-Bewertungen sind
// in Deutschland/der EU seit der Omnibus-Richtlinie ausdrücklich verboten). Deshalb startet jedes
// Produkt ohne Bewertungen — die Anzeige blendet sich überall automatisch ein, sobald die erste
// echte Rezension eingetragen wird, und bleibt sonst dezent (kein "0 Bewertungen"-Ballast).
export interface Bewertung {
name: string;
bewertung: number; // 15 (Herzen statt Sterne, siehe HeartRating.astro)
text?: LocalizedText;
datum?: string; // ISO-Datum, z.B. "2026-08-03"
}
export interface Product {
slug: string;
name: LocalizedText;
@@ -47,6 +59,7 @@ export interface Product {
bis?: string; // ISO-Datum, z.B. "2026-12-31" — Rabatt gilt nur bis einschließlich diesem Tag
mengenrabatt?: { abMenge: number; prozent: number }[];
};
bewertungen?: Bewertung[];
}
const modules = import.meta.glob("/src/content/products/*.json", { eager: true }) as Record<
@@ -73,3 +86,32 @@ export function productsByUnterkategorie(kategorie: string, unterkategorie: stri
export function saleProducts(): Product[] {
return products.filter((p) => p.badges.includes("sale") || p.preisAlt);
}
// Rezensions-Helfer — überall genutzt, wo eine Herzen-Bewertung angezeigt wird (Produktkarte,
// Produktseite, Shop-Filter/Sortierung, Startseiten-Kundenstimmen), damit die Rechenlogik nur an
// einer Stelle steht.
export function durchschnittsbewertung(product: Product): number | null {
const b = product.bewertungen;
if (!b || b.length === 0) return null;
return b.reduce((sum, r) => sum + r.bewertung, 0) / b.length;
}
export function anzahlBewertungen(product: Product): number {
return product.bewertungen?.length ?? 0;
}
// Alle Rezensionen über alle Produkte hinweg, neueste zuerst — Grundlage für die
// Kundenstimmen-Sektion auf der Startseite.
export function alleBewertungen(): (Bewertung & { produktSlug: string; produktName: LocalizedText })[] {
return products
.flatMap((p) => (p.bewertungen ?? []).map((b) => ({ ...b, produktSlug: p.slug, produktName: p.name })))
.sort((a, b) => (b.datum ?? "").localeCompare(a.datum ?? ""));
}
// Gesamt-Durchschnitt über alle Produkte hinweg — für die Kundenstimmen-Sektion auf der
// Startseite (erscheint erst automatisch, sobald mindestens eine echte Bewertung existiert).
export function gesamtDurchschnitt(): number | null {
const alle = alleBewertungen();
if (alle.length === 0) return null;
return alle.reduce((sum, r) => sum + r.bewertung, 0) / alle.length;
}