176 lines
6 KiB
TypeScript
176 lines
6 KiB
TypeScript
import { useState, useCallback } from 'react';
|
|
import type {
|
|
FeatureMeta,
|
|
FeatureFilters,
|
|
Property,
|
|
HexagonPropertiesResponse,
|
|
HexagonStatsResponse,
|
|
} from '../types';
|
|
import { buildFilterString, apiUrl, logNonAbortError, authHeaders } from '../lib/api';
|
|
|
|
interface SelectedHexagon {
|
|
id: string;
|
|
type: 'hexagon' | 'postcode';
|
|
resolution: number;
|
|
}
|
|
|
|
interface UseHexagonSelectionOptions {
|
|
filters: FeatureFilters;
|
|
features: FeatureMeta[];
|
|
resolution: number;
|
|
}
|
|
|
|
export function useHexagonSelection({ filters, features, resolution }: UseHexagonSelectionOptions) {
|
|
const [selectedHexagon, setSelectedHexagon] = useState<SelectedHexagon | null>(null);
|
|
const [properties, setProperties] = useState<Property[]>([]);
|
|
const [propertiesTotal, setPropertiesTotal] = useState(0);
|
|
const [propertiesOffset, setPropertiesOffset] = useState(0);
|
|
const [loadingProperties, setLoadingProperties] = useState(false);
|
|
const [areaStats, setAreaStats] = useState<HexagonStatsResponse | null>(null);
|
|
const [loadingAreaStats, setLoadingAreaStats] = useState(false);
|
|
const [hoveredHexagon, setHoveredHexagon] = useState<string | null>(null);
|
|
const [rightPaneTab, setRightPaneTab] = useState<'pois' | 'properties' | 'area'>('pois');
|
|
|
|
const fetchHexagonStats = useCallback(
|
|
async (h3: string, res: number, signal?: AbortSignal, fields?: string[]) => {
|
|
const params = new URLSearchParams({
|
|
h3,
|
|
resolution: res.toString(),
|
|
});
|
|
const filterStr = buildFilterString(filters, features);
|
|
if (filterStr) params.append('filters', filterStr);
|
|
if (fields) {
|
|
params.set('fields', fields.join(','));
|
|
}
|
|
const response = await fetch(apiUrl('hexagon-stats', params), authHeaders({ signal }));
|
|
return (await response.json()) as HexagonStatsResponse;
|
|
},
|
|
[filters, features]
|
|
);
|
|
|
|
const fetchPostcodeStats = useCallback(
|
|
async (postcode: string, signal?: AbortSignal) => {
|
|
const params = new URLSearchParams({ postcode });
|
|
const filterStr = buildFilterString(filters, features);
|
|
if (filterStr) params.append('filters', filterStr);
|
|
const response = await fetch(apiUrl('postcode-stats', params), authHeaders({ signal }));
|
|
return (await response.json()) as HexagonStatsResponse;
|
|
},
|
|
[filters, features]
|
|
);
|
|
|
|
const fetchHexagonProperties = useCallback(
|
|
async (h3: string, res: number, offset = 0) => {
|
|
setLoadingProperties(true);
|
|
try {
|
|
const params = new URLSearchParams({
|
|
h3,
|
|
resolution: res.toString(),
|
|
limit: '100',
|
|
offset: offset.toString(),
|
|
});
|
|
|
|
const filterStr = buildFilterString(filters, features);
|
|
if (filterStr) params.append('filters', filterStr);
|
|
|
|
const response = await fetch(apiUrl('hexagon-properties', params), authHeaders());
|
|
const data: HexagonPropertiesResponse = await response.json();
|
|
|
|
if (offset === 0) {
|
|
setProperties(data.properties);
|
|
} else {
|
|
setProperties((prev) => [...prev, ...data.properties]);
|
|
}
|
|
setPropertiesTotal(data.total);
|
|
setPropertiesOffset(offset + data.properties.length);
|
|
} catch (err) {
|
|
console.error('Failed to fetch properties:', err);
|
|
} finally {
|
|
setLoadingProperties(false);
|
|
}
|
|
},
|
|
[filters, features]
|
|
);
|
|
|
|
const handleHexagonClick = useCallback(
|
|
(id: string, isPostcode = false) => {
|
|
if (selectedHexagon?.id === id) {
|
|
setSelectedHexagon(null);
|
|
setProperties([]);
|
|
setAreaStats(null);
|
|
} else {
|
|
const type = isPostcode ? 'postcode' : 'hexagon';
|
|
setSelectedHexagon({ id, type, resolution });
|
|
setProperties([]);
|
|
setPropertiesTotal(0);
|
|
setPropertiesOffset(0);
|
|
setRightPaneTab('area');
|
|
|
|
if (isPostcode) {
|
|
setLoadingAreaStats(true);
|
|
fetchPostcodeStats(id)
|
|
.then((stats) => setAreaStats(stats))
|
|
.catch((error) => logNonAbortError('Failed to fetch postcode stats', error))
|
|
.finally(() => setLoadingAreaStats(false));
|
|
} else {
|
|
setLoadingAreaStats(true);
|
|
fetchHexagonStats(id, resolution)
|
|
.then((stats) => setAreaStats(stats))
|
|
.catch((error) => logNonAbortError('Failed to fetch area stats', error))
|
|
.finally(() => setLoadingAreaStats(false));
|
|
}
|
|
}
|
|
},
|
|
[selectedHexagon, resolution, fetchHexagonStats, fetchPostcodeStats]
|
|
);
|
|
|
|
const handleHexagonHover = useCallback((h3: string | null) => {
|
|
setHoveredHexagon(h3);
|
|
}, []);
|
|
|
|
const handleViewPropertiesFromArea = useCallback(() => {
|
|
if (selectedHexagon && selectedHexagon.type === 'hexagon') {
|
|
setRightPaneTab('properties');
|
|
setPropertiesOffset(0);
|
|
fetchHexagonProperties(selectedHexagon.id, selectedHexagon.resolution, 0);
|
|
}
|
|
}, [selectedHexagon, fetchHexagonProperties]);
|
|
|
|
const handlePropertiesTabClick = useCallback(() => {
|
|
setRightPaneTab('properties');
|
|
if (selectedHexagon?.type === 'hexagon' && properties.length === 0 && !loadingProperties) {
|
|
setPropertiesOffset(0);
|
|
fetchHexagonProperties(selectedHexagon.id, selectedHexagon.resolution, 0);
|
|
}
|
|
}, [selectedHexagon, properties.length, loadingProperties, fetchHexagonProperties]);
|
|
|
|
const handleLoadMoreProperties = useCallback(() => {
|
|
if (selectedHexagon && selectedHexagon.type === 'hexagon') {
|
|
fetchHexagonProperties(selectedHexagon.id, selectedHexagon.resolution, propertiesOffset);
|
|
}
|
|
}, [selectedHexagon, propertiesOffset, fetchHexagonProperties]);
|
|
|
|
const handleCloseSelection = useCallback(() => {
|
|
setSelectedHexagon(null);
|
|
setProperties([]);
|
|
setAreaStats(null);
|
|
}, []);
|
|
|
|
return {
|
|
selectedHexagon,
|
|
properties,
|
|
propertiesTotal,
|
|
loadingProperties,
|
|
areaStats,
|
|
loadingAreaStats,
|
|
hoveredHexagon,
|
|
rightPaneTab,
|
|
setRightPaneTab,
|
|
handleHexagonClick,
|
|
handleHexagonHover,
|
|
handleViewPropertiesFromArea,
|
|
handlePropertiesTabClick,
|
|
handleLoadMoreProperties,
|
|
handleCloseSelection,
|
|
};
|
|
}
|