import { useMemo, useState } from 'react' import type { GameRow } from '../estimate' import { fmtHoursPrecise, fmtPlaytime } from '../format' import { tierLabel } from '../protondb' interface Props { rows: GameRow[] search: string onSearch: (value: string) => void onToggleExclude: (appId: number) => void } type SortKey = | 'name' | 'completion' | 'playtime' | 'rush' | 'leisure' | 'worst' | 'count_comp' | 'proton' const DISPLAY_CAP = 300 const COLUMNS: { key: SortKey; label: string; numeric: boolean }[] = [ { key: 'name', label: 'Game', numeric: false }, { key: 'completion', label: '%', numeric: true }, { key: 'playtime', label: 'Played', numeric: true }, { key: 'rush', label: 'Rush', numeric: true }, { key: 'leisure', label: 'Leisure', numeric: true }, { key: 'worst', label: 'Worst', numeric: true }, { key: 'count_comp', label: 'HLTB n', numeric: true }, { key: 'proton', label: 'ProtonDB', numeric: true }, ] function sortValue(row: GameRow, key: SortKey): number | string { const g = row.game switch (key) { case 'name': return g.name.toLowerCase() case 'completion': return g.completion_pct case 'playtime': return g.playtime_minutes case 'rush': return row.rush case 'leisure': return row.leisure case 'worst': return row.worst case 'count_comp': return g.count_comp case 'proton': return g.protondb_score } } function hltbUrl(game: GameRow['game']): string { if (game.hltb_game_id > 0) { return `https://howlongtobeat.com/game/${game.hltb_game_id}` } return `https://howlongtobeat.com/?q=${encodeURIComponent(game.name)}` } export function GameTable({ rows, search, onSearch, onToggleExclude }: Props) { const [sortKey, setSortKey] = useState('leisure') const [asc, setAsc] = useState(true) const visible = useMemo(() => { const q = search.trim().toLowerCase() const filtered = q ? rows.filter((r) => r.game.name.toLowerCase().includes(q)) : rows const sorted = [...filtered].sort((a, b) => { const va = sortValue(a, sortKey) const vb = sortValue(b, sortKey) const cmp = va < vb ? -1 : va > vb ? 1 : 0 return asc ? cmp : -cmp }) return sorted }, [rows, search, sortKey, asc]) const onHeader = (key: SortKey) => { if (key === sortKey) setAsc(!asc) else { setSortKey(key) setAsc(key === 'name') } } const shown = visible.slice(0, DISPLAY_CAP) return (

Games ({visible.length})

onSearch(e.target.value)} />
{COLUMNS.map((c) => ( ))} {shown.map((r) => ( ))}
onHeader(c.key)} > {c.label} {sortKey === c.key ? (asc ? ' ▲' : ' ▼') : ''} Keep
{r.game.name} {r.noData && no data} {r.game.completion_pct.toFixed(0)} {fmtPlaytime(r.game.playtime_minutes)} {fmtHoursPrecise(r.rush)} {fmtHoursPrecise(r.leisure)} {fmtHoursPrecise(r.worst)} {r.game.count_comp} {tierLabel(r.game.protondb_tier, r.game.protondb_trending_tier)} onToggleExclude(r.game.app_id)} aria-label={r.excluded ? 'Re-include' : 'Exclude'} />
{visible.length > DISPLAY_CAP && (

Showing first {DISPLAY_CAP} of {visible.length}. Use search to narrow.

)}
) }