mirror of
https://github.com/pheralb/svgl.git
synced 2025-02-05 22:48:17 +08:00
feat: implement fuzzy search and optimize sorting
This commit is contained in:
parent
b5207c4bf0
commit
07f474b1b0
@ -43,6 +43,7 @@
|
|||||||
"@upstash/redis": "1.34.0",
|
"@upstash/redis": "1.34.0",
|
||||||
"clsx": "2.1.1",
|
"clsx": "2.1.1",
|
||||||
"downloadjs": "1.4.7",
|
"downloadjs": "1.4.7",
|
||||||
|
"fuse.js": "^7.0.0",
|
||||||
"jszip": "3.10.1",
|
"jszip": "3.10.1",
|
||||||
"lucide-svelte": "0.445.0",
|
"lucide-svelte": "0.445.0",
|
||||||
"mode-watcher": "0.4.1",
|
"mode-watcher": "0.4.1",
|
||||||
|
10
pnpm-lock.yaml
generated
10
pnpm-lock.yaml
generated
@ -32,6 +32,9 @@ importers:
|
|||||||
downloadjs:
|
downloadjs:
|
||||||
specifier: 1.4.7
|
specifier: 1.4.7
|
||||||
version: 1.4.7
|
version: 1.4.7
|
||||||
|
fuse.js:
|
||||||
|
specifier: ^7.0.0
|
||||||
|
version: 7.0.0
|
||||||
jszip:
|
jszip:
|
||||||
specifier: 3.10.1
|
specifier: 3.10.1
|
||||||
version: 3.10.1
|
version: 3.10.1
|
||||||
@ -1459,6 +1462,10 @@ packages:
|
|||||||
function-bind@1.1.2:
|
function-bind@1.1.2:
|
||||||
resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
|
resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
|
||||||
|
|
||||||
|
fuse.js@7.0.0:
|
||||||
|
resolution: {integrity: sha512-14F4hBIxqKvD4Zz/XjDc3y94mNZN6pRv3U13Udo0lNLCWRBUsrMv2xwcF/y/Z5sV6+FQW+/ow68cHpm4sunt8Q==}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
gensync@1.0.0-beta.2:
|
gensync@1.0.0-beta.2:
|
||||||
resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==}
|
resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==}
|
||||||
engines: {node: '>=6.9.0'}
|
engines: {node: '>=6.9.0'}
|
||||||
@ -2296,6 +2303,7 @@ packages:
|
|||||||
|
|
||||||
shikiji-core@0.10.2:
|
shikiji-core@0.10.2:
|
||||||
resolution: {integrity: sha512-9Of8HMlF96usXJHmCL3Gd0Fcf0EcyJUF9m8EoAKKd98mHXi0La2AZl1h6PegSFGtiYcBDK/fLuKbDa1l16r1fA==}
|
resolution: {integrity: sha512-9Of8HMlF96usXJHmCL3Gd0Fcf0EcyJUF9m8EoAKKd98mHXi0La2AZl1h6PegSFGtiYcBDK/fLuKbDa1l16r1fA==}
|
||||||
|
deprecated: Shikiji is merged back to Shiki v1.0, please migrate over to get the latest updates
|
||||||
|
|
||||||
shikiji@0.10.2:
|
shikiji@0.10.2:
|
||||||
resolution: {integrity: sha512-wtZg3T0vtYV2PnqusWQs3mDaJBdCPWxFDrBM/SE5LfrX92gjUvfEMlc+vJnoKY6Z/S44OWaCRzNIsdBRWcTAiw==}
|
resolution: {integrity: sha512-wtZg3T0vtYV2PnqusWQs3mDaJBdCPWxFDrBM/SE5LfrX92gjUvfEMlc+vJnoKY6Z/S44OWaCRzNIsdBRWcTAiw==}
|
||||||
@ -3995,6 +4003,8 @@ snapshots:
|
|||||||
|
|
||||||
function-bind@1.1.2: {}
|
function-bind@1.1.2: {}
|
||||||
|
|
||||||
|
fuse.js@7.0.0: {}
|
||||||
|
|
||||||
gensync@1.0.0-beta.2: {}
|
gensync@1.0.0-beta.2: {}
|
||||||
|
|
||||||
get-caller-file@2.0.5: {}
|
get-caller-file@2.0.5: {}
|
||||||
|
@ -1,12 +1,27 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { iSVG } from '@/types/svg';
|
import type { iSVG } from '@/types/svg';
|
||||||
import { cn } from '@/utils/cn';
|
import { cn } from '@/utils/cn';
|
||||||
|
import { onMount } from 'svelte';
|
||||||
import { queryParam } from 'sveltekit-search-params';
|
import { queryParam } from 'sveltekit-search-params';
|
||||||
|
import Fuse from 'fuse.js';
|
||||||
|
|
||||||
// Get all svgs:
|
// Get all svgs:
|
||||||
import { svgsData } from '@/data';
|
import { svgsData } from '@/data';
|
||||||
const allSvgs = JSON.parse(JSON.stringify(svgsData));
|
const allSvgs = JSON.parse(JSON.stringify(svgsData));
|
||||||
|
|
||||||
|
// Cache sorted arrays
|
||||||
|
const latestSorted = [...allSvgs].sort((a, b) => b.id! - a.id!);
|
||||||
|
const alphabeticallySorted = [...allSvgs].sort((a, b) => a.title.localeCompare(b.title));
|
||||||
|
|
||||||
|
// Fuzzy search setup:
|
||||||
|
const fuse = new Fuse<iSVG>(allSvgs, {
|
||||||
|
keys: ['title'],
|
||||||
|
threshold: 0.35,
|
||||||
|
ignoreLocation: true,
|
||||||
|
isCaseSensitive: false,
|
||||||
|
shouldSort: true
|
||||||
|
});
|
||||||
|
|
||||||
// Components:
|
// Components:
|
||||||
import Search from '@/components/search.svelte';
|
import Search from '@/components/search.svelte';
|
||||||
import Container from '@/components/container.svelte';
|
import Container from '@/components/container.svelte';
|
||||||
@ -22,73 +37,66 @@
|
|||||||
import { buttonStyles } from '@/ui/styles';
|
import { buttonStyles } from '@/ui/styles';
|
||||||
|
|
||||||
let sorted: boolean = false;
|
let sorted: boolean = false;
|
||||||
let isFirstLoad: boolean = true;
|
|
||||||
let showAll: boolean = false;
|
let showAll: boolean = false;
|
||||||
|
|
||||||
// Search:
|
// Search:
|
||||||
let searchTerm = $searchParam || '';
|
let searchTerm = '';
|
||||||
let filteredSvgs: iSVG[] = [];
|
let filteredSvgs: iSVG[] = [];
|
||||||
|
let displaySvgs: iSVG[] = [];
|
||||||
|
|
||||||
// Order by last added:
|
const updateDisplaySvgs = () => {
|
||||||
if (searchTerm.length === 0) {
|
displaySvgs = showAll ? filteredSvgs : filteredSvgs.slice(0, 30);
|
||||||
filteredSvgs = allSvgs.sort((a: iSVG, b: iSVG) => {
|
|
||||||
return b.id! - a.id!;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const loadSvgs = () => {
|
|
||||||
if (isFirstLoad || showAll) {
|
|
||||||
filteredSvgs = allSvgs;
|
|
||||||
isFirstLoad = false;
|
|
||||||
} else {
|
|
||||||
filteredSvgs = allSvgs.slice(0, 30);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Search svgs:
|
// 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;
|
||||||
loadSvgs();
|
|
||||||
filteredSvgs = allSvgs.filter((svg: iSVG) => {
|
if (!searchTerm) {
|
||||||
let svgTitle = svg.title.toLowerCase();
|
filteredSvgs = sorted ? alphabeticallySorted : latestSorted;
|
||||||
return svgTitle.includes(searchTerm.toLowerCase());
|
updateDisplaySvgs();
|
||||||
});
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (searchTerm.length < 3) {
|
||||||
|
filteredSvgs = allSvgs.filter((svg: iSVG) =>
|
||||||
|
svg.title.toLowerCase().includes(searchTerm.toLowerCase())
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
filteredSvgs = fuse.search(searchTerm).map((result) => result.item);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateDisplaySvgs();
|
||||||
};
|
};
|
||||||
|
|
||||||
// Clear search:
|
// Clear search:
|
||||||
const clearSearch = () => {
|
const clearSearch = () => {
|
||||||
searchTerm = '';
|
searchTerm = '';
|
||||||
searchSvgs();
|
// Use current sort state to determine order
|
||||||
|
filteredSvgs = sorted ? alphabeticallySorted : latestSorted;
|
||||||
|
updateDisplaySvgs();
|
||||||
};
|
};
|
||||||
|
|
||||||
// Sort:
|
// Sort:
|
||||||
const sort = () => {
|
const sort = () => {
|
||||||
if (sorted) {
|
|
||||||
sortByLatest();
|
|
||||||
} else {
|
|
||||||
sortAlphabetically();
|
|
||||||
}
|
|
||||||
sorted = !sorted;
|
sorted = !sorted;
|
||||||
|
filteredSvgs = sorted ? alphabeticallySorted : latestSorted;
|
||||||
|
updateDisplaySvgs();
|
||||||
};
|
};
|
||||||
|
|
||||||
// Sort alphabetically:
|
onMount(() => {
|
||||||
const sortAlphabetically = () => {
|
|
||||||
filteredSvgs = allSvgs.sort((a: iSVG, b: iSVG) => {
|
|
||||||
return a.title.localeCompare(b.title);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// Sort by latest:
|
|
||||||
const sortByLatest = () => {
|
|
||||||
filteredSvgs = filteredSvgs.sort((a: iSVG, b: iSVG) => {
|
|
||||||
return b.id! - a.id!;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
if ($searchParam) {
|
if ($searchParam) {
|
||||||
|
searchTerm = $searchParam;
|
||||||
|
}
|
||||||
searchSvgs();
|
searchSvgs();
|
||||||
} else {
|
});
|
||||||
loadSvgs();
|
|
||||||
|
$: {
|
||||||
|
if (showAll || filteredSvgs) {
|
||||||
|
updateDisplaySvgs();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@ -100,15 +108,15 @@
|
|||||||
bind:searchTerm
|
bind:searchTerm
|
||||||
on:input={searchSvgs}
|
on:input={searchSvgs}
|
||||||
clearSearch={() => clearSearch()}
|
clearSearch={() => clearSearch()}
|
||||||
placeholder={`Search ${filteredSvgs.length} logos...`}
|
placeholder={`Search ${allSvgs.length} logos...`}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Container>
|
<Container>
|
||||||
<div class={cn('flex items-center mb-4 justify-end', searchTerm.length > 0 && 'justify-between')}>
|
<div class={cn('mb-4 flex items-center justify-end', searchTerm.length > 0 && 'justify-between')}>
|
||||||
{#if searchTerm.length > 0}
|
{#if searchTerm.length > 0}
|
||||||
<button
|
<button
|
||||||
class={cn(
|
class={cn(
|
||||||
'flex items-center justify-center space-x-1 rounded-md py-1.5 text-sm font-medium opacity-80 hover:opacity-100 transition-opacity',
|
'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()}
|
on:click={() => clearSearch()}
|
||||||
@ -119,7 +127,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
<button
|
<button
|
||||||
class={cn(
|
class={cn(
|
||||||
'flex items-center justify-center space-x-1 rounded-md py-1.5 text-sm font-medium opacity-80 hover:opacity-100 transition-opacity',
|
'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()}
|
on:click={() => sort()}
|
||||||
@ -129,18 +137,24 @@
|
|||||||
{:else}
|
{:else}
|
||||||
<ArrowUpDownIcon size={16} strokeWidth={2} class="mr-1" />
|
<ArrowUpDownIcon size={16} strokeWidth={2} class="mr-1" />
|
||||||
{/if}
|
{/if}
|
||||||
<span>{sorted ? 'Sort by latest' : 'Sort alphabetically'}</span>
|
<span>{sorted ? 'Sort by latest' : 'Sort A-Z'}</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<Grid>
|
<Grid>
|
||||||
{#each filteredSvgs.slice(0, showAll ? undefined : 30) as svg}
|
{#each displaySvgs as svg}
|
||||||
<SvgCard svgInfo={svg} searchTerm={searchTerm} />
|
<SvgCard svgInfo={svg} {searchTerm} />
|
||||||
{/each}
|
{/each}
|
||||||
</Grid>
|
</Grid>
|
||||||
{#if filteredSvgs.length > 30 && !showAll}
|
{#if filteredSvgs.length > 30 && !showAll}
|
||||||
<div class="flex items-center justify-center mt-4">
|
<div class="mt-4 flex items-center justify-center">
|
||||||
<button class={buttonStyles} on:click={() => (showAll = true)}>
|
<button
|
||||||
<div class="flex items-center space-x-2 relative">
|
class={buttonStyles}
|
||||||
|
on:click={() => {
|
||||||
|
showAll = true;
|
||||||
|
updateDisplaySvgs();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div class="relative flex items-center space-x-2">
|
||||||
<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">
|
||||||
|
Loading…
x
Reference in New Issue
Block a user