nomadweb/frontend/src/app/login/page.tsx
eric bdf0516197 Fix dating match gate and expand seed data for usable demos.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-04 06:18:25 -05:00

166 lines
6.0 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client";
import { useState } from "react";
import Link from "next/link";
import { useRouter, useSearchParams } from "next/navigation";
import { Suspense } from "react";
import { useAuth } from "@/lib/auth";
import { useToast } from "@/lib/toast";
import { useI18n } from "@/lib/i18n";
import { resolveAuthNext } from "@/lib/authNext";
import SiteShell from "@/components/SiteShell";
function LoginForm() {
const { login, register, demoLogin } = useAuth();
const { toast } = useToast();
const { t } = useI18n();
const router = useRouter();
const searchParams = useSearchParams();
const meta = resolveAuthNext(searchParams.get("next"), t.login);
const [mode, setMode] = useState<"login" | "register">("login");
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [name, setName] = useState("");
const [showPwd, setShowPwd] = useState(false);
const [error, setError] = useState("");
const [loading, setLoading] = useState(false);
const goAfterAuth = (msg: string) => {
toast(msg, "success", { href: meta.path, label: meta.label });
setTimeout(() => { router.replace(meta.path); }, 400);
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError("");
if (mode === "register") {
if (!name.trim() || name.trim().length < 2) {
setError(t.login.nameMin);
return;
}
if (password.length < 6) {
setError(t.login.pwdMin);
return;
}
}
setLoading(true);
const ok = mode === "login"
? await login(email, password)
: await register(email, password, name.trim());
setLoading(false);
if (ok) {
goAfterAuth(
mode === "login" ? meta.welcomeToast : t.login.registerToast
);
} else {
setError(mode === "login" ? t.login.loginError : t.login.registerError);
}
};
const handleDemo = async () => {
setLoading(true);
const ok = await demoLogin();
setLoading(false);
if (ok) goAfterAuth(t.login.demoToast);
};
const headerHint = mode === "login" ? meta.hint : t.login.registerHint;
return (
<div className="auth-page">
<div className="auth-bg-shapes" aria-hidden="true">
<span className="auth-shape auth-shape-1">🌍</span>
<span className="auth-shape auth-shape-2">✈️</span>
<span className="auth-shape auth-shape-3">🏝️</span>
</div>
<div className="auth-card">
<Link href="/" className="auth-back">{t.common.backHome}</Link>
<div className="auth-header">
<span className="auth-logo-emoji">🌍</span>
<h1>{mode === "login" ? t.login.welcome : t.login.join}</h1>
<p>{headerHint}</p>
</div>
{meta.showRedirect && (
<div className="auth-redirect-card">
{meta.showPlanMerge && (
<p className="auth-redirect-merge">{t.login.planMergeNote}</p>
)}
<p className="auth-redirect-dest">
<span>{t.login.redirectTo}</span>
<strong className="auth-redirect-badge">{meta.label}</strong>
</p>
</div>
)}
<div className="auth-tabs">
<button className={mode === "login" ? "active" : ""} onClick={() => { setMode("login"); setError(""); }}>{t.login.tabLogin}</button>
<button className={mode === "register" ? "active" : ""} onClick={() => { setMode("register"); setError(""); }}>{t.login.tabRegister}</button>
</div>
<form onSubmit={handleSubmit} className="auth-form">
{mode === "register" && (
<div className="calc-field">
<label>{t.login.nickname}</label>
<input value={name} onChange={(e) => setName(e.target.value)} placeholder={t.login.nickPlaceholder} required />
</div>
)}
<div className="calc-field">
<label>{t.login.email}</label>
<input type="email" value={email} onChange={(e) => setEmail(e.target.value)} placeholder="you@example.com" required autoComplete="email" />
</div>
<div className="calc-field">
<label>{t.login.password}</label>
<div className="auth-pwd-wrap">
<input
type={showPwd ? "text" : "password"}
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder={t.login.pwdPlaceholder}
required
minLength={6}
autoComplete={mode === "login" ? "current-password" : "new-password"}
/>
<button type="button" className="auth-pwd-toggle" onClick={() => setShowPwd(!showPwd)} aria-label={t.login.showPwd}>
{showPwd ? "🙈" : "👁️"}
</button>
</div>
{mode === "register" && (
<p className="auth-pwd-hint">
{password.length === 0
? t.login.pwdHint
: password.length < 6
? t.login.pwdNeed.replace("{n}", String(6 - password.length))
: password.length < 10
? t.login.pwdOk
: t.login.pwdStrong}
</p>
)}
</div>
{error && <p className="auth-error">{error}</p>}
<button type="submit" className="btn btn-primary auth-submit" disabled={loading}>
{loading ? <span className="auth-loading">{t.login.processing}</span> : (mode === "login" ? meta.submitLogin : t.login.submitRegister)}
</button>
</form>
<div className="auth-divider"><span>{t.login.or}</span></div>
<button className="btn btn-ghost auth-demo-btn" type="button" onClick={handleDemo} disabled={loading}>
{t.login.demo}
</button>
<p className="auth-hint">{meta.demoHint}</p>
</div>
</div>
);
}
export default function LoginPage() {
return (
<SiteShell>
<Suspense fallback={<div className="auth-page"><p className="plan-loading">加载中…</p></div>}>
<LoginForm />
</Suspense>
</SiteShell>
);
}