feat!: add repo searching, only show most relevant repos first

This commit is contained in:
2026-01-16 23:29:15 -08:00
parent 68d9a0a626
commit 831259a6a6
14 changed files with 174 additions and 59 deletions

View File

@@ -1,32 +1,57 @@
<script lang="ts">
import { Button } from 'bits-ui';
import { GitBranch, Star, Clock, Import, Search, RefreshCw } from '@lucide/svelte';
import { GitBranch, Star, Clock, Import, Search, RefreshCw, LoaderCircle } from '@lucide/svelte';
import type { Repository } from './types/repo';
import { createQuery } from '@tanstack/svelte-query';
import { authClient } from './auth-client';
import { apiClient } from './api-client';
import NProgress from 'nprogress';
const session = authClient.useSession();
const query = createQuery(() => ({
queryKey: ['github-repositories'],
queryKey: ['recent-repositories'],
queryFn: async () => {
return await apiClient.request<Repository[]>(`/api/v0/user/${$session.data?.user.id}/repos`);
return await apiClient.request<Repository[]>(`/api/v0/user/repos`);
},
enabled: !!$session.data?.user.id,
staleTime: 30000
}));
let searchQuery = $state('');
let throttledQuery = $state('');
$effect(() => {
const value = searchQuery;
const t = setTimeout(() => {
throttledQuery = value;
}, 300);
return () => clearTimeout(t);
});
$effect(() => {
if (searchResultsQuery.isFetching || query.isFetching) {
NProgress.start();
} else {
NProgress.done();
}
});
const searchResultsQuery = createQuery(() => ({
queryKey: ['search-repositories', throttledQuery],
queryFn: async () => {
return await apiClient.request<Repository[]>(
`/api/v0/user/repos/search?q=${encodeURIComponent(throttledQuery)}`
);
},
enabled: !!$session.data?.user.id && throttledQuery.length > 0
}));
let adding = $state<number | null>(null);
const filteredRepositories = $derived(
(query.data ?? []).filter(
(repo) =>
repo.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
repo.description?.toLowerCase().includes(searchQuery.toLowerCase())
)
);
const searching = $derived(throttledQuery.length > 0);
const repos = $derived(searching ? (searchResultsQuery.data ?? []) : (query.data ?? []));
const isPending = $derived(searching ? searchResultsQuery.isPending : query.isPending);
const handleImport = async (repoId: number) => {
adding = repoId;
@@ -48,9 +73,11 @@
<div class="flex items-center gap-3">
<GitBranch class="h-6 w-6 text-gray-400" />
<h2 class="text-xl font-semibold text-white">Import Git Repository</h2>
<span class="text-xs text-gray-500">repo must be public</span>
</div>
<Button.Root
class="flex items-center gap-2 rounded-lg px-3 py-2 text-sm text-gray-400 transition-colors hover:bg-gray-800 hover:text-white"
onclick={() => (searching ? searchResultsQuery.refetch() : query.refetch())}
>
<RefreshCw class="h-4 w-4" />
<span>Refresh</span>
@@ -68,18 +95,13 @@
</div>
<div class="space-y-3">
{#each filteredRepositories as repo (repo.id)}
{#each repos as repo (repo.id)}
<div
class="group flex items-center justify-between rounded-lg border border-gray-800 bg-gray-800/50 p-4 transition-colors hover:border-gray-700 hover:bg-gray-800"
>
<div class="min-w-0 flex-1">
<div class="flex items-center gap-2">
<h3 class="truncate font-medium text-white">{repo.name}</h3>
{#if repo.isPrivate}
<span class="rounded-full border border-gray-600 px-2 py-0.5 text-xs text-gray-400">
Private
</span>
{/if}
</div>
{#if repo.description}
<p class="mt-1 truncate text-sm text-gray-400">{repo.description}</p>
@@ -106,7 +128,7 @@
</div>
</div>
<Button.Root
class="ml-4 flex items-center gap-2 rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-blue-500 disabled:cursor-not-allowed disabled:opacity-50"
class="ml-4 flex items-center gap-2 rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-blue-500 disabled:cursor-default disabled:opacity-50"
disabled={adding !== null}
onclick={() => handleImport(repo.id)}
>
@@ -120,9 +142,15 @@
</Button.Root>
</div>
{:else}
<div class="py-8 text-center text-gray-500">
<p>No repositories found matching "{searchQuery}"</p>
</div>
{#if isPending}
<div class="flex justify-center">
<LoaderCircle class="animate-spin" />
</div>
{:else}
<div class="py-8 text-center text-gray-500">
<p>No repositories found matching "{searchQuery}"</p>
</div>
{/if}
{/each}
</div>

View File

@@ -5,15 +5,16 @@ import * as authSchema from './db/auth-schema';
import { jwt } from 'better-auth/plugins';
export const auth = betterAuth({
database: drizzleAdapter(db, {
provider: 'pg',
schema: { ...authSchema }
}),
plugins: [jwt()],
socialProviders: {
github: {
clientId: process.env.GH_CLIENT_ID!,
clientSecret: process.env.GH_CLIENT_SECRET!
}
}
database: drizzleAdapter(db, {
provider: 'pg',
schema: { ...authSchema }
}),
plugins: [jwt()],
socialProviders: {
github: {
clientId: process.env.GH_CLIENT_ID!,
clientSecret: process.env.GH_CLIENT_SECRET!,
scope: ["repo"]
}
}
});