From a14ae0aa276282b0628580e289190ab4a51ba5f5 Mon Sep 17 00:00:00 2001 From: Floke Date: Wed, 4 Feb 2026 14:41:02 +0000 Subject: [PATCH] refactor([2fd88f42]): consolidate tooltip manager into filter panel and fix app structure --- heatmap-tool/frontend/src/App.tsx | 37 ++-- .../frontend/src/components/FilterPanel.tsx | 164 +++++++++++++----- .../frontend/src/components/MapDisplay.tsx | 4 +- 3 files changed, 145 insertions(+), 60 deletions(-) diff --git a/heatmap-tool/frontend/src/App.tsx b/heatmap-tool/frontend/src/App.tsx index b1e67203..59b8c1e2 100644 --- a/heatmap-tool/frontend/src/App.tsx +++ b/heatmap-tool/frontend/src/App.tsx @@ -8,13 +8,18 @@ import FilterPanel from './components/FilterPanel'; import MapDisplay from './components/MapDisplay'; import ErrorBoundary from './components/ErrorBoundary'; import PlzSelector from './components/PlzSelector'; -import TooltipManager, { type TooltipColumn } from './components/TooltipManager'; // Define types for our state export interface FilterOptions { [key: string]: string[]; } +export interface TooltipColumn { + id: string; + name: string; + visible: boolean; +} + export interface HeatmapPoint { plz: string; lat: number; @@ -70,10 +75,12 @@ function App() { } catch (error: any) { if (axios.isAxiosError(error) && error.response) { setError(`Failed to set PLZ column: ${error.response.data.detail || error.message}`); - } else { + } + else { setError(`Failed to set PLZ column: ${error.message}`); } - } finally { + } + finally { setIsLoading(false); } }; @@ -90,24 +97,21 @@ function App() { } catch (error: any) { if (axios.isAxiosError(error) && error.response) { setError(`Failed to fetch heatmap data: ${error.response.data.detail || error.message}`); - } else { + } + else { setError(`Failed to fetch heatmap data: ${error.message}`); } setHeatmapData([]); // Clear data on error - } finally { + } + finally { setIsLoading(false); } }; - // Need to re-fetch data when tooltip config changes - useEffect(() => { - if (Object.keys(filters).length > 0) { - // This assumes you want to re-fetch with the *last applied* filters, which is complex to track. - // For simplicity, we won't re-fetch automatically on tooltip change for now, - // but the config will be applied on the *next* "Apply Filters" click. - } - }, [tooltipColumns]); - + // Need to re-fetch data when tooltip config changes. + // Optimization: In a real app, we might debounce this or add an "Apply" button for sorting. + // For now, let's keep it manual via the "Apply Filters" button in the FilterPanel to avoid excessive API calls while dragging. + // The FilterPanel calls onFilterChange when "Apply" is clicked, passing both filters and the current tooltipColumns. return ( @@ -132,12 +136,11 @@ function App() { <> handleFilterChange(selectedFilters, tooltipColumns)} isLoading={isLoading} /> - {tooltipColumns.length > 0 && ( - - )}

Map Settings

