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

+55
View File
@@ -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 (
<button
type="button"
onClick={copy}
className="inline-flex h-10 items-center gap-2 border border-forest px-4 text-xs tracking-wide-caps text-forest transition-colors hover:bg-forest hover:text-cream"
aria-live="polite"
>
<AnimatePresence mode="wait" initial={false}>
<motion.span
key={copied ? "done" : "idle"}
initial={{ opacity: 0, y: 5 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -5 }}
transition={{ duration: 0.15 }}
className="inline-flex items-center gap-2"
>
<svg viewBox="0 0 24 24" className="h-3.5 w-3.5" fill="none" stroke="currentColor" strokeWidth="1.6" aria-hidden="true">
{copied ? (
<path d="M5 12.5l4.5 4.5L19 7.5" strokeLinecap="round" strokeLinejoin="round" />
) : (
<>
<rect x="8" y="8" width="12" height="12" rx="1.5" />
<path d="M16 8V5.5A1.5 1.5 0 0 0 14.5 4h-9A1.5 1.5 0 0 0 4 5.5v9A1.5 1.5 0 0 0 5.5 16H8" />
</>
)}
</svg>
{copied ? copiedLabel : label}
</motion.span>
</AnimatePresence>
</button>
);
}