Last night
This commit is contained in:
parent
2906b01734
commit
42ee2d4c51
47 changed files with 848 additions and 478 deletions
|
|
@ -7,6 +7,7 @@ import Filters from './Filters';
|
|||
import POIPane from './POIPane';
|
||||
import { PropertiesPane } from './PropertiesPane';
|
||||
import AreaPane from './AreaPane';
|
||||
import MobileDrawer from './MobileDrawer';
|
||||
import DataSources from '../data-sources/DataSources';
|
||||
import { TabButton } from '../ui/TabButton';
|
||||
import { useMapData } from '../../hooks/useMapData';
|
||||
|
|
@ -24,6 +25,8 @@ export interface ExportState {
|
|||
exporting: boolean;
|
||||
}
|
||||
|
||||
type MobileBottomTab = 'filters' | 'pois';
|
||||
|
||||
interface MapPageProps {
|
||||
features: FeatureMeta[];
|
||||
poiCategoryGroups: POICategoryGroup[];
|
||||
|
|
@ -39,6 +42,7 @@ interface MapPageProps {
|
|||
onExportStateChange?: (state: ExportState) => void;
|
||||
screenshotMode?: boolean;
|
||||
ogMode?: boolean;
|
||||
isMobile?: boolean;
|
||||
}
|
||||
|
||||
export default function MapPage({
|
||||
|
|
@ -56,6 +60,7 @@ export default function MapPage({
|
|||
onExportStateChange,
|
||||
screenshotMode,
|
||||
ogMode,
|
||||
isMobile = false,
|
||||
}: MapPageProps) {
|
||||
const [searchedPostcode, setSearchedPostcode] = useState<SearchedPostcode | null>(null);
|
||||
const [selectedPOICategories, setSelectedPOICategories] = useState<Set<string>>(initialPOICategories);
|
||||
|
|
@ -63,6 +68,10 @@ export default function MapPage({
|
|||
const [leftPaneWidth, leftPaneHandlers] = usePaneResize(384, 200, 600, 'left');
|
||||
const [rightPaneWidth, rightPaneHandlers] = usePaneResize(288, 200, 500, 'right');
|
||||
|
||||
// Mobile state
|
||||
const [mobileBottomTab, setMobileBottomTab] = useState<MobileBottomTab>('filters');
|
||||
const [mobileDrawerOpen, setMobileDrawerOpen] = useState(false);
|
||||
|
||||
// Initialize filters first
|
||||
const {
|
||||
filters,
|
||||
|
|
@ -123,6 +132,15 @@ export default function MapPage({
|
|||
selection.setRightPaneTab(initialTab);
|
||||
}, []); // 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
|
||||
|
||||
// Compute hexagon location for external links
|
||||
const hexagonLocation = useMemo(() => {
|
||||
const hexId = selection.selectedHexagon?.id;
|
||||
|
|
@ -194,7 +212,7 @@ export default function MapPage({
|
|||
|
||||
if (screenshotMode) {
|
||||
return (
|
||||
<div className="h-screen w-screen">
|
||||
<div className="h-full w-full">
|
||||
<Map
|
||||
data={mapData.data}
|
||||
postcodeData={mapData.postcodeData}
|
||||
|
|
@ -215,11 +233,164 @@ export default function MapPage({
|
|||
theme={theme}
|
||||
screenshotMode
|
||||
ogMode={ogMode}
|
||||
bounds={mapData.bounds}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Shared pane content renderers
|
||||
const renderAreaPane = () => (
|
||||
<AreaPane
|
||||
stats={selection.areaStats}
|
||||
globalFeatures={features}
|
||||
loading={selection.loadingAreaStats}
|
||||
hexagonId={selection.selectedHexagon?.id || null}
|
||||
isPostcode={selection.selectedHexagon?.type === 'postcode'}
|
||||
postcodeData={
|
||||
selection.selectedHexagon?.type === 'postcode'
|
||||
? mapData.postcodeData.find((f) => f.properties.postcode === selection.selectedHexagon?.id) || null
|
||||
: null
|
||||
}
|
||||
onViewProperties={selection.handleViewPropertiesFromArea}
|
||||
onClose={selection.handleCloseSelection}
|
||||
hexagonLocation={hexagonLocation}
|
||||
filters={filters}
|
||||
onNavigateToSource={(slug, featureName) => onNavigateTo('data-sources', slug, featureName)}
|
||||
aiSummary={aiSummary.summary}
|
||||
aiSummaryLoading={aiSummary.loading}
|
||||
aiSummaryError={aiSummary.error}
|
||||
onRetryAiSummary={aiSummary.retry}
|
||||
/>
|
||||
);
|
||||
|
||||
const renderPropertiesPane = () => (
|
||||
<PropertiesPane
|
||||
properties={selection.properties}
|
||||
total={selection.propertiesTotal}
|
||||
loading={selection.loadingProperties}
|
||||
hexagonId={selection.selectedHexagon?.id || null}
|
||||
onLoadMore={selection.handleLoadMoreProperties}
|
||||
onClose={selection.handleCloseSelection}
|
||||
onNavigateToSource={(slug) => onNavigateTo('data-sources', slug)}
|
||||
/>
|
||||
);
|
||||
|
||||
const renderPOIPane = () => (
|
||||
<POIPane
|
||||
groups={poiCategoryGroups}
|
||||
selectedCategories={selectedPOICategories}
|
||||
onCategoriesChange={setSelectedPOICategories}
|
||||
poiCount={pois.length}
|
||||
onNavigateToSource={(slug) => onNavigateTo('data-sources', slug)}
|
||||
/>
|
||||
);
|
||||
|
||||
const renderFilters = () => (
|
||||
<Filters
|
||||
features={features}
|
||||
filters={filters}
|
||||
activeFeature={activeFeature}
|
||||
dragValue={dragValue}
|
||||
enabledFeatures={enabledFeatures}
|
||||
onAddFilter={handleAddFilter}
|
||||
onRemoveFilter={handleRemoveFilter}
|
||||
onFilterChange={handleFilterChange}
|
||||
onDragStart={handleDragStart}
|
||||
onDragChange={handleDragChange}
|
||||
onDragEnd={handleDragEnd}
|
||||
zoom={mapData.zoom}
|
||||
itemCount={mapData.usePostcodeView ? mapData.postcodeData.length : mapData.data.length}
|
||||
usePostcodeView={mapData.usePostcodeView}
|
||||
pinnedFeature={pinnedFeature}
|
||||
onTogglePin={handleTogglePin}
|
||||
onCancelPin={handleCancelPin}
|
||||
onNavigateToSource={(slug, featureName) => onNavigateTo('data-sources', slug, featureName)}
|
||||
openInfoFeature={pendingInfoFeature}
|
||||
onClearOpenInfoFeature={onClearPendingInfoFeature}
|
||||
/>
|
||||
);
|
||||
|
||||
// Mobile layout
|
||||
if (isMobile) {
|
||||
return (
|
||||
<div className="flex-1 flex flex-col overflow-hidden relative">
|
||||
{initialLoading && (
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Map — 45% */}
|
||||
<div className="relative overflow-hidden" style={{ flex: '45 0 0' }}>
|
||||
<Map
|
||||
data={mapData.data}
|
||||
postcodeData={mapData.postcodeData}
|
||||
usePostcodeView={mapData.usePostcodeView}
|
||||
pois={pois}
|
||||
onViewChange={mapData.handleViewChange}
|
||||
viewFeature={viewFeature}
|
||||
colorRange={mapData.colorRange}
|
||||
filterRange={filterRange}
|
||||
viewSource={viewSource}
|
||||
onCancelPin={handleCancelPin}
|
||||
features={features}
|
||||
selectedHexagonId={selection.selectedHexagon?.id || null}
|
||||
hoveredHexagonId={selection.hoveredHexagon}
|
||||
onHexagonClick={handleMobileHexagonClick}
|
||||
onHexagonHover={selection.handleHexagonHover}
|
||||
initialViewState={initialViewState}
|
||||
theme={theme}
|
||||
filters={filters}
|
||||
searchedPostcode={searchedPostcode}
|
||||
onPostcodeSearched={setSearchedPostcode}
|
||||
bounds={mapData.bounds}
|
||||
/>
|
||||
{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">
|
||||
Loading...
|
||||
</div>
|
||||
)}
|
||||
<DataSources onNavigate={() => onNavigateTo('data-sources')} />
|
||||
</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' }}>
|
||||
{/* 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>
|
||||
|
||||
{/* Tab content */}
|
||||
<div className="flex-1 min-h-0">
|
||||
{mobileBottomTab === 'pois' ? (
|
||||
<div className="h-full overflow-y-auto">
|
||||
{renderPOIPane()}
|
||||
</div>
|
||||
) : (
|
||||
renderFilters()
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Mobile drawer for full-screen hexagon details */}
|
||||
{mobileDrawerOpen && selection.selectedHexagon && (
|
||||
<MobileDrawer
|
||||
onClose={() => setMobileDrawerOpen(false)}
|
||||
renderArea={renderAreaPane}
|
||||
renderProperties={renderPropertiesPane}
|
||||
renderPOIs={renderPOIPane}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Desktop layout (unchanged)
|
||||
return (
|
||||
<div className="flex-1 flex overflow-hidden relative">
|
||||
{initialLoading && (
|
||||
|
|
@ -234,28 +405,7 @@ export default function MapPage({
|
|||
{/* 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">
|
||||
<Filters
|
||||
features={features}
|
||||
filters={filters}
|
||||
activeFeature={activeFeature}
|
||||
dragValue={dragValue}
|
||||
enabledFeatures={enabledFeatures}
|
||||
onAddFilter={handleAddFilter}
|
||||
onRemoveFilter={handleRemoveFilter}
|
||||
onFilterChange={handleFilterChange}
|
||||
onDragStart={handleDragStart}
|
||||
onDragChange={handleDragChange}
|
||||
onDragEnd={handleDragEnd}
|
||||
zoom={mapData.zoom}
|
||||
itemCount={mapData.usePostcodeView ? mapData.postcodeData.length : mapData.data.length}
|
||||
usePostcodeView={mapData.usePostcodeView}
|
||||
pinnedFeature={pinnedFeature}
|
||||
onTogglePin={handleTogglePin}
|
||||
onCancelPin={handleCancelPin}
|
||||
onNavigateToSource={(slug, featureName) => onNavigateTo('data-sources', slug, featureName)}
|
||||
openInfoFeature={pendingInfoFeature}
|
||||
onClearOpenInfoFeature={onClearPendingInfoFeature}
|
||||
/>
|
||||
{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"
|
||||
|
|
@ -288,6 +438,7 @@ export default function MapPage({
|
|||
filters={filters}
|
||||
searchedPostcode={searchedPostcode}
|
||||
onPostcodeSearched={setSearchedPostcode}
|
||||
bounds={mapData.bounds}
|
||||
/>
|
||||
{mapData.loading && (
|
||||
<div className="absolute bottom-4 left-4 bg-white dark:bg-navy-800 dark:text-warm-200 px-3 py-1 rounded shadow">
|
||||
|
|
@ -314,45 +465,11 @@ export default function MapPage({
|
|||
|
||||
<div className="flex-1 overflow-hidden">
|
||||
{selection.rightPaneTab === 'area' ? (
|
||||
<AreaPane
|
||||
stats={selection.areaStats}
|
||||
globalFeatures={features}
|
||||
loading={selection.loadingAreaStats}
|
||||
hexagonId={selection.selectedHexagon?.id || null}
|
||||
isPostcode={selection.selectedHexagon?.type === 'postcode'}
|
||||
postcodeData={
|
||||
selection.selectedHexagon?.type === 'postcode'
|
||||
? mapData.postcodeData.find((f) => f.properties.postcode === selection.selectedHexagon?.id) || null
|
||||
: null
|
||||
}
|
||||
onViewProperties={selection.handleViewPropertiesFromArea}
|
||||
onClose={selection.handleCloseSelection}
|
||||
hexagonLocation={hexagonLocation}
|
||||
filters={filters}
|
||||
onNavigateToSource={(slug, featureName) => onNavigateTo('data-sources', slug, featureName)}
|
||||
aiSummary={aiSummary.summary}
|
||||
aiSummaryLoading={aiSummary.loading}
|
||||
aiSummaryError={aiSummary.error}
|
||||
onRetryAiSummary={aiSummary.retry}
|
||||
/>
|
||||
renderAreaPane()
|
||||
) : selection.rightPaneTab === 'properties' ? (
|
||||
<PropertiesPane
|
||||
properties={selection.properties}
|
||||
total={selection.propertiesTotal}
|
||||
loading={selection.loadingProperties}
|
||||
hexagonId={selection.selectedHexagon?.id || null}
|
||||
onLoadMore={selection.handleLoadMoreProperties}
|
||||
onClose={selection.handleCloseSelection}
|
||||
onNavigateToSource={(slug) => onNavigateTo('data-sources', slug)}
|
||||
/>
|
||||
renderPropertiesPane()
|
||||
) : (
|
||||
<POIPane
|
||||
groups={poiCategoryGroups}
|
||||
selectedCategories={selectedPOICategories}
|
||||
onCategoriesChange={setSelectedPOICategories}
|
||||
poiCount={pois.length}
|
||||
onNavigateToSource={(slug) => onNavigateTo('data-sources', slug)}
|
||||
/>
|
||||
renderPOIPane()
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue