Format and lint
This commit is contained in:
parent
42ee2d4c51
commit
04a78e7bfe
75 changed files with 1290 additions and 719 deletions
|
|
@ -9,6 +9,7 @@ import { PropertiesPane } from './PropertiesPane';
|
|||
import AreaPane from './AreaPane';
|
||||
import MobileDrawer from './MobileDrawer';
|
||||
import DataSources from '../data-sources/DataSources';
|
||||
import MapLegend from './MapLegend';
|
||||
import { TabButton } from '../ui/TabButton';
|
||||
import { useMapData } from '../../hooks/useMapData';
|
||||
import { usePOIData } from '../../hooks/usePOIData';
|
||||
|
|
@ -25,7 +26,7 @@ export interface ExportState {
|
|||
exporting: boolean;
|
||||
}
|
||||
|
||||
type MobileBottomTab = 'filters' | 'pois';
|
||||
type MobileBottomTab = 'filters' | 'pois' | 'area';
|
||||
|
||||
interface MapPageProps {
|
||||
features: FeatureMeta[];
|
||||
|
|
@ -63,7 +64,8 @@ export default function MapPage({
|
|||
isMobile = false,
|
||||
}: MapPageProps) {
|
||||
const [searchedPostcode, setSearchedPostcode] = useState<SearchedPostcode | null>(null);
|
||||
const [selectedPOICategories, setSelectedPOICategories] = useState<Set<string>>(initialPOICategories);
|
||||
const [selectedPOICategories, setSelectedPOICategories] =
|
||||
useState<Set<string>>(initialPOICategories);
|
||||
|
||||
const [leftPaneWidth, leftPaneHandlers] = usePaneResize(384, 200, 600, 'left');
|
||||
const [rightPaneWidth, rightPaneHandlers] = usePaneResize(288, 200, 500, 'right');
|
||||
|
|
@ -116,7 +118,6 @@ export default function MapPage({
|
|||
const selection = useHexagonSelection({
|
||||
filters,
|
||||
features,
|
||||
postcodeData: mapData.postcodeData,
|
||||
resolution: mapData.resolution,
|
||||
});
|
||||
|
||||
|
|
@ -133,13 +134,17 @@ export default function MapPage({
|
|||
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
// On mobile, open drawer and switch tab when hexagon is clicked
|
||||
const handleMobileHexagonClick = useCallback((id: string, isPostcode?: boolean) => {
|
||||
selection.handleHexagonClick(id, isPostcode);
|
||||
if (id) {
|
||||
setMobileDrawerOpen(true);
|
||||
setMobileBottomTab('area');
|
||||
}
|
||||
}, [selection.handleHexagonClick]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
const { handleHexagonClick } = selection;
|
||||
const handleMobileHexagonClick = useCallback(
|
||||
(id: string, isPostcode?: boolean) => {
|
||||
handleHexagonClick(id, isPostcode);
|
||||
if (id) {
|
||||
setMobileDrawerOpen(true);
|
||||
setMobileBottomTab('area');
|
||||
}
|
||||
},
|
||||
[handleHexagonClick]
|
||||
);
|
||||
|
||||
// Compute hexagon location for external links
|
||||
const hexagonLocation = useMemo(() => {
|
||||
|
|
@ -158,7 +163,13 @@ export default function MapPage({
|
|||
if (!hex || typeof hex.lat !== 'number' || typeof hex.lon !== 'number') return null;
|
||||
return { lat: hex.lat as number, lon: hex.lon as number, resolution: mapData.resolution };
|
||||
}
|
||||
}, [selection.selectedHexagon?.id, selection.selectedHexagon?.type, mapData.data, mapData.postcodeData, mapData.resolution]);
|
||||
}, [
|
||||
selection.selectedHexagon?.id,
|
||||
selection.selectedHexagon?.type,
|
||||
mapData.data,
|
||||
mapData.postcodeData,
|
||||
mapData.resolution,
|
||||
]);
|
||||
|
||||
// AI area summary
|
||||
const aiSummary = useAreaSummary({
|
||||
|
|
@ -203,10 +214,33 @@ export default function MapPage({
|
|||
onExportStateChange?.({ onExport: handleExport, exporting });
|
||||
}, [handleExport, exporting, onExportStateChange]);
|
||||
|
||||
// Mobile legend data (computed from API-fetched data, which is already viewport-scoped)
|
||||
const mobileLegendMeta = useMemo(
|
||||
() => (viewFeature ? features.find((f) => f.name === viewFeature) || null : null),
|
||||
[viewFeature, features]
|
||||
);
|
||||
const mobileDensityRange = useMemo((): [number, number] => {
|
||||
const items = mapData.usePostcodeView ? mapData.postcodeData : mapData.data;
|
||||
if (items.length === 0) return [0, 1];
|
||||
let min = Infinity;
|
||||
let max = -Infinity;
|
||||
for (const d of items) {
|
||||
const c =
|
||||
'count' in d
|
||||
? (d as { count: number }).count
|
||||
: (d as { properties: { count: number } }).properties.count;
|
||||
if (c < min) min = c;
|
||||
if (c > max) max = c;
|
||||
}
|
||||
if (min === Infinity) return [0, 1];
|
||||
if (min === max) return [min, min + 1];
|
||||
return [min, max];
|
||||
}, [mapData.data, mapData.postcodeData, mapData.usePostcodeView]);
|
||||
|
||||
// Signal screenshot readiness once map data has loaded
|
||||
useEffect(() => {
|
||||
if (screenshotMode && !mapData.loading && mapData.data.length > 0) {
|
||||
window.__og_ready = true;
|
||||
window.__screenshot_ready = true;
|
||||
}
|
||||
}, [screenshotMode, mapData.loading, mapData.data.length]);
|
||||
|
||||
|
|
@ -249,7 +283,9 @@ export default function MapPage({
|
|||
isPostcode={selection.selectedHexagon?.type === 'postcode'}
|
||||
postcodeData={
|
||||
selection.selectedHexagon?.type === 'postcode'
|
||||
? mapData.postcodeData.find((f) => f.properties.postcode === selection.selectedHexagon?.id) || null
|
||||
? mapData.postcodeData.find(
|
||||
(f) => f.properties.postcode === selection.selectedHexagon?.id
|
||||
) || null
|
||||
: null
|
||||
}
|
||||
onViewProperties={selection.handleViewPropertiesFromArea}
|
||||
|
|
@ -260,7 +296,6 @@ export default function MapPage({
|
|||
aiSummary={aiSummary.summary}
|
||||
aiSummaryLoading={aiSummary.loading}
|
||||
aiSummaryError={aiSummary.error}
|
||||
onRetryAiSummary={aiSummary.retry}
|
||||
/>
|
||||
);
|
||||
|
||||
|
|
@ -319,7 +354,9 @@ export default function MapPage({
|
|||
<div className="absolute inset-0 z-50 flex items-center justify-center bg-warm-50/80 dark:bg-navy-950/80 backdrop-blur-sm">
|
||||
<div className="flex flex-col items-center gap-4">
|
||||
<SpinnerIcon className="w-12 h-12 text-teal-600 dark:text-teal-400 animate-spin" />
|
||||
<p className="text-warm-600 dark:text-warm-300 text-sm font-medium">Connecting to server...</p>
|
||||
<p className="text-warm-600 dark:text-warm-300 text-sm font-medium">
|
||||
Connecting to server...
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -348,6 +385,7 @@ export default function MapPage({
|
|||
searchedPostcode={searchedPostcode}
|
||||
onPostcodeSearched={setSearchedPostcode}
|
||||
bounds={mapData.bounds}
|
||||
hideLegend
|
||||
/>
|
||||
{mapData.loading && (
|
||||
<div className="absolute bottom-2 left-2 bg-white dark:bg-navy-800 dark:text-warm-200 px-2 py-1 rounded shadow text-xs">
|
||||
|
|
@ -358,19 +396,55 @@ export default function MapPage({
|
|||
</div>
|
||||
|
||||
{/* Bottom panel — 55% */}
|
||||
<div className="bg-white dark:bg-navy-950 border-t border-warm-200 dark:border-navy-700 overflow-hidden flex flex-col" style={{ flex: '55 0 0' }}>
|
||||
<div
|
||||
className="bg-white dark:bg-warm-900 border-t border-warm-200 dark:border-warm-700 overflow-hidden flex flex-col"
|
||||
style={{ flex: '55 0 0' }}
|
||||
>
|
||||
{/* Legend */}
|
||||
{viewFeature && mapData.colorRange && mobileLegendMeta ? (
|
||||
<MapLegend
|
||||
featureLabel={
|
||||
viewSource === 'eye'
|
||||
? `Previewing \u201c${mobileLegendMeta.name}\u201d`
|
||||
: mobileLegendMeta.name
|
||||
}
|
||||
range={mapData.colorRange}
|
||||
showCancel={viewSource === 'eye'}
|
||||
onCancel={handleCancelPin}
|
||||
mode="feature"
|
||||
enumValues={mobileLegendMeta.type === 'enum' ? mobileLegendMeta.values : undefined}
|
||||
theme={theme}
|
||||
inline
|
||||
/>
|
||||
) : (
|
||||
<MapLegend
|
||||
featureLabel="Property density"
|
||||
range={mobileDensityRange}
|
||||
showCancel={false}
|
||||
onCancel={handleCancelPin}
|
||||
mode="density"
|
||||
theme={theme}
|
||||
inline
|
||||
/>
|
||||
)}
|
||||
{/* Tab bar */}
|
||||
<div className="flex shrink-0 border-b border-warm-200 dark:border-navy-700 text-sm">
|
||||
<TabButton label="Filters" isActive={mobileBottomTab === 'filters'} onClick={() => setMobileBottomTab('filters')} />
|
||||
<TabButton label="POIs" isActive={mobileBottomTab === 'pois'} onClick={() => setMobileBottomTab('pois')} />
|
||||
<div className="flex shrink-0 border-b border-warm-200 dark:border-warm-700 text-sm">
|
||||
<TabButton
|
||||
label="Filters"
|
||||
isActive={mobileBottomTab === 'filters'}
|
||||
onClick={() => setMobileBottomTab('filters')}
|
||||
/>
|
||||
<TabButton
|
||||
label="POIs"
|
||||
isActive={mobileBottomTab === 'pois'}
|
||||
onClick={() => setMobileBottomTab('pois')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Tab content */}
|
||||
<div className="flex-1 min-h-0">
|
||||
{mobileBottomTab === 'pois' ? (
|
||||
<div className="h-full overflow-y-auto">
|
||||
{renderPOIPane()}
|
||||
</div>
|
||||
<div className="h-full overflow-y-auto">{renderPOIPane()}</div>
|
||||
) : (
|
||||
renderFilters()
|
||||
)}
|
||||
|
|
@ -397,16 +471,19 @@ export default function MapPage({
|
|||
<div className="absolute inset-0 z-50 flex items-center justify-center bg-warm-50/80 dark:bg-navy-950/80 backdrop-blur-sm">
|
||||
<div className="flex flex-col items-center gap-4">
|
||||
<SpinnerIcon className="w-12 h-12 text-teal-600 dark:text-teal-400 animate-spin" />
|
||||
<p className="text-warm-600 dark:text-warm-300 text-sm font-medium">Connecting to server...</p>
|
||||
<p className="text-warm-600 dark:text-warm-300 text-sm font-medium">
|
||||
Connecting to server...
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Left Pane */}
|
||||
<div className="flex bg-white dark:bg-navy-950 shadow-lg overflow-hidden" style={{ width: leftPaneWidth }}>
|
||||
<div className="flex-1 flex flex-col overflow-hidden">
|
||||
{renderFilters()}
|
||||
</div>
|
||||
<div
|
||||
className="flex bg-white dark:bg-navy-950 shadow-lg overflow-hidden"
|
||||
style={{ width: leftPaneWidth }}
|
||||
>
|
||||
<div className="flex-1 flex flex-col overflow-hidden">{renderFilters()}</div>
|
||||
<div
|
||||
className="w-1.5 cursor-col-resize flex items-center justify-center bg-warm-100 dark:bg-navy-800 hover:bg-warm-200 dark:hover:bg-navy-700 border-x border-warm-200 dark:border-navy-700"
|
||||
{...leftPaneHandlers}
|
||||
|
|
@ -449,7 +526,10 @@ export default function MapPage({
|
|||
</div>
|
||||
|
||||
{/* Right Pane */}
|
||||
<div className="flex bg-white dark:bg-navy-950 shadow-lg z-10" style={{ width: rightPaneWidth }}>
|
||||
<div
|
||||
className="flex bg-white dark:bg-navy-950 shadow-lg z-10"
|
||||
style={{ width: rightPaneWidth }}
|
||||
>
|
||||
<div
|
||||
className="w-1.5 cursor-col-resize flex items-center justify-center bg-warm-100 dark:bg-navy-800 hover:bg-warm-200 dark:hover:bg-navy-700 border-x border-warm-200 dark:border-navy-700"
|
||||
{...rightPaneHandlers}
|
||||
|
|
@ -458,19 +538,29 @@ export default function MapPage({
|
|||
</div>
|
||||
<div className="flex-1 flex flex-col overflow-hidden">
|
||||
<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')} />
|
||||
<TabButton label="Properties" isActive={selection.rightPaneTab === 'properties'} onClick={selection.handlePropertiesTabClick} />
|
||||
<TabButton label="POIs" isActive={selection.rightPaneTab === 'pois'} onClick={() => selection.setRightPaneTab('pois')} />
|
||||
<TabButton
|
||||
label="Area"
|
||||
isActive={selection.rightPaneTab === 'area'}
|
||||
onClick={() => selection.setRightPaneTab('area')}
|
||||
/>
|
||||
<TabButton
|
||||
label="Properties"
|
||||
isActive={selection.rightPaneTab === 'properties'}
|
||||
onClick={selection.handlePropertiesTabClick}
|
||||
/>
|
||||
<TabButton
|
||||
label="POIs"
|
||||
isActive={selection.rightPaneTab === 'pois'}
|
||||
onClick={() => selection.setRightPaneTab('pois')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-hidden">
|
||||
{selection.rightPaneTab === 'area' ? (
|
||||
renderAreaPane()
|
||||
) : selection.rightPaneTab === 'properties' ? (
|
||||
renderPropertiesPane()
|
||||
) : (
|
||||
renderPOIPane()
|
||||
)}
|
||||
{selection.rightPaneTab === 'area'
|
||||
? renderAreaPane()
|
||||
: selection.rightPaneTab === 'properties'
|
||||
? renderPropertiesPane()
|
||||
: renderPOIPane()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue