Zwei Bugs:
1) Audio kam nicht durch — die Content-Security-Policy hatte kein
media-src und fiel auf default-src 'self' zurück. data:- (silent-WAV
zum Element-Unlock) und blob:-URLs (ElevenLabs-MP3-Cache) wurden
geblockt. Browser-Fehlermeldung im Console: „Loading media from
‚data:audio/wav;base64,…' violates the following Content Security
Policy directive". Fix: ``media-src 'self' data: blob:;`` ergänzt.
2) Tour war nur auf Startseite + Antrag-Detail eingebunden. User-Wunsch:
auf jeder Page außer Administration. Lösung: Tour-Engine-Include in
v2/base.html, mit ``{% if v2_active_nav not in [admin_*] %}``-Guard.
Pages ohne eigene ``window.GWOE_TOUR_STEPS`` bekommen einen Fallback
mit drei Stationen (Logo+Konzept, Topbar, Sidebar).
Topbar-Tour-Link sichtbar wenn ``window.gwoeTourStart`` existiert
(Engine geladen) — nicht mehr abhängig von Page-eigenen Steps.
Aufräumen: redundante Tour-Includes aus durchsuchen.html und
antrag_detail.html entfernt — die Engine kommt jetzt nur einmal aus
base.html.
449 lines
17 KiB
HTML
449 lines
17 KiB
HTML
{% extends "v2/base.html" %}
|
||
|
||
{% from "v2/components/result_row.html" import result_row %}
|
||
{% from "v2/components/chip.html" import chip %}
|
||
|
||
{% block title %}Durchsuchen — GWÖ-Antragsprüfer{% endblock %}
|
||
|
||
{% set v2_active_nav = "durchsuchen" %}
|
||
|
||
{% block main %}
|
||
|
||
{# ── Welcome-Banner: Tour-Einstieg für Erst-Besucher:innen (#185) ── #}
|
||
<aside id="gwoe-welcome-banner" class="gwoe-welcome-banner" hidden role="region"
|
||
aria-label="Willkommen — Tour-Einstieg">
|
||
<span aria-hidden="true" class="gwoe-welcome-icon">🧭</span>
|
||
<div class="gwoe-welcome-text">
|
||
<strong>Du bist neu hier?</strong>
|
||
Soll ich dir die Seite kurz erklären? Geführte Tour mit Sprachausgabe — jederzeit abbrechbar.
|
||
</div>
|
||
<div class="gwoe-welcome-actions">
|
||
<button type="button" onclick="gwoeTourStart()" class="v2-chip primary"
|
||
id="gwoe-welcome-start">Tour starten</button>
|
||
<button type="button" onclick="gwoeWelcomeDismiss()" class="gwoe-welcome-skip"
|
||
id="gwoe-welcome-dismiss" aria-label="Banner schließen">Später</button>
|
||
</div>
|
||
</aside>
|
||
|
||
{# ── Toolbar: Suche ──────────────────────────────────────────────── #}
|
||
{# BL-Filter läuft jetzt über den globalen Selector in der Topbar. #}
|
||
<div class="v2-toolbar" id="v2-toolbar" role="toolbar" aria-label="Filter und Suche">
|
||
<input class="v2-search"
|
||
type="search"
|
||
placeholder="Anträge durchsuchen …"
|
||
aria-label="Anträge durchsuchen"
|
||
id="v2-search-input">
|
||
</div>
|
||
|
||
{# ── Score-Filter + Sortierung ───────────────────────────────────── #}
|
||
<div class="v2-toolbar" role="toolbar" aria-label="Score-Filter und Sortierung" style="border:0;padding:4px 0;margin:0 0 8px;position:static;">
|
||
<button class="v2-chip active" data-band="ALL" onclick="v2SetBand(this,'ALL')">Alle Scores</button>
|
||
<button class="v2-chip" data-band="HIGH" onclick="v2SetBand(this,'HIGH')">Score 8–10</button>
|
||
<button class="v2-chip" data-band="MID" onclick="v2SetBand(this,'MID')">5–7</button>
|
||
<button class="v2-chip" data-band="LOW" onclick="v2SetBand(this,'LOW')">0–4</button>
|
||
<span class="v2-toolbar-sep"></span>
|
||
<select id="v2-sort"
|
||
aria-label="Sortierung"
|
||
onchange="v2ApplySort(this.value)"
|
||
style="padding:4px 8px;border:1px solid var(--hairline);border-radius:4px;
|
||
font-family:var(--font-mono);font-size:11px;background:var(--surface);
|
||
color:var(--ecg-dark);cursor:pointer;">
|
||
<option value="score-desc">Score ↓</option>
|
||
<option value="score-asc">Score ↑</option>
|
||
<option value="date-desc">Datum ↓</option>
|
||
<option value="date-asc">Datum ↑</option>
|
||
<option value="drucksache-desc">Drs.-Nr. ↓</option>
|
||
<option value="drucksache-asc">Drs.-Nr. ↑</option>
|
||
<option value="title-asc">Titel A–Z</option>
|
||
<option value="title-desc">Titel Z–A</option>
|
||
</select>
|
||
</div>
|
||
|
||
{# ── Ergebnisliste ───────────────────────────────────────────────── #}
|
||
<div id="v2-results" role="list">
|
||
|
||
{% if assessments %}
|
||
{% for a in assessments %}
|
||
{{ result_row(a) }}
|
||
{% endfor %}
|
||
{% else %}
|
||
<p style="font-family:var(--font-mono);font-size:13px;color:var(--ecg-dark);opacity:0.6;margin-top:32px;">
|
||
Noch keine Bewertungen in der Datenbank.
|
||
</p>
|
||
{% endif %}
|
||
|
||
</div>{# #v2-results #}
|
||
|
||
{# ── Empty-State ─────────────────────────────────────────────────── #}
|
||
<div id="v2-empty-state" class="v2-kasten outline-green" style="display:none;margin-top:32px;">
|
||
<h4>Keine Ergebnisse</h4>
|
||
<p>Die aktuelle Filterauswahl liefert keine Treffer.
|
||
<button onclick="v2ResetFilters()"
|
||
style="background:none;border:none;color:var(--ecg-green);cursor:pointer;font-weight:900;text-decoration:underline;font-size:inherit;font-family:inherit;">
|
||
Filter zurücksetzen
|
||
</button>
|
||
</p>
|
||
</div>
|
||
|
||
{# ── Tour-Stationen — was anonyme vs eingeloggte User wirklich sehen ── #}
|
||
<script>
|
||
window.GWOE_TOUR_STEPS = [
|
||
{ selector: '.v2-brand-link, .v2-brand',
|
||
title: 'Willkommen beim GWÖ-Antragsprüfer',
|
||
text: 'Diese Seite bewertet Anträge aus deutschen Parlamenten nach der Gemeinwohl-Matrix. Was sie auszeichnet: Sie schaut nicht nur, ob ein Antrag „gut klingt", sondern wie sehr er fünf Werten dient — Würde, Solidarität, Nachhaltigkeit, Gerechtigkeit und Demokratie.' },
|
||
{ selector: '#v2-search-input',
|
||
title: 'Anträge durchsuchen',
|
||
text: 'Hier findest du Anträge nach Stichwort, Drucksachen-Nummer oder Antragstellerin. Die Liste filtert sich beim Tippen mit.' },
|
||
{ selector: '.v2-toolbar:nth-of-type(2), [data-band]',
|
||
title: 'Filter nach Note',
|
||
text: 'Mit diesen Knöpfen filterst du nach Gemeinwohl-Note. Acht bis Zehn sind vorbildlich, fünf bis sieben durchwachsen, null bis vier problematisch. Daneben kannst du nach Datum, Score oder Titel sortieren.' },
|
||
{ selector: '#v2-results .v2-result-row, #v2-results',
|
||
title: 'Die Antrags-Liste',
|
||
text: 'Jede Karte zeigt einen Antrag mit seiner Note. Klick auf eine Karte öffnet die ausführliche Bewertung — dort findest du eine eigene Tour, die das Detail erklärt.' },
|
||
{% if is_authenticated %}
|
||
{ selector: '#v2-sidebar nav',
|
||
title: 'Navigation links',
|
||
text: 'Links findest du weitere Sichten. „Auswertungen" zeigt Aggregate über alle Anträge, „Stimmverhalten" die Konsistenz jeder Fraktion zwischen Wahlprogramm und tatsächlicher Stimme. Oben in der Topbar findest du außerdem „Quellen" — alle indizierten Wahl- und Grundsatzprogramme, semantisch durchsuchbar.' },
|
||
{% else %}
|
||
{ selector: '#v2-sidebar nav',
|
||
title: 'Navigation links',
|
||
text: 'Links findest du weitere Sichten — aktuell „Tags", die thematische Übersicht über alle Anträge. Oben in der Topbar findest du „Quellen", die semantisch durchsuchbare Bibliothek aller Wahl- und Grundsatzprogramme. Wenn du dich anmeldest, kommen Auswertungen, Stimmverhalten und persönliche Funktionen wie eine Merkliste dazu.' },
|
||
{% endif %}
|
||
];
|
||
</script>
|
||
|
||
{% endblock %}
|
||
|
||
{% block body_scripts %}
|
||
<script>
|
||
/* ── v2 Listenfilter ─────────────────────────────────────────────── */
|
||
(function () {
|
||
var activeBl = 'ALL';
|
||
var activeBand = 'ALL';
|
||
|
||
function getRows() {
|
||
return document.querySelectorAll('#v2-results .v2-result-row');
|
||
}
|
||
|
||
function getScore(row) {
|
||
var cell = row.querySelector('.v2-score-cell');
|
||
return cell ? parseFloat(cell.textContent) || 0 : 0;
|
||
}
|
||
|
||
function getBl(row) {
|
||
var state = row.querySelector('.v2-r-state');
|
||
return state ? state.textContent.trim().split('·')[0].trim() : '';
|
||
}
|
||
|
||
function applyFilters() {
|
||
var q = (document.getElementById('v2-search-input').value || '').toLowerCase().trim();
|
||
var rows = getRows();
|
||
var visible = 0;
|
||
|
||
rows.forEach(function (row) {
|
||
var score = getScore(row);
|
||
var bl = getBl(row);
|
||
var text = row.textContent.toLowerCase();
|
||
|
||
var blOk = (activeBl === 'ALL') || (bl === activeBl);
|
||
var bandOk = (activeBand === 'ALL') ||
|
||
(activeBand === 'HIGH' && score >= 8) ||
|
||
(activeBand === 'MID' && score >= 5 && score < 8) ||
|
||
(activeBand === 'LOW' && score < 5);
|
||
var qOk = !q || text.includes(q);
|
||
|
||
var show = blOk && bandOk && qOk;
|
||
row.style.display = show ? '' : 'none';
|
||
if (show) visible++;
|
||
});
|
||
|
||
var empty = document.getElementById('v2-empty-state');
|
||
if (empty) empty.style.display = (visible === 0) ? 'block' : 'none';
|
||
}
|
||
|
||
/* BL-Filter: globaler Selector in der Topbar */
|
||
window.addEventListener('v2-bl-changed', function (e) {
|
||
activeBl = (e.detail && e.detail.bl) ? e.detail.bl : 'ALL';
|
||
applyFilters();
|
||
});
|
||
|
||
window.v2SetBand = function (btn, band) {
|
||
activeBand = band;
|
||
document.querySelectorAll('[data-band]').forEach(function (b) {
|
||
b.classList.toggle('active', b.dataset.band === band);
|
||
});
|
||
applyFilters();
|
||
};
|
||
|
||
window.v2ResetFilters = function () {
|
||
document.getElementById('v2-search-input').value = '';
|
||
// BL auf ALL zurücksetzen: globalen Selector aktualisieren
|
||
var sel = document.getElementById('v2-global-bl');
|
||
if (sel) sel.value = 'ALL';
|
||
window.v2SetGlobalBl && window.v2SetGlobalBl('ALL');
|
||
v2SetBand(null, 'ALL');
|
||
};
|
||
|
||
document.addEventListener('DOMContentLoaded', function () {
|
||
var input = document.getElementById('v2-search-input');
|
||
if (input) input.addEventListener('input', applyFilters);
|
||
// Gespeicherten BL-Wert beim Laden anwenden
|
||
activeBl = (window.v2GetGlobalBl && window.v2GetGlobalBl()) || 'ALL';
|
||
applyFilters();
|
||
});
|
||
})();
|
||
|
||
/* ── Sort ────────────────────────────────────────────────────────── */
|
||
(function () {
|
||
var SORT_KEY = 'gwoe.v2-sort';
|
||
|
||
function getRowVal(row, field) {
|
||
if (field === 'score') {
|
||
var cell = row.querySelector('.v2-score-cell');
|
||
return cell ? parseFloat(cell.textContent) || 0 : 0;
|
||
}
|
||
if (field === 'title') {
|
||
var titleEl = row.querySelector('.v2-r-title');
|
||
return titleEl ? titleEl.textContent.trim().toLowerCase() : '';
|
||
}
|
||
if (field === 'date') {
|
||
var dateEl = row.querySelector('.v2-r-date');
|
||
return dateEl ? dateEl.textContent.trim() : '';
|
||
}
|
||
if (field === 'drucksache') {
|
||
var stateEl = row.querySelector('.v2-r-state');
|
||
return stateEl ? stateEl.textContent.trim() : '';
|
||
}
|
||
return '';
|
||
}
|
||
|
||
function sortRows(sortVal) {
|
||
var parts = sortVal.split('-');
|
||
var field = parts.slice(0, -1).join('-');
|
||
var dir = parts[parts.length - 1]; // 'asc' or 'desc'
|
||
var container = document.getElementById('v2-results');
|
||
if (!container) return;
|
||
var rows = Array.from(container.querySelectorAll('.v2-result-row'));
|
||
rows.sort(function(a, b) {
|
||
var va = getRowVal(a, field);
|
||
var vb = getRowVal(b, field);
|
||
var cmp;
|
||
if (typeof va === 'number' && typeof vb === 'number') {
|
||
cmp = va - vb;
|
||
} else {
|
||
cmp = String(va).localeCompare(String(vb), 'de');
|
||
}
|
||
return dir === 'asc' ? cmp : -cmp;
|
||
});
|
||
rows.forEach(function(row) { container.appendChild(row); });
|
||
}
|
||
|
||
window.v2ApplySort = function(val) {
|
||
try { localStorage.setItem(SORT_KEY, val); } catch (_) {}
|
||
sortRows(val);
|
||
};
|
||
|
||
document.addEventListener('DOMContentLoaded', function() {
|
||
var savedSort;
|
||
try { savedSort = localStorage.getItem(SORT_KEY); } catch (_) {}
|
||
var sortSelect = document.getElementById('v2-sort');
|
||
if (savedSort && sortSelect) {
|
||
sortSelect.value = savedSort;
|
||
sortRows(savedSort);
|
||
}
|
||
});
|
||
})();
|
||
|
||
/* ── Keyboard Shortcuts (#116 port to v2) ────────────────────────── */
|
||
(function () {
|
||
document.addEventListener('keydown', function (e) {
|
||
// Nicht in Input-Feldern auslösen
|
||
if (e.target.matches('input, textarea, select')) return;
|
||
|
||
var rows = Array.from(document.querySelectorAll('#v2-results .v2-result-row'));
|
||
var active = document.querySelector('#v2-results .v2-result-row.v2-kb-active');
|
||
var idx = active ? rows.indexOf(active) : -1;
|
||
|
||
switch (e.key) {
|
||
case 'j': // nächster Eintrag
|
||
e.preventDefault();
|
||
if (!rows.length) break;
|
||
idx = Math.min(idx + 1, rows.length - 1);
|
||
if (active) active.classList.remove('v2-kb-active');
|
||
rows[idx].classList.add('v2-kb-active');
|
||
rows[idx].scrollIntoView({ block: 'nearest' });
|
||
break;
|
||
|
||
case 'k': // vorheriger Eintrag
|
||
e.preventDefault();
|
||
if (!rows.length) break;
|
||
idx = Math.max(idx - 1, 0);
|
||
if (active) active.classList.remove('v2-kb-active');
|
||
rows[idx].classList.add('v2-kb-active');
|
||
rows[idx].scrollIntoView({ block: 'nearest' });
|
||
break;
|
||
|
||
case 'Enter': // Eintrag öffnen
|
||
if (active) {
|
||
e.preventDefault();
|
||
var href = active.getAttribute('href');
|
||
if (href) window.location.href = href;
|
||
}
|
||
break;
|
||
|
||
case '/': // Suche fokussieren
|
||
e.preventDefault();
|
||
var searchInput = document.getElementById('v2-search-input');
|
||
if (searchInput) searchInput.focus();
|
||
break;
|
||
|
||
case '?': // Shortcuts-Hilfe
|
||
e.preventDefault();
|
||
var modal = document.getElementById('v2-kb-help-modal');
|
||
if (modal) {
|
||
modal.style.display = modal.style.display === 'flex' ? 'none' : 'flex';
|
||
}
|
||
break;
|
||
|
||
case 'Escape': // Hilfe-Modal schließen (auf Detailseiten: history.back() — dort eigener Handler)
|
||
var helpModal = document.getElementById('v2-kb-help-modal');
|
||
if (helpModal && helpModal.style.display === 'flex') {
|
||
e.preventDefault();
|
||
helpModal.style.display = 'none';
|
||
}
|
||
break;
|
||
}
|
||
});
|
||
})();
|
||
</script>
|
||
|
||
{# ── Keyboard-Hilfe-Modal ───────────────────────────────────────── #}
|
||
<div id="v2-kb-help-modal"
|
||
role="dialog" aria-modal="true" aria-label="Tastenkürzel"
|
||
style="display:none;position:fixed;inset:0;background:rgba(0,0,0,0.45);
|
||
z-index:9000;align-items:center;justify-content:center;"
|
||
onclick="if(event.target===this)this.style.display='none'">
|
||
<div style="background:var(--ecg-card-bg);border:1px solid var(--ecg-border);
|
||
border-radius:8px;padding:28px 32px;min-width:280px;max-width:400px;
|
||
font-family:var(--font-mono);font-size:13px;color:var(--ecg-dark);">
|
||
<div style="font-family:var(--font-display);font-size:15px;font-weight:900;
|
||
color:var(--ecg-teal);margin-bottom:16px;letter-spacing:0.03em;">
|
||
Tastenkürzel
|
||
</div>
|
||
<table style="width:100%;border-collapse:collapse;line-height:1.9;">
|
||
<tr><td style="width:50px;"><kbd>j</kbd></td><td>Nächster Antrag</td></tr>
|
||
<tr><td><kbd>k</kbd></td><td>Vorheriger Antrag</td></tr>
|
||
<tr><td><kbd>Enter</kbd></td><td>Antrag öffnen</td></tr>
|
||
<tr><td><kbd>Esc</kbd></td><td>Detail schließen / zurück</td></tr>
|
||
<tr><td><kbd>/</kbd></td><td>Suche fokussieren</td></tr>
|
||
<tr><td><kbd>?</kbd></td><td>Diese Hilfe</td></tr>
|
||
</table>
|
||
<button onclick="document.getElementById('v2-kb-help-modal').style.display='none'"
|
||
style="margin-top:18px;font-family:var(--font-mono);font-size:11px;
|
||
background:none;border:1px solid var(--ecg-border);border-radius:4px;
|
||
padding:5px 14px;cursor:pointer;color:var(--ecg-dark);">
|
||
Schließen
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<style>
|
||
.v2-kb-active {
|
||
outline: 2px solid var(--ecg-teal);
|
||
outline-offset: -2px;
|
||
border-radius: 4px;
|
||
}
|
||
kbd {
|
||
display: inline-block;
|
||
font-family: var(--font-mono);
|
||
font-size: 11px;
|
||
background: var(--ecg-border);
|
||
border: 1px solid color-mix(in srgb, var(--ecg-border) 60%, #000);
|
||
border-radius: 3px;
|
||
padding: 1px 5px;
|
||
color: var(--ecg-dark);
|
||
}
|
||
|
||
.gwoe-welcome-banner {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 14px;
|
||
flex-wrap: wrap;
|
||
margin-bottom: 14px;
|
||
padding: 14px 18px;
|
||
border-radius: 12px;
|
||
/* dezent-warmer Hint statt scharfes Outline-Kasten */
|
||
background: linear-gradient(
|
||
135deg,
|
||
color-mix(in srgb, var(--ecg-teal) 6%, var(--surface)) 0%,
|
||
color-mix(in srgb, var(--ecg-blue) 4%, var(--surface)) 100%
|
||
);
|
||
border: 1px solid color-mix(in srgb, var(--ecg-teal) 22%, transparent);
|
||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.04);
|
||
}
|
||
.gwoe-welcome-banner[hidden] { display: none !important; }
|
||
.gwoe-welcome-icon {
|
||
font-size: 22px;
|
||
flex-shrink: 0;
|
||
filter: drop-shadow(0 1px 1px rgba(0, 0, 0, 0.08));
|
||
}
|
||
.gwoe-welcome-text {
|
||
flex: 1 1 320px;
|
||
font-size: 14px;
|
||
line-height: 1.55;
|
||
color: var(--ecg-dark);
|
||
}
|
||
.gwoe-welcome-text strong {
|
||
font-weight: 700;
|
||
color: var(--ecg-teal);
|
||
}
|
||
.gwoe-welcome-actions {
|
||
display: flex;
|
||
gap: 4px;
|
||
align-items: center;
|
||
}
|
||
.gwoe-welcome-skip {
|
||
background: none;
|
||
border: none;
|
||
color: var(--ecg-dark);
|
||
font-size: 12px;
|
||
font-family: var(--font-mono);
|
||
letter-spacing: 0.04em;
|
||
opacity: 0.55;
|
||
padding: 6px 10px;
|
||
cursor: pointer;
|
||
border-radius: 999px;
|
||
transition: opacity 0.15s ease, background 0.15s ease;
|
||
}
|
||
.gwoe-welcome-skip:hover {
|
||
opacity: 0.9;
|
||
background: color-mix(in srgb, var(--ecg-dark) 6%, transparent);
|
||
}
|
||
</style>
|
||
|
||
<script>
|
||
/* Welcome-Banner sichtbar bis Tour gestartet oder „Nein, danke".
|
||
localStorage: 'gwoe_welcome_dismissed' = '1' merkt den Schließ-Klick. */
|
||
(function () {
|
||
var KEY = 'gwoe_welcome_dismissed';
|
||
var banner = document.getElementById('gwoe-welcome-banner');
|
||
if (!banner) return;
|
||
if (localStorage.getItem(KEY) !== '1') {
|
||
banner.hidden = false;
|
||
}
|
||
window.gwoeWelcomeDismiss = function () {
|
||
try { localStorage.setItem(KEY, '1'); } catch (_) {}
|
||
banner.hidden = true;
|
||
};
|
||
// Wenn Tour gestartet wird, Banner auch wegblenden + dismissed merken.
|
||
var origStart = window.gwoeTourStart;
|
||
if (typeof origStart === 'function') {
|
||
window.gwoeTourStart = function () {
|
||
try { localStorage.setItem(KEY, '1'); } catch (_) {}
|
||
banner.hidden = true;
|
||
return origStart.apply(this, arguments);
|
||
};
|
||
}
|
||
})();
|
||
</script>
|
||
{% endblock %}
|