fix: Topbar-Wrap auf Mobile, Klassische-Ansicht raus, Tags-Greying + Title-Bug
Drei Korrekturen:
1. Mobile-Topbar: Auth-Widget + Bundesland-Selector + Theme-Toggle
pushten die rechte Kante über 390 px Phone-Viewport. Fix: Topbar
darf in @media (max-width: 900px) flex-wrappen, height auto,
row-gap fuer mehrzeilig.
2. Topbar-Link "Klassische Ansicht" → /classic entfernt (verlinkt auf
das alte v1-Frontend; v2 bzw. das neue v3 sind die aktiven Modi).
3. /tags-Seite hatte zwei Bugs:
- Titel wurde aus a.titel (existiert nicht) statt a.title gelesen
→ User sah nur Drucksachen-Nummern und dachte "kaum Daten".
- Kein Visual-Feedback welche Tag-Kombinationen leer wären.
Beide gefixt: title-Field korrekt, plus Tag-Greying via class
.tag-pill.disabled fuer Tags die zu 0 Treffern fuehren wuerden.
Ausserdem Score-Field gwoeScore-Fallback und HTML-Escape fuer alle
Strings (vorher XSS-anfaellig bei Title/Fraktion).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
848039627c
commit
f0588714bf
@ -819,6 +819,18 @@ body.v2 ul.v2-manual ul li::before {
|
|||||||
body.v2 .v2-main {
|
body.v2 .v2-main {
|
||||||
padding: 16px 12px;
|
padding: 16px 12px;
|
||||||
}
|
}
|
||||||
|
/* Topbar darf auf Mobile umbrechen, sonst pusht das Auth-Widget +
|
||||||
|
Bundesland-Selector + Theme-Toggle die rechte Kante ueber 390 px. */
|
||||||
|
body.v2 .v2-topbar {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
height: auto;
|
||||||
|
min-height: 32px;
|
||||||
|
row-gap: 4px;
|
||||||
|
padding: 4px 12px;
|
||||||
|
}
|
||||||
|
body.v2 .v2-topbar > * {
|
||||||
|
max-height: none;
|
||||||
|
}
|
||||||
|
|
||||||
.v2-sidebar {
|
.v2-sidebar {
|
||||||
display: none;
|
display: none;
|
||||||
|
|||||||
@ -79,7 +79,6 @@
|
|||||||
<header class="v2-topbar">
|
<header class="v2-topbar">
|
||||||
<button id="v2-menu-toggle" class="v2-menu-toggle" aria-label="Navigation öffnen">☰</button>
|
<button id="v2-menu-toggle" class="v2-menu-toggle" aria-label="Navigation öffnen">☰</button>
|
||||||
<span class="v2-topbar-spacer"></span>
|
<span class="v2-topbar-spacer"></span>
|
||||||
<a href="/classic" class="v2-back-link">{{ icon("arrow-square-out", 13) }} Klassische Ansicht</a>
|
|
||||||
<a href="/methodik">{{ icon("info", 13) }} Methodik</a>
|
<a href="/methodik">{{ icon("info", 13) }} Methodik</a>
|
||||||
<a href="/quellen">{{ icon("book-open", 13) }} Quellen</a>
|
<a href="/quellen">{{ icon("book-open", 13) }} Quellen</a>
|
||||||
|
|
||||||
|
|||||||
@ -37,6 +37,20 @@
|
|||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
}
|
}
|
||||||
.tag-pill.active .tag-count { opacity: 0.9; }
|
.tag-pill.active .tag-count { opacity: 0.9; }
|
||||||
|
/* Tags, die mit aktuellen Filtern zu 0 Treffern führen würden, werden
|
||||||
|
ausgegraut. Klickbar bleiben sie (User kann „falsche" Auswahl zurück-
|
||||||
|
nehmen), aber visuell deutlich entwertet. */
|
||||||
|
.tag-pill.disabled {
|
||||||
|
opacity: 0.35;
|
||||||
|
background: var(--ecg-bg-subtle);
|
||||||
|
border-style: dashed;
|
||||||
|
color: var(--ecg-text-muted);
|
||||||
|
}
|
||||||
|
.tag-pill.disabled:hover {
|
||||||
|
background: var(--ecg-bg-subtle);
|
||||||
|
border-color: var(--ecg-text-muted);
|
||||||
|
color: var(--ecg-text-muted);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
@ -116,12 +130,42 @@ function toggleTag(btn, tag) {
|
|||||||
btn.classList.add('active');
|
btn.classList.add('active');
|
||||||
}
|
}
|
||||||
renderFiltered();
|
renderFiltered();
|
||||||
|
updateTagAvailability();
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearFilters() {
|
function clearFilters() {
|
||||||
_selectedTags.clear();
|
_selectedTags.clear();
|
||||||
document.querySelectorAll('.tag-pill.active').forEach(b => b.classList.remove('active'));
|
document.querySelectorAll('.tag-pill.active').forEach(b => b.classList.remove('active'));
|
||||||
renderFiltered();
|
renderFiltered();
|
||||||
|
updateTagAvailability();
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Items, die zu den aktuellen aktiven Filtern passen.
|
||||||
|
Wird sowohl fuer die Ergebnis-Liste als auch fuer Tag-Greying genutzt. */
|
||||||
|
function currentFilteredItems() {
|
||||||
|
if (!_selectedTags.size) return _allItems.slice();
|
||||||
|
return _allItems.filter(a => {
|
||||||
|
const tags = new Set(a.themen || []);
|
||||||
|
return Array.from(_selectedTags).every(t => tags.has(t));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Pro Tag-Pill pruefen: wuerde Hinzufuegen dieses Tags die Ergebnisliste
|
||||||
|
auf 0 reduzieren? Wenn ja → ausgrauen. Aktive Tags werden nicht
|
||||||
|
ausgegraut (sonst kann man sie nicht mehr abwaehlen). */
|
||||||
|
function updateTagAvailability() {
|
||||||
|
const base = currentFilteredItems();
|
||||||
|
document.querySelectorAll('.tag-cloud .tag-pill').forEach(btn => {
|
||||||
|
const tag = btn.dataset.tag;
|
||||||
|
if (!tag) return;
|
||||||
|
if (_selectedTags.has(tag)) {
|
||||||
|
btn.classList.remove('disabled');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Wuerde Hinzufuegen dieses Tags die Liste leer machen?
|
||||||
|
const wouldHave = base.some(a => (a.themen || []).includes(tag));
|
||||||
|
btn.classList.toggle('disabled', !wouldHave);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderFiltered() {
|
function renderFiltered() {
|
||||||
@ -140,10 +184,7 @@ function renderFiltered() {
|
|||||||
`<span class="tag-pill active" style="cursor:default;">${t}</span>`
|
`<span class="tag-pill active" style="cursor:default;">${t}</span>`
|
||||||
).join(' ');
|
).join(' ');
|
||||||
|
|
||||||
const filtered = _allItems.filter(a => {
|
const filtered = currentFilteredItems();
|
||||||
const tags = new Set(a.themen || []);
|
|
||||||
return Array.from(_selectedTags).every(t => tags.has(t));
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!filtered.length) {
|
if (!filtered.length) {
|
||||||
resultsEl.innerHTML = '<div class="v2-kasten outline-green"><h4>Keine Ergebnisse</h4><p>Die Filterauswahl liefert keine Treffer.</p></div>';
|
resultsEl.innerHTML = '<div class="v2-kasten outline-green"><h4>Keine Ergebnisse</h4><p>Die Filterauswahl liefert keine Treffer.</p></div>';
|
||||||
@ -151,19 +192,25 @@ function renderFiltered() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
resultsEl.innerHTML = filtered.map(a => {
|
resultsEl.innerHTML = filtered.map(a => {
|
||||||
const score = (typeof a.gwoe_score === 'number') ? a.gwoe_score.toFixed(1) : '—';
|
/* API-Felder: title (englisch), gwoe_score oder gwoeScore. Vorher
|
||||||
|
wurde a.titel (deutsch) gelesen — gibt's nicht → Fallback auf
|
||||||
|
Drucksache, was dann wie ein leeres Resultat aussieht. */
|
||||||
|
const score = (typeof a.gwoe_score === 'number') ? a.gwoe_score.toFixed(1)
|
||||||
|
: (typeof a.gwoeScore === 'number') ? a.gwoeScore.toFixed(1)
|
||||||
|
: '—';
|
||||||
const bl = a.bundesland || '';
|
const bl = a.bundesland || '';
|
||||||
const fraktion = (a.fraktionen || []).join(', ');
|
const fraktion = (a.fraktionen || []).join(', ');
|
||||||
const title = a.titel || a.drucksache;
|
const title = a.title || a.titel || a.drucksache;
|
||||||
|
const escAttr = (s) => String(s).replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''})[c]);
|
||||||
return `<a href="/antrag/${encodeURIComponent(a.drucksache)}"
|
return `<a href="/antrag/${encodeURIComponent(a.drucksache)}"
|
||||||
class="v2-result-row"
|
class="v2-result-row"
|
||||||
style="display:block;text-decoration:none;">
|
style="display:block;text-decoration:none;">
|
||||||
<div class="v2-result-meta">
|
<div class="v2-result-meta">
|
||||||
<span class="v2-chip" style="font-size:10px;">${bl}</span>
|
<span class="v2-chip" style="font-size:10px;">${escAttr(bl)}</span>
|
||||||
<span style="font-size:11px;opacity:0.6;font-family:var(--font-mono);">${a.drucksache}</span>
|
<span style="font-size:11px;opacity:0.6;font-family:var(--font-mono);">${escAttr(a.drucksache)}</span>
|
||||||
${fraktion ? `<span style="font-size:11px;opacity:0.6;">${fraktion}</span>` : ''}
|
${fraktion ? `<span style="font-size:11px;opacity:0.6;">${escAttr(fraktion)}</span>` : ''}
|
||||||
</div>
|
</div>
|
||||||
<div class="v2-result-title">${title}</div>
|
<div class="v2-result-title">${escAttr(title)}</div>
|
||||||
<div class="v2-result-score" style="font-family:var(--font-mono);font-size:13px;color:var(--ecg-teal);font-weight:700;">
|
<div class="v2-result-score" style="font-family:var(--font-mono);font-size:13px;color:var(--ecg-teal);font-weight:700;">
|
||||||
Score ${score}
|
Score ${score}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user