HTML 77.2%
TypeScript 10.5%
Python 9.6%
JavaScript 2.5%
1'use client';2import { useState } from 'react';3import { clientApi } from '@/lib/client-api';4import { fmtDate } from '@/lib/format';5import type { ChangeEvent } from '@/lib/types';6import { ChangeRow, groupByDay } from './change-row';78/**9 * Cursor pagination for /changes: the server renders the first page; this appends more via `before=<cursor>`.10 * `qs` carries the active filters (category, type, importance_min, since, include_backfill, date_field…). The cursor is11 * the API's `next_before` (1.1: follows `date_field`, `occurred_at` by default) — falls back to the last row's date.12 */13export function LoadMore({ qs, initialCursor, lastDay, dateField = 'occurred' }: { qs: string; initialCursor: string | null; lastDay: string | null; dateField?: 'occurred' | 'observed' }) {14 const [items, setItems] = useState<ChangeEvent[]>([]);15 const [cursor, setCursor] = useState<string | null>(initialCursor);16 const [loading, setLoading] = useState(false);17 const [error, setError] = useState(false);18 const groups = groupByDay(items, dateField);1920 const more = async () => {21 if (!cursor) return;22 setLoading(true);23 setError(false);24 try {25 const p = new URLSearchParams(qs);26 p.set('before', cursor);27 p.set('limit', '50');28 const res = (await clientApi.changes(p.toString())) as Awaited<ReturnType<typeof clientApi.changes>> & { next_before?: string | null };29 setItems((prev) => [...prev, ...res.items]);30 const last = res.items[res.items.length - 1];31 const fallback = last ? (dateField === 'occurred' ? last.occurred_at ?? last.effective_at ?? last.observed_at : last.observed_at) : null;32 setCursor(res.items.length < 50 || !last ? null : res.next_before ?? fallback);33 } catch {34 setError(true);35 } finally {36 setLoading(false);37 }38 };3940 return (41 <>42 {groups.map((g, i) => (43 <section key={g.day} className="mt-8">44 {(i > 0 || g.day !== lastDay) && (45 <h2 className="eyebrow sticky top-[var(--header-h)] z-10 -mx-4 bg-canvas/95 px-4 py-2 backdrop-blur md:mx-0 md:px-0">46 {fmtDate(g.day)} <span className="tnum text-ink-3">{g.items.length}</span>47 </h2>48 )}49 <ul className="border-t border-rule">50 {g.items.map((e) => (51 <ChangeRow key={e.id} e={e} showDate />52 ))}53 </ul>54 </section>55 ))}56 <div className="mt-6 flex items-center gap-3">57 {cursor ? (58 <button type="button" onClick={more} disabled={loading} className="h-10 border border-rule px-4 text-sm text-ink-2 hover:border-rule-strong hover:text-ink disabled:opacity-50" data-load-more>59 {loading ? 'Loading…' : 'Load older events'}60 </button>61 ) : (62 <p className="text-xs text-ink-3">End of the recorded history for these filters.</p>63 )}64 {error && <p className="text-xs text-danger">Could not load more — try again.</p>}65 </div>66 </>67 );68}69