All changes

This commit is contained in:
Andras Schmelczer 2026-03-14 21:36:00 +00:00
parent 593f380581
commit 49f7ec2f5a
60 changed files with 1783 additions and 679 deletions

View file

@ -1,4 +1,4 @@
import { useMemo, useState } from 'react';
import { useMemo, useState, useCallback } from 'react';
import { Property } from '../../types';
import { formatDuration, formatAge, formatNumber, formatTransactionDate } from '../../lib/format';
import { getNum } from '../../lib/property-fields';
@ -6,6 +6,7 @@ import InfoPopup from '../ui/InfoPopup';
import { SearchInput } from '../ui/SearchInput';
import { EmptyState } from '../ui/EmptyState';
import { InfoIcon } from '../ui/icons';
import { BookmarkIcon } from '../ui/icons/BookmarkIcon';
interface PropertiesPaneProps {
properties: Property[];
@ -14,6 +15,10 @@ interface PropertiesPaneProps {
hexagonId: string | null;
onLoadMore: () => void;
onNavigateToSource?: (slug: string) => void;
onSaveProperty?: (property: Property) => void;
onUnsaveProperty?: (id: string) => void;
isPropertySaved?: (address?: string, postcode?: string) => boolean;
getSavedPropertyId?: (address?: string, postcode?: string) => string | undefined;
}
export function PropertiesPane({
@ -23,6 +28,10 @@ export function PropertiesPane({
hexagonId,
onLoadMore,
onNavigateToSource,
onSaveProperty,
onUnsaveProperty,
isPropertySaved,
getSavedPropertyId,
}: PropertiesPaneProps) {
const [search, setSearch] = useState('');
const [showInfo, setShowInfo] = useState(false);
@ -70,7 +79,7 @@ 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 age, and tenure from EPC surveys, plus the most recent sale price
ratings, construction year, and tenure from EPC surveys, plus the most recent sale price
from the Land Registry.
</p>
</InfoPopup>
@ -91,7 +100,14 @@ export function PropertiesPane({
) : (
<>
{filtered.map((property, idx) => (
<PropertyCard key={idx} property={property} />
<PropertyCard
key={idx}
property={property}
onSave={onSaveProperty}
onUnsave={onUnsaveProperty}
isSaved={isPropertySaved?.(property.address, property.postcode)}
savedId={getSavedPropertyId?.(property.address, property.postcode)}
/>
))}
{properties.length < total && (
<button
@ -135,14 +151,33 @@ function PropertyLoadingSkeleton() {
);
}
function PropertyCard({ property }: { property: Property }) {
function PropertyCard({
property,
onSave,
onUnsave,
isSaved,
savedId,
}: {
property: Property;
onSave?: (property: Property) => void;
onUnsave?: (id: string) => void;
isSaved?: boolean;
savedId?: string;
}) {
const handleToggleSave = useCallback(() => {
if (isSaved && savedId && onUnsave) {
onUnsave(savedId);
} else if (onSave) {
onSave(property);
}
}, [isSaved, savedId, onSave, onUnsave, 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, 'Number of bedrooms & living rooms');
const age = getNum(property, 'Construction age');
const age = getNum(property, 'Construction year');
const transactionDate = getNum(property, 'Date of last transaction');
const askingPrice = getNum(property, 'Asking price');
const askingRent = getNum(property, 'Asking rent (monthly)');
@ -152,11 +187,26 @@ function PropertyCard({ property }: { property: Property }) {
return (
<div className="p-4 border-b border-warm-100 dark:border-warm-800 hover:bg-warm-50 dark:hover:bg-warm-800">
<div>
<div className="font-semibold dark:text-warm-100">
{property.address || 'Unknown Address'}
<div className="flex items-start justify-between gap-2">
<div className="min-w-0">
<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>
</div>
<div className="text-sm text-warm-600 dark:text-warm-400">{property.postcode}</div>
{onSave && (
<button
onClick={handleToggleSave}
className={`shrink-0 p-1 rounded ${
isSaved
? 'text-teal-600 dark:text-teal-400'
: 'text-warm-300 dark:text-warm-600 hover:text-warm-500 dark:hover:text-warm-400'
}`}
title={isSaved ? 'Unsave property' : 'Save property'}
>
<BookmarkIcon className="w-4 h-4" filled={isSaved} />
</button>
)}
</div>
{property.property_sub_type && (