Update UI
This commit is contained in:
parent
2ac37ece97
commit
5f311233e4
10 changed files with 663 additions and 408 deletions
|
|
@ -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>
|
||||
)}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue