Lint & small changes

This commit is contained in:
Andras Schmelczer 2026-04-04 22:59:07 +01:00
parent 0c6d207967
commit 55238f59aa
21 changed files with 2522 additions and 423 deletions

View file

@ -116,7 +116,7 @@ export default function MapPage({
const [leftPaneWidth, leftPaneHandlers] = usePaneResize(384, 200, 0.45, 'left');
const [rightPaneWidth, rightPaneHandlers] = usePaneResize(384, 200, 0.45, 'right');
const [mobileMapHeight, mobileResizeHandlers, mobileMapRef] = usePaneResize(
const [, mobileResizeHandlers, mobileMapRef] = usePaneResize(
Math.round(window.innerHeight * 0.4),
120,
0.8,
@ -167,16 +167,32 @@ export default function MapPage({
features,
});
const aiFilters = useAiFilters();
const {
fetchAiFilters,
loading: aiFilterLoading,
error: aiFilterError,
errorType: aiFilterErrorType,
notes: aiFilterNotes,
summary: aiFilterSummary,
} = useAiFilters();
const travelTime = useTravelTime(initialTravelTime);
const {
entries,
activeEntries,
handleAddEntry,
handleRemoveEntry,
handleSetDestination,
handleSetEntries,
handleTimeRangeChange,
handleToggleBest,
} = useTravelTime(initialTravelTime);
const handleAiFilterSubmit = useCallback(
async (query: string) => {
// Build context from current filters for conversational refinement
const context = {
filters,
travelTime: travelTime.activeEntries.map((entry) => ({
travelTime: activeEntries.map((entry) => ({
mode: entry.mode,
label: entry.label,
min: entry.timeRange?.[0],
@ -185,7 +201,7 @@ export default function MapPage({
};
const hasContext = Object.keys(context.filters).length > 0 || context.travelTime.length > 0;
const result = await aiFilters.fetchAiFilters(query, hasContext ? context : undefined);
const result = await fetchAiFilters(query, hasContext ? context : undefined);
if (!result) return;
handleSetFilters(result.filters);
// Always sync travel time entries — clear stale ones when AI returns none
@ -196,40 +212,34 @@ export default function MapPage({
timeRange: [tt.min ?? 0, tt.max ?? 120] as [number, number],
useBest: false,
}));
travelTime.handleSetEntries(newEntries);
handleSetEntries(newEntries);
},
[
aiFilters.fetchAiFilters,
handleSetFilters,
travelTime.handleSetEntries,
travelTime.activeEntries,
filters,
]
[fetchAiFilters, handleSetFilters, handleSetEntries, activeEntries, filters]
);
const handleClearAll = useCallback(() => {
handleSetFilters({});
handleCancelPin();
travelTime.handleSetEntries([]);
}, [handleSetFilters, handleCancelPin, travelTime.handleSetEntries]);
handleSetEntries([]);
}, [handleSetFilters, handleCancelPin, handleSetEntries]);
const handleTravelTimeRemoveEntry = useCallback(
(index: number) => {
const entry = travelTime.entries[index];
const entry = entries[index];
if (entry?.slug && pinnedFeature === travelFieldKey(entry)) {
handleCancelPin();
}
travelTime.handleRemoveEntry(index);
handleRemoveEntry(index);
},
[travelTime.handleRemoveEntry, travelTime.entries, pinnedFeature, handleCancelPin]
[handleRemoveEntry, entries, pinnedFeature, handleCancelPin]
);
const handleTravelTimeDragEnd = useCallback(
(index: number) => {
const dv = handleDragEndNoCommit();
if (dv) travelTime.handleTimeRangeChange(index, dv);
if (dv) handleTimeRangeChange(index, dv);
},
[handleDragEndNoCommit, travelTime.handleTimeRangeChange]
[handleDragEndNoCommit, handleTimeRangeChange]
);
const license = useLicense();
@ -241,28 +251,46 @@ export default function MapPage({
features,
viewFeature,
activeFeature,
travelTimeEntries: travelTime.entries,
travelTimeEntries: entries,
});
const filterCounts = useFilterCounts(filters, features, mapData.bounds, travelTime.entries);
const filterCounts = useFilterCounts(filters, features, mapData.bounds, entries);
const handleTravelTimeSetDestination = useCallback(
(index: number, slug: string, label: string, lat: number, lon: number) => {
travelTime.handleSetDestination(index, slug, label);
handleSetDestination(index, slug, label);
if (slug) {
mapFlyToRef.current?.(lat, lon, mapData.currentView?.zoom ?? INITIAL_VIEW_STATE.zoom);
}
},
[travelTime.handleSetDestination, mapData.currentView?.zoom]
[handleSetDestination, mapData.currentView?.zoom]
);
// First transit destination — used to pick the best central_postcode for journey display
const journeyDest = useMemo(() => {
const entry = travelTime.entries.find((e) => e.mode === 'transit' && e.slug);
const entry = entries.find((e) => e.mode === 'transit' && e.slug);
return entry ? { mode: entry.mode, slug: entry.slug } : null;
}, [travelTime.entries]);
}, [entries]);
const selection = useHexagonSelection({
const {
selectedHexagon,
properties,
propertiesTotal,
loadingProperties,
areaStats,
loadingAreaStats,
hoveredHexagon,
rightPaneTab,
setRightPaneTab,
handleHexagonClick,
handleHexagonHover,
handleViewPropertiesFromArea,
handlePropertiesTabClick,
handleLoadMoreProperties,
handleCloseSelection,
selectedPostcodeGeometry,
handleLocationSearch,
} = useHexagonSelection({
filters,
features,
resolution: mapData.resolution,
@ -272,13 +300,13 @@ export default function MapPage({
const handleLocationSearchResult = useCallback(
(result: SearchedLocation | null) => {
if (result) {
selection.handleLocationSearch(result.postcode, result.geometry);
handleLocationSearch(result.postcode, result.geometry, result.latitude, result.longitude);
if (isMobile) setMobileDrawerOpen(true);
} else {
selection.handleCloseSelection();
handleCloseSelection();
}
},
[selection.handleLocationSearch, selection.handleCloseSelection, isMobile]
[handleLocationSearch, handleCloseSelection, isMobile]
);
const handleZoomToFreeZone = useCallback(() => {
@ -292,18 +320,11 @@ export default function MapPage({
const pois = usePOIData(mapData.bounds, selectedPOICategories);
const [isAreaGroupExpanded, toggleAreaGroup] = useCollapsibleGroups(true);
useUrlSync(
mapData.currentView,
filters,
features,
selectedPOICategories,
selection.rightPaneTab,
travelTime.entries
);
useUrlSync(mapData.currentView, filters, features, selectedPOICategories, rightPaneTab, entries);
useEffect(() => {
mapData.setInitialView(initialViewState);
selection.setRightPaneTab(initialTab);
setRightPaneTab(initialTab);
}, []); // eslint-disable-line react-hooks/exhaustive-deps
// Navigate to a specific postcode on mount (e.g. from saved properties)
@ -329,7 +350,7 @@ export default function MapPage({
geometry: PostcodeGeometry;
}) => {
mapFlyToRef.current?.(data.latitude, data.longitude, 16);
selection.handleLocationSearch(data.postcode, data.geometry);
handleLocationSearch(data.postcode, data.geometry, data.latitude, data.longitude);
if (isMobile) setMobileDrawerOpen(true);
}
)
@ -361,7 +382,6 @@ export default function MapPage({
return () => window.removeEventListener('popstate', handlePopState);
}, [isMobile]);
const { handleHexagonClick } = selection;
const handleMobileHexagonClick = useCallback(
(id: string, isPostcode?: boolean, geometry?: PostcodeGeometry) => {
handleHexagonClick(id, isPostcode, geometry);
@ -373,8 +393,8 @@ export default function MapPage({
);
const hexagonLocation = useMemo(() => {
const hexId = selection.selectedHexagon?.id;
const isPostcode = selection.selectedHexagon?.type === 'postcode';
const hexId = selectedHexagon?.id;
const isPostcode = selectedHexagon?.type === 'postcode';
if (isPostcode) {
// For postcodes, get centroid from postcodeData; postcode string is the selection id
@ -390,16 +410,16 @@ export default function MapPage({
lat: hex.lat as number,
lon: hex.lon as number,
resolution: mapData.resolution,
postcode: selection.areaStats?.central_postcode,
postcode: areaStats?.central_postcode,
};
}
}, [
selection.selectedHexagon?.id,
selection.selectedHexagon?.type,
selectedHexagon?.id,
selectedHexagon?.type,
mapData.data,
mapData.postcodeData,
mapData.resolution,
selection.areaStats?.central_postcode,
areaStats?.central_postcode,
]);
const tutorial = useTutorial(initialLoading, isMobile, deferTutorial);
@ -548,7 +568,7 @@ export default function MapPage({
screenshotMode
ogMode={ogMode}
bounds={mapData.bounds}
travelTimeEntries={travelTime.entries}
travelTimeEntries={entries}
/>
</div>
);
@ -556,22 +576,20 @@ export default function MapPage({
const renderAreaPane = () => (
<AreaPane
stats={selection.areaStats}
stats={areaStats}
globalFeatures={features}
loading={selection.loadingAreaStats}
hexagonId={selection.selectedHexagon?.id || null}
isPostcode={selection.selectedHexagon?.type === 'postcode'}
loading={loadingAreaStats}
hexagonId={selectedHexagon?.id || null}
isPostcode={selectedHexagon?.type === 'postcode'}
postcodeData={
selection.selectedHexagon?.type === 'postcode'
? mapData.postcodeData.find(
(f) => f.properties.postcode === selection.selectedHexagon?.id
) || null
selectedHexagon?.type === 'postcode'
? mapData.postcodeData.find((f) => f.properties.postcode === selectedHexagon?.id) || null
: null
}
onViewProperties={selection.handleViewPropertiesFromArea}
onViewProperties={handleViewPropertiesFromArea}
hexagonLocation={hexagonLocation}
filters={filters}
travelTimeEntries={travelTime.activeEntries}
travelTimeEntries={activeEntries}
isGroupExpanded={isAreaGroupExpanded}
onToggleGroup={toggleAreaGroup}
/>
@ -579,11 +597,11 @@ export default function MapPage({
const renderPropertiesPane = () => (
<PropertiesPane
properties={selection.properties}
total={selection.propertiesTotal}
loading={selection.loadingProperties}
hexagonId={selection.selectedHexagon?.id || null}
onLoadMore={selection.handleLoadMoreProperties}
properties={properties}
total={propertiesTotal}
loading={loadingProperties}
hexagonId={selectedHexagon?.id || null}
onLoadMore={handleLoadMoreProperties}
onSaveProperty={onSaveProperty ? handleSavePropertyWithToast : undefined}
onUnsaveProperty={onUnsaveProperty}
isPropertySaved={isPropertySaved}
@ -618,18 +636,18 @@ export default function MapPage({
onTogglePin={handleTogglePin}
openInfoFeature={pendingInfoFeature}
onClearOpenInfoFeature={onClearPendingInfoFeature}
travelTimeEntries={travelTime.entries}
onTravelTimeAddEntry={travelTime.handleAddEntry}
travelTimeEntries={entries}
onTravelTimeAddEntry={handleAddEntry}
onTravelTimeRemoveEntry={handleTravelTimeRemoveEntry}
onTravelTimeSetDestination={handleTravelTimeSetDestination}
onTravelTimeRangeChange={travelTime.handleTimeRangeChange}
onTravelTimeRangeChange={handleTimeRangeChange}
onTravelTimeDragEnd={handleTravelTimeDragEnd}
onTravelTimeToggleBest={travelTime.handleToggleBest}
aiFilterLoading={aiFilters.loading}
aiFilterError={aiFilters.error}
aiFilterErrorType={aiFilters.errorType}
aiFilterNotes={aiFilters.notes}
aiFilterSummary={aiFilters.summary}
onTravelTimeToggleBest={handleToggleBest}
aiFilterLoading={aiFilterLoading}
aiFilterError={aiFilterError}
aiFilterErrorType={aiFilterErrorType}
aiFilterNotes={aiFilterNotes}
aiFilterSummary={aiFilterSummary}
onAiFilterSubmit={handleAiFilterSubmit}
isLoggedIn={!!user}
onLoginRequired={onRegisterClick ?? (() => {})}
@ -661,7 +679,6 @@ export default function MapPage({
<div
ref={mobileMapRef}
className="relative overflow-hidden"
style={{ height: mobileMapHeight }}
>
<Map
data={mapData.data}
@ -675,19 +692,19 @@ export default function MapPage({
viewSource={viewSource}
onCancelPin={handleCancelPin}
features={features}
selectedHexagonId={selection.selectedHexagon?.id || null}
hoveredHexagonId={selection.hoveredHexagon}
selectedHexagonId={selectedHexagon?.id || null}
hoveredHexagonId={hoveredHexagon}
onHexagonClick={handleMobileHexagonClick}
onHexagonHover={selection.handleHexagonHover}
onHexagonHover={handleHexagonHover}
initialViewState={initialViewState}
flyToRef={mapFlyToRef}
theme={theme}
filters={filters}
selectedPostcodeGeometry={selection.selectedPostcodeGeometry}
selectedPostcodeGeometry={selectedPostcodeGeometry}
onLocationSearched={handleLocationSearchResult}
bounds={mapData.bounds}
hideLegend
travelTimeEntries={travelTime.entries}
travelTimeEntries={entries}
/>
{mapData.loading && (
<div className="absolute inset-0 flex items-center justify-center pointer-events-none">
@ -773,17 +790,17 @@ export default function MapPage({
<div className="flex-1 min-h-0">{renderFilters()}</div>
</div>
{mobileDrawerOpen && selection.selectedHexagon && (
{mobileDrawerOpen && selectedHexagon && (
<MobileDrawer
onClose={() => setMobileDrawerOpen(false)}
renderArea={renderAreaPane}
renderProperties={renderPropertiesPane}
tab={selection.rightPaneTab}
tab={rightPaneTab}
onTabChange={(t) => {
if (t === 'properties') {
selection.handlePropertiesTabClick();
handlePropertiesTabClick();
} else {
selection.setRightPaneTab(t);
setRightPaneTab(t);
}
}}
/>
@ -860,18 +877,18 @@ export default function MapPage({
viewSource={viewSource}
onCancelPin={handleCancelPin}
features={features}
selectedHexagonId={selection.selectedHexagon?.id || null}
hoveredHexagonId={selection.hoveredHexagon}
onHexagonClick={selection.handleHexagonClick}
onHexagonHover={selection.handleHexagonHover}
selectedHexagonId={selectedHexagon?.id || null}
hoveredHexagonId={hoveredHexagon}
onHexagonClick={handleHexagonClick}
onHexagonHover={handleHexagonHover}
initialViewState={initialViewState}
flyToRef={mapFlyToRef}
theme={theme}
filters={filters}
selectedPostcodeGeometry={selection.selectedPostcodeGeometry}
selectedPostcodeGeometry={selectedPostcodeGeometry}
onLocationSearched={handleLocationSearchResult}
bounds={mapData.bounds}
travelTimeEntries={travelTime.entries}
travelTimeEntries={entries}
densityLabel={densityLabel}
totalCount={filterCounts.total || undefined}
/>
@ -902,7 +919,7 @@ export default function MapPage({
)}
</div>
{selection.selectedHexagon && (
{selectedHexagon && (
<div
data-tutorial="right-pane"
className="flex bg-white dark:bg-navy-950 shadow-lg z-10"
@ -922,16 +939,16 @@ export default function MapPage({
<div className="flex border-b border-warm-200 dark:border-navy-700 text-sm">
<TabButton
label="Area"
isActive={selection.rightPaneTab === 'area'}
onClick={() => selection.setRightPaneTab('area')}
isActive={rightPaneTab === 'area'}
onClick={() => setRightPaneTab('area')}
/>
<TabButton
label="Properties"
isActive={selection.rightPaneTab === 'properties'}
onClick={selection.handlePropertiesTabClick}
isActive={rightPaneTab === 'properties'}
onClick={handlePropertiesTabClick}
/>
<button
onClick={selection.handleCloseSelection}
onClick={handleCloseSelection}
className="px-2 flex items-center text-warm-400 hover:text-warm-700 dark:hover:text-warm-300"
title="Close pane"
>
@ -940,7 +957,7 @@ export default function MapPage({
</div>
<div className="flex-1 overflow-hidden">
{selection.rightPaneTab === 'properties' ? renderPropertiesPane() : renderAreaPane()}
{rightPaneTab === 'properties' ? renderPropertiesPane() : renderAreaPane()}
</div>
</div>
</div>