Wandernder Mehrfarb-Verlaufsrand, sanfter Farbschein im Hintergrund, ein Glanz-Sweep, der regelmäßig durchläuft, ein pulsierender Leucht-Ring um den Avatar und zwei funkelnde ✨-Sternchen — dieselbe Formsprache wie beim "Meine Daten"-Knopf, nur größer und ruhiger für gute Lesbarkeit. Respektiert prefers-reduced-motion. Co-Authored-By: Claude Sonnet 5 <[email protected]>
370 lines
18 KiB
Plaintext
370 lines
18 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 = "de";
|
||
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="Mein Konto" description="Persönlicher Kundenbereich von 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">
|
||
<span class="account-head-sparkle account-head-sparkle-1" aria-hidden="true">✨</span>
|
||
<span class="account-head-sparkle account-head-sparkle-2" aria-hidden="true">✨</span>
|
||
<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>
|
||
|
||
{/* Schnellnavigation: eine zusammenhängende Leiste direkt über "Meine Bestellungen" /
|
||
"Meine Wunschliste" statt drei loser Kästchen — jedes Segment springt zum passenden
|
||
Abschnitt weiter unten, Icon und Akzentfarbe passen zur jeweiligen Überschrift. */}
|
||
<nav class="account-nav" aria-label={t.accountDash.navLabel}>
|
||
<button type="button" class="account-nav-item account-nav-orders" data-tab="bestellungen">
|
||
<span class="account-nav-icon" aria-hidden="true">📦</span>
|
||
<span class="account-nav-value">{beispielBestellungen.length}</span>
|
||
<span class="account-nav-label">{t.accountDash.statOrders}</span>
|
||
</button>
|
||
<button type="button" class="account-nav-item account-nav-open" data-tab="offene">
|
||
<span class="account-nav-icon" aria-hidden="true">🚚</span>
|
||
<span class="account-nav-value">{offeneSendungen}</span>
|
||
<span class="account-nav-label">{t.accountDash.statOpen}</span>
|
||
</button>
|
||
<button type="button" class="account-nav-item account-nav-wishlist" data-tab="wunschliste">
|
||
<span class="account-nav-icon" aria-hidden="true">🩵</span>
|
||
<span class="account-nav-value">0</span>
|
||
<span class="account-nav-label">{t.accountDash.statWishlist}</span>
|
||
</button>
|
||
<button type="button" class="account-nav-item account-nav-data" data-tab="daten">
|
||
<span class="account-nav-sparkle" aria-hidden="true">✨</span>
|
||
<span class="account-nav-icon" aria-hidden="true">👤</span>
|
||
<span class="account-nav-label">{t.accountDash.statData}</span>
|
||
</button>
|
||
</nav>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="section-tight" id="bestellungen">
|
||
<div class="container">
|
||
<h2>{t.accountDash.ordersTitle}</h2>
|
||
<p class="small" id="orders-empty-note" style="display:none;" data-empty-open={t.accountDash.noOpenOrders} data-empty-arrived={t.accountDash.noArrivedOrders}></p>
|
||
<div class="order-list" id="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={`/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="/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" id="wunschliste">
|
||
<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" id="zuletzt-angesehen">
|
||
<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" id="meine-daten">
|
||
<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>
|
||
|
||
{/* Optionaler Spitzname zuerst (Bonus, wird bevorzugt angezeigt), darunter der feste
|
||
Pflicht-Name (z.B. für Bestellungen/Lieferadresse). */}
|
||
<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>
|
||
{/* Name, E-Mail und Lieferadresse sind fest (wie bei einer echten Bestellung) — nur
|
||
der Spitzname oben ist änderbar. */}
|
||
<tr><th>{t.accountDash.profileName}</th><td id="profile-name-display">–</td></tr>
|
||
<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: "/konto/", fallbackName: "Kundin", greetingTemplate: t.accountDash.greeting("{name}"), reviewedBadgeText: t.accountDash.reviewedBadge }}>
|
||
window.__accountDashVars = { loginPath, fallbackName, greetingTemplate, reviewedBadgeText };
|
||
</script>
|
||
<script>
|
||
// Schnellnavigation als echte Filter-Tabs: Klick zeigt NUR den passenden Bereich, blendet den
|
||
// Rest aus (statt nur hinzuspringen). "Bestellungen" zeigt nur schon ANGEKOMMENE (abgeschlossene)
|
||
// Bestellungen, "Offene Sendungen" nur die noch nicht angekommenen — echte Trennung statt
|
||
// Überschneidung.
|
||
const OFFENE_STATUS = ["bezahlt", "bearbeitung", "versandVorbereitet", "versendet"];
|
||
const navItems = Array.from(document.querySelectorAll(".account-nav-item")) as HTMLButtonElement[];
|
||
const bestellungenSection = document.getElementById("bestellungen");
|
||
const wunschlisteSection = document.getElementById("wunschliste");
|
||
const recentSection = document.getElementById("zuletzt-angesehen");
|
||
const datenSection = document.getElementById("meine-daten");
|
||
const orderCards = Array.from(document.querySelectorAll(".order-card"));
|
||
const emptyNote = document.getElementById("orders-empty-note") as HTMLElement | null;
|
||
|
||
// Beim Laden ist noch KEIN Knopf gedrückt — solange nichts angeklickt wurde, steht direkt
|
||
// "Zuletzt angesehen" da, alle anderen (per Tab umschaltbaren) Bereiche bleiben versteckt.
|
||
function setActiveTab(tab: string | null) {
|
||
navItems.forEach((item) => item.classList.toggle("is-active", item.dataset.tab === tab));
|
||
|
||
if (bestellungenSection) bestellungenSection.style.display = "none";
|
||
if (wunschlisteSection) wunschlisteSection.style.display = "none";
|
||
if (datenSection) datenSection.style.display = "none";
|
||
if (recentSection) recentSection.style.display = "none";
|
||
|
||
if (tab === null) {
|
||
if (recentSection) recentSection.style.display = "";
|
||
return;
|
||
}
|
||
|
||
if (tab === "wunschliste") {
|
||
if (wunschlisteSection) wunschlisteSection.style.display = "";
|
||
return;
|
||
}
|
||
|
||
if (tab === "daten") {
|
||
if (datenSection) datenSection.style.display = "";
|
||
return;
|
||
}
|
||
|
||
if (bestellungenSection) bestellungenSection.style.display = "";
|
||
let visibleCount = 0;
|
||
orderCards.forEach((card) => {
|
||
const status = OFFENE_STATUS.find((s) => card.classList.contains("order-card-" + s));
|
||
const show = tab === "offene" ? !!status : card.classList.contains("order-card-abgeschlossen");
|
||
(card as HTMLElement).style.display = show ? "" : "none";
|
||
if (show) visibleCount++;
|
||
});
|
||
if (emptyNote) {
|
||
if (visibleCount === 0 && (tab === "offene" || tab === "bestellungen")) {
|
||
emptyNote.textContent = tab === "offene" ? emptyNote.dataset.emptyOpen ?? "" : emptyNote.dataset.emptyArrived ?? "";
|
||
emptyNote.style.display = "block";
|
||
} else {
|
||
emptyNote.style.display = "none";
|
||
}
|
||
}
|
||
}
|
||
|
||
navItems.forEach((item) => {
|
||
item.addEventListener("click", () => { if (item.dataset.tab) setActiveTab(item.dataset.tab); });
|
||
});
|
||
|
||
setActiveTab(null);
|
||
</script>
|
||
<script>
|
||
import { getAccount, isLoggedIn, logout, 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 nameDisplay = document.getElementById("profile-name-display");
|
||
if (nameDisplay) nameDisplay.textContent = 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);
|
||
|
||
// 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>
|