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:
qcigano
2026-08-03 04:17:04 +02:00
co-authored by Claude Sonnet 5
parent d27a858d8f
commit 6f10e41386
28 changed files with 2338 additions and 334 deletions
+78 -6
View File
@@ -78,12 +78,30 @@ const p = (href: string) => `${localePrefix(lang)}${href}`;
<span class="install-label">{t.common.installAppShort}</span>
</button>
<LanguageSwitcher lang={lang} path={path} legalOnly={legalOnly} />
<a href={p("/konto/")} class="account-btn">
<span class="account-icon" aria-hidden="true">
<svg viewBox="0 0 24 24" width="16" height="16" fill="currentColor"><path d="M12 12a5 5 0 1 0-5-5 5 5 0 0 0 5 5Zm0 2c-4.4 0-9 2.2-9 5v2h18v-2c0-2.8-4.6-5-9-5Z"/></svg>
</span>
<span class="account-label">{t.nav.login}</span>
</a>
{/* Zwei Zustände nebeneinander im HTML, per JS umgeschaltet (siehe Script unten) — ohne
eingebautes Login (Phase 1: nur clientseitig im localStorage, siehe scripts/account.ts)
weiß der Server beim Rendern nie, ob jemand "angemeldet" ist. Standardmäßig ist nur der
Anmelden-Link sichtbar; sobald JS geladen hat und ein lokales Konto findet, blendet es
auf das Konto-Menü um. */}
<div class="account-area" id="account-area">
<a href={p("/konto/")} class="account-btn" id="account-login-btn">
<span class="account-icon" aria-hidden="true">
<svg viewBox="0 0 24 24" width="16" height="16" fill="currentColor"><path d="M12 12a5 5 0 1 0-5-5 5 5 0 0 0 5 5Zm0 2c-4.4 0-9 2.2-9 5v2h18v-2c0-2.8-4.6-5-9-5Z"/></svg>
</span>
<span class="account-label">{t.nav.login}</span>
</a>
<div class="account-menu" id="account-menu" style="display:none;">
<button type="button" class="account-menu-btn" id="account-menu-btn" aria-haspopup="true" aria-expanded="false">
<span class="account-avatar" id="account-avatar" aria-hidden="true"></span>
<span class="account-username" id="account-username-label"></span>
<span class="chevron" aria-hidden="true">▼</span>
</button>
<div class="account-dropdown" id="account-dropdown" role="menu">
<a href={p("/konto/angemeldet/")} role="menuitem">👤 {t.accountMenu.myAccount}</a>
<button type="button" id="account-logout-btn" role="menuitem">🚪 {t.accountMenu.logout}</button>
</div>
</div>
</div>
<a href={p("/warenkorb/")} class="icon-link cart-link" aria-label={t.nav.account}>
🧺 <span class="cart-count" id="cart-count">0</span>
</a>
@@ -158,6 +176,8 @@ const p = (href: string) => `${localePrefix(lang)}${href}`;
<script>
import { cartCount } from "../scripts/cart";
import { getAccount, logout } from "../scripts/account";
function updateCount() {
const el = document.getElementById("cart-count");
if (el) el.textContent = String(cartCount());
@@ -166,6 +186,58 @@ const p = (href: string) => `${localePrefix(lang)}${href}`;
window.addEventListener("cart:changed", updateCount);
window.addEventListener("storage", updateCount);
// Konto-Bereich der Kopfzeile: "Anmelden"-Link vs. Konto-Menü, je nachdem ob auf diesem
// Gerät gerade ein (Demo-)Konto angemeldet ist (siehe scripts/account.ts). Läuft auf JEDER
// Seite, weil Layout.astro von allen Seiten eingebunden wird — daher überall konsistent.
const loginBtn = document.getElementById("account-login-btn");
const menu = document.getElementById("account-menu");
const menuBtn = document.getElementById("account-menu-btn");
const dropdown = document.getElementById("account-dropdown");
const avatar = document.getElementById("account-avatar");
const usernameLabel = document.getElementById("account-username-label");
const logoutBtn = document.getElementById("account-logout-btn");
function renderAccountArea() {
const a = getAccount();
if (loginBtn) loginBtn.style.display = a.loggedIn ? "none" : "";
if (menu) menu.style.display = a.loggedIn ? "flex" : "none";
if (!a.loggedIn) return;
if (usernameLabel) usernameLabel.textContent = a.username || "Konto";
if (avatar) {
if (a.photo) {
avatar.style.backgroundImage = `url(${a.photo})`;
avatar.textContent = "";
} else {
avatar.style.backgroundImage = "";
avatar.textContent = (a.username || "?").trim().charAt(0).toUpperCase();
}
}
}
renderAccountArea();
window.addEventListener("account:changed", renderAccountArea);
window.addEventListener("storage", renderAccountArea);
menuBtn?.addEventListener("click", (e) => {
e.stopPropagation();
const open = menu?.classList.toggle("open");
menuBtn.setAttribute("aria-expanded", open ? "true" : "false");
});
document.addEventListener("click", () => {
menu?.classList.remove("open");
menuBtn?.setAttribute("aria-expanded", "false");
});
document.addEventListener("keydown", (e) => {
if (e.key === "Escape") {
menu?.classList.remove("open");
menuBtn?.setAttribute("aria-expanded", "false");
}
});
logoutBtn?.addEventListener("click", () => {
logout();
menu?.classList.remove("open");
menuBtn?.setAttribute("aria-expanded", "false");
});
const hamburger = document.getElementById("hamburger");
const mobileNav = document.getElementById("mobile-nav");
hamburger?.addEventListener("click", () => {