Rewrite in TS
This commit is contained in:
parent
8c1f6a82e2
commit
bfcf26e425
19 changed files with 3229 additions and 632 deletions
93
frontend/src/App.tsx
Normal file
93
frontend/src/App.tsx
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
import { useState, useEffect, useCallback, useRef } from 'react';
|
||||
import Map from './components/Map';
|
||||
import Filters from './components/Filters';
|
||||
import { DEFAULT_FILTERS } from './lib/constants';
|
||||
import type {
|
||||
Filters as FiltersType,
|
||||
Bounds,
|
||||
HexagonData,
|
||||
ViewChangeParams,
|
||||
ApiResponse,
|
||||
} from './types';
|
||||
|
||||
const DEBOUNCE_MS = 150;
|
||||
|
||||
export default function App() {
|
||||
const [filters, setFilters] = useState<FiltersType>(DEFAULT_FILTERS);
|
||||
const [data, setData] = useState<HexagonData[]>([]);
|
||||
const [resolution, setResolution] = useState<number>(8);
|
||||
const [bounds, setBounds] = useState<Bounds | null>(null);
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
const [zoom, setZoom] = useState<number>(6);
|
||||
const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
const abortControllerRef = useRef<AbortController | null>(null);
|
||||
|
||||
// Debounced fetch when dependencies change
|
||||
useEffect(() => {
|
||||
if (!bounds) return;
|
||||
|
||||
// Clear previous debounce timer
|
||||
if (debounceRef.current) {
|
||||
clearTimeout(debounceRef.current);
|
||||
}
|
||||
|
||||
debounceRef.current = setTimeout(async () => {
|
||||
// Cancel any in-flight request
|
||||
if (abortControllerRef.current) {
|
||||
abortControllerRef.current.abort();
|
||||
}
|
||||
abortControllerRef.current = new AbortController();
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
const boundsStr = `${bounds.south},${bounds.west},${bounds.north},${bounds.east}`;
|
||||
const params = new URLSearchParams({
|
||||
resolution: resolution.toString(),
|
||||
min_year: filters.minYear.toString(),
|
||||
max_year: filters.maxYear.toString(),
|
||||
min_price: filters.minPrice.toString(),
|
||||
max_price: filters.maxPrice.toString(),
|
||||
bounds: boundsStr,
|
||||
});
|
||||
const res = await fetch(`/api/hexagons?${params}`, {
|
||||
signal: abortControllerRef.current.signal,
|
||||
});
|
||||
const json: ApiResponse = await res.json();
|
||||
setData(json.features || []);
|
||||
} catch (err) {
|
||||
if (err instanceof Error && err.name !== 'AbortError') {
|
||||
console.error('Failed to fetch data:', err);
|
||||
}
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, DEBOUNCE_MS);
|
||||
|
||||
return () => {
|
||||
if (debounceRef.current) {
|
||||
clearTimeout(debounceRef.current);
|
||||
}
|
||||
};
|
||||
}, [filters, resolution, bounds]);
|
||||
|
||||
const handleViewChange = useCallback(
|
||||
({ resolution: newRes, bounds: newBounds, zoom: newZoom }: ViewChangeParams) => {
|
||||
setResolution(newRes);
|
||||
setBounds(newBounds);
|
||||
setZoom(newZoom);
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="h-screen flex">
|
||||
<Filters filters={filters} onChange={setFilters} zoom={zoom} />
|
||||
<div className="flex-1 relative">
|
||||
<Map data={data} onViewChange={handleViewChange} />
|
||||
{loading && (
|
||||
<div className="absolute top-4 right-4 bg-white px-3 py-1 rounded shadow">Loading...</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue