Repository navigation
feat: per-type search filters on list pages (closes #25) - #34
Merged
Merged
Conversation
Adds a faceted-filter layer on every list page, end to end. URL-synced
so deep-linking + browser back/forward work.
Backend
- GET /api/movies extended: yearFrom / yearTo (range), director,
studio, genre (substring), status, watchStatus, ratingMin, tag
(repeatable).
- GET /api/music: yearFrom / yearTo, artist, label, genre, status,
ratingMin, tag.
- GET /api/games: yearFrom / yearTo, publisher, developer, status,
completionStatus, digitalStore, ratingMin, tag. Pre-existing platform
+ digital + year stay.
- Cross-type semantics from the issue:
* AND across distinct filters.
* OR within the multi-value `tag` filter (item matches if any of
its tags are in the requested set).
- Tag values are normalised lower-case to match TagResolver before the
predicate runs, so the URL doesn't have to care about case.
- Each predicate is explicit per type (no generic builder) -- three
endpoints aren't worth that abstraction yet.
Client
- services/filters.ts: per-type Filters interfaces (MovieFilters /
AlbumFilters / GameFilters), filtersToParams() / paramsToFilters()
round-trip via URLSearchParams, and useFiltersState() that
bidirectionally syncs filters with the URL search-string via
react-router. Free-text `q` and any other unrelated params are
preserved on every update so the search input doesn't get clobbered.
- services/collection.ts: useList(type, query, filters?) serializes
filters into the request URL and folds them into the TanStack
queryKey so changes refetch deterministically.
- components/FiltersPanel.tsx: collapsible disclosure trigger with an
"active" count + "Clear all"; opens to a grid of fields appropriate
to the type (year range, free-text substring inputs, enum selects,
digital/physical toggle, min-rating select, etc.). Active filters
render as removable chips above the panel body so users can drop one
at a time without opening the panel.
- CollectionList wires the panel above the search input and threads
the filters into useList.
Tests
- Server (+9): movies year range / director-studio-genre /
status+watch+rating / tag OR / AND combination; music year range +
artist/label/genre/status/rating; games year range + publisher /
developer / completionStatus / rating; existing tests stay green
(we added flat new params with optional binders, no breaking
changes).
- Client (+7): filtersToParams round-trip (omitting empty / NaN,
repeating arrays, stringifying booleans/numbers, dropping empty
array entries) plus activeFilterCount semantics.
Server 286/286 (was 278; +8). Client 73/73 (was 66; +7). Build clean.
does leave `q` alone
The PR description claimed "Clear all wipes every filter but leaves
the search input's q alone" -- but the search input was useState, not
URL-synced, so the URL never had a `q` to begin with. The claim was
vacuously true and confusing in practice ("I cleared and my search
went too").
Move the search input to ?q= via useSearchParams. useFiltersState
already preserves every param it doesn't own, so the two states are
genuinely independent now: Clear-all drops only the filter params,
the search input stays put across filter changes, and the whole list
state (query + filters) deep-links from a single URL.
3 of 5 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #25. Adds a faceted-filter layer on every list page, end to end. URL-synced so deep-linking + browser back/forward work, AND across distinct filters and OR within multi-value filters per the issue spec.
Summary
Backend
Each list endpoint gains the missing filter params (existing ones —
format,year,platform,digital— stay for back-compat):GET /api/moviesyearFrom,yearTo,director,studio,genre,status,watchStatus,ratingMin,tag(repeatable)GET /api/musicyearFrom,yearTo,artist,label,genre,status,ratingMin,tagGET /api/gamesyearFrom,yearTo,publisher,developer,status,completionStatus,digitalStore,ratingMin,tagANDacross distinct filters,ORwithin the multi-valuetagfilter (item matches if any of its tags are in the requested set). Per the issue: "OR semantics within multi-value filters."TagResolver), so the URL doesn't have to care about case.Frontend
services/filters.ts— per-typeFiltersinterfaces,filtersToParams()/paramsToFilters()round-trip viaURLSearchParams, and auseFiltersState(type)hook that bidirectionally syncs filters with the URL?search. Unrelated params (e.g. the search input'sqif you wire one up) are preserved on every update.services/collection.ts—useList(type, query, filters?)serializes filters into the request URL and folds them into the TanStack query key so a filter change refetches deterministically.components/FiltersPanel.tsx— collapsible disclosure with an active-count + Clear all; opens to a grid of fields appropriate to the type (year range, free-text substring inputs, enum selects, digital/physical toggle, min-rating select). Active filters render as removable chips above the panel body so users can drop one at a time.CollectionListwires the panel above the search input and threads the filters intouseList.Test plan
CI
dotnet test— server 286/286 (was 278; +8).npm test -- --run— client 73/73 (was 66; +7).Backend (per-type filter tests)
director=Nolan&yearFrom=2015).Frontend (
filters.test.ts)filtersToParams: omitsundefined/null/ empty /NaN; repeats array values; stringifies booleans + numbers; drops empty strings inside arrays.activeFilterCount: counts set scalars, treats empty arrays as inactive, ignores empty strings.Manual
/movies, click Filters, setDirector: Nolan+Year range: 2015–. List narrows to a single row. URL shows?director=Nolan&yearFrom=2015. Browser back undoes filters one step.Director: Nolan✕ → removed from URL + list reflows.qalone./musicand/games.Out of scope
OrderByDescending(AddedAt)stays.LIKE %x%is enough at our cardinality.?platform=Ps5&platform=Switch) — single-value selects today; documented as a future enhancement if the URL parameter starts feeling cramped. The server already accepts repeatedtagso the same shape will plug in.🤖 Generated with Claude Code
Generated by Claude Code