Files
CamTalk/frontend/src/components/LandingPage/LoginModal.tsx

164 lines
5.6 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.
// ============================================================
// 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>
);
}