Skip to content

Commit

Permalink
Basic infinite scroll for search results
Browse files Browse the repository at this point in the history
  • Loading branch information
eyeseast committed Mar 28, 2024
1 parent 5a12fba commit 828871d
Show file tree
Hide file tree
Showing 8 changed files with 130 additions and 98 deletions.
22 changes: 17 additions & 5 deletions src/lib/api/documents.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/** API helpers related to documents.
* Lots of duplicated code here that should get consolidated at some point.
*/
import type { Document, sizes } from "./types";
import type { Document, DocumentResults, SearchOptions, sizes } from "./types";

import { error } from "@sveltejs/kit";

Expand All @@ -10,17 +10,29 @@ import { isOrg } from "@/api/types/orgAndUser";
import { APP_URL, BASE_API_URL } from "@/config/config.js";
import { isErrorCode } from "../utils";

/** Search documents */
/**
* Search documents
* https://www.documentcloud.org/help/search/
*
* */
export async function search(
query = "",
highlight = false,
options: SearchOptions = {
hl: Boolean(query),
per_page: 25,
cursor: "",
version: "2.0",
},
fetch = globalThis.fetch,
) {
): Promise<DocumentResults> {
const endpoint = new URL("documents/search/", BASE_API_URL);

endpoint.searchParams.set("expand", DEFAULT_EXPAND);
endpoint.searchParams.set("q", query);
endpoint.searchParams.set("hl", String(highlight));

for (const [k, v] of Object.entries(options)) {
endpoint.searchParams.set(k, String(v));
}

const resp = await fetch(endpoint, { credentials: "include" });

Expand Down
9 changes: 8 additions & 1 deletion src/lib/api/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,6 @@ export interface Document {
note_highlights?: Record<string, Highlight[]>;
}

// export type DocumentResults = Page<Document>;
export interface DocumentResults extends Page<Document> {}

export interface Note {
Expand Down Expand Up @@ -135,6 +134,14 @@ export interface Section {

export type SectionResults = Page<Section>;

export interface SearchOptions {
hl?: boolean;
per_page?: number;
cursor?: string;
expand?: string;
version?: number | string;
}

export interface OEmbed {
version: "1.0";
provider_name: "DocumentCloud";
Expand Down
12 changes: 7 additions & 5 deletions src/lib/components/Search.svelte
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
<script lang="ts">
import { Search24, XCircleFill24 } from "svelte-octicons";
import Button from "./common/Button.svelte";
export let query: string = "";
let input: HTMLInputElement;
Expand All @@ -11,16 +10,19 @@
}
</script>

<form class="container" on:submit|preventDefault>
<form class="container" on:submit>
<label for="query" title="Search"><Search24 /></label>
<input
type="search"
id="query"
name="query"
name="q"
autocomplete="off"
placeholder="Search…"
bind:value={query}
bind:this={input}
on:change
autocomplete="off"
placeholder="Search…"
on:input
on:reset
/>
<button
title="Clear Search"
Expand Down
60 changes: 46 additions & 14 deletions src/lib/components/documents/ResultsList.svelte
Original file line number Diff line number Diff line change
@@ -1,37 +1,53 @@
<script context="module" lang="ts">
import { writable, type Writable } from "svelte/store";
import Button from "../common/Button.svelte";
export const selected: Writable<(number | string)[]> = writable([]);
</script>

<script lang="ts">
import type { Document, DocumentResults } from "$lib/api/types";
import { _ } from "svelte-i18n";
import type { DocumentResults } from "$lib/api/types";
import DocumentListItem from "./DocumentListItem.svelte";
import Flex from "../common/Flex.svelte";
import Checkbox from "../common/Checkbox.svelte";
import { Search24 } from "svelte-octicons";
import Empty from "../common/Empty.svelte";
export let results: DocumentResults = undefined;
export let results: Document[] = [];
export let count: number = undefined;
export let next: string | null = null;
let loading = false;
function updateSelection(event: Event, id: string | number) {
const { checked } = event.target as HTMLInputElement;
if (checked) {
selected.set([...$selected, id]);
} else {
selected.set([...$selected.filter((d) => d !== id)]);
// load the next set of results
async function load(url: URL) {
loading = true;
const res = await fetch(url, { credentials: "include" });
if (!res.ok) {
// todo: better error handling
console.error(res.statusText);
loading = false;
}
const r: DocumentResults = await res.json();
results = [...results, ...r.results];
count = r.count;
next = r.next;
loading = false;
}
</script>

<div class="container">
{#each results.results as document (document.id)}
{#each results as document (document.id)}
<Flex gap={0.625} align="center">
<Checkbox
checked={$selected.includes(document.id)}
on:change={(event) => updateSelection(event, document.id)}
/>
<label>
<span class="sr-only">Select</span>
<input type="checkbox" bind:group={$selected} value={document.id} />
</label>
<DocumentListItem {document} />
</Flex>
{:else}
Expand All @@ -40,10 +56,26 @@
<p>{$_("noDocuments.queryNoResults")}</p>
</Empty>
{/each}
<div class="end">
{#if next}
<Button on:click={(e) => load(new URL(next))}>Load more</Button>
{/if}
</div>
</div>

<style>
.container {
padding: 0 2rem;
}
label {
display: flex;
align-items: center;
gap: 0.5rem;
}
input[type="checkbox"] {
height: 1.25rem;
width: 1.25rem;
}
</style>
18 changes: 6 additions & 12 deletions src/lib/components/documents/stories/ResultsList.stories.svelte
Original file line number Diff line number Diff line change
@@ -1,32 +1,26 @@
<script context="module" lang="ts">
import type { DocumentResults } from "@/lib/api/types";
import type { Document, DocumentResults } from "@/lib/api/types";
import { Story } from "@storybook/addon-svelte-csf";
import ResultsList from "../ResultsList.svelte";
// typescript complains without the type assertion
import searchResults from "../../../api/fixtures/documents/search-highlight.json";
const results = searchResults as DocumentResults;
const results = searchResults.results as Document[];
const count = searchResults.count;
const next = searchResults.next;
export const meta = {
title: "Components / Documents / Results list",
component: ResultsList,
tags: ["autodocs"],
parameters: { layout: "centered" },
};
const empty = {
results: [],
count: 0,
next: null,
previous: null,
escaped: false,
};
</script>

<Story name="With Results">
<div style="width: 36rem"><ResultsList {results} /></div>
<div style="width: 36rem"><ResultsList {results} {count} {next} /></div>
</Story>

<Story name="Empty">
<div style="width: 36rem"><ResultsList results={empty} /></div>
<div style="width: 36rem"><ResultsList /></div>
</Story>
64 changes: 6 additions & 58 deletions src/routes/app/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -6,35 +6,11 @@
import PageToolbar from "$lib/components/common/PageToolbar.svelte";
import Search from "$lib/components/Search.svelte";
import Empty from "$lib/components/common/Empty.svelte";
import Paginator from "@/common/Paginator.svelte";
import type { DocumentResults } from "@/lib/api/types";
export let data: {
query: string;
searchResults: Promise<DocumentResults>;
};
let page = 1;
let per_page = 25;
let error: Error;
export let data;
$: searchResults = data.searchResults;
$: query = data.query;
async function load(url) {
const res = await fetch(url, { credentials: "include" }).catch((e) => {
error = e;
throw e; // if something went wrong here, something broke
});
if (!res.ok) {
// 404 or something similar
console.error(res.statusText);
error = { name: "Loading error", message: res.statusText };
}
data.searchResults = res.json();
}
</script>

<ContentLayout>
Expand All @@ -44,45 +20,17 @@
{#await searchResults}
<Empty icon={Hourglass24}>Loading…</Empty>
{:then results}
<ResultsList {results} />
<ResultsList
results={results.results}
count={results.count}
next={results.next}
/>
{/await}

<PageToolbar slot="footer">
<label slot="left">
<input type="checkbox" name="select_all" />
Select all
</label>

<svelte:fragment slot="center">
{#await searchResults then sr}
{@const count = sr.count}
{@const total_pages = Math.ceil(count / per_page)}
{@const next = sr.next}
{@const previous = sr.previous}
<Paginator
{page}
totalPages={total_pages}
has_next={Boolean(next)}
has_previous={Boolean(previous)}
on:next={(e) => {
page = Math.min(total_pages, page + 1);
load(next);
}}
on:previous={(e) => {
page = Math.max(1, page - 1);
load(previous);
}}
/>
{/await}
</svelte:fragment>

<label slot="right">
Per page
<select name="per_page" bind:value={per_page}>
<option value={25}>25</option>
<option value={50}>50</option>
<option value={100}>100</option>
</select>
</label>
</PageToolbar>
</ContentLayout>
25 changes: 22 additions & 3 deletions src/routes/app/+page.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,30 @@
import { search } from "$lib/api/documents.js";
import type { SearchOptions } from "$lib/api/types";
import { search } from "$lib/api/documents";

export async function load({ url, fetch }) {
const query = url.searchParams.get("q") || "";
const searchResults = search(query, true, fetch);
const per_page = +url.searchParams.get("per_page") || 25;
const cursor = url.searchParams.get("cursor") || "";

const options: SearchOptions = {
hl: Boolean(query),
version: "2.0",
};

if (per_page) {
options.per_page = per_page;
}

if (cursor) {
options.cursor = cursor;
}

const searchResults = search(query, options, fetch);

return {
searchResults,
query,
per_page,
cursor,
searchResults,
};
}
18 changes: 18 additions & 0 deletions src/style/kit.css
Original file line number Diff line number Diff line change
Expand Up @@ -139,3 +139,21 @@ dd {
font-size: var(--font-s, 0.875rem);
font-weight: var(--font-regular, 400);
}

/*
Utility classes
*/

/* https://css-tricks.com/inclusively-hidden/
* Hiding class, making content visible only to screen readers but not visually
* "sr" meaning "screen-reader"
*/
.sr-only:not(:focus):not(:active) {
clip: rect(0 0 0 0);
clip-path: inset(50%);
height: 1px;
overflow: hidden;
position: absolute;
white-space: nowrap;
width: 1px;
}

0 comments on commit 828871d

Please sign in to comment.