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

148
components/PasswordForm.tsx Normal file
View File

@@ -0,0 +1,148 @@
"use client";
import { useState } from "react";
import { useRouter } from "next/navigation";
import { createBrowserClient } from "@supabase/ssr";
import { passwordUpdateSchema } from "@/shared/account";
export function PasswordForm({ recovery = false }: { recovery?: boolean }) {
const [current, setCurrent] = useState(""),
[password, setPassword] = useState(""),
[confirmation, setConfirmation] = useState(""),
[nonce, setNonce] = useState(""),
[needsNonce, setNeedsNonce] = useState(false),
[error, setError] = useState(""),
[message, setMessage] = useState(""),
[busy, setBusy] = useState(false);
const router = useRouter();
async function submit(event: React.FormEvent) {
event.preventDefault();
setError("");
setMessage("");
const body = {
password,
...(!recovery ? { currentPassword: current } : {}),
...(needsNonce ? { nonce } : {}),
};
const valid = passwordUpdateSchema.safeParse(body);
if (!valid.success) {
setError(valid.error.issues[0].message);
return;
}
if (password !== confirmation) {
setError("Your passwords do not match.");
return;
}
setBusy(true);
try {
const response = await fetch("/api/v1/auth/password", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(valid.data),
signal: AbortSignal.timeout(30000),
});
const data = await response.json();
if (!response.ok) {
if (data.needsReauthentication) {
const client = createBrowserClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!,
);
const { error } = await client.auth.reauthenticate();
if (error) throw error;
setNeedsNonce(true);
setMessage(
"Check your email for a verification code, then enter it below to finish.",
);
return;
}
throw new Error(data.error || "Could not update your password");
}
setCurrent("");
setPassword("");
setConfirmation("");
setNonce("");
setNeedsNonce(false);
setMessage(data.message);
if (recovery) {
router.replace("/settings?password=updated");
router.refresh();
}
} catch (e) {
setError((e as Error).message);
} finally {
setBusy(false);
}
}
return (
<form onSubmit={submit} aria-busy={busy} className="password-form">
{!recovery && (
<label className="field">
Current password
<input
name="currentPassword"
type="password"
autoComplete="current-password"
value={current}
onChange={(e) => setCurrent(e.target.value)}
required
disabled={busy}
/>
</label>
)}
<label className="field">
New password
<input
name="newPassword"
type="password"
autoComplete="new-password"
minLength={12}
value={password}
onChange={(e) => setPassword(e.target.value)}
required
disabled={busy}
/>
<small>
At least 12 characters. Use a unique password or passphrase.
</small>
</label>
<label className="field">
Confirm new password
<input
name="confirmPassword"
type="password"
autoComplete="new-password"
value={confirmation}
onChange={(e) => setConfirmation(e.target.value)}
required
disabled={busy}
/>
</label>
{needsNonce && (
<label className="field">
Email verification code
<input
inputMode="numeric"
autoComplete="one-time-code"
value={nonce}
onChange={(e) => setNonce(e.target.value)}
required
disabled={busy}
/>
</label>
)}
{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 ? "Updating…" : recovery ? "Set new password" : "Update password"}
</button>
</form>
);
}