78a1cdca24
- 13 Kapitel: alle Fachbegriffe per g()-Tooltip verlinkt, fehlende Begriffe als vollwertige Glossar-Einträge angelegt (short/text + Leichte-Sprache-Felder, glossar:-Querverweise, ohne Bild — Bild-Batch folgt) - Modul-Mappings vervollständigt: jeder Buch-Begriff erscheint auf glossar?module=<sim> (kofferdetektiv erstmals gemappt, 31 Begriffe; klima auf beide module_ids) - Ergänzende Erklärsätze im Buch, wo Begriffe bisher nur genannt wurden - glossar.php: Chip „📖 Zum Schulbuch-Kapitel" bei aktivem Modul-Filter (BUCH_LINK-Map, klima-3d → buch-klima) - SQL idempotent (INSERT IGNORE, Mappings per key_slug-Subquery, keine harten IDs): App/Don_t_Deploy/2026-07-15-buch-glossar-*.sql Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1157 lines
49 KiB
PHP
1157 lines
49 KiB
PHP
<?php
|
||
/**
|
||
* Glossar — Übersichtsseite + Detailansicht.
|
||
*
|
||
* /App/pages/glossar.php → Alle Begriffe als Grid
|
||
* /App/pages/glossar.php?term=co2 → Eintrag direkt geöffnet (für Tooltip-Links aus Sims)
|
||
* /App/pages/glossar.php?module=klima → vorgefilterter Grid
|
||
*
|
||
* Datenquelle: /php/api/glossar.php
|
||
* Modul-Metadaten: DB-Tabelle module_info (Admin-pflegbar)
|
||
*/
|
||
require_once __DIR__ . '/../php/config/app.php';
|
||
require_once __DIR__ . '/../php/config/db.php';
|
||
|
||
$term = isset($_GET['term']) ? preg_replace('/[^a-z0-9_-]/', '', strtolower($_GET['term'])) : '';
|
||
$module = isset($_GET['module']) ? preg_replace('/[^a-z0-9_-]/', '', strtolower($_GET['module'])) : '';
|
||
|
||
$bp = BASE_PATH;
|
||
$api = BASE_URL . '/php/api/glossar.php';
|
||
|
||
// Modul-Metadaten aus DB (Icons + Titel; Farben bleiben Design-Code).
|
||
// Filter-Chips zeigen NUR Module mit echter Sim — Status 'aktiv' allein
|
||
// reicht nicht (z.B. 'stadt' war als aktiv markiert ohne Sim-Code, 'klima-3d'
|
||
// teilt sich Sim-Ordner mit 'klima'). Indikator: existiert App/sims/<slug>/
|
||
// als Verzeichnis. Damit bleibt der Filter automatisch in Sync mit den
|
||
// tatsächlich aufrufbaren Simulationen.
|
||
$simsRoot = realpath(__DIR__ . '/../sims');
|
||
$modDb = getDB()->query("
|
||
SELECT module_id, title, icon, sort_order
|
||
FROM module_info
|
||
WHERE status IN ('aktiv', 'beta')
|
||
ORDER BY sort_order, title
|
||
")->fetchAll(PDO::FETCH_ASSOC);
|
||
$moduleMeta = [];
|
||
$moduleOrder = [];
|
||
foreach ($modDb as $m) {
|
||
if (!is_dir($simsRoot . '/' . $m['module_id'])) continue; // kein Sim-Code → kein Filter-Chip
|
||
$moduleMeta[$m['module_id']] = [
|
||
'icon' => $m['icon'] ?: '📘',
|
||
'label' => $m['title'],
|
||
];
|
||
$moduleOrder[] = $m['module_id'];
|
||
}
|
||
|
||
// Schulbuch-Kapitel je Modul (für den 📖-Link bei aktivem Modul-Filter)
|
||
$buchLink = [];
|
||
$buchAlias = ['klima-3d' => 'klima'];
|
||
foreach (array_keys($moduleMeta) as $mid) {
|
||
$ziel = $buchAlias[$mid] ?? $mid;
|
||
if (is_file(__DIR__ . '/buch-' . $ziel . '.php')) $buchLink[$mid] = 'buch-' . $ziel;
|
||
}
|
||
?><!DOCTYPE html>
|
||
<html lang="de">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
|
||
<title>Glossar geograsim.at<?= $term ? ' — ' . htmlspecialchars($term) : '' ?></title>
|
||
<meta name="description" content="Das GeoGraSim-Glossar erklärt Fachbegriffe aus Klima, Energie, Raumplanung und Geografie kinderverständlich mit konkreten Beispielen und Vergleichsgrafiken.">
|
||
<link rel="icon" type="image/svg+xml" href="<?= $bp ?>/favicon.svg">
|
||
<link rel="icon" type="image/png" href="<?= $bp ?>/favicon-96x96.png" sizes="96x96">
|
||
<link rel="stylesheet" href="<?= $bp ?>/assets/fonts/inter.css">
|
||
<link rel="stylesheet" href="<?= $bp ?>/assets/css/design-system.css">
|
||
<style>
|
||
/* Override body overflow — Glossar is scrollable */
|
||
body { overflow: auto; height: auto; min-height: 100dvh; background: var(--ggs-bg); }
|
||
|
||
/* ===== Quick-Strip (Mindmap-Link, früher in der Topbar) ===== */
|
||
.gl-quick-strip {
|
||
max-width: 1100px; margin: .8rem auto 0; padding: 0 1.2rem;
|
||
text-align: right;
|
||
}
|
||
.gl-quick-strip .gl-btn-sm { display: inline-flex; }
|
||
|
||
/* ===== Legacy-Topbar (nicht mehr benutzt — Klassen behalten falls noch referenziert) ===== */
|
||
.gl-topbar {
|
||
background: var(--ggs-white);
|
||
padding: .55rem 1.2rem;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: .8rem;
|
||
border-bottom: 1px solid var(--ggs-border);
|
||
position: sticky; top: 0; z-index: 50;
|
||
}
|
||
.gl-topbar a { text-decoration: none; }
|
||
.gl-topbar-brand { display: flex; align-items: center; gap: .5rem; }
|
||
.gl-topbar-brand img.gl-text { height: 40px; display: block; }
|
||
.gl-topbar .spacer { flex: 1; }
|
||
.gl-btn-sm {
|
||
display: inline-flex; align-items: center; gap: .3rem;
|
||
min-height: 40px; padding: .4rem .8rem;
|
||
border: 1px solid var(--ggs-border);
|
||
border-radius: var(--ggs-radius-sm); background: var(--ggs-white);
|
||
font-size: .78rem; font-weight: 600; cursor: pointer;
|
||
color: var(--ggs-text); font-family: inherit;
|
||
text-decoration: none;
|
||
}
|
||
.gl-btn-sm.gl-btn-primary { background: var(--ggs-fjord); color: var(--ggs-white); border-color: var(--ggs-fjord); }
|
||
@media (hover: hover) {
|
||
.gl-btn-sm:hover { background: var(--ggs-fjord-light); color: var(--ggs-fjord-dark); border-color: var(--ggs-fjord); }
|
||
.gl-btn-sm.gl-btn-primary:hover { background: var(--ggs-fjord-dark); }
|
||
}
|
||
|
||
/* ===== Hero ===== */
|
||
.gl-hero {
|
||
background: linear-gradient(135deg, var(--ggs-fjord-dark) 0%, var(--ggs-fjord) 100%);
|
||
color: var(--ggs-white);
|
||
padding: 3rem 1.5rem 3.5rem;
|
||
text-align: center;
|
||
}
|
||
.gl-hero h1 { font-size: clamp(1.8rem, 4vw, 2.6rem); font-weight: 900; letter-spacing: -.03em; margin-bottom: .5rem; }
|
||
.gl-hero p { font-size: 1.02rem; color: rgba(255,255,255,.88); max-width: 640px; margin: 0 auto 1.8rem; }
|
||
.gl-hero-stats { display: flex; justify-content: center; gap: 2rem; flex-wrap: wrap; margin-top: 1.2rem; }
|
||
.gl-hero-stat { text-align: center; }
|
||
.gl-hero-stat-num { font-size: 1.8rem; font-weight: 900; display: block; }
|
||
.gl-hero-stat-lbl { font-size: .72rem; opacity: .8; text-transform: uppercase; letter-spacing: .08em; }
|
||
|
||
/* ===== Controls (Search + Filter) ===== */
|
||
.gl-controls { max-width: 1100px; margin: -1.8rem auto 1.5rem; padding: 0 1.2rem; position: relative; z-index: 5; }
|
||
.gl-search-wrap {
|
||
background: var(--ggs-white); border-radius: var(--ggs-radius-lg);
|
||
box-shadow: var(--ggs-shadow-lg); padding: 1rem 1.2rem;
|
||
display: flex; align-items: center; gap: .8rem; border: 1px solid var(--ggs-border);
|
||
}
|
||
.gl-search-icon { font-size: 1.2rem; opacity: .55; }
|
||
.gl-search-input {
|
||
flex: 1; border: 0; outline: 0; font-size: 1rem; font-family: inherit;
|
||
color: var(--ggs-text); background: transparent; padding: .4rem 0;
|
||
}
|
||
.gl-search-input::placeholder { color: var(--ggs-text-light); }
|
||
.gl-search-clear {
|
||
background: transparent; border: 0; cursor: pointer; color: var(--ggs-text-muted);
|
||
font-size: 1.3rem; padding: 0 .4rem; display: none;
|
||
}
|
||
.gl-search-clear.show { display: inline-block; }
|
||
|
||
.gl-filters { display: flex; flex-wrap: wrap; gap: .5rem; margin-top: 1rem; justify-content: center; }
|
||
.gl-chip {
|
||
display: inline-flex; align-items: center; gap: .3rem;
|
||
min-height: 38px; padding: .45rem 1rem;
|
||
border-radius: var(--ggs-radius-pill);
|
||
background: var(--ggs-white); border: 1.5px solid var(--ggs-border);
|
||
font-size: .82rem; font-weight: 600; color: var(--ggs-text-muted);
|
||
cursor: pointer; transition: all .2s var(--ggs-ease); font-family: inherit;
|
||
white-space: nowrap;
|
||
}
|
||
.gl-chip.active { background: var(--ggs-fjord); color: var(--ggs-white); border-color: var(--ggs-fjord); }
|
||
a.gl-chip.gl-buch { background: var(--ggs-forest, #1f4b37); color: var(--ggs-white); border-color: var(--ggs-forest, #1f4b37); text-decoration: none; }
|
||
@media (hover: hover) {
|
||
.gl-chip:hover { border-color: var(--ggs-fjord); color: var(--ggs-fjord); }
|
||
}
|
||
.gl-chip-count { font-size: .65rem; opacity: .65; font-weight: 500; }
|
||
|
||
/* ===== Grid ===== */
|
||
.gl-grid-wrap { max-width: 1100px; margin: 0 auto 3rem; padding: 0 1.2rem; scroll-behavior: smooth; }
|
||
html { scroll-behavior: smooth; }
|
||
.gl-grid-info { font-size: .8rem; color: var(--ggs-text-muted); margin-bottom: .8rem; padding: 0 .3rem; }
|
||
|
||
/* Buchstaben-Sprungnavigation (A-Z-Leiste) */
|
||
.gl-letter-nav {
|
||
display: flex; flex-wrap: wrap; gap: .2rem;
|
||
background: var(--ggs-white);
|
||
border: 1px solid var(--ggs-border);
|
||
border-radius: var(--ggs-radius-md);
|
||
padding: .5rem .6rem;
|
||
margin: .3rem 0 1rem;
|
||
position: sticky; top: 52px; z-index: 40;
|
||
box-shadow: var(--ggs-shadow-sm);
|
||
}
|
||
.gl-letter-nav a, .gl-letter-nav span {
|
||
display: inline-flex; align-items: center; justify-content: center;
|
||
min-width: 28px; height: 28px; padding: 0 .35rem;
|
||
font-size: .82rem; font-weight: 700;
|
||
border-radius: var(--ggs-radius-sm);
|
||
text-decoration: none; user-select: none;
|
||
}
|
||
.gl-letter-nav a {
|
||
color: var(--ggs-fjord-dark); background: var(--ggs-fjord-light);
|
||
transition: all .15s var(--ggs-ease);
|
||
}
|
||
.gl-letter-nav a, .gl-letter-nav span {
|
||
min-width: 36px; height: 36px;
|
||
}
|
||
@media (hover: hover) {
|
||
.gl-letter-nav a:hover { background: var(--ggs-fjord); color: var(--ggs-white); }
|
||
}
|
||
.gl-letter-nav span { color: var(--ggs-text-light); opacity: .45; cursor: default; }
|
||
|
||
/* Buchstabenbanner (Warning-Style: schmal, volle Breite, Buchstabe links) */
|
||
.gl-letter-banner {
|
||
grid-column: 1 / -1;
|
||
background: var(--ggs-sand);
|
||
color: var(--ggs-fjord-dark);
|
||
border-left: 5px solid var(--ggs-sand-dark);
|
||
border-radius: var(--ggs-radius-sm);
|
||
padding: .5rem 1rem;
|
||
font-size: 1.15rem; font-weight: 900;
|
||
letter-spacing: -.01em;
|
||
display: flex; align-items: baseline; gap: .6rem;
|
||
scroll-margin-top: 100px;
|
||
margin-top: .4rem;
|
||
}
|
||
.gl-letter-banner:first-child { margin-top: 0; }
|
||
.gl-letter-banner-count {
|
||
font-size: .72rem; font-weight: 600; color: var(--ggs-text-muted);
|
||
letter-spacing: 0;
|
||
}
|
||
|
||
.gl-grid {
|
||
display: grid; grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
|
||
gap: 1rem;
|
||
}
|
||
.gl-card {
|
||
background: var(--ggs-white); border: 1px solid var(--ggs-border);
|
||
border-radius: var(--ggs-radius-md); overflow: hidden; cursor: pointer;
|
||
transition: all .25s var(--ggs-ease); text-align: left;
|
||
font-family: inherit; padding: 0; display: flex; flex-direction: column;
|
||
}
|
||
@media (hover: hover) {
|
||
.gl-card:hover { transform: translateY(-3px); border-color: var(--ggs-fjord); box-shadow: var(--ggs-shadow-md); }
|
||
}
|
||
.gl-card:active { transform: scale(.98); border-color: var(--ggs-fjord); }
|
||
.gl-card-img {
|
||
height: 110px; background: var(--ggs-fjord-light);
|
||
display: flex; align-items: center; justify-content: center;
|
||
overflow: hidden; position: relative;
|
||
}
|
||
.gl-card-img img, .gl-card-img svg { max-width: 100%; max-height: 100%; object-fit: contain; }
|
||
.gl-card-img.gl-no-img {
|
||
background: linear-gradient(135deg, var(--ggs-fjord-light), var(--ggs-moss-light));
|
||
}
|
||
.gl-card-img-emoji { font-size: 3rem; opacity: .45; }
|
||
.gl-card-body { padding: .9rem 1rem 1rem; flex: 1; display: flex; flex-direction: column; }
|
||
.gl-card-category {
|
||
font-size: .62rem; font-weight: 700; text-transform: uppercase; letter-spacing: .06em;
|
||
color: var(--ggs-fjord); margin-bottom: .2rem;
|
||
}
|
||
.gl-card-title { font-size: 1rem; font-weight: 800; color: var(--ggs-fjord-dark); margin-bottom: .3rem; line-height: 1.25; }
|
||
.gl-card-short { font-size: .82rem; color: var(--ggs-text); line-height: 1.45; flex: 1; }
|
||
.gl-card-modules { display: flex; gap: .25rem; margin-top: .6rem; }
|
||
.gl-card-mod { font-size: .85rem; }
|
||
|
||
/* Empty / loading */
|
||
.gl-empty { text-align: center; padding: 3rem 1rem; color: var(--ggs-text-muted); }
|
||
.gl-empty strong { display: block; font-size: 1.1rem; color: var(--ggs-text); margin-bottom: .3rem; }
|
||
.gl-loading {
|
||
text-align: center; padding: 3rem 1rem; color: var(--ggs-text-muted);
|
||
font-size: .9rem;
|
||
}
|
||
|
||
/* ===== Detail Modal ===== */
|
||
.gl-modal {
|
||
position: fixed; inset: 0; background: rgba(31, 78, 90, .78); backdrop-filter: blur(6px);
|
||
z-index: 100; display: none; align-items: flex-start; justify-content: center;
|
||
overflow-y: auto; padding: 2rem 1rem;
|
||
}
|
||
.gl-modal.open { display: flex; }
|
||
.gl-modal-card {
|
||
background: var(--ggs-white); border-radius: var(--ggs-radius-lg);
|
||
max-width: 780px; width: 100%; padding: 1.8rem 2rem 2rem;
|
||
position: relative; box-shadow: 0 20px 60px rgba(0,0,0,.3);
|
||
margin: auto;
|
||
}
|
||
.gl-modal-close {
|
||
position: absolute; top: 14px; right: 14px;
|
||
width: 34px; height: 34px; border-radius: 50%;
|
||
background: var(--ggs-bg); border: 0; cursor: pointer;
|
||
display: flex; align-items: center; justify-content: center;
|
||
font-size: 1.3rem; color: var(--ggs-text-muted);
|
||
transition: all .2s var(--ggs-ease);
|
||
}
|
||
.gl-modal-close:hover { background: var(--ggs-coral-light); color: var(--ggs-coral); }
|
||
|
||
.gl-detail-category {
|
||
display: inline-block; font-size: .68rem; font-weight: 700; text-transform: uppercase;
|
||
letter-spacing: .08em; color: var(--ggs-fjord);
|
||
background: var(--ggs-fjord-light); padding: 3px 10px;
|
||
border-radius: var(--ggs-radius-pill); margin-bottom: .4rem;
|
||
}
|
||
.gl-detail-title { font-size: 1.7rem; font-weight: 900; color: var(--ggs-fjord-dark); margin-bottom: .3rem; letter-spacing: -.02em; }
|
||
.gl-detail-short { font-size: 1.02rem; color: var(--ggs-text); font-style: italic; margin-bottom: 1rem; line-height: 1.5; }
|
||
.gl-detail-img {
|
||
background: var(--ggs-bg);
|
||
border-radius: var(--ggs-radius-md);
|
||
margin: 1rem 0 1.2rem; padding: 1rem;
|
||
text-align: center; border: 1px solid var(--ggs-border);
|
||
cursor: zoom-in; position: relative;
|
||
transition: border-color .2s var(--ggs-ease);
|
||
}
|
||
.gl-detail-img:hover { border-color: var(--ggs-fjord); }
|
||
.gl-detail-img img, .gl-detail-img svg { max-width: 100%; height: auto; }
|
||
.gl-detail-img::after {
|
||
content: '⤢ Vollbild';
|
||
position: absolute; bottom: 6px; right: 10px;
|
||
font-size: .7rem; font-weight: 700; color: var(--ggs-text-muted);
|
||
background: var(--ggs-white); padding: 2px 8px;
|
||
border-radius: var(--ggs-radius-pill);
|
||
opacity: .75; pointer-events: none;
|
||
}
|
||
|
||
/* Toggle Leichte Sprache */
|
||
.gl-easy-toggle {
|
||
display: inline-flex; background: var(--ggs-bg);
|
||
border-radius: var(--ggs-radius-pill); padding: 3px;
|
||
gap: 2px; margin-bottom: 1rem;
|
||
border: 1px solid var(--ggs-border);
|
||
}
|
||
.gl-easy-toggle button {
|
||
display: inline-flex; align-items: center; gap: .3rem;
|
||
min-height: 32px; padding: .25rem .75rem;
|
||
border: 0; background: transparent; font-family: inherit;
|
||
font-size: .78rem; font-weight: 600; color: var(--ggs-text-muted);
|
||
cursor: pointer; border-radius: var(--ggs-radius-pill);
|
||
transition: all .15s var(--ggs-ease);
|
||
}
|
||
.gl-easy-toggle button.active {
|
||
background: var(--ggs-fjord); color: var(--ggs-white);
|
||
box-shadow: 0 1px 3px rgba(0,0,0,.12);
|
||
}
|
||
.gl-easy-toggle button:not(.active):hover { color: var(--ggs-fjord-dark); }
|
||
|
||
/* Lightbox für Infografiken */
|
||
.gl-lightbox {
|
||
position: fixed; inset: 0; z-index: 200;
|
||
background: rgba(15, 34, 48, .94);
|
||
display: none; align-items: center; justify-content: center;
|
||
padding: 40px 2vw 60px; cursor: zoom-out;
|
||
animation: gl-fade-in .2s var(--ggs-ease);
|
||
overflow: auto;
|
||
}
|
||
.gl-lightbox.open { display: flex; }
|
||
.gl-lightbox-frame {
|
||
/* KONKRETE Breite — sonst kollabiert der Flex-Item auf Padding, */
|
||
/* wenn die SVG-intrinsische Größe 0 ist (Safari / iPad-Bug). */
|
||
width: min(1200px, 96vw);
|
||
background: var(--ggs-bg);
|
||
border-radius: var(--ggs-radius-md);
|
||
box-shadow: 0 20px 80px rgba(0,0,0,.5);
|
||
padding: 20px;
|
||
box-sizing: border-box;
|
||
display: block;
|
||
}
|
||
.gl-lightbox-frame img {
|
||
display: block;
|
||
width: 100%;
|
||
height: auto;
|
||
max-height: calc(100dvh - 140px);
|
||
}
|
||
.gl-lightbox-hint {
|
||
position: fixed; bottom: 16px; left: 50%; transform: translateX(-50%);
|
||
color: var(--ggs-white); font-size: .8rem; opacity: .75;
|
||
background: rgba(0,0,0,.4); padding: 6px 14px; border-radius: var(--ggs-radius-pill);
|
||
pointer-events: none;
|
||
}
|
||
@keyframes gl-fade-in { from { opacity: 0 } to { opacity: 1 } }
|
||
.gl-detail-text { font-size: .95rem; line-height: 1.7; color: var(--ggs-text); margin-bottom: 1rem; }
|
||
.gl-detail-text p { margin-bottom: .9rem; }
|
||
.gl-detail-text p:last-child { margin-bottom: 0; }
|
||
.gl-inline-link {
|
||
color: var(--ggs-fjord); text-decoration: none; font-weight: 600;
|
||
border-bottom: 1px dashed var(--ggs-fjord);
|
||
}
|
||
@media (hover: hover) {
|
||
.gl-inline-link:hover { color: var(--ggs-fjord-dark); border-bottom-style: solid; background: var(--ggs-fjord-light); }
|
||
}
|
||
.gl-inline-ext { color: var(--ggs-fjord); text-decoration: underline; }
|
||
.gl-ref a {
|
||
font-size: .72em; font-weight: 700; color: var(--ggs-coral);
|
||
text-decoration: none; padding: 0 2px;
|
||
}
|
||
.gl-ref a:hover { color: var(--ggs-fjord-dark); }
|
||
.gl-sources {
|
||
padding-left: 1.5em; margin-bottom: 1rem;
|
||
font-size: .82rem; line-height: 1.6; color: var(--ggs-text);
|
||
}
|
||
.gl-sources li { margin-bottom: .3rem; }
|
||
.gl-sources li::marker { color: var(--ggs-coral); font-weight: 700; }
|
||
.gl-sources a { color: var(--ggs-fjord-dark); text-decoration: none; border-bottom: 1px dotted var(--ggs-fjord); }
|
||
.gl-sources a:hover { color: var(--ggs-fjord); border-bottom-style: solid; }
|
||
.gl-source-date { font-size: .85em; color: var(--ggs-text-muted); }
|
||
.gl-source-hi { background: var(--ggs-sand); transition: background .4s; border-radius: var(--ggs-radius-sm); padding: 2px 4px; }
|
||
|
||
/* ===== Interaktive Experimente ===== */
|
||
.gl-experiment {
|
||
margin: 1.4rem 0;
|
||
padding: 1rem 1.2rem;
|
||
background: linear-gradient(180deg, var(--ggs-fjord-light), var(--ggs-white));
|
||
border: 1px solid var(--ggs-border);
|
||
border-radius: var(--ggs-radius-md);
|
||
}
|
||
.ge-wrap { display: flex; flex-direction: column; gap: .8rem; }
|
||
.ge-head h3 { font-size: 1rem; font-weight: 800; color: var(--ggs-fjord-dark); margin-bottom: .2rem; }
|
||
.ge-head p { font-size: .85rem; color: var(--ggs-text); line-height: 1.5; }
|
||
.ge-track, .ge-albedo-svg { width: 100%; height: auto; background: var(--ggs-white); border-radius: var(--ggs-radius-sm); padding: .4rem; }
|
||
.ge-controls { display: flex; gap: .5rem; flex-wrap: wrap; }
|
||
.ge-btn {
|
||
display: inline-flex; align-items: center; gap: .3rem;
|
||
min-height: 40px; padding: .4rem 1rem;
|
||
border: 1.5px solid var(--ggs-border);
|
||
border-radius: var(--ggs-radius-sm);
|
||
background: var(--ggs-white); color: var(--ggs-text);
|
||
font-family: inherit; font-size: .82rem; font-weight: 600;
|
||
cursor: pointer; text-align: left;
|
||
}
|
||
.ge-btn-primary { background: var(--ggs-fjord); color: var(--ggs-white); border-color: var(--ggs-fjord); }
|
||
@media (hover: hover) {
|
||
.ge-btn:not(.ge-btn-primary):hover { border-color: var(--ggs-fjord); color: var(--ggs-fjord); }
|
||
.ge-btn-primary:hover { background: var(--ggs-fjord-dark); }
|
||
}
|
||
.ge-btn:active { transform: scale(.97); }
|
||
.ge-legend { list-style: none; padding: 0; font-size: .82rem; line-height: 1.6; }
|
||
.ge-legend li { display: flex; align-items: center; gap: .5rem; margin-bottom: .2rem; }
|
||
.ge-dot { width: 12px; height: 12px; border-radius: 50%; flex-shrink: 0; }
|
||
.ge-note { font-size: .75rem; color: var(--ggs-text-muted); line-height: 1.5; font-style: italic; }
|
||
|
||
/* ppm-Raster */
|
||
.ge-ppm-grid {
|
||
display: grid;
|
||
gap: 0;
|
||
background: var(--ggs-bg);
|
||
padding: 4px;
|
||
border-radius: var(--ggs-radius-sm);
|
||
border: 1px solid var(--ggs-border);
|
||
cursor: crosshair;
|
||
}
|
||
.ge-ppm-grid > span {
|
||
aspect-ratio: 1;
|
||
background: var(--ggs-fjord-light);
|
||
}
|
||
.ge-ppm-grid > span.ge-ppm-special {
|
||
background: var(--ggs-coral);
|
||
border-radius: 50%;
|
||
position: relative; z-index: 1;
|
||
}
|
||
.ge-ppm-grid > span.ge-ppm-flash { animation: ge-flash 1.8s ease-out; }
|
||
@keyframes ge-flash {
|
||
0% { box-shadow: 0 0 0 0 rgba(200,92,74,.95); }
|
||
60% { box-shadow: 0 0 0 24px rgba(200,92,74,0); transform: scale(3); }
|
||
100% { transform: scale(1); }
|
||
}
|
||
.ge-ppm-grid > span.ge-ppm-hit {
|
||
position: relative; z-index: 10;
|
||
animation: ge-ppm-bounce 1.1s cubic-bezier(.22, 1, .36, 1) both;
|
||
}
|
||
@keyframes ge-ppm-bounce {
|
||
0% { transform: scale(1) translateY(0); }
|
||
25% { transform: scale(5) translateY(-10px); }
|
||
45% { transform: scale(4) translateY(0); }
|
||
60% { transform: scale(4.5) translateY(-6px); }
|
||
80% { transform: scale(4) translateY(0); }
|
||
100% { transform: scale(4) translateY(-2px); }
|
||
}
|
||
.ge-ppm-msg {
|
||
font-size: .9rem; font-weight: 700; color: var(--ggs-moss-dark);
|
||
padding: .4rem .6rem; min-height: 22px;
|
||
opacity: 0; transition: opacity .25s var(--ggs-ease);
|
||
}
|
||
.ge-ppm-msg.show { opacity: 1; }
|
||
.ge-ppm-msg.miss { color: var(--ggs-text-muted); font-weight: 600; }
|
||
|
||
/* Albedo */
|
||
.ge-albedo-choice { display: flex; gap: .4rem; flex-wrap: wrap; }
|
||
.ge-albedo-stats {
|
||
display: flex; gap: 1.2rem; flex-wrap: wrap;
|
||
font-size: .88rem;
|
||
background: var(--ggs-bg); padding: .6rem .9rem;
|
||
border-radius: var(--ggs-radius-sm);
|
||
}
|
||
.ge-albedo-stats strong { color: var(--ggs-fjord-dark); }
|
||
|
||
/* Fußabdruck-Rechner */
|
||
.ge-calc { display: flex; flex-direction: column; gap: .7rem; }
|
||
.ge-calc-row { display: flex; flex-direction: column; gap: .3rem; }
|
||
.ge-calc-q { font-size: .85rem; font-weight: 700; color: var(--ggs-fjord-dark); }
|
||
.ge-calc-opts { display: flex; gap: .35rem; flex-wrap: wrap; }
|
||
.ge-calc-opts .ge-btn { flex: 1 1 30%; font-size: .78rem; min-height: 44px; padding: .4rem .6rem; justify-content: center; text-align: center; }
|
||
.ge-calc-result {
|
||
background: var(--ggs-white);
|
||
border: 1.5px solid var(--ggs-fjord);
|
||
border-radius: var(--ggs-radius-sm);
|
||
padding: .8rem 1rem;
|
||
text-align: center;
|
||
}
|
||
.ge-calc-result > div:first-child { font-size: .82rem; color: var(--ggs-text-muted); }
|
||
.ge-calc-total { font-size: 1.4rem; font-weight: 900; color: var(--ggs-fjord-dark); display: block; margin: .2rem 0 .5rem; }
|
||
.ge-calc-bar {
|
||
height: 10px; background: var(--ggs-bg); border-radius: var(--ggs-radius-pill);
|
||
overflow: hidden; margin-bottom: .3rem;
|
||
}
|
||
.ge-calc-fill {
|
||
height: 100%; background: linear-gradient(90deg, var(--ggs-moss), var(--ggs-orange), var(--ggs-coral));
|
||
transition: width .4s var(--ggs-ease);
|
||
}
|
||
.ge-calc-scale { display: flex; justify-content: space-between; font-size: .65rem; color: var(--ggs-text-muted); margin-bottom: .3rem; }
|
||
.ge-calc-label { font-size: .75rem; color: var(--ggs-text); }
|
||
|
||
/* Slider-basierte Experimente (Treibhauseffekt, Windrad, Methan-Waage) */
|
||
.ge-gh-svg, .ge-wind-svg, .ge-bal-svg {
|
||
width: 100%; height: auto;
|
||
background: linear-gradient(180deg, #dae8ec 0%, #eff5f6 60%, var(--ggs-white) 100%);
|
||
border-radius: var(--ggs-radius-sm);
|
||
}
|
||
.ge-bal-svg { background: var(--ggs-white); }
|
||
.ge-gh-slider {
|
||
display: flex; flex-direction: column; gap: .3rem;
|
||
background: var(--ggs-white);
|
||
padding: .6rem .9rem;
|
||
border-radius: var(--ggs-radius-sm);
|
||
border: 1px solid var(--ggs-border);
|
||
}
|
||
.ge-gh-slider label {
|
||
font-size: .85rem; color: var(--ggs-text);
|
||
}
|
||
.ge-gh-slider label strong { color: var(--ggs-fjord-dark); font-size: 1rem; }
|
||
.ge-gh-slider input[type="range"] {
|
||
width: 100%; accent-color: var(--ggs-fjord); height: 28px;
|
||
}
|
||
.ge-gh-markers {
|
||
display: flex; justify-content: space-between;
|
||
font-size: .65rem; color: var(--ggs-text-muted); text-align: center;
|
||
line-height: 1.1;
|
||
}
|
||
.ge-gh-result { display: flex; gap: .5rem; flex-wrap: wrap; }
|
||
.ge-gh-pill {
|
||
background: var(--ggs-white);
|
||
border: 1.5px solid var(--ggs-border);
|
||
border-radius: var(--ggs-radius-pill);
|
||
padding: .4rem .9rem;
|
||
font-size: .82rem;
|
||
font-weight: 600;
|
||
color: var(--ggs-text);
|
||
}
|
||
.ge-gh-pill strong { color: var(--ggs-fjord-dark); }
|
||
.ge-gh-pill[data-state="good"] { border-color: var(--ggs-moss); color: var(--ggs-moss-dark); background: var(--ggs-moss-light); }
|
||
.ge-gh-pill[data-state="now"] { border-color: var(--ggs-fjord); color: var(--ggs-fjord-dark); background: var(--ggs-fjord-light); }
|
||
.ge-gh-pill[data-state="warn"] { border-color: var(--ggs-orange); color: var(--ggs-orange); background: #fbe8d3; }
|
||
.ge-gh-pill[data-state="bad"] { border-color: var(--ggs-coral); color: var(--ggs-coral); background: var(--ggs-coral-light); }
|
||
|
||
.ge-wind-obj { opacity: .45; transition: opacity .25s var(--ggs-ease), filter .25s var(--ggs-ease); }
|
||
.ge-wind-obj.ge-wind-active { opacity: 1; filter: drop-shadow(0 2px 6px rgba(74,124,138,.35)); }
|
||
.gl-detail-unit {
|
||
display: inline-block; font-size: .78rem; font-weight: 700; font-family: var(--ggs-font-mono);
|
||
color: var(--ggs-fjord-dark); background: var(--ggs-fjord-light);
|
||
padding: 4px 10px; border-radius: var(--ggs-radius-sm); margin-bottom: 1rem;
|
||
}
|
||
.gl-section-title {
|
||
font-size: .72rem; font-weight: 800; text-transform: uppercase; letter-spacing: .08em;
|
||
color: var(--ggs-text-muted); margin: 1.3rem 0 .6rem;
|
||
}
|
||
.gl-examples { list-style: none; padding: 0; }
|
||
.gl-examples li {
|
||
background: var(--ggs-bg); border-left: 3px solid var(--ggs-moss);
|
||
padding: .55rem .9rem; border-radius: 0 var(--ggs-radius-sm) var(--ggs-radius-sm) 0;
|
||
margin-bottom: .35rem; font-size: .88rem;
|
||
}
|
||
.gl-links, .gl-related { display: flex; flex-wrap: wrap; gap: .5rem; }
|
||
.gl-link, .gl-related a {
|
||
display: inline-flex; align-items: center; gap: .3rem;
|
||
background: var(--ggs-white); border: 1.5px solid var(--ggs-border);
|
||
border-radius: var(--ggs-radius-sm); padding: .4rem .8rem;
|
||
font-size: .8rem; color: var(--ggs-fjord-dark); text-decoration: none; font-weight: 600;
|
||
}
|
||
.gl-link:hover, .gl-related a:hover { border-color: var(--ggs-fjord); background: var(--ggs-fjord-light); color: var(--ggs-fjord); }
|
||
.gl-modules-row { display: flex; flex-wrap: wrap; gap: .4rem; margin-top: .2rem; margin-bottom: .6rem; }
|
||
.gl-modules-row .gl-chip { cursor: default; font-size: .7rem; padding: .25rem .6rem; }
|
||
|
||
/* ===== Footer ===== */
|
||
.gl-footer {
|
||
text-align: center; padding: 2rem 1rem 2.5rem;
|
||
font-size: .72rem; color: var(--ggs-text-muted);
|
||
border-top: 1px solid var(--ggs-border);
|
||
background: var(--ggs-white);
|
||
}
|
||
.gl-footer img { height: 36px; opacity: .5; margin-bottom: .5rem; }
|
||
.gl-footer a { color: var(--ggs-fjord); text-decoration: none; }
|
||
|
||
/* Responsive */
|
||
@media (max-width: 640px) {
|
||
.gl-grid { grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); gap: .6rem; }
|
||
.gl-hero { padding: 2rem 1rem 2.5rem; }
|
||
.gl-hero-stats { gap: 1.2rem; }
|
||
.gl-modal-card { padding: 1.3rem 1.2rem 1.5rem; }
|
||
.gl-detail-title { font-size: 1.3rem; }
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
|
||
<?php
|
||
/* Standard-Hauptnav + Kontext-Sub-Nav (N.5.1 — Glossar zeigt keinen Country-Picker,
|
||
weil das Glossar selbst country-agnostisch ist; nur Lehrplan braucht den Picker) */
|
||
$activeMain = null; /* Glossar ist nicht in der Hauptnav als Tab */
|
||
$activeSub = 'glossar';
|
||
$showCountryPicker = false;
|
||
include __DIR__ . '/_partials/public_topnav.php';
|
||
include __DIR__ . '/_partials/erkundungs_subnav.php';
|
||
?>
|
||
|
||
<!-- Mindmap-Quick-Link bleibt erhalten, jetzt als kleiner Strip rechts oben -->
|
||
<div class="gl-quick-strip">
|
||
<a href="<?= $bp ?>/pages/mindmap.php" class="gl-btn-sm">🧠 Mindmap der Begriffe</a>
|
||
</div>
|
||
|
||
<section class="gl-hero">
|
||
<h1>Das GeoGraSim-Glossar</h1>
|
||
<p>Fachbegriffe aus Klima, Energie, Raumplanung und Geografie — kinderverständlich erklärt, mit konkreten Zahlen und Vergleichen, die Größenordnungen fassbar machen.</p>
|
||
<div class="gl-hero-stats">
|
||
<div class="gl-hero-stat"><span class="gl-hero-stat-num" id="gl-stat-total">–</span><span class="gl-hero-stat-lbl">Begriffe</span></div>
|
||
<div class="gl-hero-stat"><span class="gl-hero-stat-num" id="gl-stat-images">–</span><span class="gl-hero-stat-lbl">Grafiken</span></div>
|
||
<div class="gl-hero-stat"><span class="gl-hero-stat-num">4</span><span class="gl-hero-stat-lbl">Simulationen</span></div>
|
||
</div>
|
||
</section>
|
||
|
||
<div class="gl-controls">
|
||
<div class="gl-search-wrap">
|
||
<span class="gl-search-icon">🔍</span>
|
||
<input id="gl-search" class="gl-search-input" type="search" autocomplete="off"
|
||
placeholder="Begriff suchen — z.B. Treibhaus, ppm, Photovoltaik …">
|
||
<button id="gl-search-clear" class="gl-search-clear" aria-label="Suche löschen">×</button>
|
||
</div>
|
||
<div id="gl-filters" class="gl-filters"></div>
|
||
</div>
|
||
|
||
<div class="gl-grid-wrap">
|
||
<div id="gl-grid-info" class="gl-grid-info"></div>
|
||
<nav id="gl-letter-nav" class="gl-letter-nav" aria-label="Sprung zu Buchstabe"></nav>
|
||
<div id="gl-grid" class="gl-grid"></div>
|
||
<div id="gl-loading" class="gl-loading">Glossar wird geladen …</div>
|
||
</div>
|
||
|
||
<div id="gl-modal" class="gl-modal" role="dialog" aria-modal="true" aria-labelledby="gl-detail-title">
|
||
<div class="gl-modal-card">
|
||
<button id="gl-modal-close" class="gl-modal-close" aria-label="Schließen">×</button>
|
||
<div id="gl-modal-content">
|
||
<div class="gl-loading">Lädt …</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="gl-lightbox" class="gl-lightbox" role="dialog" aria-modal="true" aria-label="Grafik Vollbildansicht">
|
||
<div class="gl-lightbox-frame">
|
||
<img id="gl-lightbox-img" src="" alt="">
|
||
</div>
|
||
<div class="gl-lightbox-hint">Tippen oder Esc drücken, um zu schließen</div>
|
||
</div>
|
||
|
||
<script src="<?= $bp ?>/assets/js/glossar-experiments.js?v=<?= @filemtime(__DIR__ . '/../assets/js/glossar-experiments.js') ?: time() ?>"></script>
|
||
|
||
<footer class="gl-footer">
|
||
<img src="<?= $bp ?>/assets/img/bildLogo.png" alt="">
|
||
<div>GeoGraSim · <a href="<?= $bp ?>/ueber.html">Über</a> · <a href="<?= $bp ?>/handbuch.html">Handbuch</a> · <a href="<?= $bp ?>/impressum.html">Impressum</a> · <a href="<?= $bp ?>/datenschutz.html">Datenschutz</a></div>
|
||
</footer>
|
||
|
||
<script>
|
||
const API = <?= json_encode($api) ?>;
|
||
const BP = <?= json_encode($bp) ?>;
|
||
const INIT_TERM = <?= json_encode($term) ?>;
|
||
const INIT_MODULE = <?= json_encode($module) ?>;
|
||
|
||
// Aus DB-Tabelle module_info (Admin-pflegbar, Source-of-Truth)
|
||
const MODULE_META = <?= json_encode($moduleMeta, JSON_UNESCAPED_UNICODE) ?>;
|
||
const MODULE_ORDER = <?= json_encode($moduleOrder, JSON_UNESCAPED_UNICODE) ?>;
|
||
const BUCH_LINK = <?= json_encode($buchLink, JSON_UNESCAPED_UNICODE) ?>; // Modul → Schulbuch-Kapitel
|
||
const CATEGORY_LABEL = {
|
||
klima: 'Klima',
|
||
einheit: 'Einheit',
|
||
energie: 'Energie',
|
||
stadt: 'Stadt',
|
||
wasser: 'Wasser',
|
||
geologie: 'Geologie',
|
||
wirtschaft: 'Wirtschaft',
|
||
biologie: 'Biologie',
|
||
astronomie: 'Astronomie',
|
||
gesellschaft: 'Gesellschaft'
|
||
};
|
||
|
||
let STATE = {
|
||
all: [],
|
||
filter: { module: '', category: '', search: '' },
|
||
easy: localStorage.getItem('ggs-glossar-easy') === '1',
|
||
currentKey: null
|
||
};
|
||
|
||
function esc(s) {
|
||
return String(s ?? '').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
||
}
|
||
function fmtImg(path) {
|
||
if (!path) return null;
|
||
if (/^https?:/.test(path)) return path;
|
||
return BP + '/' + path.replace(/^\//, '');
|
||
}
|
||
|
||
// Thumbnail-Pfad ableiten: assets/img/glossar/<slug>.png
|
||
// → assets/img/glossar/thumbs/<slug>.webp
|
||
// SVGs bleiben unverändert (sind sowieso klein). Wenn der Thumb fehlt,
|
||
// fällt das <img> per onerror auf das Original-Bild zurück.
|
||
function fmtThumb(path) {
|
||
if (!path) return null;
|
||
if (/^https?:/.test(path)) return path;
|
||
if (/\.svg$/i.test(path)) return BP + '/' + path.replace(/^\//, '');
|
||
const m = path.match(/^(.*\/)([^\/]+)\.(png|jpe?g|webp)$/i);
|
||
if (!m) return BP + '/' + path.replace(/^\//, '');
|
||
return BP + '/' + m[1].replace(/^\//, '') + 'thumbs/' + m[2] + '.webp';
|
||
}
|
||
|
||
/**
|
||
* Fachartikel-Rendering: nimmt Plain-Text mit Markdown-artigen Markups und
|
||
* baut daraus sicheres HTML.
|
||
* [text](glossar:slug) → klickbarer Glossar-Link (öffnet Detail-Modal)
|
||
* [^n] → Hochgestellte Quellen-Referenz
|
||
* **fett** → <strong>
|
||
* *kursiv* → <em>
|
||
* Absätze durch Leerzeilen.
|
||
*/
|
||
function renderArticle(text) {
|
||
if (!text) return '';
|
||
// Zuerst HTML-escapen, dann Markups einsetzen (deren Syntax benutzt ASCII-Zeichen).
|
||
let h = esc(text);
|
||
// Glossar-Links
|
||
h = h.replace(/\[([^\]]+)\]\(glossar:([a-z0-9_-]+)\)/g,
|
||
(_, label, slug) => `<a class="gl-inline-link" href="?term=${encodeURIComponent(slug)}" data-key="${esc(slug)}">${label}</a>`);
|
||
// Externe Links (normale Markdown)
|
||
h = h.replace(/\[([^\]]+)\]\((https?:\/\/[^\)]+)\)/g,
|
||
(_, label, url) => `<a class="gl-inline-ext" href="${esc(url)}" target="_blank" rel="noopener">${label}</a>`);
|
||
// Quellen-Refs [^n] → hochgestellt
|
||
h = h.replace(/\[\^(\d+)\]/g,
|
||
(_, n) => `<sup class="gl-ref"><a href="#gl-source-${n}" data-ref="${n}">${n}</a></sup>`);
|
||
// Einfache Textauszeichnung
|
||
h = h.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
|
||
h = h.replace(/(?<!\*)\*([^*\n]+)\*(?!\*)/g, '<em>$1</em>');
|
||
// Absätze
|
||
const paras = h.split(/\n\s*\n/).map(p => p.trim()).filter(Boolean);
|
||
return paras.map(p => '<p>' + p.replace(/\n/g, '<br>') + '</p>').join('');
|
||
}
|
||
|
||
/** Datum yyyy-mm-dd → Abgerufen am D.M.YYYY */
|
||
function fmtDate(d) {
|
||
if (!d) return '';
|
||
const m = /^(\d{4})-(\d{2})-(\d{2})/.exec(d);
|
||
if (!m) return d;
|
||
return `abgerufen ${+m[3]}.${+m[2]}.${m[1]}`;
|
||
}
|
||
|
||
/** Inline-Varianten-Rendering für Beispiele (ein Satz pro Item, keine Absätze). */
|
||
function renderInline(text) {
|
||
if (!text) return '';
|
||
let h = esc(text);
|
||
h = h.replace(/\[([^\]]+)\]\(glossar:([a-z0-9_-]+)\)/g,
|
||
(_, label, slug) => `<a class="gl-inline-link" href="?term=${encodeURIComponent(slug)}" data-key="${esc(slug)}">${label}</a>`);
|
||
h = h.replace(/\[([^\]]+)\]\((https?:\/\/[^\)]+)\)/g,
|
||
(_, label, url) => `<a class="gl-inline-ext" href="${esc(url)}" target="_blank" rel="noopener">${label}</a>`);
|
||
h = h.replace(/\[\^(\d+)\]/g,
|
||
(_, n) => `<sup class="gl-ref"><a href="#gl-source-${n}" data-ref="${n}">${n}</a></sup>`);
|
||
h = h.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
|
||
return h;
|
||
}
|
||
function categoryLabel(key) {
|
||
if (!key) return '';
|
||
return CATEGORY_LABEL[key] || (key.charAt(0).toUpperCase() + key.slice(1));
|
||
}
|
||
|
||
// ============ LOAD ============
|
||
async function loadAll() {
|
||
const loading = document.getElementById('gl-loading');
|
||
try {
|
||
const res = await fetch(API);
|
||
if (!res.ok) throw new Error('HTTP ' + res.status);
|
||
const list = await res.json();
|
||
STATE.all = Array.isArray(list) ? list : [];
|
||
// Statistik
|
||
document.getElementById('gl-stat-total').textContent = STATE.all.length;
|
||
document.getElementById('gl-stat-images').textContent = STATE.all.filter(e => e.image_path).length;
|
||
loading.style.display = 'none';
|
||
renderFilters();
|
||
render();
|
||
// Tiefen-Link ausführen
|
||
if (INIT_TERM) openDetail(INIT_TERM, false);
|
||
} catch (err) {
|
||
loading.innerHTML = '<strong style="color:var(--ggs-coral)">Glossar konnte nicht geladen werden.</strong><br>' + esc(err.message);
|
||
}
|
||
}
|
||
|
||
// ============ FILTERS ============
|
||
function renderFilters() {
|
||
const wrap = document.getElementById('gl-filters');
|
||
const modules = {};
|
||
const cats = {};
|
||
STATE.all.forEach(e => {
|
||
(e.modules || []).forEach(m => { modules[m] = (modules[m] || 0) + 1; });
|
||
if (e.category) cats[e.category] = (cats[e.category] || 0) + 1;
|
||
});
|
||
|
||
let html = `<button class="gl-chip${!STATE.filter.module && !STATE.filter.category ? ' active' : ''}" data-type="all">
|
||
Alle <span class="gl-chip-count">${STATE.all.length}</span>
|
||
</button>`;
|
||
|
||
// Module-Filter (Reihenfolge aus DB sort_order)
|
||
MODULE_ORDER.forEach(m => {
|
||
if (!modules[m]) return;
|
||
const meta = MODULE_META[m] || { icon: '•', label: m };
|
||
html += `<button class="gl-chip${STATE.filter.module === m ? ' active' : ''}" data-type="module" data-value="${m}">
|
||
${meta.icon} ${meta.label} <span class="gl-chip-count">${modules[m]}</span>
|
||
</button>`;
|
||
});
|
||
|
||
// Kategorie-Filter (nur wenn vorhanden)
|
||
Object.keys(cats).sort().forEach(c => {
|
||
html += `<button class="gl-chip${STATE.filter.category === c ? ' active' : ''}" data-type="category" data-value="${c}">
|
||
${esc(categoryLabel(c))} <span class="gl-chip-count">${cats[c]}</span>
|
||
</button>`;
|
||
});
|
||
|
||
// Schulbuch-Link, wenn ein Modul mit Kapitel gefiltert ist
|
||
if (STATE.filter.module && BUCH_LINK[STATE.filter.module]) {
|
||
html += `<a class="gl-chip gl-buch" href="<?= $bp ?>/${BUCH_LINK[STATE.filter.module]}">📖 Zum Schulbuch-Kapitel</a>`;
|
||
}
|
||
|
||
wrap.innerHTML = html;
|
||
|
||
wrap.querySelectorAll('.gl-chip').forEach(btn => {
|
||
btn.addEventListener('click', () => {
|
||
const type = btn.dataset.type, val = btn.dataset.value || '';
|
||
if (type === 'all') {
|
||
STATE.filter.module = '';
|
||
STATE.filter.category = '';
|
||
} else if (type === 'module') {
|
||
STATE.filter.module = STATE.filter.module === val ? '' : val;
|
||
STATE.filter.category = '';
|
||
} else if (type === 'category') {
|
||
STATE.filter.category = STATE.filter.category === val ? '' : val;
|
||
STATE.filter.module = '';
|
||
}
|
||
renderFilters(); render();
|
||
});
|
||
});
|
||
}
|
||
|
||
// ============ RENDER GRID ============
|
||
const ALPHABET = ['A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z','#'];
|
||
|
||
function normFirstLetter(title) {
|
||
const c = String(title || '').trim().charAt(0).toUpperCase();
|
||
if (c === 'Ä') return 'A';
|
||
if (c === 'Ö') return 'O';
|
||
if (c === 'Ü') return 'U';
|
||
if (c === 'ß') return 'S';
|
||
if (!/[A-Z]/.test(c)) return '#';
|
||
return c;
|
||
}
|
||
|
||
function renderCard(e) {
|
||
const thumb = fmtThumb(e.image_path);
|
||
const full = fmtImg(e.image_path);
|
||
// Card lädt das kleine Thumbnail (webp ~10–30 KB). Fehlt der Thumb (z.B.
|
||
// weil das Generierungs-Skript für diesen Eintrag noch nicht gelaufen ist),
|
||
// fällt das <img> per onerror auf das Original zurück — kein Layout-Sprung.
|
||
const imgBlock = thumb
|
||
? `<div class="gl-card-img"><img src="${esc(thumb)}" alt="" loading="lazy" onerror="this.onerror=null;this.src='${esc(full)}'"></div>`
|
||
: `<div class="gl-card-img gl-no-img"><span class="gl-card-img-emoji">${moduleEmoji(e.modules)}</span></div>`;
|
||
const modEmoji = (e.modules || []).map(m => MODULE_META[m] ? MODULE_META[m].icon : '').join('');
|
||
return `<button class="gl-card" data-key="${esc(e.key_slug)}">
|
||
${imgBlock}
|
||
<div class="gl-card-body">
|
||
${e.category ? `<span class="gl-card-category">${esc(categoryLabel(e.category))}</span>` : ''}
|
||
<div class="gl-card-title">${esc(e.title)}</div>
|
||
<div class="gl-card-short">${esc(e.short || '')}</div>
|
||
${modEmoji ? `<div class="gl-card-modules"><span class="gl-card-mod">${modEmoji}</span></div>` : ''}
|
||
</div>
|
||
</button>`;
|
||
}
|
||
|
||
function render() {
|
||
const grid = document.getElementById('gl-grid');
|
||
const info = document.getElementById('gl-grid-info');
|
||
const nav = document.getElementById('gl-letter-nav');
|
||
const s = STATE.filter.search.trim().toLowerCase();
|
||
|
||
const filtered = STATE.all.filter(e => {
|
||
if (STATE.filter.module && !(e.modules || []).includes(STATE.filter.module)) return false;
|
||
if (STATE.filter.category && e.category !== STATE.filter.category) return false;
|
||
if (s) {
|
||
const hay = (e.title + ' ' + (e.short || '') + ' ' + (e.key_slug || '')).toLowerCase();
|
||
if (!hay.includes(s)) return false;
|
||
}
|
||
return true;
|
||
});
|
||
|
||
if (filtered.length === 0) {
|
||
info.innerHTML = '';
|
||
nav.innerHTML = '';
|
||
nav.style.display = 'none';
|
||
grid.innerHTML = `<div class="gl-empty" style="grid-column:1/-1">
|
||
<strong>Kein Begriff gefunden.</strong>
|
||
${s ? 'Versuche einen anderen Suchbegriff oder entferne den Filter.' : 'Ändere den Filter oben, um andere Kategorien zu sehen.'}
|
||
</div>`;
|
||
return;
|
||
}
|
||
|
||
info.innerHTML = `<strong>${filtered.length}</strong> Begriff${filtered.length === 1 ? '' : 'e'}${s ? ' für „' + esc(s) + '"' : ''}`;
|
||
|
||
// Alphabetisch sortieren (Deutsch, Umlaute korrekt)
|
||
filtered.sort((a, b) => a.title.localeCompare(b.title, 'de', { sensitivity: 'base' }));
|
||
|
||
// Nach Anfangsbuchstaben gruppieren
|
||
const groups = new Map();
|
||
filtered.forEach(e => {
|
||
const L = normFirstLetter(e.title);
|
||
if (!groups.has(L)) groups.set(L, []);
|
||
groups.get(L).push(e);
|
||
});
|
||
|
||
// Sprung-Nav: alle Buchstaben, aktive sind Links, fehlende sind graue Spans
|
||
nav.style.display = '';
|
||
let navHtml = '';
|
||
ALPHABET.forEach(L => {
|
||
if (groups.has(L)) {
|
||
navHtml += `<a href="#gl-letter-${L === '#' ? 'hash' : L}" data-letter="${L}">${L}</a>`;
|
||
} else {
|
||
navHtml += `<span>${L}</span>`;
|
||
}
|
||
});
|
||
nav.innerHTML = navHtml;
|
||
|
||
// Grid: Banner + Cards pro Buchstabe
|
||
let html = '';
|
||
groups.forEach((entries, L) => {
|
||
const anchor = L === '#' ? 'hash' : L;
|
||
html += `<div class="gl-letter-banner" id="gl-letter-${anchor}">
|
||
<span>${esc(L)}</span>
|
||
<span class="gl-letter-banner-count">${entries.length} Begriff${entries.length === 1 ? '' : 'e'}</span>
|
||
</div>`;
|
||
entries.forEach(e => { html += renderCard(e); });
|
||
});
|
||
grid.innerHTML = html;
|
||
|
||
grid.querySelectorAll('.gl-card').forEach(card => {
|
||
card.addEventListener('click', () => openDetail(card.dataset.key, true));
|
||
});
|
||
}
|
||
|
||
function moduleEmoji(modules) {
|
||
if (!modules || !modules.length) return '📘';
|
||
return MODULE_META[modules[0]] ? MODULE_META[modules[0]].icon : '📘';
|
||
}
|
||
|
||
// ============ DETAIL MODAL ============
|
||
async function openDetail(key, pushUrl) {
|
||
const modal = document.getElementById('gl-modal');
|
||
const content = document.getElementById('gl-modal-content');
|
||
modal.classList.add('open');
|
||
document.body.style.overflow = 'hidden';
|
||
content.innerHTML = '<div class="gl-loading">Lädt …</div>';
|
||
STATE.currentKey = key;
|
||
|
||
if (pushUrl) {
|
||
try { history.pushState({ term: key }, '', '?term=' + encodeURIComponent(key)); } catch {}
|
||
}
|
||
|
||
try {
|
||
const url = API + '?key=' + encodeURIComponent(key) + (STATE.easy ? '&easy=1' : '');
|
||
const res = await fetch(url);
|
||
if (!res.ok) throw new Error('Eintrag nicht gefunden.');
|
||
const d = await res.json();
|
||
if (d.error) throw new Error(d.error);
|
||
|
||
// Hat dieser Eintrag überhaupt eine Leichte-Sprache-Version?
|
||
// (Wenn kein text_easy, zeigt API automatisch Normaltext — Toggle bleibt sichtbar als Info)
|
||
const hasEasy = STATE.easy ? d._easy === true : true;
|
||
|
||
const img = fmtImg(d.image_path);
|
||
let html = '';
|
||
|
||
// Toggle: Standard / Leichte Sprache
|
||
html += `<div class="gl-easy-toggle" role="tablist" aria-label="Sprach-Niveau">
|
||
<button type="button" data-easy="0" class="${STATE.easy ? '' : 'active'}" role="tab" aria-selected="${!STATE.easy}">📖 Standard</button>
|
||
<button type="button" data-easy="1" class="${STATE.easy ? 'active' : ''}" role="tab" aria-selected="${STATE.easy}">🙂 Leichte Sprache</button>
|
||
</div>`;
|
||
|
||
if (d.category) html += `<span class="gl-detail-category">${esc(categoryLabel(d.category))}</span>`;
|
||
html += `<h1 id="gl-detail-title" class="gl-detail-title">${esc(d.title)}</h1>`;
|
||
if (d.short) html += `<div class="gl-detail-short">${esc(d.short)}</div>`;
|
||
|
||
if (d.modules && d.modules.length) {
|
||
html += '<div class="gl-modules-row">';
|
||
d.modules.forEach(m => {
|
||
const meta = MODULE_META[m] || { icon: '•', label: m };
|
||
html += `<span class="gl-chip">${meta.icon} ${meta.label}</span>`;
|
||
});
|
||
html += '</div>';
|
||
}
|
||
|
||
if (img) {
|
||
html += `<div class="gl-detail-img" data-src="${esc(img)}" data-alt="Vergleichsgrafik ${esc(d.title)}" role="button" tabindex="0" aria-label="Grafik im Vollbild ansehen"><img src="${esc(img)}" alt="Vergleichsgrafik ${esc(d.title)}"></div>`;
|
||
}
|
||
if (d.text) html += `<div class="gl-detail-text">${renderArticle(d.text)}</div>`;
|
||
if (d.unit) html += `<div class="gl-detail-unit">${esc(d.unit)}</div>`;
|
||
|
||
// Interaktives Experiment, falls für diesen key_slug registriert
|
||
if (window.GlossarExperiments && window.GlossarExperiments[d.key_slug]) {
|
||
html += `<div class="gl-section-title">Selbst ausprobieren</div>
|
||
<div class="gl-experiment" id="gl-experiment-root"></div>`;
|
||
}
|
||
|
||
if (d.examples && d.examples.length) {
|
||
html += '<div class="gl-section-title">Zahlen zum Einordnen</div><ul class="gl-examples">';
|
||
d.examples.forEach(ex => {
|
||
const txt = typeof ex === 'string' ? ex : ex.text;
|
||
const ref = typeof ex === 'object' && ex.source_ref ? ` <sup class="gl-ref"><a href="#gl-source-${ex.source_ref}" data-ref="${ex.source_ref}">${ex.source_ref}</a></sup>` : '';
|
||
html += '<li>' + renderInline(txt) + ref + '</li>';
|
||
});
|
||
html += '</ul>';
|
||
}
|
||
if (d.sources && d.sources.length) {
|
||
html += '<div class="gl-section-title">Quellen</div><ol class="gl-sources">';
|
||
d.sources.forEach(s => {
|
||
html += `<li id="gl-source-${s.ref_num}" value="${s.ref_num}">` +
|
||
(s.url ? `<a href="${esc(s.url)}" target="_blank" rel="noopener">${esc(s.label)}</a>` : esc(s.label)) +
|
||
(s.access_date ? ` <span class="gl-source-date">(${fmtDate(s.access_date)})</span>` : '') +
|
||
'</li>';
|
||
});
|
||
html += '</ol>';
|
||
}
|
||
if (d.links && d.links.length) {
|
||
html += '<div class="gl-section-title">Weiterführende Links</div><div class="gl-links">';
|
||
d.links.forEach(l => { html += `<a class="gl-link" href="${esc(l.url)}" target="_blank" rel="noopener">${esc(l.label)} →</a>`; });
|
||
html += '</div>';
|
||
}
|
||
if (d.related && d.related.length) {
|
||
html += '<div class="gl-section-title">Verwandte Begriffe</div><div class="gl-related">';
|
||
d.related.forEach(r => { html += `<a data-key="${esc(r.key_slug)}" href="?term=${encodeURIComponent(r.key_slug)}">${esc(r.title)}</a>`; });
|
||
html += '</div>';
|
||
}
|
||
|
||
content.innerHTML = html;
|
||
document.title = 'Glossar geograsim.at — ' + d.title;
|
||
|
||
// Related + Inline-Links im Modal abfangen (zu anderen Glossar-Begriffen)
|
||
content.querySelectorAll('.gl-related a, .gl-inline-link').forEach(a => {
|
||
a.addEventListener('click', ev => {
|
||
ev.preventDefault();
|
||
openDetail(a.dataset.key, true);
|
||
});
|
||
});
|
||
// Footnote-Klicks: smooth scroll zur Quelle
|
||
content.querySelectorAll('.gl-ref a').forEach(a => {
|
||
a.addEventListener('click', ev => {
|
||
ev.preventDefault();
|
||
const ref = a.dataset.ref;
|
||
const t = content.querySelector('#gl-source-' + ref);
|
||
if (t) t.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||
if (t) { t.classList.add('gl-source-hi'); setTimeout(() => t.classList.remove('gl-source-hi'), 1500); }
|
||
});
|
||
});
|
||
// Experiment instanziieren, falls Container vorhanden
|
||
const expRoot = content.querySelector('#gl-experiment-root');
|
||
if (expRoot && window.GlossarExperiments && window.GlossarExperiments[d.key_slug]) {
|
||
try { window.GlossarExperiments[d.key_slug](expRoot); }
|
||
catch (e) { expRoot.innerHTML = '<div class="gl-empty">Experiment konnte nicht geladen werden.</div>'; }
|
||
}
|
||
|
||
// Easy-Toggle: umschalten und neu laden
|
||
content.querySelectorAll('.gl-easy-toggle button').forEach(btn => {
|
||
btn.addEventListener('click', () => {
|
||
const want = btn.dataset.easy === '1';
|
||
if (STATE.easy === want) return;
|
||
STATE.easy = want;
|
||
localStorage.setItem('ggs-glossar-easy', want ? '1' : '0');
|
||
openDetail(STATE.currentKey, false);
|
||
});
|
||
});
|
||
// Infografik-Klick öffnet Lightbox — robust: nimm data-src ODER inneres <img>
|
||
content.querySelectorAll('.gl-detail-img').forEach(el => {
|
||
const getSrc = () => el.dataset.src || el.querySelector('img')?.getAttribute('src') || '';
|
||
const getAlt = () => el.dataset.alt || el.querySelector('img')?.getAttribute('alt') || '';
|
||
el.addEventListener('click', ev => { ev.stopPropagation(); openLightbox(getSrc(), getAlt()); });
|
||
el.addEventListener('keydown', ev => {
|
||
if (ev.key === 'Enter' || ev.key === ' ') { ev.preventDefault(); openLightbox(getSrc(), getAlt()); }
|
||
});
|
||
});
|
||
} catch (err) {
|
||
content.innerHTML = '<div class="gl-empty"><strong>Eintrag nicht verfügbar</strong>' + esc(err.message) + '</div>';
|
||
}
|
||
}
|
||
|
||
function closeDetail(replaceUrl) {
|
||
const modal = document.getElementById('gl-modal');
|
||
modal.classList.remove('open');
|
||
document.body.style.overflow = '';
|
||
document.title = 'Glossar geograsim.at';
|
||
STATE.currentKey = null;
|
||
if (replaceUrl) {
|
||
try { history.pushState({}, '', location.pathname); } catch {}
|
||
}
|
||
}
|
||
|
||
// ============ LIGHTBOX ============
|
||
function openLightbox(src, alt) {
|
||
if (!src) return;
|
||
const box = document.getElementById('gl-lightbox');
|
||
const img = document.getElementById('gl-lightbox-img');
|
||
img.src = src;
|
||
img.alt = alt || '';
|
||
box.classList.add('open');
|
||
document.body.style.overflow = 'hidden';
|
||
}
|
||
function closeLightbox() {
|
||
const box = document.getElementById('gl-lightbox');
|
||
box.classList.remove('open');
|
||
document.getElementById('gl-lightbox-img').src = '';
|
||
if (!document.getElementById('gl-modal').classList.contains('open')) {
|
||
document.body.style.overflow = '';
|
||
}
|
||
}
|
||
document.getElementById('gl-lightbox').addEventListener('click', closeLightbox);
|
||
|
||
// ============ EVENTS ============
|
||
document.getElementById('gl-modal-close').addEventListener('click', () => closeDetail(true));
|
||
document.getElementById('gl-modal').addEventListener('click', ev => {
|
||
if (ev.target.id === 'gl-modal') closeDetail(true);
|
||
});
|
||
document.addEventListener('keydown', ev => {
|
||
if (ev.key !== 'Escape') return;
|
||
// Priorität: erst Lightbox schließen, dann Modal
|
||
if (document.getElementById('gl-lightbox').classList.contains('open')) {
|
||
closeLightbox();
|
||
} else if (document.getElementById('gl-modal').classList.contains('open')) {
|
||
closeDetail(true);
|
||
}
|
||
});
|
||
|
||
const search = document.getElementById('gl-search');
|
||
const searchClear = document.getElementById('gl-search-clear');
|
||
search.addEventListener('input', () => {
|
||
STATE.filter.search = search.value;
|
||
searchClear.classList.toggle('show', !!search.value);
|
||
render();
|
||
});
|
||
searchClear.addEventListener('click', () => {
|
||
search.value = ''; STATE.filter.search = '';
|
||
searchClear.classList.remove('show');
|
||
search.focus(); render();
|
||
});
|
||
|
||
// Back-Button schließt Detail oder navigiert zurück
|
||
window.addEventListener('popstate', ev => {
|
||
const term = new URLSearchParams(location.search).get('term');
|
||
if (term) openDetail(term, false);
|
||
else closeDetail(false);
|
||
});
|
||
|
||
// Initial: Modul-Filter aus URL übernehmen
|
||
if (INIT_MODULE) STATE.filter.module = INIT_MODULE;
|
||
|
||
loadAll();
|
||
</script>
|
||
</body>
|
||
</html>
|