83 lines
2.5 KiB
JavaScript
83 lines
2.5 KiB
JavaScript
import React, { useState, useEffect, useCallback, useRef } from 'react';
|
|
import Map from './components/Map';
|
|
import Filters from './components/Filters';
|
|
import { DEFAULT_FILTERS } from './lib/constants';
|
|
|
|
const DEBOUNCE_MS = 150;
|
|
|
|
export default function App() {
|
|
const [filters, setFilters] = useState(DEFAULT_FILTERS);
|
|
const [data, setData] = useState([]);
|
|
const [resolution, setResolution] = useState(8);
|
|
const [bounds, setBounds] = useState(null);
|
|
const [loading, setLoading] = useState(false);
|
|
const debounceRef = useRef(null);
|
|
const abortControllerRef = useRef(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 = await res.json();
|
|
setData(json.features || []);
|
|
} catch (err) {
|
|
if (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 }) => {
|
|
setResolution(newRes);
|
|
setBounds(newBounds);
|
|
}, []);
|
|
|
|
return (
|
|
<div className="h-screen flex">
|
|
<Filters filters={filters} onChange={setFilters} />
|
|
<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>
|
|
);
|
|
}
|