feat(search): add browse text search across local library and novelfire.net

Adds GET /api/search Go endpoint that queries PocketBase books by title/author
substring and fetches novelfire.net /search results via parseBrowsePage(),
merging results with local-first de-duplication. The browse page gains a search
input that navigates to ?q=; results show local vs remote counts and hide
pagination/filter controls while in search mode.
This commit is contained in:
Admin
2026-03-05 14:00:59 +05:00
parent a54d8d43aa
commit 1d00fd4e2e
4 changed files with 189 additions and 7 deletions

View File

@@ -25,10 +25,52 @@ export const load: PageServerLoad = async ({ url, locals }) => {
const genre = url.searchParams.get('genre') ?? 'all';
const sort = url.searchParams.get('sort') ?? 'popular';
const status = url.searchParams.get('status') ?? 'all';
const q = url.searchParams.get('q') ?? '';
let novels: NovelListing[] = [];
let pageNum = parseInt(page, 10) || 1;
let hasNext = false;
let searchQuery = '';
let searchLocalCount = 0;
let searchRemoteCount = 0;
// ── Search mode: ?q= overrides browse/ranking ─────────────────────────
if (q.trim().length >= 2) {
searchQuery = q.trim();
const apiURL = `${SCRAPER_URL}/api/search?q=${encodeURIComponent(searchQuery)}`;
try {
const res = await fetch(apiURL);
if (!res.ok) {
log.error('browse', 'search returned error', { status: res.status });
throw error(502, `Search failed: ${res.status}`);
}
const data: {
results: NovelListing[];
local_count: number;
remote_count: number;
} = await res.json();
novels = data.results ?? [];
searchLocalCount = data.local_count ?? 0;
searchRemoteCount = data.remote_count ?? 0;
} catch (e) {
if (e instanceof Error && 'status' in e) throw e;
log.error('browse', 'search network error', { q: searchQuery, err: String(e) });
throw error(502, 'Could not reach search service');
}
return {
novels,
page: 1,
hasNext: false,
genre,
sort,
status,
isAdmin: locals.user?.role === 'admin',
searchQuery,
searchLocalCount,
searchRemoteCount
};
}
if (sort === 'rank') {
// Ranking view: fetch from /api/ranking which returns richer metadata.
@@ -99,7 +141,10 @@ export const load: PageServerLoad = async ({ url, locals }) => {
genre,
sort,
status,
isAdmin: locals.user?.role === 'admin'
isAdmin: locals.user?.role === 'admin',
searchQuery: '',
searchLocalCount: 0,
searchRemoteCount: 0
};
};