gwoe-antragspruefer/app/templates/v2/base.html

319 lines
13 KiB
HTML
Raw Normal View History

{% from "v2/components/icon.html" import icon %}
<!DOCTYPE html>
<html lang="de" data-theme="auto">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}GWÖ-Antragsprüfer{% endblock %}</title>
<link rel="alternate" type="application/atom+xml" title="GWÖ-Antragsprüfer — Neue Bewertungen" href="/api/feed.xml">
{# Design-System: Tokens zuerst, dann Fonts, dann Base-Styles #}
<link rel="stylesheet" href="/static/v2/tokens.css?v={{ app_version|default('1') }}">
<link rel="stylesheet" href="/static/v2/fonts.css?v={{ app_version|default('1') }}">
<link rel="stylesheet" href="/static/v2/v2.css?v={{ app_version|default('1') }}">
{% block head_extra %}{% endblock %}
</head>
<body class="v2">
{% block body %}
{# AppShell inline, damit {% block main %} aus Screen-Templates rendert.
include propagiert Blocks nicht (Jinja2-Limitierung), darum direkt hier. #}
<div id="v2-overlay" class="v2-overlay"></div>
<div class="v2-shell">
<aside id="v2-sidebar" class="v2-sidebar">
<div class="v2-brand">
GWÖ-<span class="grn">ANTRAGS</span><span class="blu">PRÜFER</span>
</div>
<div class="v2-brand-sub">Matrix 2.0 · Gemeinden</div>
<nav aria-label="Hauptnavigation">
<div class="v2-nav-group">
<div class="v2-nav-label">— Lesen</div>
<a href="/" class="v2-nav-item {% if v2_active_nav == 'durchsuchen' %}active{% endif %}"
aria-current="{% if v2_active_nav == 'durchsuchen' %}page{% endif %}">
{{ icon("magnifying-glass", 14) }} Durchsuchen
{% if assessment_count is defined and assessment_count %}
<span class="v2-nav-count">{{ assessment_count }}</span>
{% endif %}
</a>
{% if is_authenticated %}<a href="/v2/merkliste" class="v2-nav-item {% if v2_active_nav == 'merkliste' %}active{% endif %}">{{ icon("bookmark-simple", 14) }} Merkliste</a>{% endif %}
<a href="/v2/tags" class="v2-nav-item {% if v2_active_nav == 'tags' %}active{% endif %}">{{ icon("tag", 14) }} Tags</a>
{% if is_admin %}<a href="/v2/cluster" class="v2-nav-item {% if v2_active_nav == 'cluster' %}active{% endif %}">{{ icon("graph", 14) }} Cluster</a>{% endif %}
{% if is_authenticated %}<a href="/v2/landtag-suche" class="v2-nav-item {% if v2_active_nav == 'landtag_suche' %}active{% endif %}">{{ icon("magnifying-glass-plus", 14) }} Landtag-Suche</a>{% endif %}
</div>
{% if is_authenticated %}
<div class="v2-nav-group">
<div class="v2-nav-label">— Prüfen</div>
<a href="/v2/neu" class="v2-nav-item {% if v2_active_nav == 'neu' %}active{% endif %}">{{ icon("file-plus", 14) }} Neuer Antrag</a>
{% if is_admin %}<a href="/v2/batch" class="v2-nav-item {% if v2_active_nav == 'batch' %}active{% endif %}">{{ icon("stack", 14) }} Batch-Analyse</a>{% endif %}
</div>
<div class="v2-nav-group">
<div class="v2-nav-label">— Daten</div>
<a href="/auswertungen" class="v2-nav-item {% if v2_active_nav == 'auswertungen' %}active{% endif %}">{{ icon("chart-bar", 14) }} Auswertungen</a>
<a href="/api/auswertungen/export.csv" class="v2-nav-item">{{ icon("file-csv", 14) }} Export · API</a>
<a href="/v2/feed" class="v2-nav-item {% if v2_active_nav == 'feed' %}active{% endif %}">{{ icon("rss", 14) }} Atom-Feed</a>
<a href="/v2/abos" class="v2-nav-item {% if v2_active_nav == 'abos' %}active{% endif %}">{{ icon("envelope-simple", 14) }} Meine Abos</a>
</div>
{% endif %}
{% if is_admin %}
<div class="v2-nav-group">
<div class="v2-nav-label">— Administration</div>
<a href="/v2/admin/freischaltungen" class="v2-nav-item">{{ icon("user-check", 14) }} Freischaltungen</a>
<a href="/v2/admin/queue" class="v2-nav-item">{{ icon("list-checks", 14) }} Queue</a>
<a href="/v2/admin/abos" class="v2-nav-item">{{ icon("envelope-simple", 14) }} Abos</a>
</div>
{% endif %}
</nav>
</aside>
<header class="v2-topbar">
<button id="v2-menu-toggle" class="v2-menu-toggle" aria-label="Navigation öffnen">&#9776;</button>
<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="/quellen">{{ icon("book-open", 13) }} Quellen</a>
{# ── Globaler Bundesland-Selector ─────────────────────────────────── #}
<select id="v2-global-bl"
onchange="v2SetGlobalBl(this.value)"
aria-label="Bundesland wählen"
style="font-family:var(--font-mono);font-size:11px;padding:3px 6px;border:1px solid var(--ecg-light, var(--ecg-border));background:var(--ecg-card-bg);color:var(--ecg-dark);text-transform:uppercase;border-radius:3px;cursor:pointer;">
<option value="ALL">Bundesweit</option>
{% for bl in v2_bundeslaender %}<option value="{{ bl.code }}">{{ bl.code }}</option>{% endfor %}
</select>
{# ── Auth-Control — wird per JS nach /api/auth/me-Aufruf umgeschaltet ── #}
<div id="v2-auth-control" style="display:inline-flex;align-items:center;">
{# Platzhalter, bis initV2Auth() den Zustand kennt — unsichtbar #}
</div>
<button id="v2-theme-toggle"
onclick="window.__v2CycleTheme && window.__v2CycleTheme()"
aria-label="Farbschema wechseln"
style="background:none;border:none;cursor:pointer;font-family:var(--font-mono);font-size:11px;color:var(--ecg-dark);letter-spacing:0.06em;text-transform:uppercase;opacity:0.75;padding:0;display:inline-flex;align-items:center;gap:4px;">
<span id="v2-theme-icon">{{ icon("circle-half", 14) }}</span><span id="v2-theme-label">Auto</span>
</button>
</header>
<main class="v2-main" id="v2-main">
{% block main %}{% endblock %}
</main>
<footer class="v2-footer">
<span>GWÖ-Antragsprüfer · Matrix 2.0 · CC BY 4.0</span>
<a href="/methodik">Methodik</a>
<a href="/quellen">Quellen</a>
<a href="/impressum">Impressum</a>
<a href="/datenschutz">Datenschutz</a>
<a href="https://repo.toppyr.de/tobias/gwoe-antragspruefer">Quellcode</a>
<span class="v2-topbar-spacer"></span>
<a href="/classic" style="color:var(--ecg-green);opacity:1;">Zurück zur klassischen Ansicht</a>
</footer>
</div>
{% endblock %}
<script>
/* ── Dark-Mode Toggle ────────────────────────────────────────────── */
(function () {
const STORAGE_KEY = 'gwoe.theme';
const PREF_KEY = 'gwoe.ui';
const root = document.documentElement;
function applyTheme(theme) {
root.setAttribute('data-theme', theme);
localStorage.setItem(STORAGE_KEY, theme);
}
function cycleTheme() {
const current = localStorage.getItem(STORAGE_KEY) || 'auto';
const next = { auto: 'light', light: 'dark', dark: 'auto' }[current] || 'auto';
applyTheme(next);
updateToggleLabel();
}
function updateToggleLabel() {
const labelEl = document.getElementById('v2-theme-label');
const iconEl = document.getElementById('v2-theme-icon');
if (!labelEl) return;
const current = localStorage.getItem(STORAGE_KEY) || 'auto';
const labels = { auto: 'Auto', light: 'Hell', dark: 'Dunkel' };
const icons = {
auto: 'circle-half',
light: 'sun',
dark: 'moon'
};
labelEl.textContent = labels[current] || 'Auto';
if (iconEl) {
const iconName = icons[current] || 'circle-half';
iconEl.dataset.icon = iconName;
// Replace icon SVG dynamically via fetch (icons are static files)
fetch('/static/v2/icons/phosphor/' + iconName + '.svg')
.then(r => r.text())
.then(svg => { iconEl.innerHTML = svg; })
.catch(() => {});
}
}
// Restore stored theme
const stored = localStorage.getItem(STORAGE_KEY);
if (stored) applyTheme(stored);
window.__v2CycleTheme = cycleTheme;
document.addEventListener('DOMContentLoaded', updateToggleLabel);
})();
/* ── UI Preference (v2 = Default) ───────────────────────────────── */
/* AUTO-REDIRECT DEAKTIVIERT — er verursacht Loop mit aggressivem classic-localStorage-Set.
Umschalter via Topbar-Link „Klassische Ansicht" reicht als Opt-Out. */
(function () {
// Alte Preference-Spuren löschen, damit niemand festklebt
localStorage.removeItem('gwoe.ui');
return;
// dead code weiter unten, bewusst belassen für Nachvollziehbarkeit
// Wenn Nutzer:in zuvor explizit "classic" gewählt hat, zu /classic weiterleiten.
// gwoe.ui='classic' wird von index.html gesetzt, wenn man /classic besucht.
// Nach Rückkehr via "Zum neuen Design"-Link überschreibt v2 das localStorage,
// damit die Präferenz für den nächsten Tab-Start korrekt ist.
var pref = localStorage.getItem('gwoe.ui');
if (pref === 'classic') {
// Einmal weiterleiten; danach bleibt Nutzer:in auf v2 (kein Loop)
localStorage.removeItem('gwoe.ui');
window.location.replace('/classic');
} else {
localStorage.setItem('gwoe.ui', 'v2');
}
})();
/* ── Mobile Sidebar Toggle ───────────────────────────────────────── */
(function () {
document.addEventListener('DOMContentLoaded', function () {
const toggle = document.getElementById('v2-menu-toggle');
const sidebar = document.getElementById('v2-sidebar');
const overlay = document.getElementById('v2-overlay');
if (!toggle || !sidebar || !overlay) return;
toggle.addEventListener('click', function () {
sidebar.classList.toggle('open');
overlay.classList.toggle('open');
});
overlay.addEventListener('click', function () {
sidebar.classList.remove('open');
overlay.classList.remove('open');
});
});
})();
</script>
{% block body_scripts %}{% endblock %}
{# ── Globaler BL-Selector — Persistenz + Event ───────────────────────────── #}
<script>
(function () {
var BL_KEY = 'gwoe.bl';
window.v2SetGlobalBl = function (code) {
try { localStorage.setItem(BL_KEY, code); } catch (_) {}
window.dispatchEvent(new CustomEvent('v2-bl-changed', { detail: { bl: code } }));
};
window.v2GetGlobalBl = function () {
try { return localStorage.getItem(BL_KEY) || 'ALL'; } catch (_) { return 'ALL'; }
};
document.addEventListener('DOMContentLoaded', function () {
var sel = document.getElementById('v2-global-bl');
if (sel) sel.value = window.v2GetGlobalBl();
});
})();
</script>
{# ── Auth Modal (global, einmal pro Seite) ────────────────────────────── #}
{% include "v2/components/auth_modal.html" %}
<script>
/* ── v2 Auth-State — Topbar-Control ─────────────────────────────────── */
(function () {
var TOPBAR_CONTROL_ID = 'v2-auth-control';
var BTN_BASE = [
'background:none',
'border:none',
'cursor:pointer',
'font-family:var(--font-sans)',
'font-size:11px',
'color:var(--ecg-dark)',
'letter-spacing:0.06em',
'text-transform:uppercase',
'opacity:0.75',
'padding:0',
'display:inline-flex',
'align-items:center',
'gap:4px'
].join(';');
function renderUnauthenticated(container) {
container.innerHTML =
'<button style="' + BTN_BASE + '" onclick="v2AuthModalOpen()" aria-label="Anmelden">' +
'{{ icon("key", 13) | replace("\"", "\'") }} Anmelden' +
'</button>';
}
function renderAuthenticated(container, user) {
var name = user.preferred_username || user.name || user.sub || 'Konto';
container.innerHTML =
'<span style="' + BTN_BASE + ';cursor:default;gap:4px;">' +
'{{ icon("user", 13) | replace("\"", "\'") }} ' +
'<span style="max-width:110px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;" title="' + name + '">' + name + '</span>' +
'</span>' +
'&nbsp;' +
'<button style="' + BTN_BASE + '" onclick="v2AuthLogout()" aria-label="Abmelden">' +
'{{ icon("sign-out", 13) | replace("\"", "\'") }} Abmelden' +
'</button>';
}
async function initV2Auth() {
var container = document.getElementById(TOPBAR_CONTROL_ID);
if (!container) return;
try {
var resp = await fetch('/api/auth/me');
var data = await resp.json();
if (data && data.authenticated) {
renderAuthenticated(container, data);
} else {
renderUnauthenticated(container);
}
} catch (_) {
renderUnauthenticated(container);
}
}
async function logout() {
// Server-seitig Cookies löschen (HttpOnly → client-side nicht möglich)
try {
await fetch('/api/auth/logout', { method: 'POST', credentials: 'same-origin' });
} catch (e) { /* ignore, reload trotzdem */ }
location.reload();
}
window.v2AuthLogout = logout;
document.addEventListener('DOMContentLoaded', initV2Auth);
})();
</script>
{# Queue-Statusbar mit Hover-Tooltip — analog zu classic-UI (#149) #}
{% include "v2/components/queue_widget.html" %}
</body>
</html>