Replaces em dashes with en dashes across all hardcoded copy, emails, and seed content, then reworks the site's visual language so it reads as deliberately designed rather than assembled from generic components: - New ArrowLink component replaces ad hoc "Mehr erfahren ->" links and ghost buttons; one shared FormField (TextField/TextAreaField) replaces 7 near-duplicate field styles across every form - Navbar loses its glassmorphism blur for a solid warm background, gets a subtle active-link underline and a softer, warmer mega-menu - PageHeader supports per-page blob tone/position and a compact mode instead of an identical band on every interior page - Homepage: the core offer is featured large instead of four equal cards, the philosophy section gets a single calmer accent blob, blog teasers now show their cover image like the real /aktuelles page does - Booking calendar and widget: 44px touch targets throughout (month nav, day cells, time slots, cancel action), a visible "today" ring, and responsive padding so the calendar isn't cramped on small screens - Quieter, warmer empty states on /termine and /aktuelles No functional changes: all interaction logic (nav menus, forms, booking, CMS data fetching) is untouched, only markup and styling.
137 lines
4.2 KiB
TypeScript
137 lines
4.2 KiB
TypeScript
"use client";
|
|
|
|
import { useId, useState, type FormEvent } from "react";
|
|
import { TextField } from "@/components/FormField";
|
|
|
|
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);
|
|
const emailFieldId = useId();
|
|
|
|
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">
|
|
<TextField
|
|
label="Deine E-Mail-Adresse"
|
|
hideLabel
|
|
id={emailFieldId}
|
|
type="email"
|
|
name="email"
|
|
required
|
|
placeholder="Deine E-Mail-Adresse"
|
|
wrapperClassName="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>
|
|
);
|
|
}
|