diff --git a/src/components/ngos/NgoExplorer.test.tsx b/src/components/ngos/NgoExplorer.test.tsx index 75f2ce3..c1e5b36 100644 --- a/src/components/ngos/NgoExplorer.test.tsx +++ b/src/components/ngos/NgoExplorer.test.tsx @@ -114,6 +114,34 @@ describe('NgoExplorer', () => { expect(screen.getAllByRole('heading', { level: 2 })).toHaveLength(15); }); + it('restores the sort from the URL on mount', () => { + mockSearchParams = new URLSearchParams('sort=most-active-streams'); + const ngos = [ + makeNgo({ id: 'a', name: 'Alpha', stats: { totalCommitted: '0', totalWithdrawn: '0', activeStreamCount: 1, donorCount: 0 } }), + makeNgo({ id: 'b', name: 'Beta', stats: { totalCommitted: '0', totalWithdrawn: '0', activeStreamCount: 5, donorCount: 0 } }), + makeNgo({ id: 'c', name: 'Gamma', stats: { totalCommitted: '0', totalWithdrawn: '0', activeStreamCount: 3, donorCount: 0 } }), + ]; + render(); + + expect(screen.getByRole('combobox')).toHaveValue('most-active-streams'); + + const headings = screen.getAllByRole('heading', { level: 2 }).map((h) => h.textContent); + expect(headings).toEqual(['Beta', 'Gamma', 'Alpha']); + }); + + it('writes the sort to the URL when changed (and clears page)', () => { + mockSearchParams = new URLSearchParams('q=Water&page=2'); + const ngos = buildNgos(); + render(); + + fireEvent.change(screen.getByRole('combobox'), { target: { value: 'most-committed' } }); + + expect(mockReplace).toHaveBeenLastCalledWith( + '/ngos?q=Water&sort=most-committed', + { scroll: false }, + ); + }); + it('shows an empty state when the search matches nothing', () => { const ngos = buildNgos(); render(); diff --git a/src/components/ngos/NgoExplorer.tsx b/src/components/ngos/NgoExplorer.tsx index 7c5b34f..a3372b9 100644 --- a/src/components/ngos/NgoExplorer.tsx +++ b/src/components/ngos/NgoExplorer.tsx @@ -32,6 +32,14 @@ function parsePage(value: string | null): number { return Number.isInteger(page) && page > 0 ? page : 1; } +// Only a non-default sort is carried in the URL; 'newest' is the implicit +// default and is omitted to keep shared links tidy. +const SORT_OPTIONS: readonly SortOption[] = ['newest', 'most-active-streams', 'most-committed']; + +function parseSort(value: string | null): SortOption { + return SORT_OPTIONS.includes(value as SortOption) ? (value as SortOption) : 'newest'; +} + export function NgoExplorer({ ngos }: { ngos: NgoProfile[] }) { const router = useRouter(); const pathname = usePathname(); @@ -39,7 +47,7 @@ export function NgoExplorer({ ngos }: { ngos: NgoProfile[] }) { const [searchQuery, setSearchQuery] = useState(() => searchParams.get('q') ?? ''); const [page, setPage] = useState(() => parsePage(searchParams.get('page'))); - const [sortBy, setSortBy] = useState('newest'); + const [sortBy, setSortBy] = useState(() => parseSort(searchParams.get('sort'))); // Sync incoming URL changes before rendering, while keeping immediate // local updates from the handlers below until navigation completes. @@ -49,14 +57,15 @@ export function NgoExplorer({ ngos }: { ngos: NgoProfile[] }) { setPreviousQueryString(queryString); setSearchQuery(searchParams.get('q') ?? ''); setPage(parsePage(searchParams.get('page'))); + setSortBy(parseSort(searchParams.get('sort'))); } const visibleCount = page * PAGE_SIZE; - // Only q and page are synced (see acceptance criteria) — sort stays - // local. Replaced rather than pushed so search-as-you-type doesn't fill - // the back-button history with one entry per keystroke. - function syncParams(next: { q?: string; page?: number }): void { + // q, page and sort are synced to the URL so links/back-forward preserve + // the full explorer state. router.replace (not push) so search-as-you-type + // doesn't fill history with one entry per keystroke. + function syncParams(next: { q?: string; page?: number; sort?: SortOption }): void { const params = new URLSearchParams(searchParams.toString()); if (next.q !== undefined) { @@ -73,6 +82,13 @@ export function NgoExplorer({ ngos }: { ngos: NgoProfile[] }) { params.delete('page'); } } + if (next.sort !== undefined) { + if (next.sort !== 'newest') { + params.set('sort', next.sort); + } else { + params.delete('sort'); + } + } const query = params.toString(); router.replace(query ? `${pathname}?${query}` : pathname, { scroll: false }); @@ -98,7 +114,7 @@ export function NgoExplorer({ ngos }: { ngos: NgoProfile[] }) { function handleSortChange(value: SortOption): void { setSortBy(value); setPage(1); - syncParams({ page: 1 }); + syncParams({ sort: value, page: 1 }); } function handleLoadMore(): void {