Kundenrezensionen jetzt vollständig durch Kund:innen selbst, nicht mehr über Admin
Bisher musste VanVan Bewertungen manuell im CMS eintragen. Jetzt können angemeldete Kund:innen Rezensionen direkt auf der Produktseite abgeben (inkl. Herzen-Sternebewertung und Text). Die Durchschnittsbewertung wird automatisch berechnet und überall live eingebunden: Produktkarten (shop-weit), Produktdetailseite, Shop-Filter/Sortierung nach Bewertung und die Kundenstimmen-Sektion auf der Startseite. Neu dazu: ein Kontobereich fürs Login (Demo-Login per E-Mail), Profilfoto- Upload, wählbarer Benutzername, und ein Header-Kontomenü, das nur bei Anmeldung erscheint. Jede Bestellung im Konto-Dashboard hat einen "Rezension schreiben"-Link, der direkt zur Produktseite führt und dort per Bestellnummer verknüpft wird; im Profil ist dann sofort sichtbar, welche Bestellungen schon bewertet wurden. Technisch als reines Frontend/localStorage-System umgesetzt (kein Backend in dieser Phase), analog zum bestehenden Warenkorb-Muster. Admin-CMS-Feld für manuelle Bewertungen entfernt, da Rezensionen jetzt ausschließlich von Kund:innen kommen (auch aus rechtlichen Gründen: keine Fake-Bewertungen). Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
d27a858d8f
commit
6f10e41386
@@ -26,7 +26,7 @@ const badgeLabel: Record<string, string> = {
|
||||
const name = product.name[lang];
|
||||
const desc = product.beschreibung[lang];
|
||||
---
|
||||
<a href={`${localePrefix(lang)}/produkt/${product.slug}/`} class="card product-card">
|
||||
<a href={`${localePrefix(lang)}/produkt/${product.slug}/`} class="card product-card" data-slug={product.slug}>
|
||||
<div class="product-media" data-carousel>
|
||||
{product.bilder && product.bilder.length > 0 ? (
|
||||
<>
|
||||
@@ -59,9 +59,23 @@ const desc = product.beschreibung[lang];
|
||||
</div>
|
||||
</div>
|
||||
<h3>{name}</h3>
|
||||
{bewertungSchnitt !== null && (
|
||||
<HeartRating bewertung={bewertungSchnitt} size={13} anzahlLabel={`(${bewertungAnzahl})`} />
|
||||
)}
|
||||
{/* Immer gerendert (auch ohne Server-Bewertungen), aber standardmäßig versteckt — ein
|
||||
gemeinsames Skript (siehe unten) blendet die Zeile automatisch ein, sobald auf diesem
|
||||
Gerät lokal gespeicherte Kundenrezensionen für dieses Produkt dazukommen (siehe
|
||||
scripts/reviews.ts), egal auf welcher Seite die Karte gerade angezeigt wird. */}
|
||||
<span
|
||||
class="card-rating"
|
||||
data-rating-slot
|
||||
data-basis-schnitt={bewertungSchnitt ?? 0}
|
||||
data-basis-anzahl={bewertungAnzahl}
|
||||
data-count-one={t.product.reviewCount(1)}
|
||||
data-count-other={t.product.reviewCount(2)}
|
||||
style={bewertungSchnitt === null ? "display:none;" : undefined}
|
||||
>
|
||||
{bewertungSchnitt !== null && (
|
||||
<HeartRating bewertung={bewertungSchnitt} size={13} anzahlLabel={`(${bewertungAnzahl})`} />
|
||||
)}
|
||||
</span>
|
||||
<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>}
|
||||
@@ -146,6 +160,39 @@ const desc = product.beschreibung[lang];
|
||||
}, true);
|
||||
</script>
|
||||
|
||||
<script>
|
||||
// Bewertungszeile auf JEDER Produktkarte der Seite (Shop-Grid, Kategorie-Seiten, Startseite,
|
||||
// "Das könnte dir auch gefallen" ...) live mit den lokal auf diesem Gerät gespeicherten
|
||||
// Kundenrezensionen zusammenführen (siehe scripts/reviews.ts) — Astro rendert die Karte selbst
|
||||
// nur mit dem Bau-Zeit-Stand (VanVans Admin-Daten), neue Rezensionen von Kund:innen kommen erst
|
||||
// im Browser dazu. Ein einziges, delegiertes Skript für alle Karten (Astro dedupliziert dieses
|
||||
// <script> automatisch, egal wie oft ProductCard auf der Seite eingebunden wird).
|
||||
import { heartsHtml } from "../scripts/hearts";
|
||||
import { lokaleBewertungenFuer } from "../scripts/reviews";
|
||||
|
||||
function reviewCountLabel(slot: HTMLElement, n: number): string {
|
||||
if (n === 1) return slot.dataset.countOne!;
|
||||
return slot.dataset.countOther!.replace(/^\d+/, String(n));
|
||||
}
|
||||
|
||||
document.querySelectorAll<HTMLElement>("[data-rating-slot]").forEach((slot) => {
|
||||
const card = slot.closest<HTMLElement>(".product-card");
|
||||
const slug = card?.dataset.slug;
|
||||
if (!slug) return;
|
||||
const lokale = lokaleBewertungenFuer(slug);
|
||||
if (lokale.length === 0) return; // Bau-Zeit-Stand reicht bereits, nichts zu ergänzen
|
||||
|
||||
const basisSchnitt = Number(slot.dataset.basisSchnitt || 0);
|
||||
const basisAnzahl = Number(slot.dataset.basisAnzahl || 0);
|
||||
const summe = basisSchnitt * basisAnzahl + lokale.reduce((s, r) => s + r.bewertung, 0);
|
||||
const anzahl = basisAnzahl + lokale.length;
|
||||
const schnitt = summe / anzahl;
|
||||
|
||||
slot.innerHTML = heartsHtml(schnitt, 13, { anzahlLabel: `(${anzahl})` });
|
||||
slot.style.display = "";
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.product-card { display: flex; flex-direction: column; gap: 0.6rem; padding: 1rem; }
|
||||
.product-media { position: relative; isolation: isolate; touch-action: pan-y; }
|
||||
|
||||
Reference in New Issue
Block a user