From ee854bd77b0503354848f0dcb9537a0c8746a9ee Mon Sep 17 00:00:00 2001 From: Nick Szaunig Date: Fri, 25 Sep 2026 13:17:58 +0200 Subject: [PATCH] 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) --- src/app/konto/passwort-vergessen/page.tsx | 6 +- src/app/konto/passwort-zuruecksetzen/page.tsx | 6 +- src/app/konto/zwei-faktor-code/page.tsx | 8 ++- .../konto/zwei-faktor/backup-codes/page.tsx | 18 ++++-- src/app/konto/zwei-faktor/einrichten/page.tsx | 6 +- src/app/konto/zwei-faktor/page.tsx | 6 +- src/app/newsletter-bestaetigt/page.tsx | 22 ++++++-- src/components/copy-code-button.tsx | 55 +++++++++++++++++++ src/components/ui.tsx | 20 ++++++- src/i18n/dictionaries/de.ts | 2 + src/i18n/dictionaries/en.ts | 2 + src/i18n/dictionary-type.ts | 2 + 12 files changed, 130 insertions(+), 23 deletions(-) create mode 100644 src/components/copy-code-button.tsx diff --git a/src/app/konto/passwort-vergessen/page.tsx b/src/app/konto/passwort-vergessen/page.tsx index 870e814..88ad750 100644 --- a/src/app/konto/passwort-vergessen/page.tsx +++ b/src/app/konto/passwort-vergessen/page.tsx @@ -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 (
- + + @@ -46,6 +47,7 @@ export default async function PasswortVergessenPage(props: PageProps<"/konto/pas )} +
); diff --git a/src/app/konto/passwort-zuruecksetzen/page.tsx b/src/app/konto/passwort-zuruecksetzen/page.tsx index eaa7f54..83aa799 100644 --- a/src/app/konto/passwort-zuruecksetzen/page.tsx +++ b/src/app/konto/passwort-zuruecksetzen/page.tsx @@ -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 (
- + + @@ -47,6 +48,7 @@ export default async function PasswortZuruecksetzenPage( )} +
); diff --git a/src/app/konto/zwei-faktor-code/page.tsx b/src/app/konto/zwei-faktor-code/page.tsx index bdd10b9..0b8d823 100644 --- a/src/app/konto/zwei-faktor-code/page.tsx +++ b/src/app/konto/zwei-faktor-code/page.tsx @@ -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 (
- + + {process.env.NEXT_PUBLIC_TURNSTILE_SITE_KEY ? ( @@ -46,6 +47,7 @@ export default async function ZweiFaktorCodePage() { ) : null} +
); diff --git a/src/app/konto/zwei-faktor/backup-codes/page.tsx b/src/app/konto/zwei-faktor/backup-codes/page.tsx index a144397..ca8215a 100644 --- a/src/app/konto/zwei-faktor/backup-codes/page.tsx +++ b/src/app/konto/zwei-faktor/backup-codes/page.tsx @@ -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 (
- + + -
    - {codes.map((code) => ( -
  • +
      + {codes.map((code, i) => ( +
    1. + {String(i + 1).padStart(2, "0")} {code}
    2. ))} -
+

{t.twoFactorBackupCodesPage.warning} @@ -41,6 +46,7 @@ export default async function ZweiFaktorBackupCodesPage() {

+
); diff --git a/src/app/konto/zwei-faktor/einrichten/page.tsx b/src/app/konto/zwei-faktor/einrichten/page.tsx index 84ce85f..52678b7 100644 --- a/src/app/konto/zwei-faktor/einrichten/page.tsx +++ b/src/app/konto/zwei-faktor/einrichten/page.tsx @@ -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 (
- + + +
); diff --git a/src/app/konto/zwei-faktor/page.tsx b/src/app/konto/zwei-faktor/page.tsx index f521838..8dc42a5 100644 --- a/src/app/konto/zwei-faktor/page.tsx +++ b/src/app/konto/zwei-faktor/page.tsx @@ -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 (
- + + {t.twoFactorPage.setupButton} )} +
); diff --git a/src/app/newsletter-bestaetigt/page.tsx b/src/app/newsletter-bestaetigt/page.tsx index 56d23e4..976ab7f 100644 --- a/src/app/newsletter-bestaetigt/page.tsx +++ b/src/app/newsletter-bestaetigt/page.tsx @@ -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

{text}

{status === "ok" ? ( -
+ // Der Rabatt als Gutschein: gestrichelter Rand wie zum Ausschneiden, + // Code gross und mit einem Klick kopierbar. +
+
) : null} diff --git a/src/components/copy-code-button.tsx b/src/components/copy-code-button.tsx new file mode 100644 index 0000000..7dd7bc2 --- /dev/null +++ b/src/components/copy-code-button.tsx @@ -0,0 +1,55 @@ +"use client"; + +import { useState } from "react"; +import { AnimatePresence, motion } from "motion/react"; + +/** + * Kopiert einen Gutscheincode in die Zwischenablage und bestaetigt das kurz + * am Knopf selbst. Ohne Clipboard-API (aeltere Browser, unsicherer Kontext) + * bleibt der Code sichtbar und markierbar — der Knopf meldet dann nichts. + */ +export function CopyCodeButton({ code, label, copiedLabel }: { code: string; label: string; copiedLabel: string }) { + const [copied, setCopied] = useState(false); + + async function copy() { + try { + await navigator.clipboard.writeText(code); + setCopied(true); + window.setTimeout(() => setCopied(false), 2000); + } catch { + // Kein Zugriff auf die Zwischenablage — der Code bleibt zum Markieren sichtbar. + } + } + + return ( + + ); +} diff --git a/src/components/ui.tsx b/src/components/ui.tsx index 1538b90..e45c591 100644 --- a/src/components/ui.tsx +++ b/src/components/ui.tsx @@ -1,6 +1,6 @@ import Link from "next/link"; import type { ComponentPropsWithoutRef, ReactNode } from "react"; -import { HexMark } from "@/components/honeycomb"; +import { HexMark, HoneycombPattern } from "@/components/honeycomb"; export function Container({ id, @@ -164,3 +164,21 @@ export function FramedMedia({
); } + +/** + * Karte fuer die kurzen Formularseiten (Passwort, Zwei-Faktor, ...): hebt + * das Formular von der Flaeche ab, mit Wabenraster in der oberen Ecke als + * leiser Markengruss. + */ +export function FormPanel({ children, className = "" }: { children: ReactNode; className?: string }) { + return ( +
+ +
{children}
+
+ ); +} diff --git a/src/i18n/dictionaries/de.ts b/src/i18n/dictionaries/de.ts index 8e3c142..5140277 100644 --- a/src/i18n/dictionaries/de.ts +++ b/src/i18n/dictionaries/de.ts @@ -668,6 +668,8 @@ export const de: Dictionary = { discountHeading: "Dein Willkommensrabatt", discountText: (percent) => `${percent}% auf deine nächste Bestellung mit Code`, discountCopyHint: "Code beim Checkout eingeben.", + copyCode: "Code kopieren", + codeCopied: "Kopiert", }, adminReviewsPage: { backToDashboard: "Zurück zum Dashboard", diff --git a/src/i18n/dictionaries/en.ts b/src/i18n/dictionaries/en.ts index 7af7022..3c2a5a1 100644 --- a/src/i18n/dictionaries/en.ts +++ b/src/i18n/dictionaries/en.ts @@ -656,6 +656,8 @@ export const en: Dictionary = { discountHeading: "Your welcome discount", discountText: (percent) => `${percent}% off your next order with code`, discountCopyHint: "Enter the code at checkout.", + copyCode: "Copy code", + codeCopied: "Copied", }, adminReviewsPage: { backToDashboard: "Back to dashboard", diff --git a/src/i18n/dictionary-type.ts b/src/i18n/dictionary-type.ts index 515bbd2..5d1813d 100644 --- a/src/i18n/dictionary-type.ts +++ b/src/i18n/dictionary-type.ts @@ -347,6 +347,8 @@ export interface Dictionary { discountHeading: string; discountText: (percent: number) => string; discountCopyHint: string; + copyCode: string; + codeCopied: string; }; adminReviewsPage: { backToDashboard: string;