Market snapshot
summarizeListings turns a set of listings into the numbers a buyer wants at a glance: how many are for sale, the price range, median price and price per square foot, typical HOA, days on market, bedroom range and how many allow pets. It works on listings your page already fetched, for a building, a neighborhood or a search, with no extra request.
import { summarizeListings, monthlyHoa } from '@sonordev/re-site-kit'Both are pure functions that are safe anywhere: server components, client components, route handlers.
summarizeListings
summarizeListings(listings: RealEstateListing[]): ListingStatsconst { listings } = await getBuildingListings('harbor-point', { limit: 50 })
const stats = summarizeListings(listings)
// stats.count → 7, stats.medianPrice → 415000, stats.medianMonthlyHoa → 380, ...| Field | Type | What it is |
|---|---|---|
count | number | How many listings you passed in, whether or not they have the other values. |
priceMin, priceMax | number | null | The lowest and highest price. |
medianPrice | number | null | The median price. |
medianPricePerSqft | number | null | The median price per square foot. |
medianSqft | number | null | The median square footage. |
medianMonthlyHoa | number | null | The median HOA fee per month, with quarterly and annual fees converted (see monthlyHoa). |
medianDaysOnMarket | number | null | The median days on market. |
bedsMin, bedsMax | number | null | The bedroom range. A studio counts as 0. |
petFriendlyShare | number | null | The share of listings that allow pets, from 0 to 1, counting only listings that say either way. |
How the numbers are worked out:
- Missing values are skipped, figure by figure. A listing with no square footage still counts toward the median price; it's just left out of the square footage and price per square foot. So one incomplete listing doesn't skew anything.
- Prices, square footage and HOA fees count only when they're above zero.
- Price per square foot is the listing's price divided by its square footage when it has both. Otherwise the feed's own
price_per_sqftis used, when it has one. - Medians are rounded to whole numbers. The price range and
petFriendlySharearen't rounded. - With nothing to measure, a figure is
null. An empty list givescount: 0andnulleverywhere else.
The snapshot covers exactly the listings you pass in, so fetch the whole set you want to describe. For a building with more listings than one page holds, raise limit.
Showing the snapshot
The stats are plain numbers, so format them however your site formats money. This component shows each figure the listings have and skips the rest:
// components/MarketSnapshot.tsx
import type { ListingStats } from '@sonordev/re-site-kit'
const usd = (n: number) =>
new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
maximumFractionDigits: 0,
}).format(n)
export function MarketSnapshot({ stats }: { stats: ListingStats }) {
const rows: Array<[string, string | null]> = [
['For sale', String(stats.count)],
['Median price', stats.medianPrice == null ? null : usd(stats.medianPrice)],
['Median $/sqft', stats.medianPricePerSqft == null ? null : usd(stats.medianPricePerSqft)],
['Typical HOA', stats.medianMonthlyHoa == null ? null : `${usd(stats.medianMonthlyHoa)}/mo`],
['Days on market', stats.medianDaysOnMarket == null ? null : String(stats.medianDaysOnMarket)],
[
'Pet friendly',
stats.petFriendlyShare == null ? null : `${Math.round(stats.petFriendlyShare * 100)}%`,
],
]
return (
<dl className="market-snapshot">
{rows
.filter((row): row is [string, string] => row[1] != null)
.map(([label, value]) => (
<div key={label}>
<dt>{label}</dt>
<dd>{value}</dd>
</div>
))}
</dl>
)
}Then on any page that has listings:
<MarketSnapshot stats={summarizeListings(listings)} />Buildings and the registry has a full building page with a snapshot.
monthlyHoa
monthlyHoa(listing: Pick<RealEstateListing, 'hoa_fee' | 'hoa_frequency'>): number | nullOne listing's HOA fee as a monthly amount, rounded to whole dollars. summarizeListings uses it for medianMonthlyHoa, and it's handy on a listing page too:
monthlyHoa({ hoa_fee: 900, hoa_frequency: 'Quarterly' }) // 300
monthlyHoa({ hoa_fee: 1200, hoa_frequency: 'Annually' }) // 100
monthlyHoa({ hoa_fee: 250, hoa_frequency: null }) // 250 (no frequency means monthly)
monthlyHoa({ hoa_fee: 250, hoa_frequency: 'Weekly' }) // nullIt understands these frequencies, ignoring case and surrounding spaces:
| Frequency | Divided by |
|---|---|
monthly, month | 1 |
quarterly, quarter | 3 |
semiannually, semi-annually | 6 |
annually, annual, yearly | 12 |
It returns null when there's no fee, the fee isn't above zero, or the frequency is one it doesn't know.