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 7b096a860627a4b1c1b87f472cf0e980556792d1
Parent: 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-cospan
3 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) = &params.did {
28+    let repos = if let Some(query) = &params.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) = &params.did {
2835         db::repo::list_by_did(&state.db, did, limit + 1, params.cursor.as_deref()).await?
2936     } else if let Some(source) = &params.source {
3037         if sort_popular {
cospan · schematic version control on atproto built on AT Protocol