SPB Git forge
15commits 1branches 0releases
29.7 MBsize
maindefault branch
10 days agolast push
TypeScript 36.3% Python 31.8% Go 18% JavaScript 9.8% Shell 1.9% SQL 1.4% CSS 0.5%
3.7 KB · 91 lines tsx
Raw Blame History
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