1// Frontend integration with RentCast, via the /.netlify/functions/rentcast 2// proxy (see netlify/functions/rentcast.js) so the API key never reaches 3// the browser. 4// 5// Design principle: never break the page. Every section that pulls live 6// data ships with static placeholder content already in the HTML. We only 7// overwrite it on a *successful* fetch; any failure (key not configured 8// yet, network error, rate limit) just leaves the existing placeholder 9// and its "illustrative" notice in place. 10 11(function () { 12 var RC_ENDPOINT = "/.netlify/functions/rentcast"; 13 14 function rcFetch(type, params) { 15 var qs = new URLSearchParams(Object.assign({ type: type }, params)); 16 return fetch(RC_ENDPOINT + "?" + qs.toString()).then(function (res) { 17 if (!res.ok) throw new Error("rentcast " + type + " failed: " + res.status); 18 return res.json(); 19 }); 20 } 21 22 function formatMoney(n) { 23 if (typeof n !== "number") return null; 24 return "$" + Math.round(n).toLocaleString(); 25 } 26 27 // ---------- Live listings ---------- 28 function renderListingCard(listing, photoClass) { 29 var address = 30 listing.formattedAddress || 31 [listing.addressLine1, listing.city].filter(Boolean).join(", ") || 32 "Address on request"; 33 var price = formatMoney(listing.price); 34 var bd = listing.bedrooms != null ? listing.bedrooms + " bd" : null; 35 var ba = listing.bathrooms != null ? listing.bathrooms + " ba" : null; 36 var sqft = listing.squareFootage ? listing.squareFootage.toLocaleString() + " sqft" : null; 37 var meta = [bd, ba, sqft].filter(Boolean).map(function (m) { return "<span>" + m + "</span>"; }).join(""); 38 var office = (listing.listingOffice && listing.listingOffice.name) || null; 39 var agent = (listing.listingAgent && listing.listingAgent.name) || null; 40 var tag = listing.status || "Active"; 41 42 return ( 43 '<article class="card">' + 44 '<div class="card-photo ' + photoClass + '"><span class="price-tag">' + escapeHtml(tag) + "</span></div>" + 45 '<div class="card-body">' + 46 (price ? '<span class="listing-price">' + price + "</span>" : "") + 47 "<h3>" + escapeHtml(address) + "</h3>" + 48 (meta ? '<div class="listing-meta">' + meta + "</div>" : "") + 49 '<a class="card-link" href="contact.html">Schedule a Tour →</a>' + 50 '<p class="listing-courtesy">' + 51 (office || agent ? "Listed by " + escapeHtml(office || agent) + " · " : "") + 52 '<span class="rc-badge">Data via RentCast</span>' + 53 "</p>" + 54 "</div>" + 55 "</article>" 56 ); 57 } 58 59 function initLiveListings() { 60 var containers = document.querySelectorAll("[data-rc-listings]"); 61 containers.forEach(function (container) { 62 var city = container.getAttribute("data-rc-city"); 63 var state = container.getAttribute("data-rc-state") || "FL"; 64 var zipCode = container.getAttribute("data-rc-zip"); 65 var limit = container.getAttribute("data-rc-limit") || "3"; 66 var photoClasses = ["p1", "p2", "p3", "p4", "p5", "p6"]; 67 68 rcFetch("listings-sale", { 69 city: city || "", 70 state: state, 71 zipCode: zipCode || "", 72 status: "Active", 73 limit: limit, 74 }) 75 .then(function (data) { 76 var listings = Array.isArray(data) ? data : data.listings || []; 77 if (!listings.length) return; // leave placeholders in place 78 79 var grid = container.querySelector(".card-grid"); 80 if (!grid) return; 81 grid.innerHTML = listings 82 .slice(0, Number(limit)) 83 .map(function (l, i) { return renderListingCard(l, photoClasses[i % photoClasses.length]); }) 84 .join(""); 85 86 var note = container.querySelector(".sample-note"); 87 if (note) note.style.display = "none"; 88 }) 89 .catch(function () { 90 /* keep static placeholders + sample-note as-is */ 91 }); 92 }); 93 } 94 95 // ---------- Market stats ---------- 96 function initMarketStats() { 97 var containers = document.querySelectorAll("[data-rc-market]");
98 containers.forEach(function (container) { 99 var zipCode = container.getAttribute("data-rc-zip"); 100 if (!zipCode) return; 101 102 rcFetch("market", { zipCode: zipCode, dataType: "Sale" }) 103 .then(function (data) { 104 var sale = data.saleData; 105 if (!sale) return; 106 107 var median = formatMoney(sale.averagePrice || sale.medianPrice); 108 var dom = sale.averageDaysOnMarket; 109 var listings = sale.totalListings || sale.newListings; 110 var history = sale.history && Object.values(sale.history); 111 var trend = null; 112 if (Array.isArray(history) && history.length >= 2) { 113 var first = history[0], last = history[history.length - 1]; 114 if (first && last && first.averagePrice) { 115 trend = (((last.averagePrice - first.averagePrice) / first.averagePrice) * 100).toFixed(1) + "%"; 116 } 117 } 118 119 var fields = { median: median, dom: dom, listings: listings, trend: trend }; 120 var updated = false; 121 Object.keys(fields).forEach(function (key) { 122 var el = container.querySelector('[data-stat="' + key + '"]'); 123 if (el && fields[key] != null) { 124 el.textContent = fields[key]; 125 updated = true; 126 } 127 }); 128 129 if (updated) { 130 var note = container.querySelector(".sample-note"); 131 if (note) note.style.display = "none"; 132 } 133 }) 134 .catch(function () { 135 /* keep static placeholder numbers as-is */ 136 }); 137 }); 138 } 139 140 function escapeHtml(s) { 141 return String(s).replace(/[&<>"']/g, function (c) { 142 return { "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c]; 143 }); 144 } 145 146 document.addEventListener("DOMContentLoaded", function () { 147 initLiveListings(); 148 initMarketStats(); 149 }); 150})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.