refactor([2fd88f42]): consolidate tooltip manager into filter panel and fix app structure
This commit is contained in:
@@ -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' }}>
|
||||
|
||||
@@ -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 (
|
||||
<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 = () => {
|
||||
onFilterChange(selectedFilters);
|
||||
onFilterChange(selectedFilters);
|
||||
};
|
||||
|
||||
const handleResetClick = () => {
|
||||
@@ -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)}
|
||||
/>
|
||||
<label htmlFor={`${category}-${option}`}>{option}</label>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</details>
|
||||
))}
|
||||
<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}
|
||||
/>
|
||||
))}
|
||||
</SortableContext>
|
||||
</DndContext>
|
||||
<div className="filter-buttons">
|
||||
<button onClick={handleApplyClick} disabled={isLoading} className="apply-button">
|
||||
{isLoading ? 'Loading...' : 'Apply Filters'}
|
||||
|
||||
@@ -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[];
|
||||
|
||||
Reference in New Issue
Block a user