feat: add fuse.js search module

This commit is contained in:
Levente Orban
2025-08-31 18:50:47 +02:00
parent 834b9e0715
commit 16ad15071c
3 changed files with 111 additions and 62 deletions

33
package-lock.json generated
View File

@@ -10,13 +10,13 @@
"dependencies": { "dependencies": {
"@sveltejs/adapter-node": "^5.3.1", "@sveltejs/adapter-node": "^5.3.1",
"drizzle-orm": "^0.44.5", "drizzle-orm": "^0.44.5",
"fuse.js": "^7.1.0",
"postgres": "^3.4.7" "postgres": "^3.4.7"
}, },
"devDependencies": { "devDependencies": {
"@eslint/compat": "^1.2.5", "@eslint/compat": "^1.2.5",
"@eslint/js": "^9.18.0", "@eslint/js": "^9.18.0",
"@sveltejs/adapter-auto": "^6.0.0", "@sveltejs/adapter-auto": "^6.0.0",
"@sveltejs/adapter-netlify": "^5.2.2",
"@sveltejs/kit": "^2.22.0", "@sveltejs/kit": "^2.22.0",
"@sveltejs/vite-plugin-svelte": "^6.0.0", "@sveltejs/vite-plugin-svelte": "^6.0.0",
"@tailwindcss/forms": "^0.5.9", "@tailwindcss/forms": "^0.5.9",
@@ -1135,13 +1135,6 @@
"url": "https://github.com/sponsors/nzakas" "url": "https://github.com/sponsors/nzakas"
} }
}, },
"node_modules/@iarna/toml": {
"version": "2.2.5",
"resolved": "https://registry.npmjs.org/@iarna/toml/-/toml-2.2.5.tgz",
"integrity": "sha512-trnsAYxU3xnS1gPHPyU961coFyLkh4gAD/0zQ5mymY4yOZ+CYvsPqUbOFSw0aDM4y0tV7tiFxL/1XfXPNC6IPg==",
"dev": true,
"license": "ISC"
},
"node_modules/@isaacs/fs-minipass": { "node_modules/@isaacs/fs-minipass": {
"version": "4.0.1", "version": "4.0.1",
"resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz",
@@ -1630,21 +1623,6 @@
"@sveltejs/kit": "^2.0.0" "@sveltejs/kit": "^2.0.0"
} }
}, },
"node_modules/@sveltejs/adapter-netlify": {
"version": "5.2.2",
"resolved": "https://registry.npmjs.org/@sveltejs/adapter-netlify/-/adapter-netlify-5.2.2.tgz",
"integrity": "sha512-pFWB/lxG8NuJLEYOcPxD059v5QQDFX1vxpBbVobHjgJDCpSDLySGMi4ipDKNPfysqIA9TEG+rwdexz0iaIAocg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@iarna/toml": "^2.2.5",
"esbuild": "^0.25.4",
"set-cookie-parser": "^2.6.0"
},
"peerDependencies": {
"@sveltejs/kit": "^2.4.0"
}
},
"node_modules/@sveltejs/adapter-node": { "node_modules/@sveltejs/adapter-node": {
"version": "5.3.1", "version": "5.3.1",
"resolved": "https://registry.npmjs.org/@sveltejs/adapter-node/-/adapter-node-5.3.1.tgz", "resolved": "https://registry.npmjs.org/@sveltejs/adapter-node/-/adapter-node-5.3.1.tgz",
@@ -3242,6 +3220,15 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/fuse.js": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/fuse.js/-/fuse.js-7.1.0.tgz",
"integrity": "sha512-trLf4SzuuUxfusZADLINj+dE8clK1frKdmqiJNb1Es75fmI5oY6X2mxLVUciLLjxqw/xr72Dhy+lER6dGd02FQ==",
"license": "Apache-2.0",
"engines": {
"node": ">=10"
}
},
"node_modules/get-tsconfig": { "node_modules/get-tsconfig": {
"version": "4.10.1", "version": "4.10.1",
"resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.10.1.tgz", "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.10.1.tgz",

View File

@@ -40,6 +40,7 @@
"dependencies": { "dependencies": {
"@sveltejs/adapter-node": "^5.3.1", "@sveltejs/adapter-node": "^5.3.1",
"drizzle-orm": "^0.44.5", "drizzle-orm": "^0.44.5",
"fuse.js": "^7.1.0",
"postgres": "^3.4.7" "postgres": "^3.4.7"
} }
} }

View File

@@ -1,24 +1,51 @@
<script lang="ts"> <script lang="ts">
import type { Event } from '$lib/types'; import type { Event, EventType } from '$lib/types';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import type { PageData } from '../$types'; import type { PageData } from '../$types';
import { formatTime, formatDate } from '$lib/dateFormatter'; import { formatTime, formatDate } from '$lib/dateFormatter';
import Fuse from 'fuse.js';
let publicEvents: Event[] = []; let publicEvents: Event[] = [];
let error = ''; let error = '';
let searchQuery = ''; let searchQuery = '';
let selectedEventType: EventType | 'all' = 'all';
let fuse: Fuse<Event>;
export let data: PageData; export let data: PageData;
// Use the server-side data // Use the server-side data
$: publicEvents = data.events; $: publicEvents = data.events;
// Filter events based on search query // Initialize Fuse.js with search options
$: filteredEvents = $: fuse = new Fuse(publicEvents, {
searchQuery.trim() === '' keys: [
? publicEvents { name: 'name', weight: 0.7 },
: publicEvents.filter((event) => { name: 'location', weight: 0.3 }
event.name.toLowerCase().includes(searchQuery.toLowerCase()) ],
); threshold: 0.3, // Lower threshold = more strict matching
includeScore: true,
includeMatches: true
});
// Filter events based on search query and event type using Fuse.js
$: filteredEvents = (() => {
let events = publicEvents;
// First filter by event type
if (selectedEventType !== 'all') {
events = events.filter((event) => event.type === selectedEventType);
}
// Then apply search query
if (searchQuery.trim() !== '') {
events = fuse.search(searchQuery).map((result) => result.item);
// Re-apply type filter after search
if (selectedEventType !== 'all') {
events = events.filter((event) => event.type === selectedEventType);
}
}
return events;
})();
</script> </script>
<svelte:head> <svelte:head>
@@ -61,9 +88,11 @@
<p class="text-slate-500">Discover events created by the community</p> <p class="text-slate-500">Discover events created by the community</p>
</div> </div>
<!-- Search Bar --> <!-- Search and Filter Section -->
<div class="mb-8"> <div class="mb-8">
<div class="relative mx-auto max-w-md"> <div class="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-center">
<!-- Search Bar -->
<div class="relative mx-auto max-w-md sm:mx-0">
<div class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3"> <div class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
<svg <svg
class="h-5 w-5 text-slate-400" class="h-5 w-5 text-slate-400"
@@ -82,7 +111,7 @@
<input <input
type="text" type="text"
bind:value={searchQuery} bind:value={searchQuery}
placeholder="Search events by title..." placeholder="Search events by name, location..."
class="w-full rounded-lg border border-slate-600 bg-slate-800 px-4 py-3 pl-10 text-white placeholder-slate-400 focus:border-violet-500 focus:ring-2 focus:ring-violet-500/20 focus:outline-none" class="w-full rounded-lg border border-slate-600 bg-slate-800 px-4 py-3 pl-10 text-white placeholder-slate-400 focus:border-violet-500 focus:ring-2 focus:ring-violet-500/20 focus:outline-none"
/> />
{#if searchQuery} {#if searchQuery}
@@ -101,6 +130,38 @@
</button> </button>
{/if} {/if}
</div> </div>
<!-- Event Type Filter -->
<div class="flex items-center justify-center gap-2">
<button
on:click={() => (selectedEventType = 'all')}
class="rounded-sm border px-3 py-2 text-sm font-medium transition-colors {selectedEventType ===
'all'
? 'border-violet-500 bg-violet-500/20 text-violet-400'
: 'border-slate-600 text-slate-400 hover:border-slate-500 hover:text-slate-300'}"
>
All
</button>
<button
on:click={() => (selectedEventType = 'limited')}
class="rounded-sm border px-3 py-2 text-sm font-medium transition-colors {selectedEventType ===
'limited'
? 'border-amber-600 bg-amber-600/20 text-amber-600'
: 'border-slate-600 text-slate-400 hover:border-slate-500 hover:text-slate-300'}"
>
Limited
</button>
<button
on:click={() => (selectedEventType = 'unlimited')}
class="rounded-sm border px-3 py-2 text-sm font-medium transition-colors {selectedEventType ===
'unlimited'
? 'border-teal-500 bg-teal-500/20 text-teal-500'
: 'border-slate-600 text-slate-400 hover:border-slate-500 hover:text-slate-300'}"
>
Unlimited
</button>
</div>
</div>
</div> </div>
<div class="grid gap-4 md:grid-cols-2 lg:grid-cols-3"> <div class="grid gap-4 md:grid-cols-2 lg:grid-cols-3">