fix: Filter-Navigation vollstaendig implementiert - hashchange-Event-Listener und Navigation-Klick-Handler aufgeraeumt

- toten Nav-Link 'Kategorien' entfernt (type existiert nicht in
  templates.json)
- Filter-State (currentType, currentQuery) + gemeinsamer
  applyFilters()-Helper statt drei duplizierter Bloecke
  (hashchange, nav-click, init). Behebt Active-Class-Inkonsistenz
  zwischen Initial-Load und hashchange-Handler.
- Such- und Typ-Filter jetzt gekoppelt: applyFilters wendet beide
  kombiniert auf allTemplates an (kein Cache-Bypass via
  loadTemplates mehr).
- setNavActive leitet den aktiven Link aus dem href ab, nicht aus
  textContent -> keine Sonderbehandlung fuer 'Alle' noetig.

Verifiziert: JS parst (node --check), GET / 200, applyFilters hat
genau eine Definition, search- und hashchange-Handler rufen
loadTemplates nicht mehr auf.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Michael 2026-04-24 16:14:41 +02:00
parent 9be2ccfd9b
commit 1e2072c4fe

View file

@ -510,7 +510,6 @@
<a href="#?type=system">System</a> <a href="#?type=system">System</a>
<a href="#?type=user">User</a> <a href="#?type=user">User</a>
<a href="#?type=custom">Custom</a> <a href="#?type=custom">Custom</a>
<a href="#?type=categories">Kategorien</a>
</nav> </nav>
<div class="filter-bar"> <div class="filter-bar">
@ -939,16 +938,44 @@ $ python web/serve.py</div>
`).join(''); `).join('');
} }
// Search // Filter state
document.getElementById('search').addEventListener('input', async (e) => { let currentType = null;
const query = e.target.value.toLowerCase(); let currentQuery = '';
const templates = await loadTemplates();
const filtered = templates.filter(t => function parseTypeFromHash() {
t.name.toLowerCase().includes(query) || const match = window.location.hash.match(/[?&]type=([^&]+)/);
t.description.toLowerCase().includes(query) || return match ? decodeURIComponent(match[1]) : null;
t.tags.some(tag => tag.toLowerCase().includes(query)) }
function setNavActive(type) {
document.querySelectorAll('.nav a').forEach(a => {
const m = (a.getAttribute('href') || '').match(/type=([^&]+)/);
const aType = m ? m[1] : null;
a.classList.toggle('active', aType === type);
});
}
function applyFilters() {
let list = allTemplates;
if (currentType) {
list = list.filter(t => t.type === currentType);
}
if (currentQuery) {
const q = currentQuery;
list = list.filter(t =>
(t.name || '').toLowerCase().includes(q) ||
(t.description || '').toLowerCase().includes(q) ||
(t.tags || []).some(tag => tag.toLowerCase().includes(q))
); );
renderTemplates(filtered); }
setNavActive(currentType);
renderTemplates(list);
}
// Search
document.getElementById('search').addEventListener('input', (e) => {
currentQuery = e.target.value.toLowerCase();
applyFilters();
}); });
// Close modal on overlay click // Close modal on overlay click
@ -961,69 +988,29 @@ $ python web/serve.py</div>
if (e.key === 'Escape') closeModal(); if (e.key === 'Escape') closeModal();
}); });
// Handle navigation filter clicks via hash // Hash -> type filter
window.addEventListener('hashchange', async () => { window.addEventListener('hashchange', () => {
const hash = window.location.hash.substring(2); currentType = parseTypeFromHash();
let templates = await loadTemplates(); applyFilters();
let typeFilter = hash.split('=')[1];
// Filter templates by type
if (typeFilter && typeFilter !== 'Alle') {
templates = templates.filter(t => t.type === typeFilter);
}
// Update active state in navigation
document.querySelectorAll('.nav a').forEach(a => {
a.classList.remove('active');
if (a.textContent === 'Alle' && (!typeFilter || typeFilter === 'Alle')) {
a.classList.add('active');
} else if (a.getAttribute('href').includes(`type=${typeFilter}`)) {
a.classList.add('active');
}
}); });
renderTemplates(templates); // Nav clicks set the hash; hashchange drives filtering
});
// Click handler for navigation
document.querySelectorAll('.nav a').forEach(link => { document.querySelectorAll('.nav a').forEach(link => {
link.addEventListener('click', (e) => { link.addEventListener('click', (e) => {
e.preventDefault(); e.preventDefault();
// Spezialbehandlung für "Alle" const target = link.getAttribute('href') || '#';
if (link.textContent.trim() === 'Alle') { if (window.location.hash === target || (target === '#' && window.location.hash === '')) {
window.location.hash = '#'; return; // same target, no hashchange would fire
} else {
const href = link.getAttribute('href');
const type = href.substring(href.indexOf('=') + 1);
window.location.hash = `#?type=${type}`;
} }
window.location.hash = target;
}); });
}); });
// Initial load // Initial load
loadTemplates().then(t => { loadTemplates().then(t => {
allTemplates = t; allTemplates = t;
currentType = parseTypeFromHash();
// Check for initial hash applyFilters();
const hash = window.location.hash.substring(2);
let typeFilter = hash ? hash.split('=')[1] : undefined;
let filteredTemplates = t;
if (typeFilter && typeFilter !== 'Alle') {
filteredTemplates = t.filter(template => template.type === typeFilter);
}
// Update active state
document.querySelectorAll('.nav a').forEach(a => {
a.classList.remove('active');
if (!typeFilter || a.textContent === 'Alle') {
a.classList.add('active');
} else if (a.getAttribute('href').includes(`type=${typeFilter}`)) {
a.classList.add('active');
}
});
renderTemplates(filteredTemplates);
}); });
</script> </script>
</body> </body>