feat(ui): Formular-Karten, Backup-Codes als Kacheln, Gutschein zum Kopieren

- Gemeinsame FormPanel-Karte fuer Passwort- und Zwei-Faktor-Seiten
- Zwei-Faktor-Code: grosses, zentriertes Ziffernfeld
- Backup-Codes als nummerierte Kacheln statt langer Liste
- Newsletter-Bestaetigung: Willkommensrabatt als Gutschein mit
  gestricheltem Rand und Kopier-Knopf (bestaetigt "Kopiert")

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
Nick SzaunigandClaude Opus 5.5 committed 2026-09-25 13:17:58 +02:00
1 parent 76be1292ed
commit ee854bd77b
12 files changed
+130 -23

No files matched your search

+4 -2
View File
@@ -1,5 +1,5 @@
import type { Metadata } from "next";
import { Container, SectionHeading, Button } from "@/components/ui";
import { Container, SectionHeading, Button, FormPanel } from "@/components/ui";
import { BackLink } from "@/components/back-link";
import { TurnstileField } from "@/components/turnstile-widget";
import { requestPasswordResetAction } from "../actions";
@@ -14,7 +14,8 @@ export default async function PasswortVergessenPage(props: PageProps<"/konto/pas
return (
<div className="py-20">
<Container className="max-w-md">
<Container className="max-w-lg">
<FormPanel>
<BackLink href="/konto" label={t.passwortVergessenPage.backLabel} />
<SectionHeading eyebrow={t.account.eyebrow} title={t.passwortVergessenPage.title} />
@@ -46,6 +47,7 @@ export default async function PasswortVergessenPage(props: PageProps<"/konto/pas
</form>
</>
)}
</FormPanel>
</Container>
</div>
);
@@ -1,5 +1,5 @@
import type { Metadata } from "next";
import { Container, SectionHeading, Button } from "@/components/ui";
import { Container, SectionHeading, Button, FormPanel } from "@/components/ui";
import { BackLink } from "@/components/back-link";
import { PasswordField } from "@/components/password-field";
import { TurnstileField } from "@/components/turnstile-widget";
@@ -17,7 +17,8 @@ export default async function PasswortZuruecksetzenPage(
return (
<div className="py-20">
<Container className="max-w-md">
<Container className="max-w-lg">
<FormPanel>
<BackLink href="/konto" label={t.passwortZuruecksetzenPage.backLabel} />
<SectionHeading eyebrow={t.account.eyebrow} title={t.passwortZuruecksetzenPage.title} />
@@ -47,6 +48,7 @@ export default async function PasswortZuruecksetzenPage(
<Button type="submit">{t.passwortZuruecksetzenPage.save}</Button>
</form>
)}
</FormPanel>
</Container>
</div>
);
+5 -3
View File
@@ -1,6 +1,6 @@
import type { Metadata } from "next";
import { redirect } from "next/navigation";
import { Container, SectionHeading, Button } from "@/components/ui";
import { Container, SectionHeading, Button, FormPanel } from "@/components/ui";
import { BackLink } from "@/components/back-link";
import { TurnstileField } from "@/components/turnstile-widget";
import { getTwoFactorChallengeCustomerId } from "@/lib/auth";
@@ -19,7 +19,8 @@ export default async function ZweiFaktorCodePage() {
return (
<div className="py-20">
<Container className="max-w-md">
<Container className="max-w-lg">
<FormPanel>
<BackLink href="/konto" label={t.twoFactorCodePage.cancelLabel} />
<SectionHeading
eyebrow={t.twoFactorCodePage.eyebrow}
@@ -38,7 +39,7 @@ export default async function ZweiFaktorCodePage() {
autoComplete="one-time-code"
autoFocus
required
className="mt-1 w-full border border-ink/20 bg-background px-4 py-3 tracking-widest focus:outline-none focus:border-forest"
className="mt-2 w-full border border-ink/20 bg-background px-4 py-4 text-center font-serif text-3xl tracking-[0.4em] tabular-nums focus:outline-none focus:border-forest"
/>
</label>
{process.env.NEXT_PUBLIC_TURNSTILE_SITE_KEY ? (
@@ -46,6 +47,7 @@ export default async function ZweiFaktorCodePage() {
) : null}
<Button type="submit">{t.twoFactorCodePage.confirm}</Button>
</form>
</FormPanel>
</Container>
</div>
);
@@ -1,6 +1,6 @@
import type { Metadata } from "next";
import { redirect } from "next/navigation";
import { Container, SectionHeading, Button } from "@/components/ui";
import { Container, SectionHeading, Button, FormPanel } from "@/components/ui";
import { getCurrentCustomer, getTwoFactorBackupCodesDisplay } from "@/lib/auth";
import { dismissBackupCodesAction } from "../../actions";
import { getLocale } from "@/i18n/locale";
@@ -19,20 +19,25 @@ export default async function ZweiFaktorBackupCodesPage() {
return (
<div className="py-20">
<Container className="max-w-md">
<Container className="max-w-lg">
<FormPanel>
<SectionHeading
eyebrow={t.twoFactorBackupCodesPage.eyebrow}
title={t.twoFactorBackupCodesPage.title}
subtitle={t.twoFactorBackupCodesPage.subtitle}
/>
<ul className="mt-8 border border-ink/10 divide-y divide-ink/10 font-mono text-sm">
{codes.map((code) => (
<li key={code} className="px-4 py-3">
<ol className="mt-8 grid grid-cols-2 gap-2">
{codes.map((code, i) => (
<li
key={code}
className="flex items-center gap-3 border border-ink/10 bg-cream/50 px-3 py-3 font-mono text-sm tabular-nums tracking-wider text-forest"
>
<span className="text-[10px] text-ink/45 tabular-nums">{String(i + 1).padStart(2, "0")}</span>
{code}
</li>
))}
</ul>
</ol>
<p className="mt-6 text-sm text-red-700 bg-red-50 border border-red-200 px-4 py-3">
{t.twoFactorBackupCodesPage.warning}
@@ -41,6 +46,7 @@ export default async function ZweiFaktorBackupCodesPage() {
<form action={dismissBackupCodesAction} className="mt-8">
<Button type="submit">{t.twoFactorBackupCodesPage.done}</Button>
</form>
</FormPanel>
</Container>
</div>
);
@@ -1,6 +1,6 @@
import type { Metadata } from "next";
import { redirect } from "next/navigation";
import { Container, SectionHeading, Button } from "@/components/ui";
import { Container, SectionHeading, Button, FormPanel } from "@/components/ui";
import { BackLink } from "@/components/back-link";
import { getCurrentCustomer, getTwoFactorSetup } from "@/lib/auth";
import { generateTotpQrCodeDataUrl } from "@/lib/two-factor";
@@ -24,7 +24,8 @@ export default async function ZweiFaktorEinrichtenPage() {
return (
<div className="py-20">
<Container className="max-w-md">
<Container className="max-w-lg">
<FormPanel>
<BackLink href="/konto/zwei-faktor" label={t.twoFactorSetupPage.backLabel} />
<SectionHeading
eyebrow={t.twoFactorSetupPage.eyebrow}
@@ -55,6 +56,7 @@ export default async function ZweiFaktorEinrichtenPage() {
</label>
<Button type="submit">{t.twoFactorSetupPage.activate}</Button>
</form>
</FormPanel>
</Container>
</div>
);
+4 -2
View File
@@ -1,6 +1,6 @@
import type { Metadata } from "next";
import { redirect } from "next/navigation";
import { Container, SectionHeading, Button } from "@/components/ui";
import { Container, SectionHeading, Button, FormPanel } from "@/components/ui";
import { BackLink } from "@/components/back-link";
import { ShieldIcon } from "@/components/account-ui";
import { PasswordField } from "@/components/password-field";
@@ -25,7 +25,8 @@ export default async function ZweiFaktorPage() {
return (
<div className="py-20">
<Container className="max-w-md">
<Container className="max-w-lg">
<FormPanel>
<BackLink href="/konto" label={t.twoFactorPage.backLabel} />
<SectionHeading
eyebrow={t.twoFactorPage.eyebrow}
@@ -61,6 +62,7 @@ export default async function ZweiFaktorPage() {
<Button type="submit">{t.twoFactorPage.setupButton}</Button>
</form>
)}
</FormPanel>
</Container>
</div>
);
+17 -5
View File
@@ -1,4 +1,5 @@
import type { Metadata } from "next";
import { CopyCodeButton } from "@/components/copy-code-button";
import { Container, SectionHeading, LinkButton } from "@/components/ui";
import { getLocale } from "@/i18n/locale";
import { getDictionary } from "@/i18n/get-dictionary";
@@ -38,13 +39,24 @@ export default async function NewsletterBestaetigtPage(props: PageProps<"/newsle
<p className="mt-6 text-ink/70">{text}</p>
{status === "ok" ? (
<div className="mt-8 border border-forest/20 bg-forest/5 p-6">
// Der Rabatt als Gutschein: gestrichelter Rand wie zum Ausschneiden,
// Code gross und mit einem Klick kopierbar.
<div className="relative mt-10 border-2 border-dashed border-honey/60 bg-honey/[0.06] px-6 py-8">
<span aria-hidden="true" className="absolute -left-3 top-1/2 h-6 w-6 -translate-y-1/2 rounded-full bg-background" />
<span aria-hidden="true" className="absolute -right-3 top-1/2 h-6 w-6 -translate-y-1/2 rounded-full bg-background" />
<p className="text-sm text-ink/70">{t.newsletterBestaetigtPage.discountHeading}</p>
<p className="mt-2 text-2xl text-forest tracking-wide">
{t.newsletterBestaetigtPage.discountText(NEWSLETTER_DISCOUNT_PERCENT)}{" "}
<span className="font-medium">{NEWSLETTER_DISCOUNT_CODE}</span>
<p className="mt-2 font-serif text-2xl text-forest">
{t.newsletterBestaetigtPage.discountText(NEWSLETTER_DISCOUNT_PERCENT)}
</p>
<p className="mt-2 text-xs text-ink/70">{t.newsletterBestaetigtPage.discountCopyHint}</p>
<p className="mt-4 font-mono text-3xl tracking-[0.2em] text-honey-dark select-all">{NEWSLETTER_DISCOUNT_CODE}</p>
<div className="mt-5 flex flex-col items-center gap-2">
<CopyCodeButton
code={NEWSLETTER_DISCOUNT_CODE}
label={t.newsletterBestaetigtPage.copyCode}
copiedLabel={t.newsletterBestaetigtPage.codeCopied}
/>
<p className="text-xs text-ink/70">{t.newsletterBestaetigtPage.discountCopyHint}</p>
</div>
</div>
) : null}