More
This commit is contained in:
parent
cd34ee693f
commit
05a1f316e1
58 changed files with 3113 additions and 1277 deletions
|
|
@ -17,6 +17,7 @@ import POIPane from './POIPane';
|
|||
import { PropertiesPane } from './PropertiesPane';
|
||||
import AreaPane from './AreaPane';
|
||||
import MobileDrawer from './MobileDrawer';
|
||||
import MobileBottomSheet from './MobileBottomSheet';
|
||||
import MapLegend from './MapLegend';
|
||||
import { MapPageSelectionPane } from './MapPageSelectionPane';
|
||||
import { useMapData } from '../../hooks/useMapData';
|
||||
|
|
@ -41,6 +42,7 @@ import { useFilterCounts } from '../../hooks/useFilterCounts';
|
|||
import { ts } from '../../i18n/server';
|
||||
import { trackEvent } from '../../lib/analytics';
|
||||
import { INITIAL_VIEW_STATE } from '../../lib/consts';
|
||||
import { getSchoolBackendFeatureName } from '../../lib/school-filter';
|
||||
import { useLicense } from '../../hooks/useLicense';
|
||||
import UpgradeModal from '../ui/UpgradeModal';
|
||||
import { SpinnerIcon } from '../ui/icons/SpinnerIcon';
|
||||
|
|
@ -116,12 +118,6 @@ export default function MapPage({
|
|||
|
||||
const [leftPaneWidth, leftPaneHandlers] = usePaneResize(384, 200, 0.45, 'left');
|
||||
const [rightPaneWidth, rightPaneHandlers] = usePaneResize(384, 200, 0.45, 'right');
|
||||
const [, mobileResizeHandlers, mobileMapRef] = usePaneResize(
|
||||
Math.round(window.innerHeight * 0.4),
|
||||
120,
|
||||
0.8,
|
||||
'top'
|
||||
);
|
||||
|
||||
const [mobileDrawerOpen, setMobileDrawerOpen] = useState(false);
|
||||
const [poiPaneOpen, setPoiPaneOpen] = useState(false);
|
||||
|
|
@ -510,9 +506,15 @@ export default function MapPage({
|
|||
const densityLabel = t('mapLegend.historicalMatches');
|
||||
const hasActiveFilters = Object.keys(filters).length > 0 || activeEntries.length > 0;
|
||||
|
||||
const mobileLegendMeta = useMemo(
|
||||
() => (viewFeature ? features.find((f) => f.name === viewFeature) || null : null),
|
||||
[viewFeature, features]
|
||||
const mobileLegendMeta = useMemo(() => {
|
||||
const featureName = viewFeature
|
||||
? (getSchoolBackendFeatureName(viewFeature) ?? viewFeature)
|
||||
: null;
|
||||
return featureName ? features.find((f) => f.name === featureName) || null : null;
|
||||
}, [viewFeature, features]);
|
||||
const mapViewFeature = useMemo(
|
||||
() => (viewFeature ? (getSchoolBackendFeatureName(viewFeature) ?? viewFeature) : null),
|
||||
[viewFeature]
|
||||
);
|
||||
const mobileDensityRange = useMemo((): [number, number] => {
|
||||
const items = mapData.usePostcodeView ? mapData.postcodeData : mapData.data;
|
||||
|
|
@ -595,7 +597,7 @@ export default function MapPage({
|
|||
usePostcodeView={mapData.usePostcodeView}
|
||||
pois={[]}
|
||||
onViewChange={mapData.handleViewChange}
|
||||
viewFeature={viewFeature}
|
||||
viewFeature={mapViewFeature}
|
||||
colorRange={mapData.colorRange}
|
||||
filterRange={filterRange}
|
||||
viewSource={viewSource}
|
||||
|
|
@ -663,7 +665,7 @@ export default function MapPage({
|
|||
/>
|
||||
);
|
||||
|
||||
const renderFilters = () => (
|
||||
const renderFilters = (options?: { destinationDropdownPortal?: boolean }) => (
|
||||
<Filters
|
||||
features={features}
|
||||
filters={filters}
|
||||
|
|
@ -702,12 +704,71 @@ export default function MapPage({
|
|||
onClearAll={handleClearAll}
|
||||
onSaveSearch={onSaveSearch}
|
||||
savingSearch={savingSearch}
|
||||
destinationDropdownPortal={options?.destinationDropdownPortal}
|
||||
/>
|
||||
);
|
||||
|
||||
const renderMobileLegend = () => {
|
||||
if (mapViewFeature && mapData.colorRange) {
|
||||
if (mapViewFeature.startsWith('tt_')) {
|
||||
return (
|
||||
<MapLegend
|
||||
featureLabel={t('travel.travelTime', {
|
||||
mode: modes.label(
|
||||
mapViewFeature.split('_')[1] as 'car' | 'bicycle' | 'walking' | 'transit'
|
||||
),
|
||||
})}
|
||||
range={mapData.colorRange}
|
||||
showCancel={viewSource === 'eye'}
|
||||
onCancel={handleCancelPin}
|
||||
mode="feature"
|
||||
theme={theme}
|
||||
inline
|
||||
suffix=" min"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (mobileLegendMeta) {
|
||||
return (
|
||||
<MapLegend
|
||||
featureLabel={
|
||||
viewSource === 'eye'
|
||||
? t('mapLegend.previewing', { name: ts(mobileLegendMeta.name) })
|
||||
: ts(mobileLegendMeta.name)
|
||||
}
|
||||
range={mapData.colorRange}
|
||||
showCancel={viewSource === 'eye'}
|
||||
onCancel={handleCancelPin}
|
||||
mode="feature"
|
||||
enumValues={mobileLegendMeta.type === 'enum' ? mobileLegendMeta.values : undefined}
|
||||
featureName={mobileLegendMeta.name}
|
||||
theme={theme}
|
||||
inline
|
||||
raw={mobileLegendMeta.raw}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<MapLegend
|
||||
featureLabel={densityLabel}
|
||||
range={mobileDensityRange}
|
||||
showCancel={false}
|
||||
onCancel={handleCancelPin}
|
||||
mode="density"
|
||||
theme={theme}
|
||||
inline
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
if (isMobile) {
|
||||
return (
|
||||
<div className="flex-1 flex flex-col overflow-hidden relative touch-pan-y">
|
||||
<div className="flex-1 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">
|
||||
|
|
@ -719,14 +780,14 @@ export default function MapPage({
|
|||
</div>
|
||||
)}
|
||||
|
||||
<div ref={mobileMapRef} className="relative overflow-hidden">
|
||||
<div className="absolute inset-0">
|
||||
<Map
|
||||
data={mapData.data}
|
||||
postcodeData={mapData.postcodeData}
|
||||
usePostcodeView={mapData.usePostcodeView}
|
||||
pois={pois}
|
||||
onViewChange={mapData.handleViewChange}
|
||||
viewFeature={viewFeature}
|
||||
viewFeature={mapViewFeature}
|
||||
colorRange={mapData.colorRange}
|
||||
filterRange={filterRange}
|
||||
viewSource={viewSource}
|
||||
|
|
@ -748,90 +809,39 @@ export default function MapPage({
|
|||
hideLegend
|
||||
travelTimeEntries={entries}
|
||||
/>
|
||||
{mapData.loading && (
|
||||
<div className="absolute inset-0 flex items-center justify-center pointer-events-none">
|
||||
<div className="flex items-center gap-2 bg-white/90 dark:bg-warm-800/90 px-4 py-2.5 rounded-lg shadow-lg pointer-events-auto">
|
||||
<SpinnerIcon className="w-5 h-5 text-teal-600 dark:text-teal-400 animate-spin" />
|
||||
<span className="text-sm font-medium text-warm-700 dark:text-warm-200">
|
||||
Loading...
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<button
|
||||
onClick={() => setPoiPaneOpen((p) => !p)}
|
||||
className={`absolute bottom-2 right-2 z-10 p-2 rounded-lg shadow-lg bg-white dark:bg-warm-800 ${poiPaneOpen ? 'text-teal-600 dark:text-teal-400' : 'text-warm-500 dark:text-warm-400 hover:text-teal-600 dark:hover:text-teal-400'}`}
|
||||
>
|
||||
<MapPinIcon className="w-5 h-5" />
|
||||
</button>
|
||||
{poiPaneOpen && (
|
||||
<div className="absolute bottom-12 right-2 z-10 w-[calc(100%-1rem)] max-h-[60%] overflow-hidden flex flex-col rounded-lg shadow-xl border border-warm-200 dark:border-warm-700 bg-white dark:bg-warm-900">
|
||||
{renderPOIPane()}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="relative z-10 py-2 -my-2 cursor-row-resize touch-none group"
|
||||
{...mobileResizeHandlers}
|
||||
>
|
||||
<div className="h-3 flex items-center justify-center bg-warm-100 dark:bg-navy-800 group-hover:bg-warm-200 dark:group-hover:bg-navy-700 border-y border-warm-200 dark:border-navy-700">
|
||||
<div className="flex flex-row gap-1.5">
|
||||
<div className="w-1 h-1 rounded-full bg-warm-300 dark:bg-navy-600 group-hover:bg-warm-400 dark:group-hover:bg-navy-500" />
|
||||
<div className="w-1 h-1 rounded-full bg-warm-300 dark:bg-navy-600 group-hover:bg-warm-400 dark:group-hover:bg-navy-500" />
|
||||
<div className="w-1 h-1 rounded-full bg-warm-300 dark:bg-navy-600 group-hover:bg-warm-400 dark:group-hover:bg-navy-500" />
|
||||
{mapData.loading && (
|
||||
<div className="absolute inset-0 z-10 flex items-center justify-center pointer-events-none">
|
||||
<div className="flex items-center gap-2 bg-white/90 dark:bg-warm-800/90 px-4 py-2.5 rounded-lg shadow-lg pointer-events-auto">
|
||||
<SpinnerIcon className="w-5 h-5 text-teal-600 dark:text-teal-400 animate-spin" />
|
||||
<span className="text-sm font-medium text-warm-700 dark:text-warm-200">
|
||||
Loading...
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex-1 min-h-0 bg-white dark:bg-warm-900 overflow-hidden flex flex-col">
|
||||
{viewFeature && mapData.colorRange ? (
|
||||
viewFeature.startsWith('tt_') ? (
|
||||
<MapLegend
|
||||
featureLabel={t('travel.travelTime', {
|
||||
mode: modes.label(
|
||||
viewFeature.split('_')[1] as 'car' | 'bicycle' | 'walking' | 'transit'
|
||||
),
|
||||
})}
|
||||
range={mapData.colorRange}
|
||||
showCancel={viewSource === 'eye'}
|
||||
onCancel={handleCancelPin}
|
||||
mode="feature"
|
||||
theme={theme}
|
||||
inline
|
||||
suffix=" min"
|
||||
/>
|
||||
) : mobileLegendMeta ? (
|
||||
<MapLegend
|
||||
featureLabel={
|
||||
viewSource === 'eye'
|
||||
? t('mapLegend.previewing', { name: ts(mobileLegendMeta.name) })
|
||||
: ts(mobileLegendMeta.name)
|
||||
}
|
||||
range={mapData.colorRange}
|
||||
showCancel={viewSource === 'eye'}
|
||||
onCancel={handleCancelPin}
|
||||
mode="feature"
|
||||
enumValues={mobileLegendMeta.type === 'enum' ? mobileLegendMeta.values : undefined}
|
||||
featureName={mobileLegendMeta.name}
|
||||
theme={theme}
|
||||
inline
|
||||
raw={mobileLegendMeta.raw}
|
||||
/>
|
||||
) : null
|
||||
) : (
|
||||
<MapLegend
|
||||
featureLabel={densityLabel}
|
||||
range={mobileDensityRange}
|
||||
showCancel={false}
|
||||
onCancel={handleCancelPin}
|
||||
mode="density"
|
||||
theme={theme}
|
||||
inline
|
||||
/>
|
||||
)}
|
||||
<div className="flex-1 min-h-0">{renderFilters()}</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setPoiPaneOpen((p) => !p)}
|
||||
className={`absolute top-3 right-3 z-20 p-2 rounded-lg shadow-lg bg-white dark:bg-warm-800 ${poiPaneOpen ? 'text-teal-600 dark:text-teal-400' : 'text-warm-500 dark:text-warm-400 hover:text-teal-600 dark:hover:text-teal-400'}`}
|
||||
aria-label={t('poiPane.pointsOfInterest')}
|
||||
>
|
||||
<MapPinIcon className="w-5 h-5" />
|
||||
</button>
|
||||
|
||||
{poiPaneOpen && (
|
||||
<div className="absolute top-14 right-3 left-3 z-20 max-h-[45dvh] overflow-hidden flex flex-col rounded-lg shadow-xl border border-warm-200 dark:border-warm-700 bg-white dark:bg-warm-900">
|
||||
{renderPOIPane()}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<MobileBottomSheet
|
||||
activeCount={Object.keys(filters).length + entries.length}
|
||||
legend={renderMobileLegend()}
|
||||
>
|
||||
{renderFilters({ destinationDropdownPortal: false })}
|
||||
</MobileBottomSheet>
|
||||
|
||||
{mobileDrawerOpen && selectedHexagon && (
|
||||
<MobileDrawer
|
||||
|
|
@ -914,7 +924,7 @@ export default function MapPage({
|
|||
usePostcodeView={mapData.usePostcodeView}
|
||||
pois={pois}
|
||||
onViewChange={mapData.handleViewChange}
|
||||
viewFeature={viewFeature}
|
||||
viewFeature={mapViewFeature}
|
||||
colorRange={mapData.colorRange}
|
||||
filterRange={filterRange}
|
||||
viewSource={viewSource}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue