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 MapDisplay from './components/MapDisplay';
import ErrorBoundary from './components/ErrorBoundary'; import ErrorBoundary from './components/ErrorBoundary';
import PlzSelector from './components/PlzSelector'; import PlzSelector from './components/PlzSelector';
import TooltipManager, { type TooltipColumn } from './components/TooltipManager';
// Define types for our state // Define types for our state
export interface FilterOptions { export interface FilterOptions {
[key: string]: string[]; [key: string]: string[];
} }
export interface TooltipColumn {
id: string;
name: string;
visible: boolean;
}
export interface HeatmapPoint { export interface HeatmapPoint {
plz: string; plz: string;
lat: number; lat: number;
@@ -70,10 +75,12 @@ function App() {
} catch (error: any) { } catch (error: any) {
if (axios.isAxiosError(error) && error.response) { if (axios.isAxiosError(error) && error.response) {
setError(`Failed to set PLZ column: ${error.response.data.detail || error.message}`); setError(`Failed to set PLZ column: ${error.response.data.detail || error.message}`);
} else { }
else {
setError(`Failed to set PLZ column: ${error.message}`); setError(`Failed to set PLZ column: ${error.message}`);
} }
} finally { }
finally {
setIsLoading(false); setIsLoading(false);
} }
}; };
@@ -90,24 +97,21 @@ function App() {
} catch (error: any) { } catch (error: any) {
if (axios.isAxiosError(error) && error.response) { if (axios.isAxiosError(error) && error.response) {
setError(`Failed to fetch heatmap data: ${error.response.data.detail || error.message}`); setError(`Failed to fetch heatmap data: ${error.response.data.detail || error.message}`);
} else { }
else {
setError(`Failed to fetch heatmap data: ${error.message}`); setError(`Failed to fetch heatmap data: ${error.message}`);
} }
setHeatmapData([]); // Clear data on error setHeatmapData([]); // Clear data on error
} finally { }
finally {
setIsLoading(false); setIsLoading(false);
} }
}; };
// Need to re-fetch data when tooltip config changes // Need to re-fetch data when tooltip config changes.
useEffect(() => { // Optimization: In a real app, we might debounce this or add an "Apply" button for sorting.
if (Object.keys(filters).length > 0) { // For now, let's keep it manual via the "Apply Filters" button in the FilterPanel to avoid excessive API calls while dragging.
// This assumes you want to re-fetch with the *last applied* filters, which is complex to track. // The FilterPanel calls onFilterChange when "Apply" is clicked, passing both filters and the current tooltipColumns.
// 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]);
return ( return (
<ErrorBoundary> <ErrorBoundary>
@@ -132,12 +136,11 @@ function App() {
<> <>
<FilterPanel <FilterPanel
filters={filters} filters={filters}
tooltipColumns={tooltipColumns}
setTooltipColumns={setTooltipColumns}
onFilterChange={(selectedFilters) => handleFilterChange(selectedFilters, tooltipColumns)} onFilterChange={(selectedFilters) => handleFilterChange(selectedFilters, tooltipColumns)}
isLoading={isLoading} isLoading={isLoading}
/> />
{tooltipColumns.length > 0 && (
<TooltipManager columns={tooltipColumns} setColumns={setTooltipColumns} />
)}
<div className="map-controls" style={{ marginTop: '20px', paddingTop: '20px', borderTop: '1px solid #555' }}> <div className="map-controls" style={{ marginTop: '20px', paddingTop: '20px', borderTop: '1px solid #555' }}>
<h3>Map Settings</h3> <h3>Map Settings</h3>
<div className="toggle-switch" style={{ marginBottom: '15px' }}> <div className="toggle-switch" style={{ marginBottom: '15px' }}>

View File

@@ -1,36 +1,124 @@
// src/components/FilterPanel.tsx // src/components/FilterPanel.tsx
import React, { useState, useEffect } from 'react'; 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 { interface FilterPanelProps {
filters: FilterOptions; filters: Record<string, string[]>;
onFilterChange: (selectedFilters: FilterOptions) => void; tooltipColumns: TooltipColumn[];
setTooltipColumns: (columns: TooltipColumn[]) => void;
onFilterChange: (selectedFilters: Record<string, string[]>) => void;
isLoading: boolean; isLoading: boolean;
} }
const FilterPanel: React.FC<FilterPanelProps> = ({ filters, onFilterChange, isLoading }) => { const FilterPanel: React.FC<FilterPanelProps> = ({ filters, tooltipColumns, setTooltipColumns, onFilterChange, isLoading }) => {
const [selectedFilters, setSelectedFilters] = useState<FilterOptions>({}); const [selectedFilters, setSelectedFilters] = useState<Record<string, string[]>>({});
useEffect(() => { useEffect(() => {
setSelectedFilters({}); setSelectedFilters({});
}, [filters]); }, [filters]);
const handleCheckboxChange = (category: string, value: string) => { const sensors = useSensors(useSensor(PointerSensor));
setSelectedFilters(prev => {
const currentSelection = prev[category] || []; const handleDragEnd = (event: any) => {
const newSelection = currentSelection.includes(value) const { active, over } = event;
? currentSelection.filter(item => item !== value) if (active.id !== over.id) {
// Ensure "N/A" is not sorted to the top if that's not desired const oldIndex = tooltipColumns.findIndex(c => c.id === active.id);
: [...currentSelection, value].sort((a, b) => a === 'N/A' ? 1 : b === 'N/A' ? -1 : a.localeCompare(b)); const newIndex = tooltipColumns.findIndex(c => c.id === over.id);
return { ...prev, [category]: newSelection }; 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 = () => { const handleApplyClick = () => {
onFilterChange(selectedFilters); onFilterChange(selectedFilters);
}; };
const handleResetClick = () => { const handleResetClick = () => {
@@ -44,10 +132,10 @@ const FilterPanel: React.FC<FilterPanelProps> = ({ filters, onFilterChange, isLo
return ( return (
<> <>
{/* Styles can be moved to a CSS file, but are here for simplicity */}
<style>{` <style>{`
.filter-panel { padding-top: 20px; } .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; display: flex; align-items: center; }
.filter-group summary { font-weight: bold; cursor: pointer; padding: 5px; background-color: #444; border-radius: 4px; }
.filter-options { max-height: 200px; overflow-y: auto; padding: 10px; background-color: #2a2a2a; border-radius: 4px; margin-top: 5px;} .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 { display: flex; align-items: center; margin-bottom: 5px; }
.filter-option input { margin-right: 10px; } .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; } .filter-buttons button:disabled { background-color: #555; cursor: not-allowed; }
`}</style> `}</style>
<div className="filter-panel"> <div className="filter-panel">
<h3>Filters</h3> <h3>Filters & Tooltip Settings</h3>
{Object.entries(filters).map(([category, options]) => ( <p style={{fontSize: '0.8em', color: '#aaa'}}>Drag category to reorder tooltip. Uncheck to hide from tooltip.</p>
<details key={category} className="filter-group"> <DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={handleDragEnd}>
<summary>{category}</summary> <SortableContext items={tooltipColumns} strategy={verticalListSortingStrategy}>
<div className="filter-options"> {tooltipColumns.map(col => (
{options.map(option => ( <SortableFilterItem
<div key={option} className="filter-option"> key={col.id}
<input item={col}
type="checkbox" options={filters[col.name] || []}
id={`${category}-${option}`} selectedValues={selectedFilters[col.name] || []}
value={option} onSelectionChange={handleSelectionChange}
checked={(selectedFilters[category] || []).includes(option)} onVisibilityChange={handleVisibilityChange}
onChange={() => handleCheckboxChange(category, option)} />
/> ))}
<label htmlFor={`${category}-${option}`}>{option}</label> </SortableContext>
</div> </DndContext>
))}
</div>
</details>
))}
<div className="filter-buttons"> <div className="filter-buttons">
<button onClick={handleApplyClick} disabled={isLoading} className="apply-button"> <button onClick={handleApplyClick} disabled={isLoading} className="apply-button">
{isLoading ? 'Loading...' : 'Apply Filters'} {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 { HeatmapLayer } from 'react-leaflet-heatmap-layer-v3';
import 'leaflet/dist/leaflet.css'; import 'leaflet/dist/leaflet.css';
import 'leaflet.heat'; import 'leaflet.heat';
import type { HeatmapPoint, MapMode } from '../App'; import type { HeatmapPoint, MapMode, TooltipColumn } from '../App';
import MarkerClusterGroup from 'react-leaflet-cluster';
import type { TooltipColumn } from './TooltipManager';
interface MapDisplayProps { interface MapDisplayProps {
heatmapData: HeatmapPoint[]; heatmapData: HeatmapPoint[];