Gift Search Bar
(To see error handling, try searching for "bad".)
Challenge
Source code
+page.svelte
<script lang="ts">
import type { PageData } from './$types';
import { page, navigating } from '$app/stores';
import debounce from 'just-debounce-it';
import Spinner from '$lib/Spinner.svelte';
import { submitReplaceState } from '$lib/util';
import Products from './Products.svelte';
export let data: PageData;
let form: HTMLFormElement;
// intentionally not reactive - we don't want to clobber the input on subsequent navigations
let initialValue = data.query;
const debouncedSubmit = debounce(() => {
// not supported in all browsers
if (typeof HTMLFormElement.prototype.requestSubmit == 'function') {
form.requestSubmit();
}
}, 300);
$: isLoading = $navigating?.to?.url.pathname === $page.url.pathname;
</script>
<h1>Gift Search Bar</h1>
<form bind:this={form} on:submit|preventDefault={submitReplaceState}>
<label for="q">Query</label>
<input
id="q"
type="text"
name="q"
placeholder="Start typing..."
autocomplete="off"
autocorrect="off"
autocapitalize="off"
spellcheck="false"
on:input={debouncedSubmit}
value={initialValue}
/>
<div role="status" class="spin">
{#if isLoading}
<Spinner />
<span class="visually-hidden">Loading...</span>
{/if}
</div>
</form>
{#if data.result}
<Products data={data.result} query={data.query} />
{:else if data.error}
<p class="error">Error: {data.error}</p>
{/if}
<p>(To see error handling, try searching for "bad".)</p>
<style>
label {
display: block;
}
form {
position: relative;
}
.spin {
position: absolute;
right: 4px;
bottom: 2px;
}
.error {
color: var(--red-7);
font-weight: 700;
font-size: var(--font-size-3);
}
</style>
+page.ts
import type { PageLoad } from './$types';
import type { ProductsResult } from '$lib/types';
export const load: PageLoad = async ({ url, fetch }) => {
const query = url.searchParams.get('q') ?? '';
const skip = url.searchParams.get('skip') ?? '0';
const search = new URLSearchParams({ q: query, skip, select: 'title,price,id', limit: '30' });
// API doc: https://dummyjson.com/docs/products
try {
if (query === 'bad') {
throw Error('argh'); // demonstrate error handling
}
const result: ProductsResult = query
? await fetch(`https://dummyjson.com/products/search?${search.toString()}`).then((res) =>
res.json()
)
: null;
return {
result,
query
};
} catch (e) {
return {
result: null,
query,
error: 'Could not retrieve products'
};
}
};
Products.svelte
<script lang="ts">
import type { ProductsResult } from '$lib/types';
import { fade } from 'svelte/transition';
export let data: ProductsResult;
export let query: string;
$: showNext = data.skip + data.limit < data.total;
$: showPrev = data.skip > 0;
</script>
{#if data.products && data.products.length > 0}
<p>Showing {data.skip + 1} - {data.skip + data.limit} of {data.total} results</p>
<ul in:fade={{ duration: 200 }}>
{#each data.products as product (product.id)}
<li>{product.title} - ${product.price}</li>
{/each}
</ul>
<div class="links">
{#if showPrev}
<a href="?q={query}&skip={data.skip - 30}">Previous Page</a>
{/if}
{#if showNext}
<a href="?q={query}&skip={data.skip + 30}">Next Page</a>
{/if}
</div>
{:else}
<p>No results!</p>
{/if}
<style>
ul {
list-style-type: disc;
align-self: start;
}
.links {
display: flex;
gap: 1rem;
}
</style>