feat(auth): welcome/landing page + email-password login UI (5b)

Replace the old Google-only login card with a public landing page (Welcome.tsx):
hero + auth card (email+password Sign in / Create account / Forgot password,
Continue with Google, explicit Try-the-demo) over a feature pitch (6 pain->solution
cards) and demo-account screenshot slots (graceful placeholder until the images are
dropped into frontend/public/welcome/). Auth forms wire to the 5a endpoints; errors
show inline via a new req() quiet flag (no global modal for a wrong password).
Handles ?verify, ?reset (set-new-password form), ?access banners. EN/HU/DE.
Verified end-to-end on a fresh load: render, register->approve->password-login.
This commit is contained in:
npeter83 2026-06-19 15:15:58 +02:00
parent 732acfce52
commit a81734b4d6
7 changed files with 602 additions and 124 deletions

View file

@ -194,6 +194,9 @@ const delay = (ms: number) => new Promise((res) => window.setTimeout(res, ms));
// are safe to repeat). Used to recover from the keepalive race without surfacing a 502.
interface ReqConfig {
idempotent?: boolean;
// Suppress the global error modal for 4xx/5xx so the caller can show the error inline
// (used by the auth forms — a wrong password shouldn't pop a blocking dialog).
quiet?: boolean;
}
async function req(url: string, opts: RequestInit = {}, cfg: ReqConfig = {}): Promise<any> {
@ -248,6 +251,8 @@ async function req(url: string, opts: RequestInit = {}, cfg: ReqConfig = {}): Pr
// 400/409/422 (validation/conflict). 401/403/404 are caller-handled control flow.
if (RETRIABLE_GATEWAY.has(r.status)) {
markConnectivityLost();
} else if (cfg.quiet) {
/* caller handles the error inline — no global modal */
} else if (r.status >= 500) {
reportError(detail || `${i18n.t("errors.server")} (${r.status})`);
} else if (r.status === 400 || r.status === 409 || r.status === 422) {
@ -608,6 +613,15 @@ export const api = {
body: JSON.stringify({ revalidate_batch: revalidateBatch }),
}),
// --- auth: email + password (errors handled inline via quiet) ---
register: (email: string, password: string): Promise<{ status: string }> =>
req("/auth/register", { method: "POST", body: JSON.stringify({ email, password }) }, { quiet: true }),
passwordLogin: (email: string, password: string): Promise<{ ok: boolean }> =>
req("/auth/password-login", { method: "POST", body: JSON.stringify({ email, password }) }, { quiet: true }),
requestPasswordReset: (email: string): Promise<{ status: string }> =>
req("/auth/password-reset/request", { method: "POST", body: JSON.stringify({ email }) }, { quiet: true }),
confirmPasswordReset: (token: string, password: string): Promise<{ ok: boolean }> =>
req("/auth/password-reset/confirm", { method: "POST", body: JSON.stringify({ token, password }) }, { quiet: true }),
// --- onboarding / admin ---
requestAccess: (email: string): Promise<{ status: string }> =>
req("/auth/request-access", { method: "POST", body: JSON.stringify({ email }) }),