import { useState, useRef, useCallback } from 'react'; import type { POICategoryGroup } from '../../types'; import { useClickOutside } from '../../hooks/useClickOutside'; import InfoPopup from '../ui/InfoPopup'; import { SearchInput } from '../ui/SearchInput'; import { InfoIcon, ChevronIcon } from '../ui/icons'; import { IconButton } from '../ui/IconButton'; interface POIPaneProps { groups: POICategoryGroup[]; selectedCategories: Set; onCategoriesChange: (categories: Set) => void; poiCount: number; onNavigateToSource?: (slug: string) => void; } export default function POIPane({ groups, selectedCategories, onCategoriesChange, poiCount, onNavigateToSource, }: POIPaneProps) { const [dropdownOpen, setDropdownOpen] = useState(false); const [searchTerm, setSearchTerm] = useState(''); const [collapsedGroups, setCollapsedGroups] = useState>(new Set()); const [showInfo, setShowInfo] = useState(false); const dropdownRef = useRef(null); useClickOutside(dropdownRef, () => setDropdownOpen(false)); const allCategories = groups.flatMap((g) => g.categories); const toggleCategory = (category: string) => { const newSet = new Set(selectedCategories); if (newSet.has(category)) { newSet.delete(category); } else { newSet.add(category); } onCategoriesChange(newSet); }; const selectAll = () => { onCategoriesChange(new Set(allCategories)); }; const selectNone = () => { onCategoriesChange(new Set()); }; const toggleGroup = useCallback( (groupName: string) => { const group = groups.find((g) => g.name === groupName); if (!group) return; const allSelected = group.categories.every((c) => selectedCategories.has(c)); const newSet = new Set(selectedCategories); if (allSelected) { group.categories.forEach((c) => newSet.delete(c)); } else { group.categories.forEach((c) => newSet.add(c)); } onCategoriesChange(newSet); }, [groups, selectedCategories, onCategoriesChange] ); const toggleCollapse = (groupName: string) => { setCollapsedGroups((prev) => { const next = new Set(prev); if (next.has(groupName)) { next.delete(groupName); } else { next.add(groupName); } return next; }); }; const lowerSearch = searchTerm.toLowerCase(); const filteredGroups = groups .map((group) => { if (!searchTerm) return group; const matchingCats = group.categories.filter((c) => c.toLowerCase().includes(lowerSearch)); const groupMatches = group.name.toLowerCase().includes(lowerSearch); if (groupMatches) return group; if (matchingCats.length === 0) return null; return { ...group, categories: matchingCats }; }) .filter(Boolean) as POICategoryGroup[]; const selectedCount = selectedCategories.size; return (

Points of Interest

setShowInfo(true)} title="Data source info">
{showInfo && ( setShowInfo(false)} sourceLink={ onNavigateToSource ? { label: 'View data source', onClick: () => { onNavigateToSource('osm-pois'); setShowInfo(false); }, } : undefined } >

Points of interest are sourced from OpenStreetMap via Geofabrik extracts. Categories include public transport stops, shops, restaurants, healthcare facilities, leisure venues, and more. Data is filtered and mapped to friendly names with exhaustive category coverage.

)}
{dropdownOpen && (
{filteredGroups.map((group) => { const groupSelected = group.categories.filter((c) => selectedCategories.has(c) ).length; const allInGroupSelected = groupSelected === group.categories.length; const someInGroupSelected = groupSelected > 0 && !allInGroupSelected; const isCollapsed = collapsedGroups.has(group.name) && !searchTerm; return (
{groupSelected}/{group.categories.length}
{!isCollapsed && group.categories.map((category) => ( ))}
); })}
)}
{selectedCount > 0 && (
{poiCount.toLocaleString()} POI{poiCount !== 1 ? 's' : ''} visible
{selectedCount} categor{selectedCount !== 1 ? 'ies' : 'y'} selected
)}

Select categories to display POIs on the map.

Zoom in for better visibility of individual locations.

); }