From 86ff97bc824bf7652f9c3de3a6302c0479f62651 Mon Sep 17 00:00:00 2001 From: neon <18559658+neonsolstice@users.noreply.github.com> Date: Thu, 15 Jan 2026 05:38:45 -0600 Subject: [PATCH] feat(client): add responsive header with global search and filter bar --- client/src/components/AppHeader.vue | 418 +++++++++++++----- .../book/composables/useGlobalSearch.ts | 46 ++ client/src/views/HomeView.vue | 15 +- server/src/db/migrations/meta/_journal.json | 2 +- server/src/modules/book/book.controller.ts | 7 + server/src/modules/book/book.repository.ts | 47 +- server/src/modules/book/book.service.ts | 6 + .../src/modules/book/dto/search-books.dto.ts | 15 + 8 files changed, 448 insertions(+), 108 deletions(-) create mode 100644 client/src/features/book/composables/useGlobalSearch.ts create mode 100644 server/src/modules/book/dto/search-books.dto.ts diff --git a/client/src/components/AppHeader.vue b/client/src/components/AppHeader.vue index 964803daf..7516fffac 100644 --- a/client/src/components/AppHeader.vue +++ b/client/src/components/AppHeader.vue @@ -1,153 +1,361 @@ - - - - - - - {{ title }} - - {{ loaded.toLocaleString() }}/{{ total.toLocaleString() }} - - - - - - - + + + + + + - + + + + + Searching... + No results + + + + {{ result.title ?? 'Untitled' }} + {{ result.authors.join(', ') }} + + + {{ result.libraryName }} + + + + + + + + + + + + + + {{ title }} + + + {{ loaded.toLocaleString() }}/{{ total.toLocaleString() }} + - + + + + + + + - - - - - + + + Searching... + No results + + + + {{ result.title ?? 'Untitled' }} + {{ result.authors.join(', ') }} + + + {{ result.libraryName }} + + + + + + + + + + + + + + + + - - - - Display + + + + - - - - Cover size - {{ props.coverSize }}px + + + + + + + + + + + + Display + + + Cover size + {{ props.coverSize }}px + + - - - - - - - Grid gap - {{ props.gridGap }}px + + + Grid gap + {{ props.gridGap }}px + + + + + + Accent + + + + Radius + - + + - + - - - Accent - - + + + + - - - Radius - - - - - - - - - - - - - - - - - - + + + + + + + + + View + + Grid + List + + + + + Display + + + + + {{ themeStore.theme === 'dark' ? 'Light mode' : 'Dark mode' }} + + + + + + + + + + + Display + + + + + Cover size + {{ props.coverSize }}px + + + + + + Grid gap + {{ props.gridGap }}px + + + + + + Accent + + + + Radius + + + + + diff --git a/client/src/features/book/composables/useGlobalSearch.ts b/client/src/features/book/composables/useGlobalSearch.ts new file mode 100644 index 000000000..ab7477460 --- /dev/null +++ b/client/src/features/book/composables/useGlobalSearch.ts @@ -0,0 +1,46 @@ +import { ref, watch, type Ref } from 'vue' +import { api } from '@/lib/api' + +export interface GlobalSearchResult { + id: number + title: string | null + authors: string[] + libraryId: number + libraryName: string +} + +export function useGlobalSearch(query: Ref) { + const results = ref([]) + const loading = ref(false) + let timer: ReturnType | null = null + + watch(query, (q) => { + if (timer) clearTimeout(timer) + if (q.trim().length < 2) { + results.value = [] + loading.value = false + return + } + loading.value = true + timer = setTimeout(async () => { + try { + const params = new URLSearchParams({ q: q.trim(), limit: '10' }) + const res = await api(`/api/books/search?${params}`) + if (!res.ok) throw new Error(`HTTP ${res.status}`) + results.value = await res.json() + } catch { + results.value = [] + } finally { + loading.value = false + } + }, 300) + }) + + function clear() { + if (timer) clearTimeout(timer) + results.value = [] + loading.value = false + } + + return { results, loading, clear } +} diff --git a/client/src/views/HomeView.vue b/client/src/views/HomeView.vue index dad9739c7..9a47d431a 100644 --- a/client/src/views/HomeView.vue +++ b/client/src/views/HomeView.vue @@ -1,6 +1,7 @@
{{ result.title ?? 'Untitled' }}
{{ result.authors.join(', ') }}
Display