Lint & small changes
This commit is contained in:
parent
0c6d207967
commit
55238f59aa
21 changed files with 2522 additions and 423 deletions
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue