166 lines
6.0 KiB
TypeScript
166 lines
6.0 KiB
TypeScript
"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>
|
||
);
|
||
}
|