Files
geograsim/App/pages/simulationen.php
T
Adminator 7deec2a26d Lehrplan: Country-Helper + Landeinstellung live
- App/php/lib/Country.php: Kaskade session → classes → teachers →
  cookie → 'AT', Helper current()/flag()/label()/allCountries()
- App/php/api/country.php: GET + POST /api/country
- country-Spalten in teachers, classes, student_sessions (idempotent)
- Native-<select>-Picker im Header aller öffentlichen Seiten
  (modul-*.php, lehrplan.php, simulationen.php)
- Footer-Links "Andere Länder: 🇨🇭 🇩🇪 🇱🇮"
- lehrplan.php liest Default-Filter jetzt aus Country::current()
- iPad-freundlich: Native Select gibt Wheel-Picker auf iOS,
  Touch-Ziel >=40px

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 00:09:43 +02:00

462 lines
18 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/**
* Simulationen — Einstiegs-Grid
* -----------------------------
* Übersicht aller Module aus `module_info`. Zwei CTAs pro Card:
* "Mehr erfahren" → modul-*.php Detailseite · "Sofort probieren" → play_url.
*
* URL: http://localhost/geograsim/App/simulationen
* ?status=aktiv vorgewählter Status-Filter
* ?theme=klima vorgewähltes Kompetenz-Thema
*/
require_once __DIR__ . '/../php/config/app.php';
require_once __DIR__ . '/../php/config/db.php';
require_once __DIR__ . '/../php/lib/EasyLang.php';
require_once __DIR__ . '/../php/lib/Country.php';
$db = getDB();
// Filter aus URL
$qStatus = preg_replace('/[^a-z]/', '', strtolower($_GET['status'] ?? ''));
$qTheme = preg_replace('/[^a-z0-9_-]/', '', strtolower($_GET['theme'] ?? ''));
// Alle Module laden
$modules = $db->query('
SELECT module_id, title, subtitle, short_desc, short_desc_easy,
duration_min, age_min, age_max, icon, card_image,
page_url, play_url, status, sort_order
FROM module_info
ORDER BY FIELD(status, "aktiv", "beta", "geplant", "archiv"), sort_order, title
')->fetchAll();
// Kompetenz-Zuordnung je Modul (fuer Theme-Chip + Thema-Filter)
$kompMap = [];
$kompQ = $db->query('
SELECT km.module_id, k.key_slug, k.title, k.category, km.strength
FROM kompetenz_modules km
INNER JOIN kompetenzen k ON k.id = km.kompetenz_id
ORDER BY km.strength, k.sort_order
');
foreach ($kompQ as $r) {
$kompMap[$r['module_id']][] = $r;
}
// Alle moeglichen Themen / Kompetenzen einsammeln (Filter-Chips)
$themes = $db->query('SELECT key_slug, title, category FROM kompetenzen ORDER BY sort_order')->fetchAll();
// Zaehlungen pro Status
$counts = ['aktiv' => 0, 'beta' => 0, 'geplant' => 0, 'archiv' => 0];
foreach ($modules as $m) {
$counts[$m['status']] = ($counts[$m['status']] ?? 0) + 1;
}
$bp = BASE_PATH;
$easyActive = EasyLang::isActive();
function esc(?string $s): string { return htmlspecialchars((string)$s, ENT_QUOTES, 'UTF-8'); }
$statusMeta = [
'aktiv' => ['label' => 'Aktiv', 'tone' => 'moss', 'icon' => '✓'],
'beta' => ['label' => 'Beta', 'tone' => 'sand', 'icon' => '⚠'],
'geplant' => ['label' => 'In Entw.', 'tone' => 'fjord', 'icon' => '🔒'],
'archiv' => ['label' => 'Archiv', 'tone' => 'muted', 'icon' => '□'],
];
?><!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Simulationen — Lehrplan geograsim.at</title>
<meta name="description" content="Alle GeoGraSim-Simulationen auf einen Blick — Klimawächter, Flussmanagement, Heli-Navigation, Stadt &amp; Raumplanung und weitere in Entwicklung.">
<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>
body { overflow: auto; height: auto; min-height: 100vh; background: var(--ggs-bg); }
/* Topbar identisch zu lehrplan.php */
.sp-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;
}
.sp-topbar a { text-decoration: none; }
.sp-topbar-brand img { height: 40px; display: block; }
.sp-topbar .spacer { flex: 1; }
.sp-btn-sm {
padding: .5rem .85rem; 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;
display: inline-flex; align-items: center; min-height: 40px;
}
.sp-btn-sm:hover { background: var(--ggs-fjord-light); color: var(--ggs-fjord-dark); border-color: var(--ggs-fjord); }
.sp-btn-sm.sp-btn-primary { background: var(--ggs-fjord); color: var(--ggs-white); border-color: var(--ggs-fjord); }
.sp-easy-pill { background: var(--ggs-moss-light); color: var(--ggs-moss-dark); border-color: var(--ggs-moss); font-weight: 700; }
/* Hero */
.sp-hero {
background: linear-gradient(135deg, var(--ggs-fjord-dark) 0%, var(--ggs-fjord) 100%);
color: var(--ggs-white); padding: 2.5rem 1.5rem 3rem; text-align: center;
}
.sp-hero h1 { font-size: clamp(1.8rem, 4vw, 2.6rem); font-weight: 900; letter-spacing: -.03em; margin-bottom: .5rem; }
.sp-hero p { font-size: 1.02rem; color: rgba(255,255,255,.88); max-width: 640px; margin: 0 auto 1.4rem; }
.sp-hero-stats { display: flex; justify-content: center; gap: 2rem; flex-wrap: wrap; margin-top: 1.2rem; }
.sp-hero-stat { text-align: center; }
.sp-hero-stat-num { font-size: 1.8rem; font-weight: 900; display: block; }
.sp-hero-stat-lbl { font-size: .72rem; opacity: .8; text-transform: uppercase; letter-spacing: .08em; }
/* Filter-Bar */
.sp-filters {
max-width: 1200px; margin: -1.4rem auto 1.5rem;
padding: 1rem 1.2rem; background: var(--ggs-white);
border-radius: var(--ggs-radius-lg); box-shadow: var(--ggs-shadow-lg);
border: 1px solid var(--ggs-border);
position: relative; z-index: 5;
display: flex; flex-direction: column; gap: .9rem;
}
.sp-filter-row { display: flex; align-items: center; flex-wrap: wrap; gap: .5rem; }
.sp-filter-label {
font-size: .72rem; font-weight: 800; text-transform: uppercase;
letter-spacing: .08em; color: var(--ggs-text-muted); min-width: 60px;
}
.sp-chip {
display: inline-flex; align-items: center; gap: .3rem;
padding: .55rem 1rem; border-radius: var(--ggs-radius-pill);
background: var(--ggs-white); border: 1.5px solid var(--ggs-border);
font-size: .88rem; font-weight: 600; color: var(--ggs-text-muted);
cursor: pointer; transition: all .2s var(--ggs-ease); font-family: inherit;
min-height: 40px; min-width: 40px;
}
.sp-chip:hover { border-color: var(--ggs-fjord); color: var(--ggs-fjord); }
.sp-chip:active { transform: scale(0.96); }
.sp-chip.active { background: var(--ggs-fjord); color: var(--ggs-white); border-color: var(--ggs-fjord); }
.sp-chip-badge {
font-size: .7rem; padding: 1px 6px; border-radius: var(--ggs-radius-pill);
background: var(--ggs-bg-dark); color: var(--ggs-text-muted); font-weight: 700;
}
.sp-chip.active .sp-chip-badge { background: rgba(255,255,255,.22); color: var(--ggs-white); }
/* Grid */
.sp-wrap { max-width: 1200px; margin: 0 auto 3rem; padding: 0 1.2rem; }
.sp-results-info {
font-size: .82rem; color: var(--ggs-text-muted);
margin-bottom: 1rem;
}
.sp-grid {
display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 1.4rem;
}
/* Modul-Card */
.sp-card {
background: var(--ggs-white); border: 1px solid var(--ggs-border);
border-radius: var(--ggs-radius-lg); overflow: hidden;
display: flex; flex-direction: column;
transition: transform .25s var(--ggs-ease), box-shadow .25s var(--ggs-ease),
border-color .25s var(--ggs-ease);
box-shadow: var(--ggs-shadow-sm);
}
.sp-card:hover { transform: translateY(-3px); box-shadow: var(--ggs-shadow-md);
border-color: var(--ggs-fjord-light); }
.sp-card.sp-card-geplant { opacity: .88; }
.sp-card-media {
aspect-ratio: 16 / 10; overflow: hidden; position: relative;
background: var(--ggs-fjord-light);
}
.sp-card-media img { width: 100%; height: 100%; object-fit: cover; display: block; }
.sp-card-badge {
position: absolute; top: 10px; right: 10px;
font-size: .7rem; font-weight: 700; padding: 4px 10px;
border-radius: var(--ggs-radius-pill); letter-spacing: .04em;
text-transform: uppercase;
}
.sp-card-badge.moss { background: var(--ggs-moss); color: var(--ggs-white); }
.sp-card-badge.sand { background: var(--ggs-sand-dark); color: var(--ggs-white); }
.sp-card-badge.fjord { background: var(--ggs-fjord); color: var(--ggs-white); opacity: .85; }
.sp-card-badge.muted { background: var(--ggs-text-muted); color: var(--ggs-white); }
.sp-card-body {
padding: 1rem 1.2rem 1.2rem; display: flex; flex-direction: column; gap: .6rem;
flex: 1;
}
.sp-card-title {
font-size: 1.2rem; font-weight: 800; color: var(--ggs-fjord-dark);
letter-spacing: -.02em; display: flex; align-items: center; gap: .4rem;
line-height: 1.2;
}
.sp-card-icon { font-size: 1.4rem; line-height: 1; }
.sp-card-subtitle {
font-size: .88rem; color: var(--ggs-text); line-height: 1.5;
flex: 1;
}
.sp-card-meta {
display: flex; flex-wrap: wrap; gap: .4rem;
margin-top: .3rem;
}
.sp-card-meta-item {
font-size: .72rem; color: var(--ggs-text-muted);
background: var(--ggs-bg); padding: 3px 9px;
border-radius: var(--ggs-radius-pill);
display: inline-flex; align-items: center; gap: .25rem;
}
.sp-card-themes {
display: flex; flex-wrap: wrap; gap: .3rem;
margin-top: .2rem;
}
.sp-card-theme {
font-size: .7rem; color: var(--ggs-fjord-dark);
background: var(--ggs-fjord-light); padding: 2px 8px;
border-radius: var(--ggs-radius-pill); font-weight: 600;
}
.sp-card-cta {
display: flex; gap: .5rem; flex-wrap: wrap; margin-top: .4rem;
}
.sp-btn {
display: inline-flex; align-items: center; justify-content: center;
gap: .35rem; padding: .7rem 1rem;
border-radius: var(--ggs-radius-sm);
font-weight: 700; font-size: .88rem;
text-decoration: none; border: 1.5px solid transparent;
transition: all .2s var(--ggs-ease); min-height: 44px;
flex: 1; text-align: center;
}
.sp-btn-primary { background: var(--ggs-fjord); color: var(--ggs-white); }
.sp-btn-primary:hover { background: var(--ggs-fjord-dark); }
.sp-btn-primary:active { transform: scale(0.98); }
.sp-btn-secondary {
background: var(--ggs-white); color: var(--ggs-fjord);
border-color: var(--ggs-fjord);
}
.sp-btn-secondary:hover { background: var(--ggs-fjord-light); }
.sp-btn-secondary:active { transform: scale(0.98); }
.sp-btn-disabled {
background: var(--ggs-bg); color: var(--ggs-text-muted);
border-color: var(--ggs-border); cursor: not-allowed;
}
.sp-empty {
grid-column: 1 / -1;
text-align: center; padding: 3rem 1rem;
color: var(--ggs-text-muted); font-style: italic;
}
/* Footer */
.sp-footer {
text-align: center; padding: 2rem 1rem 2.5rem;
font-size: .78rem; color: var(--ggs-text-muted);
border-top: 1px solid var(--ggs-border);
background: var(--ggs-white); margin-top: 2rem;
}
.sp-footer img { height: 36px; opacity: .5; margin-bottom: .5rem; }
.sp-footer a { color: var(--ggs-fjord); text-decoration: none; padding: .3rem .2rem; }
@media (max-width: 900px) {
.sp-filter-row { flex-direction: column; align-items: flex-start; gap: .3rem; }
.sp-filter-label { min-width: 0; }
.sp-grid { grid-template-columns: 1fr; }
}
</style>
</head>
<body>
<header class="sp-topbar">
<a href="<?= $bp ?>/index.html" class="sp-topbar-brand" aria-label="GeoGraSim Startseite">
<img src="<?= $bp ?>/assets/img/geograsim_t.svg" alt="GeoGraSim">
</a>
<span class="spacer"></span>
<?php if ($easyActive): ?>
<span class="sp-btn-sm sp-easy-pill">🟢 Leichte Sprache</span>
<?php endif; ?>
<?php include __DIR__ . '/_partials/country_picker.php'; ?>
<a href="<?= $bp ?>/pages/lehrplan.php" class="sp-btn-sm">Lehrplan</a>
<a href="<?= $bp ?>/glossar" class="sp-btn-sm">Glossar</a>
<a href="<?= $bp ?>/login.html" class="sp-btn-sm sp-btn-primary">Anmelden</a>
</header>
<section class="sp-hero">
<h1>Alle Simulationen</h1>
<p>
Zu jedem Modul gibt es eine Detailseite mit Lernzielen, Glossar-Begriffen
und Lehrplan-Bezug für Österreich, Deutschland, Schweiz und Liechtenstein.
Die aktiven Module lassen sich sofort ausprobieren.
</p>
<div class="sp-hero-stats">
<div class="sp-hero-stat"><span class="sp-hero-stat-num"><?= count($modules) ?></span><span class="sp-hero-stat-lbl">Module</span></div>
<div class="sp-hero-stat"><span class="sp-hero-stat-num"><?= $counts['aktiv'] ?></span><span class="sp-hero-stat-lbl">Aktiv</span></div>
<div class="sp-hero-stat"><span class="sp-hero-stat-num"><?= $counts['geplant'] ?></span><span class="sp-hero-stat-lbl">In Entwicklung</span></div>
</div>
</section>
<div class="sp-filters" id="sp-filters">
<div class="sp-filter-row">
<span class="sp-filter-label">Status</span>
<button type="button" class="sp-chip<?= !$qStatus ? ' active' : '' ?>" data-filter="status" data-value="">
Alle <span class="sp-chip-badge"><?= count($modules) ?></span>
</button>
<?php foreach (['aktiv','beta','geplant'] as $s):
if (!($counts[$s] ?? 0)) continue; ?>
<button type="button" class="sp-chip<?= $qStatus === $s ? ' active' : '' ?>"
data-filter="status" data-value="<?= esc($s) ?>">
<?= esc($statusMeta[$s]['label']) ?>
<span class="sp-chip-badge"><?= (int)$counts[$s] ?></span>
</button>
<?php endforeach; ?>
</div>
<div class="sp-filter-row">
<span class="sp-filter-label">Thema</span>
<button type="button" class="sp-chip<?= !$qTheme ? ' active' : '' ?>" data-filter="theme" data-value="">
Alle
</button>
<?php foreach ($themes as $t): ?>
<button type="button" class="sp-chip<?= $qTheme === $t['key_slug'] ? ' active' : '' ?>"
data-filter="theme" data-value="<?= esc($t['key_slug']) ?>">
<?= esc($t['title']) ?>
</button>
<?php endforeach; ?>
</div>
</div>
<div class="sp-wrap">
<div class="sp-results-info" id="sp-results-info"></div>
<div class="sp-grid" id="sp-grid">
<?php foreach ($modules as $m):
$shortText = EasyLang::pick($m['short_desc'] ?? null, $m['short_desc_easy'] ?? null);
$displayText = $shortText ?: ($m['subtitle'] ?? '');
$status = $m['status'];
$statusInfo = $statusMeta[$status] ?? $statusMeta['beta'];
$themesForModule = $kompMap[$m['module_id']] ?? [];
$themeSlugs = array_map(fn($r) => $r['key_slug'], $themesForModule);
$themeAttr = implode(',', $themeSlugs);
$canPlay = in_array($status, ['aktiv','beta'], true);
?>
<article class="sp-card sp-card-<?= esc($status) ?>"
data-status="<?= esc($status) ?>"
data-themes="<?= esc($themeAttr) ?>">
<div class="sp-card-media">
<?php if (!empty($m['card_image'])): ?>
<img src="<?= $bp ?>/assets/img/<?= esc($m['card_image']) ?>" alt="<?= esc($m['title']) ?>">
<?php endif; ?>
<span class="sp-card-badge <?= esc($statusInfo['tone']) ?>">
<?= esc($statusInfo['icon']) ?> <?= esc($statusInfo['label']) ?>
</span>
</div>
<div class="sp-card-body">
<h2 class="sp-card-title">
<span class="sp-card-icon"><?= esc($m['icon']) ?></span>
<?= esc($m['title']) ?>
</h2>
<p class="sp-card-subtitle"><?= esc($displayText) ?></p>
<div class="sp-card-meta">
<?php if (!empty($m['duration_min'])): ?>
<span class="sp-card-meta-item">⏱ ca. <?= (int)$m['duration_min'] ?> Min</span>
<?php endif; ?>
<?php if (!empty($m['age_min']) && !empty($m['age_max'])): ?>
<span class="sp-card-meta-item">👧 <?= (int)$m['age_min'] ?><?= (int)$m['age_max'] ?> J</span>
<?php endif; ?>
</div>
<?php if (!empty($themesForModule)): ?>
<div class="sp-card-themes">
<?php foreach ($themesForModule as $t): ?>
<span class="sp-card-theme" title="Kompetenzbereich"><?= esc($t['title']) ?></span>
<?php endforeach; ?>
</div>
<?php endif; ?>
<div class="sp-card-cta">
<?php if (!empty($m['page_url'])): ?>
<a class="sp-btn sp-btn-secondary" href="<?= $bp ?>/<?= esc($m['page_url']) ?>">
📖 Mehr erfahren
</a>
<?php endif; ?>
<?php if ($canPlay && !empty($m['play_url'])): ?>
<a class="sp-btn sp-btn-primary" href="<?= $bp ?>/<?= esc($m['play_url']) ?>">
▶ Sofort probieren
</a>
<?php else: ?>
<span class="sp-btn sp-btn-disabled" aria-disabled="true">
🔒 In Entwicklung
</span>
<?php endif; ?>
</div>
</div>
</article>
<?php endforeach; ?>
</div>
</div>
<footer class="sp-footer">
<img src="<?= $bp ?>/assets/img/bildLogo.png" alt="">
<div>
Lehrplan geograsim.at ·
<a href="<?= $bp ?>/pages/lehrplan.php">Lehrplan</a> ·
<a href="<?= $bp ?>/glossar">Glossar</a> ·
<a href="<?= $bp ?>/ueber.html">Über</a> ·
<a href="<?= $bp ?>/impressum.html">Impressum</a> ·
<a href="<?= $bp ?>/datenschutz.html">Datenschutz</a>
</div>
<?php include __DIR__ . '/_partials/country_footer.php'; ?>
</footer>
<script>
(function () {
const bar = document.getElementById('sp-filters');
const info = document.getElementById('sp-results-info');
const cards = document.querySelectorAll('.sp-card');
const total = cards.length;
const state = {
status: <?= json_encode($qStatus) ?>,
theme: <?= json_encode($qTheme) ?>,
};
bar.addEventListener('click', (e) => {
const btn = e.target.closest('.sp-chip');
if (!btn) return;
const f = btn.dataset.filter;
const v = btn.dataset.value;
state[f] = v;
bar.querySelectorAll(`.sp-chip[data-filter="${f}"]`).forEach(c => {
c.classList.toggle('active', c.dataset.value === v);
});
apply();
const params = new URLSearchParams();
if (state.status) params.set('status', state.status);
if (state.theme) params.set('theme', state.theme);
const q = params.toString();
history.replaceState(null, '', q ? '?' + q : location.pathname);
});
function apply() {
let shown = 0;
cards.forEach(c => {
let hide = false;
if (state.status && c.dataset.status !== state.status) hide = true;
if (state.theme) {
const ts = (c.dataset.themes || '').split(',').filter(Boolean);
if (!ts.includes(state.theme)) hide = true;
}
c.style.display = hide ? 'none' : '';
if (!hide) shown++;
});
if (shown === total && !state.status && !state.theme) {
info.textContent = '';
} else {
info.textContent = `${shown} von ${total} Modulen sichtbar`;
}
}
apply();
})();
</script>
</body>
</html>