Renames every .js module under src/ to .ts (api, useResource, adminSchema, navConfig, adminNav and src/data/*) and points imports, comments and the seed script's module paths at the new names. Their types now come from inference; no .d.ts files and no shape interfaces. tsc stays strict (noImplicitAny off). The errors inference leaves behind get the lightest fix that clears them: `any` on empty state, contexts and list defaults, `: any` on components with optional or spread props, class fields on ApiError, and option shapes on get/useResource. Kept as real types, since they belong to modules that were already TypeScript and later features import them: PageShell's ShellSection and props, useContent's corrected record types (website/email/ instagram are bare strings) and EventListItem, and TimelineRef's orgKind. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
142 lines
4.8 KiB
TypeScript
142 lines
4.8 KiB
TypeScript
/* ═══════════════════════════════════════════════════════════════
|
|
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.ts";
|
|
|
|
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<any>(null);
|
|
const [busy, setBusy] = useState(false);
|
|
|
|
const destination = location.state?.from?.pathname ?? "/admin/home";
|
|
|
|
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>
|
|
);
|
|
}
|