spb/internetpressure
Public
TypeScript 36.3%
Python 31.8%
Go 18%
JavaScript 9.8%
Shell 1.9%
SQL 1.4%
CSS 0.5%
1'use client';23import { useCallback, useEffect, useState, type ReactNode } from 'react';4import { AdminError, adminFetch } from '@/lib/admin-fetch';56/** Fetch an admin resource with loading/error state and a `reload()` handle. */7export function useAdmin<T>(path: string | null, params?: Record<string, string | number | undefined>) {8 const [result, setResult] = useState<{ key: string; data: T | null; err: string | null }>({ key: '', data: null, err: null });9 const [nonce, setNonce] = useState(0);10 const key = `${path}?${JSON.stringify(params ?? {})}#${nonce}`;11 useEffect(() => {12 if (!path) return;13 let alive = true;14 adminFetch<T>(path, { params: JSON.parse(key.slice(key.indexOf('?') + 1, key.lastIndexOf('#'))) as Record<string, string | number | undefined> })15 .then((d) => alive && setResult({ key, data: d, err: null }))16 .catch((e: unknown) => alive && setResult((r) => ({ key, data: r.data, err: e instanceof AdminError ? `${e.status} ${JSON.stringify(e.body ?? '')}` : String(e) })));17 return () => {18 alive = false;19 };20 }, [path, key]);21 const reload = useCallback(() => setNonce((n) => n + 1), []);22 const loading = result.key !== key;23 return { data: result.data, err: loading ? null : result.err, loading, reload };24}2526export function AdminPage({ title, desc, right, children }: { title: string; desc?: string; right?: ReactNode; children: ReactNode }) {27 return (28 <div>29 <header className="mb-4 flex flex-wrap items-end justify-between gap-3">30 <div>31 <h1 className="text-[22px] font-medium tracking-tight">{title}</h1>32 {desc && <p className="mt-0.5 max-w-[760px] text-[12.5px] text-ink-2">{desc}</p>}33 </div>34 {right}35 </header>36 {children}37 </div>38 );39}4041export function Panel({ title, right, children, className = '' }: { title?: string; right?: ReactNode; children: ReactNode; className?: string }) {42 return (43 <section className={`panel p-3 ${className}`}>44 {(title || right) && (45 <header className="mb-2 flex items-baseline justify-between gap-3">46 {title && <h2 className="label">{title}</h2>}47 {right && <div className="text-[11px] text-ink-2">{right}</div>}48 </header>49 )}50 {children}51 </section>52 );53}5455export function ErrorNote({ err }: { err: string | null }) {56 if (!err) return null;57 return <p className="my-2 text-[12px] text-bad">Admin API error: {err}</p>;58}5960export function Ok({ ok, label }: { ok: boolean; label?: string }) {61 return (62 <span className="inline-flex items-center gap-1.5 text-[11px] uppercase tracking-[0.1em]" style={{ color: ok ? 'var(--ok)' : 'var(--bad)' }}>63 <span className="size-1.5 rounded-full" style={{ background: ok ? 'var(--ok)' : 'var(--bad)' }} aria-hidden="true" />64 {label ?? (ok ? 'ok' : 'down')}65 </span>66 );67}6869export const inputCls = 'h-8 rounded-[4px] border border-line bg-panel px-2 text-[12.5px] text-ink placeholder:text-ink-3';70export const btnCls = 'h-8 rounded-[4px] border border-line px-3 text-[12px] text-ink hover:border-line-2 disabled:opacity-40';71export const btnPrimary = 'h-8 rounded-[4px] bg-accent px-3 text-[12px] font-medium text-bg hover:opacity-90 disabled:opacity-40';72export const btnDanger = 'h-8 rounded-[4px] border border-bad/60 px-3 text-[12px] text-bad hover:bg-bad/10 disabled:opacity-40';7374export function Field({ label, children }: { label: string; children: ReactNode }) {75 return (76 <label className="block min-w-0">77 <span className="label">{label}</span>78 <div className="mt-1">{children}</div>79 </label>80 );81}8283export function Toast({ msg }: { msg: string | null }) {84 if (!msg) return null;85 return (86 <p role="status" className="my-2 text-[12px] text-ok">87 {msg}88 </p>89 );90}91