Udpates
This commit is contained in:
parent
c38d654ac7
commit
3e9fba5303
17 changed files with 195 additions and 174 deletions
|
|
@ -71,13 +71,7 @@ function DeleteDialog({
|
|||
);
|
||||
}
|
||||
|
||||
function NotesInput({
|
||||
value,
|
||||
onSave,
|
||||
}: {
|
||||
value: string;
|
||||
onSave: (notes: string) => void;
|
||||
}) {
|
||||
function NotesInput({ value, onSave }: { value: string; onSave: (notes: string) => void }) {
|
||||
const [text, setText] = useState(value);
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
|
|
@ -207,7 +201,7 @@ function SavedSearchesTab({
|
|||
No saved searches yet
|
||||
</p>
|
||||
<p className="text-sm text-warm-500 dark:text-warm-500">
|
||||
Save your dashboard filters and view to quickly return to them later.
|
||||
Save your filters and map view so you can pick up exactly where you left off.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
|
|
@ -333,7 +327,7 @@ function SavedPropertiesTab({
|
|||
No saved properties yet
|
||||
</p>
|
||||
<p className="text-sm text-warm-500 dark:text-warm-500">
|
||||
Click the bookmark icon on any property in the dashboard to save it here.
|
||||
Bookmark properties as you explore and build your shortlist without losing track.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
|
|
@ -367,10 +361,7 @@ function SavedPropertiesTab({
|
|||
</p>
|
||||
|
||||
<div className="mb-3">
|
||||
<NotesInput
|
||||
value={prop.notes}
|
||||
onSave={(notes) => onUpdateNotes(prop.id, notes)}
|
||||
/>
|
||||
<NotesInput value={prop.notes} onSave={(notes) => onUpdateNotes(prop.id, notes)} />
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
|
|
@ -823,7 +814,7 @@ export default function AccountPage({
|
|||
<span
|
||||
className={`inline-block text-sm font-medium px-2.5 py-0.5 rounded-full mt-1 ${badgeColor}`}
|
||||
>
|
||||
{user.subscription === 'licensed' ? 'Licensed' : 'Free'}
|
||||
{user.subscription === 'licensed' ? 'Full Access' : 'Inner London'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -181,13 +181,13 @@ export default function HomePage({
|
|||
</h2>
|
||||
<div className="space-y-4 text-lg md:text-xl leading-relaxed text-warm-700 dark:text-warm-300">
|
||||
<p>
|
||||
Listings show what's available, not what's possible — fragments
|
||||
without context. Traditional tools force you to begin with a location, separating area
|
||||
insight from property detail. You search, cross-reference, and repeat per location.
|
||||
On Rightmove, you pick an area first, then hope it's good. You end up
|
||||
cross-referencing crime stats, school reports, and broadband checkers across a dozen
|
||||
tabs, one postcode at a time.
|
||||
</p>
|
||||
<p>
|
||||
We take a different approach. Start with what matters to you, and the right places
|
||||
reveal themselves. No context lost. No property missed.
|
||||
We flip that. Tell us what you need (budget, commute, schools, safety) and we show
|
||||
you every area in England that qualifies. No guesswork. No wasted viewings.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -302,7 +302,7 @@ export default function HomePage({
|
|||
Make your biggest investment your smartest move.
|
||||
</h2>
|
||||
<p className="text-warm-600 dark:text-warm-400 mb-8 max-w-xl mx-auto leading-relaxed">
|
||||
This deserves proper tools behind it — don't leave it to luck.
|
||||
This deserves proper tools behind it, don't leave it to luck.
|
||||
</p>
|
||||
<button
|
||||
onClick={() => {
|
||||
|
|
|
|||
|
|
@ -141,8 +141,8 @@ const STEPS: { heading: string | null; body: React.ReactNode }[] = [
|
|||
</p>
|
||||
<p className="text-base md:text-lg leading-snug md:leading-relaxed">
|
||||
That's just 4 filters. We've built{' '}
|
||||
<strong className="text-navy-950 dark:text-warm-100">56</strong> — covering commute
|
||||
times, crime, broadband, noise, schools, amenities, and more.
|
||||
<strong className="text-navy-950 dark:text-warm-100">56</strong>, covering commute times,
|
||||
crime, broadband, noise, schools, amenities, and more.
|
||||
</p>
|
||||
</>
|
||||
),
|
||||
|
|
|
|||
|
|
@ -142,19 +142,19 @@ const FAQ_SECTIONS: FAQSection[] = [
|
|||
title: 'Finding Your Area',
|
||||
items: [
|
||||
{
|
||||
question: "I don't even know which areas to look at \u2014 can this help with that?",
|
||||
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 \u2014 whatever matters) and the map lights up to show you where ticks every box. No more Googling \"best areas to live near Manchester\" at 1am.",
|
||||
'That\'s exactly what it\'s for. Set your filters (budget, commute time, low crime, good schools, whatever matters) and the map lights up to show you where ticks every box. No more Googling "best areas to live near Manchester" at 1am.',
|
||||
},
|
||||
{
|
||||
question: "I'm moving somewhere I've never been \u2014 how do I even start?",
|
||||
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 \u2014 it's like having a local's knowledge of every neighbourhood in England.",
|
||||
"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. It's like having a local's knowledge of every neighbourhood in England.",
|
||||
},
|
||||
{
|
||||
question: 'How do I find areas that tick all my boxes at once?',
|
||||
answer:
|
||||
'Stack multiple filters \u2014 say, crime below average, good schools, and commute under 40 minutes \u2014 then colour the map by price to spot the affordable sweet spots. The map updates live as you drag sliders, so you can watch neighbourhoods light up or drop off in real time.',
|
||||
'Stack multiple filters (crime below average, good schools, commute under 40 minutes) then colour the map by price to spot the affordable sweet spots. The map updates live as you drag sliders, so you can watch neighbourhoods light up or drop off in real time.',
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
@ -164,7 +164,7 @@ const FAQ_SECTIONS: FAQSection[] = [
|
|||
{
|
||||
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 \u2014 by car, bike, or public transport. Filter to your max commute and the rest disappears, so you're only looking at areas that actually work.",
|
||||
"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?',
|
||||
|
|
@ -179,12 +179,12 @@ const FAQ_SECTIONS: FAQSection[] = [
|
|||
{
|
||||
question: 'How do I find areas where I get the most space for my money?',
|
||||
answer:
|
||||
"Filter by price per sqm \u2014 you'll instantly see which postcodes give you the most square footage per pound. Pair it with the energy rating filter to avoid cheap-but-freezing money pits.",
|
||||
"Filter by price per sqm and you'll instantly see which postcodes give you the most square footage per pound. Pair it with the energy rating filter to avoid cheap-but-freezing money pits.",
|
||||
},
|
||||
{
|
||||
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 the stuff that matters, that's your hidden gem \u2014 not just a cheap postcode with a catch.",
|
||||
"Layer deprivation scores, crime stats, school ratings, and broadband speeds alongside price. If a postcode is affordable AND scores well on the stuff that matters, that's your hidden gem, not just a cheap postcode with a catch.",
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
@ -194,12 +194,13 @@ const FAQ_SECTIONS: FAQSection[] = [
|
|||
{
|
||||
question: 'How can I check if an area is safe before I move there?',
|
||||
answer:
|
||||
"We overlay real police-recorded crime data \u2014 broken down by type \u2014 onto every neighbourhood in England. Filter by violent crime, burglary, or antisocial behaviour and instantly see which postcodes have the lowest numbers, so you're not relying on gut feeling.",
|
||||
"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 grim.',
|
||||
question:
|
||||
'I keep finding flats that look great online, then the area turns out to be grim.',
|
||||
answer:
|
||||
"That's why we built this. Stack crime rates, noise levels, deprivation scores, nearby pubs and parks, and broadband speeds all on one map \u2014 so you know what a neighbourhood is actually like before you waste a Saturday viewing.",
|
||||
"That's why we built this. 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 waste a Saturday viewing.",
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
@ -209,7 +210,7 @@ const FAQ_SECTIONS: FAQSection[] = [
|
|||
{
|
||||
question: 'Can I find areas with good schools AND low crime in one search?',
|
||||
answer:
|
||||
'Yes \u2014 stack filters for Ofsted ratings, crime rates, parks, and whatever else matters to your family, then watch the map highlight only the areas that tick every box. No more cross-referencing five different websites with a spreadsheet.',
|
||||
'Absolutely. Stack filters for Ofsted ratings, crime rates, parks, and whatever else matters to your family, then watch the map highlight only the areas that tick every box. No more cross-referencing five different websites with a spreadsheet.',
|
||||
},
|
||||
{
|
||||
question: 'How do I know if a neighbourhood has parks and playgrounds nearby?',
|
||||
|
|
@ -222,9 +223,9 @@ const FAQ_SECTIONS: FAQSection[] = [
|
|||
title: 'Environment & Quality of Life',
|
||||
items: [
|
||||
{
|
||||
question: 'Can I find energy-efficient homes that aren\'t on a noisy road?',
|
||||
question: "Can I find energy-efficient homes that aren't on a noisy road?",
|
||||
answer:
|
||||
'Filter by EPC rating (A\u2013C), 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.',
|
||||
'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?',
|
||||
|
|
@ -242,9 +243,9 @@ const FAQ_SECTIONS: FAQSection[] = [
|
|||
title: 'Why Perfect Postcode',
|
||||
items: [
|
||||
{
|
||||
question: 'I already use Rightmove \u2014 what does this add?',
|
||||
question: 'I already use Rightmove. What does this add?',
|
||||
answer:
|
||||
"Rightmove shows you houses. We show you areas. You'll see 56 layers of data \u2014 crime rates, school ratings, broadband speeds, noise levels, deprivation scores \u2014 all on one map, so you can judge a neighbourhood before you even look at listings.",
|
||||
"Rightmove shows you houses. We show you areas. You'll see 56 layers of data (crime rates, school ratings, broadband speeds, noise levels, deprivation scores) all on one map, so you can judge a neighbourhood before you even look at listings.",
|
||||
},
|
||||
{
|
||||
question: "Can't I just research all this myself for free?",
|
||||
|
|
@ -254,7 +255,7 @@ const FAQ_SECTIONS: FAQSection[] = [
|
|||
{
|
||||
question: 'Where does the data actually come from?',
|
||||
answer:
|
||||
"Every dataset comes from official UK government sources \u2014 Land Registry, the EPC register, ONS, Ofsted, Ofcom, data.police.uk, and Defra. We don't scrape estate agents or make anything up \u2014 you can verify any record against the original source.",
|
||||
"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.",
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
@ -264,7 +265,7 @@ const FAQ_SECTIONS: FAQSection[] = [
|
|||
{
|
||||
question: 'Is it really worth paying for a property search tool?',
|
||||
answer:
|
||||
"You're making a decision worth \u00a3200k\u2013\u00a3500k or more. Even spotting one red flag \u2014 a noisy road, poor broadband, rising crime \u2014 that changes your mind could save you years of regret. This costs less than a single viewing trip in petrol.",
|
||||
"You're making a decision worth \u00a3200k to \u00a3500k or more. Even spotting one red flag (a noisy road, poor broadband, rising crime) that changes your mind could save you years of regret. This costs less than a single viewing trip in petrol.",
|
||||
},
|
||||
{
|
||||
question: "Is this another subscription that'll drain my account?",
|
||||
|
|
@ -274,12 +275,12 @@ const FAQ_SECTIONS: FAQSection[] = [
|
|||
{
|
||||
question: 'What can I access on the free tier?',
|
||||
answer:
|
||||
'Free users can explore all features within inner London (roughly zones 1\u20132). To access data for the rest of England, you need lifetime access.',
|
||||
'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:
|
||||
'Yes \u2014 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.',
|
||||
'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.',
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
@ -289,12 +290,12 @@ const FAQ_SECTIONS: FAQSection[] = [
|
|||
{
|
||||
question: 'How do I use the AI filter instead of adding filters one by one?',
|
||||
answer:
|
||||
'Type what you want in plain English \u2014 something like "quiet area near good schools with fast broadband under \u00a3400k" \u2014 and it\'ll set up all the relevant filters in one go. Tweak any of them manually afterwards.',
|
||||
'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 \u2014 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.',
|
||||
'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?",
|
||||
|
|
@ -484,7 +485,7 @@ export default function LearnPage() {
|
|||
) : 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
|
||||
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">
|
||||
|
|
|
|||
|
|
@ -127,7 +127,7 @@ export default memo(function AiFilterInput({
|
|||
<SparklesIcon className="w-3.5 h-3.5 text-teal-500 dark:text-teal-400 shrink-0" />
|
||||
<span className="text-xs font-medium text-teal-700 dark:text-teal-300">AI Search</span>
|
||||
<span className="text-xs text-warm-400 dark:text-warm-500">
|
||||
— describe what you're looking for
|
||||
describe what you're looking for
|
||||
</span>
|
||||
</div>
|
||||
<form onSubmit={handleSubmit} className="flex items-center gap-1.5">
|
||||
|
|
|
|||
|
|
@ -78,7 +78,7 @@ export default function AreaPane({
|
|||
<EmptyState
|
||||
icon={<InfoIcon className="w-8 h-8 text-warm-300 dark:text-warm-600" />}
|
||||
title="No area selected"
|
||||
description="Click a hexagon or postcode to view area statistics"
|
||||
description="Click any coloured area on the map to see crime, schools, prices, and more"
|
||||
centered
|
||||
/>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -127,28 +127,28 @@ export default function FeatureBrowser({
|
|||
{isExpanded && (
|
||||
<>
|
||||
{group.features.map((f) => {
|
||||
const isPinned = pinnedFeature === f.name;
|
||||
return (
|
||||
<div
|
||||
key={f.name}
|
||||
className="flex items-center justify-between px-3 py-1.5 hover:bg-teal-50 dark:hover:bg-teal-900/30 dark:text-warm-300"
|
||||
>
|
||||
<div className="min-w-0 mr-2">
|
||||
<FeatureLabel feature={f} onShowInfo={setInfoFeature} size="sm" />
|
||||
{f.description && (
|
||||
<span className="text-xs text-warm-400 dark:text-warm-500 truncate block">
|
||||
{f.description}
|
||||
</span>
|
||||
)}
|
||||
const isPinned = pinnedFeature === f.name;
|
||||
return (
|
||||
<div
|
||||
key={f.name}
|
||||
className="flex items-center justify-between px-3 py-1.5 hover:bg-teal-50 dark:hover:bg-teal-900/30 dark:text-warm-300"
|
||||
>
|
||||
<div className="min-w-0 mr-2">
|
||||
<FeatureLabel feature={f} onShowInfo={setInfoFeature} size="sm" />
|
||||
{f.description && (
|
||||
<span className="text-xs text-warm-400 dark:text-warm-500 truncate block">
|
||||
{f.description}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<FeatureActions
|
||||
feature={f}
|
||||
isPinned={isPinned}
|
||||
onTogglePin={onTogglePin}
|
||||
onAdd={onAddFilter}
|
||||
/>
|
||||
</div>
|
||||
<FeatureActions
|
||||
feature={f}
|
||||
isPinned={isPinned}
|
||||
onTogglePin={onTogglePin}
|
||||
onAdd={onAddFilter}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
);
|
||||
})}
|
||||
{group.name === 'Transport' &&
|
||||
showTravelModes &&
|
||||
|
|
|
|||
|
|
@ -370,15 +370,14 @@ export default memo(function Filters({
|
|||
</div>
|
||||
{enabledFeatureList.length === 0 && activeEntryCount === 0 && (
|
||||
<p className="px-3 py-1.5 text-xs text-warm-400 dark:text-warm-500">
|
||||
Browse features below and click + to add a filter
|
||||
Add filters below to narrow the map to areas that match
|
||||
</p>
|
||||
)}
|
||||
|
||||
{mergedGroups.map((group) => {
|
||||
const isExpanded = !collapsedGroups.has(group.name);
|
||||
const isTransport = group.name === 'Transport';
|
||||
const groupCount =
|
||||
group.features.length + (isTransport ? travelTimeEntries.length : 0);
|
||||
const groupCount = group.features.length + (isTransport ? travelTimeEntries.length : 0);
|
||||
return (
|
||||
<div key={group.name}>
|
||||
<CollapsibleGroupHeader
|
||||
|
|
@ -428,10 +427,7 @@ export default memo(function Filters({
|
|||
className={`scroll-mt-10 space-y-0.5 px-2 py-1.5 rounded ${pinnedFeature === feature.name ? 'ring-2 ring-teal-400 bg-teal-50/50 dark:bg-teal-900/20' : ''}`}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<FeatureLabel
|
||||
feature={feature}
|
||||
size="sm"
|
||||
/>
|
||||
<FeatureLabel feature={feature} size="sm" />
|
||||
<FeatureActions
|
||||
feature={feature}
|
||||
isPinned={pinnedFeature === feature.name}
|
||||
|
|
@ -492,11 +488,7 @@ export default memo(function Filters({
|
|||
className={`scroll-mt-10 space-y-0.5 px-2 py-1.5 rounded ${isActive ? 'ring-2 ring-teal-400 bg-teal-50 dark:bg-teal-900/30' : isPinned ? 'ring-2 ring-teal-400 bg-teal-50/50 dark:bg-teal-900/20' : ''}`}
|
||||
>
|
||||
<div className="flex items-center justify-between gap-1">
|
||||
<FeatureLabel
|
||||
feature={feature}
|
||||
size="sm"
|
||||
className="min-w-0 shrink"
|
||||
/>
|
||||
<FeatureLabel feature={feature} size="sm" className="min-w-0 shrink" />
|
||||
<FeatureActions
|
||||
feature={feature}
|
||||
isPinned={isPinned}
|
||||
|
|
@ -584,79 +576,72 @@ export default memo(function Filters({
|
|||
<div className="space-y-4 text-sm">
|
||||
<p className="text-warm-600 dark:text-warm-300">
|
||||
Start with your must-haves, then layer on nice-to-haves. The map narrows down as you
|
||||
add filters — the areas that survive are your best matches.
|
||||
add filters. The areas that survive are your best matches.
|
||||
</p>
|
||||
|
||||
<div>
|
||||
<h4 className="font-semibold text-navy-950 dark:text-warm-100 mb-1">
|
||||
1. Budget & property basics
|
||||
</h4>
|
||||
<p className="text-warm-600 dark:text-warm-300">
|
||||
Set your price range, minimum floor area, and property type. If you need a lease
|
||||
over freehold (or vice versa), filter for that too. This eliminates most of the map
|
||||
immediately.
|
||||
</p>
|
||||
<div className="space-y-2">
|
||||
<div className="flex gap-2">
|
||||
<span className="shrink-0 w-5 h-5 rounded-full bg-teal-600 text-white flex items-center justify-center text-xs font-bold">
|
||||
1
|
||||
</span>
|
||||
<p className="text-warm-600 dark:text-warm-300">
|
||||
<span className="font-semibold text-navy-950 dark:text-warm-100">
|
||||
Budget & basics
|
||||
</span>{' '}
|
||||
(price range, floor area, property type)
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<span className="shrink-0 w-5 h-5 rounded-full bg-teal-600 text-white flex items-center justify-center text-xs font-bold">
|
||||
2
|
||||
</span>
|
||||
<p className="text-warm-600 dark:text-warm-300">
|
||||
<span className="font-semibold text-navy-950 dark:text-warm-100">Commute</span>{' '}
|
||||
(travel time to your workplace by car, bike, or transit)
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<span className="shrink-0 w-5 h-5 rounded-full bg-teal-600 text-white flex items-center justify-center text-xs font-bold">
|
||||
3
|
||||
</span>
|
||||
<p className="text-warm-600 dark:text-warm-300">
|
||||
<span className="font-semibold text-navy-950 dark:text-warm-100">Safety</span>{' '}
|
||||
(crime rates, noise levels, ground stability)
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<span className="shrink-0 w-5 h-5 rounded-full bg-teal-600 text-white flex items-center justify-center text-xs font-bold">
|
||||
4
|
||||
</span>
|
||||
<p className="text-warm-600 dark:text-warm-300">
|
||||
<span className="font-semibold text-navy-950 dark:text-warm-100">Schools</span>{' '}
|
||||
(nearby Ofsted-rated Good or Outstanding schools)
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<span className="shrink-0 w-5 h-5 rounded-full bg-teal-600 text-white flex items-center justify-center text-xs font-bold">
|
||||
5
|
||||
</span>
|
||||
<p className="text-warm-600 dark:text-warm-300">
|
||||
<span className="font-semibold text-navy-950 dark:text-warm-100">Lifestyle</span>{' '}
|
||||
(restaurants, parks, broadband speed)
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<span className="shrink-0 w-5 h-5 rounded-full bg-teal-600 text-white flex items-center justify-center text-xs font-bold">
|
||||
6
|
||||
</span>
|
||||
<p className="text-warm-600 dark:text-warm-300">
|
||||
<span className="font-semibold text-navy-950 dark:text-warm-100">Energy</span>{' '}
|
||||
(EPC ratings for lower bills and fewer surprises)
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 className="font-semibold text-navy-950 dark:text-warm-100 mb-1">
|
||||
2. Commute & transport
|
||||
</h4>
|
||||
<p className="text-warm-600 dark:text-warm-300">
|
||||
Add a travel time filter to your workplace — choose public transport or
|
||||
cycling and set your maximum tolerable commute. You can also filter by how many
|
||||
stations are within walking distance.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 className="font-semibold text-navy-950 dark:text-warm-100 mb-1">
|
||||
3. Safety & environment
|
||||
</h4>
|
||||
<p className="text-warm-600 dark:text-warm-300">
|
||||
Use the crime filters to cap serious or minor crime rates. Check road noise levels
|
||||
if you're a light sleeper, and environmental risk filters for ground stability
|
||||
concerns.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 className="font-semibold text-navy-950 dark:text-warm-100 mb-1">
|
||||
4. Schools & education
|
||||
</h4>
|
||||
<p className="text-warm-600 dark:text-warm-300">
|
||||
Filter by the number of Ofsted-rated Good or Outstanding primary and secondary
|
||||
schools nearby. The education deprivation score captures broader area-level
|
||||
attainment.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 className="font-semibold text-navy-950 dark:text-warm-100 mb-1">
|
||||
5. Lifestyle & amenities
|
||||
</h4>
|
||||
<p className="text-warm-600 dark:text-warm-300">
|
||||
Want restaurants, parks, or grocery shops within walking distance? Filter by nearby
|
||||
amenity counts. Broadband speed filters help if you work from home.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 className="font-semibold text-navy-950 dark:text-warm-100 mb-1">
|
||||
6. Energy & running costs
|
||||
</h4>
|
||||
<p className="text-warm-600 dark:text-warm-300">
|
||||
EPC ratings from A to G indicate energy efficiency. Filter for better ratings to
|
||||
find homes with lower bills and fewer upgrade headaches.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="pt-1 border-t border-warm-200 dark:border-warm-700">
|
||||
<p className="text-warm-500 dark:text-warm-400 italic">
|
||||
Tip: if nothing survives your filters, relax one constraint at a time to see which
|
||||
compromise unlocks the most options.
|
||||
</p>
|
||||
</div>
|
||||
<p className="text-warm-500 dark:text-warm-400 italic text-xs">
|
||||
Tip: if nothing survives, relax one constraint at a time to see which compromise
|
||||
unlocks the most options.
|
||||
</p>
|
||||
|
||||
{onResetTutorial && (
|
||||
<button
|
||||
|
|
|
|||
|
|
@ -64,6 +64,27 @@ function getRouteDisplay(mode: string): { label: string; color: string; darkText
|
|||
return { label: clean, color: '#6b7280', darkText: false };
|
||||
}
|
||||
|
||||
/** Returns a Unix timestamp for the next Monday at 07:30 local time. */
|
||||
function nextMondayAt730(): number {
|
||||
const now = new Date();
|
||||
const day = now.getDay(); // 0=Sun … 6=Sat
|
||||
const daysUntil = day === 0 ? 1 : day === 1 ? 7 : 8 - day;
|
||||
const monday = new Date(now);
|
||||
monday.setDate(now.getDate() + daysUntil);
|
||||
monday.setHours(7, 30, 0, 0);
|
||||
return Math.floor(monday.getTime() / 1000);
|
||||
}
|
||||
|
||||
function googleMapsUrl(postcode: string, destination: string): string {
|
||||
const params = new URLSearchParams({
|
||||
api: '1',
|
||||
origin: postcode,
|
||||
destination,
|
||||
travelmode: 'transit',
|
||||
});
|
||||
return `https://www.google.com/maps/dir/?${params}&departure_time=${nextMondayAt730()}`;
|
||||
}
|
||||
|
||||
function invertLegs(legs: JourneyLeg[]): JourneyLeg[] {
|
||||
return [...legs]
|
||||
.reverse()
|
||||
|
|
@ -235,6 +256,17 @@ export default function JourneyInstructions({
|
|||
{displayLegs.map((leg, i) => (
|
||||
<TimelineLeg key={i} leg={leg} isLast={i === displayLegs.length - 1} />
|
||||
))}
|
||||
<a
|
||||
href={googleMapsUrl(postcode, j.label || j.slug)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="mt-2 flex items-center justify-center gap-1.5 w-full text-[11px] font-medium text-teal-600 dark:text-teal-400 hover:text-teal-800 dark:hover:text-teal-300 bg-white dark:bg-warm-900 border border-warm-200 dark:border-warm-700 rounded-md py-1.5 transition-colors"
|
||||
>
|
||||
View on Google Maps
|
||||
<svg className="w-3 h-3" viewBox="0 0 12 12" fill="none" stroke="currentColor" strokeWidth="1.5">
|
||||
<path d="M4.5 1.5H2a.5.5 0 00-.5.5v8a.5.5 0 00.5.5h8a.5.5 0 00.5-.5V7.5M7.5 1.5H10.5V4.5M10.5 1.5L5.5 6.5" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
) : (
|
||||
<span className="text-xs text-warm-500 dark:text-warm-400">
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ export function PropertiesPane({
|
|||
<EmptyState
|
||||
icon={<InfoIcon className="w-8 h-8 text-warm-300 dark:text-warm-600" />}
|
||||
title="No area selected"
|
||||
description="Click a hexagon or postcode to view area statistics"
|
||||
description="Click any coloured area on the map to see crime, schools, prices, and more"
|
||||
centered
|
||||
/>
|
||||
);
|
||||
|
|
@ -77,10 +77,9 @@ export function PropertiesPane({
|
|||
}
|
||||
>
|
||||
<p className="text-sm text-warm-700 dark:text-warm-300 mb-4 leading-relaxed">
|
||||
Property data combines Energy Performance Certificates (EPC) with HM Land Registry Price
|
||||
Paid records, fuzzy-matched by address within each postcode. Includes floor area, energy
|
||||
ratings, construction year, and tenure from EPC surveys, plus the most recent sale price
|
||||
from the Land Registry.
|
||||
Prices come from HM Land Registry (what buyers actually paid). Floor area, energy
|
||||
ratings, construction year, and tenure come from official EPC surveys. Both sources are
|
||||
matched by address within each postcode.
|
||||
</p>
|
||||
</InfoPopup>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -127,6 +127,13 @@ export default function AuthModal({
|
|||
)}
|
||||
|
||||
<div className="p-5 space-y-4">
|
||||
{/* Value prop */}
|
||||
{view !== 'forgot' && (
|
||||
<p className="text-xs text-warm-500 dark:text-warm-400 text-center">
|
||||
Save searches, bookmark properties, and pick up where you left off.
|
||||
</p>
|
||||
)}
|
||||
|
||||
{/* OAuth buttons (hidden in forgot view) */}
|
||||
{view !== 'forgot' && (
|
||||
<>
|
||||
|
|
|
|||
|
|
@ -157,7 +157,7 @@ export default function Header({
|
|||
className={tabClass('invites')}
|
||||
onClick={(e) => navLink('invites', e)}
|
||||
>
|
||||
Invite
|
||||
Invite Friends
|
||||
</a>
|
||||
</>
|
||||
)}
|
||||
|
|
@ -238,7 +238,12 @@ export default function Header({
|
|||
{!isMobile && (
|
||||
<>
|
||||
{user ? (
|
||||
<UserMenu user={user} theme={theme} onToggleTheme={onToggleTheme} onLogout={onLogout} />
|
||||
<UserMenu
|
||||
user={user}
|
||||
theme={theme}
|
||||
onToggleTheme={onToggleTheme}
|
||||
onLogout={onLogout}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<button
|
||||
|
|
|
|||
|
|
@ -89,7 +89,7 @@ export default function MobileMenu({
|
|||
!user?.isAdmin &&
|
||||
mobileNavItem('pricing', 'Pricing')}
|
||||
{user && mobileNavItem('saved', 'Saved')}
|
||||
{user && mobileNavItem('invites', 'Invite')}
|
||||
{user && mobileNavItem('invites', 'Invite Friends')}
|
||||
{user && mobileNavItem('account', 'Account')}
|
||||
|
||||
{/* Dashboard actions */}
|
||||
|
|
|
|||
|
|
@ -60,9 +60,10 @@ export default function UpgradeModal({
|
|||
|
||||
{/* Header */}
|
||||
<div className="bg-gradient-to-br from-navy-950 to-teal-900 px-6 py-8 text-center">
|
||||
<h2 className="text-2xl font-bold text-white mb-2">Unlock the full map</h2>
|
||||
<h2 className="text-2xl font-bold text-white mb-2">See all of England</h2>
|
||||
<p className="text-warm-300 text-sm">
|
||||
Free users can explore inner London. Upgrade for lifetime access to all of England.
|
||||
You're currently exploring inner London. Get lifetime access to every postcode,
|
||||
every filter, every neighbourhood. One payment, forever.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
|
@ -118,7 +119,7 @@ export default function UpgradeModal({
|
|||
onClick={onZoomToFreeZone}
|
||||
className="w-full mt-4 text-center text-sm text-warm-400 dark:text-warm-500 hover:text-warm-600 dark:hover:text-warm-400"
|
||||
>
|
||||
Or zoom back to demo area
|
||||
Or continue exploring inner London
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ export default function UserMenu({
|
|||
: 'bg-warm-100 text-warm-500 dark:bg-warm-700 dark:text-warm-400'
|
||||
}`}
|
||||
>
|
||||
{user.subscription === 'licensed' || user.isAdmin ? 'Pro' : 'Free'}
|
||||
{user.subscription === 'licensed' || user.isAdmin ? 'Full Access' : 'Inner London'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -7,48 +7,48 @@ const STORAGE_KEY = 'tutorial_completed';
|
|||
const STEPS: Step[] = [
|
||||
{
|
||||
target: '[data-tutorial="filters"]',
|
||||
title: 'Filter Properties',
|
||||
title: 'Tell the map what matters',
|
||||
content:
|
||||
'Use filters to narrow down to areas which contain matching properties. Filter by crime rate, number of schools around, or filter to an area with detached houses. Pin a filter with the eye icon to colour the map by that feature.',
|
||||
'Set your budget, commute limit, school quality, crime threshold \u2014 whatever matters to you. Only areas that qualify stay lit. Use the eye icon to colour by any feature.',
|
||||
placement: 'right',
|
||||
disableBeacon: true,
|
||||
},
|
||||
{
|
||||
target: '[data-tutorial="ai-filters"]',
|
||||
title: 'AI-Powered Filters',
|
||||
title: 'Or just describe it',
|
||||
content:
|
||||
'Describe your ideal area in plain English — like "quiet neighbourhood with good schools" — and AI will set up the right filters for you automatically.',
|
||||
'Type what you want in plain English \u2014 like "quiet area near good schools under \u00A3400k" \u2014 and we\u2019ll set up the filters for you.',
|
||||
placement: 'right',
|
||||
disableBeacon: true,
|
||||
},
|
||||
{
|
||||
target: '[data-tutorial="map"]',
|
||||
title: 'Explore the Map',
|
||||
title: 'Explore what\u2019s out there',
|
||||
content:
|
||||
'Pan and zoom to explore property data across England. Click any area (hexagon or postcode boundary) to see detailed stats of historical or currently sold properties matching your filters.',
|
||||
'Pan and zoom across England. Click any coloured area to see crime, schools, prices, broadband, noise \u2014 everything about that neighbourhood.',
|
||||
placement: 'bottom',
|
||||
disableBeacon: true,
|
||||
},
|
||||
{
|
||||
target: '[data-tutorial="search"]',
|
||||
title: 'Search Locations',
|
||||
content: 'Search for a place name or postcode to jump directly to that area on the map.',
|
||||
title: 'Jump to a location',
|
||||
content: 'Search for any place or postcode to fly straight there.',
|
||||
placement: 'bottom',
|
||||
disableBeacon: true,
|
||||
},
|
||||
{
|
||||
target: '[data-tutorial="right-pane"]',
|
||||
title: 'Area Stats & Properties',
|
||||
title: 'Dig into the details',
|
||||
content:
|
||||
'After clicking a hexagon, view aggregated area statistics or browse individual properties in this pane.',
|
||||
'See area statistics, histograms, and individual property records \u2014 prices, floor area, energy ratings, and more.',
|
||||
placement: 'left',
|
||||
disableBeacon: true,
|
||||
},
|
||||
{
|
||||
target: '[data-tutorial="poi-button"]',
|
||||
title: 'Points of Interest',
|
||||
title: 'What\u2019s nearby?',
|
||||
content:
|
||||
'Toggle points of interest like schools, shops, and transport stops to see what amenities are nearby.',
|
||||
'Toggle schools, shops, stations, parks, and restaurants on the map to see what\u2019s within reach.',
|
||||
placement: 'left',
|
||||
disableBeacon: true,
|
||||
styles: {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue