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 ──────────────────────────────── // ── Load when tab activated ────────────────────────────────
document.addEventListener('tabChanged', function(e) { document.addEventListener('tabChanged', function(e) {
if (e.detail && e.detail.tab === 'learning') { 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 // Load CMS when Content Manager tab is opened
if (e.detail && e.detail.tab === 'cms') { 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; } if (rmOpt) { var row = rmOpt.closest('.lh-option-row'); if (row) row.remove(); return; }
}); });
// Search // wireSearch() called after component is in DOM
var searchTimeout = null; var _searchTimeout = null;
var searchEl = document.getElementById('lh-search'); function wireSearch() {
if (searchEl) { var searchEl = document.getElementById('lh-search');
if (!searchEl || searchEl._wired) return;
searchEl._wired = true;
searchEl.addEventListener('input', function() { searchEl.addEventListener('input', function() {
clearTimeout(searchTimeout); clearTimeout(_searchTimeout);
var q = searchEl.value.trim(); var q = searchEl.value.trim();
if (!q) { loadFeed(); return; } 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'); var feedEl = document.getElementById('lh-feed');
if (!feedEl) return; if (!feedEl) return;
showFeed(); 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() }) fetch('/api/learning/search?q=' + encodeURIComponent(q), { headers: getAuthHeaders() })
.then(function(r) { return r.json(); }) .then(function(r) { return r.json(); })
.then(function(data) { .then(function(data) {
if (!data.success) return; if (!data.success) { feedEl.innerHTML = '<p style="color:var(--red);padding:12px;">Search failed</p>'; return; }
renderFeed(data.content, feedEl); 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) { function renderFeed(items, feedEl) {
@ -250,26 +288,28 @@
function showFeed() { function showFeed() {
currentView = 'feed'; currentView = 'feed';
var feedEl = document.getElementById('lh-feed'); var feedEl = document.getElementById('lh-feed');
var viewerEl = document.getElementById('lh-viewer'); var viewerEl = document.getElementById('lh-viewer');
var catsEl = document.getElementById('lh-categories'); var catsEl = document.getElementById('lh-categories');
var searchWrap = document.getElementById('lh-search'); var searchEl = document.getElementById('lh-search');
if (feedEl) feedEl.classList.remove('hidden'); // Use style.display — classList.add('hidden') is overridden by .lh-feed{display:flex}
if (viewerEl) viewerEl.classList.add('hidden'); // and .lh-category-bar{display:flex} which are defined later in CSS
if (catsEl) catsEl.classList.remove('hidden'); if (feedEl) feedEl.style.display = 'flex';
if (searchWrap) searchWrap.closest('.card').classList.remove('hidden'); 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) { function showViewer(item) {
currentView = 'viewer'; currentView = 'viewer';
var feedEl = document.getElementById('lh-feed'); var feedEl = document.getElementById('lh-feed');
var viewerEl = document.getElementById('lh-viewer'); var viewerEl = document.getElementById('lh-viewer');
var catsEl = document.getElementById('lh-categories'); var catsEl = document.getElementById('lh-categories');
var searchWrap = document.getElementById('lh-search'); var searchEl = document.getElementById('lh-search');
if (feedEl) feedEl.classList.add('hidden'); if (feedEl) feedEl.style.display = 'none';
if (viewerEl) viewerEl.classList.remove('hidden'); if (viewerEl) viewerEl.style.display = 'block';
if (catsEl) catsEl.classList.add('hidden'); if (catsEl) catsEl.style.display = 'none';
if (searchWrap) searchWrap.closest('.card').classList.add('hidden'); if (searchEl) { var sc = searchEl.closest('.card'); if (sc) sc.style.display = 'none'; }
var titleEl = document.getElementById('lh-viewer-title'); var titleEl = document.getElementById('lh-viewer-title');
var metaEl = document.getElementById('lh-viewer-meta'); var metaEl = document.getElementById('lh-viewer-meta');