Advent of SvelteKit 2022

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>