shared page: window the rendered rows — DOM no longer grows with history
ci / lint-test (push) Successful in 1m4s

The table rendered every split row (1,279 today, ~600/yr growth), so page
weight was unbounded even after the re-render fix. Render the first 100 of
the filtered/sorted set with Show more (+100) / Show all; any filter or
sort change resets the window. Data stays fully client-side on purpose —
search still counts matches across all history — only the DOM is capped.
Headless-measured expand stall: 165ms -> 35ms.
This commit is contained in:
2026-08-10 19:46:58 +10:00
parent b4f28010df
commit 872da3b12c
+31 -1
View File
@@ -498,6 +498,17 @@ export default function SharedPage() {
const [paymentModal, setPaymentModal] = useState<{ id: number; name: string; balance: number } | null>(null); const [paymentModal, setPaymentModal] = useState<{ id: number; name: string; balance: number } | null>(null);
const [showHistory, setShowHistory] = useState<number | null>(null); const [showHistory, setShowHistory] = useState<number | null>(null);
const [editModal, setEditModal] = useState<SharedTransactionRow | null>(null); const [editModal, setEditModal] = useState<SharedTransactionRow | null>(null);
// Rendering is windowed even though the data is not: the query returns every
// split row (1,279 as of 2026-08, growing ~600/yr) so search and sort stay
// instant over full history, but the DOM stops at visibleCount — an unbounded
// table was what made expanding a receipt freeze the browser. "Show more"
// extends the window; changing any filter resets it.
const [visibleCount, setVisibleCount] = useState(100);
useEffect(() => {
setVisibleCount(100);
}, [search, sortCol, sortDir, tagIds, participantId]);
const visible = (transactions as SharedTransactionRow[]).slice(0, visibleCount);
// Receipt expansion, same pattern as the transactions page. The shared // Receipt expansion, same pattern as the transactions page. The shared
// viewer is a split participant, so /api/transactions/[id]/order // viewer is a split participant, so /api/transactions/[id]/order
// authorises them — the item list is part of what was shared. // authorises them — the item list is part of what was shared.
@@ -681,7 +692,7 @@ export default function SharedPage() {
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{(transactions as SharedTransactionRow[]).map((tx) => ( {visible.map((tx) => (
<SharedTxRow <SharedTxRow
key={tx.id} key={tx.id}
tx={tx} tx={tx}
@@ -694,6 +705,25 @@ export default function SharedPage() {
</tbody> </tbody>
</table> </table>
)} )}
{!txLoading && transactions.length > visible.length && (
<div className="px-4 py-3 border-t border-zinc-800 flex items-center gap-3 text-xs">
<button
onClick={() => setVisibleCount((c) => c + 100)}
className="px-3 py-1.5 bg-zinc-800 hover:bg-zinc-700 text-zinc-300 rounded-lg"
>
Show more
</button>
<button
onClick={() => setVisibleCount(transactions.length)}
className="text-zinc-500 hover:text-zinc-300 underline underline-offset-2"
>
Show all
</button>
<span className="text-zinc-600 ml-auto tabular-nums">
showing {visible.length} of {transactions.length}
</span>
</div>
)}
</div> </div>
{/* Payment modal */} {/* Payment modal */}