import React, { useEffect, useMemo, useRef, useState } from 'react' import { Search, Bookmark, Loader2, ChevronDown, Check, Layers } from 'lucide-react' import PlaceAvatar from '../shared/PlaceAvatar' import { collectionsApi } from '../../api/collections' import { STATUS_META, STATUS_ORDER } from '../../pages/collections/collectionsModel' import type { CollectionPlace, CollectionStatus } from '@trek/shared' import type { TranslationFn } from '../../types' interface LocationBias { low: { lat: number; lng: number } high: { lat: number; lng: number } } interface ListMeta { id: number; name: string; color: string | null } interface CollectionPickerProps { /** Trip bounding box used for autocomplete — sorts the saved places by * proximity to the trip so the relevant ones surface first. */ bias?: LocationBias /** Fills the place form from the chosen saved place (handleSelectMapsResult). */ onSelect: (place: CollectionPlace) => void t: TranslationFn } function distanceTo(p: CollectionPlace, center: { lat: number; lng: number }): number { if (p.lat == null || p.lng == null) return Number.POSITIVE_INFINITY const dlat = p.lat - center.lat const dlng = p.lng - center.lng return dlat * dlat + dlng * dlng } interface Opt { key: string | number; label: string; icon?: React.ReactNode; count?: number } /** Compact click-away dropdown (Tailwind — this panel lives outside .trek-dash). */ function FilterDropdown({ current, options, onSelect, lead }: { current: string | number options: Opt[] onSelect: (key: string | number) => void lead: React.ReactNode }): React.ReactElement { const [open, setOpen] = useState(false) const ref = useRef(null) useEffect(() => { if (!open) return const onDoc = (e: MouseEvent) => { if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false) } const onKey = (e: KeyboardEvent) => { if (e.key === 'Escape') setOpen(false) } document.addEventListener('mousedown', onDoc) document.addEventListener('keydown', onKey) return () => { document.removeEventListener('mousedown', onDoc); document.removeEventListener('keydown', onKey) } }, [open]) const cur = options.find(o => o.key === current) ?? options[0] return (
{open && (
{options.map(o => ( ))}
)}
) } /** * Right-hand column of the desktop add-place modal: the user's saved collection * places, searchable, filterable by list + status, and proximity-sorted, so a * place saved on an earlier trip can be dropped straight into the form. * Desktop only — gated by the caller. */ export default function CollectionPicker({ bias, onSelect, t }: CollectionPickerProps): React.ReactElement { const [places, setPlaces] = useState([]) const [lists, setLists] = useState([]) const [loading, setLoading] = useState(true) const [search, setSearch] = useState('') const [listFilter, setListFilter] = useState('all') const [statusFilter, setStatusFilter] = useState('all') useEffect(() => { let cancelled = false setLoading(true) collectionsApi.list() .then(async (res) => { const detail = await Promise.all(res.collections.map(c => collectionsApi.get(c.id).catch(() => null))) if (cancelled) return const merged: CollectionPlace[] = [] for (const d of detail) { if (!d) continue for (const p of d.places) merged.push(p) } setLists(res.collections.map(c => ({ id: c.id, name: c.name, color: c.color ?? null }))) setPlaces(merged) }) .catch(() => { if (!cancelled) { setPlaces([]); setLists([]) } }) .finally(() => { if (!cancelled) setLoading(false) }) return () => { cancelled = true } }, []) const center = useMemo( () => (bias ? { lat: (bias.low.lat + bias.high.lat) / 2, lng: (bias.low.lng + bias.high.lng) / 2 } : null), [bias], ) const visible = useMemo(() => { const q = search.trim().toLowerCase() const list = places.filter(p => { if (listFilter !== 'all' && p.collection_id !== listFilter) return false if (statusFilter !== 'all' && p.status !== statusFilter) return false if (!q) return true return p.name.toLowerCase().includes(q) || (p.address ?? '').toLowerCase().includes(q) }) if (center) list.sort((a, b) => distanceTo(a, center) - distanceTo(b, center)) else list.sort((a, b) => a.name.localeCompare(b.name)) return list }, [places, search, center, listFilter, statusFilter]) const listOpts: Opt[] = [ { key: 'all', label: t('collections.picker.allLists'), icon: , count: places.length }, ...lists.map(l => ({ key: l.id, label: l.name, icon: , count: places.filter(p => p.collection_id === l.id).length, })), ] const statusOpts: Opt[] = [ { key: 'all', label: t('common.all') }, ...STATUS_ORDER.map(s => { const Icon = STATUS_META[s].icon return { key: s, label: t(STATUS_META[s].labelKey), icon: } }), ] return ( ) }