|
|
|
|
@@ -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>
|
|
|
|
|
|
|
|
|
|
|