Rewrite in TS

This commit is contained in:
Andras Schmelczer 2026-01-25 21:54:22 +00:00
parent 8c1f6a82e2
commit bfcf26e425
19 changed files with 3229 additions and 632 deletions

93
frontend/src/App.tsx Normal file
View 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>
);
}