diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 8c9bbc9..1c2f0f4 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -305,7 +305,6 @@ export default function App() { onOpenDashboard={() => navigateTo('dashboard')} onOpenPricing={() => navigateTo('pricing')} theme={theme} - features={features} hidePricing={user?.subscription === 'licensed' || user?.isAdmin} /> ) : activePage === 'pricing' && !(user?.subscription === 'licensed' || user?.isAdmin) ? ( diff --git a/frontend/src/components/home/HomePage.tsx b/frontend/src/components/home/HomePage.tsx index fbe93d7..1207add 100644 --- a/frontend/src/components/home/HomePage.tsx +++ b/frontend/src/components/home/HomePage.tsx @@ -1,25 +1,20 @@ import { useState, useEffect, useRef } from 'react'; import { useFadeInRef } from '../../hooks/useFadeIn'; import HexCanvas from './HexCanvas'; -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 { trackEvent } from '../../lib/analytics'; -import type { FeatureMeta } from '../../types'; export default function HomePage({ onOpenDashboard, onOpenPricing: _onOpenPricing, theme = 'light', - features = [], hidePricing: _hidePricing, }: { onOpenDashboard: () => void; onOpenPricing: () => void; theme?: 'light' | 'dark'; - features?: FeatureMeta[]; hidePricing?: boolean; }) { const [statsActive, setStatsActive] = useState(false); @@ -34,7 +29,7 @@ export default function HomePage({ // Scroll depth tracking const scrolledSections = useRef(new Set()); useEffect(() => { - const ids = ['how-it-works', 'demo']; + const ids = ['how-it-works']; const observers: IntersectionObserver[] = []; ids.forEach((id) => { const el = document.getElementById(id); @@ -142,35 +137,6 @@ export default function HomePage({
-
@@ -286,15 +252,6 @@ export default function HomePage({ - {/* Scrollytelling: Problem + Solution + Demo map */} -

- See It in Action -

- - {/* The real cost CTA */}
diff --git a/frontend/src/components/home/ScrollStory.tsx b/frontend/src/components/home/ScrollStory.tsx deleted file mode 100644 index 9d95880..0000000 --- a/frontend/src/components/home/ScrollStory.tsx +++ /dev/null @@ -1,466 +0,0 @@ -import { useState, useEffect, useRef, useMemo } from 'react'; -import MapComponent from '../map/Map'; -import { apiUrl, assertOk, authHeaders, isAbortError, logNonAbortError } from '../../lib/api'; -import { formatValue } from '../../lib/format'; -import { zoomToResolution } from '../../lib/map-utils'; -import { DENSITY_GRADIENT, DENSITY_GRADIENT_DARK } from '../../lib/consts'; -import { gradientToCss } from '../../lib/utils'; -import { SpinnerIcon } from '../ui/icons/SpinnerIcon'; -import type { FeatureMeta, HexagonData } from '../../types'; - -const DEMO_VIEW = { longitude: -0.12, latitude: 51.51, zoom: 5.5, pitch: 0 }; -const DEMO_FEATURE_NAMES = [ - 'Estimated current price', - 'Good+ primary schools within 5km', - 'Number of restaurants within 2km', -]; -const noop = () => {}; - -// Filter fractions per stage: featureName -> [minFrac, maxFrac] -// 0 = feature.min, 1 = feature.max -interface StageDef { - filters: Record; - travel?: { mode: string; slug: string; min: number; max: number }; -} - -const STAGES: StageDef[] = [ - // 0: No filters — the problem - { filters: {} }, - // 1: Price filter - { - filters: { 'Estimated current price': [0, 0.4] }, - }, - // 2: Price + schools - { - filters: { - 'Estimated current price': [0, 0.4], - 'Good+ primary schools within 5km': [0.3, 1], - }, - }, - // 3: Price + schools + restaurants - { - filters: { - 'Estimated current price': [0, 0.4], - 'Good+ primary schools within 5km': [0.3, 1], - 'Number of restaurants within 2km': [0.15, 1], - }, - }, - // 4: Price + schools + restaurants + commute to Manchester - { - filters: { - 'Estimated current price': [0, 0.4], - 'Good+ primary schools within 5km': [0.3, 1], - 'Number of restaurants within 2km': [0.15, 1], - }, - travel: { mode: 'transit', slug: 'manchester', min: 0, max: 45 }, - }, - // 5: Summary — same filters - { - filters: { - 'Estimated current price': [0, 0.4], - 'Good+ primary schools within 5km': [0.3, 1], - 'Number of restaurants within 2km': [0.15, 1], - }, - travel: { mode: 'transit', slug: 'manchester', min: 0, max: 45 }, - }, -]; - -const STEPS: { heading: string | null; body: React.ReactNode }[] = [ - { - heading: null, - body: ( - <> -

- Let's look at an example: -

-

- You're about to spend{' '} - up to £500k on a home. -

- - ), - }, - { - heading: null, - body: ( - <> -
-
- 1 -
-

- Set your must-haves -

-
-

- Say you want a home{' '} - under £500k… -

- - ), - }, - { - heading: null, - body: ( -

- …with{' '} - good primary schools{' '} - nearby… -

- ), - }, - { - heading: null, - body: ( -

- …and{' '} - - restaurants within walking distance - - … -

- ), - }, - { - heading: null, - body: ( -

- …all within{' '} - 45 minutes of Manchester by - public transport. -

- ), - }, - { - heading: null, - body: ( - <> -

- No area chosen. No listings browsed. Yet you already know exactly where your needs are - met. -

-

- That's just 4 filters. We've built{' '} - 56, covering commute times, - crime, broadband, noise, schools, amenities, and more. -

- - ), - }, -]; - -interface ScrollStoryProps { - features: FeatureMeta[]; - theme: 'light' | 'dark'; -} - -export default function ScrollStory({ features, theme }: ScrollStoryProps) { - const [stage, setStage] = useState(0); - const [hexData, setHexData] = useState([]); - const [loading, setLoading] = useState(true); - const abortRef = useRef(); - - const demoFeatures = useMemo( - () => - DEMO_FEATURE_NAMES.map((name) => features.find((f) => f.name === name)).filter( - Boolean - ) as FeatureMeta[], - [features] - ); - - // Compute actual filter values from stage fractions + feature metadata - const stageFilters = useMemo(() => { - const stageDef = STAGES[stage]; - const result: Record = {}; - for (const [name, [minFrac, maxFrac]] of Object.entries(stageDef.filters)) { - const meta = demoFeatures.find((f) => f.name === name); - if (meta?.min != null && meta?.max != null) { - const range = meta.max - meta.min; - result[name] = [meta.min + range * minFrac, meta.min + range * maxFrac]; - } - } - return result; - }, [stage, demoFeatures]); - - const demoView = useMemo(() => DEMO_VIEW, []); - - // Derive H3 resolution from current zoom (discrete — only changes at thresholds) - const resolution = zoomToResolution(demoView.zoom); - - // Compute bounds string from current view, rounded to 0.5° for stability - const demoBounds = useMemo(() => { - const { longitude, latitude, zoom } = demoView; - const scale = Math.pow(2, zoom); - const degreesPerPixelLng = 360 / (512 * scale); - const halfW = (1200 / 2) * degreesPerPixelLng * 1.3; - const latRad = (latitude * Math.PI) / 180; - const mercY = (1 - Math.log(Math.tan(latRad) + 1 / Math.cos(latRad)) / Math.PI) / 2; - const worldSize = 512 * scale; - const halfH = (800 / 2) * 1.3; - const topY = mercY * worldSize - halfH; - const botY = mercY * worldSize + halfH; - const toLat = (py: number) => { - const my = Math.max(0.001, Math.min(0.999, py / worldSize)); - return (Math.atan(Math.sinh(Math.PI * (1 - 2 * my))) * 180) / Math.PI; - }; - const snap = (v: number) => Math.round(v * 2) / 2; - const south = snap(Math.max(-85, toLat(botY))); - const west = snap(Math.max(-180, longitude - halfW)); - const north = snap(Math.min(85, toLat(topY))); - const east = snap(Math.min(180, longitude + halfW)); - return `${south},${west},${north},${east}`; - }, [demoView]); - - // Fetch hex data when stage filters change - useEffect(() => { - if (features.length === 0) return; - - // Clear stale data and show loading spinner immediately - setLoading(true); - setHexData([]); - - const params = new URLSearchParams({ - resolution: String(resolution), - bounds: demoBounds, - }); - const filterParts: string[] = []; - for (const [name, [min, max]] of Object.entries(stageFilters)) { - filterParts.push(`${name}:${min}:${max}`); - } - if (filterParts.length > 0) params.set('filters', filterParts.join(',')); - - const stageDef = STAGES[stage]; - if (stageDef.travel) { - const { mode, slug, min, max } = stageDef.travel; - params.set('travel', `${mode}:${slug}:${min}:${max}`); - } - - const controller = new AbortController(); - abortRef.current?.abort(); - abortRef.current = controller; - fetch(apiUrl('hexagons', params), authHeaders({ signal: controller.signal })) - .then((res) => { - assertOk(res, 'hexagons'); - return res.json(); - }) - .then((data: { features: HexagonData[] }) => { - setHexData(data.features); - setLoading(false); - }) - .catch((err) => { - if (!isAbortError(err)) { - logNonAbortError('Failed to fetch story hexagons', err); - setLoading(false); - } - }); - - return () => controller.abort(); - }, [features, stageFilters, stage, resolution, demoBounds]); - - const isLastStage = stage === STEPS.length - 1; - - return ( -
- {/* Map background */} -
- -
- - {/* Interaction blocker */} -
- - {/* Loading */} - {loading && ( -
- -
- )} - - {/* Filter indicators — top left */} -
-
-
- Filters -
- {demoFeatures.map((feature) => { - const filterVal = stageFilters[feature.name]; - const isActive = !!filterVal; - const min = feature.min ?? 0; - const max = feature.max ?? 1; - const range = max - min || 1; - const leftPct = filterVal ? ((filterVal[0] - min) / range) * 100 : 0; - const widthPct = filterVal ? ((filterVal[1] - filterVal[0]) / range) * 100 : 100; - return ( -
-
- - {feature.name} - - {isActive && filterVal && ( - - {formatValue(filterVal[0], feature)}– - {formatValue(filterVal[1], feature)} - - )} -
-
-
-
-
- ); - })} - {/* Travel time indicator */} -
-
- - Commute to Manchester - - {STAGES[stage].travel && ( - - 0–45 min - - )} -
-
-
-
-
-
-
- - {/* Density legend — top right */} -
-
-
- Colour -
-
- Number of properties -
-
-
- Fewer - More -
-
-
- - {/* Card stack overlay — bottom on mobile, right-center on desktop */} -
-
- {STEPS.map((step, i) => ( -
-
- {step.heading && ( -

- {step.heading} -

- )} -
{step.body}
- - {/* Navigation */} -
- {/* Step dots */} -
- {STEPS.map((_, dotIdx) => ( -
- - {/* Prev / Next / CTA buttons */} -
- {stage > 0 && ( - - )} - {isLastStage ? ( - - Start exploring → - - ) : ( - - )} -
-
-
-
- ))} -
-
-
- ); -}