// Small pieces the phase 9 Admin tabs share. import { useEffect, useState } from "react"; import { admin9Api, type ProfileUser } from "../../api/admin"; import { errorText } from "../../lib/format"; import type { Load } from "../../lib/useLoad"; export function Problem({ load }: { load: Load }) { if (load.state === "loading") return
Loading
; if (load.state === "error") return
{load.message}
; return null; } export type Msg = { ok: boolean; text: string } | null; export function Notice({ msg }: { msg: Msg }) { if (!msg) return null; return (
{msg.text}
); } /** Search the users Packet Station knows and pick one. `label` names the search box. */ export function UserPicker({ label, onPick, exclude = [] }: { label: string; onPick: (u: ProfileUser) => void; exclude?: readonly string[] }) { const [q, setQ] = useState(""); const [rows, setRows] = useState([]); const [error, setError] = useState(null); useEffect(() => { const term = q.trim(); if (term.length < 2) { setRows([]); return; } const ctl = new AbortController(); const t = window.setTimeout(() => { admin9Api .users(term, ctl.signal) .then((r) => { setRows(r.slice(0, 10)); setError(null); }) .catch((err: unknown) => { if (!ctl.signal.aborted) setError(errorText(err)); }); }, 250); return () => { window.clearTimeout(t); ctl.abort(); }; }, [q]); const shown = rows.filter((u) => !exclude.includes(u.id)); return (
{error ?
{error}
: null} {shown.length ? (
    {shown.map((u) => (
  • ))}
) : null}
); }