🚀 Upgrade search component to Svelte 5 + add `onChange` parameter
Some checks failed
⚙️ Check / ⚡ Testing with Vitest (push) Has been cancelled
⚙️ Check / 📦 SVGs Size (push) Has been cancelled
⚙️ Check / 🛠️ Build app (push) Has been cancelled

This commit is contained in:
pheralb 2025-02-06 22:38:21 +00:00
parent d6be3ad941
commit f9951efd7f
3 changed files with 62 additions and 83 deletions

View File

@ -1,13 +1,16 @@
<script lang="ts"> <script lang="ts">
import { page } from '$app/stores';
import { inputStyles } from '@/ui/styles';
import { Command, SearchIcon } from 'lucide-svelte'; import { Command, SearchIcon } from 'lucide-svelte';
export let searchTerm: string; import { inputStyles } from '@/ui/styles';
export let placeholder: string = 'Search...'; import { cn } from '@/utils/cn';
export let clearSearch: () => void;
import { X } from 'lucide-svelte'; interface Props {
searchTerm?: string;
placeholder?: string;
onChange: (value: string) => void;
}
let inputElement; let inputElement;
let { searchTerm = $bindable(), placeholder = 'Search...', onChange }: Props = $props();
function focusInput(node: HTMLElement) { function focusInput(node: HTMLElement) {
const handleKeydown = (event: KeyboardEvent) => { const handleKeydown = (event: KeyboardEvent) => {
@ -16,62 +19,35 @@
node.focus(); node.focus();
} }
}; };
window.addEventListener('keydown', handleKeydown); window.addEventListener('keydown', handleKeydown);
return { return {
destroy() { destroy() {
window.removeEventListener('keydown', handleKeydown); window.removeEventListener('keydown', handleKeydown);
} }
}; };
} }
let searchParams = {} as { [key: string]: string };
$: {
if ($page) {
searchParams = Object.fromEntries($page.url.searchParams);
if (!searchParams?.search) {
clearSearch();
}
}
}
</script> </script>
<div class="sticky top-[63px] z-50"> <div class="sticky top-[63px] z-50">
<div class="relative w-full text-[16px]"> <div class="relative w-full text-[16px]">
<div class="absolute inset-y-0 left-0 flex items-center pl-3 text-neutral-500"> <div class="absolute inset-y-0 left-0 flex items-center pl-3 text-neutral-500">
<div class="pointer-events-none"> <SearchIcon size={20} class="pointer-events-none" strokeWidth={searchTerm ? 2.5 : 1.5} />
<SearchIcon size={20} strokeWidth={searchTerm ? 2.5 : 1.5} />
</div>
</div> </div>
<input <input
type="text" type="search"
{placeholder}
autocomplete="off" autocomplete="off"
class={inputStyles} class={cn(inputStyles, 'py-3 pr-[54px] text-[16px]')}
{placeholder}
bind:value={searchTerm} bind:value={searchTerm}
on:input
use:focusInput
bind:this={inputElement} bind:this={inputElement}
use:focusInput
oninput={(event) => onChange(event.currentTarget.value)}
/> />
{#if searchTerm.length > 0} <div class="absolute inset-y-0 right-0 flex items-center pr-4 text-neutral-500">
<div class="absolute inset-y-0 right-0 flex items-center pr-3"> <div class="pointer-events-none flex h-full items-center gap-x-1 font-mono">
<button <Command size={16} />
type="button" <span>K</span>
class="focus:outline-none focus:ring-1 focus:ring-neutral-300"
on:click={clearSearch}
>
<X size={18} />
</button>
</div> </div>
{:else} </div>
<div class="absolute inset-y-0 right-0 flex items-center pr-4 text-neutral-500">
<div class="flex h-full items-center pointer-events-none gap-x-1 font-mono">
<Command size={16} />
<span>K</span>
</div>
</div>
{/if}
</div> </div>
</div> </div>

View File

@ -1,17 +1,30 @@
<script lang="ts"> <script lang="ts">
import type { iSVG } from '@/types/svg'; import type { iSVG } from '@/types/svg';
import { cn } from '@/utils/cn';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { queryParam } from 'sveltekit-search-params'; import { queryParam } from 'sveltekit-search-params';
import { ArrowDown, ArrowDownUpIcon, ArrowUpDownIcon, TrashIcon } from 'lucide-svelte';
import Fuse from 'fuse.js'; import Fuse from 'fuse.js';
// Styles:
import { cn } from '@/utils/cn';
import { buttonStyles } from '@/ui/styles';
// Components:
import Search from '@/components/search.svelte';
import Container from '@/components/container.svelte';
import SvgCard from '@/components/svgCard.svelte';
import Grid from '@/components/grid.svelte';
import NotFound from '@/components/notFound.svelte';
// Get all svgs: // Get all svgs:
import { svgsData } from '@/data'; import { svgsData } from '@/data';
const allSvgs = JSON.parse(JSON.stringify(svgsData));
// Cache sorted arrays // Settings:
const allSvgs = JSON.parse(JSON.stringify(svgsData));
const latestSorted = [...allSvgs].sort((a, b) => b.id! - a.id!); const latestSorted = [...allSvgs].sort((a, b) => b.id! - a.id!);
const alphabeticallySorted = [...allSvgs].sort((a, b) => a.title.localeCompare(b.title)); const alphabeticallySorted = [...allSvgs].sort((a, b) => a.title.localeCompare(b.title));
const maxSvgsToShow = 30;
// Fuzzy search setup: // Fuzzy search setup:
const fuse = new Fuse<iSVG>(allSvgs, { const fuse = new Fuse<iSVG>(allSvgs, {
@ -22,35 +35,20 @@
shouldSort: true shouldSort: true
}); });
// Components:
import Search from '@/components/search.svelte';
import Container from '@/components/container.svelte';
import SvgCard from '@/components/svgCard.svelte';
import Grid from '@/components/grid.svelte';
import NotFound from '@/components/notFound.svelte';
// URL params // URL params
const searchParam = queryParam('search'); const searchParam = queryParam('search');
// Icons: // States:
import { ArrowDown, ArrowDownUpIcon, ArrowUpDownIcon, TrashIcon } from 'lucide-svelte'; let sorted = $state<boolean>(false);
import { buttonStyles } from '@/ui/styles'; let showAll = $state<boolean>(false);
let searchTerm = $state<string>('');
let sorted: boolean = false; let filteredSvgs = $state<iSVG[]>([]);
let showAll: boolean = false; let displaySvgs = $state<iSVG[]>([]);
// Search:
let searchTerm = '';
let filteredSvgs: iSVG[] = [];
let displaySvgs: iSVG[] = [];
const updateDisplaySvgs = () => { const updateDisplaySvgs = () => {
displaySvgs = showAll ? filteredSvgs : filteredSvgs.slice(0, 30); displaySvgs = showAll ? filteredSvgs : filteredSvgs.slice(0, 30);
}; };
// Hybrid search strategy:
// - Simple string matching for queries < 3 chars
// - Fuzzy search for longer queries (handle typos and partial matches)
const searchSvgs = () => { const searchSvgs = () => {
$searchParam = searchTerm || null; $searchParam = searchTerm || null;
@ -74,7 +72,6 @@
// Clear search: // Clear search:
const clearSearch = () => { const clearSearch = () => {
searchTerm = ''; searchTerm = '';
// Use current sort state to determine order
filteredSvgs = sorted ? alphabeticallySorted : latestSorted; filteredSvgs = sorted ? alphabeticallySorted : latestSorted;
updateDisplaySvgs(); updateDisplaySvgs();
}; };
@ -93,11 +90,11 @@
searchSvgs(); searchSvgs();
}); });
$: { $effect.pre(() => {
if (showAll || filteredSvgs) { if (showAll || filteredSvgs) {
updateDisplaySvgs(); updateDisplaySvgs();
} }
} });
</script> </script>
<svelte:head> <svelte:head>
@ -105,10 +102,13 @@
</svelte:head> </svelte:head>
<Search <Search
bind:searchTerm {searchTerm}
on:input={searchSvgs}
clearSearch={() => clearSearch()}
placeholder={`Search ${allSvgs.length} logos...`} placeholder={`Search ${allSvgs.length} logos...`}
onChange={(value) => {
searchParam.set(value);
searchTerm = value;
searchSvgs();
}}
/> />
<Container> <Container>
@ -119,7 +119,7 @@
'flex items-center justify-center space-x-1 rounded-md py-1.5 text-sm font-medium opacity-80 transition-opacity hover:opacity-100', 'flex items-center justify-center space-x-1 rounded-md py-1.5 text-sm font-medium opacity-80 transition-opacity hover:opacity-100',
filteredSvgs.length === 0 && 'hidden' filteredSvgs.length === 0 && 'hidden'
)} )}
on:click={() => clearSearch()} onclick={() => clearSearch()}
> >
<TrashIcon size={16} strokeWidth={2} class="mr-1" /> <TrashIcon size={16} strokeWidth={2} class="mr-1" />
<span>Clear results</span> <span>Clear results</span>
@ -130,7 +130,7 @@
'flex items-center justify-center space-x-1 rounded-md py-1.5 text-sm font-medium opacity-80 transition-opacity hover:opacity-100', 'flex items-center justify-center space-x-1 rounded-md py-1.5 text-sm font-medium opacity-80 transition-opacity hover:opacity-100',
filteredSvgs.length === 0 && 'hidden' filteredSvgs.length === 0 && 'hidden'
)} )}
on:click={() => sort()} onclick={() => sort()}
> >
{#if sorted} {#if sorted}
<ArrowDownUpIcon size={16} strokeWidth={2} class="mr-1" /> <ArrowDownUpIcon size={16} strokeWidth={2} class="mr-1" />
@ -145,11 +145,11 @@
<SvgCard svgInfo={svg} {searchTerm} /> <SvgCard svgInfo={svg} {searchTerm} />
{/each} {/each}
</Grid> </Grid>
{#if filteredSvgs.length > 30 && !showAll} {#if filteredSvgs.length > maxSvgsToShow && !showAll}
<div class="mt-4 flex items-center justify-center"> <div class="mt-4 flex items-center justify-center">
<button <button
class={buttonStyles} class={buttonStyles}
on:click={() => { onclick={() => {
showAll = true; showAll = true;
updateDisplaySvgs(); updateDisplaySvgs();
}} }}
@ -158,7 +158,7 @@
<ArrowDown size={16} strokeWidth={2} /> <ArrowDown size={16} strokeWidth={2} />
<span>Load All SVGs</span> <span>Load All SVGs</span>
<span class="opacity-70"> <span class="opacity-70">
({filteredSvgs.length - 30} more) ({filteredSvgs.length - maxSvgsToShow} more)
</span> </span>
</div> </div>
</button> </button>

View File

@ -52,13 +52,16 @@
<Container> <Container>
<Search <Search
bind:searchTerm bind:searchTerm
on:input={searchSvgs}
clearSearch={() => clearSearch()}
placeholder={`Search ${filteredSvgs.length} ${category} logos...`} placeholder={`Search ${filteredSvgs.length} ${category} logos...`}
onChange={(value) => {
searchParam.set(value);
searchTerm = value;
searchSvgs();
}}
/> />
<Grid> <Grid>
{#each filteredSvgs as svg} {#each filteredSvgs as svg}
<SvgCard svgInfo={svg} /> <SvgCard svgInfo={svg} {searchTerm} />
{/each} {/each}
</Grid> </Grid>
{#if filteredSvgs.length === 0} {#if filteredSvgs.length === 0}