Files
Wabenhain/src/app/bestellung-verfolgen/page.tsx
T
Nick SzaunigandClaude Opus 5.5 1fa8ca246a refactor(ui): Kicker-Zeilen ueber Ueberschriften entfernt
Jede zweite Ueberschrift trug eine kleine Versalien-Zeile mit Wabenpunkt
("• STIMMEN", "• AKTUELL VERFUEGBAR", "• KUNDENKONTO" ...). Auf 29 Seiten
und Abschnitten wiederholt las sich das wie eine Vorlage. SectionHeading
rendert sie nicht mehr, die Prop ist entfernt; die Ueberschrift traegt den
Abschnitt allein.

Die eyebrow-Texte, die weiterhin als Seitentitel, Bildbeschriftung oder
Breadcrumb dienen, bleiben; 18 danach ungenutzte Eintraege sind aus Typ und
beiden Woerterbuechern entfernt.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
2026-09-26 01:41:48 +02:00

197 lines
8.3 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 Link from "next/link";
import { Container, SectionHeading, Button, FormPanel } from "@/components/ui";
import { OrderStatusBadge, OrderStatusTimeline } from "@/components/account-ui";
import { getOrderForTracking } from "@/lib/account-data";
import { formatPrice, formatDate } from "@/lib/format";
import { getCarrierTrackingUrl, isDhlCarrier } from "@/lib/shipment-tracking";
import { getDhlTracking, type DhlTrackingResult } from "@/lib/dhl-tracking";
import { ShipmentTrackingHistory } from "@/components/shipment-tracking-history";
import { rateLimit, getClientIp } from "@/lib/rate-limit";
import { getLocale } from "@/i18n/locale";
import { getDictionary } from "@/i18n/get-dictionary";
export const metadata: Metadata = { title: "Bestellung verfolgen", robots: { index: false } };
export const dynamic = "force-dynamic";
const TRACK_ATTEMPTS_PER_WINDOW = 10;
const TRACK_WINDOW_MS = 15 * 60 * 1000;
export default async function TrackOrderPage(props: PageProps<"/bestellung-verfolgen">) {
const locale = await getLocale();
const t = getDictionary(locale);
const { order: orderNumber, email } = await props.searchParams;
const hasQuery = typeof orderNumber === "string" && typeof email === "string" && orderNumber && email;
let order = null;
let rateLimited = false;
if (hasQuery) {
const ip = await getClientIp();
if (rateLimit(`track-order:${ip}`, TRACK_ATTEMPTS_PER_WINDOW, TRACK_WINDOW_MS)) {
order = await getOrderForTracking(orderNumber, email);
} else {
rateLimited = true;
}
}
// Erst NACH erfolgreicher Pruefung von Bestellnummer + E-Mail bei DHL
// nachfragen: sonst liesse sich diese Seite als offener Proxy fuer beliebige
// Sendungsabfragen missbrauchen und das DHL-Kontingent leerraeumen.
let dhlTracking: DhlTrackingResult = { kind: "disabled" };
if (order?.shipment?.trackingNumber && isDhlCarrier(order.shipment.carrier)) {
dhlTracking = await getDhlTracking({
trackingNumber: order.shipment.trackingNumber,
postalCode: order.zip,
locale,
});
}
return (
<div className="py-20">
<Container className="max-w-xl">
<SectionHeading title={t.trackOrderPage.title} />
<p className="mt-4 text-ink/70 leading-relaxed">{t.trackOrderPage.intro}</p>
<FormPanel className="mt-8">
<form method="GET" className="space-y-4">
<label className="block text-sm">
<span className="text-ink/70">{t.trackOrderPage.orderNumberLabel}</span>
<input
name="order"
type="text"
defaultValue={typeof orderNumber === "string" ? orderNumber : ""}
placeholder={t.trackOrderPage.orderNumberPlaceholder}
required
className="mt-1 w-full border border-ink/20 bg-background px-4 py-3 focus:outline-none focus:border-forest"
/>
</label>
<label className="block text-sm">
<span className="text-ink/70">{t.account.emailLabel}</span>
<input
name="email"
type="email"
defaultValue={typeof email === "string" ? email : ""}
required
autoComplete="email"
className="mt-1 w-full border border-ink/20 bg-background px-4 py-3 focus:outline-none focus:border-forest"
/>
</label>
<Button type="submit">{t.trackOrderPage.submit}</Button>
</form>
</FormPanel>
{rateLimited ? (
<p className="mt-8 text-sm text-red-700 bg-red-50 border border-red-200 px-4 py-3">
{t.trackOrderPage.rateLimitError}
</p>
) : null}
{hasQuery && !order && !rateLimited ? (
<p className="mt-8 text-sm text-red-700 bg-red-50 border border-red-200 px-4 py-3">
{t.trackOrderPage.notFoundError}
</p>
) : null}
{order ? (
<div className="mt-10 border border-ink/10 bg-background p-6 shadow-[0_24px_50px_-34px_rgba(36,53,43,0.45)] sm:p-8">
<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={t.orderStatusBadge[order.status as keyof typeof t.orderStatusBadge] ?? 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">
<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}
<ShipmentTrackingHistory
result={dhlTracking}
locale={locale}
labels={{
heading: t.shipmentInfo.historyHeading,
ariaLabel: t.shipmentInfo.historyAriaLabel,
estimatedDelivery: t.shipmentInfo.estimatedDelivery,
notYetScanned: t.shipmentInfo.notYetScanned,
unavailable: t.shipmentInfo.historyUnavailable,
dataSource: t.shipmentInfo.dataSource,
statuses: t.shipmentInfo.statuses,
}}
/>
<div className="mt-5 flex items-center justify-between gap-4">
<p className="font-serif text-2xl text-forest tabular-nums">{formatPrice(order.totalCents, order.currency, locale)}</p>
{order.invoice ? (
<Link href={`/rechnung/${order.invoice.invoiceNumber}`} className="text-sm text-forest underline">
{t.trackOrderPage.viewInvoice}
</Link>
) : null}
</div>
</div>
) : null}
</Container>
</div>
);
}