diff --git a/heatmap-tool/frontend/src/components/FilterPanel.tsx b/heatmap-tool/frontend/src/components/FilterPanel.tsx index 56b489ef..146aa60f 100644 --- a/heatmap-tool/frontend/src/components/FilterPanel.tsx +++ b/heatmap-tool/frontend/src/components/FilterPanel.tsx @@ -1,36 +1,124 @@ // src/components/FilterPanel.tsx import React, { useState, useEffect } from 'react'; +import { + DndContext, + closestCenter, + PointerSensor, + useSensor, + useSensors, +} from '@dnd-kit/core'; +import { + arrayMove, + SortableContext, + verticalListSortingStrategy, + useSortable, +} from '@dnd-kit/sortable'; +import { CSS } from '@dnd-kit/utilities'; +import type { TooltipColumn } from '../App'; // Use type from App + +// Represents a single sortable filter category +const SortableFilterItem: React.FC<{ + item: TooltipColumn; + options: string[]; + selectedValues: string[]; + onSelectionChange: (category: string, values: string[]) => void; + onVisibilityChange: (id: string) => void; +}> = ({ item, options, selectedValues, onSelectionChange, onVisibilityChange }) => { + const { + attributes, + listeners, + setNodeRef, + transform, + transition, + } = useSortable({ id: item.id }); + + const style = { + transform: CSS.Transform.toString(transform), + transition, + marginBottom: '15px', + }; + + return ( +
+
+ + onVisibilityChange(item.id)} + onClick={(e) => e.stopPropagation()} // Prevent details from closing when clicking checkbox + style={{ marginRight: '10px' }} + /> + + {item.name} + + +
+ {options.map(option => ( +
+ { + const newSelection = selectedValues.includes(option) + ? selectedValues.filter(v => v !== option) + : [...selectedValues, option]; + onSelectionChange(item.name, newSelection); + }} + /> + +
+ ))} +
+
+
+ ); +}; -interface FilterOptions { - [key: string]: string[]; -} interface FilterPanelProps { - filters: FilterOptions; - onFilterChange: (selectedFilters: FilterOptions) => void; + filters: Record; + tooltipColumns: TooltipColumn[]; + setTooltipColumns: (columns: TooltipColumn[]) => void; + onFilterChange: (selectedFilters: Record) => void; isLoading: boolean; } -const FilterPanel: React.FC = ({ filters, onFilterChange, isLoading }) => { - const [selectedFilters, setSelectedFilters] = useState({}); +const FilterPanel: React.FC = ({ filters, tooltipColumns, setTooltipColumns, onFilterChange, isLoading }) => { + const [selectedFilters, setSelectedFilters] = useState>({}); useEffect(() => { setSelectedFilters({}); }, [filters]); - const handleCheckboxChange = (category: string, value: string) => { - setSelectedFilters(prev => { - const currentSelection = prev[category] || []; - const newSelection = currentSelection.includes(value) - ? currentSelection.filter(item => item !== value) - // Ensure "N/A" is not sorted to the top if that's not desired - : [...currentSelection, value].sort((a, b) => a === 'N/A' ? 1 : b === 'N/A' ? -1 : a.localeCompare(b)); - return { ...prev, [category]: newSelection }; - }); + const sensors = useSensors(useSensor(PointerSensor)); + + const handleDragEnd = (event: any) => { + const { active, over } = event; + if (active.id !== over.id) { + const oldIndex = tooltipColumns.findIndex(c => c.id === active.id); + const newIndex = tooltipColumns.findIndex(c => c.id === over.id); + setTooltipColumns(arrayMove(tooltipColumns, oldIndex, newIndex)); + } + }; + + const handleVisibilityChange = (id: string) => { + setTooltipColumns( + tooltipColumns.map(c => c.id === id ? { ...c, visible: !c.visible } : c) + ); + }; + + const handleSelectionChange = (category: string, values: string[]) => { + setSelectedFilters(prev => ({ + ...prev, + [category]: values, + })); }; const handleApplyClick = () => { - onFilterChange(selectedFilters); + onFilterChange(selectedFilters); }; const handleResetClick = () => { @@ -44,10 +132,10 @@ const FilterPanel: React.FC = ({ filters, onFilterChange, isLo return ( <> + {/* Styles can be moved to a CSS file, but are here for simplicity */}
-

Filters

- {Object.entries(filters).map(([category, options]) => ( -
- {category} -
- {options.map(option => ( -
- handleCheckboxChange(category, option)} - /> - -
- ))} -
-
- ))} +

Filters & Tooltip Settings

+

Drag category to reorder tooltip. Uncheck to hide from tooltip.

+ + + {tooltipColumns.map(col => ( + + ))} + +