This commit is contained in:
Andras Schmelczer 2026-05-14 22:07:14 +01:00
parent 084117cea8
commit a8de0a614d
36 changed files with 1329 additions and 522 deletions

View file

@ -69,6 +69,7 @@ interface MapProps {
bounds?: Bounds | null;
hideLegend?: boolean;
hideLocationSearch?: boolean;
hideTopCardsWhenNarrow?: boolean;
travelTimeEntries?: TravelTimeEntry[];
densityLabel?: string;
totalCount?: number;
@ -82,6 +83,17 @@ interface Dimensions {
height: number;
}
const DESKTOP_TOP_CARD_WIDTH = 300;
const DESKTOP_TOP_CARD_GAP = 8;
const DESKTOP_TOP_CARD_HORIZONTAL_INSET = 24;
const DESKTOP_TOP_CARDS_STACKED_MIN_MAP_WIDTH =
DESKTOP_TOP_CARD_WIDTH + DESKTOP_TOP_CARD_HORIZONTAL_INSET;
const DESKTOP_TOP_CARDS_ROW_MIN_MAP_WIDTH =
DESKTOP_TOP_CARD_WIDTH * 2 + DESKTOP_TOP_CARD_GAP + DESKTOP_TOP_CARD_HORIZONTAL_INSET;
const DESKTOP_TOP_CARD_CLASS = 'w-[300px]';
const DESKTOP_LOCATION_SEARCH_INPUT_CLASS =
'px-2 py-2 text-sm w-full border-none outline-none bg-transparent text-warm-700 dark:text-warm-200 placeholder-warm-400 dark:placeholder-warm-500';
type MapContainerStyle = CSSProperties & {
'--map-mobile-bottom-inset'?: string;
};
@ -208,6 +220,23 @@ function getRenderedViewState(map: MapRef | null): ViewState | null {
};
}
function getRenderedVisibleCenter(
map: MapRef | null,
dimensions: Dimensions,
bottomScreenInset: number
): Pick<ViewState, 'latitude' | 'longitude'> | null {
if (!map || dimensions.width <= 0 || dimensions.height <= 0) return null;
const visibleBottomInset = clamp(bottomScreenInset, 0, dimensions.height);
const visibleCenterY = (dimensions.height - visibleBottomInset) / 2;
const center = map.unproject([dimensions.width / 2, visibleCenterY]);
return {
longitude: center.lng,
latitude: center.lat,
};
}
function DeckOverlay({
layers,
getTooltip,
@ -260,6 +289,7 @@ export default memo(function Map({
bounds: viewportBounds,
hideLegend = false,
hideLocationSearch = false,
hideTopCardsWhenNarrow = false,
travelTimeEntries = EMPTY_TRAVEL_ENTRIES,
densityLabel: densityLabelProp,
totalCount: totalCountProp,
@ -319,6 +349,9 @@ export default memo(function Map({
const dataBoundsHeight = dimensions.height + Math.max(0, bottomScreenInset);
const bounds = getBoundsFromViewState(renderedViewState, dimensions.width, dataBoundsHeight);
const resolution = zoomToResolution(renderedViewState.zoom);
const renderedVisibleCenter =
getRenderedVisibleCenter(mapRef.current, dimensions, bottomScreenInset) ??
renderedViewState;
onViewChange({
resolution,
@ -326,6 +359,8 @@ export default memo(function Map({
zoom: renderedViewState.zoom,
latitude: renderedViewState.latitude,
longitude: renderedViewState.longitude,
visibleLatitude: renderedVisibleCenter.latitude,
visibleLongitude: renderedVisibleCenter.longitude,
});
};
frame = window.requestAnimationFrame(emit);
@ -389,6 +424,19 @@ export default memo(function Map({
() => (bottomScreenInset > 0 ? { '--map-mobile-bottom-inset': `${bottomScreenInset}px` } : {}),
[bottomScreenInset]
);
const hideDesktopTopCardsForWidth =
hideTopCardsWhenNarrow &&
dimensions.width > 0 &&
dimensions.width < DESKTOP_TOP_CARDS_STACKED_MIN_MAP_WIDTH;
const stackDesktopTopCards =
hideTopCardsWhenNarrow &&
dimensions.width >= DESKTOP_TOP_CARDS_STACKED_MIN_MAP_WIDTH &&
dimensions.width < DESKTOP_TOP_CARDS_ROW_MIN_MAP_WIDTH;
const showLocationSearch = !hideLocationSearch && !hideDesktopTopCardsForWidth;
const showLegend = !hideLegend && !hideDesktopTopCardsForWidth;
const desktopTopCardsLayoutClass = stackDesktopTopCards
? 'flex-col items-start'
: 'items-start justify-between';
const {
layers,
@ -452,11 +500,11 @@ export default memo(function Map({
<div className="absolute inset-0 z-20 pointer-events-none flex flex-col">
{/* Center: Logo card with hero text */}
<div className="flex-1 flex items-center justify-center">
<div className="flex items-center gap-8 bg-navy-900/90 rounded-3xl px-14 py-10">
<LogoIcon className="w-24 h-24 text-teal-400" />
<div className="flex items-center gap-8 bg-navy-900/90 rounded-3xl px-14 py-10 max-w-[1040px]">
<LogoIcon className="w-24 h-24 shrink-0 text-teal-400" />
<span
className="font-bold text-white whitespace-nowrap"
style={{ fontSize: '5rem' }}
className="font-bold text-white/50"
style={{ fontSize: '4rem', lineHeight: 1.05, maxWidth: '760px' }}
>
{t('map.ogTitle')}
</span>
@ -494,74 +542,83 @@ export default memo(function Map({
) : null
) : (
<>
<div className="absolute top-3 left-3 right-3 z-20 flex flex-wrap items-start justify-between gap-2 pointer-events-none">
{!hideLocationSearch && (
<LocationSearch
onFlyTo={handleFlyTo}
onLocationSearched={onLocationSearched}
onCurrentLocationFound={onCurrentLocationFound}
onMouseEnter={handleMouseLeave}
/>
)}
{!hideLegend &&
(viewFeature && colorRange ? (
viewFeature.startsWith('tt_') ? (
<MapLegend
featureLabel={t('travel.travelTime', {
mode: modes.label(
viewFeature.split('_')[1] as 'car' | 'bicycle' | 'walking' | 'transit'
),
})}
range={colorRange}
showCancel={viewSource === 'eye'}
onCancel={onCancelPin}
onResetScale={viewSource === 'eye' ? onResetPreviewScale : undefined}
resetScaleDisabled={!canResetPreviewScale}
mode="feature"
theme={theme}
suffix=" min"
/>
) : colorFeatureMeta ? (
<MapLegend
featureLabel={
viewSource === 'eye'
? t('mapLegend.previewing', { name: ts(colorFeatureMeta.name) })
: ts(colorFeatureMeta.name)
}
range={colorRange}
showCancel={viewSource === 'eye'}
onCancel={onCancelPin}
onResetScale={viewSource === 'eye' ? onResetPreviewScale : undefined}
resetScaleDisabled={!canResetPreviewScale}
mode="feature"
enumValues={
colorFeatureMeta.type === 'enum' ? colorFeatureMeta.values : undefined
}
featureName={colorFeatureMeta.name}
theme={theme}
suffix={colorFeatureMeta.suffix}
raw={colorFeatureMeta.raw}
/>
) : null
) : (
<MapLegend
featureLabel={densityLabel}
range={
usePostcodeView
? [postcodeCountRange.min, postcodeCountRange.max]
: [countRange.min, countRange.max]
}
totalCount={
totalCountProp ??
(usePostcodeView ? postcodeCountRange.total : countRange.total)
}
showCancel={false}
onCancel={onCancelPin}
mode="density"
theme={theme}
{(showLocationSearch || showLegend) && (
<div
className={`absolute top-3 left-3 right-3 z-20 flex gap-2 pointer-events-none ${desktopTopCardsLayoutClass}`}
>
{showLocationSearch && (
<LocationSearch
onFlyTo={handleFlyTo}
onLocationSearched={onLocationSearched}
onCurrentLocationFound={onCurrentLocationFound}
onMouseEnter={handleMouseLeave}
className={DESKTOP_TOP_CARD_CLASS}
inputClassName={DESKTOP_LOCATION_SEARCH_INPUT_CLASS}
/>
))}
</div>
)}
{showLegend &&
(viewFeature && colorRange ? (
viewFeature.startsWith('tt_') ? (
<MapLegend
featureLabel={t('travel.travelTime', {
mode: modes.label(
viewFeature.split('_')[1] as 'car' | 'bicycle' | 'walking' | 'transit'
),
})}
range={colorRange}
showCancel={viewSource === 'eye'}
onCancel={onCancelPin}
onResetScale={viewSource === 'eye' ? onResetPreviewScale : undefined}
resetScaleDisabled={!canResetPreviewScale}
mode="feature"
theme={theme}
suffix=" min"
className={DESKTOP_TOP_CARD_CLASS}
/>
) : colorFeatureMeta ? (
<MapLegend
featureLabel={
viewSource === 'eye'
? t('mapLegend.previewing', { name: ts(colorFeatureMeta.name) })
: ts(colorFeatureMeta.name)
}
range={colorRange}
showCancel={viewSource === 'eye'}
onCancel={onCancelPin}
onResetScale={viewSource === 'eye' ? onResetPreviewScale : undefined}
resetScaleDisabled={!canResetPreviewScale}
mode="feature"
enumValues={
colorFeatureMeta.type === 'enum' ? colorFeatureMeta.values : undefined
}
featureName={colorFeatureMeta.name}
theme={theme}
suffix={colorFeatureMeta.suffix}
raw={colorFeatureMeta.raw}
className={DESKTOP_TOP_CARD_CLASS}
/>
) : null
) : (
<MapLegend
featureLabel={densityLabel}
range={
usePostcodeView
? [postcodeCountRange.min, postcodeCountRange.max]
: [countRange.min, countRange.max]
}
totalCount={
totalCountProp ??
(usePostcodeView ? postcodeCountRange.total : countRange.total)
}
showCancel={false}
onCancel={onCancelPin}
mode="density"
theme={theme}
className={DESKTOP_TOP_CARD_CLASS}
/>
))}
</div>
)}
{popupInfo && (
<div
className="pointer-events-none absolute bg-white dark:bg-warm-800 rounded-lg shadow-lg text-sm dark:text-white"