v1.4 - added admin page and auth

This commit is contained in:
Zaldimmar 2026-09-25 02:36:49 -05:00
parent 5efdafbb97
commit 1f0aa3078f
29 changed files with 5264 additions and 217 deletions

View file

@ -0,0 +1,142 @@
/* ═══════════════════════════════════════════════════════════════
ADMIN LOGIN
Deliberately outside PageShell and the site nav. This isn't a
page of the website; it's the door to the back office, and it
shouldn't carry a banner, a subnav, or a footer site map.
The Google button is stubbed and disabled until the OAuth
routes exist. It's here so the layout doesn't change when it
starts working.
═══════════════════════════════════════════════════════════════ */
import { useState } from "react";
import { useLocation, useNavigate } from "react-router-dom";
import { useAuth } from "../../lib/auth.tsx";
import { ApiError } from "../../lib/api.js";
const GOOGLE_ENABLED = false;
const fieldClass =
"w-full rounded-xl border border-[#4a6b72]/25 bg-white px-4 py-3 text-[#26454c] " +
"outline-none transition-colors focus:border-[#138ba0] focus:ring-2 focus:ring-[#138ba0]/25";
export default function AdminLogin() {
const { login } = useAuth();
const navigate = useNavigate();
const location = useLocation();
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [error, setError] = useState(null);
const [busy, setBusy] = useState(false);
const destination = location.state?.from?.pathname ?? "/admin/feedback";
async function handleSubmit(event) {
event.preventDefault();
if (busy) return;
setBusy(true);
setError(null);
try {
await login(email.trim(), password);
navigate(destination, { replace: true });
} catch (err) {
setError(
err instanceof ApiError
? err.message
: "Couldn't reach the server. Try again in a moment.",
);
setPassword("");
setBusy(false);
}
}
return (
<div className="flex min-h-screen items-center justify-center bg-[#eef9fb] px-6 py-16">
<div className="w-full max-w-sm">
<h1 className="text-3xl font-bold text-[#138ba0]">NGU admin</h1>
<p className="mt-2 text-sm text-[#4a6b72]">
Sign in to read and triage site feedback.
</p>
<form
onSubmit={handleSubmit}
noValidate
className="mt-8 rounded-2xl border border-[#138ba0]/20 bg-white p-6"
>
<label
htmlFor="admin-email"
className="block text-sm font-medium text-[#26454c]"
>
Email
</label>
<input
id="admin-email"
type="email"
autoComplete="username"
autoFocus
value={email}
onChange={(e) => setEmail(e.target.value)}
className={`mt-2 ${fieldClass}`}
/>
<label
htmlFor="admin-password"
className="mt-5 block text-sm font-medium text-[#26454c]"
>
Password
</label>
<input
id="admin-password"
type="password"
autoComplete="current-password"
value={password}
onChange={(e) => setPassword(e.target.value)}
className={`mt-2 ${fieldClass}`}
/>
{error && (
<p
role="alert"
className="mt-5 rounded-xl border border-[#b3261e]/30 bg-[#fdf3f2] px-4 py-3 text-sm text-[#b3261e]"
>
{error}
</p>
)}
<button
type="submit"
disabled={busy || !email || !password}
className="mt-6 w-full rounded-full bg-[#138ba0] px-6 py-3 font-semibold text-white transition-colors hover:bg-[#0f7183] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#138ba0]/40 disabled:cursor-not-allowed disabled:bg-[#4a6b72]/25"
>
{busy ? "Signing in…" : "Sign in"}
</button>
{GOOGLE_ENABLED && (
<>
<div className="my-6 flex items-center gap-3 text-xs text-[#4a6b72]">
<span className="h-px flex-1 bg-[#4a6b72]/20" />
or
<span className="h-px flex-1 bg-[#4a6b72]/20" />
</div>
<a
href="/api/auth/google"
className="block rounded-full border border-[#4a6b72]/30 px-6 py-3 text-center font-semibold text-[#26454c] transition-colors hover:bg-[#eef9fb]"
>
Continue with Google
</a>
</>
)}
</form>
<p className="mt-6 text-center text-xs text-[#4a6b72]">
Accounts are created on the server. Ask whoever runs the box.
</p>
</div>
</div>
);
}