Skip to content

Commit

Permalink
mvp search
Browse files Browse the repository at this point in the history
  • Loading branch information
t-shah02 committed Feb 19, 2024
1 parent 927c4c1 commit 6a02138
Show file tree
Hide file tree
Showing 25 changed files with 549 additions and 66 deletions.
16 changes: 11 additions & 5 deletions src/lib/client/components/layout/Navbar.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,23 @@
import { page } from '$app/stores';
import { authenticatedUserStore } from '$lib/client/stores/users';
import { Button, DarkMode, NavBrand, NavHamburger, NavLi, NavUl, Navbar } from 'flowbite-svelte';
import GlobalSearchbar from '../search/GlobalSearchbar.svelte';
import ProfileDropdown from './ProfileDropdown.svelte';
const currentPath = $page.url.pathname;
</script>

<Navbar class="sticky top-0 z-50">
<NavBrand href="/">
<img src="/favicon.png" class="mr-3 h-6 sm:h-9 rounded-md" alt="Dexbooru Logo" />
<span class="self-center whitespace-nowrap text-xl font-semibold dark:text-white">Dexbooru</span
>
</NavBrand>
<div class="flex space-x-4">
<NavBrand>
<img src="/favicon.png" class="mr-3 h-6 sm:h-9 rounded-md" alt="Dexbooru Logo" />
<span class="self-center whitespace-nowrap text-xl font-semibold dark:text-white"
>Dexbooru</span
>
</NavBrand>
<GlobalSearchbar />
</div>

