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:
parent
272288f1e2
commit
bd6ba375b3
2 changed files with 43 additions and 27 deletions
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue