Files
vans-diy-bastelbedarf/src/pages/en/konto/angemeldet.astro
T
qciganoandClaude Sonnet 5 6f10e41386 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]>
2026-08-03 04:17:04 +02:00

285 lines
13 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
import Layout from "../../../layouts/Layout.astro";
import ProductCard from "../../../components/ProductCard.astro";
import { products, getProduct } from "../../../data/products";
import type { Locale } from "../../../i18n/config";
import { useTranslations } from "../../../i18n/ui";
import { formatPrice } from "../../../i18n/format";
const lang: Locale = "en";
const t = useTranslations(lang);
// Bestellungen sind weiterhin Beispieldaten (siehe previewNote) — ECHT sind ab jetzt aber
// Anmeldung, Profil (Benutzername/Foto) und Rezensionen, gespeichert im localStorage dieses
// Geräts (siehe scripts/account.ts, scripts/reviews.ts). Jede Bestellposition referenziert
// einen echten Produkt-Slug — der Name kommt live aus den Produktdaten (mehrsprachig, immer
// konsistent mit dem Shop), damit "Rezension schreiben" wirklich zur passenden Seite führt.
const beispielBestellungenRoh = [
{
nummer: "VDB-2026-0184",
datum: "28.07.2026",
status: "versendet" as const,
tracking: "00340434161094015791",
artikel: [
{ slug: "hasidog-plushie-blaueroverall", menge: 1 },
{ slug: "perlen-armband-hellblau", menge: 1 },
],
},
{
nummer: "VDB-2026-0157",
datum: "12.07.2026",
status: "abgeschlossen" as const,
tracking: null,
artikel: [{ slug: "haekelnadel-set-ergonomisch", menge: 1 }],
},
];
const beispielBestellungen = beispielBestellungenRoh.map((b) => {
const artikel = b.artikel.map((a) => {
const p = getProduct(a.slug);
return { ...a, name: p?.name[lang] ?? a.slug, preis: p?.preis ?? 0 };
});
return { ...b, artikel, summe: artikel.reduce((s, a) => s + a.preis * a.menge, 0) };
});
const statusLabel = {
bezahlt: t.accountDash.statusBezahlt,
bearbeitung: t.accountDash.statusBearbeitung,
versandVorbereitet: t.accountDash.statusVersandVorbereitet,
versendet: t.accountDash.statusVersendet,
abgeschlossen: t.accountDash.statusAbgeschlossen,
} as const;
// "Zuletzt angesehen" zeigt in dieser Vorschau einfach die ersten echten Produkte aus dem Shop —
// in Phase 2 kommt hier die tatsächliche Verlaufs-Historie der angemeldeten Person hin.
const zuletztAngesehen = products.slice(0, 3);
const offeneSendungen = beispielBestellungen.filter((b) => b.status === "versendet").length;
---
<Layout title="My Account" description="Personal customer area at Van's DIY & Bastelbedarf." lang={lang} path="/konto/angemeldet/">
<section class="section-tight">
<div class="container">
<p class="todo-note" id="not-logged-in-note" style="display:none;">{t.accountDash.notLoggedIn}</p>
<p class="todo-note" id="preview-note">{t.accountDash.previewNote}</p>
</div>
</section>
<section class="section-tight">
<div class="container">
<div class="account-head card">
<div class="account-head-identity">
<span class="account-avatar-lg" id="dash-avatar" aria-hidden="true"></span>
<div>
<span class="eyebrow">{t.accountDash.eyebrow}</span>
<h1 id="dash-greeting">{t.accountDash.greeting("…")}</h1>
<p class="lead">{t.accountDash.lead}</p>
</div>
</div>
<button type="button" class="btn btn-outline btn-sm" id="dash-logout-btn">{t.accountDash.logout}</button>
</div>
<div class="account-stats">
<div class="card account-stat">
<span class="account-stat-value">{beispielBestellungen.length}</span>
<span class="small">{t.accountDash.statOrders}</span>
</div>
<div class="card account-stat">
<span class="account-stat-value">0</span>
<span class="small">{t.accountDash.statWishlist}</span>
</div>
<div class="card account-stat">
<span class="account-stat-value">{offeneSendungen}</span>
<span class="small">{t.accountDash.statOpen}</span>
</div>
</div>
</div>
</section>
<section class="section-tight">
<div class="container">
<h2>{t.accountDash.ordersTitle}</h2>
<div class="order-list">
{beispielBestellungen.map((b) => (
<div class="card order-card">
<div class="order-card-head">
<div>
<span class="order-no">{t.accountDash.orderNo} {b.nummer}</span>
<span class="small order-meta">{t.accountDash.orderDate} {b.datum}</span>
</div>
<span class={`badge order-status order-status-${b.status}`}>{statusLabel[b.status]}</span>
</div>
<ul class="order-items">
{b.artikel.map((a) => (
<li>
<span>{a.menge}× {a.name}</span>
<span class="order-item-right">
<span class="order-item-price">{formatPrice(a.preis * a.menge, lang)}</span>
{/* Zustand (Button vs. "✓ Bewertet") hängt vom localStorage ab — wird per JS
beim Laden gesetzt (siehe Script unten), Grundzustand ist der Button. */}
<span class="order-item-review" data-order-review data-bestellnummer={b.nummer} data-slug={a.slug}>
<a class="small review-link" href={`/en/produkt/${a.slug}/?bewerten=${b.nummer}`}>{t.accountDash.reviewBtn}</a>
</span>
</span>
</li>
))}
</ul>
<div class="order-card-foot">
<span class="order-total">{t.accountDash.orderTotal}: <strong>{formatPrice(b.summe, lang)}</strong></span>
<div class="order-actions">
{b.tracking && <a class="btn btn-outline btn-sm" href={`https://www.dhl.de/de/privatkunden/dhl-sendungsverfolgung.html?piececode=${b.tracking}`} target="_blank" rel="noopener">{t.accountDash.trackingBtn}</a>}
<a class="btn btn-outline btn-sm" href="#">{t.accountDash.invoiceBtn}</a>
<a class="btn btn-outline btn-sm" href="/en/shop/">{t.accountDash.reorderBtn}</a>
</div>
</div>
{b.tracking && (
<p class="small order-tracking-no">{t.accountDash.trackingLabel}: <code>{b.tracking}</code></p>
)}
</div>
))}
</div>
</div>
</section>
<section class="section-tight">
<div class="container">
<h2>{t.accountDash.wishlistTitle}</h2>
<div class="card">
<p class="small" style="margin:0;">{t.accountDash.wishlistEmpty}</p>
</div>
</div>
</section>
{zuletztAngesehen.length > 0 && (
<section class="section-tight">
<div class="container">
<h2>{t.accountDash.recentTitle}</h2>
<div class="grid grid-3">{zuletztAngesehen.map((p) => <ProductCard product={p} lang={lang} />)}</div>
</div>
</section>
)}
<section class="section-tight">
<div class="container">
<h2>{t.accountDash.profileTitle}</h2>
<div class="card profile-card">
<div class="profile-photo-row">
<span class="account-avatar-lg" id="profile-avatar" aria-hidden="true"></span>
<div>
<span class="small" style="display:block; margin-bottom:0.4rem;">{t.accountDash.photoLabel}</span>
<label class="btn btn-outline btn-sm profile-photo-btn">
{t.accountDash.photoChange}
<input type="file" accept="image/*" id="profile-photo-input" style="display:none;" />
</label>
<button type="button" class="btn btn-outline btn-sm" id="profile-photo-remove">{t.accountDash.photoRemove}</button>
</div>
</div>
<div class="profile-field">
<label for="profile-username">{t.accountDash.usernameLabel}</label>
<div class="profile-field-row">
<input type="text" id="profile-username" maxlength="40" />
<button type="button" class="btn btn-primary btn-sm" id="profile-username-save">{t.accountDash.usernameSave}</button>
</div>
<span class="small" id="profile-username-status" style="display:none; color: var(--c-accent);">{t.accountDash.usernameSaved}</span>
</div>
<table class="specs">
<tbody>
<tr><th>{t.accountDash.profileEmail}</th><td id="profile-email"></td></tr>
<tr><th>{t.accountDash.profileAddress}</th><td>Musterstraße 1<br />10115 Berlin<br />Deutschland</td></tr>
</tbody>
</table>
</div>
</div>
</section>
</Layout>
<script define:vars={{ loginPath: "/en/konto/", fallbackName: "Customer", greetingTemplate: t.accountDash.greeting("{name}"), reviewedBadgeText: t.accountDash.reviewedBadge }}>
window.__accountDashVars = { loginPath, fallbackName, greetingTemplate, reviewedBadgeText };
</script>
<script>
import { getAccount, isLoggedIn, logout, setUsername, setPhoto, hatBestellungBewertet } from "../../../scripts/account";
const { loginPath, fallbackName, greetingTemplate, reviewedBadgeText } = (window as any).__accountDashVars;
// Zugriffsschutz: diese Seite ist nur für angemeldete Kund:innen gedacht. Es gibt noch kein
// echtes Backend, das den Zugriff serverseitig verweigern könnte (Phase 2) — deshalb hier per
// JS geprüft und bei fehlender Anmeldung sofort zur Login-Seite weitergeleitet.
if (!isLoggedIn()) {
const note = document.getElementById("not-logged-in-note");
const preview = document.getElementById("preview-note");
if (note) note.style.display = "block";
if (preview) preview.style.display = "none";
setTimeout(() => { window.location.href = loginPath; }, 900);
} else {
function avatarHtml(username: string, photo: string): { bg: string; text: string } {
if (photo) return { bg: `url(${photo})`, text: "" };
return { bg: "", text: (username || "?").trim().charAt(0).toUpperCase() };
}
function renderProfil() {
const a = getAccount();
const greetingEl = document.getElementById("dash-greeting");
if (greetingEl) greetingEl.textContent = greetingTemplate.replace("{name}", a.username || fallbackName);
const emailEl = document.getElementById("profile-email");
if (emailEl) emailEl.textContent = a.email || "";
const usernameInput = document.getElementById("profile-username") as HTMLInputElement | null;
if (usernameInput && document.activeElement !== usernameInput) usernameInput.value = a.username;
const { bg, text } = avatarHtml(a.username, a.photo);
[document.getElementById("dash-avatar"), document.getElementById("profile-avatar")].forEach((el) => {
if (!el) return;
el.style.backgroundImage = bg;
el.textContent = text;
});
}
renderProfil();
window.addEventListener("account:changed", renderProfil);
// Benutzername ändern
const saveBtn = document.getElementById("profile-username-save");
const usernameInput = document.getElementById("profile-username") as HTMLInputElement | null;
const savedStatus = document.getElementById("profile-username-status");
saveBtn?.addEventListener("click", () => {
if (!usernameInput?.value.trim()) return;
setUsername(usernameInput.value);
if (savedStatus) {
savedStatus.style.display = "inline";
setTimeout(() => { savedStatus.style.display = "none"; }, 2000);
}
});
// Profilfoto hochladen (wird als data:-URL im localStorage abgelegt — reicht für diese
// Demo völlig aus, in Phase 2 würde ein echter Datei-Upload an den Server dahinterstehen)
const photoInput = document.getElementById("profile-photo-input") as HTMLInputElement | null;
photoInput?.addEventListener("change", () => {
const file = photoInput.files?.[0];
if (!file) return;
const reader = new FileReader();
reader.onload = () => setPhoto(String(reader.result));
reader.readAsDataURL(file);
});
document.getElementById("profile-photo-remove")?.addEventListener("click", () => setPhoto(""));
// Logout direkt aus dem Dashboard heraus
document.getElementById("dash-logout-btn")?.addEventListener("click", () => {
logout();
window.location.href = loginPath;
});
// Pro Bestellposition: Button "Rezension schreiben" durch "✓ Bewertet" ersetzen, sobald für
// genau diese Bestellung+Produkt schon eine Rezension existiert (siehe scripts/reviews.ts,
// wird beim Absenden des Formulars auf der Produktseite markiert).
document.querySelectorAll<HTMLElement>("[data-order-review]").forEach((el) => {
const bestellnummer = el.dataset.bestellnummer!;
const slug = el.dataset.slug!;
if (hatBestellungBewertet(bestellnummer, slug)) {
el.innerHTML = "";
const badge = document.createElement("span");
badge.className = "badge review-done-badge";
badge.textContent = reviewedBadgeText;
el.appendChild(badge);
}
});
}
</script>