Skip to contentVibraUI
Dashboards

Real estate vocabulary

The Real estate dashboard's words and small rules: what a listing's kind and status are called, the tone each status wears, the glyph that stands in for a photograph, how an asking price is written, which listings are on the market, a listing page's href, and how an agent's book is printed.

One module every Real estate page prints its words from, so "Under offer" is retuned in one place and a listing's page is linked the same way everywhere. Plain values and pure functions: the store's types it reads are erased, so a client island and a server file can both import it without the store coming along. agentBookItems is the one way an agent's book is printed: the Agents page's cards and a listing's agent card both hand it to a StatList, so the two read against each other label for label.

Install

npx shadcn@latest add @vibra/vocabulary-real-estate

Needs the @vibra registry in your components.json — set it up once.

Props

PropTypeDefaultDescription
KIND_LABELS / KIND_ICONSRecord<Listing["kind"], string> / Record<Listing["kind"], LucideIcon>—House, Apartment, Villa and Office, and the glyph that stands for each where a photograph would go.
LISTING_STATUS_LABELS / LISTING_STATUS_MAPRecord<Listing["status"], string> / Record<string, StatusVariant>—Listed, Under offer, Sold and Rented, and the tone each wears on a StatusBadge.
money(cents: number) => string—An asking price in whole dollars, from minor units.
isOpen(row: Pick<Listing, "status">) => boolean—On the market: listed, or under offer.
listingHref(id: string) => string—The listing page's route with the id in it: /real-estate/listings/lst_0012.
median(values: number[]) => number—The middle value, or the rounded mean of the middle two; 0 for none.
agentBookItems(book: AgentBook, year: number) => { label: string; value: string }[]—On the market, sold and let in the year, and the enquiries on their book, as StatList items.

Dependencies

Source

lib/dashboards/real-estate/vocabulary.ts
import { BriefcaseBusinessIcon, Building2Icon, HouseIcon, TreePalmIcon, type LucideIcon } from "lucide-react"

import { ROUTES } from "@/lib/dashboards/real-estate/nav"
import { formatCurrency, formatNumber } from "@/lib/format"
import { type Listing } from "@/lib/sample-data"
import { type StatusVariant } from "@/components/ui/status-badge"

/**
 * The Real estate dashboard's words and the small rules every page reads the
 * book by: what a listing's kind and status are called and the tone each
 * status wears, the glyph that stands for a listing where a photograph would
 * go, how an asking price is written, which listings are on the market, where
 * a listing's page is, and how an agent's book is printed. One module the
 * pages share, so a label or a rule is retuned in one place. Plain values
 * with no rows behind them — the store's types above are erased — so a
 * client island and a server file can both import it without the store
 * coming along.
 */

export const KIND_LABELS: Record<Listing["kind"], string> = {
  house: "House",
  apartment: "Apartment",
  villa: "Villa",
  office: "Office",
}

/** A listing has no photograph: its kind's glyph, drawn in ink, stands for it. */
export const KIND_ICONS: Record<Listing["kind"], LucideIcon> = {
  house: HouseIcon,
  apartment: Building2Icon,
  villa: TreePalmIcon,
  office: BriefcaseBusinessIcon,
}

export const LISTING_STATUS_LABELS: Record<Listing["status"], string> = {
  listed: "Listed",
  under_offer: "Under offer",
  sold: "Sold",
  rented: "Rented",
}

export const LISTING_STATUS_MAP: Record<string, StatusVariant> = {
  listed: "info",
  under_offer: "warning",
  sold: "success",
  rented: "primary",
}

/** Whole dollars from minor units: an asking price is round by nature. */
export const money = (cents: number): string => formatCurrency(cents / 100, "USD", { maximumFractionDigits: 0 })

/** On the market: listed, or under offer. A listing that has sold or let is off it. */
export const isOpen = (row: Pick<Listing, "status">): boolean => row.status === "listed" || row.status === "under_offer"

/** `/real-estate/listings/lst_0012`: the listing page's route with the id in it. */
export const listingHref = (id: string): string => ROUTES.listing.replace("[id]", id)

/** The middle value, or the mean of the middle two; 0 for nothing. */
export function median(values: number[]): number {
  if (values.length === 0) return 0
  const sorted = [...values].sort((a, b) => a - b)
  const middle = Math.floor(sorted.length / 2)
  return sorted.length % 2 === 1 ? sorted[middle] : Math.round((sorted[middle - 1] + sorted[middle]) / 2)
}

/** What an agent's book comes to: their listings on the market, this year's closings, and every enquiry. */
export type AgentBook = {
  active: number
  soldThisYear: number
  rentedThisYear: number
  /** Every enquiry their whole book has drawn, open and closed listings alike. */
  enquiries: number
}

/**
 * An agent's book as a stat list, the way the Agents page and a listing's
 * agent card both print it — one set of labels, so the two can be read
 * against each other.
 */
export function agentBookItems(book: AgentBook, year: number): { label: string; value: string }[] {
  return [
    { label: "On the market", value: formatNumber(book.active) },
    { label: `Sold in ${year}`, value: formatNumber(book.soldThisYear) },
    { label: `Let in ${year}`, value: formatNumber(book.rentedThisYear) },
    { label: "Enquiries on their book", value: formatNumber(book.enquiries) },
  ]
}