.
This commit is contained in:
parent
909e241907
commit
1ee796b282
29 changed files with 250 additions and 126 deletions
|
|
@ -12,6 +12,7 @@ import type { TFunction } from 'i18next';
|
|||
import { useTranslation } from 'react-i18next';
|
||||
import { cellToLatLng, polygonToCells } from 'h3-js';
|
||||
import PriceHistoryChart from '../map/PriceHistoryChart';
|
||||
import { MapErrorBoundary } from '../map/MapErrorBoundary';
|
||||
import StackedBarChart from '../map/StackedBarChart';
|
||||
import JourneyInstructions, { type JourneyInstructionPreset } from '../map/JourneyInstructions';
|
||||
import { DualHistogram } from '../map/DualHistogram';
|
||||
|
|
@ -214,10 +215,13 @@ const SHOWCASE_MAP_START_VIEW: ViewState = {
|
|||
bearing: 0,
|
||||
};
|
||||
|
||||
// End the Match fly-in over Greater London so the "match" step lands on the same
|
||||
// place the Inspect (SW5 9AA) and Scout (SW5/SE22/N4) steps then drill into. The
|
||||
// single-search narrative used to break by zooming to Birmingham then showing London.
|
||||
const SHOWCASE_MAP_END_VIEW: ViewState = {
|
||||
longitude: -1.89,
|
||||
latitude: 52.49,
|
||||
zoom: 8.72,
|
||||
longitude: -0.12,
|
||||
latitude: 51.51,
|
||||
zoom: 9.05,
|
||||
pitch: 0,
|
||||
bearing: 0,
|
||||
};
|
||||
|
|
@ -260,7 +264,12 @@ function buildShowcaseMapData(): HexagonData[] {
|
|||
}
|
||||
|
||||
const SHOWCASE_MAP_DATA = buildShowcaseMapData();
|
||||
const SHOWCASE_MAP_TOTAL_COUNT = SHOWCASE_MAP_DATA.reduce((sum, item) => sum + item.count, 0);
|
||||
// Count only the hexes around Greater London so the Match card's figure matches the
|
||||
// area it has zoomed to, instead of mislabelling the England-wide total as one city.
|
||||
const SHOWCASE_LONDON_COUNT = SHOWCASE_MAP_DATA.reduce((sum, item) => {
|
||||
const nearLondon = Math.abs(item.lat - 51.5072) < 0.55 && Math.abs(item.lon + 0.1276) < 0.85;
|
||||
return nearLondon ? sum + item.count : sum;
|
||||
}, 0);
|
||||
const EMPTY_SHOWCASE_POSTCODES: PostcodeFeature[] = [];
|
||||
const EMPTY_SHOWCASE_POIS: POI[] = [];
|
||||
|
||||
|
|
@ -601,37 +610,42 @@ function EnglandHexMapScreen({ isActive }: { isActive: boolean }) {
|
|||
return (
|
||||
<div className="pointer-events-none relative h-full overflow-hidden bg-warm-100 dark:bg-navy-950/50">
|
||||
{shouldRenderMap && (
|
||||
<Suspense fallback={<div className="h-full bg-navy-950/40" aria-hidden="true" />}>
|
||||
<ProductMap
|
||||
data={SHOWCASE_MAP_DATA}
|
||||
postcodeData={EMPTY_SHOWCASE_POSTCODES}
|
||||
usePostcodeView={false}
|
||||
pois={EMPTY_SHOWCASE_POIS}
|
||||
onViewChange={noopViewChange}
|
||||
viewFeature={null}
|
||||
colorRange={null}
|
||||
filterRange={null}
|
||||
viewSource={null}
|
||||
onCancelPin={() => {}}
|
||||
features={DEMO_FEATURES}
|
||||
selectedHexagonId={null}
|
||||
hoveredHexagonId={null}
|
||||
onHexagonClick={noopHexagonClick}
|
||||
onHexagonHover={noopHexagonHover}
|
||||
initialViewState={viewState}
|
||||
theme="dark"
|
||||
screenshotMode
|
||||
hideLegend
|
||||
densityLabel={t('home.showcaseMatchingHomesLabel')}
|
||||
totalCount={SHOWCASE_MAP_TOTAL_COUNT}
|
||||
/>
|
||||
</Suspense>
|
||||
// The full deck.gl/maplibre map sits above the fold; contain any WebGL
|
||||
// context-loss crash to this pane (and auto-recover) instead of letting it
|
||||
// bubble to the top-level boundary and blank the hero.
|
||||
<MapErrorBoundary>
|
||||
<Suspense fallback={<div className="h-full bg-navy-950/40" aria-hidden="true" />}>
|
||||
<ProductMap
|
||||
data={SHOWCASE_MAP_DATA}
|
||||
postcodeData={EMPTY_SHOWCASE_POSTCODES}
|
||||
usePostcodeView={false}
|
||||
pois={EMPTY_SHOWCASE_POIS}
|
||||
onViewChange={noopViewChange}
|
||||
viewFeature={null}
|
||||
colorRange={null}
|
||||
filterRange={null}
|
||||
viewSource={null}
|
||||
onCancelPin={() => {}}
|
||||
features={DEMO_FEATURES}
|
||||
selectedHexagonId={null}
|
||||
hoveredHexagonId={null}
|
||||
onHexagonClick={noopHexagonClick}
|
||||
onHexagonHover={noopHexagonHover}
|
||||
initialViewState={viewState}
|
||||
theme="dark"
|
||||
screenshotMode
|
||||
hideLegend
|
||||
densityLabel={t('home.showcaseMatchingHomesLabel')}
|
||||
totalCount={SHOWCASE_LONDON_COUNT}
|
||||
/>
|
||||
</Suspense>
|
||||
</MapErrorBoundary>
|
||||
)}
|
||||
<div className="pointer-events-none absolute bottom-4 left-4 max-w-[16rem] rounded-md border border-white/15 bg-navy-950/95 px-4 py-3 text-white shadow-2xl shadow-navy-950/35">
|
||||
<div className="text-sm font-black leading-none">Birmingham</div>
|
||||
<div className="text-sm font-black leading-none">{t('home.showcaseStep2Region')}</div>
|
||||
<div className="mt-1 text-xs font-bold leading-tight text-warm-200">
|
||||
{t('home.showcaseMatchingHomes', {
|
||||
value: SHOWCASE_MAP_TOTAL_COUNT.toLocaleString(),
|
||||
value: SHOWCASE_LONDON_COUNT.toLocaleString(),
|
||||
})}
|
||||
</div>
|
||||
<div className="mt-2 text-[10px] font-bold uppercase tracking-wide text-warm-400">
|
||||
|
|
@ -789,19 +803,22 @@ function ScoutScreen({ isActive }: { isActive: boolean }) {
|
|||
postcode: 'SW5 9AA',
|
||||
score: '94%',
|
||||
commute: t('home.showcaseMinutes', { count: 23 }),
|
||||
price: '£492k',
|
||||
perSqm: '£8,200',
|
||||
delta: '−16%',
|
||||
},
|
||||
{
|
||||
postcode: 'SE22 8EF',
|
||||
score: '91%',
|
||||
commute: t('home.showcaseMinutes', { count: 28 }),
|
||||
price: '£518k',
|
||||
perSqm: '£5,900',
|
||||
delta: '−24%',
|
||||
},
|
||||
{
|
||||
postcode: 'N4 2AB',
|
||||
score: '88%',
|
||||
commute: t('home.showcaseMinutes', { count: 31 }),
|
||||
price: '£476k',
|
||||
perSqm: '£6,400',
|
||||
delta: '−21%',
|
||||
},
|
||||
];
|
||||
|
||||
|
|
@ -934,7 +951,12 @@ function ScoutScreen({ isActive }: { isActive: boolean }) {
|
|||
<div className="truncate border-r border-warm-200 px-2 py-1.5 dark:border-navy-700 sm:px-3 sm:py-2.5">
|
||||
{row.commute}
|
||||
</div>
|
||||
<div className="truncate px-2 py-1.5 font-black sm:px-3 sm:py-2.5">{row.price}</div>
|
||||
<div className="px-2 py-1.5 sm:px-3 sm:py-2.5">
|
||||
<div className="truncate font-black tabular-nums">{row.perSqm}</div>
|
||||
<div className="text-[10px] font-bold tabular-nums text-emerald-600 dark:text-emerald-300">
|
||||
{row.delta}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
|
@ -1031,6 +1053,7 @@ export default function ProductShowcase({ className = '' }: ProductShowcaseProps
|
|||
const [isStagePaused, setIsStagePaused] = useState(false);
|
||||
const [hasStarted, setHasStarted] = useState(false);
|
||||
const [canPauseOnHover, setCanPauseOnHover] = useState(false);
|
||||
const [prefersReducedMotion, setPrefersReducedMotion] = useState(false);
|
||||
const showcaseRef = useRef<HTMLDivElement | null>(null);
|
||||
const inspectUserScrolledRef = useRef(false);
|
||||
|
||||
|
|
@ -1101,6 +1124,27 @@ export default function ProductShowcase({ className = '' }: ProductShowcaseProps
|
|||
return () => mediaQuery.removeEventListener('change', updateCanPause);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window.matchMedia !== 'function') return;
|
||||
const mediaQuery = window.matchMedia('(prefers-reduced-motion: reduce)');
|
||||
const update = () => setPrefersReducedMotion(mediaQuery.matches);
|
||||
update();
|
||||
mediaQuery.addEventListener('change', update);
|
||||
return () => mediaQuery.removeEventListener('change', update);
|
||||
}, []);
|
||||
|
||||
// Under prefers-reduced-motion the progress-bar animation is disabled in CSS, so
|
||||
// its onAnimationEnd (which normally advances the carousel) never fires and the
|
||||
// demo would freeze on step 1. Drive the advance from a timer in that case.
|
||||
useEffect(() => {
|
||||
if (!prefersReducedMotion || !isProgressRunning) return;
|
||||
const timer = window.setTimeout(
|
||||
() => setActiveStep((step) => (step + 1) % SHOWCASE_STEP_COUNT),
|
||||
activeStepIntervalMs
|
||||
);
|
||||
return () => window.clearTimeout(timer);
|
||||
}, [prefersReducedMotion, isProgressRunning, activeStep, activeStepIntervalMs]);
|
||||
|
||||
const pauseForHover = () => {
|
||||
if (canPauseOnHover) setIsStagePaused(true);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -235,6 +235,7 @@ export interface HistoricalPrice {
|
|||
year: number;
|
||||
month: number;
|
||||
price: number;
|
||||
is_new: boolean;
|
||||
}
|
||||
|
||||
export interface Property {
|
||||
|
|
@ -386,7 +387,7 @@ export interface HexagonStatsResponse {
|
|||
* sector, shown alongside the national average for each crime metric. */
|
||||
crime_area_averages?: CrimeAreaAverage[];
|
||||
/** Total individual crime records (last 7 years) across the selection's
|
||||
* postcodes — the count behind the "individual crimes" list. */
|
||||
* postcodes, the count behind the "individual crimes" list. */
|
||||
crime_total_records?: number;
|
||||
central_postcode?: string;
|
||||
/** Total usual residents (ONS Census 2021) across the postcodes in this
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue