Fix checkout login-prompt placement and unify profile country select
Login prompt (email already has an account) now renders inline under Card 1's own email field instead of a separate block above the whole form — no scrolling needed in the common case, and no more re-typing the email into a second field. The submit-time fallback still scrolls it into view via a useEffect, now that the target is conditionally rendered. /konto/profil's "Land" select was still hardcoded to Deutschland/ Österreich/Schweiz independently of /checkout's own Payload-configurable shipping-countries list. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -142,11 +142,30 @@ export function CheckoutContent({
|
||||
const [purchaseError, setPurchaseError] = useState<string | null>(null);
|
||||
const [purchasing, setPurchasing] = useState(false);
|
||||
const [showLogin, setShowLogin] = useState(false);
|
||||
const accountGateRef = useRef<HTMLDivElement>(null);
|
||||
const [loginEmail, setLoginEmail] = useState("");
|
||||
// Scroll target for the submit-time emailExists fallback below — the
|
||||
// common case (blur-triggered, see handleEmailBlur) needs no scroll at
|
||||
// all, since the inline login prompt already renders right where the
|
||||
// shopper is looking (under Card 1's own email field); this ref only
|
||||
// matters if a shopper filled the whole form and hit submit from further
|
||||
// down the page without ever blurring the email field first (e.g.
|
||||
// browser autofill).
|
||||
const loginGateRef = useRef<HTMLDivElement>(null);
|
||||
const [loginPassword, setLoginPassword] = useState("");
|
||||
const [loginError, setLoginError] = useState<string | null>(null);
|
||||
const [loggingIn, setLoggingIn] = useState(false);
|
||||
|
||||
// Scrolls the inline login prompt into view once it actually exists in
|
||||
// the DOM — can't do this synchronously right where setShowLogin(true)
|
||||
// is called (handleEmailBlur/handleSubmit below): the prompt is
|
||||
// conditionally rendered on `showLogin`, so loginGateRef.current is
|
||||
// still null until after the next render flushes. Effect fires post-
|
||||
// render instead, once the ref is actually attached. A no-op on the
|
||||
// common blur-triggered path in practice — the prompt renders right
|
||||
// under the email field the shopper is already looking at, already in
|
||||
// view — but still correct/harmless there too.
|
||||
useEffect(() => {
|
||||
if (showLogin) loginGateRef.current?.scrollIntoView({ behavior: "smooth", block: "center" });
|
||||
}, [showLogin]);
|
||||
// Per-field inline validation, populated on blur (see each field's own
|
||||
// onBlur below) — surfaces the same plausibility checks the pattern/
|
||||
// required attributes already declare, immediately instead of only at
|
||||
@@ -416,7 +435,13 @@ export function CheckoutContent({
|
||||
const res = await fetch("/api/account/login", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ email: loginEmail, password: loginPassword }),
|
||||
// Card 1's own `email` state, not a separate re-typed value — the
|
||||
// login prompt now renders inline right under that same field (see
|
||||
// "1. Rechnungsadresse" below), so asking for the email a second
|
||||
// time would just be redundant. Whatever's currently in the field
|
||||
// is authoritative; the server rejects it the normal way if it
|
||||
// doesn't match an account.
|
||||
body: JSON.stringify({ email, password: loginPassword }),
|
||||
});
|
||||
const data = await res.json();
|
||||
if (!data.ok) {
|
||||
@@ -458,7 +483,6 @@ export function CheckoutContent({
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.exists) {
|
||||
setLoginEmail(email);
|
||||
setShowLogin(true);
|
||||
}
|
||||
} catch {
|
||||
@@ -522,13 +546,15 @@ export function CheckoutContent({
|
||||
if (!data.ok) {
|
||||
// The email typed into Card 1 already belongs to an existing
|
||||
// account — registering was never going to work here. Switch
|
||||
// straight to the login toggle with that email pre-filled instead
|
||||
// of just showing an error with no clear next step; the customer
|
||||
// only needs to add their password and resubmit.
|
||||
// straight to the login prompt instead of just showing an error
|
||||
// with no clear next step; the customer only needs to add their
|
||||
// password and resubmit. The scroll-into-view (for a shopper who
|
||||
// filled the whole form and hit submit without ever blurring the
|
||||
// email field, e.g. autofill) happens in the loginGateRef effect
|
||||
// below, not here — this div doesn't exist in the DOM yet at this
|
||||
// exact point, `showLogin` only flips it in after the next render.
|
||||
if (data.emailExists) {
|
||||
setLoginEmail(body.email);
|
||||
setShowLogin(true);
|
||||
accountGateRef.current?.scrollIntoView({ behavior: "smooth", block: "center" });
|
||||
}
|
||||
setPurchaseError(data.reason || "Die Bestellung konnte nicht abgeschlossen werden.");
|
||||
setPurchasing(false);
|
||||
@@ -611,67 +637,24 @@ 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. 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 ? (
|
||||
{/* Only the passive "already logged in" state stays up here — the
|
||||
reactive "this email already has an account, please log in"
|
||||
prompt used to render in this same spot too, which meant it
|
||||
could pop in well above wherever the shopper had scrolled to
|
||||
fill in Card 1's email field, mobile especially (no scroll-to
|
||||
was even wired up for the common case — only the submit-time
|
||||
fallback further down had one). Moved inline right under Card
|
||||
1's own email field instead (see "1. Rechnungsadresse" below) —
|
||||
it now appears exactly where the shopper's attention already
|
||||
is, no scrolling needed either way. */}
|
||||
{customerEmail && (
|
||||
<p className="text-body-sm text-text-primary">
|
||||
Eingeloggt als <span className="font-bold">{customerEmail}</span>{" "}
|
||||
<button type="button" onClick={handleLogout} className="underline hover:text-brand transition-colors">
|
||||
Abmelden
|
||||
</button>
|
||||
</p>
|
||||
) : 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={() => {
|
||||
setShowLogin(false);
|
||||
setLoginError(null);
|
||||
}}
|
||||
className="text-label text-text-muted underline hover:text-text-primary transition-colors"
|
||||
>
|
||||
Andere E-Mail-Adresse verwenden
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
)}
|
||||
</Reveal>
|
||||
|
||||
<form onSubmit={handleSubmit} className="flex flex-col lg:flex-row gap-8 lg:gap-10 items-start pb-10 pt-2 px-[var(--layout-padding-x)] w-full">
|
||||
@@ -782,9 +765,53 @@ export function CheckoutContent({
|
||||
error={fieldErrors.email}
|
||||
wrapperClassName="w-full sm:w-[calc(50%-0.5rem)] sm:flex-none min-w-0"
|
||||
/>
|
||||
{/* Only needed for the inline-registration path — an existing
|
||||
session already has an account, no password to collect. */}
|
||||
{!customerEmail && (
|
||||
{/* Not needed once already logged in (existing session, no
|
||||
password to collect) — and swapped for the inline login
|
||||
prompt below instead of the "create a new account" password
|
||||
field the moment handleEmailBlur/handleSubmit's own
|
||||
emailExists fallback detects the typed email already
|
||||
belongs to an account. Rendered right under the email field
|
||||
itself (previously a separate block all the way at the top
|
||||
of the page, above the form — easy to lose track of once
|
||||
scrolled down to fill in Card 1, especially on mobile,
|
||||
since nothing auto-scrolled to it on the common
|
||||
blur-triggered path either). */}
|
||||
{!customerEmail && (showLogin ? (
|
||||
<div ref={loginGateRef} className="flex flex-col gap-2 w-full sm:w-[calc(50%-0.5rem)] sm:flex-none min-w-0">
|
||||
<p className="text-label text-text-muted">
|
||||
Für diese E-Mail-Adresse existiert bereits ein Konto.
|
||||
</p>
|
||||
<FormField
|
||||
label="Passwort"
|
||||
type="password"
|
||||
value={loginPassword}
|
||||
onChange={(e) => setLoginPassword(e.target.value)}
|
||||
autoComplete="current-password"
|
||||
wrapperClassName="w-full"
|
||||
/>
|
||||
<div className="flex items-center gap-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleLogin}
|
||||
disabled={loggingIn}
|
||||
className={`px-5 py-2.5 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>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setShowLogin(false);
|
||||
setLoginError(null);
|
||||
}}
|
||||
className="text-label text-text-muted underline hover:text-text-primary transition-colors"
|
||||
>
|
||||
Andere E-Mail-Adresse verwenden
|
||||
</button>
|
||||
</div>
|
||||
{loginError && <p className="text-label text-red-600">{loginError}</p>}
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col gap-2 w-full sm:w-[calc(50%-0.5rem)] sm:flex-none min-w-0">
|
||||
<FormField
|
||||
label="Passwort (für dein neues Konto)"
|
||||
@@ -822,7 +849,7 @@ export function CheckoutContent({
|
||||
einsehen und bei Bedarf stornieren oder zurücksenden kannst.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
))}
|
||||
{/* Always a plain street address — Packstation isn't a valid
|
||||
Rechnungsadresse (an invoice needs a real postal address).
|
||||
Packstation is only ever offered below, in the optional
|
||||
|
||||
Reference in New Issue
Block a user