<div class="flex md:order-2 space-x-2">
{#if $authenticatedUserStore}
<ProfileDropdown />
Expand Down
15 changes: 15 additions & 0 deletions src/lib/client/components/reusable/HighlightedText.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<script lang="ts">
import { getQueryParts } from '$lib/client/helpers/search';
import { normalizeQuery } from '$lib/shared/helpers/search';
export let query: string;
export let fullText: string;
const fullTextParts = getQueryParts(normalizeQuery(fullText), normalizeQuery(query));
</script>

<p>
{#each fullTextParts as { text, type }}
<span class={type === 'highlight' ? 'bg-yellow-400' : ''}>{text}</span>
{/each}
</p>
22 changes: 13 additions & 9 deletions src/lib/client/components/reusable/Searchbar.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,28 @@
import { Input } from 'flowbite-svelte';
import { SearchOutline } from 'flowbite-svelte-icons';
export let inputElementId: string | null = null;
export let width: string = '300px';
export let isGlobal: boolean = false;
export let placeholder: string;
export let queryHandler: (query: string) => void;
const optionalProps: Record<string, string> = {};
if (inputElementId) {
optionalProps.id = inputElementId;
}
const onInput = (event: Event) => {
const inputTarget = event.target as HTMLInputElement;
queryHandler(inputTarget.value);
if (inputTarget.value.length > 0) {
queryHandler(inputTarget.value);
}
};
</script>

<div class="hidden relative md:block mr-4 searchbar-container">
<div class="hidden relative md:block {!isGlobal && 'mr-4'}" style="width: {width}">
<div class="flex absolute inset-y-0 start-0 items-center ps-3 pointer-events-none">
<SearchOutline class="w-4 h-4" />
</div>
<Input on:input={onInput} class="ps-10" {placeholder} />
<Input {...optionalProps} on:input={onInput} class="ps-10" {placeholder} />
</div>

<style>
.searchbar-container {
width: 300px;
}
</style>
84 changes: 84 additions & 0 deletions src/lib/client/components/search/GlobalSearchModal.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
<script lang="ts">
import { getGlobalSearchResults } from '$lib/client/api/search';
import {
GLOBAL_SEARCH_INPUT_ELEMENT_ID,
SEARCH_DEBOUNCE_TIMEOUT_MS
} from '$lib/client/constants/search';
import { FAILURE_TOAST_OPTIONS } from '$lib/client/constants/toasts';
import { debounce, memoize } from '$lib/client/helpers/util';
import { searchModalActiveStore } from '$lib/client/stores/layout';
import { queryStore } from '$lib/client/stores/search';
import type { IAppSearchResult } from '$lib/shared/types/search';
import { toast } from '@zerodevx/svelte-toast';
import { Modal, Spinner } from 'flowbite-svelte';
import { onDestroy } from 'svelte';
import Searchbar from '../reusable/Searchbar.svelte';
import SearchResultsContainer from './SearchResultsContainer.svelte';
let currentSearchResults: IAppSearchResult | null = null;
let searchResultsLoading = false;
const searchModalUnsubsribe = searchModalActiveStore.subscribe((active) => {
if (active) {
const globalSearchInput = document.querySelector(
`#${GLOBAL_SEARCH_INPUT_ELEMENT_ID}`
) as HTMLInputElement | null;
if (globalSearchInput) {
globalSearchInput.focus();
}
} else {
currentSearchResults = null;
}
});
const fetchQueryResults = memoize(async (query: string) => {
const response = await getGlobalSearchResults(query);
if (response.ok) {
return (await response.json()) as IAppSearchResult;
} else {
toast.push(
'An unexpected error occured while retrieving the search results',
FAILURE_TOAST_OPTIONS
);
return null;
}
}, true);
const debouncedFetchQueryResults = debounce(async (query: string) => {
searchResultsLoading = true;
queryStore.set(query);
currentSearchResults = query ? await fetchQueryResults(query as never) : null;
searchResultsLoading = false;
}, SEARCH_DEBOUNCE_TIMEOUT_MS) as (query: string) => void;
onDestroy(() => {
searchModalUnsubsribe();
});
</script>

<Modal
title="Find tags, artists, users and posts"
open={$searchModalActiveStore}
outsideclose
class="w-screen"
placement="top-center"
on:close={() => searchModalActiveStore.set(false)}
>
<div class="flex relative">
<Searchbar
inputElementId={GLOBAL_SEARCH_INPUT_ELEMENT_ID}
isGlobal
width="100%"
placeholder="Enter your search query"
queryHandler={debouncedFetchQueryResults}
/>
{#if searchResultsLoading}
<Spinner color="pink" class="absolute top-2 right-2" size="7" />
{/if}
</div>

{#if currentSearchResults}
<SearchResultsContainer results={currentSearchResults} />
{/if}
</Modal>
46 changes: 24 additions & 22 deletions src/lib/client/components/search/GlobalSearchbar.svelte
Original file line number Diff line number Diff line change
@@ -1,30 +1,32 @@
<script lang="ts">
import { getGlobalSearchResults } from '$lib/client/api/search';
import { FAILURE_TOAST_OPTIONS } from '$lib/client/constants/toasts';
import type { IAppSearchResult } from '$lib/shared/types/search';
import { toast } from '@zerodevx/svelte-toast';
import { Input } from 'flowbite-svelte';
import { searchModalActiveStore } from '$lib/client/stores/layout';
import { Button, Input, Kbd } from 'flowbite-svelte';
import { SearchOutline } from 'flowbite-svelte-icons';
let currentQuery: string;
let currentSearchResults: IAppSearchResult[] = [];
const handleSearchQueryChange = async (event: Event) => {
const target = event.target as HTMLInputElement;
currentQuery = target.value;
const response = await getGlobalSearchResults(currentQuery);
if (response.ok) {
currentSearchResults = (await response.json()) as IAppSearchResult[];
} else {
toast.push('An error occured while trying to process that query', FAILURE_TOAST_OPTIONS);
}
};
</script>

<div class="hidden relative md:block">
<div class="hidden relative md:block mt-1" id="global-searchbar">
<div class="flex absolute inset-y-0 start-0 items-center ps-3 pointer-events-none">
<SearchOutline class="w-4 h-4" />
</div>
<Input class="ps-10" placeholder="Search..." />
<div class="flex space-x-2 absolute inset-y-0 end-8 items-center ps-3 pointer-events-none">
<Kbd class="p-1">CTRL</Kbd>
<Kbd class="p-1">K</Kbd>
</div>
<Button
on:click={() => searchModalActiveStore.set(true)}
color="alternative"
class="p-0 border-none opacity-50 hover:opacity-100"
>
<Input
readonly
class="ps-10 hover:cursor-pointer border-none outline-none caret-transparent focus:ring-0 focus:ring-offset-0"
placeholder="Search"
/>
</Button>
</div>

<style>
#global-searchbar {
width: 250px;
}
</style>
48 changes: 48 additions & 0 deletions src/lib/client/components/search/SearchResultsContainer.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<script lang="ts">
import type { IAppSearchResult } from '$lib/shared/types/search';
import { TabItem, Tabs } from 'flowbite-svelte';
import LabelTable from '../tables/LabelTable.svelte';
import PostTable from '../tables/PostTable.svelte';
import UserTable from '../tables/UserTable.svelte';
export let results: IAppSearchResult;
let { posts, artists, tags, users } = results;
$: {
posts = results.posts;
artists = results.artists;
tags = results.tags;
users = results.users;
}
</script>

<Tabs style="underline">
<TabItem open title="Tags">
{#if tags && tags.length > 0}
<LabelTable labels={tags} labelType="tags" />
{:else}
<p>No tags were found matching that query</p>
{/if}
</TabItem>
<TabItem title="Artists">
{#if artists && artists.length > 0}
<LabelTable labels={artists} labelType="artists" />
{:else}
<p>No artists were found matching that query</p>
{/if}
</TabItem>
<TabItem title="Posts">
{#if posts && posts.length > 0}
<PostTable {posts} />
{:else}
<p>No posts were found matching that query</p>
{/if}
</TabItem>
<TabItem title="Users">
{#if users && users.length > 0}
<UserTable {users} />
{:else}
<p>No users were found matching that query</p>
{/if}
</TabItem>
</Tabs>
39 changes: 39 additions & 0 deletions src/lib/client/components/tables/LabelTable.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<script lang="ts">
import type { IAppSearchResult } from '$lib/shared/types/search';
import {
Table,
TableBody,
TableBodyCell,
TableBodyRow,
TableHead,
TableHeadCell
} from 'flowbite-svelte';
export let labels: IAppSearchResult['tags'] | IAppSearchResult['artists'];
export let labelType: 'tags' | 'artists';
</script>

<Table hoverable>
<TableHead>
<TableHeadCell>ID</TableHeadCell>
<TableHeadCell>Name</TableHeadCell>
<TableHeadCell>
<span class="sr-only">Related posts</span>
</TableHeadCell>
</TableHead>
<TableBody tableBodyClass="divide-y">
{#each labels || [] as label}
<TableBodyRow>
<TableBodyCell>{label.id}</TableBodyCell>
<TableBodyCell>{label.name}</TableBodyCell>
<TableBodyCell>
<a
href="/{labelType}/{label.name}"
class="font-medium text-primary-600 hover:underline dark:text-primary-500"
>Related posts</a
>
</TableBodyCell>
</TableBodyRow>
{/each}
</TableBody>
</Table>
60 changes: 60 additions & 0 deletions src/lib/client/components/tables/PostTable.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
<script lang="ts">
import { formatDate } from '$lib/shared/helpers/dates';
import type { IAppSearchResult } from '$lib/shared/types/search';
import {
Avatar,
Table,
TableBody,
TableBodyCell,
TableBodyRow,
TableHead,
TableHeadCell
} from 'flowbite-svelte';
import HighlightedText from '../reusable/HighlightedText.svelte';
import { queryStore } from '$lib/client/stores/search';
export let posts: IAppSearchResult['posts'];
</script>

<Table hoverable>
<TableHead>
<TableHeadCell>ID</TableHeadCell>
<TableHeadCell>Description</TableHeadCell>
<TableHeadCell>Created at</TableHeadCell>
<TableHeadCell>Author</TableHeadCell>
<TableHeadCell>
<span class="sr-only">View post</span>
</TableHeadCell>
</TableHead>
<TableBody tableBodyClass="divide-y">
{#each posts || [] as post}
<TableBodyRow>
<TableBodyCell>{post.id}</TableBodyCell>
<TableBodyCell tdClass="px-1 py-4 whitespace-wrap font-medium"
>
<HighlightedText fullText={post.description} query={$queryStore} />
</TableBodyCell
>
<TableBodyCell>{formatDate(new Date(post.createdAt))}</TableBodyCell>
<TableBodyCell class="text-center">
<Avatar
class="ml-auto mr-auto"
src={post.uploaderProfilePictureUrl}
alt="profile picture of {post.uploaderName}"
/>
<a
href="/profile/{post.uploaderName}"
class="font-medium text-primary-600 hover:underline dark:text-primary-500"
>{post.uploaderName}</a
>
</TableBodyCell>
<TableBodyCell>
<a
href="/posts/{post.id}"
class="font-medium text-primary-600 hover:underline dark:text-primary-500">View post</a
>
</TableBodyCell>
</TableBodyRow>
{/each}
</TableBody>
</Table>
Loading

0 comments on commit 6a02138

Please sign in to comment.