refactor([2fd88f42]): consolidate tooltip manager into filter panel and fix app structure

This commit is contained in:
2026-02-04 14:41:02 +00:00
parent 7214f7a687
commit a14ae0aa27
3 changed files with 145 additions and 60 deletions

View File

@@ -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 (
<ErrorBoundary>
@@ -132,12 +136,11 @@ function App() {
<>
<FilterPanel
filters={filters}
tooltipColumns={tooltipColumns}
setTooltipColumns={setTooltipColumns}
onFilterChange={(selectedFilters) => handleFilterChange(selectedFilters, tooltipColumns)}
isLoading={isLoading}
/>
{tooltipColumns.length > 0 && (
<TooltipManager columns={tooltipColumns} setColumns={setTooltipColumns} />
)}
<div className="map-controls" style={{ marginTop: '20px', paddingTop: '20px', borderTop: '1px solid #555' }}>
<h3>Map Settings</h3>
<div className="toggle-switch" style={{ marginBottom: '15px' }}>

View File

@@ -1,32 +1,120 @@
// 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 (
<div ref={setNodeRef} style={style}>
<details className="filter-group">
<summary>
<input
type="checkbox"
checked={item.visible}
onChange={() => onVisibilityChange(item.id)}
onClick={(e) => e.stopPropagation()} // Prevent details from closing when clicking checkbox
style={{ marginRight: '10px' }}
/>
<span {...attributes} {...listeners} style={{ cursor: 'grab' }}>
{item.name}
</span>
</summary>
<div className="filter-options">
{options.map(option => (
<div key={option} className="filter-option">
<input
type="checkbox"
id={`${item.id}-${option}`}
value={option}
checked={selectedValues.includes(option)}
onChange={() => {
const newSelection = selectedValues.includes(option)
? selectedValues.filter(v => v !== option)
: [...selectedValues, option];
onSelectionChange(item.name, newSelection);
}}
/>
<label htmlFor={`${item.id}-${option}`}>{option}</label>
</div>
))}
</div>
</details>
</div>
);
};
interface FilterOptions {
[key: string]: string[];
}
interface FilterPanelProps {
filters: FilterOptions;
onFilterChange: (selectedFilters: FilterOptions) => void;
filters: Record<string, string[]>;
tooltipColumns: TooltipColumn[];
setTooltipColumns: (columns: TooltipColumn[]) => void;
onFilterChange: (selectedFilters: Record<string, string[]>) => void;
isLoading: boolean;
}
const FilterPanel: React.FC<FilterPanelProps> = ({ filters, onFilterChange, isLoading }) => {
const [selectedFilters, setSelectedFilters] = useState<FilterOptions>({});
const FilterPanel: React.FC<FilterPanelProps> = ({ filters, tooltipColumns, setTooltipColumns, onFilterChange, isLoading }) => {
const [selectedFilters, setSelectedFilters] = useState<Record<string, string[]>>({});
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 = () => {
@@ -44,10 +132,10 @@ const FilterPanel: React.FC<FilterPanelProps> = ({ filters, onFilterChange, isLo
return (
<>
{/* Styles can be moved to a CSS file, but are here for simplicity */}
<style>{`
.filter-panel { padding-top: 20px; }
.filter-group { margin-bottom: 15px; }
.filter-group summary { font-weight: bold; cursor: pointer; padding: 5px; background-color: #444; border-radius: 4px; }
.filter-group summary { font-weight: bold; cursor: pointer; padding: 5px; background-color: #444; border-radius: 4px; display: flex; align-items: center; }
.filter-options { max-height: 200px; overflow-y: auto; padding: 10px; background-color: #2a2a2a; border-radius: 4px; margin-top: 5px;}
.filter-option { display: flex; align-items: center; margin-bottom: 5px; }
.filter-option input { margin-right: 10px; }
@@ -58,26 +146,22 @@ const FilterPanel: React.FC<FilterPanelProps> = ({ filters, onFilterChange, isLo
.filter-buttons button:disabled { background-color: #555; cursor: not-allowed; }
`}</style>
<div className="filter-panel">
<h3>Filters</h3>
{Object.entries(filters).map(([category, options]) => (
<details key={category} className="filter-group">
<summary>{category}</summary>
<div className="filter-options">
{options.map(option => (
<div key={option} className="filter-option">
<input
type="checkbox"
id={`${category}-${option}`}
value={option}
checked={(selectedFilters[category] || []).includes(option)}
onChange={() => handleCheckboxChange(category, option)}
<h3>Filters & Tooltip Settings</h3>
<p style={{fontSize: '0.8em', color: '#aaa'}}>Drag category to reorder tooltip. Uncheck to hide from tooltip.</p>
<DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={handleDragEnd}>
<SortableContext items={tooltipColumns} strategy={verticalListSortingStrategy}>
{tooltipColumns.map(col => (
<SortableFilterItem
key={col.id}
item={col}
options={filters[col.name] || []}
selectedValues={selectedFilters[col.name] || []}
onSelectionChange={handleSelectionChange}
onVisibilityChange={handleVisibilityChange}
/>
<label htmlFor={`${category}-${option}`}>{option}</label>
</div>
))}
</div>
</details>
))}
</SortableContext>
</DndContext>
<div className="filter-buttons">
<button onClick={handleApplyClick} disabled={isLoading} className="apply-button">
{isLoading ? 'Loading...' : 'Apply Filters'}

View File

@@ -4,9 +4,7 @@ import { MapContainer, TileLayer, CircleMarker, Tooltip } from 'react-leaflet';
import { HeatmapLayer } from 'react-leaflet-heatmap-layer-v3';
import 'leaflet/dist/leaflet.css';
import 'leaflet.heat';
import type { HeatmapPoint, MapMode } from '../App';
import MarkerClusterGroup from 'react-leaflet-cluster';
import type { TooltipColumn } from './TooltipManager';
import type { HeatmapPoint, MapMode, TooltipColumn } from '../App';
interface MapDisplayProps {
heatmapData: HeatmapPoint[];