feat: server-side search on import page Search now queries the database via repo.list?query= instead of filtering client-side. Results come from PostgreSQL full-text search across all repos, with 300ms debounce.
Author: Aaron Steven White
Commit
7b096a860627a4b1c1b87f472cf0e980556792d1Parent: 8459b2715b
Structural diff unavailable
These commits were pushed via plain git push, so no pre-parsed
schemas are available. Install git-remote-cospan and re-push via panproto:// to
see scope-level changes, breaking change detection, and semantic diffs.
brew install panproto/tap/git-remote-cospan3 files changed +47 -15
@@ -33,6 +33,7 @@ export async function listRepos(params?: {
3333 did?: string; 3434 source?: string; 3535 sort?: string; 36+ query?: string; 3637 limit?: number; 3738 cursor?: string; 3839 }): Promise<RepoListResponse> {
@@ -61,6 +61,9 @@
6161 handles = resolved; 6262 } 6363 64+ let searching = $state(false); 65+ let searchTimer: ReturnType<typeof setTimeout> | null = null; 66+ 6467 async function loadMore() { 6568 if (!cursor || loadingMore) return; 6669 loadingMore = true;
@@ -74,20 +77,40 @@
7477 } 7578 } 7679 80+ async function doSearch(query: string) { 81+ if (!query.trim()) { 82+ // Reset to default popular list 83+ searching = true; 84+ try { 85+ const result = await listRepos({ source: 'tangled', sort: 'popular', limit: 30 }); 86+ allRepos = result.items; 87+ cursor = result.cursor; 88+ resolveAllHandles(result.items); 89+ } finally { 90+ searching = false; 91+ } 92+ return; 93+ } 94+ searching = true; 95+ try { 96+ const result = await listRepos({ query, limit: 30 }); 97+ allRepos = result.items; 98+ cursor = result.cursor; 99+ resolveAllHandles(result.items); 100+ } finally { 101+ searching = false; 102+ } 103+ } 104+ 105+ function onSearchInput() { 106+ if (searchTimer) clearTimeout(searchTimer); 107+ searchTimer = setTimeout(() => doSearch(searchQuery), 300); 108+ } 109+ 77110 function getHandle(did: string): string { 78111 return handles[did] || (did.startsWith('did:plc:') ? did.slice(8, 18) + '\u2026' : did); 79112 } 80113 81- let filteredAll = $derived(() => { 82- if (!searchQuery.trim()) return allRepos; 83- const q = searchQuery.toLowerCase(); 84- return allRepos.filter((r: Repo) => 85- r.name.toLowerCase().includes(q) || 86- (r.description ?? '').toLowerCase().includes(q) || 87- getHandle(r.did).toLowerCase().includes(q) 88- ); 89- }); 90- 91114 let filteredMine = $derived(() => { 92115 if (!mySearchQuery.trim()) return myRepos; 93116 const q = mySearchQuery.toLowerCase();
@@ -203,14 +226,15 @@
203226 <input 204227 type="text" 205228 bind:value={searchQuery} 206- placeholder="Search repos…" 229+ oninput={onSearchInput} 230+ placeholder="Search all repos…" 207231 class="w-full rounded-md border border-line bg-surface py-1.5 pl-9 pr-3 text-[13px] text-ink placeholder:text-ghost focus:border-focus/50 focus:outline-none transition-colors" 208232 /> 209233 </div> 210- <span class="text-[11px] text-ghost whitespace-nowrap">{filteredAll().length} repos</span> 234+ <span class="text-[11px] text-ghost whitespace-nowrap">{searching ? 'Searching…' : `${allRepos.length} repos`}</span> 211235 </div> 212236 213- {#if filteredAll().length === 0} 237+ {#if allRepos.length === 0} 214238 <div class="py-16 text-center"> 215239 {#if searchQuery.trim()} 216240 <p class="text-[13px] text-caption">No repos matching "{searchQuery}"</p>
@@ -221,7 +245,7 @@
221245 </div> 222246 {:else} 223247 <div class="divide-y divide-line/30"> 224- {#each filteredAll() as repo (repo.did + '/' + repo.name)} 248+ {#each allRepos as repo (repo.did + '/' + repo.name)} 225249 <div class="flex items-center justify-between gap-4 py-2.5"> 226250 <div class="min-w-0 flex-1"> 227251 <div class="flex items-center gap-2">
@@ -13,6 +13,7 @@ pub struct Params {
1313 pub did: Option<String>, 1414 pub source: Option<String>, 1515 pub sort: Option<String>, 16+ pub query: Option<String>, 1617 pub limit: Option<i64>, 1718 pub cursor: Option<String>, 1819 }
@@ -24,7 +25,13 @@ pub async fn handler(
2425 let limit = params.limit.unwrap_or(25).min(100); 2526 let sort_popular = params.sort.as_deref() == Some("popular"); 2627 27- let repos = if let Some(did) = ¶ms.did { 28+ let repos = if let Some(query) = ¶ms.query { 29+ if !query.trim().is_empty() { 30+ db::repo::search(&state.db, query, limit + 1, params.cursor.as_deref()).await? 31+ } else { 32+ db::repo::list_recent(&state.db, limit + 1, params.cursor.as_deref()).await? 33+ } 34+ } else if let Some(did) = ¶ms.did { 2835 db::repo::list_by_did(&state.db, did, limit + 1, params.cursor.as_deref()).await? 2936 } else if let Some(source) = ¶ms.source { 3037 if sort_popular {