Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions src/components/ngos/NgoExplorer.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(<NgoExplorer ngos={ngos} />);

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(<NgoExplorer ngos={ngos} />);

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(<NgoExplorer ngos={ngos} />);
Expand Down
28 changes: 22 additions & 6 deletions src/components/ngos/NgoExplorer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,14 +32,22 @@ 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();
const searchParams = useSearchParams();

const [searchQuery, setSearchQuery] = useState(() => searchParams.get('q') ?? '');
const [page, setPage] = useState(() => parsePage(searchParams.get('page')));
const [sortBy, setSortBy] = useState<SortOption>('newest');
const [sortBy, setSortBy] = useState<SortOption>(() => parseSort(searchParams.get('sort')));

// Sync incoming URL changes before rendering, while keeping immediate
// local updates from the handlers below until navigation completes.
Expand All @@ -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) {
Expand All @@ -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 });
Expand All @@ -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 {
Expand Down
Loading