528 lines
24 KiB
TypeScript
528 lines
24 KiB
TypeScript
import { useEffect, useState, useRef } from 'react';
|
|
import { ChevronIcon } from '../ui/icons/ChevronIcon';
|
|
import { SubNav } from '../ui/SubNav';
|
|
|
|
type LearnTab = 'data-sources' | 'faq' | 'support';
|
|
|
|
const LEARN_TABS = [
|
|
{ key: 'faq', label: 'FAQ' },
|
|
{ key: 'data-sources', label: 'Data Sources' },
|
|
{ key: 'support', label: 'Support' },
|
|
];
|
|
|
|
const DATA_SOURCES = [
|
|
{
|
|
id: 'price-paid',
|
|
name: 'Price Paid Data',
|
|
origin: 'HM Land Registry',
|
|
use: 'Complete historical property sale prices for England.',
|
|
url: 'https://www.gov.uk/government/statistical-data-sets/price-paid-data-downloads',
|
|
license: 'Open Government Licence v3.0',
|
|
},
|
|
{
|
|
id: 'epc',
|
|
name: 'Energy Performance Certificates (EPC)',
|
|
origin: 'Ministry of Housing, Communities & Local Government',
|
|
use: 'Domestic Energy Performance Certificates providing floor area, number of rooms, construction year, energy ratings, property type, and built form. Matched with Price Paid records by address within each postcode. Property owners can opt out of public disclosure.',
|
|
optOutUrl:
|
|
'https://www.gov.uk/guidance/energy-performance-certificates-opt-out-of-public-disclosure',
|
|
url: 'https://epc.opendatacommunities.org/downloads/domestic',
|
|
license: 'Open Government Licence v3.0',
|
|
},
|
|
{
|
|
id: 'nspl',
|
|
name: 'National Statistics Postcode Lookup (NSPL)',
|
|
origin: 'ONS / ArcGIS',
|
|
use: 'Maps postcodes to coordinates and statistical area codes, used to link all area-level datasets to individual properties.',
|
|
url: 'https://www.arcgis.com/sharing/rest/content/items/077631e063eb4e1ab43575d01381ec33/data',
|
|
license: 'Open Government Licence v3.0',
|
|
},
|
|
{
|
|
id: 'iod',
|
|
name: 'English Indices of Deprivation 2025',
|
|
origin: 'Ministry of Housing, Communities & Local Government',
|
|
use: 'Relative deprivation scores across income, employment, education, health, crime, and living environment for every neighbourhood in England.',
|
|
url: 'https://www.gov.uk/government/statistics/english-indices-of-deprivation-2025',
|
|
license: 'Open Government Licence v3.0',
|
|
},
|
|
{
|
|
id: 'ethnicity',
|
|
name: 'Population by Ethnicity (2021 Census)',
|
|
origin: 'ONS',
|
|
use: 'Population percentages by ethnic group (South Asian, East Asian, Black, Mixed, White, Other) per local authority.',
|
|
url: 'https://www.ethnicity-facts-figures.service.gov.uk/uk-population-by-ethnicity/national-and-regional-populations/regional-ethnic-diversity/latest/#download-the-data',
|
|
license: 'Open Government Licence v3.0',
|
|
},
|
|
{
|
|
id: 'crime',
|
|
name: 'Street-level Crime Data',
|
|
origin: 'data.police.uk',
|
|
use: 'Street-level crime data from 2023 to 2025, aggregated into yearly averages by LSOA and crime type (violence, burglary, anti-social behaviour, drugs, vehicle crime, etc.).',
|
|
url: 'https://data.police.uk/data/',
|
|
license: 'Open Government Licence v3.0',
|
|
},
|
|
{
|
|
id: 'osm-pois',
|
|
name: 'OpenStreetMap POIs',
|
|
origin: 'OpenStreetMap contributors / Geofabrik',
|
|
use: 'Points of interest covering shops, restaurants, healthcare, leisure, tourism, and more across Great Britain.',
|
|
url: 'https://download.geofabrik.de/europe/great-britain-latest.osm.pbf',
|
|
license: 'Open Data Commons Open Database License (ODbL)',
|
|
},
|
|
{
|
|
id: 'os-open-greenspace',
|
|
name: 'OS Open Greenspace',
|
|
origin: 'Ordnance Survey',
|
|
use: 'Authoritative green space boundaries for Great Britain, including public parks, gardens, playing fields, and play spaces. Polygon centroids are used for park proximity counts and distance-to-nearest-park calculations.',
|
|
url: 'https://osdatahub.os.uk/downloads/open/OpenGreenspace',
|
|
license: 'Open Government Licence v3.0',
|
|
},
|
|
{
|
|
id: 'naptan',
|
|
name: 'NaPTAN (Public Transport Stops)',
|
|
origin: 'Department for Transport',
|
|
use: 'Station and stop locations for rail, bus, metro/tram, ferry, and airports across England.',
|
|
url: 'https://naptan.dft.gov.uk/naptan/schema/2.4/doc/NaPTANSchemaGuide-2.4-v0.57.pdf',
|
|
license: 'Open Government Licence v3.0',
|
|
},
|
|
{
|
|
id: 'noise',
|
|
name: 'Defra Noise Mapping',
|
|
origin: 'Defra / Environment Agency',
|
|
use: 'Road noise levels (24-hour weighted average) from the 2022 strategic noise mapping, modelled at high resolution and sampled at each postcode.',
|
|
url: 'https://environment.data.gov.uk/spatialdata/road-noise-all-metrics-england-round-4/wcs',
|
|
license: 'Open Government Licence v3.0',
|
|
},
|
|
{
|
|
id: 'ofsted',
|
|
name: 'Ofsted School Inspections',
|
|
origin: 'Ofsted',
|
|
use: 'Latest inspection outcomes for state-funded schools (as at April 2025). Averaged per postcode to give a local school quality score (1=Outstanding to 4=Inadequate).',
|
|
url: 'https://www.gov.uk/government/statistical-data-sets/monthly-management-information-ofsteds-school-inspections-outcomes',
|
|
license: 'Open Government Licence v3.0',
|
|
},
|
|
{
|
|
id: 'broadband',
|
|
name: 'Ofcom Broadband Performance',
|
|
origin: 'Ofcom',
|
|
use: 'Fixed broadband coverage and maximum download speeds by area from Ofcom Connected Nations 2025.',
|
|
url: 'https://www.ofcom.org.uk/phones-and-broadband/coverage-and-speeds/connected-nations-20252/data-downloads-2025',
|
|
license: 'Open Government Licence v3.0',
|
|
},
|
|
{
|
|
id: 'council-tax',
|
|
name: 'Council Tax Levels 2025-26',
|
|
origin: 'Ministry of Housing, Communities & Local Government',
|
|
use: 'Annual council tax rates for Bands A-H for all 296 billing authorities in England, for a dwelling occupied by two adults. Joined to properties via local authority district code from the NSPL postcode lookup.',
|
|
url: 'https://www.gov.uk/government/statistics/council-tax-levels-set-by-local-authorities-in-england-2025-to-2026',
|
|
license: 'Open Government Licence v3.0',
|
|
},
|
|
{
|
|
id: 'ons-rental',
|
|
name: 'Private Rental Market Statistics',
|
|
origin: 'ONS / Valuation Office Agency',
|
|
use: 'Median monthly private rental prices by local authority and bedroom category (Oct 2022 - Sep 2023). Joined to properties via local authority district code and estimated bedroom count.',
|
|
url: 'https://www.ons.gov.uk/peoplepopulationandcommunity/housing/datasets/privaterentalmarketsummarystatisticsinengland',
|
|
license: 'Open Government Licence v3.0',
|
|
},
|
|
];
|
|
|
|
interface FAQItem {
|
|
question: string;
|
|
answer: string;
|
|
}
|
|
|
|
interface FAQSection {
|
|
title: string;
|
|
items: FAQItem[];
|
|
}
|
|
|
|
const FAQ_SECTIONS: FAQSection[] = [
|
|
{
|
|
title: 'Finding Your Area',
|
|
items: [
|
|
{
|
|
question: "I don't even know which areas to look at. Can this help?",
|
|
answer:
|
|
'That\'s exactly what it\'s for. Set your filters (budget, commute time, low crime, good schools) and the map lights up to show you every area that ticks every box. No more Googling "best areas to live near Manchester" at midnight.',
|
|
},
|
|
{
|
|
question: "I'm moving somewhere I've never been. How do I even start?",
|
|
answer:
|
|
"Set your filters for what matters and the map instantly highlights the areas that qualify. You go from \"I don't know a single street\" to a shortlist in minutes.",
|
|
},
|
|
{
|
|
question: 'How do I find areas that tick all my boxes at once?',
|
|
answer:
|
|
'Stack multiple filters (crime below average, good schools, commute under 40 minutes) then colour the map by price to spot the best value areas. The map updates live as you drag sliders, so you can see results change in real time.',
|
|
},
|
|
],
|
|
},
|
|
{
|
|
title: 'Commute and Travel',
|
|
items: [
|
|
{
|
|
question: 'Can I see how long my commute would actually be from different areas?',
|
|
answer:
|
|
"Set your workplace as a destination and we'll colour every postcode by journey time, whether that's by car, bike, or public transport. Filter to your max commute and the rest disappears.",
|
|
},
|
|
{
|
|
question: 'How is that better than checking Google Maps?',
|
|
answer:
|
|
'Google Maps shows you one journey at a time. We colour every postcode in England by commute time in one go, so you can compare hundreds of areas side by side instead of searching them one by one.',
|
|
},
|
|
],
|
|
},
|
|
{
|
|
title: 'Budget and Value',
|
|
items: [
|
|
{
|
|
question: 'How do I find areas where I get the most space for my money?',
|
|
answer:
|
|
"Filter by price per sqm and you'll instantly see which postcodes give you the most space per pound. Pair it with the energy rating filter to avoid properties with high heating costs.",
|
|
},
|
|
{
|
|
question: "How do I make sure a cheap area isn't cheap for a reason?",
|
|
answer:
|
|
"Layer deprivation scores, crime stats, school ratings, and broadband speeds alongside price. If a postcode is affordable and scores well on everything that matters, you've found genuine value, not just a low price with trade-offs you haven't spotted yet.",
|
|
},
|
|
],
|
|
},
|
|
{
|
|
title: 'Safety and Neighbourhood',
|
|
items: [
|
|
{
|
|
question: 'How can I check if an area is safe before I move there?',
|
|
answer:
|
|
'We overlay real police-recorded crime data, broken down by type, onto every neighbourhood in England. Filter by violent crime, burglary, or antisocial behaviour and instantly see which postcodes have the lowest numbers.',
|
|
},
|
|
{
|
|
question:
|
|
'I keep finding flats that look great online, then the area turns out to be rough.',
|
|
answer:
|
|
"That's exactly why this exists. Stack crime rates, noise levels, deprivation scores, nearby pubs and parks, and broadband speeds all on one map so you know what a neighbourhood is actually like before you book a viewing.",
|
|
},
|
|
],
|
|
},
|
|
{
|
|
title: 'Families and Schools',
|
|
items: [
|
|
{
|
|
question: 'Can I find areas with good schools AND low crime in one search?',
|
|
answer:
|
|
'Yes. Stack filters for Ofsted ratings, crime rates, parks, and whatever else matters to your family, and the map highlights only the areas that tick every box. No more cross-referencing five different websites.',
|
|
},
|
|
{
|
|
question: 'How do I know if a neighbourhood has parks and playgrounds nearby?',
|
|
answer:
|
|
'Toggle on the parks and green spaces POI layer to see them right on the map. You can also filter by how many are within walking distance of each postcode.',
|
|
},
|
|
],
|
|
},
|
|
{
|
|
title: 'Environment and Quality of Life',
|
|
items: [
|
|
{
|
|
question: "Can I find energy-efficient homes that aren't on a noisy road?",
|
|
answer:
|
|
'Filter by EPC rating (A to C), then layer on road noise data to rule out anything above your threshold. Colour-code by either feature to spot quiet, efficient streets at a glance.',
|
|
},
|
|
{
|
|
question: 'Does it show flood or subsidence risk?',
|
|
answer:
|
|
"We include ground stability data so you can check for subsidence, shrink-swell clay, and other geological hazards before committing to a property. Filter out risky areas early.",
|
|
},
|
|
{
|
|
question: 'Can I find areas with fast broadband that are actually quiet?',
|
|
answer:
|
|
'Layer the broadband speed filter with road noise data to find streets with great connectivity and low traffic noise. Colour-code by either metric to compare areas at a glance.',
|
|
},
|
|
],
|
|
},
|
|
{
|
|
title: 'Why Perfect Postcode',
|
|
items: [
|
|
{
|
|
question: 'I already use Rightmove. What does this add?',
|
|
answer:
|
|
"Rightmove shows you houses. We show you areas. Crime rates, school ratings, broadband speeds, noise levels, deprivation scores, and more, all filterable on one map. You can judge a neighbourhood before you even look at listings.",
|
|
},
|
|
{
|
|
question: "Can't I just research all this myself for free?",
|
|
answer:
|
|
'You could cross-reference police data, Ofsted reports, EPC registers, Land Registry records, and ONS statistics one postcode at a time. Or you could have it all filterable and colour-coded on one map in seconds.',
|
|
},
|
|
{
|
|
question: 'Where does the data actually come from?',
|
|
answer:
|
|
"Every dataset comes from official UK government sources: Land Registry, the EPC register, ONS, Ofsted, Ofcom, data.police.uk, and Defra. We don't scrape estate agents or make anything up. You can verify any record against the original source.",
|
|
},
|
|
],
|
|
},
|
|
{
|
|
title: 'Pricing and Access',
|
|
items: [
|
|
{
|
|
question: 'Is it really worth paying for a property search tool?',
|
|
answer:
|
|
"Buying a home is likely the biggest purchase you'll make. Spotting one red flag (a noisy road, poor broadband, rising crime) before committing could save you years of regret. This costs less than a tank of petrol.",
|
|
},
|
|
{
|
|
question: "Is this a subscription?",
|
|
answer:
|
|
"No. One-time payment, yours forever. Use it intensively during your search, come back whenever you're curious about a new area, and it's still there if you ever move again.",
|
|
},
|
|
{
|
|
question: 'What can I access on the free tier?',
|
|
answer:
|
|
'Free users can explore all features within inner London (roughly zones 1 to 2). To access data for the rest of England, you need lifetime access.',
|
|
},
|
|
{
|
|
question: 'Can I get a refund?',
|
|
answer:
|
|
'Absolutely. We offer a 30-day money-back guarantee. If you\u2019re not satisfied, email support@perfect-postcode.co.uk within 30 days for a full refund.',
|
|
},
|
|
],
|
|
},
|
|
{
|
|
title: 'Tips and Tricks',
|
|
items: [
|
|
{
|
|
question: 'How do I use the AI filter instead of adding filters one by one?',
|
|
answer:
|
|
'Type what you want in plain English, something like "quiet area near good schools with fast broadband under \u00a3400k", and it\'ll set up all the relevant filters in one go. Tweak any of them manually afterwards.',
|
|
},
|
|
{
|
|
question: 'Can I save a search and come back to it later?',
|
|
answer:
|
|
'Hit the save button and everything is captured: your filters, zoom level, and which data layer you\u2019re colouring by. Pick up exactly where you left off or share the link with your partner.',
|
|
},
|
|
{
|
|
question: "Can I export the data I'm looking at?",
|
|
answer:
|
|
'Use the export button to download the currently filtered properties as a spreadsheet. The export respects all your active filters, so you get exactly the data you want.',
|
|
},
|
|
],
|
|
},
|
|
];
|
|
|
|
function FAQItemCard({ item }: { item: FAQItem }) {
|
|
const [open, setOpen] = useState(false);
|
|
|
|
return (
|
|
<div className="bg-white dark:bg-warm-800 rounded-lg border border-warm-200 dark:border-warm-700">
|
|
<button
|
|
className="w-full text-left px-5 py-4 flex items-center justify-between gap-4"
|
|
onClick={() => setOpen(!open)}
|
|
>
|
|
<span className="font-medium text-warm-900 dark:text-warm-100">{item.question}</span>
|
|
<ChevronIcon
|
|
direction="down"
|
|
className={`w-5 h-5 shrink-0 text-warm-400 dark:text-warm-500 transform ${open ? 'rotate-180' : ''}`}
|
|
/>
|
|
</button>
|
|
{open && (
|
|
<div className="px-5 pb-4">
|
|
<p className="text-sm text-warm-700 dark:text-warm-300 leading-relaxed">{item.answer}</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default function LearnPage() {
|
|
const [tab, setTab] = useState<LearnTab>('faq');
|
|
const [highlightedId, setHighlightedId] = useState<string | null>(null);
|
|
const cardRefs = useRef<Record<string, HTMLDivElement | null>>({});
|
|
const scrollContainerRef = useRef<HTMLDivElement | null>(null);
|
|
|
|
useEffect(() => {
|
|
function handleHash() {
|
|
const hash = window.location.hash.replace('#', '');
|
|
if (hash === 'faq') {
|
|
setTab('faq');
|
|
setHighlightedId(null);
|
|
} else if (hash === 'support') {
|
|
setTab('support');
|
|
setHighlightedId(null);
|
|
} else if (hash && DATA_SOURCES.some((s) => s.id === hash)) {
|
|
setTab('data-sources');
|
|
setHighlightedId(hash);
|
|
setTimeout(() => {
|
|
cardRefs.current[hash]?.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
}, 100);
|
|
} else {
|
|
setHighlightedId(null);
|
|
}
|
|
}
|
|
handleHash();
|
|
window.addEventListener('hashchange', handleHash);
|
|
return () => window.removeEventListener('hashchange', handleHash);
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
scrollContainerRef.current?.scrollTo(0, 0);
|
|
}, [tab]);
|
|
|
|
const switchTab = (key: string) => {
|
|
setTab(key as LearnTab);
|
|
setHighlightedId(null);
|
|
};
|
|
|
|
return (
|
|
<div className="flex-1 overflow-hidden bg-warm-50 dark:bg-navy-950 flex flex-col">
|
|
<SubNav tabs={LEARN_TABS} activeTab={tab} onTabChange={switchTab} />
|
|
|
|
<div className="flex-1 overflow-y-auto flex flex-col" ref={scrollContainerRef}>
|
|
{tab === 'data-sources' ? (
|
|
<>
|
|
<div className="flex-1">
|
|
<div className="max-w-5xl mx-auto px-6 py-6">
|
|
<p className="text-warm-600 dark:text-warm-400 mb-6">
|
|
This application combines {DATA_SOURCES.length} open datasets covering property
|
|
prices, energy performance, transport, demographics, crime, environment, and more.
|
|
</p>
|
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
|
{DATA_SOURCES.map((source) => (
|
|
<div
|
|
key={source.id}
|
|
id={source.id}
|
|
ref={(el) => {
|
|
cardRefs.current[source.id] = el;
|
|
}}
|
|
className={`bg-white dark:bg-warm-800 rounded-lg border p-5 ${
|
|
highlightedId === source.id
|
|
? 'border-teal-400 ring-2 ring-teal-400'
|
|
: 'border-warm-200 dark:border-warm-700'
|
|
}`}
|
|
>
|
|
<div className="flex items-start justify-between gap-4 mb-2">
|
|
<h2 className="text-lg font-semibold text-warm-900 dark:text-warm-100">
|
|
{source.name}
|
|
</h2>
|
|
<span className="text-xs bg-warm-100 dark:bg-navy-700 text-warm-600 dark:text-warm-300 px-2 py-1 rounded text-right">
|
|
{source.license}
|
|
</span>
|
|
</div>
|
|
<p className="text-sm text-warm-500 dark:text-warm-400 mb-2">
|
|
Source: {source.origin}
|
|
</p>
|
|
<p className="text-sm text-warm-700 dark:text-warm-300 mb-3">{source.use}</p>
|
|
<a
|
|
href={source.url}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="text-sm text-teal-600 dark:text-teal-400 hover:text-teal-800 dark:hover:text-teal-300 hover:underline break-all"
|
|
>
|
|
{source.url}
|
|
</a>
|
|
{'optOutUrl' in source && source.optOutUrl && (
|
|
<div className="mt-2">
|
|
<a
|
|
href={source.optOutUrl}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="text-sm text-teal-600 dark:text-teal-400 hover:text-teal-800 dark:hover:text-teal-300 hover:underline"
|
|
>
|
|
Opt out of public disclosure
|
|
</a>
|
|
</div>
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<footer className="bg-navy-900 text-warm-400 px-6 py-6">
|
|
<div className="max-w-5xl mx-auto">
|
|
<h2 className="text-sm font-semibold text-warm-300 uppercase tracking-wide mb-3">
|
|
Attribution
|
|
</h2>
|
|
<ul className="space-y-1.5 text-sm">
|
|
<li>
|
|
Contains HM Land Registry data © Crown copyright and database right 2025.
|
|
</li>
|
|
<li>
|
|
Contains public sector information licensed under the{' '}
|
|
<a
|
|
href="https://www.nationalarchives.gov.uk/doc/open-government-licence/version/3/"
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="text-teal-400 hover:text-teal-300 hover:underline"
|
|
>
|
|
Open Government Licence v3.0
|
|
</a>
|
|
.
|
|
</li>
|
|
<li>Contains OS data © Crown copyright and database rights 2025.</li>
|
|
<li>Powered by TfL Open Data.</li>
|
|
<li>
|
|
Contains data from{' '}
|
|
<a
|
|
href="https://www.openstreetmap.org/copyright"
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="text-teal-400 hover:text-teal-300 hover:underline"
|
|
>
|
|
© OpenStreetMap contributors
|
|
</a>
|
|
, available under the{' '}
|
|
<a
|
|
href="https://opendatacommons.org/licenses/odbl/"
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="text-teal-400 hover:text-teal-300 hover:underline"
|
|
>
|
|
Open Data Commons Open Database License (ODbL)
|
|
</a>
|
|
.
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
</footer>
|
|
</>
|
|
) : tab === 'faq' ? (
|
|
<div className="max-w-3xl mx-auto px-6 py-6 w-full">
|
|
<p className="text-warm-600 dark:text-warm-400 mb-6">
|
|
Whether you're buying, renting, or just exploring, here's how Perfect
|
|
Postcode helps you find the right area.
|
|
</p>
|
|
<div className="space-y-8">
|
|
{FAQ_SECTIONS.map((section) => (
|
|
<div key={section.title}>
|
|
<h3 className="text-sm font-semibold text-warm-500 dark:text-warm-400 uppercase tracking-wide mb-3">
|
|
{section.title}
|
|
</h3>
|
|
<div className="space-y-3">
|
|
{section.items.map((item, index) => (
|
|
<FAQItemCard key={index} item={item} />
|
|
))}
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<div className="max-w-2xl mx-auto px-6 py-6 w-full">
|
|
<p className="text-warm-600 dark:text-warm-400 mb-6">
|
|
Have a question? Check our FAQ or reach out to us directly.
|
|
</p>
|
|
<div className="bg-white dark:bg-warm-800 rounded-xl border border-warm-200 dark:border-warm-700 p-6 text-center">
|
|
<p className="text-warm-600 dark:text-warm-300 mb-2">Need help? Email us at</p>
|
|
<a
|
|
href="mailto:support@perfect-postcode.co.uk"
|
|
className="text-teal-600 dark:text-teal-400 hover:text-teal-800 dark:hover:text-teal-300 font-medium text-lg"
|
|
>
|
|
support@perfect-postcode.co.uk
|
|
</a>
|
|
<p className="text-warm-400 dark:text-warm-500 text-sm mt-2">
|
|
We typically respond within 24 hours.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|