feat(browse): sticky tabs + per-tab search bar (server-side, scope-aware)
CI / lint (push) Successful in 2s
CI / frontend-build (push) Successful in 19s
CI / backend-lint-and-test (push) Successful in 25s
CI / integration (push) Successful in 3m10s

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:
2026-06-12 00:04:06 -04:00
parent 90c68f8b2a
commit 2c544ad5af
9 changed files with 237 additions and 44 deletions
+5 -1
View File
@@ -11,7 +11,7 @@ export const usePostsStore = defineStore('posts', () => {
const cursor = ref(null)
const { loading, error, run } = useAsyncAction()
const done = ref(false)
const filters = ref({ artist_id: null, platform: null })
const filters = ref({ artist_id: null, platform: null, q: null })
// In-context (anchored) view: bidirectional cursors so the feed can load
// newer posts on scroll-up and older posts on scroll-down around a post.
@@ -32,6 +32,7 @@ export const usePostsStore = defineStore('posts', () => {
if (cursor.value) q.cursor = cursor.value
if (filters.value.artist_id != null) q.artist_id = filters.value.artist_id
if (filters.value.platform) q.platform = filters.value.platform
if (filters.value.q) q.q = filters.value.q
return q
}
@@ -47,6 +48,7 @@ export const usePostsStore = defineStore('posts', () => {
filters.value = {
artist_id: newFilters?.artist_id ?? null,
platform: newFilters?.platform ?? null,
q: newFilters?.q ?? null,
}
_reset()
await loadMore()
@@ -78,6 +80,7 @@ export const usePostsStore = defineStore('posts', () => {
const p = { ...extra }
if (filters.value.artist_id != null) p.artist_id = filters.value.artist_id
if (filters.value.platform) p.platform = filters.value.platform
if (filters.value.q) p.q = filters.value.q
return p
}
@@ -93,6 +96,7 @@ export const usePostsStore = defineStore('posts', () => {
filters.value = {
artist_id: newFilters?.artist_id ?? null,
platform: newFilters?.platform ?? null,
q: newFilters?.q ?? null,
}
loading.value = true
error.value = null