Udpates
This commit is contained in:
parent
c38d654ac7
commit
3e9fba5303
17 changed files with 195 additions and 174 deletions
|
|
@ -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>
|
||||
)}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue