Quick save

This commit is contained in:
Andras Schmelczer 2026-02-07 22:19:44 +00:00
parent e5d5819098
commit 2906b01734
25 changed files with 1070 additions and 237 deletions

View file

@ -14,6 +14,8 @@ import { usePOIData } from '../../hooks/usePOIData';
import { useFilters } from '../../hooks/useFilters';
import { useHexagonSelection } from '../../hooks/useHexagonSelection';
import { usePaneResize } from '../../hooks/usePaneResize';
import { useAreaSummary } from '../../hooks/useAreaSummary';
import { useUrlSync } from '../../hooks/useUrlSync';
import { apiUrl, buildFilterString } from '../../lib/api';
import { SpinnerIcon } from '../ui/icons/SpinnerIcon';
@ -36,6 +38,7 @@ interface MapPageProps {
onNavigateTo: (page: Page, hash?: string, infoFeature?: string) => void;
onExportStateChange?: (state: ExportState) => void;
screenshotMode?: boolean;
ogMode?: boolean;
}
export default function MapPage({
@ -52,34 +55,8 @@ export default function MapPage({
onNavigateTo,
onExportStateChange,
screenshotMode,
ogMode,
}: MapPageProps) {
if (screenshotMode) {
return (
<div className="h-screen w-screen">
<Map
data={[]}
postcodeData={[]}
usePostcodeView={false}
pois={[]}
onViewChange={() => {}}
viewFeature={null}
colorRange={null}
filterRange={null}
viewSource={null}
onCancelPin={() => {}}
features={features}
selectedHexagonId={null}
hoveredHexagonId={null}
onHexagonClick={() => {}}
onHexagonHover={() => {}}
initialViewState={initialViewState}
theme={theme}
screenshotMode
/>
</div>
);
}
const [searchedPostcode, setSearchedPostcode] = useState<SearchedPostcode | null>(null);
const [selectedPOICategories, setSelectedPOICategories] = useState<Set<string>>(initialPOICategories);
@ -137,6 +114,9 @@ export default function MapPage({
// POI data
const pois = usePOIData(mapData.bounds, selectedPOICategories);
// Sync current state to URL
useUrlSync(mapData.currentView, filters, features, selectedPOICategories, selection.rightPaneTab);
// Set initial view and tab from URL state
useEffect(() => {
mapData.setInitialView(initialViewState);
@ -146,10 +126,30 @@ export default function MapPage({
// Compute hexagon location for external links
const hexagonLocation = useMemo(() => {
const hexId = selection.selectedHexagon?.id;
const hex = hexId ? mapData.data.find((d) => d.h3 === hexId) : null;
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, mapData.data, mapData.resolution]);
const isPostcode = selection.selectedHexagon?.type === 'postcode';
if (isPostcode) {
// For postcodes, get centroid from postcodeData
const postcodeFeature = mapData.postcodeData.find((f) => f.properties.postcode === hexId);
if (!postcodeFeature?.properties.centroid) return null;
const [lon, lat] = postcodeFeature.properties.centroid;
return { lat, lon, resolution: mapData.resolution };
} else {
// For hexagons, get lat/lon from hexagon data
const hex = hexId ? mapData.data.find((d) => d.h3 === hexId) : null;
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]);
// AI area summary
const aiSummary = useAreaSummary({
stats: selection.areaStats,
hexagonId: selection.selectedHexagon?.id || null,
isPostcode: selection.selectedHexagon?.type === 'postcode',
filters,
features,
});
// Export to Excel
const [exporting, setExporting] = useState(false);
@ -185,6 +185,41 @@ export default function MapPage({
onExportStateChange?.({ onExport: handleExport, exporting });
}, [handleExport, exporting, onExportStateChange]);
// Signal screenshot readiness once map data has loaded
useEffect(() => {
if (screenshotMode && !mapData.loading && mapData.data.length > 0) {
window.__og_ready = true;
}
}, [screenshotMode, mapData.loading, mapData.data.length]);
if (screenshotMode) {
return (
<div className="h-screen w-screen">
<Map
data={mapData.data}
postcodeData={mapData.postcodeData}
usePostcodeView={mapData.usePostcodeView}
pois={[]}
onViewChange={mapData.handleViewChange}
viewFeature={viewFeature}
colorRange={mapData.colorRange}
filterRange={filterRange}
viewSource={viewSource}
onCancelPin={() => {}}
features={features}
selectedHexagonId={null}
hoveredHexagonId={null}
onHexagonClick={() => {}}
onHexagonHover={() => {}}
initialViewState={initialViewState}
theme={theme}
screenshotMode
ogMode={ogMode}
/>
</div>
);
}
return (
<div className="flex-1 flex overflow-hidden relative">
{initialLoading && (
@ -295,6 +330,10 @@ export default function MapPage({
hexagonLocation={hexagonLocation}
filters={filters}
onNavigateToSource={(slug, featureName) => onNavigateTo('data-sources', slug, featureName)}
aiSummary={aiSummary.summary}
aiSummaryLoading={aiSummary.loading}
aiSummaryError={aiSummary.error}
onRetryAiSummary={aiSummary.retry}
/>
) : selection.rightPaneTab === 'properties' ? (
<PropertiesPane