mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-18 21:16:47 +00:00
feat(planner): the places search also finds the description and the notes
The search box of the places list matched only name and address. It now also reads what was written about a place, through one helper the desktop list, its filter counts and the phone's browser share. (#2252)
This commit is contained in:
@@ -2,6 +2,7 @@ import { useState } from 'react'
|
||||
import { Plus, X, MapPin } from 'lucide-react'
|
||||
import { useTranslation } from '../../i18n'
|
||||
import type { Place, AssignmentsMap } from '../../types'
|
||||
import { placeMatchesSearch } from '../../utils/placeSearch'
|
||||
|
||||
export function MobileAddPlaceButton({ dayId, places, assignments, onAssign, onAddNew }: {
|
||||
dayId: number
|
||||
@@ -17,9 +18,7 @@ export function MobileAddPlaceButton({ dayId, places, assignments, onAssign, onA
|
||||
// Find places not assigned to this day
|
||||
const assignedToDay = new Set((assignments[String(dayId)] || []).map(a => a.place_id))
|
||||
const available = places.filter(p => !assignedToDay.has(p.id))
|
||||
const filtered = search.trim()
|
||||
? available.filter(p => p.name.toLowerCase().includes(search.toLowerCase()))
|
||||
: available
|
||||
const filtered = available.filter(p => placeMatchesSearch(p, search))
|
||||
|
||||
return (
|
||||
<div className="md:hidden" style={{ padding: '8px 12px 12px' }}>
|
||||
|
||||
@@ -16,6 +16,7 @@ import { useSaveToCollectionStore } from '../../store/saveToCollectionStore'
|
||||
import { placeToSaveTarget } from '../Collections/saveTarget'
|
||||
import type { Place, Category, Day, AssignmentsMap } from '../../types'
|
||||
import { getGoogleMapsUrlForPlace } from './placeGoogleMaps'
|
||||
import { placeMatchesSearch } from '../../utils/placeSearch'
|
||||
import { safeHttpUrl } from '../../utils/safeUrl'
|
||||
import { plannedPlaceIds, plannedPlaceIdsForDay, type PlannedAccommodation } from '../../utils/plannedPlaces'
|
||||
import type { MenuEntry } from './planParts'
|
||||
@@ -290,8 +291,7 @@ export function usePlacesSidebar(props: PlacesSidebarProps) {
|
||||
if (!categoryFilters.has('uncategorized')) return false
|
||||
} else if (!categoryFilters.has(String(p.category_id))) return false
|
||||
}
|
||||
if (search && !p.name.toLowerCase().includes(search.toLowerCase()) &&
|
||||
!(p.address || '').toLowerCase().includes(search.toLowerCase())) return false
|
||||
if (!placeMatchesSearch(p, search)) return false
|
||||
if (ratingFilter !== 'all' && (p.rating_avg == null || p.rating_avg < ratingFilter)) return false
|
||||
if (localityFilter && !matchesLocality(localityOf.get(p.id), localityFilter)) return false
|
||||
return true
|
||||
@@ -307,16 +307,13 @@ export function usePlacesSidebar(props: PlacesSidebarProps) {
|
||||
* different questions.
|
||||
*/
|
||||
const filterCounts = useMemo(() => {
|
||||
const needle = search.toLowerCase()
|
||||
const base = places.filter(p => {
|
||||
if (categoryFilters.size > 0) {
|
||||
if (p.category_id == null) {
|
||||
if (!categoryFilters.has('uncategorized')) return false
|
||||
} else if (!categoryFilters.has(String(p.category_id))) return false
|
||||
}
|
||||
if (search && !p.name.toLowerCase().includes(needle) &&
|
||||
!(p.address || '').toLowerCase().includes(needle)) return false
|
||||
return true
|
||||
return placeMatchesSearch(p, search)
|
||||
})
|
||||
return {
|
||||
all: base.length,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { AssignmentsMap, Day, Place } from '../../../../types'
|
||||
import { placeMatchesSearch } from '../../../../utils/placeSearch'
|
||||
import {
|
||||
plannedPlaceIds as sharedPlannedPlaceIds,
|
||||
type PlannedAccommodation, type PlannedReservation,
|
||||
@@ -44,9 +45,7 @@ export function matchesCategoryFilter(place: Place, categoryFilters: Set<string>
|
||||
}
|
||||
|
||||
export function matchesSearch(place: Place, search: string): boolean {
|
||||
if (!search) return true
|
||||
const q = search.toLowerCase()
|
||||
return place.name.toLowerCase().includes(q) || (place.address || '').toLowerCase().includes(q)
|
||||
return placeMatchesSearch(place, search)
|
||||
}
|
||||
|
||||
interface PoolFilterArgs {
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
// FE-UTIL-PLACESEARCH-001 to FE-UTIL-PLACESEARCH-004
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { placeMatchesSearch } from './placeSearch'
|
||||
|
||||
const place = {
|
||||
name: 'Café Central',
|
||||
address: 'Herrengasse 14, Wien',
|
||||
description: 'Classic Viennese coffee house',
|
||||
notes: 'Book a table ahead',
|
||||
}
|
||||
|
||||
describe('placeMatchesSearch', () => {
|
||||
it('FE-UTIL-PLACESEARCH-001: an empty or blank query matches every place', () => {
|
||||
expect(placeMatchesSearch(place, '')).toBe(true)
|
||||
expect(placeMatchesSearch(place, ' ')).toBe(true)
|
||||
})
|
||||
|
||||
it('FE-UTIL-PLACESEARCH-002: finds the name and the address, ignoring case', () => {
|
||||
expect(placeMatchesSearch(place, 'central')).toBe(true)
|
||||
expect(placeMatchesSearch(place, 'HERRENGASSE')).toBe(true)
|
||||
})
|
||||
|
||||
it('FE-UTIL-PLACESEARCH-003: also finds the description and the notes (#2252)', () => {
|
||||
expect(placeMatchesSearch(place, 'viennese')).toBe(true)
|
||||
expect(placeMatchesSearch(place, 'table ahead')).toBe(true)
|
||||
})
|
||||
|
||||
it('FE-UTIL-PLACESEARCH-004: missing fields are skipped, and a miss is a miss', () => {
|
||||
const bare = { name: 'Stop', address: null, description: undefined, notes: null }
|
||||
expect(placeMatchesSearch(bare, 'stop ')).toBe(true)
|
||||
expect(placeMatchesSearch(bare, 'nowhere')).toBe(false)
|
||||
expect(placeMatchesSearch(place, 'pizza')).toBe(false)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,18 @@
|
||||
import type { Place } from '../types'
|
||||
|
||||
type Searchable = Pick<Place, 'name' | 'address' | 'description' | 'notes'>
|
||||
|
||||
/**
|
||||
* Whether a place matches the search box of the places list (#2252). Name and
|
||||
* address as before, and also the description and the notes, so a place found
|
||||
* by what was written about it ("vegan", "book ahead") turns up too. One helper
|
||||
* for the desktop list, its filter counts and the phone's browser, so the three
|
||||
* can never disagree about what a search finds.
|
||||
*/
|
||||
export function placeMatchesSearch(place: Searchable, query: string): boolean {
|
||||
const q = query.trim().toLowerCase()
|
||||
if (!q) return true
|
||||
return [place.name, place.address, place.description, place.notes].some(
|
||||
field => !!field && field.toLowerCase().includes(q),
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user