Files
gitlab-instance-0a899031_di…/app/components/AuthModal.tsx
2026-03-08 01:10:31 -06:00

204 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, type FormEvent } from "react";
const PB_URL = process.env.NEXT_PUBLIC_POCKETBASE_URL || "https://pocketbase.hackrobot.cn";
interface AuthModalProps {
isOpen: boolean;
onClose: () => void;
onSuccess: (email: string) => void;
}
export default function AuthModal({ isOpen, onClose, onSuccess }: AuthModalProps) {
const [mode, setMode] = useState<"login" | "register">("login");
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [passwordConfirm, setPasswordConfirm] = useState("");
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const handleSubmit = async (e: FormEvent) => {
e.preventDefault();
setError(null);
setLoading(true);
try {
if (mode === "register") {
if (password !== passwordConfirm) {
setError("两次密码不一致");
setLoading(false);
return;
}
if (password.length < 8) {
setError("密码至少 8 位");
setLoading(false);
return;
}
const res = await fetch(`${PB_URL}/api/collections/users/records`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
email,
password,
passwordConfirm,
}),
});
if (!res.ok) {
const err = await res.json();
throw new Error(err?.message || "注册失败");
}
// 注册成功后自动登录
const authRes = await fetch(`${PB_URL}/api/collections/users/auth-with-password`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ identity: email, password }),
});
if (!authRes.ok) {
throw new Error("注册成功,请手动登录");
}
const authData = await authRes.json();
if (authData?.token) {
localStorage.setItem("pb_token", authData.token);
localStorage.setItem("pb_user", JSON.stringify(authData.record));
}
} else {
const res = await fetch(`${PB_URL}/api/collections/users/auth-with-password`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ identity: email, password }),
});
if (!res.ok) {
const err = await res.json();
throw new Error(err?.message || "登录失败");
}
const data = await res.json();
if (data?.token) {
localStorage.setItem("pb_token", data.token);
localStorage.setItem("pb_user", JSON.stringify(data.record));
}
}
onSuccess(email);
onClose();
setEmail("");
setPassword("");
setPasswordConfirm("");
} catch (err) {
setError(err instanceof Error ? err.message : "操作失败");
} finally {
setLoading(false);
}
};
if (!isOpen) return null;
return (
<div className="fixed inset-0 z-[100] flex items-center justify-center">
<div
className="absolute inset-0 bg-black/50"
onClick={onClose}
aria-hidden
/>
<div className="relative w-full max-w-sm rounded-2xl bg-white p-6 shadow-xl">
<button
onClick={onClose}
className="absolute right-4 top-4 text-slate-400 hover:text-slate-600"
aria-label="关闭"
>
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
<h2 className="text-xl font-bold text-slate-800">
{mode === "login" ? "登录" : "注册"}
</h2>
<p className="mt-1 text-sm text-slate-500">
{mode === "login" ? "使用邮箱密码登录" : "创建新账号"}
</p>
<form onSubmit={handleSubmit} className="mt-6 space-y-4">
<div>
<label className="mb-1 block text-sm font-medium text-slate-700"></label>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="your@email.com"
required
className="w-full rounded-lg border border-slate-200 px-4 py-2.5 text-slate-800 placeholder-slate-400 focus:border-sky-500 focus:outline-none focus:ring-1 focus:ring-sky-500"
/>
</div>
<div>
<label className="mb-1 block text-sm font-medium text-slate-700"></label>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="至少 8 位"
required
minLength={mode === "register" ? 8 : 1}
className="w-full rounded-lg border border-slate-200 px-4 py-2.5 text-slate-800 placeholder-slate-400 focus:border-sky-500 focus:outline-none focus:ring-1 focus:ring-sky-500"
/>
</div>
{mode === "register" && (
<div>
<label className="mb-1 block text-sm font-medium text-slate-700"></label>
<input
type="password"
value={passwordConfirm}
onChange={(e) => setPasswordConfirm(e.target.value)}
placeholder="再次输入密码"
required
className="w-full rounded-lg border border-slate-200 px-4 py-2.5 text-slate-800 placeholder-slate-400 focus:border-sky-500 focus:outline-none focus:ring-1 focus:ring-sky-500"
/>
</div>
)}
{error && (
<p className="rounded-lg bg-red-50 px-3 py-2 text-sm text-red-600">{error}</p>
)}
<button
type="submit"
disabled={loading}
className="w-full rounded-lg bg-sky-500 py-3 font-semibold text-white transition-colors hover:bg-sky-600 disabled:opacity-70"
>
{loading ? "处理中…" : mode === "login" ? "登录" : "注册"}
</button>
</form>
<p className="mt-4 text-center text-sm text-slate-500">
{mode === "login" ? (
<>
{" "}
<button
type="button"
onClick={() => {
setMode("register");
setError(null);
}}
className="font-medium text-sky-500 hover:text-sky-600"
>
</button>
</>
) : (
<>
{" "}
<button
type="button"
onClick={() => {
setMode("login");
setError(null);
}}
className="font-medium text-sky-500 hover:text-sky-600"
>
</button>
</>
)}
</p>
</div>
</div>
);
}