feat(browse): sticky tabs + per-tab search bar (server-side, scope-aware)
The Browse tab nav scrolled away (operator didn't know it existed) and Posts had no search. Roll the tab strip + a shared search field into one sticky block pinned under the 64px TopNav. - Posts gains server-side text search: PostFeedService.scroll()/around() + /api/posts accept q (ILIKE over post_title OR description), applied INSIDE the artist/platform WHERE so search stays scoped to the active filter. Scope shown as clearable chips next to the search field. - Artists/Tags search consolidates into the sticky bar: their inner search boxes are removed; they react to route.query.q (q is deep- linkable, e.g. /browse?tab=posts&q=foo). Platform/kind filters stay. - Posts empty state now distinguishes 'no matches' from 'no posts yet'. Tests: posts q-search matches title|description and stays artist-scoped (service); q passthrough (api). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -88,6 +88,16 @@ async def test_list_filter_propagates_artist(client, seeded_post):
|
||||
assert body["items"][0]["id"] == post.id
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_text_search_propagates(client, seeded_post):
|
||||
_, _, post = seeded_post # title "Hello", description "<p>hi</p>"
|
||||
hit = await client.get("/api/posts?q=hello")
|
||||
assert hit.status_code == 200
|
||||
assert [it["id"] for it in (await hit.get_json())["items"]] == [post.id]
|
||||
miss = await client.get("/api/posts?q=zzznope")
|
||||
assert (await miss.get_json())["items"] == []
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_detail_200_for_known(client, seeded_post):
|
||||
_, _, post = seeded_post
|
||||
|
||||
Reference in New Issue
Block a user