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 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' }}>
|
||||||
|
|||||||
@@ -1,32 +1,120 @@
|
|||||||
// 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 = () => {
|
||||||
@@ -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>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</details>
|
|
||||||
))}
|
))}
|
||||||
|
</SortableContext>
|
||||||
|
</DndContext>
|
||||||
<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'}
|
||||||
|
|||||||
@@ -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[];
|
||||||
|
|||||||
Reference in New Issue
Block a user