Files
MineGeo/components/SignIn.tsx

280 lines
8.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.
"use client";
import { useI18n } from "@/components/I18nProvider";
import { useState } from "react";
import { useRouter } from "next/navigation";
import { ArrowRight, Compass, Mail, ShieldCheck } from "lucide-react";
import { api } from "./StartGame";
import { authActionSchema, authDestination } from "@/shared/account";
type Mode = "login" | "signup" | "recover" | "magic";
export function SignIn({
next = "/dashboard",
initialError = "",
}: {
next?: string;
initialError?: string;
}) {
const { t } = useI18n();
const [mode, setMode] = useState<Mode>("login"),
[email, setEmail] = useState(""),
[name, setName] = useState(""),
[password, setPassword] = useState(""),
[confirmation, setConfirmation] = useState(""),
[message, setMessage] = useState(""),
[error, setError] = useState(initialError),
[busy, setBusy] = useState(false);
const router = useRouter();
function changeMode(value: Mode) {
setMode(value);
setError("");
setMessage("");
setPassword("");
setConfirmation("");
}
async function submit(event: React.FormEvent) {
event.preventDefault();
setError("");
setMessage("");
const body =
mode === "signup"
? { action: mode, email, password, displayName: name, next }
: mode === "login"
? { action: mode, email, password, next }
: mode === "magic"
? { action: mode, email, next }
: { action: mode, email };
const valid = authActionSchema.safeParse(body);
if (!valid.success) {
setError(valid.error.issues[0].message);
return;
}
if (mode === "signup" && password !== confirmation) {
setError("Your passwords do not match.");
return;
}
setBusy(true);
try {
const result = await api<{ next?: string; message?: string }>(
"auth",
valid.data,
);
setPassword("");
setConfirmation("");
if (result.next) {
router.replace(authDestination(result.next));
router.refresh();
} else setMessage(result.message ?? "Check your email to continue.");
} catch (e) {
setError((e as Error).message);
} finally {
setBusy(false);
}
}
const title = {
login: "Welcome back.",
signup: "Make yourself at home.",
recover: "Find your way back.",
magic: "An email, a way in.",
}[mode];
return (
<div className="auth-layout">
<aside className="auth-story">
<span className="eyebrow">{t("YOUR NEXT EXPEDITION")}</span>
<h1>
{t("A familiar world.")}
<br />
{t("Your own perspective.")}
</h1>
<p>
{t(
"Keep your journeys, share the places you know, and help build the next map.",
)}
</p>
<div className="auth-story-map" aria-hidden="true">
<Compass size={52} />
<span>{t("Every world has a story.")}</span>
</div>
<div className="auth-benefits">
<span>
<Compass size={18} />
{t("Save your expeditions")}
</span>
<span>
<ShieldCheck size={18} />
{t("Keep control of your maps")}
</span>
</div>
</aside>
<section className="auth-card">
<div className="auth-tabs" aria-label={t("Account options")}>
<button
type="button"
aria-pressed={mode === "login"}
disabled={busy}
onClick={() => changeMode("login")}
>
{t("Log in")}
</button>
<button
type="button"
aria-pressed={mode === "signup"}
disabled={busy}
onClick={() => changeMode("signup")}
>
{t("Create account")}
</button>
</div>
<span className="eyebrow">{t("YOUR CARTOGRAPHER ACCOUNT")}</span>
<h2>{t(title)}</h2>
<p className="muted">
{mode === "signup"
? t("Choose the name people will see on your maps.")
: mode === "recover"
? t("We’ll email you a link to set a new password.")
: mode === "magic"
? t(
"For existing accounts. Open the link on this device to continue.",
)
: t("Log in with your email and password.")}
</p>
<form onSubmit={submit} aria-busy={busy}>
{mode === "signup" && (
<label className="field">
{t("Display name")}
<input
name="displayName"
autoComplete="nickname"
minLength={2}
maxLength={32}
value={name}
onChange={(e) => setName(e.target.value)}
placeholder={t("How should we call you?")}
required
disabled={busy}
/>
<small>
{t("2–32 characters. You can change it in settings.")}
</small>
</label>
)}
<label className="field">
{t("Email address")}
<input
name="email"
type="email"
autoComplete="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="you@example.com"
required
disabled={busy}
/>
</label>
{(mode === "login" || mode === "signup") && (
<label className="field">
{t("Password")}
<input
name="password"
type="password"
autoComplete={
mode === "signup" ? "new-password" : "current-password"
}
minLength={mode === "signup" ? 12 : 1}
value={password}
onChange={(e) => setPassword(e.target.value)}
required
disabled={busy}
/>
{mode === "signup" && (
<small>
{t(
"Use at least 12 characters. A unique passphrase works well.",
)}
</small>
)}
</label>
)}
{mode === "signup" && (
<label className="field">
{t("Confirm password")}
<input
name="confirmPassword"
type="password"
autoComplete="new-password"
value={confirmation}
onChange={(e) => setConfirmation(e.target.value)}
required
disabled={busy}
/>
</label>
)}
{mode === "login" && (
<button
type="button"
className="text-link auth-recovery"
disabled={busy}
onClick={() => changeMode("recover")}
>
{t("Forgot password?")}
</button>
)}
{error && (
<p role="alert" className="error-message">
{t(error)}
</p>
)}
{message && (
<p role="status" className="success-message">
{t(message)}
</p>
)}
<button className="button primary full" disabled={busy}>
{mode === "recover" || mode === "magic" ? (
<Mail size={17} />
) : (
<ArrowRight size={17} />
)}{" "}
{busy
? t("Please wait…")
: {
login: t("Log in"),
signup: t("Create account"),
recover: t("Send reset link"),
magic: t("Send sign-in link"),
}[mode]}
</button>
</form>
{mode === "login" && (
<div className="auth-alternative">
<span>{t("Already use email links?")}</span>
<button
type="button"
className="text-link"
disabled={busy}
onClick={() => changeMode("magic")}
>
{t("Send me a sign-in link")}
</button>
</div>
)}
{(mode === "recover" || mode === "magic") && (
<button
type="button"
className="text-link auth-back"
disabled={busy}
onClick={() => changeMode("login")}
>
{t("← Back to log in")}
</button>
)}
<p className="auth-footnote">
{t(
"Your email stays private. Your display name appears with your maps.",
)}
</p>
</section>
</div>
);
}