Replace plain text search with Fuse.js fuzzy search

Uses the existing FuzzySearch component and useFuzzySearch composable
(same pattern as market module). Searches across title (50%), summary
(20%), description (15%), location (10%), and tags (5%) with threshold
0.35. Fuzzy search feeds into the display list after temporal/category/
date filters are applied. Removed manual string search from filter
composable.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Padreug 2026-04-20 08:42:10 +02:00
commit bd6ba375b3
2 changed files with 43 additions and 27 deletions

View file

@ -14,13 +14,11 @@ import { DEFAULT_FILTERS } from '../types/filters'
export function useActivityFilters() { export function useActivityFilters() {
const temporal = ref<TemporalFilter>(DEFAULT_FILTERS.temporal) const temporal = ref<TemporalFilter>(DEFAULT_FILTERS.temporal)
const selectedCategories = ref<ActivityCategory[]>([]) const selectedCategories = ref<ActivityCategory[]>([])
const searchQuery = ref('')
const selectedDate = ref<Date | undefined>(undefined) const selectedDate = ref<Date | undefined>(undefined)
const filters = computed<ActivityFilters>(() => ({ const filters = computed<ActivityFilters>(() => ({
temporal: temporal.value, temporal: temporal.value,
categories: selectedCategories.value, categories: selectedCategories.value,
search: searchQuery.value || undefined,
})) }))
/** /**
@ -49,17 +47,6 @@ export function useActivityFilters() {
) )
} }
// Search filter
if (searchQuery.value.trim()) {
const query = searchQuery.value.toLowerCase().trim()
result = result.filter(a =>
a.title.toLowerCase().includes(query) ||
a.summary?.toLowerCase().includes(query) ||
a.description.toLowerCase().includes(query) ||
a.location?.toLowerCase().includes(query)
)
}
return result return result
} }
@ -93,14 +80,12 @@ export function useActivityFilters() {
function resetFilters() { function resetFilters() {
temporal.value = DEFAULT_FILTERS.temporal temporal.value = DEFAULT_FILTERS.temporal
selectedCategories.value = [] selectedCategories.value = []
searchQuery.value = ''
selectedDate.value = undefined selectedDate.value = undefined
} }
const hasActiveFilters = computed(() => const hasActiveFilters = computed(() =>
temporal.value !== 'all' || temporal.value !== 'all' ||
selectedCategories.value.length > 0 || selectedCategories.value.length > 0 ||
searchQuery.value.trim().length > 0 ||
selectedDate.value !== undefined selectedDate.value !== undefined
) )
@ -108,7 +93,6 @@ export function useActivityFilters() {
// State // State
temporal, temporal,
selectedCategories, selectedCategories,
searchQuery,
selectedDate, selectedDate,
filters, filters,
hasActiveFilters, hasActiveFilters,

View file

@ -1,16 +1,17 @@
<script setup lang="ts"> <script setup lang="ts">
import { onMounted, ref } from 'vue' import { onMounted, ref, computed } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { import {
Collapsible, Collapsible,
CollapsibleContent, CollapsibleContent,
CollapsibleTrigger, CollapsibleTrigger,
} from '@/components/ui/collapsible' } from '@/components/ui/collapsible'
import { RefreshCw, Search, SlidersHorizontal, ChevronDown, Plus } from 'lucide-vue-next' import { FuzzySearch } from '@/components/ui/fuzzy-search'
import { RefreshCw, SlidersHorizontal, ChevronDown, Plus } from 'lucide-vue-next'
import { useAuth } from '@/composables/useAuthService' import { useAuth } from '@/composables/useAuthService'
import type { FuzzySearchOptions } from '@/composables/useFuzzySearch'
import { useActivities } from '../composables/useActivities' import { useActivities } from '../composables/useActivities'
import CreateActivityDialog from '../components/CreateActivityDialog.vue' import CreateActivityDialog from '../components/CreateActivityDialog.vue'
import TemporalFilterBar from '../components/TemporalFilterBar.vue' import TemporalFilterBar from '../components/TemporalFilterBar.vue'
@ -31,7 +32,6 @@ const {
error, error,
temporal, temporal,
selectedCategories, selectedCategories,
searchQuery,
hasActiveFilters, hasActiveFilters,
selectedDate, selectedDate,
selectDate, selectDate,
@ -44,6 +44,36 @@ const {
} = useActivities() } = useActivities()
const filtersOpen = ref(false) const filtersOpen = ref(false)
const fuzzyResults = ref<Activity[]>([])
const isFuzzySearching = ref(false)
const searchOptions: FuzzySearchOptions<Activity> = {
fuseOptions: {
keys: [
{ name: 'title', weight: 0.5 },
{ name: 'summary', weight: 0.2 },
{ name: 'description', weight: 0.15 },
{ name: 'location', weight: 0.1 },
{ name: 'tags', weight: 0.05 },
],
threshold: 0.35,
ignoreLocation: true,
},
matchAllWhenSearchEmpty: true,
minSearchLength: 2,
}
const displayedActivities = computed(() => {
return isFuzzySearching.value ? fuzzyResults.value : activities.value
})
function handleSearchResults(results: Activity[]) {
fuzzyResults.value = results
}
function handleSearch(query: string) {
isFuzzySearching.value = query.trim().length >= 2
}
onMounted(() => { onMounted(() => {
subscribe() subscribe()
@ -80,13 +110,15 @@ function handleRefresh() {
</div> </div>
</div> </div>
<!-- Search bar --> <!-- Fuzzy search -->
<div class="relative mb-4"> <div class="mb-4">
<Search class="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" /> <FuzzySearch
<Input :data="activities"
v-model="searchQuery" :options="searchOptions"
placeholder="Search activities..." placeholder="Search activities..."
class="pl-9" :show-result-count="false"
@search="handleSearch"
@results="handleSearchResults"
/> />
</div> </div>
@ -136,7 +168,7 @@ function handleRefresh() {
<!-- Activity feed --> <!-- Activity feed -->
<ActivityList <ActivityList
:activities="activities" :activities="displayedActivities"
:is-loading="isLoading" :is-loading="isLoading"
@select="handleSelectActivity" @select="handleSelectActivity"
/> />