patx/miadruck

Add editor-controlled property spotlight popup

Commit c5edf2c · patx · 2026-08-27T20:20:13-04:00

Changeset
c5edf2ca142be65069a0b9cdb46f2f37d4b23cef
Parents
e4cb78091b13ae6570c22c7d925acf79865e164b

View source at this commit

Comments

No comments yet.

Log in to comment

Diff

diff --git a/docs/404.html b/docs/404.html
index b0bb494..219fa9e 100644
--- a/docs/404.html
+++ b/docs/404.html
@@ -10,8 +10,8 @@
 <link rel="icon" href="/favicon.svg" type="image/svg+xml">
 <link rel="apple-touch-icon" href="/apple-touch-icon.png">
 <script>document.documentElement.classList.add('js')</script>
-<link rel="stylesheet" href="/assets/site.css">
-<script src="/assets/site.js" defer></script>
+<link rel="stylesheet" href="/assets/site.css?v=20260827-popup">
+<script src="/assets/site.js?v=20260827-popup" defer></script>
 </head>
 <body>
 <a class="skip" href="#main">Skip to content</a>
diff --git a/docs/assets/site.css b/docs/assets/site.css
index aec4538..25076fb 100644
--- a/docs/assets/site.css
+++ b/docs/assets/site.css
@@ -944,6 +944,132 @@ footer a:hover{ color:#fff; text-decoration:underline; text-underline-offset:3px
 .copy-toast.show{ opacity:1; transform:translateX(-50%) translateY(0); }
 @media (max-width:860px){ .copy-toast{ bottom:74px; } }
 
+/* Property spotlight popup ----------------------------------------------- */
+body.property-promo-open{ overflow:hidden; }
+.property-promo-layer{
+  position:fixed;
+  inset:0;
+  z-index:1000;
+  display:grid;
+  place-items:center;
+  overflow-y:auto;
+  padding:clamp(16px,4vw,48px);
+  background:rgba(15,36,54,.76);
+  opacity:0;
+  pointer-events:none;
+  transition:opacity .22s ease;
+}
+.property-promo-layer.is-visible{ opacity:1; pointer-events:auto; }
+.property-promo{
+  display:grid;
+  width:min(920px,100%);
+  max-height:calc(100vh - clamp(32px,8vw,96px));
+  max-height:calc(100dvh - clamp(32px,8vw,96px));
+  grid-template-columns:minmax(300px,.86fr) minmax(0,1.14fr);
+  overflow:auto;
+  border:1px solid rgba(255,255,255,.28);
+  border-radius:2px;
+  background:var(--paper);
+  box-shadow:0 28px 80px rgba(8,22,33,.36);
+  transform:translateY(18px) scale(.985);
+  transition:transform .32s cubic-bezier(.22,.61,.36,1);
+}
+.property-promo-layer.is-visible .property-promo{ transform:none; }
+.property-promo-media{
+  position:relative;
+  min-height:480px;
+  overflow:hidden;
+  background:var(--sand);
+  padding-right:38px;
+}
+.property-promo-media img{
+  width:100%;
+  height:100%;
+  object-fit:cover;
+}
+.property-promo-spine{
+  position:absolute;
+  top:0;
+  right:0;
+  bottom:0;
+  display:flex;
+  width:38px;
+  align-items:center;
+  justify-content:center;
+  background:var(--brass);
+  color:#fff;
+  font-family:var(--sans);
+  font-size:10px;
+  font-weight:600;
+  letter-spacing:.2em;
+  text-transform:uppercase;
+  writing-mode:vertical-rl;
+  transform:rotate(180deg);
+}
+.property-promo-body{
+  align-self:center;
+  padding:clamp(30px,5vw,58px);
+}
+.property-promo-body .eyebrow{ margin-bottom:14px; }
+.property-promo-body h2{
+  max-width:11ch;
+  font-size:clamp(38px,5.2vw,64px);
+  line-height:.95;
+}
+.property-promo-rule{
+  width:54px;
+  height:1px;
+  margin:26px 0 24px;
+  background:var(--brass);
+}
+.property-promo-price{
+  margin:0;
+  font-family:var(--serif);
+  font-variation-settings:'opsz' 144;
+  font-size:clamp(25px,3vw,32px);
+  font-weight:600;
+  letter-spacing:-.02em;
+  line-height:1.1;
+}
+.property-promo-address{ margin:12px 0 2px; font-size:clamp(20px,2vw,25px); }
+.property-promo-location{
+  margin:0;
+  color:var(--ink-60);
+  font-family:var(--serif);
+  font-size:17px;
+  font-style:italic;
+}
+.property-promo-specs{
+  margin:20px 0 0;
+  color:var(--ink-80);
+  font-size:12px;
+  font-weight:600;
+  letter-spacing:.07em;
+  line-height:1.7;
+  text-transform:uppercase;
+}
+.property-promo-actions{
+  display:flex;
+  gap:12px;
+  margin-top:30px;
+}
+.property-promo-actions .btn{ min-width:0; padding-inline:20px; white-space:nowrap; }
+
+@media (max-width:720px){
+  .property-promo{ width:min(540px,100%); grid-template-columns:1fr; }
+  .property-promo-media{ min-height:0; height:clamp(190px,34vh,270px); }
+  .property-promo-body{ padding:28px clamp(22px,7vw,38px) 32px; }
+  .property-promo-body h2{ max-width:14ch; font-size:clamp(34px,10vw,48px); }
+  .property-promo-rule{ margin:20px 0; }
+}
+@media (max-width:440px){
+  .property-promo-layer{ align-items:end; padding:10px; }
+  .property-promo{ max-height:calc(100dvh - 20px); }
+  .property-promo-media{ height:180px; }
+  .property-promo-actions{ align-items:stretch; flex-direction:column; }
+  .property-promo-actions .btn{ width:100%; }
+}
+
 /* Utility ----------------------------------------------------------------- */
 .hide{ display:none !important; }
 
diff --git a/docs/assets/site.js b/docs/assets/site.js
index 392ca87..1e47b0b 100644
--- a/docs/assets/site.js
+++ b/docs/assets/site.js
@@ -184,6 +184,199 @@
   var deepLink = new URLSearchParams(location.search).get('listing');
   if (deepLink) setTimeout(function () { highlight(deepLink); }, 300);
 
+  /* -- Property spotlight popup ----------------------------------------- */
+  (function () {
+    // Someone following a listing deep link is already headed to a property.
+    if (deepLink || !window.fetch) return;
+
+    var manifestRequest = fetch('/properties/properties.json', {
+      credentials: 'same-origin',
+      cache: 'no-cache'
+    }).then(function (response) {
+      if (!response.ok) throw new Error('Property manifest unavailable');
+      return response.json();
+    });
+
+    function popupSelection(manifest) {
+      var settings = manifest && manifest.popup;
+      if (!settings || settings.enabled !== true ||
+          typeof settings.title !== 'string' || !settings.title.trim() ||
+          typeof settings.listing_id !== 'string') return null;
+      var listings = Array.isArray(manifest.listings) ? manifest.listings : [];
+      var listing = listings.find(function (item) {
+        return item && item.id === settings.listing_id;
+      });
+      return listing ? { settings: settings, listing: listing } : null;
+    }
+
+    function sessionKey(selection) {
+      return 'miadruck:property-popup:' + selection.listing.id + ':' +
+             selection.settings.title.trim();
+    }
+
+    function wasDismissed(key) {
+      try { return sessionStorage.getItem(key) === 'dismissed'; }
+      catch (err) { return false; }
+    }
+
+    function markDismissed(key) {
+      try { sessionStorage.setItem(key, 'dismissed'); }
+      catch (err) { /* Storage can be unavailable in privacy modes. */ }
+    }
+
+    function assetUrl(path) {
+      return '/' + String(path || '').replace(/^\/+/, '');
+    }
+
+    function detailsUrl(listing) {
+      if (listing.details_url) {
+        try {
+          var external = new URL(listing.details_url);
+          if (external.protocol === 'https:') return external.toString();
+        } catch (err) { /* Fall through to the on-site listing. */ }
+      }
+      return '/?listing=' + encodeURIComponent(listing.id);
+    }
+
+    function money(value) {
+      var number = Number(value);
+      if (!Number.isFinite(number)) return '';
+      return new Intl.NumberFormat('en-US', {
+        style: 'currency',
+        currency: 'USD',
+        maximumFractionDigits: 0
+      }).format(number);
+    }
+
+    function showPopup(manifest) {
+      var selection = popupSelection(manifest);
+      if (!selection) return;
+      var key = sessionKey(selection);
+      if (wasDismissed(key)) return;
+
+      var listing = selection.listing;
+      var address = listing.address || {};
+      var image = listing.image || {};
+      var statusLabels = {
+        for_sale: 'For sale',
+        for_rent: 'For rent',
+        off_market: 'Off market'
+      };
+      var price = money(listing.price);
+      if (listing.price_period === 'month') price += ' / month';
+
+      var layer = document.createElement('div');
+      layer.className = 'property-promo-layer';
+      layer.innerHTML =
+        '<section class="property-promo" role="dialog" aria-modal="true" ' +
+          'aria-labelledby="propertyPromoTitle" aria-describedby="propertyPromoDetails" tabindex="-1">' +
+          '<div class="property-promo-media">' +
+            '<img width="560" height="420">' +
+            '<span class="property-promo-spine" aria-hidden="true">Mia\'s spotlight</span>' +
+          '</div>' +
+          '<div class="property-promo-body">' +
+            '<p class="eyebrow property-promo-status"></p>' +
+            '<h2 id="propertyPromoTitle"></h2>' +
+            '<div class="property-promo-rule"></div>' +
+            '<p class="property-promo-price"></p>' +
+            '<h3 class="property-promo-address"></h3>' +
+            '<p class="property-promo-location"></p>' +
+            '<p class="property-promo-specs" id="propertyPromoDetails"></p>' +
+            '<div class="property-promo-actions">' +
+              '<a class="btn property-promo-view">View listing</a>' +
+              '<button class="btn btn-ghost property-promo-continue" type="button">Continue browsing</button>' +
+            '</div>' +
+          '</div>' +
+        '</section>';
+
+      var dialog = layer.querySelector('.property-promo');
+      var promoImage = layer.querySelector('img');
+      promoImage.src = assetUrl(image.thumb || image.src);
+      promoImage.alt = image.alt || (address.street ? 'Home at ' + address.street : 'Featured home');
+      layer.querySelector('.property-promo-status').textContent =
+        statusLabels[listing.status] || 'Featured property';
+      layer.querySelector('#propertyPromoTitle').textContent = selection.settings.title.trim();
+      layer.querySelector('.property-promo-price').textContent = price;
+      layer.querySelector('.property-promo-address').textContent = address.street || '';
+      layer.querySelector('.property-promo-location').textContent =
+        [address.city, address.state].filter(Boolean).join(', ');
+      layer.querySelector('.property-promo-specs').textContent = [
+        listing.bedrooms + ' bed',
+        listing.bathrooms + ' bath',
+        Number(listing.square_feet).toLocaleString('en-US') + ' sq ft',
+        listing.property_type
+      ].filter(Boolean).join(' · ');
+
+      var view = layer.querySelector('.property-promo-view');
+      var target = detailsUrl(listing);
+      view.href = target;
+      if (/^https:\/\//i.test(target)) {
+        view.target = '_blank';
+        view.rel = 'noopener';
+      }
+
+      var previousFocus = document.activeElement;
+      var closed = false;
+
+      function closePopup() {
+        if (closed) return;
+        closed = true;
+        markDismissed(key);
+        layer.classList.remove('is-visible');
+        document.body.classList.remove('property-promo-open');
+        document.removeEventListener('keydown', onKeydown);
+        setTimeout(function () {
+          layer.remove();
+          if (previousFocus && previousFocus.focus) previousFocus.focus();
+        }, reduceMotion ? 0 : 220);
+      }
+
+      function onKeydown(event) {
+        if (event.key === 'Escape') {
+          event.preventDefault();
+          closePopup();
+          return;
+        }
+        if (event.key !== 'Tab') return;
+        var focusable = Array.prototype.slice.call(
+          dialog.querySelectorAll('a[href],button:not([disabled])')
+        );
+        if (!focusable.length) return;
+        var first = focusable[0];
+        var last = focusable[focusable.length - 1];
+        if (event.shiftKey && document.activeElement === first) {
+          event.preventDefault(); last.focus();
+        } else if (!event.shiftKey && document.activeElement === last) {
+          event.preventDefault(); first.focus();
+        }
+      }
+
+      layer.querySelector('.property-promo-continue').addEventListener('click', closePopup);
+      view.addEventListener('click', closePopup);
+      layer.addEventListener('click', function (event) {
+        if (event.target === layer) closePopup();
+      });
+      document.addEventListener('keydown', onKeydown);
+      document.body.appendChild(layer);
+      document.body.classList.add('property-promo-open');
+      requestAnimationFrame(function () {
+        layer.classList.add('is-visible');
+        view.focus();
+      });
+    }
+
+    function schedulePopup() {
+      setTimeout(function () {
+        manifestRequest.then(showPopup).catch(function () {
+          // The promotion is optional; the website remains fully usable.
+        });
+      }, 2000);
+    }
+
+    if (document.readyState === 'complete') schedulePopup();
+    else window.addEventListener('load', schedulePopup, { once: true });
+  })();
+
   /* -- Contact form (Formspree, AJAX) ------------------------------------- */
   (function () {
     var form = document.getElementById('contactForm');
diff --git a/docs/boca-raton-real-estate/index.html b/docs/boca-raton-real-estate/index.html
index 209355b..5f4d11e 100644
--- a/docs/boca-raton-real-estate/index.html
+++ b/docs/boca-raton-real-estate/index.html
@@ -32,8 +32,8 @@
 <link rel="manifest" href="/site.webmanifest">
 <link rel="preload" href="/assets/fonts/fraunces-latin-var.woff2" as="font" type="font/woff2" crossorigin>
 <script>document.documentElement.classList.add('js')</script>
-<link rel="stylesheet" href="/assets/site.css">
-<script src="/assets/site.js" defer></script>
+<link rel="stylesheet" href="/assets/site.css?v=20260827-popup">
+<script src="/assets/site.js?v=20260827-popup" defer></script>
 </head>
 
 <body>
diff --git a/docs/fort-lauderdale-real-estate/index.html b/docs/fort-lauderdale-real-estate/index.html
index 711b89e..59e9272 100644
--- a/docs/fort-lauderdale-real-estate/index.html
+++ b/docs/fort-lauderdale-real-estate/index.html
@@ -32,8 +32,8 @@
 <link rel="manifest" href="/site.webmanifest">
 <link rel="preload" href="/assets/fonts/fraunces-latin-var.woff2" as="font" type="font/woff2" crossorigin>
 <script>document.documentElement.classList.add('js')</script>
-<link rel="stylesheet" href="/assets/site.css">
-<script src="/assets/site.js" defer></script>
+<link rel="stylesheet" href="/assets/site.css?v=20260827-popup">
+<script src="/assets/site.js?v=20260827-popup" defer></script>
 </head>
 
 <body>
diff --git a/docs/index.html b/docs/index.html
index 247939a..f885194 100644
--- a/docs/index.html
+++ b/docs/index.html
@@ -45,8 +45,8 @@
       imagesrcset="/assets/img/hero-fort-lauderdale-640w.webp 640w, /assets/img/hero-fort-lauderdale-960w.webp 960w, /assets/img/hero-fort-lauderdale.webp 1280w"
       imagesizes="100vw" fetchpriority="high">
 <script>document.documentElement.classList.add('js')</script>
-<link rel="stylesheet" href="/assets/site.css">
-<script src="/assets/site.js" defer></script>
+<link rel="stylesheet" href="/assets/site.css?v=20260827-popup">
+<script src="/assets/site.js?v=20260827-popup" defer></script>
 </head>
 
 <body>
diff --git a/docs/lighthouse-point-real-estate/index.html b/docs/lighthouse-point-real-estate/index.html
index 9efefc1..4986564 100644
--- a/docs/lighthouse-point-real-estate/index.html
+++ b/docs/lighthouse-point-real-estate/index.html
@@ -32,8 +32,8 @@
 <link rel="manifest" href="/site.webmanifest">
 <link rel="preload" href="/assets/fonts/fraunces-latin-var.woff2" as="font" type="font/woff2" crossorigin>
 <script>document.documentElement.classList.add('js')</script>
-<link rel="stylesheet" href="/assets/site.css">
-<script src="/assets/site.js" defer></script>
+<link rel="stylesheet" href="/assets/site.css?v=20260827-popup">
+<script src="/assets/site.js?v=20260827-popup" defer></script>
 </head>
 
 <body>
diff --git a/docs/pompano-beach-real-estate/index.html b/docs/pompano-beach-real-estate/index.html
index c2fe8ac..29ade06 100644
--- a/docs/pompano-beach-real-estate/index.html
+++ b/docs/pompano-beach-real-estate/index.html
@@ -32,8 +32,8 @@
 <link rel="manifest" href="/site.webmanifest">
 <link rel="preload" href="/assets/fonts/fraunces-latin-var.woff2" as="font" type="font/woff2" crossorigin>
 <script>document.documentElement.classList.add('js')</script>
-<link rel="stylesheet" href="/assets/site.css">
-<script src="/assets/site.js" defer></script>
+<link rel="stylesheet" href="/assets/site.css?v=20260827-popup">
+<script src="/assets/site.js?v=20260827-popup" defer></script>
 </head>
 
 <body>
diff --git a/docs/properties/properties.json b/docs/properties/properties.json
index 4130dfd..593ec27 100644
--- a/docs/properties/properties.json
+++ b/docs/properties/properties.json
@@ -1,5 +1,10 @@
 {
   "version": 1,
+  "popup": {
+    "enabled": false,
+    "title": "",
+    "listing_id": null
+  },
   "listings": [
     {
       "id": "boca-600-nw-12",