changes
This commit is contained in:
parent
524580eb25
commit
ffe080adef
82 changed files with 2652 additions and 2956 deletions
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useMemo, useState } from 'react';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { Property } from '../../types';
|
||||
import { formatDuration, formatAge, formatNumber, formatTransactionDate } from '../../lib/format';
|
||||
import { getNum } from '../../lib/property-fields';
|
||||
|
|
@ -13,7 +13,6 @@ interface PropertiesPaneProps {
|
|||
loading: boolean;
|
||||
hexagonId: string | null;
|
||||
onLoadMore: () => void;
|
||||
onClose: () => void;
|
||||
onNavigateToSource?: (slug: string) => void;
|
||||
}
|
||||
|
||||
|
|
@ -23,7 +22,6 @@ export function PropertiesPane({
|
|||
loading,
|
||||
hexagonId,
|
||||
onLoadMore,
|
||||
onClose: _onClose,
|
||||
onNavigateToSource,
|
||||
}: PropertiesPaneProps) {
|
||||
const [search, setSearch] = useState('');
|
||||
|
|
@ -123,13 +121,9 @@ function PropertyLoadingSkeleton() {
|
|||
<div className="space-y-0">
|
||||
{Array.from({ length: 5 }).map((_, idx) => (
|
||||
<div key={idx} className="p-4 border-b border-warm-100 dark:border-navy-800 animate-pulse">
|
||||
{/* Address */}
|
||||
<div className="h-5 w-3/4 bg-warm-200 dark:bg-warm-700 rounded mb-2" />
|
||||
{/* Postcode */}
|
||||
<div className="h-4 w-24 bg-warm-200 dark:bg-warm-700 rounded mb-3" />
|
||||
{/* Price */}
|
||||
<div className="h-6 w-32 bg-warm-200 dark:bg-warm-700 rounded mb-3" />
|
||||
{/* Property details grid */}
|
||||
<div className="grid grid-cols-2 gap-x-4 gap-y-2">
|
||||
{Array.from({ length: 6 }).map((_, i) => (
|
||||
<div key={i} className="h-4 bg-warm-200 dark:bg-warm-700 rounded" />
|
||||
|
|
@ -141,39 +135,93 @@ function PropertyLoadingSkeleton() {
|
|||
);
|
||||
}
|
||||
|
||||
const LISTING_STATUS_STYLES: Record<string, string> = {
|
||||
'For sale': 'bg-teal-100 text-teal-800 dark:bg-teal-900/40 dark:text-teal-300',
|
||||
'For rent': 'bg-blue-100 text-blue-800 dark:bg-blue-900/40 dark:text-blue-300',
|
||||
'Historical sale': 'bg-warm-100 text-warm-600 dark:bg-warm-700 dark:text-warm-300',
|
||||
};
|
||||
|
||||
function ListingStatusBadge({ status }: { status: string }) {
|
||||
const style = LISTING_STATUS_STYLES[status] ?? LISTING_STATUS_STYLES['Historical sale'];
|
||||
return <span className={`text-xs font-medium px-1.5 py-0.5 rounded ${style}`}>{status}</span>;
|
||||
}
|
||||
|
||||
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 rooms = getNum(property, 'Number of bedrooms & living rooms');
|
||||
const age = getNum(property, '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)');
|
||||
const askingPrice = getNum(property, 'Asking price');
|
||||
const askingRent = getNum(property, 'Asking rent (monthly)');
|
||||
const bedrooms = getNum(property, 'Bedrooms');
|
||||
const bathrooms = getNum(property, 'Bathrooms');
|
||||
const listingDate = getNum(property, 'Listing date');
|
||||
|
||||
const listingStatus = property.listing_status;
|
||||
|
||||
return (
|
||||
<div className="p-4 border-b border-warm-100 dark:border-navy-800 hover:bg-warm-50 dark:hover:bg-navy-800">
|
||||
<div className="font-semibold dark:text-warm-100">
|
||||
{property.address || 'Unknown Address'}
|
||||
<div className="p-4 border-b border-warm-100 dark:border-warm-800 hover:bg-warm-50 dark:hover:bg-warm-800">
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div>
|
||||
<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>
|
||||
{listingStatus && <ListingStatusBadge status={listingStatus} />}
|
||||
</div>
|
||||
<div className="text-sm text-warm-600 dark:text-warm-400">{property.postcode}</div>
|
||||
|
||||
{price !== undefined && (
|
||||
{property.property_sub_type && (
|
||||
<div className="text-sm text-warm-600 dark:text-warm-400 mt-1">
|
||||
{property.property_sub_type}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{askingPrice !== undefined && (
|
||||
<div className="mt-2 text-lg font-bold text-teal-700 dark:text-teal-400">
|
||||
£{formatNumber(price)}
|
||||
{transactionDate !== undefined && (
|
||||
<span className="text-sm font-normal text-warm-600 dark:text-warm-400">
|
||||
{' '}
|
||||
({formatTransactionDate(transactionDate)})
|
||||
{property.price_qualifier && (
|
||||
<span className="text-sm font-normal text-warm-500 dark:text-warm-400">
|
||||
{property.price_qualifier}{' '}
|
||||
</span>
|
||||
)}
|
||||
{pricePerSqm !== undefined && (
|
||||
£{formatNumber(askingPrice)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{askingRent !== undefined && (
|
||||
<div className="mt-2 text-lg font-bold text-teal-700 dark:text-teal-400">
|
||||
£{formatNumber(askingRent)}
|
||||
<span className="text-sm font-normal text-warm-600 dark:text-warm-400">/mo</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{price !== undefined && (
|
||||
<div className={`${askingPrice !== undefined || askingRent !== undefined ? '' : 'mt-2 '}text-lg font-bold text-teal-700 dark:text-teal-400`}>
|
||||
{askingPrice !== undefined || askingRent !== undefined ? (
|
||||
<span className="text-sm font-normal text-warm-600 dark:text-warm-400">
|
||||
{' '}
|
||||
£{formatNumber(pricePerSqm)}/m²
|
||||
Last sold: £{formatNumber(price)}
|
||||
{transactionDate !== undefined && ` (${formatTransactionDate(transactionDate)})`}
|
||||
</span>
|
||||
) : (
|
||||
<>
|
||||
£{formatNumber(price)}
|
||||
{transactionDate !== undefined && (
|
||||
<span className="text-sm font-normal text-warm-600 dark:text-warm-400">
|
||||
{' '}
|
||||
({formatTransactionDate(transactionDate)})
|
||||
</span>
|
||||
)}
|
||||
{pricePerSqm !== undefined && (
|
||||
<span className="text-sm font-normal text-warm-600 dark:text-warm-400">
|
||||
{' '}
|
||||
£{formatNumber(pricePerSqm)}/m²
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -213,6 +261,18 @@ function PropertyCard({ property }: { property: Property }) {
|
|||
{formatNumber(floorArea)}m²
|
||||
</div>
|
||||
)}
|
||||
{bedrooms !== undefined && (
|
||||
<div>
|
||||
<span className="text-warm-500 dark:text-warm-400">Bedrooms:</span>{' '}
|
||||
{formatNumber(bedrooms)}
|
||||
</div>
|
||||
)}
|
||||
{bathrooms !== undefined && (
|
||||
<div>
|
||||
<span className="text-warm-500 dark:text-warm-400">Bathrooms:</span>{' '}
|
||||
{formatNumber(bathrooms)}
|
||||
</div>
|
||||
)}
|
||||
{rooms !== undefined && (
|
||||
<div>
|
||||
<span className="text-warm-500 dark:text-warm-400">Rooms:</span> {formatNumber(rooms)}
|
||||
|
|
@ -236,19 +296,30 @@ function PropertyCard({ property }: { property: Property }) {
|
|||
{property.potential_energy_rating}
|
||||
</div>
|
||||
)}
|
||||
{councilTax !== undefined ? (
|
||||
{listingDate !== undefined && (
|
||||
<div>
|
||||
<span className="text-warm-500 dark:text-warm-400">Council tax:</span> £
|
||||
{formatNumber(councilTax)}/yr
|
||||
<span className="text-warm-500 dark:text-warm-400">Listed:</span>{' '}
|
||||
{formatTransactionDate(listingDate)}
|
||||
</div>
|
||||
) : councilTaxD !== undefined ? (
|
||||
<div>
|
||||
<span className="text-warm-500 dark:text-warm-400">Council tax (D):</span> £
|
||||
{formatNumber(councilTaxD)}/yr
|
||||
</div>
|
||||
) : null}
|
||||
)}
|
||||
</div>
|
||||
|
||||
{property.listing_features && property.listing_features.length > 0 && (
|
||||
<div className="mt-2">
|
||||
<div className="text-xs text-warm-500 dark:text-warm-400 mb-1">Key features</div>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{property.listing_features.map((feature, idx) => (
|
||||
<span
|
||||
key={idx}
|
||||
className="text-xs bg-warm-100 dark:bg-warm-700 text-warm-700 dark:text-warm-300 rounded px-1.5 py-0.5"
|
||||
>
|
||||
{feature}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{property.renovation_history && property.renovation_history.length > 0 && (
|
||||
<div className="mt-2">
|
||||
<div className="text-xs text-warm-500 dark:text-warm-400 mb-1">Renovations</div>
|
||||
|
|
@ -265,6 +336,19 @@ function PropertyCard({ property }: { property: Property }) {
|
|||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{property.listing_url && (
|
||||
<div className="mt-2">
|
||||
<a
|
||||
href={property.listing_url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-xs text-teal-600 dark:text-teal-400 hover:text-teal-800 dark:hover:text-teal-300"
|
||||
>
|
||||
View external listing →
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue