mirror of
https://github.com/pheralb/svgl.git
synced 2025-02-15 03:30:31 +08:00
🚀 Upgrade search component to Svelte 5 + add `onChange
` parameter
This commit is contained in:
parent
d6be3ad941
commit
f9951efd7f
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user