All changes
This commit is contained in:
parent
593f380581
commit
49f7ec2f5a
60 changed files with 1783 additions and 679 deletions
|
|
@ -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 && (
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue