ruby homepage changes

This commit is contained in:
Andras Schmelczer 2026-02-21 20:49:56 +00:00
parent 94ebd0f614
commit 7777d4046e
7 changed files with 312 additions and 223 deletions

View file

@ -5,6 +5,7 @@ import ScrollStory from './ScrollStory';
import BottomIllustration from './BottomIllustration';
import { TickerValue } from '../ui/TickerValue';
import { ChevronIcon } from '../ui/icons/ChevronIcon';
import { LogoIcon } from '../ui/icons/LogoIcon';
import type { FeatureMeta } from '../../types';
export default function HomePage({
@ -27,7 +28,6 @@ export default function HomePage({
}, []);
const whyRef = useFadeInRef();
const howRef = useFadeInRef();
const ctaRef = useFadeInRef();
return (
@ -41,17 +41,16 @@ export default function HomePage({
<div className="relative z-10 max-w-4xl mx-auto px-6 md:px-10 pt-16 md:pt-24 backdrop-blur-[2px] flex-1 flex flex-col">
<div>
<h1 className="text-3xl md:text-5xl font-extrabold text-white mb-4 leading-[1.1] tracking-tight">
Maximum <span className="text-teal-400">Value</span>. Minimum Compromise.
Maximum <span className="text-teal-400">Value</span>.
<br />
Minimum Compromise.
</h1>
<p className="text-xl text-warm-300 mb-6 leading-relaxed max-w-xl">
Buying a home may be your most important decision. Why not ensure you make your
best-ever decision?
<p className="text-lg text-warm-300 mb-6 leading-relaxed max-w-xl">
House hunting? Make your biggest investment your smartest move.
</p>
<p className="text-lg text-warm-400 mb-8 max-w-lg">
You have so many options. Picking the best one is daunting and stressful. It
won&apos;t be anymore when looking at the property landscape through our
interactive map. Simply pick your exact needs and our interactive map will show
you all areas that satisfy your requirements and more.
<p className="text-lg text-warm-400 mb-8 max-w-xl">
So many options choosing the right one can feel overwhelming. Our interactive map makes it simple: select your must-haves and instantly see the areas that
fit.
</p>
<div className="flex items-center gap-4 mb-10">
<button
@ -60,20 +59,32 @@ export default function HomePage({
>
Explore the map
</button>
{hidePricing ? (
<span className="px-[26px] py-[12px] border-2 border-teal-400/50 text-teal-400 rounded-lg font-semibold text-base">
You have lifetime access!
</span>
) : (
<button
onClick={onOpenPricing}
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"
>
Get lifetime access
</button>
)}
<button
onClick={() => {
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 t = Math.min((time - startTime) / duration, 1);
const ease = t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
scroller.scrollTop = start + distance * ease;
if (t < 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"
>
See the difference
</button>
</div>
<div className="flex gap-12 pt-6 border-t border-white/10">
<div className="flex gap-12 pt-3 border-t border-white/10">
<div>
<div className="text-2xl md:text-3xl font-bold text-white">
<TickerValue text="13M" active={statsActive} />
@ -93,87 +104,145 @@ export default function HomePage({
</div>
</div>
<div className="flex-1" />
<div className="flex flex-col items-center pb-8 animate-[bounce_3s_ease-in-out_infinite]">
<button
onClick={() => {
const target = document.getElementById('demo');
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 t = Math.min((time - startTime) / duration, 1);
const ease = t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
scroller.scrollTop = start + distance * ease;
if (t < 1) requestAnimationFrame(step);
};
requestAnimationFrame(step);
}}
className="flex flex-col items-center pb-8 mt-10 md:mt-0 animate-[bounce_3s_ease-in-out_infinite] cursor-pointer"
>
<p className="text-lg md:text-xl font-semibold text-warm-300 mb-2">
How does it work?
See It in Action
</p>
<ChevronIcon direction="down" className="w-6 h-6 text-warm-400" />
</button>
</div>
</div>
{/* How to use it + Comparison table (two columns) */}
<div id="how-it-works" className="max-w-7xl mx-auto px-6 pt-20 pb-2">
<div ref={whyRef} className="fade-in-section">
<div className="grid lg:grid-cols-[2fr_3fr] gap-12 items-start">
{/* Left: How to use it */}
<div>
<h2 className="text-3xl font-bold text-navy-950 dark:text-warm-100 mb-10">
How to use it
</h2>
<div className="space-y-8">
{HOW_STEPS.map((step, i) => (
<div key={i} className="flex gap-5">
<div className="shrink-0 w-10 h-10 rounded-full bg-teal-600 text-white flex items-center justify-center font-bold text-lg">
{i + 1}
</div>
<div>
<h3 className="font-bold text-navy-950 dark:text-warm-100 mb-1">
{step.title}
</h3>
<p className="text-warm-600 dark:text-warm-400 leading-relaxed">
{step.description}
</p>
</div>
</div>
))}
</div>
</div>
{/* Right: Comparison table */}
<div id="comparison">
<h2 className="text-3xl font-bold text-navy-950 dark:text-warm-100 mb-10 flex items-center gap-3">
Others vs Perfect Postcode <LogoIcon className="w-8 h-8 text-teal-600 dark:text-teal-400" />
</h2>
<div className="overflow-x-auto rounded-xl border border-warm-200 dark:border-warm-700 bg-white dark:bg-warm-800 shadow-sm">
<table className="w-full text-left">
<thead>
<tr className="border-b border-warm-200 dark:border-warm-700 bg-warm-50 dark:bg-warm-800">
<th className="px-2 md:px-5 py-3 md:py-4 text-xs md:text-sm font-bold text-navy-950 dark:text-warm-100" />
<th className="px-1.5 md:px-3 py-3 md:py-4 text-[10px] md:text-xs font-bold text-navy-950 dark:text-warm-100 text-center">
Listing portals
</th>
<th className="px-1.5 md:px-3 py-3 md:py-4 text-[10px] md:text-xs font-bold text-navy-950 dark:text-warm-100 text-center">
{'\u201CCheck my postcode\u201D'}
</th>
<th className="px-1.5 md:px-3 py-3 md:py-4 text-[10px] md:text-xs font-bold text-navy-950 dark:text-warm-100 text-center">
Area guides
</th>
<th className="px-1.5 md:px-3 py-3 md:py-4 text-[10px] md:text-xs font-bold text-teal-700 dark:text-teal-400 text-center">
Perfect Postcode
</th>
</tr>
</thead>
<tbody>
{FEATURE_ROWS.map((row, i) => (
<tr
key={row.feature}
className={
i < FEATURE_ROWS.length - 1
? 'border-b border-warm-100 dark:border-warm-800'
: ''
}
>
<td className="px-2 md:px-5 py-2.5 md:py-3.5 text-xs md:text-sm text-warm-700 dark:text-warm-300">
{row.feature}
{row.subtitle && (
<div className="italic text-warm-500 dark:text-warm-400">{row.subtitle}</div>
)}
</td>
{[row.listings, row.postcode, row.guides].map((has, j) => (
<td
key={j}
className={`px-1.5 md:px-3 py-2.5 md:py-3.5 text-center text-base md:text-lg ${has ? 'text-green-500' : 'text-red-500'}`}
>
{has ? '\u2713' : '\u2717'}
</td>
))}
<td className="px-1.5 md:px-3 py-2.5 md:py-3.5 text-center text-base md:text-lg text-green-500">
&#x2713;
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
{/* Scrollytelling: Problem + Solution + Demo map */}
<h2 id="demo" className="text-3xl font-bold text-navy-950 dark:text-warm-100 text-center pt-6 mb-4">
See It in Action
</h2>
<p className="text-warm-600 dark:text-warm-400 text-center max-w-2xl mx-auto mb-8 leading-relaxed">
Listings only show what&apos;s on the market right now &mdash; a tiny, random slice.
They tell you nothing about the area, or potential opportunities. We flip the search:
start with what matters to you, and the right places reveal themselves.
</p>
<ScrollStory features={features} theme={theme} />
{/* Why existing tools don't cut it */}
<div className="max-w-4xl mx-auto px-6 py-20">
<div ref={whyRef} className="fade-in-section">
<h2 className="text-3xl font-bold text-navy-950 dark:text-warm-100 mb-10 text-center">
Why existing tools don&apos;t cut it
</h2>
<div className="grid md:grid-cols-3 gap-6">
{WHY_CARDS.map((card) => (
<div
key={card.title}
className="rounded-xl border border-warm-200 dark:border-warm-700 bg-white dark:bg-warm-800 p-6 shadow-sm"
>
<div className="text-2xl mb-3">{card.icon}</div>
<h3 className="font-bold text-navy-950 dark:text-warm-100 mb-2">{card.title}</h3>
<p className="text-warm-600 dark:text-warm-400 text-sm leading-relaxed">
{card.description}
</p>
</div>
))}
</div>
<p className="text-center mt-10 text-lg text-warm-700 dark:text-warm-300 max-w-2xl mx-auto leading-relaxed">
We do. 13 million historical transactions. 56 filters. Real travel-time routing to
any destination. Every postcode in England, scored and filterable, on a single map.
</p>
</div>
</div>
{/* How to use it */}
<div className="max-w-3xl mx-auto px-6 pb-20">
<div ref={howRef} className="fade-in-section">
<h2 className="text-3xl font-bold text-navy-950 dark:text-warm-100 mb-10 text-center">
How to use it
</h2>
<div className="space-y-8">
{HOW_STEPS.map((step, i) => (
<div key={i} className="flex gap-5">
<div className="shrink-0 w-10 h-10 rounded-full bg-teal-600 text-white flex items-center justify-center font-bold text-lg">
{i + 1}
</div>
<div>
<h3 className="font-bold text-navy-950 dark:text-warm-100 mb-1">
{step.title}
</h3>
<p className="text-warm-600 dark:text-warm-400 leading-relaxed">
{step.description}
</p>
</div>
</div>
))}
</div>
</div>
</div>
{/* The real cost CTA */}
<div className="max-w-3xl mx-auto px-6 pb-12">
<div className="max-w-3xl mx-auto px-6 pt-20 pb-12">
<div ref={ctaRef} className="fade-in-section text-center">
<h2 className="text-2xl md:text-3xl font-bold text-navy-950 dark:text-warm-100 mb-4 leading-snug">
The biggest financial decision of your life
<br />
deserves proper tools behind&nbsp;it.
</h2>
<p className="text-warm-600 dark:text-warm-400 mb-3 max-w-xl mx-auto leading-relaxed">
Stamp duty on a &pound;400k house: &pound;10,000. Solicitor fees: &pound;1,500.
Survey: &pound;500. Moving costs: &pound;1,000. And that&apos;s just the money. Get the
wrong area and you&apos;re stuck &mdash; with a long commute, bad schools, or a street
that looked fine on the listing photos but turns out to be on a motorway.
</p>
<p className="text-warm-700 dark:text-warm-300 mb-8 font-semibold">
One payment. Lifetime access. Less than your survey costs and vastly more useful.
<p className="text-warm-600 dark:text-warm-400 mb-8 max-w-xl mx-auto leading-relaxed">
Don&apos;t leave it to chance.
</p>
<button
onClick={onOpenDashboard}
@ -191,46 +260,33 @@ export default function HomePage({
);
}
const WHY_CARDS = [
{
icon: '\u{1F3D8}\uFE0F',
title: 'Listing portals',
description:
"Show you what's for sale today. That's a snapshot, not a strategy. You can filter by price and bedrooms \u2014 that's about it. They tell you nothing about the area.",
},
{
icon: '\u{1F4CD}',
title: '\u201CCheck my postcode\u201D sites',
description:
"Give you stats for one postcode at a time. Useful if you already know where to look. Useless if you don't \u2014 and there are 1.5 million postcodes in England.",
},
{
icon: '\u{1F5FA}\uFE0F',
title: 'Area guides',
description:
"Show one statistic on a map \u2014 crime, or school ratings, or prices. But you care about the intersection: affordable AND safe AND good schools AND short commute. Nobody else shows you that.",
},
const FEATURE_ROWS = [
// listings postcode guides
{ feature: 'Search without choosing an area first', subtitle: '(start with needs, not a location)', listings: false, postcode: false, guides: false },
{ feature: 'Area data', subtitle: '(crime, schools, noise, broadband)', listings: false, postcode: true, guides: true },
{ feature: 'Property-specific data', subtitle: '(price, EPC, floor area)', listings: true, postcode: false, guides: false },
{ feature: '56 combinable filters in one place', subtitle: '(all insights, one interactive map)', listings: false, postcode: false, guides: false },
];
const HOW_STEPS = [
{
title: 'Set your non-negotiables',
title: 'Set your must-haves',
description:
'Budget, commute, bedrooms, whatever matters most. The map narrows to only the areas that qualify.',
'Budget, commute, schools \u2014 the map shows only what qualifies.',
},
{
title: "Explore what\u2019s left",
title: 'Explore areas and discover hidden gems',
description:
"Zoom in. Toggle layers. See crime, schools, noise, amenities. Discover areas you didn\u2019t know existed.",
'Zoom in, dig into details and nice to haves.',
},
{
title: 'Drill into postcodes',
description:
'At street level, see individual properties, what they sold for, floor area, energy rating, estimated current value.',
'See individual properties, sale prices, floor area, and compare.',
},
{
title: 'Go to viewings with a shortlist, not a prayer',
title: 'Shortlist with confidence',
description:
"You\u2019ve already done the hard part. Every area on your list meets your actual criteria, not just what happened to be listed that week.",
'Every area on your list meets your actual criteria \u2014 not just what was listed that week.',
},
];