feat(05-03): add highlighting to EntryCard and integrate search UI

- Add searchQuery prop to EntryCard for text highlighting
- Use {@html highlightText()} for title and content in collapsed view
- Integrate SearchBar and FilterBar in +page.svelte
- Add search/filter state with reactive sync to filters.query
- Pass recentSearches store to SearchBar
- Update filterEntries to use generics for type preservation
This commit is contained in:
Thomas Richter
2026-01-31 17:18:55 +01:00
parent 9642b51ef8
commit bb1e64ad2b
3 changed files with 52 additions and 11 deletions

View File

@@ -6,6 +6,7 @@
import ImageGallery from './ImageGallery.svelte'; import ImageGallery from './ImageGallery.svelte';
import ImageUpload from './ImageUpload.svelte'; import ImageUpload from './ImageUpload.svelte';
import TagInput from './TagInput.svelte'; import TagInput from './TagInput.svelte';
import { highlightText } from '$lib/utils/highlightText';
interface EntryWithData extends Entry { interface EntryWithData extends Entry {
images: Image[]; images: Image[];
@@ -15,9 +16,10 @@
interface Props { interface Props {
entry: EntryWithData; entry: EntryWithData;
availableTags: Tag[]; availableTags: Tag[];
searchQuery?: string;
} }
let { entry, availableTags }: Props = $props(); let { entry, availableTags, searchQuery = '' }: Props = $props();
// Expand/collapse state // Expand/collapse state
let expanded = $state(false); let expanded = $state(false);
@@ -300,7 +302,11 @@
? 'line-through text-gray-400' ? 'line-through text-gray-400'
: ''}" : ''}"
> >
{#if !expanded && searchQuery}
{@html highlightText(entry.title || 'Untitled', searchQuery)}
{:else}
{entry.title || 'Untitled'} {entry.title || 'Untitled'}
{/if}
</h3> </h3>
{#if !expanded} {#if !expanded}
<p <p
@@ -308,7 +314,11 @@
? 'text-gray-400' ? 'text-gray-400'
: ''}" : ''}"
> >
{#if searchQuery}
{@html highlightText(entry.content, searchQuery)}
{:else}
{entry.content} {entry.content}
{/if}
</p> </p>
{#if entry.tags?.length > 0} {#if entry.tags?.length > 0}
<div class="flex flex-wrap gap-1 mt-1"> <div class="flex flex-wrap gap-1 mt-1">

View File

@@ -7,15 +7,16 @@ interface EntryWithTags extends Entry {
/** /**
* Filters entries based on search criteria. * Filters entries based on search criteria.
* Uses generics to preserve full entry type (including images, etc.)
* *
* @param entries - Array of entries with their tags * @param entries - Array of entries with their tags
* @param filters - SearchFilters object containing filter criteria * @param filters - SearchFilters object containing filter criteria
* @returns Filtered array of entries * @returns Filtered array of entries
*/ */
export function filterEntries( export function filterEntries<T extends EntryWithTags>(
entries: EntryWithTags[], entries: T[],
filters: SearchFilters filters: SearchFilters
): EntryWithTags[] { ): T[] {
let result = entries; let result = entries;
// Text search (title + content) - minimum 2 characters // Text search (title + content) - minimum 2 characters

View File

@@ -1,14 +1,29 @@
<script lang="ts"> <script lang="ts">
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { page } from '$app/state'; import { page } from '$app/state';
import { get } from 'svelte/store';
import EntryList from '$lib/components/EntryList.svelte'; import EntryList from '$lib/components/EntryList.svelte';
import QuickCapture from '$lib/components/QuickCapture.svelte'; import QuickCapture from '$lib/components/QuickCapture.svelte';
import CompletedToggle from '$lib/components/CompletedToggle.svelte'; import CompletedToggle from '$lib/components/CompletedToggle.svelte';
import SearchBar from '$lib/components/SearchBar.svelte';
import FilterBar from '$lib/components/FilterBar.svelte';
import { preferences } from '$lib/stores/preferences.svelte'; import { preferences } from '$lib/stores/preferences.svelte';
import { get } from 'svelte/store'; import { recentSearches, addRecentSearch } from '$lib/stores/recentSearches';
import { type SearchFilters, defaultFilters } from '$lib/types/search';
let { data } = $props(); let { data } = $props();
// Search and filter state
let searchQuery = $state('');
let filters = $state<SearchFilters>({ ...defaultFilters });
// Sync filters.query with searchQuery
$effect(() => {
if (filters.query !== searchQuery) {
filters = { ...filters, query: searchQuery };
}
});
// Sync URL with preference on mount (client-side only) // Sync URL with preference on mount (client-side only)
$effect(() => { $effect(() => {
if (typeof window !== 'undefined') { if (typeof window !== 'undefined') {
@@ -27,6 +42,14 @@
} }
} }
}); });
function handleFilterChange(newFilters: SearchFilters) {
filters = newFilters;
// Sync query back to searchQuery
if (newFilters.query !== searchQuery) {
searchQuery = newFilters.query;
}
}
</script> </script>
<svelte:head> <svelte:head>
@@ -35,14 +58,21 @@
<main class="min-h-screen bg-gray-50 pb-40"> <main class="min-h-screen bg-gray-50 pb-40">
<header class="bg-white border-b border-gray-200 sticky top-0 z-10"> <header class="bg-white border-b border-gray-200 sticky top-0 z-10">
<div class="max-w-2xl mx-auto px-4 py-4 flex items-center justify-between"> <div class="max-w-2xl mx-auto px-4 py-4">
<div class="flex items-center justify-between mb-3">
<h1 class="text-xl font-bold text-gray-900">TaskPlaner</h1> <h1 class="text-xl font-bold text-gray-900">TaskPlaner</h1>
<CompletedToggle /> <CompletedToggle />
</div> </div>
<SearchBar bind:value={searchQuery} recentSearches={$recentSearches} />
</div>
</header> </header>
<div class="max-w-2xl mx-auto px-4 py-3">
<FilterBar {filters} availableTags={data.allTags} onchange={handleFilterChange} />
</div>
<div class="max-w-2xl mx-auto px-4 py-4"> <div class="max-w-2xl mx-auto px-4 py-4">
<EntryList entries={data.entries} availableTags={data.allTags} /> <EntryList entries={data.entries} availableTags={data.allTags} {filters} {searchQuery} />
</div> </div>
<QuickCapture /> <QuickCapture />