Checkpoint all changes
This commit is contained in:
parent
65877acf95
commit
66c2a25457
28 changed files with 3035 additions and 621 deletions
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useMemo, useState } from 'react';
|
||||
import React, { useMemo, useState, useRef, useEffect } from 'react';
|
||||
import { Property } from '../types';
|
||||
|
||||
interface PropertiesPaneProps {
|
||||
|
|
@ -8,6 +8,10 @@ interface PropertiesPaneProps {
|
|||
hexagonId: string | null;
|
||||
onLoadMore: () => void;
|
||||
onClose: () => void;
|
||||
onNavigateToSource?: (slug: string) => void;
|
||||
isHoveredPreview?: boolean;
|
||||
hoverMode?: boolean;
|
||||
onHoverModeChange?: (enabled: boolean) => void;
|
||||
}
|
||||
|
||||
type SortBy = 'price' | 'size' | 'energy';
|
||||
|
|
@ -19,9 +23,26 @@ export function PropertiesPane({
|
|||
hexagonId,
|
||||
onLoadMore,
|
||||
onClose,
|
||||
onNavigateToSource,
|
||||
isHoveredPreview,
|
||||
hoverMode,
|
||||
onHoverModeChange,
|
||||
}: PropertiesPaneProps) {
|
||||
const [sortBy, setSortBy] = useState<SortBy>('price');
|
||||
const [search, setSearch] = useState('');
|
||||
const [showInfo, setShowInfo] = useState(false);
|
||||
const infoPopupRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!showInfo) return;
|
||||
function handleClickOutside(e: MouseEvent) {
|
||||
if (infoPopupRef.current && !infoPopupRef.current.contains(e.target as Node)) {
|
||||
setShowInfo(false);
|
||||
}
|
||||
}
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
return () => document.removeEventListener('mousedown', handleClickOutside);
|
||||
}, [showInfo]);
|
||||
|
||||
// Filter and sort properties
|
||||
const filteredAndSorted = useMemo(() => {
|
||||
|
|
@ -56,36 +77,112 @@ export function PropertiesPane({
|
|||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
{/* Header */}
|
||||
<div className="p-4 border-b border-warm-200 dark:border-warm-700">
|
||||
<div className="p-4 border-b border-warm-200 dark:border-navy-700">
|
||||
<div className="flex justify-between items-center">
|
||||
<h2 className="text-lg font-semibold dark:text-warm-100">Properties in Hexagon</h2>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="text-warm-500 hover:text-warm-700 dark:text-warm-400 dark:hover:text-warm-200 text-2xl leading-none"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
<div className="flex items-center gap-2">
|
||||
<h2 className="text-lg font-semibold dark:text-warm-100">Properties</h2>
|
||||
{isHoveredPreview && (
|
||||
<span className="text-xs px-1.5 py-0.5 rounded bg-teal-50 dark:bg-teal-900/30 text-teal-600 dark:text-teal-400">
|
||||
Preview
|
||||
</span>
|
||||
)}
|
||||
<button
|
||||
onClick={() => setShowInfo(true)}
|
||||
className="text-warm-400 hover:text-warm-700 dark:hover:text-warm-300 p-0.5 rounded"
|
||||
title="Data source info"
|
||||
>
|
||||
<svg className="w-3.5 h-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2}>
|
||||
<circle cx="12" cy="12" r="10" />
|
||||
<path strokeLinecap="round" d="M12 16v-4m0-4h.01" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
{onHoverModeChange && (
|
||||
<button
|
||||
onClick={() => onHoverModeChange(!hoverMode)}
|
||||
className={`p-1 rounded ${
|
||||
hoverMode
|
||||
? 'text-teal-600 dark:text-teal-400 bg-teal-50 dark:bg-teal-900/30'
|
||||
: 'text-warm-400 hover:text-warm-700 dark:hover:text-warm-300'
|
||||
}`}
|
||||
title={hoverMode ? 'Live preview on (click to lock)' : 'Live preview off (click to enable)'}
|
||||
>
|
||||
<svg className="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="text-warm-400 hover:text-warm-700 dark:hover:text-warm-300 p-1"
|
||||
>
|
||||
<svg className="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-sm text-warm-600 dark:text-warm-400">
|
||||
{search.trim()
|
||||
? `${filteredAndSorted.length} match${filteredAndSorted.length !== 1 ? 'es' : ''} in ${properties.length} loaded`
|
||||
: `Showing ${properties.length} of ${total} properties`}
|
||||
</p>
|
||||
{showInfo && (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/30">
|
||||
<div
|
||||
ref={infoPopupRef}
|
||||
className="bg-white dark:bg-navy-800 border border-warm-200 dark:border-navy-700 rounded-lg shadow-xl max-w-md w-full mx-4 p-5"
|
||||
>
|
||||
<div className="flex items-start justify-between mb-3">
|
||||
<h3 className="text-sm font-semibold text-warm-900 dark:text-warm-100 pr-4">
|
||||
Property Data
|
||||
</h3>
|
||||
<button
|
||||
onClick={() => setShowInfo(false)}
|
||||
className="text-warm-400 hover:text-warm-700 dark:hover:text-warm-300 shrink-0"
|
||||
>
|
||||
<svg className="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<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 age, and tenure from EPC
|
||||
surveys, plus the most recent sale price from the Land Registry.
|
||||
</p>
|
||||
{onNavigateToSource && (
|
||||
<button
|
||||
onClick={() => {
|
||||
onNavigateToSource('epc');
|
||||
setShowInfo(false);
|
||||
}}
|
||||
className="text-sm text-teal-600 dark:text-teal-400 hover:text-teal-800 dark:hover:text-teal-300 hover:underline"
|
||||
>
|
||||
View data source
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Search and sort controls */}
|
||||
<div className="p-2 border-b border-warm-200 dark:border-warm-700 space-y-2">
|
||||
<div className="p-2 border-b border-warm-200 dark:border-navy-700 space-y-2">
|
||||
<input
|
||||
type="text"
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
placeholder="Search by address or postcode..."
|
||||
className="w-full p-2 border border-warm-300 dark:border-warm-700 rounded text-sm bg-white dark:bg-warm-800 dark:text-warm-200 placeholder-warm-400 dark:placeholder-warm-500"
|
||||
className="w-full p-2 border border-warm-300 dark:border-navy-700 rounded text-sm bg-white dark:bg-navy-800 dark:text-warm-200 placeholder-warm-400 dark:placeholder-warm-500"
|
||||
/>
|
||||
<select
|
||||
value={sortBy}
|
||||
onChange={(e) => setSortBy(e.target.value as SortBy)}
|
||||
className="w-full p-2 border border-warm-300 dark:border-warm-700 rounded text-sm bg-white dark:bg-warm-800 dark:text-warm-200"
|
||||
className="w-full p-2 border border-warm-300 dark:border-navy-700 rounded text-sm bg-white dark:bg-navy-800 dark:text-warm-200"
|
||||
>
|
||||
<option value="price">Price (High to Low)</option>
|
||||
<option value="size">Size (Large to Small)</option>
|
||||
|
|
@ -156,7 +253,7 @@ function PropertyCard({ property }: { property: Property }) {
|
|||
const age = getNum(property, 'Approximate construction age', 'construction_age_band');
|
||||
|
||||
return (
|
||||
<div className="p-4 border-b border-warm-100 dark:border-warm-800 hover:bg-warm-50 dark:hover:bg-warm-800">
|
||||
<div className="p-4 border-b border-warm-100 dark:border-navy-800 hover:bg-warm-50 dark:hover:bg-navy-800">
|
||||
{/* Address & postcode */}
|
||||
<div className="font-semibold dark:text-warm-100">{property.address || 'Unknown Address'}</div>
|
||||
<div className="text-sm text-warm-600 dark:text-warm-400">{property.postcode}</div>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue