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) => (
+ -
+ {String(i + 1).padStart(2, "0")}
{code}
))}
-
+
{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.
+
+
+
{t.newsletterBestaetigtPage.discountHeading}
-
- {t.newsletterBestaetigtPage.discountText(NEWSLETTER_DISCOUNT_PERCENT)}{" "}
- {NEWSLETTER_DISCOUNT_CODE}
+
+ {t.newsletterBestaetigtPage.discountText(NEWSLETTER_DISCOUNT_PERCENT)}
-
{t.newsletterBestaetigtPage.discountCopyHint}
+
{NEWSLETTER_DISCOUNT_CODE}
+
+
+
{t.newsletterBestaetigtPage.discountCopyHint}
+
) : 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 (
+
+ );
+}
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;