Changeset 097da22 in Klonkt for src/views/partials/topnav.ejs


Ignore:
Timestamp:
06/21/2026 05:26:47 PM (3 months ago)
Author:
roboburr <roboburr@…>
Branches:
main
Children:
fefb80c
Parents:
e579a98
Message:

feat(zoeken): live results while typing + search in events & pages

  • /search/suggest JSON endpoint; overlay shows live (debounced) grouped results while typing, with "all results →" to the full page.
  • Sources expanded: in addition to posts + tracks now also calendar events (shows: city/location/country/note, if the calendar is enabled) and site pages (Agenda/Downloads/Links/Press Kit/Archive, only those available). Full search page shows those sections too. NL/EN/DE.
  • Overlay placeholder now translated (was hardcoded NL).

Co-Authored-By: Claude <noreply@…>

File:
1 edited

Legend:

Unmodified
Added
Removed
  • src/views/partials/topnav.ejs

    re579a98 r097da22  
    183183
    184184<!-- Search overlay (shared by mobile bottom-tab + desktop top-nav) -->
    185 <div id="search-overlay" class="search-overlay" hidden>
     185<div id="search-overlay" class="search-overlay" hidden data-suggest="<%= _siteUrlBase %>/search/suggest" data-action="<%= _siteUrlBase %>/search">
    186186  <div class="container">
    187187    <form class="search-overlay-form" method="get" action="<%= _siteUrlBase %>/search">
    188       <input type="search" name="q" placeholder="Zoeken in posts…" autocomplete="off">
    189       <button type="submit" class="nav-btn" aria-label="Zoeken">
     188      <input type="search" name="q" placeholder="<%= t('search.placeholder') %>" autocomplete="off">
     189      <button type="submit" class="nav-btn" aria-label="<%= t('search.button') %>">
    190190        <svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="11" cy="11" r="7"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
    191191      </button>
    192       <button type="button" class="nav-btn" id="search-close" aria-label="Sluiten">
     192      <button type="button" class="nav-btn" id="search-close" aria-label="<%= t('imed.cancel') %>">
    193193        <svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
    194194      </button>
    195195    </form>
     196    <div id="search-suggest" class="search-suggest" data-lenis-prevent></div>
    196197  </div>
    197198</div>
     
    215216  function overlay() { return document.getElementById('search-overlay'); }
    216217  function openSearch() { var o = overlay(); if (o) { o.hidden = false; var i = o.querySelector('input'); if (i) i.focus(); } }
    217   function closeSearch() { var o = overlay(); if (o) o.hidden = true; }
     218  function closeSearch() { var o = overlay(); if (o) { o.hidden = true; var b = document.getElementById('search-suggest'); if (b) b.innerHTML = ''; } }
    218219
    219220  document.body.addEventListener('click', function(e) {
     
    228229
    229230  document.addEventListener('keydown', function(e) {
    230     if (e.key === 'Escape') { var o = overlay(); if (o && !o.hidden) o.hidden = true; }
     231    if (e.key === 'Escape') { var o = overlay(); if (o && !o.hidden) closeSearch(); }
     232  });
     233
     234  // ── Live resultaten tijdens typen ───────────────────────────────
     235  var _ST = {
     236    posts: '<%= t('search.section_posts') %>', tracks: '<%= t('search.section_tracks') %>',
     237    events: '<%= t('search.section_events') %>', pages: '<%= t('search.section_pages') %>',
     238    all: '<%= t('search.suggest_all') %>', none: '<%= t('search.suggest_empty') %>'
     239  };
     240  function esc(s) { return String(s == null ? '' : s).replace(/[&<>"]/g, function(c){ return {'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c]; }); }
     241  function renderSuggest(box, d, q, ov) {
     242    var html = '';
     243    function grp(title, items, fmt) {
     244      if (!items || !items.length) return;
     245      html += '<div class="ss-group"><div class="ss-title">' + esc(title) + '</div>' + items.map(fmt).join('') + '</div>';
     246    }
     247    grp(_ST.posts, d.posts, function(p){ return '<a class="ss-item" href="' + esc(p.url) + '">' + esc(p.title) + '</a>'; });
     248    grp(_ST.tracks, d.tracks, function(tk){ var sub = tk.artist ? ' <span class="ss-sub">' + esc(tk.artist) + '</span>' : ''; return tk.url ? '<a class="ss-item" href="' + esc(tk.url) + '">' + esc(tk.title) + sub + '</a>' : '<span class="ss-item ss-noclick">' + esc(tk.title) + sub + '</span>'; });
     249    grp(_ST.events, d.events, function(ev){ return '<a class="ss-item" href="' + esc(ev.url) + '">' + esc(ev.where || ev.when) + ' <span class="ss-sub">' + esc(ev.when) + '</span></a>'; });
     250    grp(_ST.pages, d.pages, function(pg){ return '<a class="ss-item" href="' + esc(pg.url) + '">' + esc(pg.label) + '</a>'; });
     251    var hasAny = (d.posts && d.posts.length) || (d.tracks && d.tracks.length) || (d.events && d.events.length) || (d.pages && d.pages.length);
     252    if (!hasAny) { box.innerHTML = '<div class="ss-empty">' + esc(_ST.none) + '</div>'; return; }
     253    var action = (ov && ov.getAttribute('data-action')) || '/search';
     254    html += '<a class="ss-all" href="' + esc(action) + '?q=' + encodeURIComponent(q) + '">' + esc(_ST.all) + '</a>';
     255    box.innerHTML = html;
     256  }
     257  var _sTimer;
     258  document.addEventListener('input', function(e) {
     259    var inp = e.target.closest && e.target.closest('#search-overlay input[name="q"]');
     260    if (!inp) return;
     261    var box = document.getElementById('search-suggest');
     262    if (!box) return;
     263    var q = inp.value.trim();
     264    clearTimeout(_sTimer);
     265    if (q.length < 2) { box.innerHTML = ''; return; }
     266    _sTimer = setTimeout(function() {
     267      var ov = document.getElementById('search-overlay');
     268      var url = (ov && ov.getAttribute('data-suggest')) || '/search/suggest';
     269      fetch(url + '?q=' + encodeURIComponent(q))
     270        .then(function(r){ return r.ok ? r.json() : null; })
     271        .then(function(d){ if (d) renderSuggest(box, d, q, ov); })
     272        .catch(function(){});
     273    }, 200);
     274  });
     275  // Klik op een suggestie → overlay sluiten (de link/boost doet de navigatie).
     276  document.addEventListener('click', function(e) {
     277    if (e.target.closest && e.target.closest('#search-suggest a')) { var o = overlay(); if (o) o.hidden = true; }
    231278  });
    232279})();
     
    499546  display: inline-flex; align-items: center; justify-content: center;
    500547}
     548/* Live-resultaten dropdown */
     549.search-suggest { margin-top: .6rem; max-height: 60vh; overflow-y: auto; display: flex; flex-direction: column; gap: .25rem; }
     550.search-suggest:empty { display: none; }
     551.ss-group { padding: .25rem 0; }
     552.ss-title { font-size: .72rem; font-weight: 700; text-transform: uppercase; letter-spacing: .05em; color: var(--ink-soft, var(--ink-muted)); padding: .35rem .25rem .2rem; }
     553.ss-item { display: block; padding: .55rem .6rem; border-radius: 8px; color: var(--ink); text-decoration: none; font-size: .95rem; }
     554.ss-item:hover { background: var(--paper-2); color: var(--accent); }
     555.ss-item.ss-noclick { opacity: .7; cursor: default; }
     556.ss-sub { color: var(--ink-soft, var(--ink-muted)); font-size: .85em; }
     557.ss-empty { padding: .8rem .6rem; color: var(--ink-soft, var(--ink-muted)); font-size: .9rem; }
     558.ss-all { display: block; margin-top: .35rem; padding: .55rem .6rem; border-top: 1px solid var(--rule); color: var(--accent); font-weight: 600; text-decoration: none; font-size: .9rem; }
     559.ss-all:hover { text-decoration: underline; }
    501560</style>
Note: See TracChangeset for help on using the changeset viewer.