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:
179
src/lib/components/FilterBar.svelte
Normal file
179
src/lib/components/FilterBar.svelte
Normal 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>
|
||||
Reference in New Issue
Block a user