- 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]>
156 lines
6.1 KiB
TypeScript
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>
|
|
);
|
|
}
|