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
|
||||
|
||||
@@ -180,6 +180,54 @@ async def test_scroll_filters_by_platform(db):
|
||||
assert [it["id"] for it in page["items"]] == [pp.id]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_scroll_text_search_matches_title_or_description(db):
|
||||
artist = await _seed_artist(db, "alice-search")
|
||||
src = await _seed_source(db, artist.id, "patreon", "https://p/alice-se")
|
||||
now = datetime.now(UTC)
|
||||
by_title = await _seed_post(
|
||||
db, src.id, external_id="T", post_date=now,
|
||||
title="Dragon Quest cover", description="<p>nothing</p>",
|
||||
)
|
||||
by_desc = await _seed_post(
|
||||
db, src.id, external_id="D", post_date=now - timedelta(days=1),
|
||||
title="Untitled", description="<p>a sleeping dragon</p>",
|
||||
)
|
||||
await _seed_post(
|
||||
db, src.id, external_id="N", post_date=now - timedelta(days=2),
|
||||
title="Cat nap", description="<p>just a cat</p>",
|
||||
)
|
||||
await db.commit()
|
||||
|
||||
page = await PostFeedService(db).scroll(cursor=None, limit=10, q="dragon")
|
||||
ids = {it["id"] for it in page["items"]}
|
||||
# ILIKE is case-insensitive and spans title OR description; the cat post
|
||||
# matches neither.
|
||||
assert ids == {by_title.id, by_desc.id}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_scroll_text_search_stays_scoped_to_artist(db):
|
||||
alice = await _seed_artist(db, "alice-scope")
|
||||
bob = await _seed_artist(db, "bob-scope")
|
||||
src_a = await _seed_source(db, alice.id, "patreon", "https://p/alice-sc")
|
||||
src_b = await _seed_source(db, bob.id, "patreon", "https://p/bob-sc")
|
||||
now = datetime.now(UTC)
|
||||
alice_hit = await _seed_post(
|
||||
db, src_a.id, external_id="AH", post_date=now, title="comet study",
|
||||
)
|
||||
# Same query word, different artist — must be excluded by the scope.
|
||||
await _seed_post(
|
||||
db, src_b.id, external_id="BH", post_date=now, title="comet sketch",
|
||||
)
|
||||
await db.commit()
|
||||
|
||||
page = await PostFeedService(db).scroll(
|
||||
cursor=None, limit=10, q="comet", artist_id=alice.id,
|
||||
)
|
||||
assert [it["id"] for it in page["items"]] == [alice_hit.id]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_scroll_combined_artist_and_platform(db):
|
||||
alice = await _seed_artist(db, "alice-combo")
|
||||
|
||||
Reference in New Issue
Block a user