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 {