This commit is contained in:
Andras Schmelczer 2026-05-06 22:40:46 +01:00
parent 28323f145e
commit 94f9c0d594
76 changed files with 3238 additions and 1230 deletions

View file

@ -49,6 +49,7 @@ const SHOWCASE_STEP_COUNT = 4;
const SHOWCASE_INTERVAL_MS = 5200;
const FILTER_ANIMATION_MS = 5000;
const INSPECT_SCROLL_ANIMATION_MS = 4600;
const SCOUT_TABLE_REVEAL_MS = 2400;
const BRAND_NAME = 'Perfect Postcode';
const BRAND_TEXT_CLASS = 'text-teal-600 dark:text-teal-400';
const HOME_SECTION_CONTAINER_CLASS = 'max-w-7xl mx-auto px-6 md:px-10';
@ -262,19 +263,6 @@ const VOTE_SHARE_SEGMENTS = [
{ name: '% Other parties', value: 4 },
];
const HOUSE_PRICE_BREAKDOWN = [
{ label: 'Flats', value: '£612k', helper: '146 sold' },
{ label: 'Terraces', value: '£1.04m', helper: '38 sold' },
{ label: 'Semis', value: '£1.22m', helper: '14 sold' },
{ label: '£/sq ft', value: '£872', helper: 'local median' },
];
const PRICE_SIGNALS = [
['5-year change', '+22%'],
['Last 12 months', '+2.1%'],
['Days to sell', '41'],
];
const INSPECT_TRAVEL_ENTRIES: TravelTimeEntry[] = [
{
mode: 'transit',
@ -821,36 +809,6 @@ function RightPaneOnlyScreen({
</span>
</div>
<PriceHistoryChart points={PRICE_POINTS} />
<div className="mt-3 grid grid-cols-2 gap-2">
{HOUSE_PRICE_BREAKDOWN.map((item) => (
<div
key={item.label}
className="rounded-md bg-white px-2.5 py-2 shadow-sm shadow-navy-950/5 dark:bg-navy-900/60 sm:px-3"
>
<div className="text-xs font-bold uppercase text-warm-400">
{item.label}
</div>
<div className="mt-0.5 flex items-baseline justify-between gap-2">
<span className="text-sm font-black text-navy-950 dark:text-warm-100">
{item.value}
</span>
<span className="text-xs font-semibold text-warm-500">
{item.helper}
</span>
</div>
</div>
))}
</div>
<div className="mt-3 grid grid-cols-3 divide-x divide-warm-200 rounded-md bg-white text-center shadow-sm shadow-navy-950/5 dark:divide-navy-700 dark:bg-navy-900/60">
{PRICE_SIGNALS.map(([label, value]) => (
<div key={label} className="px-2 py-2">
<div className="text-xs font-bold uppercase text-warm-400">{label}</div>
<div className="mt-0.5 text-xs font-black text-navy-950 dark:text-warm-100">
{value}
</div>
</div>
))}
</div>
</div>
<div className="rounded-lg bg-warm-50 p-3 dark:bg-navy-950/50 sm:p-4">
<div className="mb-2 flex items-center gap-2 text-sm font-semibold text-warm-700 dark:text-warm-300">
@ -914,101 +872,169 @@ function RightPaneOnlyScreen({
);
}
function ScoutScreen() {
function ScoutScreen({ isActive }: { isActive: boolean }) {
const { t } = useTranslation();
const [isTableRevealed, setIsTableRevealed] = useState(false);
const scoutRows = [
{ postcode: 'SW5 9AA', score: '94%', commute: '23 min', price: '£492k' },
{ postcode: 'SE22 8EF', score: '91%', commute: '28 min', price: '£518k' },
{ postcode: 'N4 2AB', score: '88%', commute: '31 min', price: '£476k' },
];
useEffect(() => {
if (!isActive) {
setIsTableRevealed(false);
return;
}
const prefersReducedMotion =
typeof window.matchMedia === 'function' &&
window.matchMedia('(prefers-reduced-motion: reduce)').matches;
if (prefersReducedMotion) {
setIsTableRevealed(true);
return;
}
setIsTableRevealed(false);
const timer = window.setTimeout(() => setIsTableRevealed(true), SCOUT_TABLE_REVEAL_MS);
return () => window.clearTimeout(timer);
}, [isActive]);
return (
<div className="relative flex h-full min-h-0 flex-col overflow-hidden bg-[#f7f3ed] p-3 dark:bg-navy-950/45 sm:p-5">
<div className="relative z-10 mt-5 rounded-lg bg-white p-3 shadow-2xl shadow-navy-950/10 dark:bg-navy-900/65 dark:backdrop-blur-sm sm:mt-9 sm:p-5">
<div className="grid grid-cols-2 gap-2 sm:gap-4">
<div className="cursor-default select-none rounded-lg border border-warm-200 bg-warm-50 p-3 shadow-sm dark:border-navy-700 dark:bg-navy-950/50 sm:p-4">
<div className="flex items-center gap-3">
<span className="flex h-8 w-8 shrink-0 items-center justify-center rounded-md bg-teal-50 text-teal-700 dark:bg-teal-900/30 dark:text-teal-300 sm:h-10 sm:w-10">
<ClipboardIcon className="h-4 w-4 sm:h-5 sm:w-5" />
</span>
<div className="min-w-0">
<div className="text-sm font-black text-navy-950 dark:text-warm-100 sm:text-base">
Share
</div>
<div className="mt-0.5 hidden truncate text-xs font-semibold text-warm-500 dark:text-warm-400 sm:block">
Send the shortlist
<div
className={`relative flex h-full min-h-0 flex-col overflow-hidden bg-[#f7f3ed] p-3 dark:bg-navy-950/45 sm:p-5 ${
isActive ? 'scout-screen-active' : ''
}`}
>
<div className="my-auto w-full shrink-0">
<div className="relative z-10 shrink-0 rounded-lg bg-white p-2 shadow-2xl shadow-navy-950/10 dark:bg-navy-900/65 dark:backdrop-blur-sm sm:p-5">
<div className="grid grid-cols-2 gap-2 sm:gap-4">
<div className="cursor-default select-none rounded-lg border border-warm-200 bg-warm-50 p-2 shadow-sm dark:border-navy-700 dark:bg-navy-950/50 sm:p-4">
<div className="flex items-center gap-3">
<span className="flex h-7 w-7 shrink-0 items-center justify-center rounded-md bg-teal-50 text-teal-700 dark:bg-teal-900/30 dark:text-teal-300 sm:h-10 sm:w-10">
<ClipboardIcon className="h-4 w-4 sm:h-5 sm:w-5" />
</span>
<div className="min-w-0">
<div className="text-sm font-black text-navy-950 dark:text-warm-100 sm:text-base">
Share
</div>
<div className="mt-0.5 hidden truncate text-xs font-semibold text-warm-500 dark:text-warm-400 sm:block">
Send the shortlist
</div>
</div>
</div>
</div>
</div>
<div className="scout-export-action relative cursor-default select-none overflow-hidden rounded-lg border border-teal-300 bg-teal-600 p-3 text-white shadow-lg shadow-teal-900/20 dark:border-teal-500 dark:bg-teal-500 dark:text-navy-950 sm:p-4">
<span className="scout-export-ripple" aria-hidden="true" />
<div className="relative flex items-center gap-2 sm:gap-3">
<span className="flex h-8 w-8 shrink-0 items-center justify-center rounded-md bg-white/15 text-white dark:bg-navy-950/10 dark:text-navy-950 sm:h-10 sm:w-10">
<DownloadIcon className="h-4 w-4 sm:h-5 sm:w-5" />
</span>
<div className="min-w-0">
<div className="text-sm font-black sm:text-base">Export</div>
<div className="mt-0.5 hidden truncate text-xs font-semibold text-teal-50 dark:text-navy-800 sm:block">
Download .xlsx
<div className="scout-export-action relative cursor-default select-none overflow-hidden rounded-lg border border-teal-300 bg-teal-600 p-2 text-white shadow-lg shadow-teal-900/20 dark:border-teal-500 dark:bg-teal-500 dark:text-navy-950 sm:p-4">
<span className="scout-export-ripple" aria-hidden="true" />
<div className="relative flex items-center gap-2 sm:gap-3">
<span className="flex h-7 w-7 shrink-0 items-center justify-center rounded-md bg-white/15 text-white dark:bg-navy-950/10 dark:text-navy-950 sm:h-10 sm:w-10">
<DownloadIcon className="h-4 w-4 sm:h-5 sm:w-5" />
</span>
<div className="min-w-0">
<div className="text-sm font-black sm:text-base">Export</div>
<div className="mt-0.5 hidden truncate text-xs font-semibold text-teal-50 dark:text-navy-800 sm:block">
Download .xlsx
</div>
</div>
<span className="scout-export-check ml-auto hidden h-6 w-6 shrink-0 items-center justify-center rounded-full bg-white text-teal-700 shadow-sm dark:bg-navy-950 dark:text-teal-300 sm:flex">
<CheckIcon className="h-3.5 w-3.5" />
</span>
</div>
<span className="scout-export-check ml-auto hidden h-6 w-6 shrink-0 items-center justify-center rounded-full bg-white text-teal-700 shadow-sm dark:bg-navy-950 dark:text-teal-300 sm:flex">
<CheckIcon className="h-3.5 w-3.5" />
</span>
</div>
</div>
</div>
</div>
<svg
className="pointer-events-none absolute left-14 top-[8.75rem] z-20 h-36 w-20 text-teal-500/80 dark:text-teal-300/80 sm:left-12 sm:top-[11rem] sm:h-56 sm:w-72"
viewBox="0 0 100 100"
preserveAspectRatio="none"
aria-hidden="true"
>
<path
d="M50 4 L50 92"
fill="none"
stroke="currentColor"
strokeWidth="1.8"
strokeDasharray="4 4"
/>
<polygon
points="50,92 44.5,84 55.5,84"
className="fill-teal-500 dark:fill-teal-300"
/>
</svg>
<svg
className="pointer-events-none absolute right-14 top-[8.75rem] z-20 h-36 w-20 text-teal-500/80 dark:text-teal-300/80 sm:right-12 sm:top-[11rem] sm:h-56 sm:w-72"
viewBox="0 0 100 100"
preserveAspectRatio="none"
aria-hidden="true"
>
<path
d="M50 4 L50 92"
fill="none"
stroke="currentColor"
strokeWidth="1.8"
strokeDasharray="4 4"
/>
<polygon
points="50,92 44.5,84 55.5,84"
className="fill-teal-500 dark:fill-teal-300"
/>
</svg>
<div className="relative z-10 mt-auto rounded-lg bg-navy-950/55 p-4 text-white shadow-2xl shadow-navy-950/20 backdrop-blur-sm sm:p-5">
<div className="text-lg font-black leading-tight">
{t('home.showcaseStep4Conclusion')}
<div
className={`overflow-hidden transition-all duration-500 ease-out ${
isTableRevealed
? 'mt-3 max-h-64 opacity-100 sm:mt-4'
: 'mt-0 max-h-0 opacity-0'
}`}
aria-hidden={!isTableRevealed}
>
<div className="relative z-10 shrink-0 overflow-hidden rounded-lg border border-warm-200 bg-white shadow-xl shadow-navy-950/10 ring-1 ring-white/80 dark:border-navy-700 dark:bg-navy-900/70 dark:ring-white/5 dark:backdrop-blur-sm">
<div className="flex items-center justify-between gap-3 border-b border-warm-200 bg-gradient-to-r from-white via-emerald-50/80 to-white px-3 py-1.5 text-navy-950 dark:border-navy-700 dark:from-navy-900/90 dark:via-emerald-900/20 dark:to-navy-900/90 dark:text-warm-100 sm:px-4 sm:py-2">
<div className="flex min-w-0 items-center gap-2">
<span className="flex h-6 w-6 shrink-0 items-center justify-center rounded-md bg-emerald-600 text-white shadow-sm shadow-emerald-900/20 dark:bg-emerald-400 dark:text-navy-950 sm:h-7 sm:w-7">
<DownloadIcon className="h-3.5 w-3.5" />
</span>
<span className="min-w-0 truncate text-xs font-black sm:text-sm">
{t('home.showcaseStep4FileName')}
</span>
</div>
<span className="shrink-0 rounded-full border border-emerald-200 bg-emerald-50 px-2 py-0.5 text-[10px] font-black text-emerald-800 dark:border-emerald-500/30 dark:bg-emerald-500/10 dark:text-emerald-200 sm:text-xs">
Top 3
</span>
</div>
<div className="mt-4 grid gap-2 text-xs font-medium leading-relaxed text-warm-300 sm:mt-5 sm:gap-3 sm:text-sm">
<div className="grid grid-cols-[1.25fr_0.85fr_0.9fr_1fr] border-b border-warm-200 bg-warm-50 text-[10px] font-black uppercase text-warm-500 dark:border-navy-700 dark:bg-navy-950/45 dark:text-warm-400 sm:text-xs">
{[
'Walk the streets before the listing search narrows your options.',
'Test the commute from a real front door, not a borough name.',
'Compare viewings with evidence already in hand.',
].map((item) => (
<div key={item} className="flex gap-2">
<CheckIcon className="mt-0.5 h-4 w-4 shrink-0 text-teal-300" />
<span>{item}</span>
t('home.showcaseStep4ColPostcode'),
t('home.showcaseStep4ColScore'),
t('home.showcaseStep4ColCommute'),
t('home.showcaseStep4ColPrice'),
].map((heading) => (
<div
key={heading}
className="truncate border-r border-warm-200 px-2 py-1.5 last:border-r-0 dark:border-navy-700 sm:px-3 sm:py-2"
>
{heading}
</div>
))}
</div>
{scoutRows.map((row, index) => (
<div
key={row.postcode}
className={`grid grid-cols-[1.25fr_0.85fr_0.9fr_1fr] text-[11px] font-semibold text-navy-950 dark:text-warm-100 sm:text-sm ${
index % 2 === 0
? 'bg-white dark:bg-navy-900/55'
: 'bg-warm-50/70 dark:bg-navy-950/35'
}`}
>
<div className="flex min-w-0 items-center gap-1.5 border-r border-warm-200 px-2 py-1.5 dark:border-navy-700 sm:px-3 sm:py-2.5">
<span className="flex h-5 w-5 shrink-0 items-center justify-center rounded-md bg-emerald-50 text-[10px] font-black text-emerald-700 dark:bg-emerald-500/10 dark:text-emerald-300">
{index + 1}
</span>
<span className="truncate font-black">{row.postcode}</span>
</div>
<div className="min-w-0 border-r border-warm-200 px-2 py-1.5 dark:border-navy-700 sm:px-3 sm:py-2.5">
<div className="flex items-center gap-2">
<span className="shrink-0 font-black text-emerald-700 dark:text-emerald-300">
{row.score}
</span>
<span className="hidden h-1.5 min-w-0 flex-1 overflow-hidden rounded-full bg-warm-200 dark:bg-navy-700 sm:block">
<span
className="block h-full rounded-full bg-emerald-500 dark:bg-emerald-300"
style={{ width: row.score }}
/>
</span>
</div>
</div>
<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>
))}
</div>
</div>
<div className="relative z-10 mt-3 shrink-0 rounded-lg bg-navy-950/55 p-3 text-center text-white shadow-2xl shadow-navy-950/20 backdrop-blur-sm sm:mt-4 sm:p-4">
<div className="text-base font-black leading-tight">
{t('home.showcaseStep4Conclusion')}
</div>
<div className="mt-3 grid gap-1.5 text-xs font-medium leading-relaxed text-warm-300 sm:gap-2">
{[
'Walk the streets before the listing search narrows your options.',
'Test the commute from a real front door, not a borough name.',
'Compare viewings with evidence already in hand.',
].map((item) => (
<div key={item} className="flex justify-center gap-2">
<CheckIcon className="mt-0.5 h-4 w-4 shrink-0 text-teal-300" />
<span>{item}</span>
</div>
))}
</div>
</div>
</div>
</div>
);
@ -1031,7 +1057,7 @@ function DashboardShowcase({
isActive={activeStep === 2}
userScrolledRef={inspectUserScrolledRef}
/>,
<ScoutScreen key="scout" />,
<ScoutScreen key="scout" isActive={activeStep === 3} />,
];
const ActiveIcon = active.Icon;
const showStageHeader = activeStep !== 3;
@ -1112,14 +1138,14 @@ function HeroProductShowcase() {
return (
<div
className="dark relative w-full min-w-0 max-w-[58rem] justify-self-center lg:max-w-none lg:justify-self-stretch"
className="home-hero-showcase dark relative w-full min-w-0 max-w-[58rem] justify-self-center"
onMouseEnter={() => setIsStagePaused(true)}
onMouseLeave={() => setIsStagePaused(false)}
onFocus={() => setIsStagePaused(true)}
onBlur={() => setIsStagePaused(false)}
aria-label={t('home.showcaseContext')}
>
<div className="flex h-[34rem] flex-col overflow-hidden rounded-lg bg-navy-950/50 shadow-2xl shadow-black/40 backdrop-blur-sm ring-1 ring-white/10 sm:h-[46rem] md:h-[50rem] lg:h-[47rem] xl:h-[46rem]">
<div className="home-hero-showcase-frame flex h-[36rem] flex-col overflow-hidden rounded-lg bg-navy-950/50 shadow-2xl shadow-black/40 backdrop-blur-sm ring-1 ring-white/10 sm:h-[38rem] md:h-[40rem]">
<div className="shrink-0 bg-white/[0.035] p-1.5 sm:p-2 md:p-3">
<div className="grid grid-cols-4 gap-1 sm:gap-2">
{steps.map((step, index) => {
@ -1133,13 +1159,13 @@ function HeroProductShowcase() {
className={`rounded-md px-1.5 py-2 text-left transition-colors sm:px-3 ${
activeStep === index
? 'bg-teal-400/10 text-white shadow-sm'
: 'bg-white/[0.03] text-warm-400 hover:bg-white/[0.06] hover:text-warm-200'
: 'bg-white/[0.03] text-warm-200 hover:bg-white/[0.06] hover:text-white'
}`}
>
<div className="flex items-center justify-center gap-1 sm:justify-start sm:gap-2">
<Icon
className={`hidden h-4 w-4 sm:block ${
activeStep === index ? 'text-teal-300' : 'text-warm-500'
activeStep === index ? 'text-teal-300' : 'text-warm-300'
}`}
/>
<span className="text-[11px] font-bold sm:hidden">{index + 1}</span>
@ -1259,9 +1285,9 @@ export default function HomePage({
{/* Hero */}
<div className="relative overflow-hidden bg-gradient-to-b from-navy-950 via-navy-900 to-navy-900 dark:from-navy-950 dark:via-navy-900 dark:to-navy-950 min-h-[calc(100dvh-3rem)] flex flex-col">
<HexCanvas isDark={theme === 'dark'} animated={false} />
<div className="relative z-10 mx-auto flex w-full max-w-[104rem] flex-1 flex-col px-6 pb-8 pt-6 backdrop-blur-[2px] md:px-10 md:py-10 lg:py-12">
<div className="hero-roomy-lift grid flex-1 items-center gap-x-8 gap-y-6 lg:grid-cols-[minmax(0,0.82fr)_minmax(38rem,1.18fr)] lg:gap-12 xl:grid-cols-[minmax(0,0.78fr)_minmax(44rem,1.22fr)] xl:gap-16">
<div className="min-w-0 max-w-4xl lg:max-w-[42rem] xl:max-w-[45rem]">
<div className="home-hero-container relative z-10 mx-auto flex w-full max-w-[104rem] flex-1 flex-col px-6 pb-8 pt-6 backdrop-blur-[2px] md:px-10 md:py-10">
<div className="home-hero-layout hero-roomy-lift grid flex-1 items-center gap-x-8 gap-y-6">
<div className="home-hero-copy min-w-0 max-w-4xl">
<p className="text-sm font-semibold text-teal-300 mb-3">{t('home.heroEyebrow')}</p>
<h1 className="text-3xl md:text-5xl font-extrabold text-white mb-4 leading-[1.1]">
{t('home.heroTitle1')}{' '}
@ -1269,10 +1295,10 @@ export default function HomePage({
<br />
{t('home.heroTitle3')}
</h1>
<p className="text-base md:text-lg text-warm-300 mb-6 leading-relaxed max-w-xl">
<p className="text-base md:text-lg text-warm-100 mb-6 leading-relaxed max-w-xl">
{t('home.heroSubtitle')}
</p>
<p className="text-base md:text-lg text-warm-400 mb-8 max-w-xl">
<p className="text-base md:text-lg text-warm-200 mb-8 max-w-xl">
{highlightBrandText(t('home.heroDescription'))}
</p>
<div className="flex flex-col sm:flex-row sm:items-center gap-3 sm:gap-4 mb-10">
@ -1300,19 +1326,19 @@ export default function HomePage({
<div className="text-2xl md:text-3xl font-bold text-white">
<TickerValue text="13M" active={statsActive} />
</div>
<div className="text-sm text-warm-400">{t('home.statProperties')}</div>
<div className="text-sm text-warm-200">{t('home.statProperties')}</div>
</div>
<div>
<div className="text-2xl md:text-3xl font-bold text-white">
<TickerValue text="56" active={statsActive} />
</div>
<div className="text-sm text-warm-400">{t('home.statFilters')}</div>
<div className="text-sm text-warm-200">{t('home.statFilters')}</div>
</div>
<div>
<div className="text-2xl md:text-3xl font-bold text-white">
{t('home.statEvery')}
</div>
<div className="text-sm text-warm-400">{t('home.statPostcodeInEngland')}</div>
<div className="text-sm text-warm-200">{t('home.statPostcodeInEngland')}</div>
</div>
</div>
</div>