upend/webui/src/views/Search.svelte

227 lines
5.3 KiB
Svelte

<script lang="ts">
import debounce from "lodash/debounce";
import { type Readable, readable } from "svelte/store";
import type { UpListing } from "upend";
import Spinner from "../components/utils/Spinner.svelte";
import UpEntryDisplay from "../components/display/UpEntry.svelte";
import UpObjectCard from "../components/display/UpObjectCard.svelte";
import { useNavigate } from "svelte-navigator";
import { baseSearch, createLabelled } from "../util/search";
import { updateTitle } from "../util/title";
import { query as queryFn } from "../lib/entity";
import AttributeView from "../components/AttributeView.svelte";
import { queryOnce } from "../lib/api";
import EntryList from "../components/widgets/EntryList.svelte";
import Gallery from "../components/widgets/Gallery.svelte";
import type { Widget } from "src/lib/types";
import { matchSorter } from "match-sorter";
const navigate = useNavigate();
export let query: string;
let debouncedQuery = "";
const updateQuery = debounce((query: string) => {
debouncedQuery = query;
}, 200);
$: updateQuery(query);
$: looksLikeQuery =
debouncedQuery.startsWith("(") && debouncedQuery.endsWith(")");
let result: Readable<UpListing> = readable();
let error: Readable<unknown> = readable();
$: if (debouncedQuery.length) {
({ result, error } = looksLikeQuery
? queryFn(debouncedQuery)
: baseSearch(debouncedQuery));
exactHits = [];
}
$: objects = ($result?.entries || []).filter((e) => e.attribute === "LBL");
$: sortedObjects = matchSorter(objects, debouncedQuery, {
keys: ["value.c"],
});
let exactHits: string[] = [];
$: {
const addressesString = objects.map((e) => `@${e.entity}`).join(" ");
queryOnce(`(matches (in ${addressesString}) "LBL" ? )`).then(
(labelListing) => {
exactHits = labelListing.entries
.filter(
(e) => String(e.value.c).toLowerCase() === query.toLowerCase()
)
.map((e) => e.entity);
}
);
}
async function create() {
const createdAddress = await createLabelled(query);
navigate(`/browse/${createdAddress}`);
}
$: updateTitle("Search", query);
const searchWidgets: Widget[] = [
{
name: "list-table",
icon: "list-ul",
components: [
{
component: Gallery,
props: (entries) => {
return {
entities: entries.map((e) => e.entity),
sort: false,
thumbnails: false,
};
},
},
],
},
{
name: "gallery-view",
icon: "image",
components: [
{
component: Gallery,
props: (entries) => {
return {
entities: entries.map((e) => e.entity),
sort: false,
thumbnails: true,
};
},
},
],
},
];
let lastWidget: string;
</script>
<div>
{#if !$error}
{#if !looksLikeQuery}
<section class="exact">
{#if exactHits.length}
<ul>
{#each exactHits as address}
<li>
<UpObjectCard {address} --width="100%" --height="100%" />
</li>
{/each}
</ul>
{:else}
<div class="create">
<div>Create new object?</div>
<button class="create-object" on:click={create}>"{query}"</button>
</div>
{/if}
</section>
{/if}
{#if $result}
<section class="objects">
{#if sortedObjects.length}
<h2>Objects</h2>
<AttributeView
--current-background="var(--background)"
entries={sortedObjects}
widgets={searchWidgets}
initialWidget={lastWidget}
on:widgetSwitched={(ev) => {
lastWidget = ev.detail;
}}
/>
{/if}
</section>
<section class="raw">
{#if $result?.entries.length}
<h2>Raw results</h2>
<ul>
{#each $result.entries as entry}
<li><UpEntryDisplay {entry} resolve={false} /></li>
{/each}
</ul>
{:else}
<div class="global">No results found.</div>
{/if}
</section>
{:else}
<div class="global">
<Spinner centered />
</div>
{/if}
{:else}
<div class="error global">
{$error}
</div>
{/if}
</div>
<style lang="scss">
h2 {
text-align: center;
}
ul {
list-style: none;
margin: 0;
padding: 0;
}
.exact {
margin-top: 1rem;
ul {
display: flex;
gap: 1rem;
justify-content: center;
flex-wrap: wrap;
margin: 0 1rem;
}
li {
width: 14rem;
height: 8rem;
}
.create {
text-align: center;
}
.create-object {
display: inline-block;
color: var(--foreground);
background: var(--background-lighter);
border: 1px solid var(--foreground);
border-radius: 4px;
padding: 0.2em;
font-size: 1.25em;
cursor: pointer;
margin-top: 1rem;
}
}
.objects {
max-width: 66em;
margin: auto;
}
.raw {
li {
margin: 1em auto;
max-width: 66em;
}
}
.global {
font-size: 48px;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
</style>