sus
This commit is contained in:
parent
920119ff48
commit
badab57438
3 changed files with 189 additions and 33 deletions
|
|
@ -6,7 +6,7 @@ import HexCanvas from './HexCanvas';
|
|||
import HomeFinalCta from './HomeFinalCta';
|
||||
import BottomIllustration from './BottomIllustration';
|
||||
import { TickerValue } from '../ui/TickerValue';
|
||||
import { ChevronIcon, LogoIcon, PlayIcon } from '../ui/icons';
|
||||
import { CheckIcon, ChevronIcon, LogoIcon, PlayIcon } from '../ui/icons';
|
||||
import { trackEvent } from '../../lib/analytics';
|
||||
import { apiUrl } from '../../lib/api';
|
||||
|
||||
|
|
@ -17,7 +17,7 @@ const HOME_SECTION_HEADING_CLASS =
|
|||
'text-2xl md:text-3xl font-bold text-navy-950 dark:text-warm-100';
|
||||
const HOME_BODY_CLASS = 'text-base leading-relaxed text-warm-600 dark:text-warm-400';
|
||||
const HOME_PRIMARY_BUTTON_CLASS =
|
||||
'border border-[#d27a11] bg-[#f09a22] text-navy-950 rounded-lg font-semibold hover:bg-[#df8614] transition-colors text-base shadow-lg shadow-[#7a3905]/25 text-center';
|
||||
'border border-[#d27a11] bg-[#f09a22] text-navy-950 rounded-lg font-semibold hover:bg-[#df8614] transition-colors text-base shadow-lg shadow-[#7a3905]/25 text-center focus:outline-none focus-visible:ring-4 focus-visible:ring-teal-300/80';
|
||||
const PRODUCT_DEMO_VIDEO_BY_LANGUAGE: Record<string, string> = {
|
||||
en: 'recording',
|
||||
de: 'recording-de',
|
||||
|
|
@ -40,7 +40,7 @@ function getProductDemoSlug(language: string | undefined, isMobile: boolean): st
|
|||
const code = language?.toLowerCase().split('-')[0] ?? 'en';
|
||||
const base = PRODUCT_DEMO_VIDEO_BY_LANGUAGE[code] ?? PRODUCT_DEMO_VIDEO_BY_LANGUAGE.en;
|
||||
// Mobile cuts (9:16, 540x960) are published as `<base>-mobile` alongside
|
||||
// the 16:9 desktop cuts. The recorder pipeline writes both every render —
|
||||
// the 16:9 desktop cuts. The recorder pipeline writes both every render:
|
||||
// see video/src/storyboard.ts.
|
||||
return isMobile ? `${base}-mobile` : base;
|
||||
}
|
||||
|
|
@ -238,6 +238,93 @@ function PriceStrip({
|
|||
);
|
||||
}
|
||||
|
||||
const TWIN_SECTION_ID = 'twin-proof';
|
||||
|
||||
/**
|
||||
* "Cheaper twin" proof block: real, verified figures (not illustrative).
|
||||
* Median £/sqm for Land Registry "Flats/Maisonettes" sold since 2021, derived from
|
||||
* epc_pp.parquet; school/commute parity checked against postcode.parquet + modelled
|
||||
* transit times to central London. Sector grain (N1 1 / N7 9) is LOAD-BEARING. Do
|
||||
* not relabel as bare N1/N7: the gap collapses to ~14% at outward-district level.
|
||||
* Re-run the query periodically as new sales land; figures drift slightly over time.
|
||||
*/
|
||||
function TwinProof({ onOpenDashboard }: { onOpenDashboard: () => void }) {
|
||||
const { t } = useTranslation();
|
||||
const pricey = { name: 'Angel', sector: 'N1 1', perSqm: 11356 };
|
||||
const value = { name: 'Holloway', sector: 'N7 9', perSqm: 7943 };
|
||||
const deltaPct = Math.round((1 - value.perSqm / pricey.perSqm) * 100);
|
||||
const fmt = (n: number) => `£${n.toLocaleString('en-GB')}`;
|
||||
|
||||
return (
|
||||
<div id={TWIN_SECTION_ID} className={`${HOME_SECTION_CONTAINER_CLASS} pt-12 md:pt-20 pb-2`}>
|
||||
<div className="mx-auto max-w-4xl rounded-2xl border border-warm-200 bg-white/90 p-6 shadow-sm dark:border-warm-700 dark:bg-warm-800/90 md:p-8">
|
||||
<div className="mb-3 flex flex-wrap items-center gap-x-3 gap-y-2">
|
||||
<span className="rounded-full bg-teal-50 px-3 py-1 text-xs font-bold uppercase tracking-wide text-teal-700 dark:bg-teal-900/30 dark:text-teal-300">
|
||||
{t('home.twinBadge')}
|
||||
</span>
|
||||
<h2 className={HOME_SECTION_HEADING_CLASS}>{t('home.twinTitle')}</h2>
|
||||
</div>
|
||||
<p className={`${HOME_BODY_CLASS} mb-6 max-w-2xl`}>{t('home.twinIntro')}</p>
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
<div className="rounded-xl border border-warm-200 bg-warm-50 p-5 dark:border-warm-700 dark:bg-navy-950/40">
|
||||
<div className="text-xs font-semibold uppercase tracking-wide text-warm-500 dark:text-warm-400">
|
||||
{t('home.twinPriceyLabel')}
|
||||
</div>
|
||||
<div className="mt-1 text-2xl font-extrabold text-navy-950 dark:text-warm-100">
|
||||
{pricey.name}{' '}
|
||||
<span className="text-base font-semibold text-warm-500 dark:text-warm-400">
|
||||
{pricey.sector}
|
||||
</span>
|
||||
</div>
|
||||
<div className="mt-2 text-lg font-bold tabular-nums text-warm-700 dark:text-warm-300">
|
||||
{t('home.twinPerSqm', { value: fmt(pricey.perSqm) })}
|
||||
</div>
|
||||
</div>
|
||||
<div className="rounded-xl border-2 border-teal-400 bg-teal-50/60 p-5 dark:border-teal-500 dark:bg-teal-900/20">
|
||||
<div className="text-xs font-semibold uppercase tracking-wide text-teal-700 dark:text-teal-300">
|
||||
{t('home.twinValueLabel')}
|
||||
</div>
|
||||
<div className="mt-1 text-2xl font-extrabold text-navy-950 dark:text-warm-100">
|
||||
{value.name}{' '}
|
||||
<span className="text-base font-semibold text-teal-700/70 dark:text-teal-300/70">
|
||||
{value.sector}
|
||||
</span>
|
||||
</div>
|
||||
<div className="mt-2 text-lg font-bold tabular-nums text-teal-700 dark:text-teal-300">
|
||||
{t('home.twinPerSqm', { value: fmt(value.perSqm) })}
|
||||
</div>
|
||||
<div className="mt-3 text-sm font-bold tabular-nums text-teal-700 dark:text-teal-300">
|
||||
{t('home.twinDelta', { percent: `${deltaPct}%` })}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-5 flex flex-col gap-2 text-sm font-medium text-warm-600 dark:text-warm-400 sm:flex-row sm:gap-6">
|
||||
<span className="flex items-center gap-2">
|
||||
<CheckIcon className="h-4 w-4 shrink-0 text-teal-600 dark:text-teal-400" />
|
||||
{t('home.twinSameSchool')}
|
||||
</span>
|
||||
<span className="flex items-center gap-2">
|
||||
<CheckIcon className="h-4 w-4 shrink-0 text-teal-600 dark:text-teal-400" />
|
||||
{t('home.twinSameCommute')}
|
||||
</span>
|
||||
</div>
|
||||
<p className="mt-4 text-xs italic text-warm-500 dark:text-warm-400">
|
||||
{t('home.twinFootnote')}
|
||||
</p>
|
||||
<button
|
||||
onClick={() => {
|
||||
trackEvent('CTA Click', { location: 'twin', label: 'explore_map' });
|
||||
onOpenDashboard();
|
||||
}}
|
||||
className={`mt-5 w-full sm:w-auto px-6 py-3 ${HOME_PRIMARY_BUTTON_CLASS}`}
|
||||
>
|
||||
{t('home.twinCta')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function HomePage({
|
||||
onOpenDashboard,
|
||||
onOpenPricing,
|
||||
|
|
@ -326,7 +413,7 @@ export default function HomePage({
|
|||
// Scroll depth tracking
|
||||
const scrolledSections = useRef(new Set<string>());
|
||||
useEffect(() => {
|
||||
const ids = ['how-it-works'];
|
||||
const ids = ['comparison'];
|
||||
const observers: IntersectionObserver[] = [];
|
||||
ids.forEach((id) => {
|
||||
const el = document.getElementById(id);
|
||||
|
|
@ -352,8 +439,8 @@ export default function HomePage({
|
|||
return () => clearTimeout(timer);
|
||||
}, []);
|
||||
|
||||
const scrollToProductDemoVideo = () => {
|
||||
const target = document.getElementById(PRODUCT_DEMO_SECTION_ID);
|
||||
const scrollToSection = (sectionId: string) => {
|
||||
const target = document.getElementById(sectionId);
|
||||
if (!target) return;
|
||||
const scroller = target.closest('.overflow-y-auto') as HTMLElement | null;
|
||||
if (!scroller) return;
|
||||
|
|
@ -402,37 +489,38 @@ export default function HomePage({
|
|||
<p className="text-base md:text-lg text-warm-200 mb-8 max-w-xl">
|
||||
{highlightBrandText(t('home.heroDescription'), 'font-semibold text-teal-300')}
|
||||
</p>
|
||||
<div className="flex flex-col sm:flex-row sm:items-center gap-3 sm:gap-4 mb-5">
|
||||
<div className="flex flex-col sm:flex-row sm:items-center gap-3 sm:gap-4 mb-3">
|
||||
<button
|
||||
onClick={() => {
|
||||
trackEvent('CTA Click', { location: 'hero', label: 'explore_map' });
|
||||
onOpenDashboard();
|
||||
}}
|
||||
className={`px-7 py-3.5 ${HOME_PRIMARY_BUTTON_CLASS}`}
|
||||
className={`w-full sm:w-auto px-7 py-3.5 ${HOME_PRIMARY_BUTTON_CLASS}`}
|
||||
>
|
||||
{t('home.exploreTheMap')}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
trackEvent('CTA Click', { location: 'hero', label: 'see_difference' });
|
||||
scrollToProductDemoVideo();
|
||||
scrollToSection(TWIN_SECTION_ID);
|
||||
}}
|
||||
className="px-7 py-3 border-2 border-teal-400 text-teal-400 rounded-lg font-semibold hover:bg-teal-400/10 transition-colors text-base text-center"
|
||||
className="w-full sm:w-auto px-7 py-3 border-2 border-teal-400 text-teal-400 rounded-lg font-semibold hover:bg-teal-400/10 transition-colors text-base text-center focus:outline-none focus-visible:ring-4 focus-visible:ring-teal-300/60"
|
||||
>
|
||||
{t('home.seeTheDifference')}
|
||||
</button>
|
||||
</div>
|
||||
<p className="text-sm font-medium text-teal-200 mb-4">{t('home.freeToExplore')}</p>
|
||||
<PriceStrip onOpenPricing={onOpenPricing} hidePricing={hidePricing} />
|
||||
<p className="text-sm text-warm-400 mb-8">{t('home.coverageNote')}</p>
|
||||
<p className="text-sm text-warm-400 mb-6">{t('home.coverageNote')}</p>
|
||||
<div className="home-hero-stats flex flex-wrap pt-3 border-t border-white/10">
|
||||
<div className="home-hero-stat">
|
||||
<div className="home-hero-stat-value">
|
||||
<div className="home-hero-stat-value tabular-nums">
|
||||
<TickerValue text="13M" active={statsActive} />
|
||||
</div>
|
||||
<div className="home-hero-stat-label">{t('home.statProperties')}</div>
|
||||
</div>
|
||||
<div className="home-hero-stat">
|
||||
<div className="home-hero-stat-value">
|
||||
<div className="home-hero-stat-value tabular-nums">
|
||||
<TickerValue text="40+" active={statsActive} />
|
||||
</div>
|
||||
<div className="home-hero-stat-label">{t('home.statFilters')}</div>
|
||||
|
|
@ -442,6 +530,10 @@ export default function HomePage({
|
|||
<div className="home-hero-stat-label">{t('home.statPostcodeInEngland')}</div>
|
||||
</div>
|
||||
</div>
|
||||
<p className="mt-5 text-xs font-medium uppercase tracking-wide text-warm-400">
|
||||
<span className="text-warm-300">{t('home.dataSourcesLabel')}:</span>{' '}
|
||||
{t('home.dataSources')}
|
||||
</p>
|
||||
</div>
|
||||
<Suspense fallback={<ProductShowcaseFallback />}>
|
||||
<ProductShowcase />
|
||||
|
|
@ -454,7 +546,7 @@ export default function HomePage({
|
|||
aria-label={t('home.scrollToProductDemo')}
|
||||
onClick={() => {
|
||||
trackEvent('CTA Click', { location: 'hero_chevron', label: 'scroll_down' });
|
||||
scrollToProductDemoVideo();
|
||||
scrollToSection(TWIN_SECTION_ID);
|
||||
}}
|
||||
>
|
||||
<ChevronIcon direction="down" className="h-14 w-14" />
|
||||
|
|
@ -463,7 +555,8 @@ export default function HomePage({
|
|||
|
||||
<div ref={homeSurfaceRef} className="home-content-surface relative overflow-hidden">
|
||||
<div className="relative z-10">
|
||||
<ProductDemoVideo />
|
||||
{/* Concrete proof of the "cheaper twin" claim, high on the page */}
|
||||
<TwinProof onOpenDashboard={onOpenDashboard} />
|
||||
|
||||
{/* Our philosophy */}
|
||||
<div className={`${HOME_SECTION_CONTAINER_CLASS} pt-12 md:pt-20 pb-4`}>
|
||||
|
|
@ -509,12 +602,9 @@ export default function HomePage({
|
|||
</div>
|
||||
|
||||
{/* Comparison table */}
|
||||
<div
|
||||
id="how-it-works"
|
||||
className={`${HOME_SECTION_CONTAINER_CLASS} pt-10 md:pt-16 pb-2`}
|
||||
>
|
||||
<div id="comparison" className={`${HOME_SECTION_CONTAINER_CLASS} pt-10 md:pt-16 pb-2`}>
|
||||
<div ref={whyRef} className="fade-in-section">
|
||||
<div id="comparison">
|
||||
<div>
|
||||
<h2
|
||||
className={`${HOME_SECTION_HEADING_CLASS} mb-6 flex flex-wrap items-center justify-center gap-x-2 gap-y-1 text-center md:mb-10 md:gap-x-3`}
|
||||
>
|
||||
|
|
@ -547,24 +637,35 @@ export default function HomePage({
|
|||
subtitle: t('home.compSearchWithoutSub'),
|
||||
postcode: false,
|
||||
guides: false,
|
||||
perfect: true,
|
||||
},
|
||||
{
|
||||
feature: t('home.compAreaData'),
|
||||
subtitle: t('home.compAreaDataSub'),
|
||||
postcode: true,
|
||||
guides: true,
|
||||
perfect: true,
|
||||
},
|
||||
{
|
||||
feature: t('home.compPropertyData'),
|
||||
subtitle: t('home.compPropertyDataSub'),
|
||||
postcode: false,
|
||||
guides: true,
|
||||
perfect: true,
|
||||
},
|
||||
{
|
||||
feature: t('home.compFilters'),
|
||||
subtitle: t('home.compFiltersSub'),
|
||||
postcode: false,
|
||||
guides: false,
|
||||
perfect: true,
|
||||
},
|
||||
{
|
||||
feature: t('home.compListings'),
|
||||
subtitle: t('home.compListingsSub'),
|
||||
postcode: true,
|
||||
guides: false,
|
||||
perfect: false,
|
||||
},
|
||||
].map((row, i, arr) => (
|
||||
<tr
|
||||
|
|
@ -597,11 +698,15 @@ export default function HomePage({
|
|||
);
|
||||
})}
|
||||
<td
|
||||
aria-label={t('common.yes')}
|
||||
className="px-1.5 md:px-3 py-2.5 md:py-3.5 text-center text-base text-green-500 bg-teal-50 dark:bg-teal-900/30"
|
||||
aria-label={row.perfect ? t('common.yes') : t('common.no')}
|
||||
className={`px-1.5 md:px-3 py-2.5 md:py-3.5 text-center text-base bg-teal-50 dark:bg-teal-900/30 ${
|
||||
row.perfect ? 'text-green-500' : 'text-red-500'
|
||||
}`}
|
||||
>
|
||||
<span aria-hidden="true">✓</span>
|
||||
<span className="sr-only">{t('common.yes')}</span>
|
||||
<span aria-hidden="true">{row.perfect ? '✓' : '✗'}</span>
|
||||
<span className="sr-only">
|
||||
{row.perfect ? t('common.yes') : t('common.no')}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
|
|
@ -612,6 +717,10 @@ export default function HomePage({
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{/* Full product walkthrough, kept as a later proof beat, after the
|
||||
differentiation, instead of competing with the showcase in the hero */}
|
||||
<ProductDemoVideo />
|
||||
|
||||
{/* The real cost CTA */}
|
||||
<div className={`${HOME_SECTION_CONTAINER_CLASS} pt-8 md:pt-14 pb-12`}>
|
||||
<div ref={ctaRef} className="fade-in-section">
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue