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:
@@ -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">
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 />
|
||||||
|
|||||||
Reference in New Issue
Block a user