;
-};
-
-export type SectionHeading = {
- id: string;
- title: string;
- blurb?: string;
- accent: string;
- background: string;
-};
-
-export type SectionEntry> = SectionHeading & {
- Component: ComponentType
;
- /* The Component decides P; props are checked against it. */
- props?: NoInfer
;
- views?: SectionViews;
-};
-
-/* A manifest mixes components with different props, which no single
- array element type can check. This checks each entry against its
- own Component where it's written, then forgets P so the entries
- fit one array. */
-export function defineSection
(entry: SectionEntry
): SectionEntry {
- return entry as unknown as SectionEntry;
-}
-
-export type ManifestSection = SectionHeading & {
- actions?: ReactNode;
- content: ReactNode;
-};
-
-export function useSectionManifest(manifest: SectionEntry[]): ManifestSection[] {
+export function useSectionManifest(manifest) {
// One entry per section that has a toggle, seeded from its
// declared default so the control is right before anything loads.
- const [views, setViews] = useState>(() =>
+ const [views, setViews] = useState(() =>
Object.fromEntries(
manifest
.filter(entry => entry.views)
.map(entry => [
entry.id,
- entry.views?.default ?? entry.views?.options[0] ?? null,
+ entry.views.default ?? entry.views.options?.[0] ?? null,
]),
),
);
- const setView = (id: string, value: string) => setViews(prev => ({ ...prev, [id]: value }));
+ const setView = (id, value) => setViews(prev => ({ ...prev, [id]: value }));
return manifest.map(entry => {
const { Component, props, views: spec, ...heading } = entry;
- const view = views[entry.id] ?? undefined;
+ const view = views[entry.id];
const Toggle = spec?.Toggle;
return {
@@ -103,7 +59,7 @@ export function useSectionManifest(manifest: SectionEntry[]): ManifestSection[]
actions: Toggle ? (
setView(entry.id, value)}
accent={entry.accent}
options={spec.options}
diff --git a/src/lib/timeline.ts b/src/lib/timeline.ts
index 5f77d2d..29d02e9 100644
--- a/src/lib/timeline.ts
+++ b/src/lib/timeline.ts
@@ -23,8 +23,6 @@
* and the public/ layout stay the frontend's business.
*/
-import type { OrgKind } from './hrefs.ts'
-
export type DatePrecision = 'year' | 'month' | 'day'
/** What an entry is about. Drives the marker and the body layout. */
@@ -43,8 +41,6 @@ export type TimelineRef = {
kind: RefKind
/** The row's TEXT primary key — an event id, org slug, team slug. */
id: string
- /** Only on organizations: history.js copies v_timeline.org_kind. */
- orgKind?: OrgKind
}
/** Filename plus the table it came from; the directory is derived
diff --git a/src/lib/useContent.ts b/src/lib/useContent.ts
index ddf380e..71595b3 100644
--- a/src/lib/useContent.ts
+++ b/src/lib/useContent.ts
@@ -106,22 +106,12 @@ export type EventRecord = {
hosts: EventHost[]
description: string[]
links: Link[]
- /** The instagram link's label, the handle. See splitLinks in shape.js. */
- instagram?: string | null
+ instagram?: Link | null
blocks: ContentBlock[]
people: EventPerson[]
awards: EventAward[]
}
-/** One row of GET /events: shapeEvent without the detail-only
- * blocks, people and awards. */
-export type EventListItem = Omit
-
-/** An event_sections row, as GET /events sends it beside the list. */
-export type EventSection = { id: string; name: string; sort_order: number }
-
-export type EventsResponse = { sections: EventSection[]; events: EventListItem[] }
-
export const useEvent = (id?: string): Resource =>
useRecord(detailPath('/events', id), 'event')
@@ -167,17 +157,6 @@ export type OrgEvent = {
color?: string | null
}
-/** regions.scope's CHECK values. */
-export type RegionScope = 'domestic' | 'international' | 'virtual'
-
-/** A region_areas row as attachRegionDetails sends it. */
-export type RegionArea = {
- area_code: string
- share: number
- edge: 'top' | 'bottom' | null
- note: string | null
-}
-
export type OrganizationRecord = {
id: string
kind: 'national' | 'region' | 'chapter' | 'partner'
@@ -197,16 +176,14 @@ export type OrganizationRecord = {
blocks: ContentBlock[]
links: Link[]
socials: Link[]
- /* splitLinks in shape.js lifts these out as bare strings: the
- website's url, the email's label, the instagram handle. */
- website?: string | null
- email?: string | null
- instagram?: string | null
+ website?: Link | null
+ email?: Link | null
+ instagram?: Link | null
/** Shape depends on `kind`; empty object for national and partner. */
details: {
- scope?: RegionScope | null
+ scope?: string | null
map_note?: string | null
- areas?: RegionArea[]
+ areas?: Array<{ area_code: string; share?: number | null; edge?: string | null; note?: string | null }>
chapters?: Array<{ id: string; name: string; location_label?: string | null; logo?: string | null }>
region_id?: string | null
region_name?: string | null
@@ -220,14 +197,6 @@ export type OrganizationRecord = {
events: OrgEvent[]
}
-/** One row of GET /organizations: the card surface and details,
- * without the sections only the org's own page loads. */
-export type OrganizationListItem = Omit & {
- sort_order: number
-}
-
-export type OrganizationsResponse = { organizations: OrganizationListItem[] }
-
export const useOrganization = (id?: string): Resource =>
useRecord(detailPath('/organizations', id), 'organization')
@@ -245,8 +214,7 @@ export type TeamRecord = {
description: string[]
links: Link[]
socials: Link[]
- /** The instagram handle. See splitLinks in shape.js. */
- instagram?: string | null
+ instagram?: Link | null
blocks: ContentBlock[]
}
diff --git a/src/lib/useRecord.ts b/src/lib/useRecord.ts
index 10c07fd..bc26d16 100644
--- a/src/lib/useRecord.ts
+++ b/src/lib/useRecord.ts
@@ -89,7 +89,7 @@ export function useRecord(path: string | null, key: string): Resource {
setError(null)
setNotFound(false)
try {
- const body = await get | null>(path as string)
+ const body = await get(path as string)
if (!live) return
// A 200 with the key absent is a server-side shaping bug,
// not an empty record. Say so rather than rendering a page
diff --git a/src/navConfig.d.ts b/src/navConfig.d.ts
deleted file mode 100644
index 75ace2e..0000000
--- a/src/navConfig.d.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-/* Types for navConfig.js. */
-
-export type PageLink = { label: string; path: string };
-
-export type PageSectionLink = { label: string; hash: string };
-
-export type NavAction = {
- label: string;
- to: string;
- /** Picks the styling, not the destination. */
- variant: "ghost" | "fancy";
- external?: boolean;
-};
-
-export declare const PAGE_LINKS: PageLink[];
-
-/** Keyed by the owning page's path. */
-export declare const PAGE_SECTIONS: Record;
-
-export declare const NAV_ACTIONS: NavAction[];
diff --git a/src/pages/Community.tsx b/src/pages/Community.tsx
index 7b062ca..3a7dc6f 100644
--- a/src/pages/Community.tsx
+++ b/src/pages/Community.tsx
@@ -1,5 +1,5 @@
import PageShell from "../components/PageShell.tsx";
-import { defineSection, useSectionManifest } from "../lib/sections.tsx";
+import { useSectionManifest } from "../lib/sections.tsx";
import OrgListMap, { OrgMapToggle } from "./sections/OrgList-Map.tsx";
import OrgListVertical from "./sections/OrgList-Vertical.tsx";
import OrgListCards from "./sections/OrgList-Card.tsx";
@@ -13,7 +13,7 @@ import OrgListCards from "./sections/OrgList-Card.tsx";
═══════════════════════════════════════════════════════════════ */
const SECTIONS = [
- defineSection({
+ {
id: "chapters",
title: "Local Chapters",
blurb:
@@ -22,8 +22,8 @@ const SECTIONS = [
background: "#eef9fb",
Component: OrgListMap,
views: { options: ["map", "grid"], default: "map", Toggle: OrgMapToggle },
- }),
- defineSection({
+ },
+ {
id: "regions",
title: "Unity Regions",
blurb:
@@ -40,8 +40,8 @@ const SECTIONS = [
{ key: "international", title: "International Unity Regions" },
],
},
- }),
- defineSection({
+ },
+ {
id: "partners",
title: "Partner Organizations",
blurb:
@@ -54,7 +54,7 @@ const SECTIONS = [
pageLabel: "Partner page",
empty: "· Partner organizations coming soon ·",
},
- }),
+ },
];
export default function CommunityPage() {
diff --git a/src/pages/EventDetail.tsx b/src/pages/EventDetail.tsx
index f1fe27d..354de14 100644
--- a/src/pages/EventDetail.tsx
+++ b/src/pages/EventDetail.tsx
@@ -18,7 +18,7 @@
import { Link, useParams } from 'react-router-dom'
-import PageShell, { type ShellSection } from '../components/PageShell.tsx'
+import PageShell from '../components/PageShell.tsx'
import PageState from '../components/PageState.tsx'
import ContentBlocks from '../components/ContentBlocks.tsx'
import PeopleTiles, { type PeopleGroupInput } from '../components/PeopleTiles.tsx'
@@ -86,7 +86,7 @@ export default function EventDetail() {
const accent = event.color || TEAL
const groups = peopleGroups(event.people, accent)
- const sections: ShellSection[] = [
+ const sections = [
{
id: 'about',
title: event.theme || 'About',
diff --git a/src/pages/Retreats.tsx b/src/pages/Retreats.tsx
index 119bb20..17d3bbd 100644
--- a/src/pages/Retreats.tsx
+++ b/src/pages/Retreats.tsx
@@ -1,5 +1,5 @@
import PageShell from "../components/PageShell.tsx";
-import { defineSection, useSectionManifest } from "../lib/sections.tsx";
+import { useSectionManifest } from "../lib/sections.tsx";
import EventListCards, { EventCardsToggle } from "./sections/EventList-Cards.tsx";
/* ═══════════════════════════════════════════════════════════════
@@ -35,10 +35,10 @@ const CARD_VIEWS = { options: ["carousel", "grid"], Toggle: EventCardsToggle };
/* Pinned on every band. Named rather than repeated so turning this
page into "everything, filtered" later is one deletion. */
-const RETREATS = { type: "retreat" } as const;
+const RETREATS = { type: "retreat" };
const SECTIONS = [
- defineSection({
+ {
id: "national",
title: "National Retreats",
blurb: "Our flagship gatherings, open to young adults across the country.",
@@ -47,8 +47,8 @@ const SECTIONS = [
Component: EventListCards,
props: { section: "national", ...RETREATS },
views: { ...CARD_VIEWS, default: "carousel" },
- }),
- defineSection({
+ },
+ {
id: "regional",
title: "Regional Retreats",
blurb: "Smaller gatherings hosted by regions throughout the year.",
@@ -57,8 +57,8 @@ const SECTIONS = [
Component: EventListCards,
props: { section: "regional", ...RETREATS },
views: { ...CARD_VIEWS, default: "grid" },
- }),
- defineSection({
+ },
+ {
id: "partner",
title: "Partner Events",
blurb: "Retreats hosted by organizations we collaborate with.",
@@ -67,13 +67,13 @@ const SECTIONS = [
Component: EventListCards,
props: { section: "partner", ...RETREATS },
views: { ...CARD_VIEWS, default: "grid" },
- }),
+ },
/* The other three scopes, ready to uncomment. Each needs an accent
and a background of its own — those are presentation and live
here, not in event_sections.
- defineSection({
+ {
id: "local",
title: "Local Events",
blurb: "Hosted by individual chapters.",
@@ -82,8 +82,8 @@ const SECTIONS = [
Component: EventListCards,
props: { section: "local", ...RETREATS_ONLY },
views: { ...CARD_VIEWS, default: "grid" },
- }),
- defineSection({
+ },
+ {
id: "international",
title: "International Events",
blurb: "Gatherings beyond the US.",
@@ -92,8 +92,8 @@ const SECTIONS = [
Component: EventListCards,
props: { section: "international", ...RETREATS_ONLY },
views: { ...CARD_VIEWS, default: "grid" },
- }),
- defineSection({
+ },
+ {
id: "other",
title: "Other Events",
blurb: "Everything that doesn't fit the categories above.",
@@ -102,7 +102,7 @@ const SECTIONS = [
Component: EventListCards,
props: { section: "other", ...RETREATS_ONLY },
views: { ...CARD_VIEWS, default: "grid" },
- }),
+ },
*/
];
diff --git a/src/pages/admin/AdminFeedback.tsx b/src/pages/admin/AdminFeedback.tsx
index c2ef0fb..dbfbb3d 100644
--- a/src/pages/admin/AdminFeedback.tsx
+++ b/src/pages/admin/AdminFeedback.tsx
@@ -25,37 +25,9 @@ import { isUnauthorized, useAuth } from "../../lib/auth.tsx";
import { canWrite as roleCanWrite, canDelete } from "../../lib/roles.ts";
import { feedbackTypeLabel } from "../../data/feedbackTypes.js";
-/* feedback.status's CHECK values, in triage order. */
-const STATUSES = ["new", "read", "actioned", "archived", "spam"] as const;
+const STATUSES = ["new", "read", "actioned", "archived", "spam"];
-type FeedbackStatus = (typeof STATUSES)[number];
-
-/* The columns GET /api/admin/feedback selects. */
-type FeedbackRow = {
- id: number;
- created_at: string;
- feedback_type: string;
- message: string;
- name: string | null;
- email: string | null;
- page_path: string | null;
- section_id: string | null;
- status: FeedbackStatus;
- admin_note: string | null;
-};
-
-type FeedbackPage = {
- feedback: FeedbackRow[];
- /** Unfiltered, one per status. */
- counts: Record;
- nextCursor: number | null;
-};
-
-type FeedbackChanges = { status?: FeedbackStatus; admin_note?: string };
-
-type StatusFilter = FeedbackStatus | "all";
-
-const STATUS_STYLE: Record = {
+const STATUS_STYLE = {
new: "bg-[#138ba0] text-white",
read: "bg-[#eef9fb] text-[#138ba0]",
actioned: "bg-[#eaf3e2] text-[#4a6b2f]",
@@ -65,7 +37,7 @@ const STATUS_STYLE: Record = {
// created_at is UTC in 'YYYY-MM-DD HH:MM:SS' form, which Safari
// won't parse without the T and the Z.
-function formatDate(value: string) {
+function formatDate(value) {
const date = new Date(`${value.replace(" ", "T")}Z`);
return date.toLocaleString(undefined, {
dateStyle: "medium",
@@ -73,34 +45,26 @@ function formatDate(value: string) {
});
}
-function locationOf(row: FeedbackRow) {
+function locationOf(row) {
if (!row.page_path) return "Not page-specific";
return row.section_id ? `${row.page_path} #${row.section_id}` : row.page_path;
}
/* ── One submission ──────────────────────────────────────────── */
-type FeedbackCardProps = {
- row: FeedbackRow;
- onChange: (row: FeedbackRow) => void;
- onRemove: (row: FeedbackRow) => void;
- canWrite: boolean;
- canRemove: boolean;
-};
-
-function FeedbackCard({ row, onChange, onRemove, canWrite, canRemove }: FeedbackCardProps) {
+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(null);
const [confirming, setConfirming] = useState(false);
const noteDirty = note !== (row.admin_note ?? "");
- async function save(changes: FeedbackChanges) {
+ async function save(changes) {
setBusy(true);
setError(null);
try {
- const data = await patch<{ feedback: FeedbackRow }>(`/admin/feedback/${row.id}`, changes);
+ const data = await patch(`/admin/feedback/${row.id}`, changes);
onChange(data.feedback);
} catch (err) {
setError(err instanceof ApiError ? err.message : "Couldn't save that.");
@@ -177,8 +141,7 @@ function FeedbackCard({ row, onChange, onRemove, canWrite, canRemove }: Feedback
id={`status-${row.id}`}
value={row.status}
disabled={busy}
- // The options are STATUSES, so the value is always one of them.
- onChange={(e) => save({ status: e.target.value as FeedbackStatus })}
+ onChange={(e) => save({ status: e.target.value })}
className="rounded-lg border border-[#4a6b72]/25 bg-white px-3 py-1.5 text-sm text-[#26454c] outline-none focus:border-[#138ba0] focus:ring-2 focus:ring-[#138ba0]/25"
>
{STATUSES.map((s) => (
@@ -265,22 +228,22 @@ export default function AdminFeedback() {
const { user } = useAuth();
const navigate = useNavigate();
- const [status, setStatus] = useState("new");
+ const [status, setStatus] = useState("new");
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([]);
+ const [counts, setCounts] = useState({});
+ const [cursor, setCursor] = useState(null);
const [loading, setLoading] = useState(true);
- const [error, setError] = useState(null);
+ const [error, setError] = useState(null);
// Minimums, not equality — see lib/roles.ts.
const canWrite = roleCanWrite(user);
const canRemove = canDelete(user);
const load = useCallback(
- async (before: number | null = null) => {
+ async (before = null) => {
setLoading(true);
setError(null);
@@ -290,7 +253,7 @@ export default function AdminFeedback() {
if (before) params.set("before", String(before));
try {
- const data = await get(`/admin/feedback?${params}`, { ttl: 0 });
+ const data = await get(`/admin/feedback?${params}`, { ttl: 0 });
setRows((prev) => (before ? [...prev, ...data.feedback] : data.feedback));
setCounts(data.counts);
setCursor(data.nextCursor);
@@ -314,7 +277,7 @@ export default function AdminFeedback() {
load();
}, [load]);
- function replaceRow(updated: FeedbackRow) {
+ function replaceRow(updated) {
setRows((prev) =>
prev
.map((row) => (row.id === updated.id ? updated : row))
@@ -327,7 +290,7 @@ export default function AdminFeedback() {
// The deleted row is passed whole rather than by id: its status
// is what says which tab count to drop.
- function removeRow(removed: FeedbackRow) {
+ function removeRow(removed) {
setRows((prev) => prev.filter((row) => row.id !== removed.id));
setCounts((prev) => ({
...prev,
@@ -335,7 +298,7 @@ export default function AdminFeedback() {
}));
}
- const tabs: Array<{ id: StatusFilter; label: string; count?: number }> = [
+ const tabs = [
{ id: "all", label: "All" },
...STATUSES.map((s) => ({ id: s, label: s, count: counts[s] })),
];
diff --git a/src/pages/admin/AdminHome.tsx b/src/pages/admin/AdminHome.tsx
index 610b0dc..0d17675 100644
--- a/src/pages/admin/AdminHome.tsx
+++ b/src/pages/admin/AdminHome.tsx
@@ -18,18 +18,17 @@
record) wants to be a separate component that fails on its own.
═══════════════════════════════════════════════════════════════ */
-import type { ReactNode } from "react";
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, type AdminNavItem } from "./adminNav.js";
+import { CMS_NAV, FORMS_NAV, PANEL_NAV, target } from "./adminNav.js";
/* One card. The title link is stretched over the whole card with
`after:absolute`, which makes the card clickable without nesting
an anchor inside an anchor; the sub-links sit above it on z-10 so
they stay separately clickable. */
-function NavCard({ item }: { item: AdminNavItem }) {
+function NavCard({ item }) {
const to = target(item);
// Drop the child that just repeats the card's own destination —
@@ -76,15 +75,7 @@ function NavCard({ item }: { item: AdminNavItem }) {
);
}
-function CardBlock({
- title,
- blurb,
- children,
-}: {
- title: string;
- blurb?: string;
- children: ReactNode;
-}) {
+function CardBlock({ title, blurb, children }) {
return (
@@ -96,7 +87,7 @@ function CardBlock({
);
}
-function PanelSection({ title, children }: { title: string; children: ReactNode }) {
+function PanelSection({ title, children }) {
return (
@@ -116,7 +107,7 @@ const QUICK_ADD = [
export default function AdminHome() {
const { user } = useAuth();
- const role = user ? (ROLE_LABELS[user.role] ?? user.role) : undefined;
+ const role = ROLE_LABELS[user?.role] ?? user?.role;
return (
diff --git a/src/pages/admin/AdminLayout.tsx b/src/pages/admin/AdminLayout.tsx
index 51c8344..ab073ec 100644
--- a/src/pages/admin/AdminLayout.tsx
+++ b/src/pages/admin/AdminLayout.tsx
@@ -56,8 +56,8 @@ 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 stableSet = useCallback((value: string | null) => setDetail(value), []);
+ const [detail, setDetail] = useState(null);
+ const stableSet = useCallback((value) => setDetail(value), []);
const titleContext = useMemo(() => ({ setDetail: stableSet }), [stableSet]);
useEffect(() => {
diff --git a/src/pages/admin/AdminLogin.tsx b/src/pages/admin/AdminLogin.tsx
index 44c6720..e558dda 100644
--- a/src/pages/admin/AdminLogin.tsx
+++ b/src/pages/admin/AdminLogin.tsx
@@ -10,7 +10,7 @@
starts working.
═══════════════════════════════════════════════════════════════ */
-import { useState, type FormEvent } from "react";
+import { useState } from "react";
import { useLocation, useNavigate } from "react-router-dom";
import { useAuth } from "../../lib/auth.tsx";
@@ -29,12 +29,12 @@ export default function AdminLogin() {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
- const [error, setError] = useState(null);
+ const [error, setError] = useState(null);
const [busy, setBusy] = useState(false);
const destination = location.state?.from?.pathname ?? "/admin/home";
- async function handleSubmit(event: FormEvent) {
+ async function handleSubmit(event) {
event.preventDefault();
if (busy) return;
diff --git a/src/pages/admin/AdminPanel.tsx b/src/pages/admin/AdminPanel.tsx
index a2219c2..5a9e22c 100644
--- a/src/pages/admin/AdminPanel.tsx
+++ b/src/pages/admin/AdminPanel.tsx
@@ -24,54 +24,23 @@
the reason shows up before the click rather than after it.
═══════════════════════════════════════════════════════════════ */
-import { useCallback, useEffect, useState, type ReactNode } from "react";
+import { useCallback, useEffect, useState } from "react";
import { del, get, patch } from "../../lib/api.js";
import { isUnauthorized, useAuth } from "../../lib/auth.tsx";
import { useNavigate } from "react-router-dom";
-import { ROLES, ROLE_LABELS, ROLE_NOTES, type Role } from "../../lib/roles.ts";
-
-/* An admin_users row as panel.js selects it, with its live session count. */
-type PanelUser = {
- id: number;
- email: string;
- name: string | null;
- role: Role;
- is_active: number;
- created_at: string;
- last_login_at: string | null;
- sessions: number;
-};
-
-/* GET /api/admin/panel/overview. A count is null when its table
- doesn't exist yet. */
-type Overview = {
- system: {
- schemaVersion: number;
- dbPath: string | null;
- nodeVersion: string;
- platform: string;
- uptimeSeconds: number;
- startedAt: string;
- sessions: number | null;
- roles: Role[];
- };
- content: Array<{ label: string; count: number | null }>;
- users: PanelUser[];
-};
-
-type UserResponse = { user: PanelUser };
+import { ROLES, ROLE_LABELS, ROLE_NOTES } from "../../lib/roles.ts";
/* SQLite hands back "2026-09-22 04:11:07" — UTC, but without the
marker that says so. Left alone, browsers read it as local time
and last-login drifts by the timezone offset. */
-function when(value: string | null | undefined) {
+function when(value) {
if (!value) return "—";
const iso = value.includes("T") ? value : `${value.replace(" ", "T")}Z`;
const date = new Date(iso);
return Number.isNaN(date.getTime()) ? value : date.toLocaleString();
}
-function uptime(seconds: number | null | undefined) {
+function uptime(seconds) {
if (seconds == null) return "—";
const d = Math.floor(seconds / 86400);
const h = Math.floor((seconds % 86400) / 3600);
@@ -81,15 +50,7 @@ function uptime(seconds: number | null | undefined) {
return `${m}m`;
}
-function Block({
- title,
- note,
- children,
-}: {
- title: string;
- note?: string;
- children: ReactNode;
-}) {
+function Block({ title, note, children }) {
return (
@@ -101,7 +62,7 @@ function Block({
);
}
-function Stat({ label, value }: { label: string; value: ReactNode }) {
+function Stat({ label, value }) {
return (
@@ -116,23 +77,23 @@ export default function AdminPanel() {
const { user: me } = useAuth();
const navigate = useNavigate();
- const [data, setData] = useState
(null);
+ const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
- const [error, setError] = useState(null);
+ const [error, setError] = useState(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<{ id: number; message: string } | null>(null);
+ const [busyId, setBusyId] = useState(null);
+ const [rowError, setRowError] = useState(null);
const load = useCallback(async () => {
setLoading(true);
setError(null);
try {
- setData(await get("/admin/panel/overview", { ttl: 0 }));
+ setData(await get("/admin/panel/overview", { ttl: 0 }));
} catch (err) {
if (isUnauthorized(err)) return navigate("/admin/login", { replace: true });
- setError((err instanceof Error && err.message) || "Couldn't load the panel.");
+ setError(err.message || "Couldn't load the panel.");
} finally {
setLoading(false);
}
@@ -144,7 +105,7 @@ export default function AdminPanel() {
/* Replace the one row the server returns rather than refetching
the whole overview — the counts didn't change. */
- function mergeUser(updated: PanelUser) {
+ function mergeUser(updated) {
setData((current) =>
current
? {
@@ -155,37 +116,30 @@ export default function AdminPanel() {
);
}
- async function run(id: number, work: () => Promise) {
+ async function run(id, work) {
setBusyId(id);
setRowError(null);
try {
mergeUser(await work());
} catch (err) {
if (isUnauthorized(err)) return navigate("/admin/login", { replace: true });
- setRowError({ id, message: (err instanceof Error && err.message) || "That didn't work." });
+ setRowError({ id, message: err.message || "That didn't work." });
} finally {
setBusyId(null);
}
}
- const changeRole = (row: PanelUser, role: Role) =>
+ const changeRole = (row, role) =>
+ run(row.id, async () => (await patch(`/admin/panel/users/${row.id}`, { role })).user);
+
+ const setActive = (row, is_active) =>
run(
row.id,
- async () => (await patch(`/admin/panel/users/${row.id}`, { role })).user,
+ async () => (await patch(`/admin/panel/users/${row.id}`, { is_active })).user,
);
- const setActive = (row: PanelUser, is_active: number) =>
- run(
- row.id,
- async () =>
- (await patch(`/admin/panel/users/${row.id}`, { is_active })).user,
- );
-
- const revoke = (row: PanelUser) =>
- run(
- row.id,
- async () => (await del(`/admin/panel/users/${row.id}/sessions`)).user,
- );
+ const revoke = (row) =>
+ run(row.id, async () => (await del(`/admin/panel/users/${row.id}/sessions`)).user);
if (loading) {
return (
@@ -210,9 +164,6 @@ export default function AdminPanel() {
);
}
- // Not loading and no error means the overview arrived.
- if (!data) return null;
-
const { system, content, users } = data;
const activeSupers = users.filter(
(u) => u.role === "superadmin" && u.is_active === 1,
@@ -303,7 +254,7 @@ export default function AdminPanel() {