Checkpoint all changes
This commit is contained in:
parent
65877acf95
commit
66c2a25457
28 changed files with 3035 additions and 621 deletions
|
|
@ -6,6 +6,7 @@ interface POIPaneProps {
|
|||
selectedCategories: Set<string>;
|
||||
onCategoriesChange: (categories: Set<string>) => void;
|
||||
poiCount: number;
|
||||
onNavigateToSource?: (slug: string) => void;
|
||||
}
|
||||
|
||||
export default function POIPane({
|
||||
|
|
@ -13,11 +14,14 @@ export default function POIPane({
|
|||
selectedCategories,
|
||||
onCategoriesChange,
|
||||
poiCount,
|
||||
onNavigateToSource,
|
||||
}: POIPaneProps) {
|
||||
const [dropdownOpen, setDropdownOpen] = useState(false);
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [collapsedGroups, setCollapsedGroups] = useState<Set<string>>(new Set());
|
||||
const [showInfo, setShowInfo] = useState(false);
|
||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||
const infoPopupRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Close dropdown when clicking outside
|
||||
useEffect(() => {
|
||||
|
|
@ -30,6 +34,18 @@ export default function POIPane({
|
|||
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) => {
|
||||
|
|
@ -95,13 +111,65 @@ export default function POIPane({
|
|||
const selectedCount = selectedCategories.size;
|
||||
|
||||
return (
|
||||
<div className="w-72 p-4 bg-white dark:bg-warm-900 shadow-lg space-y-4 overflow-y-auto max-h-screen">
|
||||
<h2 className="text-xl font-bold dark:text-warm-100">Points of Interest</h2>
|
||||
<div className="w-72 p-4 bg-white dark:bg-navy-950 shadow-lg space-y-4 overflow-y-auto max-h-screen">
|
||||
<div className="flex items-center gap-2">
|
||||
<h2 className="text-xl font-bold dark:text-warm-100">Points of Interest</h2>
|
||||
<button
|
||||
onClick={() => setShowInfo(true)}
|
||||
className="text-warm-400 hover:text-warm-700 dark:hover:text-warm-300 p-0.5 rounded"
|
||||
title="Data source info"
|
||||
>
|
||||
<svg className="w-3.5 h-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2}>
|
||||
<circle cx="12" cy="12" r="10" />
|
||||
<path strokeLinecap="round" d="M12 16v-4m0-4h.01" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{showInfo && (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/30">
|
||||
<div
|
||||
ref={infoPopupRef}
|
||||
className="bg-white dark:bg-navy-800 border border-warm-200 dark:border-navy-700 rounded-lg shadow-xl max-w-md w-full mx-4 p-5"
|
||||
>
|
||||
<div className="flex items-start justify-between mb-3">
|
||||
<h3 className="text-sm font-semibold text-warm-900 dark:text-warm-100 pr-4">
|
||||
Points of Interest
|
||||
</h3>
|
||||
<button
|
||||
onClick={() => setShowInfo(false)}
|
||||
className="text-warm-400 hover:text-warm-700 dark:hover:text-warm-300 shrink-0"
|
||||
>
|
||||
<svg className="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<p className="text-sm text-warm-700 dark:text-warm-300 mb-4 leading-relaxed">
|
||||
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.
|
||||
</p>
|
||||
{onNavigateToSource && (
|
||||
<button
|
||||
onClick={() => {
|
||||
onNavigateToSource('osm-pois');
|
||||
setShowInfo(false);
|
||||
}}
|
||||
className="text-sm text-teal-600 dark:text-teal-400 hover:text-teal-800 dark:hover:text-teal-300 hover:underline"
|
||||
>
|
||||
View data source
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-2" ref={dropdownRef}>
|
||||
<button
|
||||
onClick={() => setDropdownOpen(!dropdownOpen)}
|
||||
className="w-full flex items-center justify-between px-3 py-2 text-sm border border-warm-300 dark:border-warm-700 rounded hover:border-warm-400 bg-white dark:bg-warm-800 dark:text-warm-200"
|
||||
className="w-full flex items-center justify-between px-3 py-2 text-sm border border-warm-300 dark:border-navy-700 rounded hover:border-warm-400 bg-white dark:bg-navy-800 dark:text-warm-200"
|
||||
>
|
||||
<span className="truncate text-left">
|
||||
{selectedCount === 0
|
||||
|
|
@ -121,8 +189,8 @@ export default function POIPane({
|
|||
</button>
|
||||
|
||||
{dropdownOpen && (
|
||||
<div className="border border-warm-300 dark:border-warm-700 rounded shadow-lg bg-white dark:bg-warm-800">
|
||||
<div className="flex gap-2 px-3 py-2 border-b border-warm-200 dark:border-warm-700">
|
||||
<div className="border border-warm-300 dark:border-navy-700 rounded shadow-lg bg-white dark:bg-navy-800">
|
||||
<div className="flex gap-2 px-3 py-2 border-b border-warm-200 dark:border-navy-700">
|
||||
<button onClick={selectAll} className="text-xs text-teal-600 dark:text-teal-400 hover:text-teal-800 dark:hover:text-teal-300">
|
||||
All
|
||||
</button>
|
||||
|
|
@ -131,13 +199,13 @@ export default function POIPane({
|
|||
None
|
||||
</button>
|
||||
</div>
|
||||
<div className="px-3 py-2 border-b border-warm-200 dark:border-warm-700">
|
||||
<div className="px-3 py-2 border-b border-warm-200 dark:border-navy-700">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search categories..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="w-full px-2 py-1 text-sm border border-warm-300 dark:border-warm-700 rounded bg-white dark:bg-warm-900 dark:text-warm-200 dark:placeholder-warm-500"
|
||||
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"
|
||||
/>
|
||||
</div>
|
||||
<div className="max-h-96 overflow-y-auto py-1">
|
||||
|
|
@ -151,7 +219,7 @@ export default function POIPane({
|
|||
|
||||
return (
|
||||
<div key={group.name}>
|
||||
<div className="flex items-center gap-1 px-3 py-1.5 bg-warm-50 dark:bg-warm-900 border-y border-warm-100 dark:border-warm-700">
|
||||
<div className="flex items-center gap-1 px-3 py-1.5 bg-warm-50 dark:bg-navy-950 border-y border-warm-100 dark:border-navy-700">
|
||||
<button
|
||||
onClick={() => toggleCollapse(group.name)}
|
||||
className="p-0.5 text-warm-400 hover:text-warm-600"
|
||||
|
|
@ -190,7 +258,7 @@ export default function POIPane({
|
|||
group.categories.map((category) => (
|
||||
<label
|
||||
key={category}
|
||||
className="flex items-center gap-2 px-3 pl-8 py-1.5 hover:bg-warm-50 dark:hover:bg-warm-700 cursor-pointer dark:text-warm-300"
|
||||
className="flex items-center gap-2 px-3 pl-8 py-1.5 hover:bg-warm-50 dark:hover:bg-navy-700 cursor-pointer dark:text-warm-300"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
|
|
@ -220,7 +288,7 @@ export default function POIPane({
|
|||
</div>
|
||||
)}
|
||||
|
||||
<div className="p-3 bg-warm-100 dark:bg-warm-800 rounded text-xs text-warm-600 dark:text-warm-400">
|
||||
<div className="p-3 bg-warm-100 dark:bg-navy-800 rounded text-xs text-warm-600 dark:text-warm-400">
|
||||
<p>Select categories to display POIs on the map.</p>
|
||||
<p className="mt-2">Zoom in for better visibility of individual locations.</p>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue