import { useState, useRef, useEffect, useCallback } from 'react'; import type { POICategoryGroup } from '../types'; 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); const infoPopupRef = useRef(null); // Close dropdown when clicking outside useEffect(() => { function handleClickOutside(event: MouseEvent) { if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) { setDropdownOpen(false); } } document.addEventListener('mousedown', handleClickOutside); return () => document.removeEventListener('mousedown', handleClickOutside); }, []); // Close info popup when clicking outside useEffect(() => { if (!showInfo) return; function handleClickOutside(e: MouseEvent) { if (infoPopupRef.current && !infoPopupRef.current.contains(e.target as Node)) { setShowInfo(false); } } document.addEventListener('mousedown', handleClickOutside); return () => document.removeEventListener('mousedown', handleClickOutside); }, [showInfo]); 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(); // Filter groups and categories by search term 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

{showInfo && (

Points of Interest

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.

{onNavigateToSource && ( )}
)}
{dropdownOpen && (
|
setSearchTerm(e.target.value)} className="w-full px-2 py-1 text-sm border border-warm-300 dark:border-navy-700 rounded bg-white dark:bg-navy-950 dark:text-warm-200 dark:placeholder-warm-500" />
{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.

); }