163 lines
5.2 KiB
TypeScript
163 lines
5.2 KiB
TypeScript
import { useState } from "react";
|
|
import { auth } from "../api";
|
|
import type { UserInfo } from "../types";
|
|
import "./LoginModal.css";
|
|
|
|
interface Props {
|
|
onClose: () => void;
|
|
onLogin: (user: UserInfo) => void;
|
|
}
|
|
|
|
type Tab = "login" | "register";
|
|
|
|
export default function LoginModal({ onClose, onLogin }: Props) {
|
|
const [tab, setTab] = useState<Tab>("login");
|
|
const [username, setUsername] = useState("");
|
|
const [password, setPassword] = useState("");
|
|
const [confirm, setConfirm] = useState("");
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [loading, setLoading] = useState(false);
|
|
|
|
async function handleSubmit(e: React.FormEvent) {
|
|
e.preventDefault();
|
|
setError(null);
|
|
|
|
if (tab === "register" && password !== confirm) {
|
|
setError("Passwords do not match");
|
|
return;
|
|
}
|
|
|
|
setLoading(true);
|
|
try {
|
|
if (tab === "login") {
|
|
await auth.loginLocal(username, password);
|
|
} else {
|
|
await auth.register(username, password);
|
|
}
|
|
// Cookie is now set server-side; fetch user info to update parent
|
|
const user = await auth.me();
|
|
if (user) {
|
|
onLogin(user);
|
|
onClose();
|
|
} else {
|
|
setError("Login succeeded but could not load user info.");
|
|
}
|
|
} catch (err: unknown) {
|
|
const msg = err instanceof Error ? err.message : String(err);
|
|
// Extract the human-readable part (strip leading status code)
|
|
setError(
|
|
msg
|
|
.replace(/^\d+:\s*/, "")
|
|
.replace(/\{.*\}/s, "")
|
|
.trim() || "Authentication failed",
|
|
);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}
|
|
|
|
async function handleDiscordLogin() {
|
|
try {
|
|
await auth.loginDiscord(window.location.origin + "/map");
|
|
} catch (err) {
|
|
console.error("Discord login failed:", err);
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div className="modal-backdrop" onClick={onClose}>
|
|
<div className="modal" onClick={(e) => e.stopPropagation()}>
|
|
<button className="modal-close" onClick={onClose} aria-label="Close">
|
|
✕
|
|
</button>
|
|
|
|
{/* Tab switcher */}
|
|
<div className="modal-tabs">
|
|
<button
|
|
className={`modal-tab ${tab === "login" ? "active" : ""}`}
|
|
onClick={() => {
|
|
setTab("login");
|
|
setError(null);
|
|
}}
|
|
>
|
|
Log In
|
|
</button>
|
|
<button
|
|
className={`modal-tab ${tab === "register" ? "active" : ""}`}
|
|
onClick={() => {
|
|
setTab("register");
|
|
setError(null);
|
|
}}
|
|
>
|
|
Register
|
|
</button>
|
|
</div>
|
|
|
|
{/* Username / password form */}
|
|
<form className="modal-form" onSubmit={handleSubmit}>
|
|
<label>
|
|
Username
|
|
<input
|
|
type="text"
|
|
value={username}
|
|
onChange={(e) => setUsername(e.target.value)}
|
|
autoComplete="username"
|
|
required
|
|
minLength={1}
|
|
maxLength={32}
|
|
/>
|
|
</label>
|
|
<label>
|
|
Password
|
|
<input
|
|
type="password"
|
|
value={password}
|
|
onChange={(e) => setPassword(e.target.value)}
|
|
autoComplete={
|
|
tab === "login" ? "current-password" : "new-password"
|
|
}
|
|
required
|
|
minLength={8}
|
|
/>
|
|
</label>
|
|
{tab === "register" && (
|
|
<label>
|
|
Confirm Password
|
|
<input
|
|
type="password"
|
|
value={confirm}
|
|
onChange={(e) => setConfirm(e.target.value)}
|
|
autoComplete="new-password"
|
|
required
|
|
minLength={8}
|
|
/>
|
|
</label>
|
|
)}
|
|
|
|
{error && <p className="modal-error">{error}</p>}
|
|
|
|
<button type="submit" className="btn-primary" disabled={loading}>
|
|
{loading
|
|
? "Loading…"
|
|
: tab === "login"
|
|
? "Log In"
|
|
: "Create Account"}
|
|
</button>
|
|
</form>
|
|
|
|
<div className="modal-divider">
|
|
<span>or</span>
|
|
</div>
|
|
|
|
{/* Discord OAuth */}
|
|
<button className="btn-discord" onClick={handleDiscordLogin}>
|
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
|
|
<path d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0 12.64 12.64 0 0 0-.617-1.25.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057 19.9 19.9 0 0 0 5.993 3.03.078.078 0 0 0 .084-.028 14.09 14.09 0 0 0 1.226-1.994.076.076 0 0 0-.041-.106 13.107 13.107 0 0 1-1.872-.892.077.077 0 0 1-.008-.128 10.2 10.2 0 0 0 .372-.292.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127 12.299 12.299 0 0 1-1.873.892.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028 19.839 19.839 0 0 0 6.002-3.03.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03z" />
|
|
</svg>
|
|
Log In with Discord
|
|
</button>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|