Files
jiang13-bbs/frontend/app/register/RegisterClient.tsx

226 lines
7.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import { useState, useEffect } from "react";
import { useRouter } from "next/navigation";
import Link from "next/link";
import { UserPlus } from "lucide-react";
import { apiRegister, apiMe, apiOperations } from "@/lib/api";
import { hasAuthCookieHint } from "@/lib/cookies";
/** 关闭注册时的说明页(提交失败切到关闭态时复用) */
export function RegisterClosedPanel({ notice }: { notice: string }) {
return (
<div className="max-w-md mx-auto pt-8 sm:pt-14">
<div className="panel p-8 text-center">
<h1 className="text-lg font-extrabold" style={{ color: "var(--ink)" }}>
暂不开放注册
</h1>
<p className="meta mt-2 text-[13px]">
{notice || "站点当前已关闭新用户注册,已有账号可直接登录。"}
</p>
<Link href="/login" className="btn btn-primary mt-6">
去登录
</Link>
</div>
</div>
);
}
export default function RegisterClient({
verifyEmail: initialVerifyEmail,
registerNotice,
}: {
verifyEmail: boolean;
registerNotice: string;
}) {
const router = useRouter();
const [username, setUsername] = useState("");
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [error, setError] = useState("");
const [loading, setLoading] = useState(false);
const [registerClosed, setRegisterClosed] = useState(false);
const [code, setCode] = useState("");
const [notice, setNotice] = useState(registerNotice);
const [sending, setSending] = useState(false);
const [sentMessage, setSentMessage] = useState("");
const verifyEmail = initialVerifyEmail;
// 已登录用户不必再注册,直接回首页
useEffect(() => {
if (!hasAuthCookieHint()) return;
apiMe()
.then((res) => {
if (res.user) router.replace("/");
})
.catch(() => {});
}, [router]);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError("");
setLoading(true);
try {
const res = await apiRegister(username, email, password, code);
if (res.user) {
router.push("/");
router.refresh();
} else if (res.id) {
router.push("/login");
} else {
const msg = res.error || "注册失败";
if (msg.includes("关闭注册")) {
// 热关闭:补拉提示文案后切关闭态
try {
const s = await apiOperations<{ register_notice: string }>("/api/site-state");
setNotice(s.register_notice || notice);
} catch {
/* 沿用 SSR 提示 */
}
setRegisterClosed(true);
}
setError(msg);
}
} finally {
setLoading(false);
}
};
if (registerClosed) {
return <RegisterClosedPanel notice={notice} />;
}
return (
<div className="max-w-md mx-auto pt-8 sm:pt-14">
<div className="mb-7 text-center">
<span
className="inline-flex w-14 h-14 rounded-2xl items-center justify-center mb-4"
style={{ background: "var(--accent-soft)", color: "var(--accent)" }}
aria-hidden
>
<UserPlus size={26} />
</span>
<h1 className="text-[28px] font-extrabold tracking-tight" style={{ color: "var(--ink)" }}>
创建账号
</h1>
<p className="text-sm mt-2" style={{ color: "var(--ink-2)" }}>
加入社区,开始你的第一帖
</p>
</div>
<form onSubmit={handleSubmit} className="panel p-6 sm:p-7 space-y-4">
{error && (
<div className="alert-error" role="alert">
{error}
</div>
)}
<div>
<label className="field-label" htmlFor="reg-username">
用户名
</label>
<input
id="reg-username"
type="text"
value={username}
onChange={(e) => setUsername(e.target.value)}
required
minLength={3}
maxLength={32}
autoFocus
autoComplete="username"
className="field"
placeholder="3–32 位"
/>
</div>
<div>
<label className="field-label" htmlFor="reg-email">
{verifyEmail ? "邮箱(必填)" : "邮箱(可选)"}
</label>
<input
id="reg-email"
required={verifyEmail}
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
autoComplete="email"
className="field"
placeholder="you@example.com"
/>
</div>
{verifyEmail && (
<div>
<label className="field-label" htmlFor="reg-code">
邮箱验证码
</label>
<div className="flex gap-2 items-stretch">
<input
id="reg-code"
className="field min-w-0 flex-1"
value={code}
onChange={(e) => setCode(e.target.value)}
required
autoComplete="one-time-code"
inputMode="numeric"
placeholder="6 位验证码"
/>
<button
type="button"
className="btn btn-line shrink-0"
disabled={sending || !email}
onClick={async () => {
setSending(true);
setError("");
try {
const r = await apiOperations<{ message: string }>("/api/auth/code", "POST", {
email,
purpose: "register",
});
setSentMessage(r.message);
} catch (err) {
setError((err as Error).message);
} finally {
setSending(false);
}
}}
>
{sending ? "正在排队…" : "发送验证码"}
</button>
</div>
{sentMessage && (
<p className="meta mt-2" role="status">
{sentMessage}
</p>
)}
</div>
)}
<div>
<label className="field-label" htmlFor="reg-password">
密码
</label>
<input
id="reg-password"
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
minLength={6}
autoComplete="new-password"
className="field"
placeholder="至少 6 位"
/>
</div>
<button type="submit" disabled={loading} className="btn btn-primary w-full justify-center mt-2">
{loading ? "注册中..." : "注册"}
</button>
</form>
<p className="meta text-center mt-5">
已有账号?
<Link href="/login" className="ml-1 font-semibold" style={{ color: "var(--accent)" }}>
去登录
</Link>
</p>
</div>
);
}