// Sold archive — header stats + table + gallery const SoldPage = ({ showToast }) => { useLucide(); const [view, setView] = React.useState('gallery'); const [yearFilter, setYearFilter] = React.useState('all'); const filtered = SITE_DATA.sold.filter(s => { if (yearFilter === 'all') return true; return (s.soldDate || '').startsWith(yearFilter); }); const totalSold = SITE_DATA.sold.length; const totalValue = SITE_DATA.sold.reduce((a, b) => a + b.soldPrice, 0); // Days-on-market and ask price aren't always available (e.g. rew-scraped sales). // Compute over just the rows that have them, and show "—" when there's nothing. const domItems = SITE_DATA.sold.filter(s => s.daysOnMarket != null); const avgDays = domItems.length ? Math.round(domItems.reduce((a, b) => a + b.daysOnMarket, 0) / domItems.length) : null; const askItems = SITE_DATA.sold.filter(s => s.askPrice && s.askPrice !== s.soldPrice); const overAsk = SITE_DATA.sold.filter(s => s.soldPrice > s.askPrice).length; const overAskPct = askItems.length ? Math.round((overAsk / totalSold) * 100) : null; return (
Twenty years of negotiated outcomes. The detail page for each sale shows the days on market, the gap to ask, and what made the property move. Browse the highlights below — the full archive is available on request.