Files
Wabenhain/src/app/konto/bestellungen/page.tsx
T
Nick SzaunigandClaude Opus 5.5 6ac7cc5d1f feat(ui): Kundenkonto im Wabenstil
- Konto-Kacheln: Icon in einer Wabe, die sich beim Hover mit Honig fuellt,
  einlaufender Pfeil, Serifen-Titel
- Bestell-Statusleiste: Waben statt Punkte, erledigte Abschnitte fliessen
  nacheinander voll, aktueller Schritt pulsiert sanft
- Status-Badge mit pulsierendem Punkt bei laufenden Bestellungen
- Leere Zustaende mit gestrichelter Wabe und Serifen-Titel
- Formularfelder: einheitlicher Honig-Schein beim Fokus
- Groessere Abschnittstitel im Konto, Bestellsummen in Serifen-Preisschrift
- Nur CSS-Animationen (Komponenten rendern auch auf Admin-Serverseiten);
  reduzierte Bewegung schaltet Puls und Fuellen ab

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
2026-09-25 07:53:02 +02:00

177 lines
7.6 KiB
TypeScript
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 type { Metadata } from "next";
import { redirect } from "next/navigation";
import Link from "next/link";
import { Container, SectionHeading, LinkButton } from "@/components/ui";
import { BackLink } from "@/components/back-link";
import { ReorderButton } from "@/components/reorder-button";
import { AccountEmptyState, OrderStatusBadge, OrderStatusTimeline, OrderIcon } from "@/components/account-ui";
import { getCurrentCustomer } from "@/lib/auth";
import { getOrdersForCustomer } from "@/lib/account-data";
import { formatPrice, formatDate } from "@/lib/format";
import { getCarrierTrackingUrl } from "@/lib/shipment-tracking";
import { getLocale } from "@/i18n/locale";
import { getDictionary } from "@/i18n/get-dictionary";
export const metadata: Metadata = { title: "Meine Bestellungen" };
export const dynamic = "force-dynamic";
// Deckt sich mit RETURN_ELIGIBLE_STATUSES in bestellungen/actions.ts.
const RETURN_ELIGIBLE_STATUSES = [
"BEZAHLT",
"IN_VORBEREITUNG",
"VERSANDBEREIT",
"VERSENDET",
"ABHOLBEREIT",
"ABGEHOLT",
];
export default async function KontoBestellungenPage() {
const locale = await getLocale();
const t = getDictionary(locale);
const statusLabels = t.orderStatusBadge;
const returnStatusLabels = t.ordersPage.returnStatusLabels;
const customer = await getCurrentCustomer();
if (!customer) redirect("/konto");
const orders = await getOrdersForCustomer(customer.id);
return (
<div className="py-20">
<Container className="max-w-3xl">
<BackLink href="/konto" label={t.ordersPage.backToAccount} />
<SectionHeading eyebrow={t.account.eyebrow} title={t.ordersPage.title} />
{orders.length === 0 ? (
<AccountEmptyState
icon={<OrderIcon />}
title={t.ordersPage.emptyTitle}
description={t.ordersPage.emptyDescription}
action={<LinkButton href="/shop">{t.cartPage.goToShop}</LinkButton>}
/>
) : (
<div className="mt-10 space-y-6">
{orders.map((order) => (
<div
key={order.id}
className="border border-ink/10 p-6 transition-colors hover:border-ink/25"
>
<div className="flex flex-wrap items-center justify-between gap-2">
<div>
<p className="text-forest">{order.orderNumber}</p>
<p className="text-xs text-ink/65">{formatDate(order.createdAt, locale)}</p>
</div>
<div className="text-right">
<OrderStatusBadge status={order.status} label={statusLabels[order.status] ?? order.status} />
<p className="mt-1.5 text-xs text-ink/65">
{order.fulfillmentMethod === "VERSAND"
? order.shipment?.shippedAt
? t.ordersPage.shippedOn(formatDate(order.shipment.shippedAt, locale))
: t.ordersPage.shippingPending
: order.pickup?.pickedUpAt
? t.ordersPage.pickedUpOn(formatDate(order.pickup.pickedUpAt, locale))
: order.pickup?.readyAt
? t.ordersPage.readySince(formatDate(order.pickup.readyAt, locale))
: t.ordersPage.pickupPending}
</p>
</div>
</div>
<ul className="mt-4 text-sm text-ink/70 space-y-1">
{order.items.map((item) => (
<li key={item.id}>
{item.quantity} ×{" "}
<Link href={`/honig/${item.product.slug}#bewertungen`} className="hover:text-forest hover:underline">
{item.productName}
</Link>
</li>
))}
</ul>
<div className="mt-5 max-w-md">
<OrderStatusTimeline
status={order.status}
fulfillmentMethod={order.fulfillmentMethod}
labels={{
ordered: t.orderStatusTimeline.ordered,
paid: t.orderStatusTimeline.paid,
preparing: t.orderStatusTimeline.preparing,
readyToShip: t.orderStatusTimeline.readyToShip,
shipped: t.orderStatusTimeline.shipped,
readyForPickup: t.orderStatusTimeline.readyForPickup,
pickedUp: t.orderStatusTimeline.pickedUp,
cancelled: t.orderStatusTimeline.cancelled,
ariaLabel: t.orderStatusTimeline.ariaLabel,
}}
/>
</div>
{order.shipment?.carrier && order.shipment.trackingNumber ? (
<p className="mt-3 text-xs text-ink/65">
{t.shipmentInfo.carrierLabel}: {order.shipment.carrier} · {t.shipmentInfo.trackingNumberLabel}:{" "}
{order.shipment.trackingNumber}
{(() => {
const url = getCarrierTrackingUrl(order.shipment.carrier, order.shipment.trackingNumber);
return url ? (
<>
{" "}
·{" "}
<a href={url} target="_blank" rel="noopener noreferrer" className="text-forest underline">
{t.shipmentInfo.trackPackage}
</a>
</>
) : null;
})()}
</p>
) : null}
{order.giftMessage ? (
<p className="mt-3 text-xs text-ink/65 bg-honey/10 border border-honey-dark/20 px-3 py-2">
🎁 {t.ordersPage.giftMessagePrefix} „{order.giftMessage}“
</p>
) : null}
<div className="mt-4 flex flex-wrap items-center justify-between gap-4">
<p className="font-serif text-2xl text-forest tabular-nums">{formatPrice(order.totalCents, order.currency, locale)}</p>
<div className="flex items-center gap-3">
{order.invoice ? (
<Link
href={`/rechnung/${order.invoice.invoiceNumber}`}
className="text-sm text-forest underline"
>
{t.ordersPage.viewInvoice}
</Link>
) : null}
{order.returnRequest ? (
<span className="text-sm text-ink/65">
{returnStatusLabels[order.returnRequest.status] ?? order.returnRequest.status}
</span>
) : RETURN_ELIGIBLE_STATUSES.includes(order.status) ? (
<Link
href={`/konto/bestellungen/${order.id}/retoure`}
className="text-sm text-forest underline"
>
{t.ordersPage.requestReturn}
</Link>
) : null}
<ReorderButton
items={order.items.map((item) => ({
productId: item.productId,
slug: item.product.slug,
name: item.productName,
weightGrams: item.weightGrams,
priceCents: item.unitPriceCents,
quantity: item.quantity,
}))}
/>
</div>
</div>
</div>
))}
</div>
)}
</Container>
</div>
);
}