Bisher unterschied sich der Status nur durch eine kleine, ähnlich gefärbte Pille — auf einen Blick kaum zu erkennen. Jetzt bekommt jede Bestellkarte einen eigenen farbigen Rand plus dezenten Farbschein passend zum Status (Blau = versendet/unterwegs, Grün = abgeschlossen/angekommen, Gold = in Bearbeitung/Versand wird vorbereitet, Violett = bezahlt), zusätzlich ein Icon direkt in der Status-Pille (🚚 unterwegs, ✅ angekommen, usw.) — genau wie schon bei den Zahlungskarten im Checkout. Co-Authored-By: Claude Sonnet 5 <[email protected]>
312 lines
14 KiB
Plaintext
312 lines
14 KiB
Plaintext
---
|
||
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 order-card-${b.status}`}>
|
||
<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>
|
||
|
||
{/* Fester Pflicht-Name (z.B. für Bestellungen/Lieferadresse) — getrennt vom optionalen
|
||
Benutzernamen weiter unten, der nur ein Spitzname-Bonus ist. */}
|
||
<div class="profile-field">
|
||
<label for="profile-name">{t.accountDash.profileName}</label>
|
||
<div class="profile-field-row">
|
||
<input type="text" id="profile-name" maxlength="60" />
|
||
<button type="button" class="btn btn-primary btn-sm" id="profile-name-save">{t.accountDash.nameSave}</button>
|
||
</div>
|
||
<span class="small" id="profile-name-status" style="display:none; color: var(--c-accent);">{t.accountDash.nameSaved}</span>
|
||
</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>
|
||
<span class="small" style="display:block; color: var(--c-text-muted); margin-top:0.3em;">{t.accountDash.usernameHint}</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, setName, setUsername, setPhoto, hatBestellungBewertet, anzeigeName } 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}", anzeigeName(a) || fallbackName);
|
||
const emailEl = document.getElementById("profile-email");
|
||
if (emailEl) emailEl.textContent = a.email || "–";
|
||
const nameInput = document.getElementById("profile-name") as HTMLInputElement | null;
|
||
if (nameInput && document.activeElement !== nameInput) nameInput.value = a.name;
|
||
const usernameInput = document.getElementById("profile-username") as HTMLInputElement | null;
|
||
if (usernameInput && document.activeElement !== usernameInput) usernameInput.value = a.username;
|
||
|
||
const { bg, text } = avatarHtml(anzeigeName(a), 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);
|
||
|
||
// Echter Name ändern (fest, Pflichtfeld)
|
||
const nameSaveBtn = document.getElementById("profile-name-save");
|
||
const nameInputEl = document.getElementById("profile-name") as HTMLInputElement | null;
|
||
const nameStatus = document.getElementById("profile-name-status");
|
||
nameSaveBtn?.addEventListener("click", () => {
|
||
if (!nameInputEl) return;
|
||
setName(nameInputEl.value);
|
||
if (nameStatus) {
|
||
nameStatus.style.display = "inline";
|
||
setTimeout(() => { nameStatus.style.display = "none"; }, 1800);
|
||
}
|
||
});
|
||
|
||
// 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>
|