PageSourceSearch

https://firstcoastliving.netlify.app/js/rentcast.js

js firstcoastliving.netlify.app collected 2026-10-03 10:44:11 UTC 5,962 bytes, 150 lines download raw bytes

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 &rarr;</a>' +
50          '<p class="listing-courtesy">' +
51            (office || agent ? "Listed by " + escapeHtml(office || agent) + " &middot; " : "") +
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 { "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" }[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.