Files
anouma/components/auth/ResendVerificationButton.tsx
T
maroandClaude Sonnet 5 1cd15aff25 Add email verification, personal calendar feed, and full SEO implementation
- Customer accounts now require email verification (hashed, single-use,
  time-limited tokens) before they can request/confirm bookings, with
  resend flows on login/account/booking widget and rate limiting.
- Admins get a private, rotatable iCalendar (ICS) subscription feed of
  their confirmed bookings and public events, timezone-correct for
  Europe/Berlin including DST, never exposing meeting passwords.
- Adds a full SEO layer: per-page canonical/OG/Twitter metadata with
  CMS-editable overrides and content-derived fallbacks, a dynamic
  sitemap.xml and robots.txt driven by real published content, JSON-LD
  (Organization/LocalBusiness, WebSite, WebPage, BreadcrumbList, Service,
  Event, BlogPosting) that never fabricates data, and a CMS-managed
  redirect table for changed slugs.
- Global ANOUMA-naming audit: the brand name is never used to label
  personal account/calendar areas anywhere in the app, CMS, or emails.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-25 22:57:31 +02:00

135 lines
4.3 KiB
TypeScript

"use client";
import { useState, type FormEvent } from "react";
const fieldClass =
"w-full rounded-2xl border border-anouma-taupe/30 bg-white px-4 py-2.5 text-sm text-anouma-plum placeholder:text-anouma-plum/50 focus:border-anouma-mauve-dark focus:outline-none";
async function callResend(email?: string): Promise<{ ok: boolean; error?: string }> {
try {
const res = await fetch("/api/auth/resend-verification", {
method: "POST",
credentials: "include",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(email ? { email } : {}),
});
const data = await res.json().catch(() => ({}));
if (!res.ok) return { ok: false, error: data.error || "Die Bestätigungs-E-Mail konnte nicht gesendet werden." };
return { ok: true };
} catch {
return { ok: false, error: "Verbindung fehlgeschlagen." };
}
}
const GENERIC_SENT_MESSAGE =
"Falls das Konto noch nicht bestätigt ist, wurde eine neue Bestätigungs-E-Mail gesendet.";
/**
* variant "session": a single button that resends for the currently logged-in
* customer (used in /konto and the booking widget, where a session already
* exists).
*
* variant "email-prompt": a toggle link that reveals an email field first
* (used on /login, where the visitor may not be authenticated yet).
*/
export function ResendVerificationButton({
variant,
triggerLabel = "E-Mail noch nicht bestätigt?",
className,
}: {
variant: "session" | "email-prompt";
triggerLabel?: string;
className?: string;
}) {
const [open, setOpen] = useState(false);
const [status, setStatus] = useState<"idle" | "sending" | "sent" | "error">("idle");
const [error, setError] = useState<string | null>(null);
async function resendForSession() {
setStatus("sending");
const result = await callResend();
if (result.ok) {
setStatus("sent");
} else {
setStatus("error");
setError(result.error ?? null);
}
}
async function handleEmailSubmit(e: FormEvent<HTMLFormElement>) {
e.preventDefault();
setStatus("sending");
setError(null);
const email = String(new FormData(e.currentTarget).get("email") ?? "");
const result = await callResend(email);
if (result.ok) {
setStatus("sent");
} else {
setStatus("error");
setError(result.error ?? null);
}
}
if (variant === "session") {
return (
<div className={className}>
{status === "sent" ? (
<p className="text-sm text-anouma-olive">{GENERIC_SENT_MESSAGE}</p>
) : (
<button
type="button"
onClick={resendForSession}
disabled={status === "sending"}
className="text-sm font-medium text-anouma-mauve-dark underline underline-offset-4 disabled:opacity-60"
>
{status === "sending" ? "Wird gesendet …" : "Bestätigungs-E-Mail erneut senden"}
</button>
)}
{status === "error" && error && (
<p role="alert" className="mt-1 text-sm text-red-700">
{error}
</p>
)}
</div>
);
}
return (
<div className={className}>
{!open ? (
<button
type="button"
onClick={() => setOpen(true)}
className="text-sm font-medium text-anouma-mauve-dark underline underline-offset-4"
>
{triggerLabel}
</button>
) : status === "sent" ? (
<p className="text-sm text-anouma-olive">{GENERIC_SENT_MESSAGE}</p>
) : (
<form onSubmit={handleEmailSubmit} className="mt-2 flex flex-wrap items-start gap-2">
<input
type="email"
name="email"
required
placeholder="Deine E-Mail-Adresse"
className={`${fieldClass} max-w-xs flex-1`}
/>
<button
type="submit"
disabled={status === "sending"}
className="rounded-full bg-anouma-mauve-dark px-5 py-2.5 text-sm font-medium text-white hover:bg-anouma-plum disabled:opacity-60"
>
{status === "sending" ? "Wird gesendet …" : "Bestätigungs-E-Mail erneut senden"}
</button>
</form>
)}
{status === "error" && error && (
<p role="alert" className="mt-1 text-sm text-red-700">
{error}
</p>
)}
</div>
);
}