164 lines
5.6 KiB
TypeScript
164 lines
5.6 KiB
TypeScript
|
|
// ============================================================
|
|||
|
|
// LoginModal — 登录 / 注册模态框
|
|||
|
|
// 职责:在 LandingPage 上弹出的认证表单,复用现有 auth 系统
|
|||
|
|
// ============================================================
|
|||
|
|
|
|||
|
|
import { useCallback, useEffect, useRef, useState } from "react";
|
|||
|
|
import { useAuth } from "../../lib/auth";
|
|||
|
|
import { useI18n } from "../../lib/i18n";
|
|||
|
|
|
|||
|
|
type AuthMode = "login" | "register";
|
|||
|
|
|
|||
|
|
interface LoginModalProps {
|
|||
|
|
initialMode?: AuthMode;
|
|||
|
|
onClose: () => void;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export function LoginModal({ initialMode = "login", onClose }: LoginModalProps) {
|
|||
|
|
const { login, register } = useAuth();
|
|||
|
|
const { t } = useI18n();
|
|||
|
|
const [mode, setMode] = useState<AuthMode>(initialMode);
|
|||
|
|
const [username, setUsername] = useState("");
|
|||
|
|
const [password, setPassword] = useState("");
|
|||
|
|
const [error, setError] = useState("");
|
|||
|
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
|||
|
|
const overlayRef = useRef<HTMLDivElement>(null);
|
|||
|
|
|
|||
|
|
// 点击遮罩关闭
|
|||
|
|
const handleOverlayClick = useCallback(
|
|||
|
|
(e: React.MouseEvent) => {
|
|||
|
|
if (e.target === overlayRef.current) onClose();
|
|||
|
|
},
|
|||
|
|
[onClose]
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
// ESC 关闭
|
|||
|
|
useEffect(() => {
|
|||
|
|
const handler = (e: KeyboardEvent) => {
|
|||
|
|
if (e.key === "Escape") onClose();
|
|||
|
|
};
|
|||
|
|
window.addEventListener("keydown", handler);
|
|||
|
|
return () => window.removeEventListener("keydown", handler);
|
|||
|
|
}, [onClose]);
|
|||
|
|
|
|||
|
|
// 阻止 body 滚动
|
|||
|
|
useEffect(() => {
|
|||
|
|
const prev = document.body.style.overflow;
|
|||
|
|
document.body.style.overflow = "hidden";
|
|||
|
|
return () => { document.body.style.overflow = prev; };
|
|||
|
|
}, []);
|
|||
|
|
|
|||
|
|
const handleSubmit = useCallback(
|
|||
|
|
async (e: React.FormEvent) => {
|
|||
|
|
e.preventDefault();
|
|||
|
|
setError("");
|
|||
|
|
|
|||
|
|
if (username.length < 3 || username.length > 64) {
|
|||
|
|
setError(t("auth.error.usernameLength"));
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
if (password.length < 8 || password.length > 72) {
|
|||
|
|
setError(t("auth.error.passwordLength"));
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
setIsSubmitting(true);
|
|||
|
|
const fn = mode === "login" ? login : register;
|
|||
|
|
const result = await fn(username, password);
|
|||
|
|
setIsSubmitting(false);
|
|||
|
|
|
|||
|
|
if (result.error) {
|
|||
|
|
setError(result.error);
|
|||
|
|
}
|
|||
|
|
// 登录成功时 auth 状态更新,App 自动切到主界面,modal 自然消失
|
|||
|
|
},
|
|||
|
|
[username, password, mode, login, register, t]
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
const switchMode = useCallback(() => {
|
|||
|
|
setMode((m) => (m === "login" ? "register" : "login"));
|
|||
|
|
setError("");
|
|||
|
|
}, []);
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<div className="lp-modal-overlay" ref={overlayRef} onClick={handleOverlayClick}>
|
|||
|
|
<div className="lp-modal" role="dialog" aria-modal="true">
|
|||
|
|
<div className="lp-modal__inner">
|
|||
|
|
<button type="button" className="lp-modal__close" onClick={onClose} aria-label="Close">
|
|||
|
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|||
|
|
<line x1="18" y1="6" x2="6" y2="18" />
|
|||
|
|
<line x1="6" y1="6" x2="18" y2="18" />
|
|||
|
|
</svg>
|
|||
|
|
</button>
|
|||
|
|
|
|||
|
|
<div className="lp-modal__header">
|
|||
|
|
<div className="lp-modal__logo">CamTalk</div>
|
|||
|
|
<div className="lp-modal__subtitle">{t("auth.subtitle")}</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<form onSubmit={handleSubmit}>
|
|||
|
|
<div className="lp-modal__tabs">
|
|||
|
|
<button
|
|||
|
|
type="button"
|
|||
|
|
className={`lp-modal__tab ${mode === "login" ? "lp-modal__tab--active" : ""}`}
|
|||
|
|
onClick={() => { setMode("login"); setError(""); }}
|
|||
|
|
>
|
|||
|
|
{t("auth.login")}
|
|||
|
|
</button>
|
|||
|
|
<button
|
|||
|
|
type="button"
|
|||
|
|
className={`lp-modal__tab ${mode === "register" ? "lp-modal__tab--active" : ""}`}
|
|||
|
|
onClick={() => { setMode("register"); setError(""); }}
|
|||
|
|
>
|
|||
|
|
{t("auth.register")}
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<label className="lp-modal__field">
|
|||
|
|
<span className="lp-modal__field-label">{t("auth.username")}</span>
|
|||
|
|
<input
|
|||
|
|
type="text"
|
|||
|
|
className="lp-modal__field-input"
|
|||
|
|
value={username}
|
|||
|
|
onChange={(e) => setUsername(e.target.value)}
|
|||
|
|
placeholder={t("auth.username.placeholder")}
|
|||
|
|
autoComplete="username"
|
|||
|
|
autoFocus
|
|||
|
|
/>
|
|||
|
|
</label>
|
|||
|
|
|
|||
|
|
<label className="lp-modal__field">
|
|||
|
|
<span className="lp-modal__field-label">{t("auth.password")}</span>
|
|||
|
|
<input
|
|||
|
|
type="password"
|
|||
|
|
className="lp-modal__field-input"
|
|||
|
|
value={password}
|
|||
|
|
onChange={(e) => setPassword(e.target.value)}
|
|||
|
|
placeholder={t("auth.password.placeholder")}
|
|||
|
|
autoComplete={mode === "login" ? "current-password" : "new-password"}
|
|||
|
|
/>
|
|||
|
|
</label>
|
|||
|
|
|
|||
|
|
{error && <div className="lp-modal__error">{error}</div>}
|
|||
|
|
|
|||
|
|
<button type="submit" className="lp-modal__submit" disabled={isSubmitting}>
|
|||
|
|
{isSubmitting
|
|||
|
|
? t("auth.submitting")
|
|||
|
|
: mode === "login"
|
|||
|
|
? t("auth.login")
|
|||
|
|
: t("auth.register")}
|
|||
|
|
</button>
|
|||
|
|
</form>
|
|||
|
|
|
|||
|
|
<div className="lp-modal__footer">
|
|||
|
|
{mode === "login" ? t("auth.noAccount") : t("auth.hasAccount")}
|
|||
|
|
<button type="button" className="lp-modal__link" onClick={switchMode}>
|
|||
|
|
{mode === "login" ? t("auth.register") : t("auth.login")}
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
);
|
|||
|
|
}
|