Initial commit
This commit is contained in:
@@ -0,0 +1,137 @@
|
||||
<script lang="ts">
|
||||
import * as Command from '$lib/components/ui/command/index';
|
||||
import * as Kbd from '$lib/components/ui/kbd/index.js';
|
||||
import * as InputGroup from '$lib/components/ui/input-group/index.js';
|
||||
import { Badge } from '$lib/components/ui/badge/index';
|
||||
import { Spinner } from '$lib/components/ui/spinner/index.js';
|
||||
import SearchIcon from '@lucide/svelte/icons/search';
|
||||
import { listBooks } from '$lib/api';
|
||||
import { getLibraryState } from '$lib/state/library.svelte';
|
||||
import type { PaginatedResponse, Book } from '$lib/schema';
|
||||
import BookImage from '../view/book-image.svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
|
||||
const libraryState = getLibraryState();
|
||||
|
||||
let open = $state(false);
|
||||
let searchString = $state('');
|
||||
let searchResult = $state<PaginatedResponse<Book>>();
|
||||
let debounceTimeout = $state<NodeJS.Timeout | undefined>(undefined);
|
||||
let isLoading = $state(false);
|
||||
|
||||
async function handleInputChange() {
|
||||
if (!searchString || searchString === '') {
|
||||
clearTimeout(debounceTimeout);
|
||||
searchResult = undefined;
|
||||
isLoading = false;
|
||||
return;
|
||||
}
|
||||
|
||||
clearTimeout(debounceTimeout);
|
||||
|
||||
isLoading = true;
|
||||
|
||||
debounceTimeout = setTimeout(async () => {
|
||||
try {
|
||||
const results = await listBooks({
|
||||
libraries: [libraryState.activeLibrary!.id],
|
||||
searchString
|
||||
});
|
||||
searchResult = results || [];
|
||||
} finally {
|
||||
isLoading = false;
|
||||
}
|
||||
}, 300);
|
||||
}
|
||||
|
||||
function handleKeydown(e: KeyboardEvent) {
|
||||
if (e.key === 'k' && (e.metaKey || e.ctrlKey)) {
|
||||
e.preventDefault();
|
||||
open = !open;
|
||||
}
|
||||
}
|
||||
|
||||
function handleClick(e: any) {
|
||||
open = true;
|
||||
e.target.blur();
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:document onkeydown={handleKeydown} />
|
||||
|
||||
<form>
|
||||
<div class="flex w-full max-w-xs flex-col gap-6">
|
||||
<InputGroup.Root>
|
||||
<InputGroup.Input placeholder="Search..." onclick={handleClick} />
|
||||
<InputGroup.Addon>
|
||||
<SearchIcon />
|
||||
</InputGroup.Addon>
|
||||
<InputGroup.Addon align="inline-end">
|
||||
<Kbd.Root>Ctrl</Kbd.Root>+
|
||||
<Kbd.Root>k</Kbd.Root>
|
||||
</InputGroup.Addon>
|
||||
</InputGroup.Root>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<Command.Dialog bind:open shouldFilter={false}>
|
||||
<Command.Input
|
||||
placeholder="Search for a book..."
|
||||
oninput={handleInputChange}
|
||||
bind:value={searchString}
|
||||
/>
|
||||
{#if searchString && searchResult?.items.length === 0 && !isLoading}
|
||||
<div class="flex h-24 items-center justify-center">
|
||||
<span class="text-sm">No results found.</span>
|
||||
</div>
|
||||
{/if}
|
||||
{#if isLoading}
|
||||
<div class="flex h-24 items-center justify-center">
|
||||
<Spinner class="scale-150" />
|
||||
</div>
|
||||
{:else}
|
||||
<Command.List class="max-h-[600px]">
|
||||
{#if searchResult?.items.length > 0}
|
||||
<Command.Group heading="Books">
|
||||
{#each searchResult?.items as book (book.id)}
|
||||
<Command.Item
|
||||
value={String(book.id)}
|
||||
onSelect={() => {
|
||||
goto(`/book/${book.id}`);
|
||||
open = false;
|
||||
}}
|
||||
class="cursor-pointer"
|
||||
>
|
||||
<div class="hover:bg-base-200 flex gap-4 p-3">
|
||||
<BookImage
|
||||
src="/api/{book.cover_image}"
|
||||
class="w-24 rounded object-cover shadow-lg"
|
||||
/>
|
||||
<div class="flex-top flex flex-col">
|
||||
<span class="text-lg font-medium">{book.title}</span>
|
||||
<span class=" text-md">{book.subtitle}</span>
|
||||
{#if book.authors.length > 0}
|
||||
<span class="line-clamp-1 w-full text-sm text-muted-foreground">
|
||||
by
|
||||
{#each book.authors as author}
|
||||
<a
|
||||
href="/library/{libraryState.activeLibrary!.id}/view?authors={author.id}"
|
||||
class="cs-list hover:underline">{author.name}</a
|
||||
>  
|
||||
{/each}
|
||||
</span>
|
||||
{/if}
|
||||
<div class="mt-1 ml-[-1.5] flex">
|
||||
{#each book.tags as tag}
|
||||
<Badge class="scale-75">{tag.name}</Badge>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Command.Item>
|
||||
{/each}
|
||||
</Command.Group>
|
||||
{/if}
|
||||
</Command.List>
|
||||
{/if}
|
||||
</Command.Dialog>
|
||||
Reference in New Issue
Block a user