feat: Wabenhain Honig-Shop Anwendung

Vollstaendige Next.js/Prisma-E-Commerce-Anwendung fuer eine Imkerei:
Shop, Warenkorb, Stripe-Checkout, Kundenkonto mit 2FA, Admin-Bereich
(Produkte, Ernten, Chargen, Bestellungen, Gutscheine, admin-editierbare
Website-Inhalte), i18n (DE/EN), Rate-Limiting/CSP/Audit-Log-Haertung,
PostgreSQL- und MySQL-Unterstuetzung ueber austauschbare Prisma-
Treiber-Adapter, Unit-/Komponenten-/E2E-Tests.
This commit is contained in:
Nick Szaunig committed 2026-08-17 14:30:28 +02:00
1 parent 5c76fcc266
commit c7927f2883
240 files changed
+26096 -137

No files matched your search

+179
View File
@@ -0,0 +1,179 @@
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(props: PageProps<"/konto/bestellungen">) {
const t = getDictionary(await getLocale());
const statusLabels = t.orderStatusBadge;
const returnStatusLabels = t.ordersPage.returnStatusLabels;
const { error, message } = await props.searchParams;
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} />
{error ? (
<p className="mt-6 text-sm text-red-700 bg-red-50 border border-red-200 px-4 py-3">{error}</p>
) : null}
{message ? (
<p className="mt-6 text-sm text-forest bg-forest/5 border border-forest/20 px-4 py-3">{message}</p>
) : null}
{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)}</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))
: t.ordersPage.shippingPending
: order.pickup?.pickedUpAt
? t.ordersPage.pickedUpOn(formatDate(order.pickup.pickedUpAt))
: order.pickup?.readyAt
? t.ordersPage.readySince(formatDate(order.pickup.readyAt))
: 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} × {item.productName}
</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="text-forest">{formatPrice(order.totalCents, order.currency)}</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>
);
}