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