Files
Brancheneinstufung2/heatmap-tool/frontend/src/App.tsx

122 lines
4.2 KiB
TypeScript

import { useState } from 'react';
import axios from 'axios';
import './App.css';
import 'react-leaflet-cluster/dist/assets/MarkerCluster.css';
import 'react-leaflet-cluster/dist/assets/MarkerCluster.Default.css';
import FileUpload from './components/FileUpload';
import FilterPanel from './components/FilterPanel';
import MapDisplay from './components/MapDisplay';
import ErrorBoundary from './components/ErrorBoundary';
// Define types for our state
export interface FilterOptions {
[key: string]: string[];
}
export interface HeatmapPoint {
plz: string;
lat: number;
lon: number;
count: number;
attributes_summary?: Record<string, string[]>;
}
export type MapMode = 'points' | 'heatmap';
function App() {
const [filters, setFilters] = useState<FilterOptions>({});
const [heatmapData, setHeatmapData] = useState<HeatmapPoint[]>([]);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [radiusMultiplier, setRadiusMultiplier] = useState(1);
const [viewMode, setViewMode] = useState<MapMode>('points');
const handleUploadSuccess = (newFilters: FilterOptions) => {
setFilters(newFilters);
setHeatmapData([]); // Clear previous heatmap data
setError(null);
// Automatically fetch data with no filters on successful upload
handleFilterChange({});
};
const handleFilterChange = async (selectedFilters: FilterOptions) => {
setIsLoading(true);
setError(null);
try {
const response = await axios.post('/api/heatmap', {
filters: selectedFilters,
});
setHeatmapData(response.data);
} catch (error: any) {
if (axios.isAxiosError(error) && error.response) {
setError(`Failed to fetch heatmap data: ${error.response.data.detail || error.message}`);
} else {
setError(`Failed to fetch heatmap data: ${error.message}`);
}
setHeatmapData([]); // Clear data on error
} finally {
setIsLoading(false);
}
};
return (
<ErrorBoundary>
<div className="App">
<header className="App-header">
<h1>German PLZ Heatmap Tool</h1>
</header>
<main className="App-main">
<div className="control-panel">
<FileUpload
onUploadSuccess={handleUploadSuccess}
setIsLoading={setIsLoading}
setError={setError}
/>
<FilterPanel
filters={filters}
onFilterChange={handleFilterChange}
isLoading={isLoading}
/>
<div className="map-controls" style={{ marginTop: '20px', paddingTop: '20px', borderTop: '1px solid #555' }}>
<h3>Map Settings</h3>
<div className="toggle-switch" style={{ marginBottom: '15px' }}>
<label>
<input type="radio" value="points" checked={viewMode === 'points'} onChange={() => setViewMode('points')} />
Points
</label>
<label>
<input type="radio" value="heatmap" checked={viewMode === 'heatmap'} onChange={() => setViewMode('heatmap')} />
Heatmap
</label>
</div>
<label htmlFor="radius-slider">Marker Size: {radiusMultiplier.toFixed(1)}x</label>
<input
type="range"
id="radius-slider"
min="0.1"
max="5"
step="0.1"
value={radiusMultiplier}
onChange={(e) => setRadiusMultiplier(parseFloat(e.target.value))}
style={{ width: '100%' }}
disabled={viewMode === 'heatmap'}
/>
</div>
</div>
<div className="map-container">
{isLoading && <p>Loading map data...</p>}
{error && <p className="error">{error}</p>}
<MapDisplay
heatmapData={heatmapData}
radiusMultiplier={radiusMultiplier}
viewMode={viewMode}
/>
</div>
</main>
</div>
</ErrorBoundary>
);
}
export default App;