alright
This commit is contained in:
parent
c645b0f1d4
commit
39ef5c6646
79 changed files with 5660 additions and 2199 deletions
|
|
@ -33,6 +33,7 @@ import {
|
|||
MAP_BOUNDS,
|
||||
POI_GROUP_COLORS,
|
||||
POSTCODE_ZOOM_THRESHOLD,
|
||||
POI_AUTO_CARD_ZOOM_THRESHOLD,
|
||||
} from '../../lib/consts';
|
||||
import LocationSearch, { type SearchedLocation } from './LocationSearch';
|
||||
import MapLegend from './MapLegend';
|
||||
|
|
@ -104,6 +105,15 @@ function formatListingHeadline(listing: ActualListing, t: TFunction): string | n
|
|||
return parts.length > 0 ? parts.join(' · ') : null;
|
||||
}
|
||||
|
||||
interface PoiPopupCardData {
|
||||
name: string;
|
||||
category: string;
|
||||
icon_category?: string;
|
||||
group: string;
|
||||
emoji: string;
|
||||
school?: SchoolMetadata;
|
||||
}
|
||||
|
||||
interface Dimensions {
|
||||
width: number;
|
||||
height: number;
|
||||
|
|
@ -289,10 +299,16 @@ function renderSchoolMetadata(school: SchoolMetadata) {
|
|||
)}
|
||||
{school.fsm_percent !== undefined && (
|
||||
<>
|
||||
<dt className="text-warm-500 dark:text-warm-400">FSM</dt>
|
||||
<dt className="text-warm-500 dark:text-warm-400">Free meal</dt>
|
||||
<dd className="dark:text-warm-200">{school.fsm_percent.toFixed(1)}%</dd>
|
||||
</>
|
||||
)}
|
||||
{school.ofsted_rating && (
|
||||
<>
|
||||
<dt className="text-warm-500 dark:text-warm-400">Ofsted</dt>
|
||||
<dd className="dark:text-warm-200">{school.ofsted_rating}</dd>
|
||||
</>
|
||||
)}
|
||||
{school.sixth_form === 'Has a sixth form' && (
|
||||
<>
|
||||
<dt className="text-warm-500 dark:text-warm-400">Sixth form</dt>
|
||||
|
|
@ -358,6 +374,36 @@ function renderSchoolMetadata(school: SchoolMetadata) {
|
|||
);
|
||||
}
|
||||
|
||||
function PoiPopupCardContent({ poi }: { poi: PoiPopupCardData }) {
|
||||
return (
|
||||
<div className="px-3 py-2 max-w-[280px]">
|
||||
<div className="flex items-center gap-2">
|
||||
<img
|
||||
src={getPoiIconUrl(poi.category, poi.emoji, poi.icon_category, poi.name)}
|
||||
alt=""
|
||||
aria-hidden="true"
|
||||
loading="lazy"
|
||||
referrerPolicy="no-referrer"
|
||||
className="h-5 w-5 shrink-0 rounded-[4px] bg-white object-contain p-0.5"
|
||||
/>
|
||||
<div className="min-w-0">
|
||||
<div className="font-semibold dark:text-warm-100">{poi.name}</div>
|
||||
<div className="flex items-center gap-1.5 text-xs text-warm-500 dark:text-warm-400">
|
||||
<span
|
||||
className="inline-block w-2 h-2 rounded-full flex-shrink-0"
|
||||
style={{
|
||||
backgroundColor: `rgb(${getPoiGroupColor(poi.group).join(',')})`,
|
||||
}}
|
||||
/>
|
||||
{ts(poi.category)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{poi.school && renderSchoolMetadata(poi.school)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function getRenderedViewState(map: MapRef | null): ViewState | null {
|
||||
if (!map) return null;
|
||||
|
||||
|
|
@ -575,6 +621,7 @@ export default memo(function Map({
|
|||
const densityLabel = densityLabelProp ?? t('mapLegend.numberOfProperties');
|
||||
const [internalViewState, setInternalViewState] = useState<ViewState>(initialViewState);
|
||||
const [dimensions, setDimensions] = useState<Dimensions>({ width: 0, height: 0 });
|
||||
const [mapReady, setMapReady] = useState(false);
|
||||
|
||||
// In screenshot mode, use the prop directly for instant updates (no async lag)
|
||||
const viewState = screenshotMode ? initialViewState : internalViewState;
|
||||
|
|
@ -664,6 +711,10 @@ export default memo(function Map({
|
|||
if (screenshotMode) window.__map_idle = true;
|
||||
}, [screenshotMode]);
|
||||
|
||||
const handleLoad = useCallback(() => {
|
||||
setMapReady(true);
|
||||
}, []);
|
||||
|
||||
const handleFlyTo = useCallback(
|
||||
(lat: number, lng: number, zoom: number, options?: MapFlyToOptions) => {
|
||||
setInternalViewState((prev) => {
|
||||
|
|
@ -715,6 +766,7 @@ export default memo(function Map({
|
|||
layers,
|
||||
popupInfo,
|
||||
clearPopupInfo,
|
||||
visiblePois,
|
||||
listingPopup,
|
||||
clearListingPopup,
|
||||
hoverPosition,
|
||||
|
|
@ -744,6 +796,31 @@ export default memo(function Map({
|
|||
travelTimeEntries,
|
||||
});
|
||||
|
||||
const showAutoPoiCards = !screenshotMode && viewState.zoom >= POI_AUTO_CARD_ZOOM_THRESHOLD;
|
||||
const autoPoiCards = useMemo(() => {
|
||||
const map = mapRef.current;
|
||||
if (!showAutoPoiCards || !mapReady || !map || dimensions.width <= 0 || dimensions.height <= 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return visiblePois.flatMap((poi) => {
|
||||
const point = map.project([poi.lng, poi.lat]);
|
||||
if (
|
||||
!Number.isFinite(point.x) ||
|
||||
!Number.isFinite(point.y) ||
|
||||
point.x < 0 ||
|
||||
point.x > dimensions.width ||
|
||||
point.y < 0 ||
|
||||
point.y > dimensions.height
|
||||
) {
|
||||
return [];
|
||||
}
|
||||
return [{ poi, x: point.x, y: point.y }];
|
||||
});
|
||||
// viewState isn't read directly but drives map.project — recompute when the camera moves.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [showAutoPoiCards, mapReady, visiblePois, dimensions, viewState]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`flex-1 h-full relative ${bottomScreenInset > 0 ? 'map-has-mobile-bottom-sheet' : ''}`}
|
||||
|
|
@ -755,7 +832,7 @@ export default memo(function Map({
|
|||
ref={mapRef}
|
||||
{...viewState}
|
||||
onMove={handleMove}
|
||||
onLoad={undefined}
|
||||
onLoad={handleLoad}
|
||||
onIdle={handleIdle}
|
||||
mapStyle={mapStyle}
|
||||
style={{ width: '100%', height: '100%' }}
|
||||
|
|
@ -896,14 +973,28 @@ export default memo(function Map({
|
|||
))}
|
||||
</div>
|
||||
)}
|
||||
{popupInfo && (
|
||||
{autoPoiCards.map(({ poi, x, y }) => (
|
||||
<div
|
||||
key={poi.id}
|
||||
className="pointer-events-none absolute bg-white dark:bg-warm-800 rounded-lg shadow-lg text-sm dark:text-white"
|
||||
style={{
|
||||
left: x,
|
||||
top: y - 12,
|
||||
transform: 'translate(-50%, -100%)',
|
||||
zIndex: 9,
|
||||
}}
|
||||
>
|
||||
<PoiPopupCardContent poi={poi} />
|
||||
</div>
|
||||
))}
|
||||
{popupInfo && (!showAutoPoiCards || popupInfo.isCluster) && (
|
||||
<div
|
||||
className="pointer-events-none absolute bg-white dark:bg-warm-800 rounded-lg shadow-lg text-sm dark:text-white"
|
||||
style={{
|
||||
left: popupInfo.x,
|
||||
top: popupInfo.y - 50,
|
||||
transform: 'translateX(-50%)',
|
||||
zIndex: 9999,
|
||||
zIndex: 30,
|
||||
}}
|
||||
>
|
||||
<button
|
||||
|
|
@ -922,36 +1013,7 @@ export default memo(function Map({
|
|||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="px-3 py-2 max-w-[280px]">
|
||||
<div className="flex items-center gap-2">
|
||||
<img
|
||||
src={getPoiIconUrl(
|
||||
popupInfo.category,
|
||||
popupInfo.emoji,
|
||||
popupInfo.icon_category,
|
||||
popupInfo.name
|
||||
)}
|
||||
alt=""
|
||||
aria-hidden="true"
|
||||
loading="lazy"
|
||||
referrerPolicy="no-referrer"
|
||||
className="h-5 w-5 shrink-0 rounded-[4px] bg-white object-contain p-0.5"
|
||||
/>
|
||||
<div>
|
||||
<div className="font-semibold dark:text-warm-100">{popupInfo.name}</div>
|
||||
<div className="flex items-center gap-1.5 text-xs text-warm-500 dark:text-warm-400">
|
||||
<span
|
||||
className="inline-block w-2 h-2 rounded-full flex-shrink-0"
|
||||
style={{
|
||||
backgroundColor: `rgb(${getPoiGroupColor(popupInfo.group).join(',')})`,
|
||||
}}
|
||||
/>
|
||||
{ts(popupInfo.category)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{popupInfo.school && renderSchoolMetadata(popupInfo.school)}
|
||||
</div>
|
||||
<PoiPopupCardContent poi={popupInfo} />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -962,7 +1024,7 @@ export default memo(function Map({
|
|||
left: listingPopup.x,
|
||||
top: listingPopup.y - 12,
|
||||
transform: 'translate(-50%, -100%)',
|
||||
zIndex: 9999,
|
||||
zIndex: 30,
|
||||
}}
|
||||
onMouseLeave={clearListingPopup}
|
||||
>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue