Files
MineGeo/components/AccountSettings.tsx

153 lines
4.5 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 } from "react";
import { useRouter } from "next/navigation";
import { UserRound, LockKeyhole, Mail } from "lucide-react";
import { api } from "./StartGame";
import { PasswordForm } from "./PasswordForm";
import { displayNameSchema } from "@/shared/account";
export function AccountSettings({
email,
displayName,
passwordUpdated = false,
}: {
email: string;
displayName: string;
passwordUpdated?: boolean;
}) {
const [name, setName] = useState(displayName),
[message, setMessage] = useState(""),
[error, setError] = useState(""),
[busy, setBusy] = useState(false);
const [recoveryMessage, setRecoveryMessage] = useState(""),
[recoveryError, setRecoveryError] = useState(""),
[sending, setSending] = useState(false);
const router = useRouter();
async function save(event: React.FormEvent) {
event.preventDefault();
setError("");
setMessage("");
const valid = displayNameSchema.safeParse(name);
if (!valid.success) {
setError(valid.error.issues[0].message);
return;
}
setBusy(true);
try {
const data = await api<{ profile: { display_name: string } }>(
"profile",
{ displayName: valid.data },
"PATCH",
);
setName(data.profile.display_name);
setMessage("Display name saved.");
router.refresh();
} catch (e) {
setError((e as Error).message);
} finally {
setBusy(false);
}
}
async function recover() {
setSending(true);
setRecoveryError("");
try {
const data = await api<{ message: string }>("auth", {
action: "recover",
email,
});
setRecoveryMessage(data.message);
} catch (e) {
setRecoveryError((e as Error).message);
} finally {
setSending(false);
}
}
return (
<div className="settings-grid">
<section className="settings-card">
<div className="settings-heading">
<UserRound size={22} />
<div>
<h2>Your public profile.</h2>
<p className="muted">A name for the maps and places you share.</p>
</div>
</div>
<form onSubmit={save}>
<label className="field">
Display name
<input
name="displayName"
autoComplete="nickname"
value={name}
onChange={(e) => setName(e.target.value)}
minLength={2}
maxLength={32}
required
disabled={busy}
/>
<small>
2–32 characters. Display names do not have to be unique.
</small>
</label>
<div className="account-email">
<Mail size={17} />
<div>
<span>Email address</span>
<strong>{email}</strong>
<small>Used for sign-in and recovery. Only you can see it.</small>
</div>
</div>
{error && (
<p role="alert" className="error-message">
{error}
</p>
)}
{message && (
<p role="status" className="success-message">
{message}
</p>
)}
<button className="button primary" disabled={busy}>
{busy ? "Saving…" : "Save display name"}
</button>
</form>
</section>
<section className="settings-card">
<div className="settings-heading">
<LockKeyhole size={22} />
<div>
<h2>Password & security.</h2>
<p className="muted">Keep your account yours.</p>
</div>
</div>
{passwordUpdated && (
<p role="status" className="success-message">
Your password has been updated.
</p>
)}
<PasswordForm />
<div className="settings-recovery">
<h3>Signed up with an email link?</h3>
<p className="muted">
Set your first password, or recover a forgotten one, using a link
sent to your account email.
</p>
<button className="button" disabled={sending} onClick={recover}>
{sending ? "Sending…" : "Email me a password reset link"}
</button>
{recoveryMessage && (
<p role="status" className="success-message">
{recoveryMessage}
</p>
)}
{recoveryError && (
<p role="alert" className="error-message">
{recoveryError}
</p>
)}
</div>
</section>
</div>
);
}