Skip to content

Commit 8552417

Browse files
committed
Drop "Apply" step from filter mechanism
* Selecting a filter now fetches and filters the listing immediately instead of requiring the user to click on "Apply Filters"
1 parent 6bcf461 commit 8552417

4 files changed

Lines changed: 54 additions & 76 deletions

File tree

app/fact-checks/page.tsx

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,7 @@
11
import type { Metadata } from "next";
22
import { FactChecksContentDesks } from "@/components/fact-checks/FactChecksContentDesks";
33
import { FactChecksExplorer } from "@/components/fact-checks/FactChecksExplorer";
4-
import {
5-
filtersToQuery,
6-
parseFilterParams,
7-
} from "@/lib/data/fact-check-filters";
4+
import { parseFilterParams } from "@/lib/data/fact-check-filters";
85
import {
96
clampPage,
107
pageOffset,
@@ -55,8 +52,6 @@ export default async function FactChecksPage({
5552
return (
5653
<>
5754
<FactChecksExplorer
58-
// Re-key on the applied filters so navigation resets the staged selection.
59-
key={JSON.stringify(filtersToQuery(filters))}
6055
stories={listing.stories}
6156
page={listing.page}
6257
totalPages={listing.totalPages}

components/fact-checks/FactChecksExplorer.tsx

Lines changed: 49 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"use client";
22

33
import { usePathname, useRouter } from "next/navigation";
4-
import { useMemo, useState } from "react";
4+
import { useOptimistic, useState, useTransition } from "react";
55
import { Pagination } from "@/components/ui/Pagination";
66
import { Container } from "@/components/ui/SectionHeading";
77
import { StoryCard } from "@/components/ui/StoryCard";
@@ -12,9 +12,9 @@ import {
1212
} from "@/lib/data/fact-check-filters";
1313
import { FILTERS, type FilterDimension } from "@/lib/fact-checks-content";
1414
import type { Story } from "@/lib/home-content";
15-
import { FilterBar, type Selection } from "./FilterBar";
15+
import { FilterBar } from "./FilterBar";
1616

17-
function clone(sel: Selection): Selection {
17+
function clone(sel: FilterSelection): FilterSelection {
1818
return {
1919
region: [...sel.region],
2020
language: [...sel.language],
@@ -26,11 +26,14 @@ function clone(sel: Selection): Selection {
2626
* The fact-checks grid + filter bar. Filtering and pagination are both
2727
* **server-side and URL-driven**: `stories` arrive already filtered/paged, and
2828
* every control navigates by mutating the query string (`?region=…&topic=…&page=N`),
29-
* which re-runs the server fetch. `filters` is the applied selection parsed from
30-
* the URL; `selected` is the reader's staged edits, committed via "Apply Filters".
29+
* which re-runs the server fetch.
3130
*
32-
* The parent keys this component by the applied filters, so navigation remounts
33-
* it and `selected` re-initializes from the new URL state.
31+
* Filters **auto-apply** — there's no "Apply" step. The URL (`filters` prop) is
32+
* the single source of truth; toggling an option, removing a chip, or clearing
33+
* navigates straight away. `useOptimistic` mirrors the change so the checkboxes
34+
* and chips update instantly while the server re-fetches the grid in a
35+
* transition. Filter changes use `replace` (so a burst of toggles doesn't spam
36+
* history); pagination uses `push`.
3437
*/
3538
export function FactChecksExplorer({
3639
stories,
@@ -45,66 +48,63 @@ export function FactChecksExplorer({
4548
}) {
4649
const router = useRouter();
4750
const pathname = usePathname();
51+
const [, startTransition] = useTransition();
4852

49-
const [selected, setSelected] = useState<Selection>(() => clone(filters));
53+
// Optimistic mirror of the applied (URL) filters — instant checkbox/chip
54+
// feedback; rebases to `filters` once the navigation transition resolves.
55+
const [optimistic, setOptimistic] = useOptimistic(filters);
5056
const [openDropdown, setOpenDropdown] = useState<FilterDimension | null>(
5157
null,
5258
);
5359

54-
const chips = useMemo(
55-
() =>
56-
FILTERS.flatMap(({ dimension }) =>
57-
selected[dimension].map((value) => ({ dimension, value })),
58-
),
59-
[selected],
60+
const chips = FILTERS.flatMap(({ dimension }) =>
61+
optimistic[dimension].map((value) => ({ dimension, value })),
6062
);
6163

62-
const toggleOption = (dimension: FilterDimension, value: string) => {
63-
setSelected((prev) => {
64-
const next = clone(prev);
65-
next[dimension] = next[dimension].includes(value)
66-
? next[dimension].filter((v) => v !== value)
67-
: [...next[dimension], value];
68-
return next;
64+
// Commit a filter/page change to the URL; the server re-fetches that slice.
65+
const navigate = (
66+
next: FilterSelection,
67+
nextPage: number,
68+
mode: "push" | "replace",
69+
) => {
70+
const params = new URLSearchParams(filtersToQuery(next));
71+
if (nextPage > 1) params.set("page", String(nextPage));
72+
const qs = params.toString();
73+
const url = qs ? `${pathname}?${qs}` : pathname;
74+
startTransition(() => {
75+
setOptimistic(next);
76+
router[mode](url);
6977
});
7078
};
7179

72-
// Removing a chip commits immediately (like Clear, but for one value) rather
73-
// than only un-staging it — the listing re-queries on the spot. `setSelected`
74-
// gives instant visual feedback; `navigate` re-fetches and the remount then
75-
// re-syncs `selected` from the new URL.
76-
const removeChip = (dimension: FilterDimension, value: string) => {
77-
const next = clone(selected);
78-
next[dimension] = next[dimension].filter((v) => v !== value);
79-
setSelected(next);
80-
navigate(next, 1);
81-
};
80+
// Any filter change auto-applies and resets to page 1.
81+
const commitFilters = (next: FilterSelection) => navigate(next, 1, "replace");
8282

83-
const toggleDropdown = (dimension: FilterDimension) =>
84-
setOpenDropdown((cur) => (cur === dimension ? null : dimension));
85-
86-
// Navigate to a new filter+page combination; the server re-fetches that slice.
87-
const navigate = (nextFilters: FilterSelection, nextPage: number) => {
88-
const params = new URLSearchParams(filtersToQuery(nextFilters));
89-
if (nextPage > 1) params.set("page", String(nextPage));
90-
const qs = params.toString();
91-
router.push(qs ? `${pathname}?${qs}` : pathname);
83+
const toggleOption = (dimension: FilterDimension, value: string) => {
84+
const next = clone(optimistic);
85+
next[dimension] = next[dimension].includes(value)
86+
? next[dimension].filter((v) => v !== value)
87+
: [...next[dimension], value];
88+
commitFilters(next);
9289
};
9390

94-
// Pagination keeps the applied filters; only the page changes.
95-
const goToPage = (next: number) => navigate(filters, next);
96-
97-
const apply = () => {
98-
setOpenDropdown(null);
99-
navigate(selected, 1);
91+
const removeChip = (dimension: FilterDimension, value: string) => {
92+
const next = clone(optimistic);
93+
next[dimension] = next[dimension].filter((v) => v !== value);
94+
commitFilters(next);
10095
};
10196

10297
const clear = () => {
103-
setSelected(clone(EMPTY_FILTERS));
10498
setOpenDropdown(null);
105-
navigate(EMPTY_FILTERS, 1);
99+
commitFilters(EMPTY_FILTERS);
106100
};
107101

102+
const toggleDropdown = (dimension: FilterDimension) =>
103+
setOpenDropdown((cur) => (cur === dimension ? null : dimension));
104+
105+
// Pagination keeps the applied filters; only the page changes.
106+
const goToPage = (next: number) => navigate(filters, next, "push");
107+
108108
const [feature, secondary, ...grid] = stories;
109109

110110
return (
@@ -113,13 +113,12 @@ export function FactChecksExplorer({
113113
<section className="bg-neutral-50">
114114
<Container className="py-14 lg:py-16">
115115
<FilterBar
116-
selected={selected}
116+
selected={optimistic}
117117
openDropdown={openDropdown}
118118
chips={chips}
119119
onToggleDropdown={toggleDropdown}
120120
onToggleOption={toggleOption}
121121
onRemoveChip={removeChip}
122-
onApply={apply}
123122
onClear={clear}
124123
/>
125124
</Container>

components/fact-checks/FilterBar.tsx

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,6 @@ export function FilterBar({
165165
onToggleDropdown,
166166
onToggleOption,
167167
onRemoveChip,
168-
onApply,
169168
onClear,
170169
}: {
171170
selected: Selection;
@@ -174,7 +173,6 @@ export function FilterBar({
174173
onToggleDropdown: (dimension: FilterDimension) => void;
175174
onToggleOption: (dimension: FilterDimension, value: string) => void;
176175
onRemoveChip: (dimension: FilterDimension, value: string) => void;
177-
onApply: () => void;
178176
onClear: () => void;
179177
}) {
180178
return (
@@ -227,14 +225,6 @@ export function FilterBar({
227225
);
228226
})}
229227

230-
<button
231-
type="button"
232-
onClick={onApply}
233-
className="inline-flex items-center gap-1.5 text-sm font-semibold text-pesacheck-blue"
234-
>
235-
<Check />
236-
Apply Filters
237-
</button>
238228
<button
239229
type="button"
240230
onClick={onClear}

components/search/SearchExplorer.tsx

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -92,8 +92,9 @@ function SearchIllustration() {
9292
}
9393

9494
export function SearchExplorer({ query }: { query: string }) {
95+
// Filters auto-apply: `selected` is the live filter set (no separate "Apply"
96+
// step), so results recompute as the reader toggles options or removes chips.
9597
const [selected, setSelected] = useState<Selection>(() => clone(EMPTY));
96-
const [applied, setApplied] = useState<Selection>(() => clone(EMPTY));
9798
const [openDropdown, setOpenDropdown] = useState<FilterDimension | null>(
9899
null,
99100
);
@@ -108,8 +109,8 @@ export function SearchExplorer({ query }: { query: string }) {
108109

109110
const results = useMemo(
110111
() =>
111-
POOL.filter((s) => matchesQuery(s, query) && matchesFilters(s, applied)),
112-
[query, applied],
112+
POOL.filter((s) => matchesQuery(s, query) && matchesFilters(s, selected)),
113+
[query, selected],
113114
);
114115

115116
const toggleOption = (dimension: FilterDimension, value: string) => {
@@ -133,14 +134,8 @@ export function SearchExplorer({ query }: { query: string }) {
133134
const toggleDropdown = (dimension: FilterDimension) =>
134135
setOpenDropdown((cur) => (cur === dimension ? null : dimension));
135136

136-
const apply = () => {
137-
setApplied(clone(selected));
138-
setOpenDropdown(null);
139-
};
140-
141137
const clear = () => {
142138
setSelected(clone(EMPTY));
143-
setApplied(clone(EMPTY));
144139
setOpenDropdown(null);
145140
};
146141

@@ -209,7 +204,6 @@ export function SearchExplorer({ query }: { query: string }) {
209204
onToggleDropdown={toggleDropdown}
210205
onToggleOption={toggleOption}
211206
onRemoveChip={removeChip}
212-
onApply={apply}
213207
onClear={clear}
214208
/>
215209
</Container>

0 commit comments

Comments
 (0)