Fix Learning Hub search and content viewer isolation

Search (was completely broken):
- wireSearch() registered inside tabChanged for 'learning' tab — runs
  after component HTML is in the DOM (lh-search existed but listener
  was never attached because element was null at IIFE load time)
- Proper debounced search with 350ms delay, Escape key to clear
- Search results show count, 'X results for "query"', Clear Search button
- Empty state with helpful message; loading state while fetching

Content viewer showing feed items beneath it (CSS cascade bug):
- .lh-feed{display:flex} and .lh-category-bar{display:flex} defined
  at CSS lines 513/507 — after .hidden{display:none} at line 242
  So classList.add('hidden') was silently overridden
- showFeed() and showViewer() now use element.style.display with
  explicit values ('flex'/'block'/'none') which always win over class rules
- viewer started hidden via class="hidden"; now shown with style.display='block'
This commit is contained in:
Daniel Onyejesi 2026-03-24 03:13:22 -04:00
parent ac8cc38e3b
commit 747fc3322a

View file

@ -13,7 +13,7 @@
// ── Load when tab activated ────────────────────────────────
document.addEventListener('tabChanged', function(e) {
if (e.detail && e.detail.tab === 'learning') {
if (!loaded) { loadFeed(); loadCategories(); loaded = true; }
if (!loaded) { loadFeed(); loadCategories(); loaded = true; wireSearch(); }
}
// Load CMS when Content Manager tab is opened
if (e.detail && e.detail.tab === 'cms') {
@ -134,15 +134,20 @@
if (rmOpt) { var row = rmOpt.closest('.lh-option-row'); if (row) row.remove(); return; }
});
// Search
var searchTimeout = null;
var searchEl = document.getElementById('lh-search');
if (searchEl) {
// wireSearch() called after component is in DOM
var _searchTimeout = null;
function wireSearch() {
var searchEl = document.getElementById('lh-search');
if (!searchEl || searchEl._wired) return;
searchEl._wired = true;
searchEl.addEventListener('input', function() {
clearTimeout(searchTimeout);
clearTimeout(_searchTimeout);
var q = searchEl.value.trim();
if (!q) { loadFeed(); return; }
searchTimeout = setTimeout(function() { searchContent(q); }, 300);
_searchTimeout = setTimeout(function() { searchContent(q); }, 350);
});
searchEl.addEventListener('keydown', function(e) {
if (e.key === 'Escape') { searchEl.value = ''; loadFeed(); }
});
}
@ -198,13 +203,46 @@
var feedEl = document.getElementById('lh-feed');
if (!feedEl) return;
showFeed();
feedEl.innerHTML =
'<div style="padding:8px 0 12px;font-size:13px;color:var(--g500);">' +
'<i class="fas fa-search"></i> Searching for <strong>"' + esc(q) + '"</strong>…' +
'</div>';
fetch('/api/learning/search?q=' + encodeURIComponent(q), { headers: getAuthHeaders() })
.then(function(r) { return r.json(); })
.then(function(data) {
if (!data.success) return;
renderFeed(data.content, feedEl);
});
if (!data.success) { feedEl.innerHTML = '<p style="color:var(--red);padding:12px;">Search failed</p>'; return; }
var header = '<div style="padding:8px 0 12px;display:flex;align-items:center;justify-content:space-between;">' +
'<span style="font-size:13px;color:var(--g600);">' +
(data.content.length > 0
? '<strong>' + data.content.length + '</strong> result' + (data.content.length !== 1 ? 's' : '') + ' for <strong>"' + esc(q) + '"</strong>'
: 'No results for <strong>"' + esc(q) + '"</strong>') +
'</span>' +
'<button id="lh-search-clear" style="background:none;border:none;font-size:12px;color:var(--blue);cursor:pointer;padding:0;">Clear search</button>' +
'</div>';
if (data.content.length === 0) {
feedEl.innerHTML = header +
'<div style="text-align:center;padding:40px;color:var(--g400);">' +
'<i class="fas fa-search" style="font-size:32px;margin-bottom:12px;display:block;opacity:0.4;"></i>' +
'Try different keywords or browse by category' +
'</div>';
} else {
var tempEl = document.createElement('div');
renderFeed(data.content, tempEl);
feedEl.innerHTML = header + tempEl.innerHTML;
}
var clearBtn = document.getElementById('lh-search-clear');
if (clearBtn) {
clearBtn.addEventListener('click', function() {
var s = document.getElementById('lh-search');
if (s) s.value = '';
loadFeed();
});
}
})
.catch(function() { feedEl.innerHTML = '<p style="color:var(--red);padding:12px;">Search error</p>'; });
}
function renderFeed(items, feedEl) {
@ -250,26 +288,28 @@
function showFeed() {
currentView = 'feed';
var feedEl = document.getElementById('lh-feed');
var viewerEl = document.getElementById('lh-viewer');
var catsEl = document.getElementById('lh-categories');
var searchWrap = document.getElementById('lh-search');
if (feedEl) feedEl.classList.remove('hidden');
if (viewerEl) viewerEl.classList.add('hidden');
if (catsEl) catsEl.classList.remove('hidden');
if (searchWrap) searchWrap.closest('.card').classList.remove('hidden');
var feedEl = document.getElementById('lh-feed');
var viewerEl = document.getElementById('lh-viewer');
var catsEl = document.getElementById('lh-categories');
var searchEl = document.getElementById('lh-search');
// Use style.display — classList.add('hidden') is overridden by .lh-feed{display:flex}
// and .lh-category-bar{display:flex} which are defined later in CSS
if (feedEl) feedEl.style.display = 'flex';
if (viewerEl) viewerEl.style.display = 'none';
if (catsEl) catsEl.style.display = 'flex';
if (searchEl) { var sc = searchEl.closest('.card'); if (sc) sc.style.display = ''; }
}
function showViewer(item) {
currentView = 'viewer';
var feedEl = document.getElementById('lh-feed');
var viewerEl = document.getElementById('lh-viewer');
var catsEl = document.getElementById('lh-categories');
var searchWrap = document.getElementById('lh-search');
if (feedEl) feedEl.classList.add('hidden');
if (viewerEl) viewerEl.classList.remove('hidden');
if (catsEl) catsEl.classList.add('hidden');
if (searchWrap) searchWrap.closest('.card').classList.add('hidden');
var feedEl = document.getElementById('lh-feed');
var viewerEl = document.getElementById('lh-viewer');
var catsEl = document.getElementById('lh-categories');
var searchEl = document.getElementById('lh-search');
if (feedEl) feedEl.style.display = 'none';
if (viewerEl) viewerEl.style.display = 'block';
if (catsEl) catsEl.style.display = 'none';
if (searchEl) { var sc = searchEl.closest('.card'); if (sc) sc.style.display = 'none'; }
var titleEl = document.getElementById('lh-viewer-title');
var metaEl = document.getElementById('lh-viewer-meta');