feat(05-01): create FilterBar component with type, tag, date controls

- Type filter: three-state toggle (All/Tasks/Thoughts)
- Tag filter: multi-select using Svelecte
- Date range: quick presets (Today/Week/Month) + custom inputs
- Clear button: only visible when hasActiveFilters() returns true
- Horizontal flex-wrap layout for responsive design
This commit is contained in:
Thomas Richter
2026-01-31 17:13:16 +01:00
parent 6dbe660a8e
commit 5e94609286

View File

@@ -0,0 +1,179 @@
<script lang="ts">
import Svelecte from 'svelecte';
import type { Tag } from '$lib/server/db/schema';
import { type SearchFilters, defaultFilters, hasActiveFilters, getDatePreset } from '$lib/types/search';
interface Props {
filters: SearchFilters;
availableTags: Tag[];
onchange: (filters: SearchFilters) => void;
}
let { filters, availableTags, onchange }: Props = $props();
// Transform tags to Svelecte format
let tagOptions = $derived(availableTags.map((t) => ({ value: t.name, label: t.name })));
// Track selected tag names for Svelecte
let selectedTagNames = $state(filters.tags);
// Sync selectedTagNames when filters prop changes externally
$effect(() => {
selectedTagNames = filters.tags;
});
function handleTypeChange(newType: 'task' | 'thought' | 'all') {
onchange({ ...filters, type: newType });
}
function handleTagChange(selection: { value: string; label: string }[] | null) {
const tags = (selection || []).map((item) => item.value);
onchange({ ...filters, tags });
}
function handleDatePreset(preset: 'today' | 'week' | 'month') {
const { start, end } = getDatePreset(preset);
onchange({ ...filters, dateRange: { start, end } });
}
function handleCustomDateStart(e: Event) {
const target = e.target as HTMLInputElement;
onchange({
...filters,
dateRange: { ...filters.dateRange, start: target.value || null }
});
}
function handleCustomDateEnd(e: Event) {
const target = e.target as HTMLInputElement;
onchange({
...filters,
dateRange: { ...filters.dateRange, end: target.value || null }
});
}
function handleClear() {
onchange({ ...defaultFilters });
}
// Check if filters are active
let showClear = $derived(hasActiveFilters(filters));
</script>
<div class="flex flex-wrap gap-3 items-center">
<!-- Type filter: three-state toggle -->
<div class="flex rounded-lg border border-gray-200 overflow-hidden">
<button
type="button"
onclick={() => handleTypeChange('all')}
class="px-3 py-1.5 text-sm font-medium transition-colors {filters.type === 'all'
? 'bg-blue-100 text-blue-700'
: 'bg-white text-gray-600 hover:bg-gray-50'}"
>
All
</button>
<button
type="button"
onclick={() => handleTypeChange('task')}
class="px-3 py-1.5 text-sm font-medium border-l border-gray-200 transition-colors {filters.type === 'task'
? 'bg-blue-100 text-blue-700'
: 'bg-white text-gray-600 hover:bg-gray-50'}"
>
Tasks
</button>
<button
type="button"
onclick={() => handleTypeChange('thought')}
class="px-3 py-1.5 text-sm font-medium border-l border-gray-200 transition-colors {filters.type === 'thought'
? 'bg-blue-100 text-blue-700'
: 'bg-white text-gray-600 hover:bg-gray-50'}"
>
Thoughts
</button>
</div>
<!-- Tag filter: multi-select -->
<div class="min-w-[180px]">
<Svelecte
options={tagOptions}
value={selectedTagNames}
multiple
valueField="value"
labelField="label"
placeholder="Filter by tags..."
onChange={handleTagChange}
class="filter-tag-input"
/>
</div>
<!-- Date range filter -->
<div class="flex items-center gap-2">
<!-- Quick presets -->
<div class="flex rounded-lg border border-gray-200 overflow-hidden">
<button
type="button"
onclick={() => handleDatePreset('today')}
class="px-2 py-1.5 text-xs font-medium transition-colors {filters.dateRange.start === getDatePreset('today').start
? 'bg-blue-100 text-blue-700'
: 'bg-white text-gray-600 hover:bg-gray-50'}"
>
Today
</button>
<button
type="button"
onclick={() => handleDatePreset('week')}
class="px-2 py-1.5 text-xs font-medium border-l border-gray-200 transition-colors {filters.dateRange.start === getDatePreset('week').start
? 'bg-blue-100 text-blue-700'
: 'bg-white text-gray-600 hover:bg-gray-50'}"
>
Week
</button>
<button
type="button"
onclick={() => handleDatePreset('month')}
class="px-2 py-1.5 text-xs font-medium border-l border-gray-200 transition-colors {filters.dateRange.start === getDatePreset('month').start
? 'bg-blue-100 text-blue-700'
: 'bg-white text-gray-600 hover:bg-gray-50'}"
>
Month
</button>
</div>
<!-- Custom date range -->
<div class="flex items-center gap-1 text-sm">
<input
type="date"
value={filters.dateRange.start || ''}
onchange={handleCustomDateStart}
class="px-2 py-1 border border-gray-200 rounded text-xs focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
<span class="text-gray-400">-</span>
<input
type="date"
value={filters.dateRange.end || ''}
onchange={handleCustomDateEnd}
class="px-2 py-1 border border-gray-200 rounded text-xs focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
</div>
<!-- Clear/Reset button - only visible when filters active -->
{#if showClear}
<button
type="button"
onclick={handleClear}
class="px-3 py-1.5 text-sm font-medium text-red-600 hover:text-red-700 hover:bg-red-50 rounded-lg transition-colors"
>
Clear filters
</button>
{/if}
</div>
<style>
:global(.filter-tag-input) {
--sv-bg: white;
--sv-border: 1px solid #e5e7eb;
--sv-border-radius: 0.5rem;
--sv-min-height: 34px;
}
</style>