Files
Wabenhain/src/app/konto/adressen/page.tsx
T
Nick SzaunigandClaude Opus 5.5 d4bb6ec1cf feat(ui): Adressen, Retoure, Suche und Cookie-Hinweis
- Adressen im Zweierraster, Standardadresse mit Honigrahmen und Wabenmarke,
  neues Adressformular in der Formular-Karte
- Retoure: Positionen als Liste mit Waben, Formular in der Karte
- Suche: Vorschlaege gleiten gestaffelt herein, Suchbegriff hervorgehoben,
  Lupe wird zum Schliessen-Symbol; das Feld legt sich ueber die Navigation
  statt sie zu verschieben (vorher: waagerechter Scrollbalken)
- Cookie-Hinweis als schwebende Karte; "Nur notwendige" und "Alle
  akzeptieren" gleich gewichtet (vorher war Akzeptieren Waldgruen auf
  Waldgruen und kaum als Knopf erkennbar)

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

156 lines
6.1 KiB
TypeScript

import type { Metadata } from "next";
import type { HTMLAttributes } from "react";
import { redirect } from "next/navigation";
import { Container, SectionHeading, Button, FormPanel } from "@/components/ui";
import { HexMark } from "@/components/honeycomb";
import { BackLink } from "@/components/back-link";
import { AccountEmptyState, AddressIcon } from "@/components/account-ui";
import { getCurrentCustomer } from "@/lib/auth";
import { getAddressesForCustomer } from "@/lib/account-data";
import { addAddressAction, deleteAddressAction, setDefaultAddressAction } from "../actions";
import { getLocale } from "@/i18n/locale";
import { getDictionary } from "@/i18n/get-dictionary";
export const metadata: Metadata = { title: "Meine Adressen" };
export const dynamic = "force-dynamic";
export default async function KontoAdressenPage() {
const t = getDictionary(await getLocale());
const customer = await getCurrentCustomer();
if (!customer) redirect("/konto");
const addresses = await getAddressesForCustomer(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.addressesPage.title} />
<div className="mt-10 grid gap-4 sm:grid-cols-2">
{addresses.map((address) => (
<div
key={address.id}
className={`relative flex flex-col justify-between gap-6 border p-6 transition-[border-color,box-shadow] duration-500 ease-[var(--ease-arrive)] ${
address.isDefault
? "border-honey/60 bg-honey/[0.05] shadow-[0_18px_34px_-26px_rgba(184,138,68,0.5)]"
: "border-ink/10 hover:border-forest/40"
}`}
>
{address.isDefault ? (
<span className="absolute right-4 top-4 inline-flex items-center gap-1.5 text-[11px] tracking-wide-caps text-honey-dark">
<HexMark className="h-2.5 w-2.5" />
{t.addressesPage.standard}
</span>
) : null}
<div className="flex gap-4">
<span className="flex-shrink-0 flex items-center justify-center w-11 h-11 border border-ink/15 text-forest">
<AddressIcon />
</span>
<div>
<p className="font-serif text-xl text-forest">{address.label}</p>
<p className="text-sm text-ink/70 mt-1">
{address.firstName} {address.lastName}
<br />
{address.street}
<br />
{address.zip} {address.city}
</p>
</div>
</div>
<div className="flex items-center gap-5 border-t border-ink/10 pt-4 text-sm">
{!address.isDefault ? (
<form action={setDefaultAddressAction}>
<input type="hidden" name="addressId" value={address.id} />
<button type="submit" className="text-forest hover:underline">
{t.addressesPage.setDefault}
</button>
</form>
) : null}
<form action={deleteAddressAction}>
<input type="hidden" name="addressId" value={address.id} />
<button type="submit" className="text-ink/65 hover:text-red-700 hover:underline">
{t.addressesPage.delete}
</button>
</form>
</div>
</div>
))}
{addresses.length === 0 ? (
<AccountEmptyState
icon={<AddressIcon />}
title={t.addressesPage.emptyTitle}
description={t.addressesPage.emptyDescription}
/>
) : null}
</div>
<FormPanel className="mt-14 max-w-xl">
<h2 className="text-2xl text-forest mb-6">{t.addressesPage.addNewHeading}</h2>
<form action={addAddressAction} className="space-y-4">
<Field
name="label"
label={t.addressesPage.labelField}
defaultValue={t.addressesPage.labelDefaultValue}
autoComplete="off"
required
/>
<div className="grid grid-cols-2 gap-4">
<Field name="firstName" label={t.account.firstNameLabel} autoComplete="given-name" required />
<Field name="lastName" label={t.account.lastNameLabel} autoComplete="family-name" required />
</div>
<Field name="street" label={t.checkoutPage.streetLabel} autoComplete="street-address" required />
<div className="grid grid-cols-2 gap-4">
<Field
name="zip"
label={t.checkoutPage.zipLabel}
autoComplete="postal-code"
inputMode="numeric"
required
/>
<Field name="city" label={t.checkoutPage.cityLabel} autoComplete="address-level2" required />
</div>
<label className="flex items-center gap-2 text-sm text-ink/70">
<input type="checkbox" name="isDefault" />
{t.addressesPage.useAsDefault}
</label>
<Button type="submit">{t.addressesPage.save}</Button>
</form>
</FormPanel>
</Container>
</div>
);
}
function Field({
name,
label,
defaultValue,
autoComplete,
inputMode,
required = false,
}: {
name: string;
label: string;
defaultValue?: string;
/** Browser-/Mobil-Autofill fuer Adressfelder — dieselben Tokens wie im Checkout-Formular. */
autoComplete?: string;
inputMode?: HTMLAttributes<HTMLInputElement>["inputMode"];
required?: boolean;
}) {
return (
<label className="block text-sm">
<span className="text-ink/70">{label}</span>
<input
name={name}
defaultValue={defaultValue}
autoComplete={autoComplete}
inputMode={inputMode}
required={required}
className="mt-1 w-full border border-ink/20 bg-background px-4 py-3 focus:outline-none focus:border-forest"
/>
</label>
);
}