Update UI

This commit is contained in:
Andras Schmelczer 2026-02-01 11:07:58 +00:00
parent 2ac37ece97
commit 5f311233e4
10 changed files with 663 additions and 408 deletions

View file

@ -1,21 +1,22 @@
import { useState, useRef, useEffect } from 'react';
import { Label } from './ui/label';
import { useState, useRef, useEffect, useCallback } from 'react';
import type { POICategoryGroup } from '../types';
interface POIPaneProps {
categories: string[];
groups: POICategoryGroup[];
selectedCategories: Set<string>;
onCategoriesChange: (categories: Set<string>) => void;
poiCount: number;
}
export default function POIPane({
categories,
groups,
selectedCategories,
onCategoriesChange,
poiCount,
}: POIPaneProps) {
const [dropdownOpen, setDropdownOpen] = useState(false);
const [searchTerm, setSearchTerm] = useState('');
const [collapsedGroups, setCollapsedGroups] = useState<Set<string>>(new Set());
const dropdownRef = useRef<HTMLDivElement>(null);
// Close dropdown when clicking outside
@ -29,6 +30,8 @@ export default function POIPane({
return () => document.removeEventListener('mousedown', handleClickOutside);
}, []);
const allCategories = groups.flatMap((g) => g.categories);
const toggleCategory = (category: string) => {
const newSet = new Set(selectedCategories);
if (newSet.has(category)) {
@ -40,17 +43,55 @@ export default function POIPane({
};
const selectAll = () => {
onCategoriesChange(new Set(categories));
onCategoriesChange(new Set(allCategories));
};
const selectNone = () => {
onCategoriesChange(new Set());
};
const filteredCategories = categories.filter((cat) =>
cat.toLowerCase().includes(searchTerm.toLowerCase())
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 (
@ -58,7 +99,6 @@ export default function POIPane({
<h2 className="text-xl font-bold">Points of Interest</h2>
<div className="space-y-2" ref={dropdownRef}>
<Label>Categories</Label>
<button
onClick={() => setDropdownOpen(!dropdownOpen)}
className="w-full flex items-center justify-between px-3 py-2 text-sm border border-warm-300 rounded hover:border-warm-400 bg-white"
@ -66,7 +106,7 @@ export default function POIPane({
<span className="truncate text-left">
{selectedCount === 0
? 'Select categories...'
: selectedCount === categories.length
: selectedCount === allCategories.length
? 'All categories'
: `${selectedCount} selected`}
</span>
@ -101,20 +141,69 @@ export default function POIPane({
/>
</div>
<div className="max-h-96 overflow-y-auto py-1">
{filteredCategories.map((category) => (
<label
key={category}
className="flex items-center gap-2 px-3 py-1.5 hover:bg-warm-50 cursor-pointer"
>
<input
type="checkbox"
checked={selectedCategories.has(category)}
onChange={() => toggleCategory(category)}
className="rounded"
/>
<span className="text-sm flex-1">{category}</span>
</label>
))}
{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 (
<div key={group.name}>
<div className="flex items-center gap-1 px-3 py-1.5 bg-warm-50 border-y border-warm-100">
<button
onClick={() => toggleCollapse(group.name)}
className="p-0.5 text-warm-400 hover:text-warm-600"
>
<svg
className={`w-3 h-3 transition-transform ${isCollapsed ? '' : 'rotate-90'}`}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M9 5l7 7-7 7"
/>
</svg>
</button>
<label className="flex items-center gap-2 flex-1 cursor-pointer">
<input
type="checkbox"
checked={allInGroupSelected}
ref={(el) => {
if (el) el.indeterminate = someInGroupSelected;
}}
onChange={() => toggleGroup(group.name)}
className="rounded"
/>
<span className="text-xs font-semibold text-warm-700">{group.name}</span>
</label>
<span className="text-xs text-warm-400">
{groupSelected}/{group.categories.length}
</span>
</div>
{!isCollapsed &&
group.categories.map((category) => (
<label
key={category}
className="flex items-center gap-2 px-3 pl-8 py-1.5 hover:bg-warm-50 cursor-pointer"
>
<input
type="checkbox"
checked={selectedCategories.has(category)}
onChange={() => toggleCategory(category)}
className="rounded"
/>
<span className="text-sm flex-1">{category}</span>
</label>
))}
</div>
);
})}
</div>
</div>
)}