Real Estate — Transaction Pipeline
An editorial real-estate deal board built with vanilla HTML, CSS, and JavaScript. Six stages run from New Lead through Showing, Offer, Under Contract, Closing, and Closed, each holding deal cards with a gradient property thumbnail, address, price pill, client avatar, days-in-stage badge, and a closing-checklist progress bar. Drag a card between columns and the per-stage counts and dollar volume recompute live, while keyboard and tap-to-move controls advance deals without a mouse. Self-contained, responsive, and accessible.
MCP
Código
:root {
--ivory: #f7f4ec;
--paper: #fffdf8;
--white: #ffffff;
--green: #1f3d34;
--green-d: #16302a;
--green-700: #26493e;
--green-50: #e8efea;
--brass: #b08d57;
--brass-d: #94733f;
--brass-50: #f3ead9;
--ink: #1c2a25;
--ink-2: #33433d;
--muted: #6b7a72;
--line: rgba(31, 61, 52, 0.12);
--line-2: rgba(31, 61, 52, 0.22);
--ok: #2f9e6f;
--warn: #c98a2b;
--danger: #c4503e;
--r-sm: 8px;
--r-md: 14px;
--r-lg: 22px;
--sh-sm: 0 1px 2px rgba(28, 42, 37, 0.06), 0 2px 6px rgba(28, 42, 37, 0.05);
--sh-md: 0 4px 14px rgba(28, 42, 37, 0.09), 0 12px 30px rgba(28, 42, 37, 0.07);
--sh-lg: 0 18px 50px rgba(28, 42, 37, 0.16);
}
* {
box-sizing: border-box;
}
html {
-webkit-text-size-adjust: 100%;
}
body {
margin: 0;
background: var(--ivory);
color: var(--ink);
font-family: "Inter", system-ui, sans-serif;
line-height: 1.55;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
h1,
h2,
h3 {
font-family: "Cormorant Garamond", Georgia, serif;
font-weight: 600;
margin: 0;
letter-spacing: 0.01em;
}
.page {
max-width: 1240px;
margin: 0 auto;
padding: clamp(22px, 4vw, 52px) clamp(16px, 3vw, 40px) 64px;
}
/* ---------- Masthead ---------- */
.masthead {
display: flex;
flex-wrap: wrap;
align-items: flex-end;
justify-content: space-between;
gap: 28px;
padding-bottom: 26px;
margin-bottom: 30px;
border-bottom: 1px solid var(--line);
position: relative;
}
.masthead::after {
content: "";
position: absolute;
bottom: -1px;
left: 0;
width: 86px;
height: 2px;
background: var(--brass);
}
.masthead-text {
max-width: 620px;
}
.eyebrow {
margin: 0 0 10px;
font-size: 12px;
font-weight: 600;
letter-spacing: 0.16em;
text-transform: uppercase;
color: var(--brass-d);
}
.masthead h1 {
font-size: clamp(34px, 5vw, 52px);
line-height: 1.04;
color: var(--green-d);
}
.lede {
margin: 12px 0 0;
color: var(--ink-2);
font-size: 15.5px;
max-width: 56ch;
}
.masthead-stats {
display: flex;
gap: 14px;
}
.stat {
background: var(--paper);
border: 1px solid var(--line);
border-radius: var(--r-md);
padding: 14px 20px;
min-width: 132px;
box-shadow: var(--sh-sm);
}
.stat.brass {
border-color: rgba(176, 141, 87, 0.4);
background: linear-gradient(180deg, var(--brass-50), var(--paper));
}
.stat-num {
display: block;
font-family: "Cormorant Garamond", Georgia, serif;
font-weight: 700;
font-size: 30px;
line-height: 1;
color: var(--green-d);
}
.stat.brass .stat-num {
color: var(--brass-d);
}
.stat-label {
display: block;
margin-top: 6px;
font-size: 11.5px;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--muted);
}
/* ---------- Board ---------- */
.board {
display: grid;
grid-auto-flow: column;
grid-auto-columns: minmax(278px, 1fr);
gap: 18px;
overflow-x: auto;
padding: 4px 4px 18px;
scroll-snap-type: x proximity;
-webkit-overflow-scrolling: touch;
}
.column {
background: linear-gradient(180deg, var(--paper), var(--ivory));
border: 1px solid var(--line);
border-radius: var(--r-lg);
display: flex;
flex-direction: column;
min-height: 280px;
scroll-snap-align: start;
transition: border-color 0.18s ease, box-shadow 0.18s ease, background 0.18s ease;
}
.column.drag-over {
border-color: var(--brass);
box-shadow: 0 0 0 3px rgba(176, 141, 87, 0.16), var(--sh-md);
background: linear-gradient(180deg, var(--brass-50), var(--paper));
}
.col-head {
padding: 16px 16px 12px;
border-bottom: 1px solid var(--line);
}
.col-title-row {
display: flex;
align-items: center;
gap: 9px;
}
.col-dot {
width: 9px;
height: 9px;
border-radius: 50%;
flex: none;
box-shadow: 0 0 0 3px rgba(255, 255, 255, 0.6);
}
.col-title {
font-family: "Inter", system-ui, sans-serif;
font-size: 13px;
font-weight: 700;
letter-spacing: 0.05em;
text-transform: uppercase;
color: var(--green-d);
margin: 0;
}
.col-count {
margin-left: auto;
font-size: 12px;
font-weight: 700;
color: var(--green-700);
background: var(--green-50);
border-radius: 999px;
padding: 2px 9px;
min-width: 24px;
text-align: center;
}
.col-volume {
margin: 9px 0 0;
font-family: "Cormorant Garamond", Georgia, serif;
font-size: 20px;
font-weight: 700;
color: var(--brass-d);
line-height: 1;
}
.col-volume span {
font-family: "Inter", system-ui, sans-serif;
font-size: 10.5px;
font-weight: 600;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--muted);
margin-left: 6px;
}
.col-body {
padding: 12px;
display: flex;
flex-direction: column;
gap: 12px;
flex: 1;
min-height: 60px;
}
.col-empty {
border: 1px dashed var(--line-2);
border-radius: var(--r-md);
padding: 22px 12px;
text-align: center;
font-size: 12.5px;
color: var(--muted);
}
/* ---------- Deal card ---------- */
.deal {
background: var(--white);
border: 1px solid var(--line);
border-radius: var(--r-md);
box-shadow: var(--sh-sm);
overflow: hidden;
cursor: grab;
transition: transform 0.16s ease, box-shadow 0.16s ease, border-color 0.16s ease, opacity 0.16s ease;
}
.deal:hover {
transform: translateY(-3px);
box-shadow: var(--sh-md);
border-color: var(--line-2);
}
.deal:active {
cursor: grabbing;
}
.deal:focus-visible {
outline: 2px solid var(--brass);
outline-offset: 2px;
}
.deal.dragging {
opacity: 0.45;
box-shadow: var(--sh-lg);
}
.deal.selected {
border-color: var(--brass);
box-shadow: 0 0 0 2px rgba(176, 141, 87, 0.4), var(--sh-md);
}
.deal-thumb {
position: relative;
aspect-ratio: 16 / 9;
width: 100%;
}
.deal-thumb .price-pill {
position: absolute;
left: 10px;
bottom: 10px;
background: rgba(22, 48, 42, 0.86);
color: var(--paper);
font-weight: 700;
font-size: 13px;
letter-spacing: 0.01em;
padding: 4px 11px;
border-radius: 999px;
backdrop-filter: blur(3px);
}
.deal-thumb .beds-pill {
position: absolute;
right: 10px;
top: 10px;
background: rgba(255, 253, 248, 0.92);
color: var(--green-d);
font-weight: 600;
font-size: 11px;
letter-spacing: 0.02em;
padding: 3px 9px;
border-radius: 999px;
box-shadow: var(--sh-sm);
}
.deal-body {
padding: 12px 13px 13px;
}
.deal-address {
font-family: "Cormorant Garamond", Georgia, serif;
font-size: 19px;
font-weight: 600;
line-height: 1.12;
color: var(--green-d);
margin: 0;
}
.deal-city {
margin: 3px 0 0;
font-size: 12px;
color: var(--muted);
}
.deal-meta {
display: flex;
align-items: center;
gap: 8px;
margin-top: 11px;
}
.deal-client {
display: flex;
align-items: center;
gap: 7px;
min-width: 0;
}
.avatar {
width: 24px;
height: 24px;
border-radius: 50%;
flex: none;
display: grid;
place-items: center;
font-size: 10.5px;
font-weight: 700;
color: var(--paper);
letter-spacing: 0.02em;
}
.client-name {
font-size: 12.5px;
font-weight: 500;
color: var(--ink-2);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.days-badge {
margin-left: auto;
flex: none;
font-size: 11px;
font-weight: 600;
color: var(--green-700);
background: var(--green-50);
border-radius: 999px;
padding: 2px 8px;
}
.days-badge.stale {
color: var(--danger);
background: rgba(196, 80, 62, 0.1);
}
.checklist {
margin-top: 12px;
}
.checklist-head {
display: flex;
justify-content: space-between;
font-size: 10.5px;
font-weight: 600;
letter-spacing: 0.06em;
text-transform: uppercase;
color: var(--muted);
margin-bottom: 6px;
}
.checklist-head .done {
color: var(--green-700);
}
.progress {
height: 6px;
border-radius: 999px;
background: var(--green-50);
overflow: hidden;
}
.progress > i {
display: block;
height: 100%;
border-radius: 999px;
background: linear-gradient(90deg, var(--brass), var(--brass-d));
transition: width 0.3s ease;
}
.progress.complete > i {
background: linear-gradient(90deg, var(--ok), #1f7d56);
}
/* ---------- Simulated property photos ---------- */
.ph {
background-color: #cdb89a;
}
.ph-1 {
background:
radial-gradient(120% 80% at 78% 18%, rgba(255, 244, 220, 0.9), rgba(255, 244, 220, 0) 55%),
linear-gradient(160deg, #e8c79a 0%, #c79a64 42%, #6f5436 100%);
}
.ph-2 {
background:
radial-gradient(100% 70% at 22% 22%, rgba(214, 232, 240, 0.85), rgba(214, 232, 240, 0) 60%),
linear-gradient(155deg, #9fb9bd 0%, #5f7f86 48%, #2f4b4a 100%);
}
.ph-3 {
background:
radial-gradient(110% 80% at 70% 80%, rgba(255, 214, 170, 0.8), rgba(255, 214, 170, 0) 58%),
linear-gradient(150deg, #c98f6a 0%, #9a5b48 45%, #4f2f33 100%);
}
.ph-4 {
background:
radial-gradient(120% 80% at 30% 16%, rgba(236, 248, 224, 0.85), rgba(236, 248, 224, 0) 56%),
linear-gradient(160deg, #aeca84 0%, #7b9159 46%, #3c5034 100%);
}
.ph-5 {
background:
radial-gradient(110% 80% at 80% 20%, rgba(255, 235, 235, 0.85), rgba(255, 235, 235, 0) 55%),
linear-gradient(155deg, #d7a9ad 0%, #9a6f86 46%, #4a3a54 100%);
}
.ph-6 {
background:
radial-gradient(120% 80% at 20% 80%, rgba(255, 246, 214, 0.85), rgba(255, 246, 214, 0) 58%),
linear-gradient(150deg, #e0c486 0%, #b58a45 45%, #6b4d26 100%);
}
.ph-7 {
background:
radial-gradient(100% 70% at 75% 25%, rgba(220, 233, 246, 0.85), rgba(220, 233, 246, 0) 60%),
linear-gradient(160deg, #aebfd2 0%, #6f7f9b 46%, #344056 100%);
}
.ph-8 {
background:
radial-gradient(120% 80% at 28% 22%, rgba(255, 240, 224, 0.86), rgba(255, 240, 224, 0) 56%),
linear-gradient(155deg, #d9b48c 0%, #a87a4f 44%, #5c3d28 100%);
}
/* ---------- Hint ---------- */
.hint {
margin: 18px 4px 0;
font-size: 12.5px;
color: var(--muted);
}
/* ---------- Toast ---------- */
.toast {
position: fixed;
left: 50%;
bottom: 26px;
transform: translate(-50%, 16px);
background: var(--green-d);
color: var(--paper);
font-size: 13.5px;
font-weight: 500;
padding: 11px 18px;
border-radius: 999px;
box-shadow: var(--sh-lg);
opacity: 0;
pointer-events: none;
transition: opacity 0.24s ease, transform 0.24s ease;
z-index: 60;
max-width: calc(100vw - 32px);
}
.toast::before {
content: "";
display: inline-block;
width: 7px;
height: 7px;
border-radius: 50%;
background: var(--brass);
margin-right: 9px;
vertical-align: middle;
}
.toast.show {
opacity: 1;
transform: translate(-50%, 0);
pointer-events: auto;
}
/* ---------- Responsive ---------- */
@media (max-width: 820px) {
.masthead-stats {
width: 100%;
}
.stat {
flex: 1;
}
}
@media (max-width: 520px) {
.page {
padding: 22px 14px 56px;
}
.masthead {
gap: 18px;
}
.board {
grid-auto-columns: 86%;
gap: 14px;
}
.masthead-stats {
gap: 10px;
}
.stat {
min-width: 0;
padding: 12px 14px;
}
.stat-num {
font-size: 26px;
}
}
@media (prefers-reduced-motion: reduce) {
* {
transition: none !important;
}
}/* Real Estate — Transaction Pipeline
Vanilla JS Kanban board with drag-and-drop, touch tap-to-move,
and live column count / dollar-volume totals. */
(function () {
"use strict";
/* ---------- Toast helper ---------- */
var toastEl = document.getElementById("toast");
var toastTimer;
function toast(msg) {
if (!toastEl) return;
toastEl.textContent = msg;
toastEl.classList.add("show");
clearTimeout(toastTimer);
toastTimer = setTimeout(function () {
toastEl.classList.remove("show");
}, 2600);
}
/* ---------- Data ---------- */
var STAGES = [
{ id: "lead", title: "New Lead", color: "#8aa0b6" },
{ id: "showing", title: "Showing", color: "#b08d57" },
{ id: "offer", title: "Offer", color: "#c98a2b" },
{ id: "contract", title: "Under Contract", color: "#26493e" },
{ id: "closing", title: "Closing", color: "#2f9e6f" },
{ id: "closed", title: "Closed", color: "#16302a" }
];
var AVATAR_TONES = [
"linear-gradient(135deg,#26493e,#16302a)",
"linear-gradient(135deg,#b08d57,#94733f)",
"linear-gradient(135deg,#5f7f86,#2f4b4a)",
"linear-gradient(135deg,#9a5b48,#4f2f33)",
"linear-gradient(135deg,#7b9159,#3c5034)",
"linear-gradient(135deg,#9a6f86,#4a3a54)"
];
// checklist = [done, total]; addedDaysAgo drives "days in stage"
var DEALS = [
{ id: "d1", stage: "lead", ph: "ph-1", address: "418 Marisol Court", city: "Cedar Bluff, OR", price: 845000, beds: 4, baths: 3, client: "Priya Desai", days: 2, check: [1, 4] },
{ id: "d2", stage: "lead", ph: "ph-4", address: "27 Larkspur Way", city: "Glenmara, OR", price: 612000, beds: 3, baths: 2, client: "Owen Hartley", days: 5, check: [0, 4] },
{ id: "d3", stage: "lead", ph: "ph-7", address: "9 Ridgeline Terrace", city: "North Hollow, OR", price: 1290000, beds: 5, baths: 4, client: "The Okafor Family", days: 1, check: [2, 4] },
{ id: "d4", stage: "showing", ph: "ph-2", address: "612 Harborview Ave", city: "Saltspar, OR", price: 1075000, beds: 4, baths: 3, client: "Marcus Lindqvist", days: 4, check: [3, 5] },
{ id: "d5", stage: "showing", ph: "ph-6", address: "1145 Juniper Mill Rd", city: "Cedar Bluff, OR", price: 728000, beds: 3, baths: 2, client: "Hana Whitfield", days: 8, check: [2, 5] },
{ id: "d6", stage: "offer", ph: "ph-3", address: "84 Copperfield Lane", city: "Glenmara, OR", price: 965000, beds: 4, baths: 3, client: "Diego Salcedo", days: 3, check: [4, 6] },
{ id: "d7", stage: "offer", ph: "ph-8", address: "330 Amberton Place", city: "Saltspar, OR", price: 1540000, beds: 5, baths: 5, client: "Eleanor Vance", days: 6, check: [3, 6] },
{ id: "d8", stage: "contract", ph: "ph-5", address: "76 Wisteria Hollow", city: "North Hollow, OR", price: 689000, beds: 3, baths: 2, client: "The Berhane Trust", days: 11, check: [5, 7] },
{ id: "d9", stage: "contract", ph: "ph-1", address: "1208 Sunmeadow Dr", city: "Glenmara, OR", price: 902000, beds: 4, baths: 3, client: "Camille Roux", days: 4, check: [6, 7] },
{ id: "d10", stage: "closing", ph: "ph-2", address: "55 Beacon Crest", city: "Saltspar, OR", price: 1180000, beds: 4, baths: 4, client: "Theo & Mara Quinn", days: 2, check: [8, 9] },
{ id: "d11", stage: "closed", ph: "ph-6", address: "210 Willowbrook Ln", city: "Cedar Bluff, OR", price: 575000, beds: 3, baths: 2, client: "Aisha Nakamura", days: 0, check: [9, 9] },
{ id: "d12", stage: "closed", ph: "ph-3", address: "640 Crestmont Row", city: "North Hollow, OR", price: 1340000, beds: 5, baths: 4, client: "The Castellano Group", days: 1, check: [9, 9] }
];
var STALE_THRESHOLD = 10; // days in stage before a card flags "stale"
/* ---------- Formatting ---------- */
function money(n) {
if (n >= 1000000) return "$" + (n / 1000000).toFixed(2).replace(/\.?0+$/, "") + "M";
if (n >= 1000) return "$" + Math.round(n / 1000) + "K";
return "$" + n;
}
function moneyFull(n) {
return "$" + n.toLocaleString("en-US");
}
function initials(name) {
var clean = name.replace(/^The\s+/i, "");
var parts = clean.split(/\s|&/).filter(Boolean);
if (parts.length === 1) return parts[0].slice(0, 2).toUpperCase();
return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase();
}
function avatarTone(id) {
var sum = 0;
for (var i = 0; i < id.length; i++) sum += id.charCodeAt(i);
return AVATAR_TONES[sum % AVATAR_TONES.length];
}
/* ---------- Build board ---------- */
var board = document.querySelector(".board");
function buildCard(d) {
var card = document.createElement("article");
card.className = "deal";
card.id = d.id;
card.setAttribute("draggable", "true");
card.setAttribute("tabindex", "0");
card.setAttribute("role", "button");
var stage = STAGES.filter(function (s) { return s.id === d.stage; })[0];
card.setAttribute(
"aria-label",
d.address + ", " + moneyFull(d.price) + ", client " + d.client +
", stage " + stage.title + ", " + d.days + " days in stage. Press Enter to advance."
);
var stale = d.days >= STALE_THRESHOLD;
var pct = d.check[1] ? Math.round((d.check[0] / d.check[1]) * 100) : 0;
var complete = d.check[0] >= d.check[1];
card.innerHTML =
'<div class="deal-thumb ph ' + d.ph + '">' +
'<span class="beds-pill">' + d.beds + " bd · " + d.baths + " ba</span>" +
'<span class="price-pill">' + money(d.price) + "</span>" +
"</div>" +
'<div class="deal-body">' +
'<h3 class="deal-address">' + d.address + "</h3>" +
'<p class="deal-city">' + d.city + "</p>" +
'<div class="deal-meta">' +
'<div class="deal-client">' +
'<span class="avatar" style="background:' + avatarTone(d.id) + '" aria-hidden="true">' +
initials(d.client) +
"</span>" +
'<span class="client-name">' + d.client + "</span>" +
"</div>" +
'<span class="days-badge' + (stale ? " stale" : "") + '" title="Days in stage">' +
(d.days === 0 ? "today" : d.days + "d") +
"</span>" +
"</div>" +
'<div class="checklist">' +
'<div class="checklist-head">' +
"<span>Closing checklist</span>" +
'<span class="done">' + d.check[0] + "/" + d.check[1] + "</span>" +
"</div>" +
'<div class="progress' + (complete ? " complete" : "") + '">' +
"<i style=\"width:" + pct + '%"></i>' +
"</div>" +
"</div>" +
"</div>";
wireCard(card);
return card;
}
function buildColumn(stage) {
var col = document.createElement("section");
col.className = "column";
col.dataset.stage = stage.id;
col.setAttribute("aria-label", stage.title + " stage");
col.innerHTML =
'<div class="col-head">' +
'<div class="col-title-row">' +
'<span class="col-dot" style="background:' + stage.color + '"></span>' +
'<h2 class="col-title">' + stage.title + "</h2>" +
'<span class="col-count" data-count>0</span>' +
"</div>" +
'<p class="col-volume"><span data-vol-label>Volume</span></p>' +
"</div>" +
'<div class="col-body" data-body></div>';
// Reorder head: put number then label looks cleaner — rebuild volume node
var vol = col.querySelector(".col-volume");
vol.innerHTML = '<em data-vol style="font-style:normal">$0</em><span>Volume</span>';
wireColumn(col);
return col;
}
// Render all columns
STAGES.forEach(function (stage) {
board.appendChild(buildColumn(stage));
});
// Place deals
DEALS.forEach(function (d) {
var body = board.querySelector('.column[data-stage="' + d.stage + '"] [data-body]');
body.appendChild(buildCard(d));
});
/* ---------- Totals ---------- */
function dealPrice(cardEl) {
var d = DEALS.filter(function (x) { return x.id === cardEl.id; })[0];
return d ? d.price : 0;
}
function recompute() {
var grandCount = 0;
var grandVol = 0;
Array.prototype.forEach.call(board.querySelectorAll(".column"), function (col) {
var body = col.querySelector("[data-body]");
var cards = body.querySelectorAll(".deal");
var count = cards.length;
var vol = 0;
Array.prototype.forEach.call(cards, function (c) { vol += dealPrice(c); });
col.querySelector("[data-count]").textContent = count;
col.querySelector("[data-vol]").textContent = money(vol);
// empty placeholder
var existing = body.querySelector(".col-empty");
if (count === 0 && !existing) {
var ph = document.createElement("div");
ph.className = "col-empty";
ph.textContent = "Drop a deal here";
body.appendChild(ph);
} else if (count > 0 && existing) {
existing.remove();
}
// The "Closed" column counts toward closed value but stays out of "active" totals.
if (col.dataset.stage !== "closed") {
grandCount += count;
grandVol += vol;
}
});
document.getElementById("totalDeals").textContent = grandCount;
document.getElementById("totalVolume").textContent = money(grandVol);
}
/* ---------- Move logic (shared by DnD + tap) ---------- */
function moveCardTo(card, col) {
if (!card || !col) return;
var body = col.querySelector("[data-body]");
var fromStage = card.closest(".column").dataset.stage;
var toStage = col.dataset.stage;
if (fromStage === toStage) return;
var ph = body.querySelector(".col-empty");
if (ph) ph.remove();
body.appendChild(card);
// Reset days-in-stage on the data + UI since the deal just advanced.
var d = DEALS.filter(function (x) { return x.id === card.id; })[0];
if (d) {
d.stage = toStage;
d.days = 0;
var badge = card.querySelector(".days-badge");
if (badge) {
badge.textContent = "today";
badge.classList.remove("stale");
}
}
var stageTitle = STAGES.filter(function (s) { return s.id === toStage; })[0].title;
var addr = d ? d.address : "Deal";
recompute();
toast(addr + " → " + stageTitle);
}
/* ---------- Drag & drop ---------- */
var dragged = null;
function wireCard(card) {
card.addEventListener("dragstart", function (e) {
dragged = card;
card.classList.add("dragging");
clearSelection();
if (e.dataTransfer) {
e.dataTransfer.effectAllowed = "move";
e.dataTransfer.setData("text/plain", card.id);
}
});
card.addEventListener("dragend", function () {
card.classList.remove("dragging");
dragged = null;
clearDragOver();
});
// Touch / click: tap to select, then tap a column to drop.
card.addEventListener("click", function (e) {
e.stopPropagation();
if (card.classList.contains("selected")) {
clearSelection();
return;
}
clearSelection();
card.classList.add("selected");
toast("Selected — tap a stage to move it");
});
// Keyboard: Enter / → advance one stage, ← move back one stage.
card.addEventListener("keydown", function (e) {
var forward = e.key === "Enter" || e.key === "ArrowRight";
var back = e.key === "ArrowLeft";
if (!forward && !back) return;
e.preventDefault();
var curCol = card.closest(".column");
var idx = STAGES.map(function (s) { return s.id; }).indexOf(curCol.dataset.stage);
var nextIdx = forward ? idx + 1 : idx - 1;
if (nextIdx < 0 || nextIdx >= STAGES.length) {
toast(forward ? "Already at the final stage" : "Already at the first stage");
return;
}
var target = board.querySelector('.column[data-stage="' + STAGES[nextIdx].id + '"]');
moveCardTo(card, target);
card.focus();
});
}
function wireColumn(col) {
col.addEventListener("dragover", function (e) {
e.preventDefault();
if (e.dataTransfer) e.dataTransfer.dropEffect = "move";
col.classList.add("drag-over");
});
col.addEventListener("dragleave", function (e) {
// only clear when leaving the column itself, not its children
if (!col.contains(e.relatedTarget)) col.classList.remove("drag-over");
});
col.addEventListener("drop", function (e) {
e.preventDefault();
col.classList.remove("drag-over");
var card = dragged;
if (!card && e.dataTransfer) {
card = document.getElementById(e.dataTransfer.getData("text/plain"));
}
moveCardTo(card, col);
});
// Tap-to-move target (touch devices)
col.addEventListener("click", function () {
var sel = board.querySelector(".deal.selected");
if (sel) {
moveCardTo(sel, col);
clearSelection();
}
});
}
function clearDragOver() {
Array.prototype.forEach.call(board.querySelectorAll(".drag-over"), function (c) {
c.classList.remove("drag-over");
});
}
function clearSelection() {
Array.prototype.forEach.call(board.querySelectorAll(".deal.selected"), function (c) {
c.classList.remove("selected");
});
}
// Click outside clears selection
document.addEventListener("click", clearSelection);
/* ---------- Init ---------- */
recompute();
setTimeout(function () {
var active = document.getElementById("totalDeals").textContent;
toast(active + " active deals loaded — drag to advance a stage");
}, 450);
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Real Estate — Transaction Pipeline</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:wght@500;600;700&family=Inter:wght@400;500;600;700&display=swap"
rel="stylesheet" />
<link rel="stylesheet" href="style.css" />
</head>
<body>
<main class="page">
<header class="masthead">
<div class="masthead-text">
<p class="eyebrow">Brookhaven & Vale · Brokerage Desk</p>
<h1>Transaction Pipeline</h1>
<p class="lede">
Every active deal from first lead to closed file. Drag a card between stages to advance
it — column totals and dollar volume recalculate as you go.
</p>
</div>
<div class="masthead-stats" aria-label="Pipeline summary">
<div class="stat">
<span class="stat-num" id="totalDeals">0</span>
<span class="stat-label">Active deals</span>
</div>
<div class="stat brass">
<span class="stat-num" id="totalVolume">$0</span>
<span class="stat-label">Pipeline volume</span>
</div>
</div>
</header>
<section class="board" aria-label="Transaction pipeline board">
<!-- Columns are injected by script.js -->
</section>
<p class="hint" id="boardHint">
Tip: grab a card and drop it onto another stage. On touch devices, tap a card then tap a
stage to move it.
</p>
</main>
<div class="toast" id="toast" role="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>Transaction Pipeline
A brokerage-desk Kanban board that tracks every live deal from first contact to closed file. Six columns — New Lead, Showing, Offer, Under Contract, Closing, and Closed — each carry a serif title, a count chip, and a running dollar-volume figure rendered in the editorial brass accent. Inside sit refined deal cards: a simulated property photo built from layered CSS gradients, a price pill and beds·baths chip overlaid on the image, the street address and city, a client avatar with initials, a days-in-stage badge that turns red when a deal goes stale, and a closing-checklist progress bar that fills green when every task is done.
The interactions are pure vanilla JavaScript. Grab any card and drop it onto another stage — the source and destination both recompute their count and volume instantly, the masthead summary of active deals and pipeline volume updates, and a toast confirms the move. Advancing a deal resets its days-in-stage to “today.” On touch devices you tap a card to select it and then tap a stage to move it, and keyboard users can focus a card and press Enter or the arrow keys to walk it forward and back through the funnel. The Closed column is tallied separately so it never inflates the active pipeline figures.
Everything is self-contained with no build step or external libraries — just two Google Fonts, a
single stylesheet, and one script. Cards expose descriptive aria-labels, the board scrolls
horizontally with snap points on narrow screens, empty columns show a drop placeholder, and a
prefers-reduced-motion block disables the hover lift and transitions for visitors who ask for
less movement.
Illustrative UI only — sample listings and data are fictional; not a real real-estate service.