← back to Lifestyle Asset Intel

public/js/app.js

109 lines

(function () {
  'use strict';

  // -- theme toggle ------------------------------------------------------
  var themeBtn = document.getElementById('themeToggle');
  if (themeBtn) {
    themeBtn.addEventListener('click', function () {
      var cur = document.documentElement.dataset.theme === 'dark' ? 'dark' : 'light';
      var next = cur === 'dark' ? 'light' : 'dark';
      if (next === 'dark') document.documentElement.dataset.theme = 'dark';
      else delete document.documentElement.dataset.theme;
      try { localStorage.setItem('lai-theme', next); } catch (e) {}
    });
  }

  // -- hamburger nav ----------------------------------------------------
  var navBtn = document.getElementById('navToggle');
  var nav = document.getElementById('topnav');
  if (navBtn && nav) {
    navBtn.addEventListener('click', function () {
      var open = nav.classList.toggle('open');
      navBtn.setAttribute('aria-expanded', open ? 'true' : 'false');
    });
  }

  // -- grid: density + sort + search ------------------------------------
  var grid = document.getElementById('grid');
  if (!grid) return;

  var density = document.getElementById('density');
  var densityOut = document.getElementById('densityOut');
  var sort = document.getElementById('sort');
  var search = document.getElementById('search');

  var initial = {
    density: parseInt(localStorage.getItem('lai-density') || '5', 10),
    sort: localStorage.getItem('lai-sort') || 'newest',
    search: localStorage.getItem('lai-search') || ''
  };

  function applyDensity(n) {
    document.documentElement.style.setProperty('--cols', String(n));
    if (densityOut) densityOut.textContent = String(n);
    try { localStorage.setItem('lai-density', String(n)); } catch (e) {}
  }
  function applySort(mode) {
    var cards = Array.prototype.slice.call(grid.querySelectorAll('.card'));
    var num = function (el, key) { return parseFloat(el.getAttribute('data-' + key) || '0') || 0; };
    var str = function (el, key) { return (el.getAttribute('data-' + key) || '').toLowerCase(); };
    var cmp;
    switch (mode) {
      case 'brand':            cmp = function (a, b) { return str(a,'brand').localeCompare(str(b,'brand')); }; break;
      case 'family':           cmp = function (a, b) { return str(a,'family').localeCompare(str(b,'family')); }; break;
      case 'q50_asc':          cmp = function (a, b) { return num(a,'q50') - num(b,'q50'); }; break;
      case 'q50_desc':         cmp = function (a, b) { return num(b,'q50') - num(a,'q50'); }; break;
      case 'liquidity_desc':   cmp = function (a, b) { return num(b,'liquidity') - num(a,'liquidity'); }; break;
      case 'confidence_desc':  cmp = function (a, b) { return num(b,'confidence') - num(a,'confidence'); }; break;
      case 'recency_desc':     cmp = function (a, b) { return str(b,'asof').localeCompare(str(a,'asof')); }; break;
      case 'newest':
      default:                 cmp = function (a, b) { return str(b,'asof').localeCompare(str(a,'asof')); };
    }
    cards.sort(cmp).forEach(function (c) { grid.appendChild(c); });
    try { localStorage.setItem('lai-sort', mode); } catch (e) {}
  }
  function applySearch(q) {
    var query = (q || '').toLowerCase().trim();
    var cards = grid.querySelectorAll('.card');
    cards.forEach(function (c) {
      if (!query) { c.style.display = ''; return; }
      var hay = ['brand','family','material','color']
        .map(function (k) { return (c.getAttribute('data-' + k) || '').toLowerCase(); })
        .join(' ');
      c.style.display = hay.indexOf(query) >= 0 ? '' : 'none';
    });
    try { localStorage.setItem('lai-search', query); } catch (e) {}
  }

  if (density) {
    density.value = String(initial.density);
    applyDensity(initial.density);
    density.addEventListener('input', function () { applyDensity(parseInt(density.value, 10)); });
  }
  if (sort) {
    sort.value = initial.sort;
    applySort(initial.sort);
    sort.addEventListener('change', function () { applySort(sort.value); });
  }
  if (search) {
    // Server-side scoping: pressing Enter navigates to /?q=… so the
    // grid is filtered by the SQL LIKE in lib/valuation.listAssets,
    // which keeps working past the client-side DOM size limit.
    search.addEventListener('keydown', function (ev) {
      if (ev.key === 'Enter') {
        ev.preventDefault();
        var q = (search.value || '').trim();
        var u = new URL(window.location.href);
        if (q) u.searchParams.set('q', q);
        else u.searchParams.delete('q');
        window.location.href = u.toString();
      }
    });
    // If the page was rendered with a server-side q, prefer that over
    // localStorage; else fall back to the persisted client-side search.
    if (!search.value) search.value = initial.search;
    if (search.value) applySearch(search.value);
    search.addEventListener('input', function () { applySearch(search.value); });
  }
})();