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:
co-authored by
Claude Sonnet 5
parent
1add9a42a1
commit
b384938b92
@@ -0,0 +1,66 @@
|
||||
---
|
||||
// Herzen statt Sterne für Kundenbewertungen — passend zum Marken-Herz 🩵 und dem
|
||||
// Lila-Babyblau-Verlauf, der schon überall auf der Seite als Signature-Look verwendet wird
|
||||
// (Verlaufsränder der Karten, Sprach-Umschalter, Story-Boxen). Volle Herzen: Verlaufsfüllung,
|
||||
// halbe Herzen: Verlaufsfüllung bei halber Deckkraft, leere Herzen: nur Umriss.
|
||||
interface Props {
|
||||
bewertung: number; // 0–5, Nachkommastellen erlaubt (z.B. 4.5)
|
||||
size?: number;
|
||||
showValue?: boolean;
|
||||
anzahl?: number;
|
||||
anzahlLabel?: string; // z.B. "(12)" oder "12 Bewertungen" — fertig formatiert übergeben
|
||||
}
|
||||
|
||||
const { bewertung, size = 16, showValue = false, anzahl, anzahlLabel } = Astro.props;
|
||||
const gradId = `hr-${Math.random().toString(36).slice(2, 9)}`;
|
||||
|
||||
const herzen = Array.from({ length: 5 }, (_, i) => {
|
||||
const diff = bewertung - i;
|
||||
if (diff >= 0.75) return "full";
|
||||
if (diff >= 0.25) return "half";
|
||||
return "empty";
|
||||
});
|
||||
|
||||
const HERZ_PFAD =
|
||||
"M12 21s-7.5-4.9-10.2-9.3C.2 8.7 1.4 5 5 4.1c2.1-.5 4 1.4 5 2.9.6-.9 1.3-1.7 2.2-2.2 1.9-1.1 4.4-.7 5.8 1 1.9 2.3 1.5 5.6-.6 8.6C14.9 17.5 12 21 12 21z";
|
||||
---
|
||||
|
||||
<span class="heart-rating" role="img" aria-label={`${bewertung.toFixed(1)} von 5 Herzen${anzahlLabel ? `, ${anzahlLabel}` : ""}`}>
|
||||
<svg width="0" height="0" style="position:absolute;" aria-hidden="true">
|
||||
<defs>
|
||||
<linearGradient id={gradId} x1="0" y1="0" x2="1" y2="1">
|
||||
<stop offset="0%" stop-color="var(--c-purple-glow)"></stop>
|
||||
<stop offset="100%" stop-color="var(--c-accent)"></stop>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
<span class="heart-icons" aria-hidden="true">
|
||||
{herzen.map((zustand) => (
|
||||
<svg
|
||||
class={`heart-icon heart-${zustand}`}
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d={HERZ_PFAD}
|
||||
fill={zustand === "empty" ? "none" : `url(#${gradId})`}
|
||||
fill-opacity={zustand === "half" ? "0.5" : "1"}
|
||||
stroke={zustand === "empty" ? "var(--c-text-muted)" : `url(#${gradId})`}
|
||||
stroke-width="1.4"
|
||||
stroke-linejoin="round"
|
||||
></path>
|
||||
</svg>
|
||||
))}
|
||||
</span>
|
||||
{showValue && <span class="heart-value">{bewertung.toFixed(1)}</span>}
|
||||
{anzahlLabel && <span class="heart-count small">{anzahlLabel}</span>}
|
||||
</span>
|
||||
|
||||
<style>
|
||||
.heart-rating { display: inline-flex; align-items: center; gap: 0.4em; }
|
||||
.heart-icons { display: inline-flex; gap: 0.15em; }
|
||||
.heart-icon { display: block; flex-shrink: 0; }
|
||||
.heart-value { font-weight: 700; color: var(--c-text); font-size: 0.92em; }
|
||||
.heart-count { color: var(--c-text-muted); }
|
||||
</style>
|
||||
@@ -5,6 +5,8 @@ import { localePrefix } from "../i18n/config";
|
||||
import { useTranslations } from "../i18n/ui";
|
||||
import { formatPrice } from "../i18n/format";
|
||||
import { effektiverPreis, rabattLabel, hatAktivenRabatt } from "../data/rabatt";
|
||||
import { durchschnittsbewertung, anzahlBewertungen } from "../data/products";
|
||||
import HeartRating from "./HeartRating.astro";
|
||||
|
||||
interface Props { product: Product; lang: Locale }
|
||||
const { product, lang } = Astro.props;
|
||||
@@ -12,6 +14,8 @@ const t = useTranslations(lang);
|
||||
const finalPrice = effektiverPreis(product);
|
||||
const discountBadge = rabattLabel(product);
|
||||
const showOldAsPreis = !product.preisAlt && hatAktivenRabatt(product);
|
||||
const bewertungSchnitt = durchschnittsbewertung(product);
|
||||
const bewertungAnzahl = anzahlBewertungen(product);
|
||||
|
||||
const badgeLabel: Record<string, string> = {
|
||||
neu: t.badge.neu,
|
||||
@@ -55,6 +59,9 @@ const desc = product.beschreibung[lang];
|
||||
</div>
|
||||
</div>
|
||||
<h3>{name}</h3>
|
||||
{bewertungSchnitt !== null && (
|
||||
<HeartRating bewertung={bewertungSchnitt} size={13} anzahlLabel={`(${bewertungAnzahl})`} />
|
||||
)}
|
||||
<p class="small">{desc.slice(0, 78)}{desc.length > 78 ? "…" : ""}</p>
|
||||
<div class="price-row">
|
||||
{product.preisAlt && <span class="price-old">{formatPrice(product.preisAlt, lang)}</span>}
|
||||
|
||||
Reference in New Issue
Block a user