Improve MineGeo exploration and mapping features

This commit is contained in:
2026-10-05 13:41:36 +05:00
parent aec0641f87
commit c5368fa8cc
69 changed files with 3581 additions and 333 deletions

View File

@@ -0,0 +1,152 @@
"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>
);
}