CMS 2.0 - Theme engine, logging, admin improvements
Major changes: - New ThemeManager with Twig templating and SCSS compilation - Dynamic themes system (themes/default, themes/demo) - LogManager with SQLite storage and syslog forwarding - RequestLogger with static helper methods - Admin UI overhaul (Bootstrap 5, dark mode) - Admin config page with logging and theme settings - Admin logs page with filters and search - Removed legacy Mustache templates - Removed test plugin and theme - Composer dependencies: Twig, scssphp, CommonMark, MaxMind GeoIP
This commit is contained in:
@@ -171,6 +171,11 @@ $layoutSidebarColor = $layoutThemeConfig['header_color'] ?? '#0a369d';
|
||||
case 'theme':
|
||||
require __DIR__ . '/pages/theme.php';
|
||||
break;
|
||||
|
||||
case 'theme-new':
|
||||
require __DIR__ . '/pages/theme-new.php';
|
||||
break;
|
||||
|
||||
case 'plugins':
|
||||
require __DIR__ . '/pages/plugins.php';
|
||||
break;
|
||||
|
||||
@@ -95,9 +95,9 @@
|
||||
value="<?= htmlspecialchars($configData['author']['name'] ?? '') ?>">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label for="author_website" class="form-label">Website</label>
|
||||
<input type="url" class="form-control" id="author_website" name="author_website"
|
||||
value="<?= htmlspecialchars($configData['author']['website'] ?? '') ?>">
|
||||
<label for="author_website" class="form-label">Website URL</label>
|
||||
<input type="text" class="form-control" id="author_website" name="author_website"
|
||||
value="<?= htmlspecialchars($configData['author']['website'] ?? '') ?>" placeholder="bijv. noorlander.info">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -140,6 +140,84 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card shadow-sm mb-4">
|
||||
<div class="card-header">
|
||||
<i class="bi bi-journal-text"></i> Logging
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<?php $logging = $configData['logging'] ?? []; ?>
|
||||
<div class="form-check mb-3">
|
||||
<input type="checkbox" class="form-check-input" id="logging_enabled" name="logging_enabled" value="1" <?= !empty($logging['enabled']) ? 'checked' : '' ?>>
|
||||
<label class="form-check-label" for="logging_enabled">Logging inschakelen</label>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-6">
|
||||
<label for="logging_driver" class="form-label">Opslag</label>
|
||||
<select class="form-select" id="logging_driver" name="logging_driver">
|
||||
<option value="sqlite" <?= ($logging['driver'] ?? 'sqlite') === 'sqlite' ? 'selected' : '' ?>>SQLite (standaard)</option>
|
||||
<option value="syslog" <?= ($logging['driver'] ?? '') === 'syslog' ? 'selected' : '' ?>>Syslog</option>
|
||||
</select>
|
||||
<div class="form-text">Kies <strong>Syslog</strong> om logregels naar een externe syslog-server te sturen. Logregels worden altijd ook lokaal opgeslagen.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-3 mb-3">
|
||||
<div class="col-md-6">
|
||||
<label for="syslog_host" class="form-label">Syslog server (host)</label>
|
||||
<input type="text" class="form-control font-monospace" id="syslog_host" name="syslog_host"
|
||||
value="<?= htmlspecialchars($logging['syslog_host'] ?? '') ?>" placeholder="bijv. 192.168.1.10">
|
||||
<div class="form-text">Vul de host van de syslog-server in (bijv. <code>192.168.1.10</code> of <code>logs.example.com</code>).</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label for="syslog_port" class="form-label">Poort</label>
|
||||
<input type="number" class="form-control" id="syslog_port" name="syslog_port" min="1" max="65535"
|
||||
value="<?= (int)($logging['syslog_port'] ?? 514) ?>">
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label for="syslog_facility" class="form-label">Facility</label>
|
||||
<select class="form-select" id="syslog_facility" name="syslog_facility">
|
||||
<?php foreach (['local0', 'local1', 'local2', 'local3', 'local4', 'local5', 'local6', 'local7', 'daemon', 'user', 'auth'] as $fac): ?>
|
||||
<option value="<?= $fac ?>" <?= ($logging['syslog_facility'] ?? 'local0') === $fac ? 'selected' : '' ?>><?= $fac ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<div class="form-text">Categorie van de logbron (bijv. <code>local0</code>–<code>local7</code> voor eigen applicaties).</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="syslog_ident" class="form-label">Syslog ident</label>
|
||||
<input type="text" class="form-control font-monospace" id="syslog_ident" name="syslog_ident"
|
||||
value="<?= htmlspecialchars($logging['syslog_ident'] ?? 'codepress') ?>">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-bold">Te registreren gebeurtenissen</label>
|
||||
<?php $events = $logging['events'] ?? []; ?>
|
||||
<div class="row">
|
||||
<?php
|
||||
$eventLabels = [
|
||||
'admin' => 'Admin activiteiten',
|
||||
'requests' => 'Requests (pagina bezoeken)',
|
||||
'errors' => 'Fouten & waarschuwingen',
|
||||
'security' => 'Beveiliging',
|
||||
'content' => 'Content wijzigingen',
|
||||
'system' => 'Systeem',
|
||||
];
|
||||
foreach ($eventLabels as $key => $label):
|
||||
?>
|
||||
<div class="col-md-4">
|
||||
<div class="form-check">
|
||||
<input type="checkbox" class="form-check-input" id="logging_event_<?= $key ?>" name="logging_events[]" value="<?= $key ?>" <?= !empty($events[$key]) ? 'checked' : '' ?>>
|
||||
<label class="form-check-label" for="logging_event_<?= $key ?>"><?= $label ?></label>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary btn-lg">
|
||||
<i class="bi bi-check-lg"></i> Configuratie opslaan
|
||||
</button>
|
||||
|
||||
@@ -18,11 +18,17 @@
|
||||
<div class="col-md-4">
|
||||
<label for="layout" class="form-label">Sjabloon / Layout</label>
|
||||
<select class="form-select" id="layout" name="layout">
|
||||
<?php if (empty($themeLayouts)): ?>
|
||||
<option value="sidebar-content" <?= $currentLayout === 'sidebar-content' ? 'selected' : '' ?>>Sidebar + Inhoud (standaard)</option>
|
||||
<option value="content" <?= $currentLayout === 'content' ? 'selected' : '' ?>>Alleen inhoud (full-width)</option>
|
||||
<option value="sidebar" <?= $currentLayout === 'sidebar' ? 'selected' : '' ?>>Alleen sidebar (full-width)</option>
|
||||
<option value="content-sidebar" <?= $currentLayout === 'content-sidebar' ? 'selected' : '' ?>>Inhoud links + sidebar rechts</option>
|
||||
<option value="content-sidebar-reverse" <?= $currentLayout === 'content-sidebar-reverse' ? 'selected' : '' ?>>Inhoud rechts + sidebar links</option>
|
||||
<?php else: ?>
|
||||
<?php foreach ($themeLayouts as $key => $file): ?>
|
||||
<option value="<?= htmlspecialchars($key) ?>" <?= $currentLayout === $key ? 'selected' : '' ?>><?= htmlspecialchars(ucfirst(str_replace('_', ' ', $key))) ?></option>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</select>
|
||||
</div>
|
||||
<?php if (!empty($availablePlugins)): ?>
|
||||
|
||||
@@ -28,11 +28,17 @@
|
||||
<div class="col-md-6">
|
||||
<label for="layout" class="form-label">Sjabloon / Layout</label>
|
||||
<select class="form-select" id="layout" name="layout">
|
||||
<?php if (empty($themeLayouts)): ?>
|
||||
<option value="sidebar-content">Sidebar + Inhoud (standaard)</option>
|
||||
<option value="content">Alleen inhoud (full-width)</option>
|
||||
<option value="sidebar">Alleen sidebar (full-width)</option>
|
||||
<option value="content-sidebar">Inhoud links + sidebar rechts</option>
|
||||
<option value="content-sidebar-reverse">Inhoud rechts + sidebar links</option>
|
||||
<?php else: ?>
|
||||
<?php foreach ($themeLayouts as $key => $file): ?>
|
||||
<option value="<?= htmlspecialchars($key) ?>"><?= htmlspecialchars(ucfirst(str_replace('_', ' ', $key))) ?></option>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</select>
|
||||
</div>
|
||||
<?php if (!empty($availablePlugins)): ?>
|
||||
|
||||
+72
-110
@@ -1,117 +1,79 @@
|
||||
<h2 class="mb-4"><i class="bi bi-journal-text"></i> Logs</h2>
|
||||
|
||||
<ul class="nav nav-tabs mb-3" id="logTabs" role="tablist">
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link <?= $activeTab === 'admin' ? 'active' : '' ?>" id="admin-log-tab" data-bs-toggle="tab" data-bs-target="#admin-log" type="button" role="tab">Activiteiten log</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link <?= $activeTab === 'requests' ? 'active' : '' ?>" id="request-log-tab" data-bs-toggle="tab" data-bs-target="#request-log" type="button" role="tab">Requests log</button>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="tab-content">
|
||||
<!-- Admin activity log -->
|
||||
<div class="tab-pane fade <?= $activeTab === 'admin' ? 'show active' : '' ?>" id="admin-log" role="tabpanel">
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<small class="text-muted"><?= count($adminLogs) ?> regels</small>
|
||||
<div>
|
||||
<a href="/admin/logs?tab=admin&download=1" class="btn btn-sm btn-outline-secondary"><i class="bi bi-download"></i> Download</a>
|
||||
<a href="/admin/logs?tab=admin&clear=1" class="btn btn-sm btn-outline-danger" onclick="return confirm('Activiteiten log wissen?')"><i class="bi bi-trash"></i> Wissen</a>
|
||||
<div class="card shadow-sm mb-4">
|
||||
<div class="card-body">
|
||||
<form method="GET" action="/admin/logs" class="row g-3 align-items-end">
|
||||
<div class="col-md-3">
|
||||
<label for="filter_event" class="form-label">Gebeurtenis</label>
|
||||
<select class="form-select" id="filter_event" name="event">
|
||||
<option value="">Alle</option>
|
||||
<?php foreach ($eventTypes as $ev): ?>
|
||||
<option value="<?= $ev ?>" <?= $filterEvent === $ev ? 'selected' : '' ?>><?= ucfirst($ev) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-body p-0" style="max-height: 500px; overflow-y: auto;">
|
||||
<?php if (empty($adminLogs)): ?>
|
||||
<p class="text-muted p-3 mb-0">Geen activiteiten geregistreerd.</p>
|
||||
<?php else: ?>
|
||||
<table class="table table-sm table-hover mb-0">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>Tijd</th>
|
||||
<th>Niveau</th>
|
||||
<th>IP</th>
|
||||
<th>Bericht</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($adminLogs as $log): ?>
|
||||
<tr>
|
||||
<td class="text-nowrap small text-muted"><?= htmlspecialchars($log['time']) ?></td>
|
||||
<td><span class="badge bg-<?= $log['level'] === 'warning' ? 'warning text-dark' : ($log['level'] === 'error' ? 'danger' : 'info') ?>"><?= htmlspecialchars($log['level']) ?></span></td>
|
||||
<td><code class="small"><?= htmlspecialchars($log['ip']) ?></code></td>
|
||||
<td><?= htmlspecialchars($log['message']) ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
<?php endif; ?>
|
||||
<div class="col-md-3">
|
||||
<label for="filter_level" class="form-label">Niveau</label>
|
||||
<select class="form-select" id="filter_level" name="level">
|
||||
<option value="">Alle</option>
|
||||
<?php foreach ($levelTypes as $lv): ?>
|
||||
<option value="<?= $lv ?>" <?= $filterLevel === $lv ? 'selected' : '' ?>><?= ucfirst($lv) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label for="filter_search" class="form-label">Zoeken</label>
|
||||
<input type="text" class="form-control" id="filter_search" name="search" value="<?= htmlspecialchars($filterSearch) ?>" placeholder="Zoek in bericht...">
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label for="filter_limit" class="form-label">Aantal</label>
|
||||
<select class="form-select" id="filter_limit" name="limit">
|
||||
<?php foreach ([50, 100, 200, 500, 1000] as $n): ?>
|
||||
<option value="<?= $n ?>" <?= $limit === $n ? 'selected' : '' ?>><?= $n ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-12 d-flex justify-content-between">
|
||||
<button type="submit" class="btn btn-primary"><i class="bi bi-funnel"></i> Filteren</button>
|
||||
<div>
|
||||
<a href="/admin/logs?download=1<?= $filterEvent ? '&event=' . urlencode($filterEvent) : '' ?><?= $filterLevel ? '&level=' . urlencode($filterLevel) : '' ?><?= $filterSearch ? '&search=' . urlencode($filterSearch) : '' ?>" class="btn btn-sm btn-outline-secondary"><i class="bi bi-download"></i> Download</a>
|
||||
<a href="/admin/logs?clear=1" class="btn btn-sm btn-outline-danger" onclick="return confirm('Log wissen?')"><i class="bi bi-trash"></i> Wissen</a>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Request log -->
|
||||
<div class="tab-pane fade <?= $activeTab === 'requests' ? 'show active' : '' ?>" id="request-log" role="tabpanel">
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<small class="text-muted"><?= count($requestLogs) ?> regels</small>
|
||||
<div>
|
||||
<a href="/admin/logs?tab=requests&download=1" class="btn btn-sm btn-outline-secondary"><i class="bi bi-download"></i> Download</a>
|
||||
<a href="/admin/logs?tab=requests&clear=1" class="btn btn-sm btn-outline-danger" onclick="return confirm('Requestlog wissen?')"><i class="bi bi-trash"></i> Wissen</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-body p-0" style="max-height: 500px; overflow-y: auto;">
|
||||
<?php if (empty($requestLogs)): ?>
|
||||
<p class="text-muted p-3 mb-0">Geen requests geregistreerd.</p>
|
||||
<?php else: ?>
|
||||
<table class="table table-sm table-hover mb-0">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>Tijd</th>
|
||||
<th>IP</th>
|
||||
<th>Land</th>
|
||||
<th>Pagina</th>
|
||||
<th>Type / Gebruiker</th>
|
||||
<th>Status</th>
|
||||
<th>Taal</th>
|
||||
<th>User agent</th>
|
||||
<th>Referrer</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($requestLogs as $log): ?>
|
||||
<tr class="<?= str_starts_with($log['status'] ?? 'ok', 'blocked') ? 'table-danger' : '' ?>">
|
||||
<td class="text-nowrap small text-muted"><?= htmlspecialchars($log['time']) ?></td>
|
||||
<td><code class="small"><?= htmlspecialchars($log['ip']) ?></code></td>
|
||||
<td class="text-nowrap small" title="<?= htmlspecialchars(GeoIP::getCountryName($log['country'] ?: null)) ?>">
|
||||
<?= GeoIP::getCountryFlagEmoji($log['country'] ?: null) ?>
|
||||
<span class="text-muted"><?= htmlspecialchars($log['country'] ?: '—') ?></span>
|
||||
</td>
|
||||
<td><?= htmlspecialchars($log['page']) ?></td>
|
||||
<td>
|
||||
<span class="badge bg-<?= $log['visitor_info']['badge'] ?>">
|
||||
<i class="bi <?= $log['visitor_info']['icon'] ?>"></i> <?= $log['visitor_info']['label'] ?>
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<?php if (str_starts_with($log['status'] ?? 'ok', 'blocked')): ?>
|
||||
<span class="badge bg-danger" title="<?= htmlspecialchars($log['status']) ?>">
|
||||
<i class="bi bi-shield-x"></i> Geblokkeerd
|
||||
</span>
|
||||
<?php else: ?>
|
||||
<span class="badge bg-success" title="Toegestaan">
|
||||
<i class="bi bi-check-circle"></i> OK
|
||||
</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td><?= htmlspecialchars($log['lang']) ?></td>
|
||||
<td class="small text-muted" title="<?= htmlspecialchars($log['ua']) ?>"><?= htmlspecialchars(substr($log['ua'], 0, 50)) ?><?= strlen($log['ua']) > 50 ? '…' : '' ?></td>
|
||||
<td class="small text-muted" title="<?= htmlspecialchars($log['referrer']) ?>"><?= htmlspecialchars(substr($log['referrer'], 0, 30)) ?><?= strlen($log['referrer']) > 30 ? '…' : '' ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<small class="text-muted"><?= count($logEntries) ?> regels</small>
|
||||
</div>
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-body p-0" style="max-height: 600px; overflow-y: auto;">
|
||||
<?php if (empty($logEntries)): ?>
|
||||
<p class="text-muted p-3 mb-0">Geen logregels gevonden.</p>
|
||||
<?php else: ?>
|
||||
<table class="table table-sm table-hover mb-0">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>Tijd</th>
|
||||
<th>Gebeurtenis</th>
|
||||
<th>Niveau</th>
|
||||
<th>IP</th>
|
||||
<th>Bericht</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($logEntries as $log): ?>
|
||||
<tr>
|
||||
<td class="text-nowrap small text-muted"><?= htmlspecialchars($log['time']) ?></td>
|
||||
<td><span class="badge bg-secondary"><?= htmlspecialchars($log['event']) ?></span></td>
|
||||
<td><span class="badge bg-<?= $log['level'] === 'warning' ? 'warning text-dark' : ($log['level'] === 'error' ? 'danger' : 'info') ?>"><?= htmlspecialchars($log['level']) ?></span></td>
|
||||
<td><code class="small"><?= htmlspecialchars($log['ip']) ?></code></td>
|
||||
<td><?= htmlspecialchars($log['message']) ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
<h2 class="mb-4"><i class="bi bi-palette"></i> Nieuw thema aanmaken</h2>
|
||||
|
||||
<form method="post" class="card shadow-sm">
|
||||
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($csrf) ?>">
|
||||
|
||||
<div class="card-body">
|
||||
<?php if (!empty($message)): ?>
|
||||
<div class="alert alert-<?= $messageType ?>"><?= htmlspecialchars($message) ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="theme_name" class="form-label">Thema naam</label>
|
||||
<input type="text" class="form-control font-monospace" id="theme_name" name="theme_name"
|
||||
value="<?= htmlspecialchars($_POST['theme_name'] ?? '') ?>" required
|
||||
placeholder="bijv. MijnThema">
|
||||
<div class="form-text">Alleen letters, cijfers, streepjes en underscores. Begin met een letter. Wordt gebruikt als <strong>mapnaam</strong>.</div>
|
||||
</div>
|
||||
|
||||
<div class="alert alert-info mb-0">
|
||||
<strong><i class="bi bi-lightbulb"></i> Wat wordt er aangemaakt?</strong>
|
||||
<ul class="mb-0 mt-2">
|
||||
<li><code>themes/ThemaNaam/theme.json</code> — Configuratie (default template + template mapping)</li>
|
||||
<li><code>themes/ThemaNaam/base.twig</code> — Hoofd layout</li>
|
||||
<li><code>themes/ThemaNaam/*.twig</code> — Layout-sjablonen</li>
|
||||
<li><code>themes/ThemaNaam/partials/</code> — header, navigation, footer</li>
|
||||
<li><code>themes/ThemaNaam/css/theme.scss</code> — Styling (kleuren, hoogtes, achtergrond)</li>
|
||||
<li><code>themes/ThemaNaam/js/theme.js</code> — Thema JavaScript</li>
|
||||
<li><code>themes/ThemaNaam/theme.png</code> — Voorbeeldafbeelding</li>
|
||||
</ul>
|
||||
<p class="text-muted mb-0 mt-2">Het nieuwe thema wordt gekopieerd van het standaard thema. Pas daarna de SCSS-kleuren en sjablonen aan naar wens.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-footer text-end">
|
||||
<a href="/admin/theme" class="btn btn-secondary">Annuleren</a>
|
||||
<button type="submit" class="btn btn-primary"><i class="bi bi-check-lg"></i> Aanmaken</button>
|
||||
</div>
|
||||
</form>
|
||||
+81
-260
@@ -1,283 +1,104 @@
|
||||
<?php if ($editTheme): ?>
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h2><i class="bi bi-palette"></i> Thema's</h2>
|
||||
<a href="/admin/theme-new" class="btn btn-primary btn-sm">
|
||||
<i class="bi bi-plus-lg"></i> Nieuwe thema
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Edit theme -->
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h2><i class="bi bi-palette"></i> Bewerk thema: <?= htmlspecialchars($editTheme['name'] ?? $editThemeName) ?></h2>
|
||||
<a href="/admin/theme" class="btn btn-outline-secondary btn-sm">
|
||||
<i class="bi bi-arrow-left"></i> Terug
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-body">
|
||||
<form method="POST" action="/admin/theme" enctype="multipart/form-data">
|
||||
<input type="hidden" name="csrf_token" value="<?= $csrf ?>">
|
||||
<input type="hidden" name="action" value="save">
|
||||
<input type="hidden" name="theme" value="<?= htmlspecialchars($editThemeName) ?>">
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-6">
|
||||
<label for="theme_name" class="form-label">Themanaam</label>
|
||||
<input type="text" class="form-control" id="theme_name" name="theme_name" value="<?= htmlspecialchars($editTheme['name'] ?? $editThemeName) ?>">
|
||||
<div class="card shadow-sm mb-4">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title"><i class="bi bi-info-circle"></i> Thema-ontwikkelaarshandleiding</h5>
|
||||
<p class="text-muted mb-2">Een thema is een volledig zelfstandige map en moet aan de volgende eisen voldoen:</p>
|
||||
<div class="row g-3">
|
||||
<div class="col-md-4">
|
||||
<div class="d-flex align-items-start">
|
||||
<i class="bi bi-folder2-open text-primary me-2 mt-1"></i>
|
||||
<div>
|
||||
<strong>Mapstructuur</strong><br>
|
||||
<code class="small">themes/Naam/theme.json</code>
|
||||
<small class="text-muted d-block">Elke thema-map moet een <code>theme.json</code> bevatten.</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-4">
|
||||
<div class="col-md-4">
|
||||
<div class="card">
|
||||
<div class="card-header">Header</div>
|
||||
<div class="card-body">
|
||||
<div class="mb-3">
|
||||
<label for="header_color" class="form-label">Achtergrond</label>
|
||||
<div class="input-group">
|
||||
<input type="color" class="form-control form-control-color" id="header_color" name="header_color" value="<?= htmlspecialchars($editTheme['header_color'] ?? '#0a369d') ?>">
|
||||
<input type="text" class="form-control form-control-color-value" value="<?= htmlspecialchars($editTheme['header_color'] ?? '#0a369d') ?>" maxlength="7" data-target="header_color">
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-0">
|
||||
<label for="header_font_color" class="form-label">Tekst</label>
|
||||
<div class="input-group">
|
||||
<input type="color" class="form-control form-control-color" id="header_font_color" name="header_font_color" value="<?= htmlspecialchars($editTheme['header_font_color'] ?? '#ffffff') ?>">
|
||||
<input type="text" class="form-control form-control-color-value" value="<?= htmlspecialchars($editTheme['header_font_color'] ?? '#ffffff') ?>" maxlength="7" data-target="header_font_color">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="card">
|
||||
<div class="card-header">Navigatie</div>
|
||||
<div class="card-body">
|
||||
<div class="mb-3">
|
||||
<label for="navigation_color" class="form-label">Achtergrond</label>
|
||||
<div class="input-group">
|
||||
<input type="color" class="form-control form-control-color" id="navigation_color" name="navigation_color" value="<?= htmlspecialchars($editTheme['navigation_color'] ?? '#2754b4') ?>">
|
||||
<input type="text" class="form-control form-control-color-value" value="<?= htmlspecialchars($editTheme['navigation_color'] ?? '#2754b4') ?>" maxlength="7" data-target="navigation_color">
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-0">
|
||||
<label for="navigation_font_color" class="form-label">Tekst</label>
|
||||
<div class="input-group">
|
||||
<input type="color" class="form-control form-control-color" id="navigation_font_color" name="navigation_font_color" value="<?= htmlspecialchars($editTheme['navigation_font_color'] ?? '#ffffff') ?>">
|
||||
<input type="text" class="form-control form-control-color-value" value="<?= htmlspecialchars($editTheme['navigation_font_color'] ?? '#ffffff') ?>" maxlength="7" data-target="navigation_font_color">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="card">
|
||||
<div class="card-header">Sidebar</div>
|
||||
<div class="card-body">
|
||||
<div class="mb-3">
|
||||
<label for="sidebar_background" class="form-label">Achtergrond</label>
|
||||
<div class="input-group">
|
||||
<input type="color" class="form-control form-control-color" id="sidebar_background" name="sidebar_background" value="<?= htmlspecialchars($editTheme['sidebar_background'] ?? '#f8f9fa') ?>">
|
||||
<input type="text" class="form-control form-control-color-value" value="<?= htmlspecialchars($editTheme['sidebar_background'] ?? '#f8f9fa') ?>" maxlength="7" data-target="sidebar_background">
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-0">
|
||||
<label for="sidebar_border" class="form-label">Rand</label>
|
||||
<div class="input-group">
|
||||
<input type="color" class="form-control form-control-color" id="sidebar_border" name="sidebar_border" value="<?= htmlspecialchars($editTheme['sidebar_border'] ?? '#dee2e6') ?>">
|
||||
<input type="text" class="form-control form-control-color-value" value="<?= htmlspecialchars($editTheme['sidebar_border'] ?? '#dee2e6') ?>" maxlength="7" data-target="sidebar_border">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="d-flex align-items-start">
|
||||
<i class="bi bi-file-earmark-code text-primary me-2 mt-1"></i>
|
||||
<div>
|
||||
<strong>Sjablonen (Twig)</strong><br>
|
||||
<code class="small">base.twig + *.twig</code>
|
||||
<small class="text-muted d-block">Layout-sjablonen die via <code>config.default_template</code> en <code>template</code> worden gekozen.</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-4 mt-2">
|
||||
<div class="col-md-4">
|
||||
<div class="card">
|
||||
<div class="card-header"><i class="bi bi-arrows-vertical"></i> Hoogte balken</div>
|
||||
<div class="card-body">
|
||||
<div class="mb-3">
|
||||
<label for="header_height" class="form-label">Header hoogte (px)</label>
|
||||
<input type="number" class="form-control" id="header_height" name="header_height" value="<?= htmlspecialchars($editTheme['header_height'] ?? '56') ?>" min="32" max="200">
|
||||
</div>
|
||||
<div class="mb-0">
|
||||
<label for="nav_height" class="form-label">Navigatie hoogte (px)</label>
|
||||
<input type="number" class="form-control" id="nav_height" name="nav_height" value="<?= htmlspecialchars($editTheme['nav_height'] ?? '42') ?>" min="24" max="200">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<div class="card">
|
||||
<div class="card-header"><i class="bi bi-image"></i> Header achtergrond afbeelding</div>
|
||||
<div class="card-body">
|
||||
<div class="mb-3">
|
||||
<label for="bg_image" class="form-label">Upload afbeelding</label>
|
||||
<input type="file" class="form-control" id="bg_image" name="bg_image" accept="image/jpeg,image/png,image/gif,image/webp,image/svg+xml">
|
||||
<small class="form-text text-muted">Toegestaan: JPG, PNG, GIF, WebP, SVG</small>
|
||||
</div>
|
||||
<div class="mb-0">
|
||||
<label for="background_image_url" class="form-label">Of URL naar afbeelding</label>
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control" id="background_image_url" name="background_image_url" placeholder="https://..." value="<?= htmlspecialchars(str_starts_with($editTheme['background_image'] ?? '', 'http') ? $editTheme['background_image'] : '') ?>">
|
||||
</div>
|
||||
<?php if (!empty($editTheme['background_image'])): ?>
|
||||
<div class="mt-2 d-flex align-items-center gap-3">
|
||||
<div>
|
||||
<small class="text-muted">Huidig:</small>
|
||||
<img src="<?= htmlspecialchars(str_starts_with($editTheme['background_image'], 'http') ? $editTheme['background_image'] : '/themes/' . $editTheme['background_image']) ?>" style="max-height: 60px; max-width: 200px;" class="img-thumbnail mt-1 d-block">
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input class="btn-check" type="checkbox" id="bg_image_remove" name="bg_image_remove" value="1" autocomplete="off">
|
||||
<label class="btn btn-outline-danger btn-sm" for="bg_image_remove">
|
||||
<i class="bi bi-trash3"></i> Verwijder afbeelding
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<div class="mb-3 mt-3">
|
||||
<label for="background_image_opacity" class="form-label">Doorzichtigheid (%)</label>
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<input type="range" class="form-range" style="max-width: 200px;" id="background_image_opacity" name="background_image_opacity" min="0" max="100" value="<?= htmlspecialchars($editTheme['background_image_opacity'] ?? '100') ?>" oninput="this.nextElementSibling.textContent=this.value+'%'">
|
||||
<span class="badge bg-secondary"><?= htmlspecialchars($editTheme['background_image_opacity'] ?? '100') ?>%</span>
|
||||
</div>
|
||||
<small class="form-text text-muted">100% = volledig zichtbaar, 50% = half doorzichtig, 0% = onzichtbaar</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="d-flex align-items-start">
|
||||
<i class="bi bi-palette text-primary me-2 mt-1"></i>
|
||||
<div>
|
||||
<strong>Styling & voorbeeld</strong><br>
|
||||
<code class="small">css/theme.scss + theme.png</code>
|
||||
<small class="text-muted d-block">Kleuren/hoogtes in SCSS, een <code>theme.png</code> voorbeeld voor de selectie.</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="bi bi-check-lg"></i> Opslaan
|
||||
</button>
|
||||
<a href="/admin/theme" class="btn btn-outline-secondary">Annuleren</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php else: ?>
|
||||
|
||||
<!-- Theme list -->
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h2><i class="bi bi-palette"></i> Thema's</h2>
|
||||
<button type="button" class="btn btn-primary btn-sm" data-bs-toggle="collapse" data-bs-target="#newThemeForm">
|
||||
<i class="bi bi-plus-lg"></i> Nieuw thema
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="collapse mb-4" id="newThemeForm">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-body">
|
||||
<form method="POST" action="/admin/theme" class="row g-3 align-items-end">
|
||||
<input type="hidden" name="csrf_token" value="<?= $csrf ?>">
|
||||
<input type="hidden" name="action" value="create">
|
||||
<div class="col-md-6">
|
||||
<label for="new_name" class="form-label">Naam nieuw thema</label>
|
||||
<input type="text" class="form-control" id="new_name" name="new_name" placeholder="bijv. donker" required>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<button type="submit" class="btn btn-success w-100">
|
||||
<i class="bi bi-plus-lg"></i> Aanmaken
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-4">
|
||||
<?php foreach ($themes as $themeName => $themeData): ?>
|
||||
<?php $isActive = $themeName === $activeTheme; ?>
|
||||
<div class="col-md-4">
|
||||
<div class="card shadow-sm h-100">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-start mb-3">
|
||||
<h5 class="card-title mb-0"><?= htmlspecialchars($themeData['name'] ?? $themeName) ?></h5>
|
||||
<?php if ($themeName === $activeTheme): ?>
|
||||
<span class="badge bg-success">Actief</span>
|
||||
<?php endif; ?>
|
||||
<div class="card shadow-sm h-100 theme-card">
|
||||
<div class="card-body text-center">
|
||||
<div class="mb-2">
|
||||
<?php $preview = '/themes/' . htmlspecialchars($themeName) . '/theme.png'; ?>
|
||||
<img src="<?= $preview ?>" alt="Voorbeeld thema <?= htmlspecialchars($themeData['title'] ?? $themeData['name'] ?? $themeName) ?>" class="img-fluid rounded theme-preview">
|
||||
</div>
|
||||
|
||||
<!-- Color preview swatches -->
|
||||
<div class="mb-3">
|
||||
<div class="d-flex align-items-center gap-1 mb-2">
|
||||
<span class="small text-muted" style="width: 70px;">Header:</span>
|
||||
<span class="d-inline-block rounded" style="width: 24px; height: 24px; background: <?= htmlspecialchars($themeData['header_color'] ?? '#0a369d') ?>; border: 1px solid #dee2e6;"></span>
|
||||
<span class="small text-muted"><?= htmlspecialchars($themeData['header_color'] ?? '#0a369d') ?></span>
|
||||
</div>
|
||||
<div class="d-flex align-items-center gap-1 mb-2">
|
||||
<span class="small text-muted" style="width: 70px;">Navigatie:</span>
|
||||
<span class="d-inline-block rounded" style="width: 24px; height: 24px; background: <?= htmlspecialchars($themeData['navigation_color'] ?? '#2754b4') ?>; border: 1px solid #dee2e6;"></span>
|
||||
<span class="small text-muted"><?= htmlspecialchars($themeData['navigation_color'] ?? '#2754b4') ?></span>
|
||||
</div>
|
||||
<div class="d-flex align-items-center gap-1">
|
||||
<span class="small text-muted" style="width: 70px;">Sidebar:</span>
|
||||
<span class="d-inline-block rounded" style="width: 24px; height: 24px; background: <?= htmlspecialchars($themeData['sidebar_background'] ?? '#f8f9fa') ?>; border: 1px solid #dee2e6;"></span>
|
||||
<span class="small text-muted"><?= htmlspecialchars($themeData['sidebar_background'] ?? '#f8f9fa') ?></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Heights and background image -->
|
||||
<div class="mb-3 small">
|
||||
<div class="text-muted mb-1">
|
||||
<i class="bi bi-arrows-vertical"></i> Header: <?= htmlspecialchars($themeData['header_height'] ?? '56') ?>px · Navigatie: <?= htmlspecialchars($themeData['nav_height'] ?? '42') ?>px
|
||||
</div>
|
||||
<?php if (!empty($themeData['background_image'])): ?>
|
||||
<div class="text-muted">
|
||||
<i class="bi bi-image"></i> Achtergrond: <?= htmlspecialchars($themeData['background_image_opacity'] ?? '100') ?>% zichtbaar
|
||||
<?php if (!str_starts_with($themeData['background_image'], 'http')): ?>
|
||||
<img src="/themes/<?= htmlspecialchars($themeData['background_image']) ?>" style="max-height: 30px; max-width: 80px;" class="img-thumbnail ms-1 align-middle">
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<div class="d-flex gap-2">
|
||||
<a href="/admin/theme?edit=<?= urlencode($themeName) ?>" class="btn btn-outline-primary btn-sm">
|
||||
<i class="bi bi-pencil"></i> Bewerken
|
||||
</a>
|
||||
<?php if ($themeName !== $activeTheme): ?>
|
||||
<form method="POST" action="/admin/theme">
|
||||
<input type="hidden" name="csrf_token" value="<?= $csrf ?>">
|
||||
<input type="hidden" name="action" value="activate">
|
||||
<input type="hidden" name="theme" value="<?= htmlspecialchars($themeName) ?>">
|
||||
<button type="submit" class="btn btn-outline-success btn-sm">
|
||||
<i class="bi bi-check-circle"></i> Activeren
|
||||
</button>
|
||||
</form>
|
||||
<?php endif; ?>
|
||||
<h5 class="card-title mb-0">
|
||||
<?= htmlspecialchars($themeData['title'] ?? $themeData['name'] ?? $themeName) ?>
|
||||
</h5>
|
||||
<?php if ($isActive): ?>
|
||||
<span class="badge bg-success mt-1">Actief</span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<div class="card-footer text-center py-2">
|
||||
<?php if ($isActive): ?>
|
||||
<span class="text-muted small">Dit thema is actief</span>
|
||||
<?php else: ?>
|
||||
<form method="POST" action="/admin/theme" class="d-inline">
|
||||
<input type="hidden" name="csrf_token" value="<?= $csrf ?>">
|
||||
<input type="hidden" name="action" value="activate">
|
||||
<input type="hidden" name="theme" value="<?= htmlspecialchars($themeName) ?>">
|
||||
<button type="submit" class="btn btn-sm btn-outline-success" title="Activeren">
|
||||
<i class="bi bi-check-circle"></i> Activeren
|
||||
</button>
|
||||
</form>
|
||||
<?php if ($themeName !== 'default'): ?>
|
||||
<form method="POST" action="/admin/theme">
|
||||
<input type="hidden" name="csrf_token" value="<?= $csrf ?>">
|
||||
<input type="hidden" name="action" value="delete">
|
||||
<input type="hidden" name="theme" value="<?= htmlspecialchars($themeName) ?>">
|
||||
<button type="submit" class="btn btn-outline-danger btn-sm" onclick="return confirm('Weet je zeker dat je thema '<?= htmlspecialchars($themeData['name'] ?? $themeName) ?>' wilt verwijderen?')">
|
||||
<i class="bi bi-trash"></i> Verwijderen
|
||||
</button>
|
||||
</form>
|
||||
<form method="POST" action="/admin/theme-delete?theme=<?= urlencode($themeName) ?>" class="d-inline" onsubmit="return confirm('Weet je zeker dat je thema '<?= htmlspecialchars($themeData['title'] ?? $themeName) ?>' wilt verwijderen? Alle bestanden worden permanent verwijderd.')">
|
||||
<input type="hidden" name="csrf_token" value="<?= $csrf ?>">
|
||||
<button type="submit" class="btn btn-sm btn-outline-danger" title="Verwijderen">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
</form>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
|
||||
<?php endif; ?>
|
||||
|
||||
<script>
|
||||
document.querySelectorAll('.form-control-color-value').forEach(function(input) {
|
||||
input.addEventListener('input', function() {
|
||||
var target = document.getElementById(this.dataset.target);
|
||||
if (target && /^#[0-9a-fA-F]{6}$/.test(this.value)) {
|
||||
target.value = this.value;
|
||||
}
|
||||
});
|
||||
});
|
||||
document.querySelectorAll('.form-control-color').forEach(function(input) {
|
||||
input.addEventListener('input', function() {
|
||||
var target = document.querySelector('[data-target="' + this.id + '"]');
|
||||
if (target) target.value = this.value;
|
||||
});
|
||||
});
|
||||
</script>
|
||||
<style>
|
||||
.theme-card {
|
||||
transition: box-shadow 0.2s ease, transform 0.2s ease;
|
||||
overflow: hidden;
|
||||
}
|
||||
.theme-card:hover {
|
||||
box-shadow: 0 0.5rem 1rem rgba(0,0,0,0.15);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
.theme-preview {
|
||||
border: 1px solid #dee2e6;
|
||||
background: #ffffff;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user