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 Link from 'next/link';4import { useRouter, useSearchParams } from 'next/navigation';5import { useEffect, useMemo, useRef, useState } from 'react';6import { PNum } from '@/components/ui/primitives';7import { fmtInt, fmtMs, fmtPct } from '@/lib/format';8import type { Target } from '@/lib/types';910type SortKey = 'pressure' | 'name' | 'ok_ratio_1h' | 'ttfb_ms_median_1h' | 'importance';1112export function TargetsRegistry({ targets, categories, initialQuery, initialCategory, total, page, pages, pageSize }: { targets: Target[]; categories: string[]; initialQuery: string; initialCategory: string; total: number; page: number; pages: number; pageSize: number }) {13 const [q, setQ] = useState(initialQuery);14 const [cat, setCat] = useState(initialCategory);15 const router = useRouter();16 const sp = useSearchParams();17 const first = useRef(true);18 // filters are applied server-side (the registry is paginated): push them to the URL, debounced19 useEffect(() => {20 if (first.current) {21 first.current = false;22 return;23 }24 const t = setTimeout(() => {25 const next = new URLSearchParams(sp.toString());26 if (q) next.set('q', q);27 else next.delete('q');28 if (cat) next.set('category', cat);29 else next.delete('category');30 next.delete('page');31 router.replace(`/targets?${next.toString()}`, { scroll: false });32 }, 350);33 return () => clearTimeout(t);34 // eslint-disable-next-line react-hooks/exhaustive-deps35 }, [q, cat]);36 const pageHref = (n: number) => {37 const next = new URLSearchParams(sp.toString());38 if (n > 1) next.set('page', String(n));39 else next.delete('page');40 return `/targets?${next.toString()}`;41 };42 const [sort, setSort] = useState<SortKey>('pressure');43 const [dir, setDir] = useState<1 | -1>(-1);4445 const rows = useMemo(() => {46 const s = q.trim().toLowerCase();47 return targets48 .filter((t) => (!cat || t.category === cat) && (!s || t.hostname.toLowerCase().includes(s) || t.name.toLowerCase().includes(s) || (t.provider ?? '').toLowerCase().includes(s) || t.target_id.includes(s)))49 .sort((a, b) => {50 const av = a[sort];51 const bv = b[sort];52 if (typeof av === 'string' && typeof bv === 'string') return av.localeCompare(bv) * dir;53 return ((av as number) - (bv as number)) * dir;54 });55 }, [targets, q, cat, sort, dir]);5657 const th = (key: SortKey, label: string, right = false) => (58 <th className={right ? 'r' : ''} aria-sort={sort === key ? (dir === 1 ? 'ascending' : 'descending') : 'none'}>59 <button60 type="button"61 onClick={() => {62 if (sort === key) setDir((d) => (d === 1 ? -1 : 1));63 else {64 setSort(key);65 setDir(key === 'name' ? 1 : -1);66 }67 }}68 className={`uppercase tracking-[0.1em] hover:text-ink ${sort === key ? 'text-ink' : ''}`}69 >70 {label}71 {sort === key ? (dir === 1 ? ' ↑' : ' ↓') : ''}72 </button>73 </th>74 );7576 return (77 <div>78 <div className="flex flex-wrap items-center gap-2">79 <input value={q} onChange={(e) => setQ(e.target.value)} placeholder="hostname, provider, id…" className="h-8 w-full max-w-[320px] rounded-[4px] border border-line bg-panel px-2 text-[13px] text-ink placeholder:text-ink-3" aria-label="Filter targets" />80 <div className="scroll-x flex gap-1" role="radiogroup" aria-label="Category">81 <button type="button" role="radio" aria-checked={!cat} onClick={() => setCat('')} className={`whitespace-nowrap rounded-[3px] border px-2 py-1 text-[11px] ${!cat ? 'border-line-2 bg-panel-2 text-ink' : 'border-line text-ink-2 hover:text-ink'}`}>82 all83 </button>84 {categories.map((c) => (85 <button key={c} type="button" role="radio" aria-checked={cat === c} onClick={() => setCat(c)} className={`whitespace-nowrap rounded-[3px] border px-2 py-1 text-[11px] ${cat === c ? 'border-line-2 bg-panel-2 text-ink' : 'border-line text-ink-2 hover:text-ink'}`}>86 {c}87 </button>88 ))}89 </div>90 <span className="num ml-auto text-[11px] text-ink-3">{fmtInt(rows.length)} shown · {fmtInt(total)} match · page {page}/{pages}</span>91 </div>92 <div className="scroll-x -mx-3 mt-3 px-3">93 <table className="tbl">94 <thead>95 <tr>96 {th('name', 'Target')}97 <th className="hidden md:table-cell">Hostname</th>98 <th className="hidden sm:table-cell">Category</th>99 <th className="hidden lg:table-cell">Service</th>100 <th className="hidden sm:table-cell">Anchor</th>101 {th('importance', 'Imp.', true)}102 <th className="r hidden md:table-cell">Tier</th>103 {th('pressure', 'Pressure', true)}104 {th('ok_ratio_1h', 'OK 1h', true)}105 {th('ttfb_ms_median_1h', 'TTFB', true)}106 </tr>107 </thead>108 <tbody>109 {rows.map((t) => (110 <tr key={t.target_id}>111 <td>112 <span className="text-ink">{t.name}</span>113 </td>114 <td className="num hidden text-ink-2 md:table-cell">{t.hostname}</td>115 <td className="hidden text-ink-2 sm:table-cell">{t.category}</td>116 <td className="hidden lg:table-cell">117 {t.service_id ? (118 <Link href={`/service/${t.service_id}`} className="text-ink-2 hover:text-accent">119 {t.service_id}120 </Link>121 ) : (122 <span className="text-ink-3">—</span>123 )}124 </td>125 <td className="hidden sm:table-cell">126 {t.country ? (127 <Link href={`/country/${t.country.toLowerCase()}`} className="text-ink-2 hover:text-accent">128 {t.country}129 </Link>130 ) : (131 <span className="text-ink-3">global</span>132 )}133 </td>134 <td className="num r text-ink-2">{t.importance}</td>135 <td className="num r hidden text-ink-2 md:table-cell">{t.tier}</td>136 <td className="r">137 <PNum value={t.pressure} />138 </td>139 <td className="num r" style={{ color: t.ok_ratio_1h != null && t.ok_ratio_1h < 0.98 ? 'var(--p-high)' : undefined }}>140 {fmtPct(t.ok_ratio_1h, 1)}141 </td>142 <td className="num r text-ink-2">{fmtMs(t.ttfb_ms_median_1h)}</td>143 </tr>144 ))}145 </tbody>146 </table>147 </div>148 {pages > 1 && (149 <nav className="mt-3 flex items-center gap-2 text-[12px]" aria-label="Pagination">150 {page > 1 ? <Link href={pageHref(page - 1)} className="text-ink-2 hover:text-accent">← previous</Link> : <span className="text-ink-3">← previous</span>}151 <span className="num text-ink-3">152 {fmtInt((page - 1) * pageSize + 1)}–{fmtInt(Math.min(page * pageSize, total))} of {fmtInt(total)}153 </span>154 {page < pages ? <Link href={pageHref(page + 1)} className="text-ink-2 hover:text-accent">next →</Link> : <span className="text-ink-3">next →</span>}155 </nav>156 )}157 </div>158 );159}160