Improve MineGeo exploration and mapping features
This commit is contained in:
@@ -1,69 +1,267 @@
|
||||
"use client";
|
||||
import { useState } from "react";
|
||||
import { createBrowserClient } from "@supabase/ssr";
|
||||
import { Mail } from "lucide-react";
|
||||
export function SignIn() {
|
||||
const [email, setEmail] = useState(""),
|
||||
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 [mode, setMode] = useState<Mode>("login"),
|
||||
[email, setEmail] = useState(""),
|
||||
[name, setName] = useState(""),
|
||||
[password, setPassword] = useState(""),
|
||||
[confirmation, setConfirmation] = useState(""),
|
||||
[message, setMessage] = useState(""),
|
||||
[error, setError] = 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();
|
||||
setBusy(true);
|
||||
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 client = createBrowserClient(
|
||||
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
||||
process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!,
|
||||
);
|
||||
const { error } = await client.auth.signInWithOtp({
|
||||
email,
|
||||
options: { emailRedirectTo: window.location.origin + "/auth/callback" },
|
||||
});
|
||||
if (error) throw error;
|
||||
setMessage(
|
||||
"Check your inbox for a sign-in link. You can return here after opening it.",
|
||||
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-card">
|
||||
<span className="eyebrow">YOUR CARTOGRAPHER ACCOUNT</span>
|
||||
<h2>Welcome, explorer.</h2>
|
||||
<p className="muted">
|
||||
Sign in to share maps and keep your expeditions. No password needed.
|
||||
</p>
|
||||
<form onSubmit={submit}>
|
||||
<label className="field">
|
||||
Email address
|
||||
<input
|
||||
type="email"
|
||||
autoComplete="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
placeholder="you@example.com"
|
||||
required
|
||||
/>
|
||||
</label>
|
||||
<button className="button primary full" disabled={busy}>
|
||||
<Mail size={16} />
|
||||
{busy ? "Sending…" : "Send me a sign-in link"}
|
||||
</button>
|
||||
</form>
|
||||
{message && (
|
||||
<p role="status" className="success-message">
|
||||
{message}
|
||||
<div className="auth-layout">
|
||||
<aside className="auth-story">
|
||||
<span className="eyebrow">YOUR NEXT EXPEDITION</span>
|
||||
<h1>
|
||||
A familiar world.
|
||||
<br />
|
||||
Your own perspective.
|
||||
</h1>
|
||||
<p>
|
||||
Keep your journeys, share the places you know, and help build the next
|
||||
map.
|
||||
</p>
|
||||
)}
|
||||
{error && (
|
||||
<p role="alert" className="error-message">
|
||||
{error}
|
||||
<div className="auth-story-map" aria-hidden="true">
|
||||
<Compass size={52} />
|
||||
<span>Every world has a story.</span>
|
||||
</div>
|
||||
<div className="auth-benefits">
|
||||
<span>
|
||||
<Compass size={18} />
|
||||
Save your expeditions
|
||||
</span>
|
||||
<span>
|
||||
<ShieldCheck size={18} />
|
||||
Keep control of your maps
|
||||
</span>
|
||||
</div>
|
||||
</aside>
|
||||
<section className="auth-card">
|
||||
<div className="auth-tabs" aria-label="Account options">
|
||||
<button
|
||||
type="button"
|
||||
aria-pressed={mode === "login"}
|
||||
disabled={busy}
|
||||
onClick={() => changeMode("login")}
|
||||
>
|
||||
Log in
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
aria-pressed={mode === "signup"}
|
||||
disabled={busy}
|
||||
onClick={() => changeMode("signup")}
|
||||
>
|
||||
Create account
|
||||
</button>
|
||||
</div>
|
||||
<span className="eyebrow">YOUR CARTOGRAPHER ACCOUNT</span>
|
||||
<h2>{title}</h2>
|
||||
<p className="muted">
|
||||
{mode === "signup"
|
||||
? "Choose the name people will see on your maps."
|
||||
: mode === "recover"
|
||||
? "We’ll email you a link to set a new password."
|
||||
: mode === "magic"
|
||||
? "For existing accounts. Open the link on this device to continue."
|
||||
: "Log in with your email and password."}
|
||||
</p>
|
||||
)}
|
||||
<form onSubmit={submit} aria-busy={busy}>
|
||||
{mode === "signup" && (
|
||||
<label className="field">
|
||||
Display name
|
||||
<input
|
||||
name="displayName"
|
||||
autoComplete="nickname"
|
||||
minLength={2}
|
||||
maxLength={32}
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
placeholder="How should we call you?"
|
||||
required
|
||||
disabled={busy}
|
||||
/>
|
||||
<small>2–32 characters. You can change it in settings.</small>
|
||||
</label>
|
||||
)}
|
||||
<label className="field">
|
||||
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">
|
||||
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>
|
||||
Use at least 12 characters. A unique passphrase works well.
|
||||
</small>
|
||||
)}
|
||||
</label>
|
||||
)}
|
||||
{mode === "signup" && (
|
||||
<label className="field">
|
||||
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")}
|
||||
>
|
||||
Forgot password?
|
||||
</button>
|
||||
)}
|
||||
{error && (
|
||||
<p role="alert" className="error-message">
|
||||
{error}
|
||||
</p>
|
||||
)}
|
||||
{message && (
|
||||
<p role="status" className="success-message">
|
||||
{message}
|
||||
</p>
|
||||
)}
|
||||
<button className="button primary full" disabled={busy}>
|
||||
{mode === "recover" || mode === "magic" ? (
|
||||
<Mail size={17} />
|
||||
) : (
|
||||
<ArrowRight size={17} />
|
||||
)}{" "}
|
||||
{busy
|
||||
? "Please wait…"
|
||||
: {
|
||||
login: "Log in",
|
||||
signup: "Create account",
|
||||
recover: "Send reset link",
|
||||
magic: "Send sign-in link",
|
||||
}[mode]}
|
||||
</button>
|
||||
</form>
|
||||
{mode === "login" && (
|
||||
<div className="auth-alternative">
|
||||
<span>Already use email links?</span>
|
||||
<button
|
||||
type="button"
|
||||
className="text-link"
|
||||
disabled={busy}
|
||||
onClick={() => changeMode("magic")}
|
||||
>
|
||||
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")}
|
||||
>
|
||||
← Back to log in
|
||||
</button>
|
||||
)}
|
||||
<p className="auth-footnote">
|
||||
Your email stays private. Your display name appears with your maps.
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user