import React, { useMemo, useState } from 'react'; import { Property } from '../../types'; import { formatDuration, formatAge, formatNumber, formatTransactionDate } from '../../lib/format'; import { getNum } from '../../lib/property-fields'; import InfoPopup from '../ui/InfoPopup'; import { SearchInput } from '../ui/SearchInput'; import { EmptyState } from '../ui/EmptyState'; import { InfoIcon } from '../ui/icons'; interface PropertiesPaneProps { properties: Property[]; total: number; loading: boolean; hexagonId: string | null; onLoadMore: () => void; onClose: () => void; onNavigateToSource?: (slug: string) => void; } export function PropertiesPane({ properties, total, loading, hexagonId, onLoadMore, onClose: _onClose, onNavigateToSource, }: PropertiesPaneProps) { const [search, setSearch] = useState(''); const [showInfo, setShowInfo] = useState(false); const filtered = useMemo(() => { const query = search.trim().toLowerCase(); return query ? properties.filter((p) => { const addr = (p.address || '').toLowerCase(); const pc = (p.postcode || '').toLowerCase(); return addr.includes(query) || pc.includes(query); }) : properties; }, [properties, search]); if (!hexagonId) { return ( } title="No area selected" description="Click a hexagon or postcode to view area statistics" centered /> ); } return (
{showInfo && ( setShowInfo(false)} sourceLink={ onNavigateToSource ? { label: 'View data source', onClick: () => { onNavigateToSource('epc'); setShowInfo(false); }, } : undefined } >

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 age, and tenure from EPC surveys, plus the most recent sale price from the Land Registry.

)}
{loading && properties.length === 0 ? ( ) : ( <> {filtered.map((property, idx) => ( ))} {properties.length < total && ( )} )}
); } function PropertyLoadingSkeleton() { return (
{Array.from({ length: 5 }).map((_, idx) => (
{/* Address */}
{/* Postcode */}
{/* Price */}
{/* Property details grid */}
{Array.from({ length: 6 }).map((_, i) => (
))}
))}
); } function PropertyCard({ property }: { property: Property }) { const price = getNum(property, 'Last known price'); const estimatedPrice = getNum(property, 'Estimated current price'); const pricePerSqm = getNum(property, 'Price per sqm'); const estPricePerSqm = getNum(property, 'Est. price per sqm'); const floorArea = getNum(property, 'Total floor area (sqm)'); const rooms = getNum(property, 'Rooms (including bedrooms & bathrooms)'); const age = getNum(property, 'Approximate construction age'); const transactionDate = getNum(property, 'Date of last transaction'); const councilTax = getNum(property, 'Council tax (£/yr)'); const councilTaxD = getNum(property, 'Council tax Band D (£/yr)'); return (
{property.address || 'Unknown Address'}
{property.postcode}
{price !== undefined && (
£{formatNumber(price)} {transactionDate !== undefined && ( {' '} ({formatTransactionDate(transactionDate)}) )} {pricePerSqm !== undefined && ( {' '} £{formatNumber(pricePerSqm)}/m² )}
)} {estimatedPrice !== undefined && (
Est. value:{' '} £{formatNumber(estimatedPrice)} {estPricePerSqm !== undefined && ( (£{formatNumber(estPricePerSqm)}/m²) )}
)}
{property.property_type && (
Type: {property.property_type}
)} {property.built_form && (
Built form:{' '} {property.built_form}
)} {property.duration && (
Tenure:{' '} {formatDuration(property.duration)}
)} {floorArea !== undefined && (
Floor area:{' '} {formatNumber(floorArea)}m²
)} {rooms !== undefined && (
Rooms: {formatNumber(rooms)}
)} {age !== undefined && (
Built:{' '} {formatAge(age, property.is_construction_date_approximate ?? true)}
)} {property.current_energy_rating && (
EPC rating:{' '} {property.current_energy_rating}
)} {property.potential_energy_rating && (
EPC potential:{' '} {property.potential_energy_rating}
)} {councilTax !== undefined ? (
Council tax: £ {formatNumber(councilTax)}/yr
) : councilTaxD !== undefined ? (
Council tax (D): £ {formatNumber(councilTaxD)}/yr
) : null}
); }