Convert src/ JavaScript modules to TypeScript

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>
This commit is contained in:
Zaldimmar 2026-09-26 15:39:42 -05:00
parent 5cedc68fd7
commit 22d5328885
43 changed files with 195 additions and 135 deletions

View file

@ -20,10 +20,10 @@
import { useCallback, useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";
import { del, get, patch, ApiError } from "../../lib/api.js";
import { del, get, patch, ApiError } from "../../lib/api.ts";
import { isUnauthorized, useAuth } from "../../lib/auth.tsx";
import { canWrite as roleCanWrite, canDelete } from "../../lib/roles.ts";
import { feedbackTypeLabel } from "../../data/feedbackTypes.js";
import { feedbackTypeLabel } from "../../data/feedbackTypes.ts";
const STATUSES = ["new", "read", "actioned", "archived", "spam"];
@ -55,7 +55,7 @@ function locationOf(row) {
function FeedbackCard({ row, onChange, onRemove, canWrite, canRemove }) {
const [note, setNote] = useState(row.admin_note ?? "");
const [busy, setBusy] = useState(false);
const [error, setError] = useState(null);
const [error, setError] = useState<any>(null);
const [confirming, setConfirming] = useState(false);
const noteDirty = note !== (row.admin_note ?? "");
@ -232,11 +232,11 @@ export default function AdminFeedback() {
const [query, setQuery] = useState("");
const [search, setSearch] = useState(""); // applied, not typed
const [rows, setRows] = useState([]);
const [counts, setCounts] = useState({});
const [cursor, setCursor] = useState(null);
const [rows, setRows] = useState<any[]>([]);
const [counts, setCounts] = useState<any>({});
const [cursor, setCursor] = useState<any>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [error, setError] = useState<any>(null);
// Minimums, not equality — see lib/roles.ts.
const canWrite = roleCanWrite(user);
@ -298,7 +298,7 @@ export default function AdminFeedback() {
}));
}
const tabs = [
const tabs: any[] = [
{ id: "all", label: "All" },
...STATUSES.map((s) => ({ id: s, label: s, count: counts[s] })),
];

View file

@ -5,7 +5,7 @@
the header deliberately drops its tab row here so the same links
aren't drawn twice — and a standing info panel on the right.
The cards come from adminNav.js, the same list the CMS header
The cards come from adminNav.ts, the same list the CMS header
reads, so a new entity shows up here the moment it's registered.
Forms sit in their own block below: they're submissions coming
in rather than content going out, and there'll be more of them
@ -22,7 +22,7 @@ import { Link } from "react-router-dom";
import { useAuth } from "../../lib/auth.tsx";
import { SITE_VERSION } from "../../lib/version.ts";
import { ROLE_LABELS, isSuper } from "../../lib/roles.ts";
import { CMS_NAV, FORMS_NAV, PANEL_NAV, target } from "./adminNav.js";
import { CMS_NAV, FORMS_NAV, PANEL_NAV, target } from "./adminNav.ts";
/* One card. The title link is stretched over the whole card with
`after:absolute`, which makes the card clickable without nesting

View file

@ -30,7 +30,7 @@ import {
matches,
navFor,
target,
} from "./adminNav.js";
} from "./adminNav.ts";
export default function AdminLayout() {
const { user, logout } = useAuth();
@ -56,7 +56,7 @@ export default function AdminLayout() {
// What the page below has published about itself — a record
// name, or null on a list. setDetail is stable so publishing
// can't loop.
const [detail, setDetail] = useState(null);
const [detail, setDetail] = useState<any>(null);
const stableSet = useCallback((value) => setDetail(value), []);
const titleContext = useMemo(() => ({ setDetail: stableSet }), [stableSet]);

View file

@ -14,7 +14,7 @@ import { useState } from "react";
import { useLocation, useNavigate } from "react-router-dom";
import { useAuth } from "../../lib/auth.tsx";
import { ApiError } from "../../lib/api.js";
import { ApiError } from "../../lib/api.ts";
const GOOGLE_ENABLED = false;
@ -29,7 +29,7 @@ export default function AdminLogin() {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [error, setError] = useState(null);
const [error, setError] = useState<any>(null);
const [busy, setBusy] = useState(false);
const destination = location.state?.from?.pathname ?? "/admin/home";

View file

@ -25,7 +25,7 @@
═══════════════════════════════════════════════════════════════ */
import { useCallback, useEffect, useState } from "react";
import { del, get, patch } from "../../lib/api.js";
import { del, get, patch } from "../../lib/api.ts";
import { isUnauthorized, useAuth } from "../../lib/auth.tsx";
import { useNavigate } from "react-router-dom";
import { ROLES, ROLE_LABELS, ROLE_NOTES } from "../../lib/roles.ts";
@ -50,7 +50,7 @@ function uptime(seconds) {
return `${m}m`;
}
function Block({ title, note, children }) {
function Block({ title, note, children }: any) {
return (
<section className="mt-8 first:mt-0">
<div className="flex items-baseline gap-3">
@ -77,21 +77,21 @@ export default function AdminPanel() {
const { user: me } = useAuth();
const navigate = useNavigate();
const [data, setData] = useState(null);
const [data, setData] = useState<any>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [error, setError] = useState<any>(null);
// Which row is mid-request, and what went wrong on it. Scoped to
// the row so a failure on one account doesn't blank the table.
const [busyId, setBusyId] = useState(null);
const [rowError, setRowError] = useState(null);
const [busyId, setBusyId] = useState<any>(null);
const [rowError, setRowError] = useState<any>(null);
const load = useCallback(async () => {
setLoading(true);
setError(null);
try {
setData(await get("/admin/panel/overview", { ttl: 0 }));
} catch (err) {
} catch (err: any) {
if (isUnauthorized(err)) return navigate("/admin/login", { replace: true });
setError(err.message || "Couldn't load the panel.");
} finally {
@ -121,7 +121,7 @@ export default function AdminPanel() {
setRowError(null);
try {
mergeUser(await work());
} catch (err) {
} catch (err: any) {
if (isUnauthorized(err)) return navigate("/admin/login", { replace: true });
setRowError({ id, message: err.message || "That didn't work." });
} finally {

View file

@ -43,10 +43,10 @@
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useNavigate, useParams } from "react-router-dom";
import { get, post, patch, del, ApiError } from "../../lib/api.js";
import { get, post, patch, del, ApiError } from "../../lib/api.ts";
import { isUnauthorized, useAuth } from "../../lib/auth.tsx";
import { useAdminDetail } from "../../lib/adminTitle.tsx";
import { ADMIN_ENTITIES, slugify } from "../../lib/adminSchema.js";
import { ADMIN_ENTITIES, slugify } from "../../lib/adminSchema.ts";
import { atLeast } from "../../lib/roles.ts";
import { Field, FieldGrid, Repeater, getPath, setPath } from "../../components/admin/fields.tsx";
@ -62,7 +62,7 @@ function friendly(message, singular) {
export default function EntityEdit() {
const { entity: entityKey, id } = useParams();
const manifest = ADMIN_ENTITIES[entityKey];
const manifest = ADMIN_ENTITIES[entityKey ?? ""];
const navigate = useNavigate();
const { user } = useAuth();
@ -120,17 +120,17 @@ export default function EntityEdit() {
// names the field to read instead.
const headingPath = slugPaths[slugPaths.length - 1] ?? manifest?.titleFrom;
const [form, setForm] = useState(null);
const [options, setOptions] = useState({});
const [errors, setErrors] = useState({});
const [message, setMessage] = useState(null);
const [form, setForm] = useState<any>(null);
const [options, setOptions] = useState<any>({});
const [errors, setErrors] = useState<any>({});
const [message, setMessage] = useState<any>(null);
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
const [slugTouched, setSlugTouched] = useState(false);
// The last state the server confirmed. Everything else compares
// against this to decide whether there's anything to lose.
const baseline = useRef(null);
const baseline = useRef<any>(null);
const load = useCallback(async () => {
if (!manifest) return;

View file

@ -15,22 +15,22 @@
import { useCallback, useEffect, useState } from "react";
import { Link, Navigate, useNavigate, useParams, useSearchParams } from "react-router-dom";
import { get, ApiError } from "../../lib/api.js";
import { get, ApiError } from "../../lib/api.ts";
import { isUnauthorized, useAuth } from "../../lib/auth.tsx";
import { ADMIN_ENTITIES } from "../../lib/adminSchema.js";
import { ADMIN_ENTITIES } from "../../lib/adminSchema.ts";
import { atLeast } from "../../lib/roles.ts";
export default function EntityList() {
const { entity: entityKey } = useParams();
const manifest = ADMIN_ENTITIES[entityKey];
const manifest = ADMIN_ENTITIES[entityKey ?? ""];
const navigate = useNavigate();
const { user } = useAuth();
const [params, setParams] = useSearchParams();
const [rows, setRows] = useState([]);
const [options, setOptions] = useState({});
const [rows, setRows] = useState<any[]>([]);
const [options, setOptions] = useState<any>({});
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [error, setError] = useState<any>(null);
const [query, setQuery] = useState(params.get("q") ?? "");
// Minimum rank, never equality. POST /api/admin/:entity is gated
@ -79,7 +79,7 @@ export default function EntityList() {
setParams(next, { replace: true });
}
function labelFor(filter, value) {
function labelFor(filter) {
if (filter.optionsFrom) {
return (options[filter.optionsFrom] ?? []).map((o) => [o.id, o.label]);
}

View file

@ -18,10 +18,10 @@
import { Navigate, Outlet } from "react-router-dom";
import { useAuth } from "../../lib/auth.tsx";
import { atLeast } from "../../lib/roles.ts";
import { ADMIN_HOME } from "./adminNav.js";
import { atLeast, type Role } from "../../lib/roles.ts";
import { ADMIN_HOME } from "./adminNav.ts";
export default function RequireRole({ role = "superadmin" }) {
export default function RequireRole({ role = "superadmin" }: { role?: Role }) {
const { user, loading } = useAuth();
// RequireAuth is already showing its own placeholder above this.

View file

@ -1,5 +1,5 @@
/* ═══════════════════════════════════════════════════════════════
ADMIN NAVIGATION — src/pages/admin/adminNav.js
ADMIN NAVIGATION — src/pages/admin/adminNav.ts
Lifted out of AdminLayout because two things read it now: the
header tabs and the card grid on the home page. Adding an entity
@ -99,7 +99,7 @@ export const PANEL_NAV = {
superOnly: true,
};
export const NAV = [...CMS_NAV, FORMS_NAV, PANEL_NAV];
export const NAV: any[] = [...CMS_NAV, FORMS_NAV, PANEL_NAV];
/* What this user may see. Call it with the user from useAuth. */
export function navFor(user) {