SPB Git forge

spb/ai-atlas

Public
41commits 1branches 0releases
4.6 MBsize
maindefault branch
12 days agolast push
HTML 77.2% TypeScript 10.5% Python 9.6% JavaScript 2.5%
2.9 KB · 69 lines tsx
Raw Blame History
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