← back to Commercialrealestate

public/global-search.js

71 lines

/* global-search.js — one search box in the top nav, searches ACROSS brokers + condos + SFRs
 * (Steve 2026-08-03: "give search global at top"). Hits /api/search, shows grouped results.
 * Self-contained: injects its own styles + markup into the first .nav (or body top). */
(function () {
  'use strict';
  if (window.__gsearchInit) return; window.__gsearchInit = true;

  var css = document.createElement('style');
  css.textContent = [
    '.gsearch{position:relative;margin-left:auto;display:flex;align-items:center}',
    '.gsearch input{width:230px;max-width:46vw;background:rgba(255,255,255,.06);border:1px solid var(--line,#2a313c);',
    'color:var(--ink,#e6edf3);border-radius:9px;padding:6px 10px;font-size:12.5px;outline:none}',
    '.gsearch input:focus{border-color:var(--blue,#58a6ff);width:300px}',
    '.gsearch .gres{position:absolute;top:110%;right:0;width:380px;max-width:92vw;max-height:70vh;overflow:auto;',
    'background:var(--card,#161b22);border:1px solid var(--line,#2a313c);border-radius:10px;box-shadow:0 12px 40px rgba(0,0,0,.5);',
    'z-index:9999;display:none;padding:4px}',
    '.gsearch .gres.on{display:block}',
    '.gsearch .ghead{font-size:10px;text-transform:uppercase;letter-spacing:.06em;color:var(--mut,#8b949e);padding:7px 9px 3px}',
    '.gsearch a.gitem{display:block;padding:6px 9px;border-radius:7px;text-decoration:none;color:var(--ink,#e6edf3)}',
    '.gsearch a.gitem:hover,.gsearch a.gitem.sel{background:rgba(88,166,255,.14)}',
    '.gsearch .gitem .gl{font-size:13px;font-weight:600}',
    '.gsearch .gitem .gs{font-size:11px;color:var(--mut,#8b949e)}',
    '.gsearch .gempty{padding:10px;color:var(--mut,#8b949e);font-size:12px}'
  ].join('');
  document.head.appendChild(css);

  var wrap = document.createElement('div'); wrap.className = 'gsearch';
  wrap.innerHTML = '<input type="search" placeholder="🔍 Search everything — address, broker, firm, DRE#…" aria-label="Global search">'
    + '<div class="gres" role="listbox"></div>';
  var host = document.querySelector('.nav') || document.querySelector('.topnav') || document.querySelector('header nav') || document.querySelector('nav') || document.body;
  if (host === document.body) { wrap.style.cssText = 'position:fixed;top:8px;right:12px;z-index:9999'; document.body.appendChild(wrap); }
  else host.appendChild(wrap);

  var input = wrap.querySelector('input'), box = wrap.querySelector('.gres');
  var esc = function (s) { return String(s == null ? '' : s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;'); };
  var timer = null, items = [], sel = -1, lastQ = '';

  function group(title, arr) {
    if (!arr.length) return '';
    return '<div class="ghead">' + esc(title) + ' (' + arr.length + ')</div>' + arr.map(function (r) {
      return '<a class="gitem" href="' + esc(r.href) + '"' + (/^https?:/.test(r.href) ? ' target="_blank" rel="noopener noreferrer"' : '') + '>'
        + '<div class="gl">' + esc(r.label || '—') + '</div>' + (r.sub ? '<div class="gs">' + esc(r.sub) + '</div>' : '') + '</a>';
    }).join('');
  }
  function render(d) {
    var html = group('Brokers', d.brokers || []) + group('Condos', d.condos || []) + group('SFR / listings', d.sfrs || []);
    box.innerHTML = html || '<div class="gempty">No matches for “' + esc(d.q) + '”</div>';
    box.classList.add('on');
    items = Array.prototype.slice.call(box.querySelectorAll('a.gitem')); sel = -1;
  }
  function run() {
    var q = input.value.trim(); if (q === lastQ) return; lastQ = q;
    if (q.length < 2) { box.classList.remove('on'); return; }
    fetch('/api/search?q=' + encodeURIComponent(q), { headers: { 'Accept': 'application/json' } })
      .then(function (r) { return r.json(); }).then(render).catch(function () { box.classList.remove('on'); });
  }
  input.addEventListener('input', function () { clearTimeout(timer); timer = setTimeout(run, 160); });
  input.addEventListener('focus', function () { if (box.innerHTML && input.value.trim().length >= 2) box.classList.add('on'); });
  input.addEventListener('keydown', function (e) {
    if (e.key === 'Escape') { box.classList.remove('on'); input.blur(); return; }
    if (!items.length) return;
    if (e.key === 'ArrowDown') { e.preventDefault(); sel = Math.min(sel + 1, items.length - 1); }
    else if (e.key === 'ArrowUp') { e.preventDefault(); sel = Math.max(sel - 1, 0); }
    else if (e.key === 'Enter') { if (sel >= 0 && items[sel]) items[sel].click(); return; }
    else return;
    items.forEach(function (el, i) { el.classList.toggle('sel', i === sel); });
    if (items[sel]) items[sel].scrollIntoView({ block: 'nearest' });
  });
  document.addEventListener('click', function (e) { if (!wrap.contains(e.target)) box.classList.remove('on'); });
})();