More
This commit is contained in:
parent
cd34ee693f
commit
05a1f316e1
58 changed files with 3113 additions and 1277 deletions
|
|
@ -1,4 +1,4 @@
|
|||
import { useState, useEffect, useRef } from 'react';
|
||||
import { useState, useEffect, useRef, type ReactNode } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useFadeInRef } from '../../hooks/useFadeIn';
|
||||
import HexCanvas from './HexCanvas';
|
||||
|
|
@ -7,6 +7,386 @@ import { TickerValue } from '../ui/TickerValue';
|
|||
import { LogoIcon } from '../ui/icons/LogoIcon';
|
||||
import { trackEvent } from '../../lib/analytics';
|
||||
|
||||
const SHOWCASE_STEP_COUNT = 4;
|
||||
const SHOWCASE_INTERVAL_MS = 4200;
|
||||
|
||||
function ProductMapFrame({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
<div className="relative h-full min-h-0 rounded-lg overflow-hidden bg-warm-900 border border-white/10">
|
||||
<div className="absolute inset-0 opacity-70">
|
||||
<div className="absolute left-[12%] top-[18%] w-[76%] h-[68%] rounded-[45%] border border-warm-500/25" />
|
||||
<div className="absolute left-[22%] top-[24%] w-[52%] h-[55%] rounded-[45%] border border-warm-500/20" />
|
||||
<div className="absolute left-[36%] top-[8%] h-[84%] w-px bg-warm-500/20 rotate-12" />
|
||||
<div className="absolute left-[18%] top-[49%] h-px w-[68%] bg-warm-500/20 -rotate-6" />
|
||||
<div className="absolute left-[48%] top-[16%] h-[72%] w-px bg-warm-500/20 -rotate-24" />
|
||||
<div className="absolute left-[4%] top-[34%] h-px w-[92%] bg-teal-400/10 rotate-12" />
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function DemoMapPin({
|
||||
name,
|
||||
detail,
|
||||
x,
|
||||
y,
|
||||
active = false,
|
||||
}: {
|
||||
name: string;
|
||||
detail: string;
|
||||
x: string;
|
||||
y: string;
|
||||
active?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className={`absolute -translate-x-1/2 -translate-y-1/2 ${active ? 'z-20' : 'z-10'}`}
|
||||
style={{ left: x, top: y }}
|
||||
>
|
||||
<div className="relative mx-auto w-4 h-4">
|
||||
<div
|
||||
className={`absolute inset-0 rounded-full ${
|
||||
active ? 'bg-coral-400 ring-8 ring-coral-400/20' : 'bg-teal-400 ring-4 ring-teal-400/15'
|
||||
}`}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className={`mt-2 w-32 rounded-md border px-2 py-1 shadow-lg ${
|
||||
active
|
||||
? 'border-coral-400/40 bg-navy-950/95 text-white'
|
||||
: 'border-white/10 bg-navy-950/85 text-warm-100'
|
||||
}`}
|
||||
>
|
||||
<div className="text-xs font-semibold leading-tight">{name}</div>
|
||||
<div className="text-[10px] leading-tight text-warm-400">{detail}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function HeroProductShowcase() {
|
||||
const { t } = useTranslation();
|
||||
const [activeStep, setActiveStep] = useState(0);
|
||||
const [isPaused, setIsPaused] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const prefersReducedMotion =
|
||||
typeof window !== 'undefined' &&
|
||||
window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
||||
if (isPaused || prefersReducedMotion) return;
|
||||
|
||||
const timer = window.setInterval(() => {
|
||||
setActiveStep((step) => (step + 1) % SHOWCASE_STEP_COUNT);
|
||||
}, SHOWCASE_INTERVAL_MS);
|
||||
|
||||
return () => window.clearInterval(timer);
|
||||
}, [isPaused]);
|
||||
|
||||
const steps = [
|
||||
{
|
||||
tab: t('home.showcaseStep1Tab'),
|
||||
title: t('home.showcaseStep1Title'),
|
||||
body: t('home.showcaseStep1Body'),
|
||||
},
|
||||
{
|
||||
tab: t('home.showcaseStep2Tab'),
|
||||
title: t('home.showcaseStep2Title'),
|
||||
body: t('home.showcaseStep2Body'),
|
||||
},
|
||||
{
|
||||
tab: t('home.showcaseStep3Tab'),
|
||||
title: t('home.showcaseStep3Title'),
|
||||
body: t('home.showcaseStep3Body'),
|
||||
},
|
||||
{
|
||||
tab: t('home.showcaseStep4Tab'),
|
||||
title: t('home.showcaseStep4Title'),
|
||||
body: t('home.showcaseStep4Body'),
|
||||
},
|
||||
];
|
||||
|
||||
const criteriaChips = [
|
||||
t('home.showcaseStep1Chip1'),
|
||||
t('home.showcaseStep1Chip2'),
|
||||
t('home.showcaseStep1Chip3'),
|
||||
t('home.showcaseStep1Chip4'),
|
||||
];
|
||||
|
||||
const knownAreas = ['Clapham', 'St Albans', 'Brighton'];
|
||||
const mapPins = [
|
||||
{ name: 'Penge', detail: t('home.showcaseMatchPenge'), x: '63%', y: '54%' },
|
||||
{ name: 'Abbey Wood', detail: t('home.showcaseMatchAbbeyWood'), x: '73%', y: '63%' },
|
||||
{ name: 'Totterdown', detail: t('home.showcaseMatchTotterdown'), x: '42%', y: '71%' },
|
||||
{ name: 'Walkley', detail: t('home.showcaseMatchWalkley'), x: '50%', y: '35%' },
|
||||
];
|
||||
const evidenceRows = [
|
||||
t('home.showcaseEvidence1'),
|
||||
t('home.showcaseEvidence2'),
|
||||
t('home.showcaseEvidence3'),
|
||||
t('home.showcaseEvidence4'),
|
||||
];
|
||||
const compareRows = [
|
||||
t('home.showcaseCompare1'),
|
||||
t('home.showcaseCompare2'),
|
||||
t('home.showcaseCompare3'),
|
||||
];
|
||||
const active = steps[activeStep];
|
||||
|
||||
const renderSidePanel = () => {
|
||||
if (activeStep === 0) {
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<div className="rounded-lg border border-white/10 bg-white/[0.04] p-3">
|
||||
<div className="text-[11px] font-semibold uppercase text-warm-400">
|
||||
{t('aiFilter.describeIdealArea')}
|
||||
</div>
|
||||
<div className="mt-2 text-sm leading-snug text-white">
|
||||
{t('home.showcaseStep1Prompt')}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{criteriaChips.map((chip) => (
|
||||
<span
|
||||
key={chip}
|
||||
className="rounded-full border border-teal-400/30 bg-teal-400/10 px-2 py-1 text-[11px] font-medium text-teal-200"
|
||||
>
|
||||
{chip}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (activeStep === 1) {
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<div className="rounded-lg border border-coral-400/30 bg-coral-400/10 p-3">
|
||||
<div className="text-2xl font-bold text-white">{t('home.showcaseStep2Metric')}</div>
|
||||
<div className="text-xs text-coral-100">{t('home.showcaseStep2Note')}</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-2 text-xs">
|
||||
<div className="rounded-lg border border-white/10 bg-white/[0.04] p-2">
|
||||
<div className="font-semibold text-warm-300">{t('home.showcaseKnownAreas')}</div>
|
||||
{knownAreas.map((area) => (
|
||||
<div key={area} className="mt-1 flex justify-between gap-2 text-warm-500">
|
||||
<span>{area}</span>
|
||||
<span>{t('home.showcaseKnownAreaStatus')}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="rounded-lg border border-teal-400/30 bg-teal-400/10 p-2">
|
||||
<div className="font-semibold text-teal-100">{t('home.showcaseNewMatches')}</div>
|
||||
{mapPins.slice(0, 3).map((pin) => (
|
||||
<div key={pin.name} className="mt-1 text-teal-200">
|
||||
{pin.name}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (activeStep === 2) {
|
||||
return (
|
||||
<div className="rounded-lg border border-white/10 bg-white/[0.04] p-3">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div>
|
||||
<div className="text-xs font-semibold text-warm-400">
|
||||
{t('home.showcaseStep3Postcode')}
|
||||
</div>
|
||||
<div className="mt-1 text-lg font-bold text-white">{t('home.showcaseStep3Area')}</div>
|
||||
</div>
|
||||
<div className="rounded-full bg-teal-400/15 px-2.5 py-1 text-xs font-semibold text-teal-200">
|
||||
{t('home.showcaseStep3Score')}
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-3 space-y-1.5">
|
||||
{evidenceRows.map((row) => (
|
||||
<div key={row} className="flex items-center gap-2 text-xs text-warm-300">
|
||||
<span className="w-4 h-4 rounded-full bg-teal-400/15 text-teal-200 flex items-center justify-center text-[10px]">
|
||||
✓
|
||||
</span>
|
||||
<span>{row}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
{compareRows.map((row, index) => (
|
||||
<div key={row} className="rounded-lg border border-white/10 bg-white/[0.04] p-2.5">
|
||||
<div className="flex items-start gap-2">
|
||||
<span className="mt-0.5 w-5 h-5 rounded-full bg-coral-400/15 text-coral-100 flex items-center justify-center text-xs font-bold">
|
||||
{index + 1}
|
||||
</span>
|
||||
<span className="min-w-0 flex-1 text-xs leading-snug text-warm-200">{row}</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<div className="text-xs font-semibold text-teal-200">{t('home.showcaseSaveLabel')}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const renderVisual = () => {
|
||||
if (activeStep === 0) {
|
||||
return (
|
||||
<ProductMapFrame>
|
||||
<div className="absolute inset-x-4 top-4 rounded-lg border border-white/10 bg-navy-950/90 p-3 shadow-xl">
|
||||
<div className="text-[10px] font-semibold uppercase text-warm-400">
|
||||
{t('home.showcaseStep1Tab')}
|
||||
</div>
|
||||
<div className="mt-2 rounded-md bg-white text-warm-800 px-3 py-2 text-xs leading-snug">
|
||||
{t('home.showcaseStep1Prompt')}
|
||||
</div>
|
||||
</div>
|
||||
<div className="absolute inset-x-4 bottom-4 flex flex-wrap gap-2">
|
||||
{criteriaChips.map((chip) => (
|
||||
<span
|
||||
key={chip}
|
||||
className="rounded-full border border-teal-400/30 bg-navy-950/85 px-2.5 py-1 text-[11px] font-semibold text-teal-100"
|
||||
>
|
||||
{chip}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</ProductMapFrame>
|
||||
);
|
||||
}
|
||||
|
||||
if (activeStep === 1) {
|
||||
return (
|
||||
<ProductMapFrame>
|
||||
<div className="absolute left-3 top-3 rounded-md bg-navy-950/85 px-2.5 py-1.5 text-xs text-warm-300 border border-white/10">
|
||||
{t('home.showcaseMapLabel')}
|
||||
</div>
|
||||
{mapPins.map((pin, index) => (
|
||||
<DemoMapPin key={pin.name} {...pin} active={index === 0} />
|
||||
))}
|
||||
</ProductMapFrame>
|
||||
);
|
||||
}
|
||||
|
||||
if (activeStep === 2) {
|
||||
return (
|
||||
<ProductMapFrame>
|
||||
<DemoMapPin {...mapPins[0]} active />
|
||||
<div className="absolute right-3 top-3 w-40 rounded-lg border border-white/10 bg-navy-950/95 p-3 shadow-xl">
|
||||
<div className="text-xs font-semibold text-warm-400">
|
||||
{t('home.showcaseStep3Postcode')}
|
||||
</div>
|
||||
<div className="mt-1 text-xl font-bold text-white">{t('home.showcaseStep3Code')}</div>
|
||||
<div className="mt-2 rounded-full bg-teal-400/15 px-2 py-1 text-center text-xs font-semibold text-teal-200">
|
||||
{t('home.showcaseStep3Score')}
|
||||
</div>
|
||||
</div>
|
||||
<div className="absolute inset-x-3 bottom-3 rounded-lg border border-white/10 bg-navy-950/90 p-3">
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{evidenceRows.map((row) => (
|
||||
<div key={row} className="text-[11px] text-warm-300">
|
||||
<span className="text-teal-300">✓</span> {row}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</ProductMapFrame>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ProductMapFrame>
|
||||
<div className="absolute inset-3 grid grid-rows-3 gap-2">
|
||||
{compareRows.map((row, index) => (
|
||||
<div
|
||||
key={row}
|
||||
className="rounded-lg border border-white/10 bg-navy-950/90 p-3 shadow-lg"
|
||||
>
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div className="min-w-0 flex-1 text-xs font-semibold leading-snug text-white">
|
||||
{row}
|
||||
</div>
|
||||
<div className="h-2 w-16 shrink-0 rounded-full bg-white/10 overflow-hidden">
|
||||
<div
|
||||
className="h-full rounded-full bg-coral-400"
|
||||
style={{ width: `${88 - index * 14}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</ProductMapFrame>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className="relative w-full max-w-xl mt-10 lg:mt-0"
|
||||
onMouseEnter={() => setIsPaused(true)}
|
||||
onMouseLeave={() => setIsPaused(false)}
|
||||
onFocus={() => setIsPaused(true)}
|
||||
onBlur={() => setIsPaused(false)}
|
||||
aria-label={t('home.showcaseHeader')}
|
||||
>
|
||||
<div className="h-[44rem] sm:h-[40rem] md:h-[34rem] lg:h-[35rem] xl:h-[33rem] rounded-xl border border-white/10 bg-navy-950/85 shadow-2xl overflow-hidden flex flex-col">
|
||||
<div className="shrink-0 px-4 py-3 border-b border-white/10 flex items-center gap-2">
|
||||
<LogoIcon className="w-5 h-5 text-teal-400" />
|
||||
<div className="text-sm font-semibold text-white">{t('home.showcaseHeader')}</div>
|
||||
<div className="ml-auto text-xs text-warm-400">{t('home.showcaseContext')}</div>
|
||||
</div>
|
||||
<div className="shrink-0 grid grid-cols-2 sm:grid-cols-4 gap-1 p-2 bg-white/[0.03] border-b border-white/10">
|
||||
{steps.map((step, index) => (
|
||||
<button
|
||||
key={step.tab}
|
||||
type="button"
|
||||
onClick={() => setActiveStep(index)}
|
||||
aria-pressed={activeStep === index}
|
||||
className={`rounded-md px-2 py-2 text-[11px] sm:text-xs font-semibold leading-tight text-left transition-colors ${
|
||||
activeStep === index
|
||||
? 'bg-white/10 text-white'
|
||||
: 'text-warm-400 hover:bg-white/[0.06] hover:text-warm-200'
|
||||
}`}
|
||||
>
|
||||
<span>{step.tab}</span>
|
||||
<span className="mt-2 block h-0.5 overflow-hidden rounded-full bg-white/10">
|
||||
{activeStep === index && (
|
||||
<span
|
||||
key={activeStep}
|
||||
className="showcase-progress block h-full origin-left bg-teal-400"
|
||||
style={{
|
||||
animationDuration: `${SHOWCASE_INTERVAL_MS}ms`,
|
||||
animationPlayState: isPaused ? 'paused' : 'running',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex-1 min-h-0 p-4 md:p-5">
|
||||
<div className="grid h-full min-h-0 grid-rows-[minmax(0,0.92fr)_minmax(0,1.18fr)] md:grid-rows-1 md:grid-cols-[0.88fr_1.12fr] gap-4">
|
||||
<div className="min-h-0 overflow-hidden flex flex-col justify-between gap-4 rounded-lg border border-white/10 bg-white/[0.035] p-4">
|
||||
<div className="shrink-0" aria-live={isPaused ? 'polite' : 'off'}>
|
||||
<div className="text-xs font-semibold text-teal-300">{steps[activeStep].tab}</div>
|
||||
<h2 className="mt-2 text-xl font-bold leading-tight text-white">{active.title}</h2>
|
||||
<p className="mt-3 text-sm leading-relaxed text-warm-400">{active.body}</p>
|
||||
</div>
|
||||
<div className="min-h-0 overflow-hidden">{renderSidePanel()}</div>
|
||||
</div>
|
||||
{renderVisual()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function HomePage({
|
||||
onOpenDashboard,
|
||||
onOpenPricing: _onOpenPricing,
|
||||
|
|
@ -63,82 +443,84 @@ 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'} />
|
||||
<div className="absolute top-1/3 left-1/4 w-[500px] h-[500px] bg-teal-500/[0.04] rounded-full blur-[120px] pointer-events-none" />
|
||||
<div className="absolute bottom-0 right-1/4 w-[400px] h-[300px] bg-teal-600/[0.03] rounded-full blur-[100px] pointer-events-none" />
|
||||
<div className="relative z-10 max-w-7xl mx-auto w-full px-6 md:px-10 pt-16 md:pt-24 backdrop-blur-[2px] flex-1 flex flex-col">
|
||||
<div className="max-w-4xl">
|
||||
<h1 className="text-3xl md:text-5xl font-extrabold text-white mb-4 leading-[1.1] tracking-tight">
|
||||
{t('home.heroTitle1')} <span className="text-teal-400">{t('home.heroTitle2')}</span>
|
||||
.
|
||||
<br />
|
||||
{t('home.heroTitle3')}
|
||||
</h1>
|
||||
<p className="text-base md:text-lg text-warm-300 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">
|
||||
{t('home.heroDescription')}
|
||||
</p>
|
||||
<div className="flex flex-col sm:flex-row sm:items-center gap-3 sm:gap-4 mb-10">
|
||||
<button
|
||||
onClick={() => {
|
||||
trackEvent('CTA Click', { location: 'hero', label: 'explore_map' });
|
||||
onOpenDashboard();
|
||||
}}
|
||||
className="px-7 py-3.5 bg-coral-500 text-white rounded-lg font-semibold hover:bg-coral-600 transition-colors text-base shadow-lg shadow-coral-500/25 text-center"
|
||||
>
|
||||
{t('home.exploreTheMap')}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
trackEvent('CTA Click', { location: 'hero', label: 'see_difference' });
|
||||
const target = document.getElementById('comparison');
|
||||
if (!target) return;
|
||||
const scroller = target.closest('.overflow-y-auto') as HTMLElement | null;
|
||||
if (!scroller) return;
|
||||
const start = scroller.scrollTop;
|
||||
const end =
|
||||
start +
|
||||
target.getBoundingClientRect().top -
|
||||
scroller.getBoundingClientRect().top -
|
||||
48;
|
||||
const distance = end - start;
|
||||
const duration = 1200;
|
||||
let startTime: number;
|
||||
const step = (time: number) => {
|
||||
if (!startTime) startTime = time;
|
||||
const p = Math.min((time - startTime) / duration, 1);
|
||||
const ease = p < 0.5 ? 4 * p * p * p : 1 - Math.pow(-2 * p + 2, 3) / 2;
|
||||
scroller.scrollTop = start + distance * ease;
|
||||
if (p < 1) requestAnimationFrame(step);
|
||||
};
|
||||
requestAnimationFrame(step);
|
||||
}}
|
||||
className="px-[26px] py-[12px] border-2 border-teal-400 text-teal-400 rounded-lg font-semibold hover:bg-teal-400/10 transition-colors text-base text-center"
|
||||
>
|
||||
{t('home.seeTheDifference')}
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-x-8 sm:gap-x-12 gap-y-4 pt-3 border-t border-white/10">
|
||||
<div>
|
||||
<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="grid lg:grid-cols-[1fr_0.9fr] gap-8 lg:gap-12 items-center">
|
||||
<div className="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')}{' '}
|
||||
<span className="text-teal-400">{t('home.heroTitle2')}</span>.
|
||||
<br />
|
||||
{t('home.heroTitle3')}
|
||||
</h1>
|
||||
<p className="text-base md:text-lg text-warm-300 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">
|
||||
{t('home.heroDescription')}
|
||||
</p>
|
||||
<div className="flex flex-col sm:flex-row sm:items-center gap-3 sm:gap-4 mb-10">
|
||||
<button
|
||||
onClick={() => {
|
||||
trackEvent('CTA Click', { location: 'hero', label: 'explore_map' });
|
||||
onOpenDashboard();
|
||||
}}
|
||||
className="px-7 py-3.5 bg-coral-500 text-white rounded-lg font-semibold hover:bg-coral-600 transition-colors text-base shadow-lg shadow-coral-500/25 text-center"
|
||||
>
|
||||
{t('home.exploreTheMap')}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
trackEvent('CTA Click', { location: 'hero', label: 'see_difference' });
|
||||
const target = document.getElementById('how-it-works');
|
||||
if (!target) return;
|
||||
const scroller = target.closest('.overflow-y-auto') as HTMLElement | null;
|
||||
if (!scroller) return;
|
||||
const start = scroller.scrollTop;
|
||||
const end =
|
||||
start +
|
||||
target.getBoundingClientRect().top -
|
||||
scroller.getBoundingClientRect().top -
|
||||
48;
|
||||
const distance = end - start;
|
||||
const duration = 1200;
|
||||
let startTime: number;
|
||||
const step = (time: number) => {
|
||||
if (!startTime) startTime = time;
|
||||
const p = Math.min((time - startTime) / duration, 1);
|
||||
const ease = p < 0.5 ? 4 * p * p * p : 1 - Math.pow(-2 * p + 2, 3) / 2;
|
||||
scroller.scrollTop = start + distance * ease;
|
||||
if (p < 1) requestAnimationFrame(step);
|
||||
};
|
||||
requestAnimationFrame(step);
|
||||
}}
|
||||
className="px-[26px] py-[12px] border-2 border-teal-400 text-teal-400 rounded-lg font-semibold hover:bg-teal-400/10 transition-colors text-base text-center"
|
||||
>
|
||||
{t('home.seeTheDifference')}
|
||||
</button>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-2xl md:text-3xl font-bold text-white">
|
||||
<TickerValue text="56" active={statsActive} />
|
||||
<div className="flex flex-wrap gap-x-8 sm:gap-x-12 gap-y-4 pt-3 border-t border-white/10">
|
||||
<div>
|
||||
<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>
|
||||
<div className="text-sm text-warm-400">{t('home.statFilters')}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-2xl md:text-3xl font-bold text-white">
|
||||
{t('home.statEvery')}
|
||||
<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>
|
||||
<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>
|
||||
<div className="text-sm text-warm-400">{t('home.statPostcodeInEngland')}</div>
|
||||
</div>
|
||||
</div>
|
||||
<HeroProductShowcase />
|
||||
</div>
|
||||
<div className="flex-1" />
|
||||
</div>
|
||||
|
|
@ -155,6 +537,32 @@ export default function HomePage({
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{/* Street by street */}
|
||||
<div className="max-w-7xl mx-auto px-6 md:px-10 pt-10 pb-2">
|
||||
<div className="grid md:grid-cols-3 gap-4">
|
||||
<div className="md:col-span-1">
|
||||
<h2 className="text-2xl md:text-3xl font-bold text-navy-950 dark:text-warm-100 mb-3">
|
||||
{t('home.streetTitle')}
|
||||
</h2>
|
||||
<p className="text-warm-600 dark:text-warm-400 leading-relaxed">
|
||||
{t('home.streetIntro')}
|
||||
</p>
|
||||
</div>
|
||||
{[
|
||||
{ label: t('home.streetCard1Title'), body: t('home.streetCard1Body') },
|
||||
{ label: t('home.streetCard2Title'), body: t('home.streetCard2Body') },
|
||||
].map((item) => (
|
||||
<div
|
||||
key={item.label}
|
||||
className="rounded-lg border border-warm-200 dark:border-warm-700 bg-white dark:bg-warm-800 p-5"
|
||||
>
|
||||
<h3 className="font-bold text-navy-950 dark:text-warm-100 mb-2">{item.label}</h3>
|
||||
<p className="text-warm-600 dark:text-warm-400 leading-relaxed">{item.body}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* How to use it + Comparison table (two columns) */}
|
||||
<div id="how-it-works" className="max-w-7xl mx-auto px-6 md:px-10 pt-10 pb-2">
|
||||
<div ref={whyRef} className="fade-in-section">
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue