Make checkout's login prompt reactive instead of persistent, and give the confirmation email real style and voice
The always-visible "Schon Kundin?" toggle was gendered and shown to every logged-out visitor regardless of relevance. Card 1's email field now checks on blur (/api/account/check-email) whether that address already has an account, and only then swaps in a gender-neutral login form, pre-filled — the collision check in handleSubmit stays as a fallback. The order-confirmation and password-reset emails also got a real visual pass: same warm background/brand color/circular success-icon treatment as the on-screen /bestellbestaetigung page, serif heading, thin brand divider, table-based layout for email-client compatibility. Copy is on-brand and a little playful now instead of generic transactional boilerplate. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -119,6 +119,33 @@ export function CheckoutContent({
|
||||
router.refresh();
|
||||
}
|
||||
|
||||
// Checks whether the email just typed into Card 1 already has an
|
||||
// account — proactively, on blur, rather than only finding out after a
|
||||
// registration attempt fails (see handleSubmit's own emailExists
|
||||
// handling, which still applies as a fallback if this check is skipped
|
||||
// or the account was created in the meantime). Silent when there's no
|
||||
// match — the account gate simply stays empty; nothing is shown "just
|
||||
// in case", only once actually relevant.
|
||||
async function handleEmailBlur(e: React.FocusEvent<HTMLInputElement>) {
|
||||
const email = e.target.value.trim();
|
||||
if (!email || customerEmail || showLogin) return;
|
||||
try {
|
||||
const res = await fetch("/api/account/check-email", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ email }),
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.exists) {
|
||||
setLoginEmail(email);
|
||||
setShowLogin(true);
|
||||
}
|
||||
} catch {
|
||||
// Best-effort — worst case, the registration attempt itself catches
|
||||
// the collision at submit time instead (handleSubmit).
|
||||
}
|
||||
}
|
||||
|
||||
// Always goes through /api/checkout now — that route re-prices
|
||||
// everything server-side, re-validates+redeems a discount code exactly
|
||||
// once (see its own comment), registers a new account inline when no
|
||||
@@ -242,12 +269,14 @@ export function CheckoutContent({
|
||||
<p className="text-body text-text-muted">Fast geschafft! Nur noch ein paar Angaben.</p>
|
||||
</div>
|
||||
|
||||
{/* Account gate — an account is required to buy, so this either
|
||||
confirms the active session or offers "already a customer?"
|
||||
login inline (registration itself happens as part of the main
|
||||
form submit below, via the password field in Card 1). Ref used
|
||||
to scroll this into view if a checkout submit turns out to
|
||||
collide with an existing account (see handleSubmit). */}
|
||||
{/* Account gate — an account is required to buy. Not a persistent
|
||||
"already a customer? log in" prompt anymore (removed —
|
||||
Nutzer-Entscheidung: don't show it unless actually relevant):
|
||||
stays empty by default, and only shows the login form once
|
||||
handleEmailBlur (Card 1's email field) or handleSubmit's own
|
||||
emailExists fallback actually detects that the typed email
|
||||
belongs to an existing account. Ref used to scroll this into
|
||||
view when that happens after a submit attempt specifically. */}
|
||||
<div ref={accountGateRef} className="w-full">
|
||||
{customerEmail ? (
|
||||
<p className="text-body-sm text-text-primary">
|
||||
@@ -256,42 +285,50 @@ export function CheckoutContent({
|
||||
Abmelden
|
||||
</button>
|
||||
</p>
|
||||
) : !showLogin ? (
|
||||
<p className="text-body-sm text-text-primary">
|
||||
Schon Kundin?{" "}
|
||||
<button type="button" onClick={() => setShowLogin(true)} className="underline font-bold hover:text-brand transition-colors">
|
||||
Hier einloggen
|
||||
</button>
|
||||
</p>
|
||||
) : (
|
||||
<div className="flex flex-col sm:flex-row gap-3 items-start sm:items-end w-full sm:w-auto">
|
||||
<FormField
|
||||
label="E-Mail-Adresse"
|
||||
type="email"
|
||||
value={loginEmail}
|
||||
onChange={(e) => setLoginEmail(e.target.value)}
|
||||
autoComplete="email"
|
||||
wrapperClassName="w-full sm:w-56"
|
||||
/>
|
||||
<FormField
|
||||
label="Passwort"
|
||||
type="password"
|
||||
value={loginPassword}
|
||||
onChange={(e) => setLoginPassword(e.target.value)}
|
||||
autoComplete="current-password"
|
||||
wrapperClassName="w-full sm:w-56"
|
||||
/>
|
||||
) : showLogin ? (
|
||||
<div className="flex flex-col gap-3 items-start w-full">
|
||||
<p className="text-body-sm text-text-primary">
|
||||
Für diese E-Mail-Adresse existiert bereits ein Konto — bitte einloggen, um fortzufahren.
|
||||
</p>
|
||||
<div className="flex flex-col sm:flex-row gap-3 items-start sm:items-end w-full sm:w-auto">
|
||||
<FormField
|
||||
label="E-Mail-Adresse"
|
||||
type="email"
|
||||
value={loginEmail}
|
||||
onChange={(e) => setLoginEmail(e.target.value)}
|
||||
autoComplete="email"
|
||||
wrapperClassName="w-full sm:w-56"
|
||||
/>
|
||||
<FormField
|
||||
label="Passwort"
|
||||
type="password"
|
||||
value={loginPassword}
|
||||
onChange={(e) => setLoginPassword(e.target.value)}
|
||||
autoComplete="current-password"
|
||||
wrapperClassName="w-full sm:w-56"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleLogin}
|
||||
disabled={loggingIn}
|
||||
className={`px-5 py-3 rounded-sm bg-brand hover:bg-brand-hover font-bold text-body-sm text-text-primary transition-colors ${loggingIn ? "opacity-70 pointer-events-none" : ""}`}
|
||||
>
|
||||
{loggingIn ? "…" : "Einloggen"}
|
||||
</button>
|
||||
</div>
|
||||
{loginError && <p className="text-label text-red-600 w-full">{loginError}</p>}
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleLogin}
|
||||
disabled={loggingIn}
|
||||
className={`px-5 py-3 rounded-sm bg-brand hover:bg-brand-hover font-bold text-body-sm text-text-primary transition-colors ${loggingIn ? "opacity-70 pointer-events-none" : ""}`}
|
||||
onClick={() => {
|
||||
setShowLogin(false);
|
||||
setLoginError(null);
|
||||
}}
|
||||
className="text-label text-text-muted underline hover:text-text-primary transition-colors"
|
||||
>
|
||||
{loggingIn ? "…" : "Einloggen"}
|
||||
Andere E-Mail-Adresse verwenden
|
||||
</button>
|
||||
{loginError && <p className="text-label text-red-600 w-full">{loginError}</p>}
|
||||
</div>
|
||||
)}
|
||||
) : null}
|
||||
</div>
|
||||
</Reveal>
|
||||
|
||||
@@ -321,6 +358,7 @@ export function CheckoutContent({
|
||||
placeholder="max@beispiel.de"
|
||||
autoComplete="email"
|
||||
required
|
||||
onBlur={handleEmailBlur}
|
||||
wrapperClassName="w-full sm:w-[calc(50%-0.5rem)] sm:flex-none min-w-0"
|
||||
/>
|
||||
{/* Only needed for the inline-registration path — an existing
|
||||
|
||||
Reference in New Issue
Block a user