v2.5.1: Admin theme refactor, Navigation plugin, user roles, guide restructure

- Reorganize admin into admin/theme/default/ (views + assets)
- Rename GuideNav to Navigation plugin (essential, protected)
- Plugin assets support (SCSS/CSS) loaded after theme CSS
- User roles: Admin, Content Manager, BI Manager, Site Admin
- Role-based access control (RBAC) for admin routes and sidebar
- Guide restructure: sub-topics in separate folders with sidebar nav
- Dynamic breadcrumb for homepage and subdirectories
- Fix theme path traversal (../../ -> ../) in admin.php
- Fix CodeMirror mode load order (xml -> css -> js -> htmlmixed -> php)
- Fix editor-toolbar.js null checks for plugin edit pages
- Layout select from theme.json with live frontmatter update
- Footer sticky at bottom of viewport (min-height: 100vh)
- Breadcrumb color fix (var(--nav-font) -> var(--header-bg))
- Remove language switcher from guide pages
- Update README.md and README.en.md
- Bump version to 2.5.1
This commit is contained in:
2026-08-10 15:36:29 +02:00
parent 0961b23b8d
commit cd498c8c3a
209 changed files with 11033 additions and 9125 deletions
@@ -0,0 +1,182 @@
<!DOCTYPE html>
<html lang="nl">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}CodePress Admin{% endblock %}</title>
<link rel="stylesheet" href="/admin/assets/css/bootstrap.min.css">
<link rel="stylesheet" href="/admin/assets/css/bootstrap-icons.css">
<link rel="stylesheet" href="/admin/assets/css/style.css">
<link rel="stylesheet" href="/plugins/Navigation/assets/css/navigation.css">
<style>
body { background-color: #f5f6fa; min-height: 100vh; }
.admin-sidebar { background-color: {{ sidebar_color|default('#0a369d') }}; min-height: 100vh; width: 240px; position: fixed; top: 0; left: 0; z-index: 100; display: flex; flex-direction: column; }
.admin-sidebar .nav.flex-column:first-of-type { flex: 1; overflow-y: auto; }
.admin-sidebar .nav-link { color: rgba(255,255,255,0.8); padding: 0.75rem 1.25rem; border-radius: 0; }
.admin-sidebar .nav-link:hover { color: #fff; background-color: rgba(255,255,255,0.1); }
.admin-sidebar .nav-link.active { color: #fff; background-color: rgba(255,255,255,0.2); border-left: 3px solid #fff; }
.admin-sidebar .nav-link i { width: 24px; text-align: center; margin-right: 0.5rem; }
.admin-sidebar .nav-section { color: rgba(255,255,255,0.4); font-size: 0.7rem; text-transform: uppercase; letter-spacing: 0.08em; padding: 1rem 1.25rem 0.3rem 1.25rem; }
.admin-main { margin-left: 240px; padding: 2rem; }
.admin-brand { color: #fff; padding: 1.25rem; font-size: 1.1rem; border-bottom: 1px solid rgba(255,255,255,0.15); }
.admin-brand i { margin-right: 0.5rem; }
.stat-card { border: none; border-radius: 0.5rem; }
.stat-card .stat-icon { font-size: 2rem; opacity: 0.7; }
.admin-user { color: rgba(255,255,255,0.6); padding: 0.75rem 1.25rem; font-size: 0.85rem; border-top: 1px solid rgba(255,255,255,0.15); }
@media (max-width: 768px) {
.admin-sidebar { width: 100%; min-height: auto; position: relative; }
.admin-main { margin-left: 0; }
}
</style>
{% if needs_editor %}
<link rel="stylesheet" href="/admin/assets/codemirror/codemirror.min.css">
<link rel="stylesheet" href="/admin/assets/css/editor.css">
{% endif %}
{% block extra_css %}{% endblock %}
</head>
<body>
<nav class="admin-sidebar d-flex flex-column">
<div class="admin-brand">
<i class="bi bi-gear-fill"></i> CodePress Admin
</div>
<ul class="nav flex-column mt-2">
<li class="nav-section">Algemeen</li>
<li class="nav-item">
<a class="nav-link {{ route == 'dashboard' or route == '' ? 'active' : '' }}" href="/admin/dashboard">
<i class="bi bi-speedometer2"></i> Dashboard
</a>
</li>
{% if has_permission('content') %}
<li class="nav-section">Content</li>
<li class="nav-item">
<a class="nav-link {{ route starts with 'content' ? 'active' : '' }}" href="/admin/content">
<i class="bi bi-file-earmark-text"></i> Content
</a>
</li>
{% endif %}
{% if has_permission('config') or has_permission('theme') or has_permission('security') %}
<li class="nav-section">Instellingen</li>
{% if has_permission('config') %}
<li class="nav-item">
<a class="nav-link {{ route == 'config' ? 'active' : '' }}" href="/admin/config">
<i class="bi bi-sliders"></i> Configuratie
</a>
</li>
{% endif %}
{% if has_permission('theme') %}
<li class="nav-item">
<a class="nav-link {{ route == 'theme' ? 'active' : '' }}" href="/admin/theme">
<i class="bi bi-palette"></i> Thema
</a>
</li>
{% endif %}
{% if has_permission('security') %}
<li class="nav-item">
<a class="nav-link {{ route == 'security' ? 'active' : '' }}" href="/admin/security">
<i class="bi bi-shield-check"></i> Beveiliging
</a>
</li>
{% endif %}
{% endif %}
{% if has_permission('statistics') or has_permission('logs') %}
<li class="nav-section">Gegevens</li>
{% if has_permission('statistics') %}
<li class="nav-item">
<a class="nav-link {{ route == 'statistics' ? 'active' : '' }}" href="/admin/statistics">
<i class="bi bi-bar-chart"></i> Statistieken
</a>
</li>
{% endif %}
{% if has_permission('logs') %}
<li class="nav-item">
<a class="nav-link {{ route == 'logs' ? 'active' : '' }}" href="/admin/logs">
<i class="bi bi-journal-text"></i> Logs
</a>
</li>
{% endif %}
{% endif %}
{% if has_permission('plugins') or has_permission('users') or has_permission('update') %}
<li class="nav-section">Systeem</li>
{% if has_permission('plugins') %}
<li class="nav-item">
<a class="nav-link {{ route == 'plugins' ? 'active' : '' }}" href="/admin/plugins">
<i class="bi bi-plug"></i> Plugins
</a>
</li>
{% endif %}
{% if has_permission('users') %}
<li class="nav-item">
<a class="nav-link {{ route == 'users' ? 'active' : '' }}" href="/admin/users">
<i class="bi bi-people"></i> Gebruikers
</a>
</li>
{% endif %}
{% if has_permission('update') %}
<li class="nav-item">
<a class="nav-link {{ route == 'update' ? 'active' : '' }}" href="/admin/update">
<i class="bi bi-cloud-arrow-down"></i> Update
</a>
</li>
{% endif %}
{% endif %}
{% if has_permission('guide') %}
<li class="nav-section">Help</li>
<li class="nav-item">
<a class="nav-link {{ route == 'guide' ? 'active' : '' }}" href="/admin/guide">
<i class="bi bi-book"></i> Handleiding
</a>
</li>
{% endif %}
</ul>
<ul class="nav flex-column mt-auto mb-5">
<li class="nav-item">
<a class="nav-link" href="/" target="_blank">
<i class="bi bi-box-arrow-up-right"></i> Website bekijken
</a>
</li>
<li class="nav-item">
<a class="nav-link text-warning" href="/admin/logout">
<i class="bi bi-box-arrow-left"></i> Uitloggen
</a>
</li>
</ul>
<div class="admin-user">
<i class="bi bi-person-circle"></i> {{ user.username|default('') }}
<br><small>{{ role_label(user_role)|default('') }}</small>
</div>
</nav>
<main class="admin-main">
{% if message is defined and message %}
<div class="alert alert-{{ message_type|default('info') }} alert-dismissible fade show" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
{% endif %}
{% block content %}{% endblock %}
</main>
<script src="/admin/assets/js/bootstrap.bundle.min.js"></script>
{% if needs_editor %}
<script src="/admin/assets/codemirror/codemirror.min.js"></script>
<script src="/admin/assets/codemirror/mode/markdown.min.js"></script>
<script src="/admin/assets/codemirror/mode/xml.min.js"></script>
<script src="/admin/assets/codemirror/mode/css.min.js"></script>
<script src="/admin/assets/codemirror/mode/javascript.min.js"></script>
<script src="/admin/assets/codemirror/mode/htmlmixed.min.js"></script>
<script src="/admin/assets/codemirror/mode/php.min.js"></script>
<script src="/admin/assets/codemirror/mode/clike.min.js"></script>
<script src="/admin/assets/codemirror/addon/edit/closebrackets.min.js"></script>
<script src="/admin/assets/codemirror/addon/selection/active-line.min.js"></script>
<script src="/admin/assets/js/editor-toolbar.js"></script>
{% endif %}
{% block extra_js %}{% endblock %}
</body>
</html>
+61
View File
@@ -0,0 +1,61 @@
<!DOCTYPE html>
<html lang="nl">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>CodePress Admin - Login</title>
<link rel="stylesheet" href="/admin/assets/css/bootstrap.min.css">
<link rel="stylesheet" href="/admin/assets/css/bootstrap-icons.css">
<link rel="stylesheet" href="/admin/assets/css/style.css">
<style>
body { background-color: #f5f6fa; }
.login-card { max-width: 400px; margin: 10vh auto; }
.login-header { background-color: #0a369d; color: #fff; padding: 2rem; text-align: center; border-radius: 0.5rem 0.5rem 0 0; }
</style>
</head>
<body>
<div class="container">
<div class="login-card">
<div class="login-header">
<h4><i class="bi bi-shield-lock"></i> CodePress Admin</h4>
</div>
<div class="card border-0 shadow-sm" style="border-radius: 0 0 0.5rem 0.5rem;">
<div class="card-body p-4">
{% if error %}
<div class="alert alert-danger alert-dismissible fade show" role="alert">
{{ error }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
{% endif %}
<form method="POST" action="/admin/login">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<div class="mb-3">
<label for="username" class="form-label">Gebruikersnaam</label>
<div class="input-group">
<span class="input-group-text"><i class="bi bi-person"></i></span>
<input type="text" class="form-control" id="username" name="username" required autofocus>
</div>
</div>
<div class="mb-3">
<label for="password" class="form-label">Wachtwoord</label>
<div class="input-group">
<span class="input-group-text"><i class="bi bi-key"></i></span>
<input type="password" class="form-control" id="password" name="password" required>
</div>
</div>
<div class="d-grid">
<button type="submit" class="btn btn-primary">
<i class="bi bi-box-arrow-in-right"></i> Inloggen
</button>
</div>
</form>
</div>
</div>
<p class="text-center text-muted mt-3 small">
<a href="/" class="text-decoration-none"><i class="bi bi-arrow-left"></i> Terug naar website</a>
</p>
</div>
</div>
<script src="/admin/assets/js/bootstrap.bundle.min.js"></script>
</body>
</html>
@@ -0,0 +1,63 @@
{% extends "layouts/admin.twig" %}
{% block title %}Configuratie - CodePress Admin{% endblock %}
{% block content %}
<h2 class="mb-4"><i class="bi bi-sliders"></i> Configuratie</h2>
<form method="POST" action="/admin/config">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<div class="card shadow-sm mb-4">
<div class="card-header">Algemene instellingen</div>
<div class="card-body">
<div class="mb-3">
<label for="site_title" class="form-label">Site titel</label>
<input type="text" class="form-control" id="site_title" name="site_title" value="{{ config.site_title|default('CodePress') }}">
</div>
<div class="mb-3">
<label for="language_default" class="form-label">Standaard taal</label>
<select class="form-select" id="language_default" name="language_default">
<option value="nl" {{ config.language.default|default('nl') == 'nl' ? 'selected' : '' }}>Nederlands</option>
<option value="en" {{ config.language.default == 'en' ? 'selected' : '' }}>Engels</option>
<option value="de" {{ config.language.default == 'de' ? 'selected' : '' }}>Duits</option>
<option value="fr" {{ config.language.default == 'fr' ? 'selected' : '' }}>Frans</option>
</select>
</div>
</div>
</div>
<div class="card shadow-sm mb-4">
<div class="card-header">Auteur</div>
<div class="card-body">
<div class="mb-3">
<label for="author_name" class="form-label">Naam</label>
<input type="text" class="form-control" id="author_name" name="author_name" value="{{ config.author.name|default('') }}">
</div>
<div class="mb-3">
<label for="author_email" class="form-label">E-mail</label>
<input type="email" class="form-control" id="author_email" name="author_email" value="{{ config.author.email|default('') }}">
</div>
</div>
</div>
<div class="card shadow-sm mb-4">
<div class="card-header">Analytics & Logging</div>
<div class="card-body">
<div class="form-check form-switch mb-3">
<input class="form-check-input" type="checkbox" id="analytics_enabled" name="analytics_enabled" {{ config.analytics.enabled ? 'checked' : '' }}>
<label class="form-check-label" for="analytics_enabled">Analytics ingeschakeld</label>
</div>
<div class="form-check form-switch mb-3">
<input class="form-check-input" type="checkbox" id="logging_enabled" name="logging_enabled" {{ config.logging.enabled ? 'checked' : '' }}>
<label class="form-check-label" for="logging_enabled">Logging ingeschakeld</label>
</div>
</div>
</div>
<button type="submit" class="btn btn-primary">
<i class="bi bi-check-lg"></i> Opslaan
</button>
<a href="/admin/dashboard" class="btn btn-outline-secondary">Annuleren</a>
</form>
{% endblock %}
@@ -0,0 +1,23 @@
{% extends "layouts/admin.twig" %}
{% block title %}Map hernoemen - CodePress Admin{% endblock %}
{% block content %}
<h2 class="mb-4"><i class="bi bi-pencil"></i> Map hernoemen</h2>
<form method="post" class="card shadow-sm">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<div class="card-body">
<div class="mb-3">
<label for="newname" class="form-label">Nieuwe naam voor {{ currentName }}</label>
<input type="text" class="form-control" id="newname" name="newname" value="{{ currentName }}" required autofocus>
</div>
</div>
<div class="card-footer bg-white">
<button type="submit" class="btn btn-primary">
<i class="bi bi-check-lg"></i> Hernoemen
</button>
<a href="/admin/content?dir={{ dir|url_encode }}" class="btn btn-outline-secondary">Annuleren</a>
</div>
</form>
{% endblock %}
@@ -0,0 +1,124 @@
{% extends "layouts/admin.twig" %}
{% block title %}{{ fileName }} - CodePress Admin{% endblock %}
{% block content %}
<h2 class="mb-4"><i class="bi bi-pencil"></i> {{ fileName }}</h2>
<div class="card shadow-sm">
<div class="card-body">
<form method="POST" action="/admin/content-edit?file={{ file|url_encode }}" id="editor-form">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<div class="row mb-3">
<div class="col-md-4">
<label for="filename" class="form-label">Bestandsnaam</label>
<div class="input-group">
<input type="text" class="form-control" id="filename" name="filename" value="{{ fileName|replace({'.md': '', '.php': '', '.html': ''}) }}" required>
<span class="input-group-text">.{{ fileExt }}</span>
</div>
<small class="form-text text-muted">Alleen letters, cijfers, punten, underscores en streepjes.</small>
</div>
{% if isEditable %}
<div class="col-md-4">
<label for="layout" class="form-label">Sjabloon / Layout <small class="text-muted">({{ activeThemeName|default('default') }} theme)</small></label>
<select class="form-select" id="layout" name="layout">
{% for key, layoutFile in themeLayouts %}
<option value="{{ key }}" {{ currentLayout == key ? 'selected' : '' }}>{{ key|replace({'_': ' '})|capitalize }}{% if key == themeDefaultLayout %} (standaard){% endif %}</option>
{% endfor %}
</select>
</div>
{% if availablePlugins is not empty %}
<div class="col-md-4">
<label class="form-label d-block">Zichtbare plugins</label>
<div class="d-flex flex-wrap gap-1">
{% for plugin in availablePlugins %}
<input type="checkbox" class="btn-check" id="plugin-{{ plugin }}" name="plugins[]" value="{{ plugin }}" autocomplete="off" {{ plugin in selectedPlugins ? 'checked' : '' }}>
<label class="btn btn-outline-primary btn-sm" for="plugin-{{ plugin }}">{{ plugin }}</label>
{% endfor %}
</div>
</div>
{% endif %}
{% endif %}
</div>
{% if isEditable %}
<div class="editor-toolbar" id="editor-toolbar"></div>
<div class="editor-wrapper">
<textarea name="content" id="editor-textarea" data-ext="{{ fileExt }}">{{ fileContent }}</textarea>
</div>
{% endif %}
<div class="d-flex gap-2 mt-3">
<button type="submit" class="btn btn-primary" title="Opslaan (Ctrl+S)">
<i class="bi bi-check-lg"></i> Opslaan
</button>
{% if isEditable %}
<a href="/{{ currentLang }}{% if fileDir %}/{{ fileDir }}{% endif %}/{{ fileName|replace({'.md': '', '.php': '', '.html': ''}) }}" target="_blank" class="btn btn-outline-info" title="Open in nieuw tabblad">
<i class="bi bi-eye"></i> Preview
</a>
{% endif %}
<a href="/admin/content?dir={{ fileDir|url_encode }}" class="btn btn-outline-secondary" id="back-btn">Terug</a>
</div>
</form>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function () {
var backBtn = document.getElementById('back-btn');
var filenameInput = document.getElementById('filename');
var layoutSelect = document.getElementById('layout');
if (!backBtn) return;
var changed = false;
function markChanged() {
if (changed) return;
changed = true;
backBtn.innerHTML = '<i class="bi bi-x-circle"></i> Annuleren';
backBtn.classList.remove('btn-outline-secondary');
backBtn.classList.add('btn-outline-danger');
}
if (filenameInput) {
filenameInput.addEventListener('input', markChanged);
}
// Update the layout value in the editor's frontmatter when the select changes
if (layoutSelect) {
layoutSelect.addEventListener('change', function () {
var newLayout = this.value;
var editor = document.getElementById('editor-textarea');
if (!editor) return;
var cm = window.codeMirrorEditor;
var content = cm ? cm.getValue() : editor.value;
// Check if frontmatter exists
var fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
if (fmMatch) {
// Update existing layout line in frontmatter
var frontmatter = fmMatch[1];
if (/^layout:\s*.+$/m.test(frontmatter)) {
frontmatter = frontmatter.replace(/^layout:\s*.+$/m, 'layout: ' + newLayout);
} else {
frontmatter = 'layout: ' + newLayout + '\n' + frontmatter;
}
content = content.replace(/^---\n([\s\S]*?)\n---/, '---\n' + frontmatter + '\n---');
} else {
// No frontmatter yet — add one
content = '---\nlayout: ' + newLayout + '\n---\n\n' + content;
}
if (cm) {
cm.setValue(content);
} else {
editor.value = content;
}
markChanged();
});
}
window.__onContentChange = markChanged;
});
</script>
{% endblock %}
@@ -0,0 +1,30 @@
{% extends "layouts/admin.twig" %}
{% block title %}{{ isDir ? 'Map' : 'Bestand' }} verplaatsen - CodePress Admin{% endblock %}
{% block content %}
<h2 class="mb-4"><i class="bi bi-arrows-move"></i> {{ isDir ? 'Map' : 'Bestand' }} verplaatsen</h2>
<form method="post" class="card shadow-sm">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<div class="card-body">
<div class="alert alert-info">
Verplaats <strong>{{ itemName }}</strong> naar:
</div>
<div class="mb-3">
<label for="destination" class="form-label">Doelmap</label>
<select class="form-select" id="destination" name="destination" required>
{% for directory in directories %}
<option value="{{ directory }}">{{ directory }}</option>
{% endfor %}
</select>
</div>
</div>
<div class="card-footer bg-white">
<button type="submit" class="btn btn-primary">
<i class="bi bi-check-lg"></i> Verplaatsen
</button>
<a href="/admin/content?dir={{ itemDir|url_encode }}" class="btn btn-outline-secondary">Annuleren</a>
</div>
</form>
{% endblock %}
@@ -0,0 +1,42 @@
{% extends "layouts/admin.twig" %}
{% block title %}Nieuwe pagina - CodePress Admin{% endblock %}
{% block content %}
<h2 class="mb-4"><i class="bi bi-plus-lg"></i> Nieuwe pagina</h2>
<div class="card shadow-sm">
<div class="card-body">
<form method="POST" action="/admin/content-new?dir={{ dir|url_encode }}" id="editor-form" data-new-page>
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<div class="mb-3">
<label for="filename" class="form-label">Bestandsnaam</label>
<input type="text" class="form-control" id="filename" name="filename" required autofocus>
<small class="form-text text-muted">Alleen letters, cijfers, punten, underscores en streepjes.</small>
</div>
<div class="mb-3">
<label for="extension" class="form-label">Bestandstype</label>
<select class="form-select" id="extension" name="extension">
{% for ext, label in availableExtensions %}
<option value="{{ ext }}">{{ label }}</option>
{% endfor %}
</select>
</div>
<div class="mb-3">
<label for="layout" class="form-label">Sjabloon / Layout <small class="text-muted">({{ activeThemeName|default('default') }} theme)</small></label>
<select class="form-select" id="layout" name="layout">
{% for key, layoutFile in themeLayouts %}
<option value="{{ key }}" {{ key == themeDefaultLayout ? 'selected' : '' }}>{{ key|replace({'_': ' '})|capitalize }}{% if key == themeDefaultLayout %} (standaard){% endif %}</option>
{% endfor %}
</select>
</div>
<div class="d-flex gap-2">
<button type="submit" class="btn btn-primary">
<i class="bi bi-check-lg"></i> Aanmaken
</button>
<a href="/admin/content?dir={{ dir|url_encode }}" class="btn btn-outline-secondary">Annuleren</a>
</div>
</form>
</div>
</div>
{% endblock %}
@@ -0,0 +1,209 @@
{% extends "layouts/admin.twig" %}
{% block title %}Content - CodePress Admin{% endblock %}
{% block content %}
<div class="d-flex justify-content-between align-items-center mb-4">
<h2><i class="bi bi-file-earmark-text"></i> Content</h2>
<div>
<button type="button" class="btn btn-outline-success btn-sm me-1" data-bs-toggle="collapse" data-bs-target="#uploadForm">
<i class="bi bi-cloud-upload"></i> Upload
</button>
<button type="button" class="btn btn-outline-secondary btn-sm me-1" data-bs-toggle="modal" data-bs-target="#createDirModal">
<i class="bi bi-folder-plus"></i> Nieuwe map
</button>
<a href="/admin/content-new?dir={{ subdir|url_encode }}" class="btn btn-primary btn-sm">
<i class="bi bi-plus-lg"></i> Nieuw bestand
</a>
</div>
</div>
<div class="collapse mb-4" id="uploadForm">
<div class="card shadow-sm">
<div class="card-body">
<form method="POST" action="/admin/content?dir={{ subdir|url_encode }}" enctype="multipart/form-data">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<div class="mb-3">
<label for="file" class="form-label">Bestanden selecteren</label>
<input type="file" class="form-control" id="file" name="file[]" multiple accept="image/jpeg,image/png,image/gif,image/webp,image/svg+xml,application/pdf,application/zip,video/mp4,video/webm,audio/mpeg,audio/wav">
<small class="form-text text-muted">Toegestaan: JPG, PNG, GIF, WebP, SVG, PDF, ZIP, MP4, WebM, MP3, WAV</small>
</div>
<button type="submit" class="btn btn-success">
<i class="bi bi-cloud-upload"></i> Uploaden naar deze map
</button>
</form>
</div>
</div>
</div>
{% if subdir %}
{% set parentDir = subdir|split('/')|slice(0, -1)|join('/') %}
<nav aria-label="breadcrumb" class="mb-3">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="/admin/content"><i class="bi bi-house"></i></a></li>
{% set crumbPath = '' %}
{% for crumb in subdir|split('/') %}
{% set crumbPath = crumbPath ? crumbPath ~ '/' ~ crumb : crumb %}
<li class="breadcrumb-item {{ crumbPath == subdir ? 'active' : '' }}">
{% if crumbPath == subdir %}
{{ crumb }}
{% else %}
<a href="/admin/content?dir={{ crumbPath|url_encode }}">{{ crumb }}</a>
{% endif %}
</li>
{% endfor %}
</ol>
</nav>
{% endif %}
<div class="card shadow-sm">
<div class="card-header bg-white py-2">
<div class="input-group input-group-sm">
<span class="input-group-text bg-white border-end-0"><i class="bi bi-search text-muted"></i></span>
<input type="search" id="contentFilter" class="form-control border-start-0" placeholder="Filter op bestands- of mapnaam…" autocomplete="off" aria-label="Filter content">
<span class="input-group-text bg-white text-muted" id="contentFilterCount"></span>
</div>
</div>
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead>
<tr>
<th>Naam</th>
<th>Type</th>
<th>Grootte</th>
<th>Gewijzigd</th>
<th style="width: 200px;">Acties</th>
</tr>
</thead>
<tbody>
{% if items is empty %}
<tr><td colspan="5" class="text-muted text-center py-4">Geen bestanden gevonden.</td></tr>
{% else %}
{% for item in items %}
<tr data-name="{{ item.name|lower }}">
<td>
{% if item.is_dir %}
<a href="/admin/content?dir={{ item.path|url_encode }}">
<i class="bi bi-folder-fill text-warning"></i> {{ item.name }}
</a>
{% else %}
<a href="/admin/content-edit?file={{ item.path|url_encode }}">
{% set icon = item.extension == 'md' ? 'bi-file-text text-primary' : (item.extension == 'php' ? 'bi-file-code text-success' : (item.extension == 'html' ? 'bi-file-earmark text-info' : 'bi-file text-muted')) %}
<i class="bi {{ icon }}"></i> {{ item.name }}
</a>
{% endif %}
</td>
<td>
{% if item.is_dir %}
<span class="badge bg-warning text-dark">Map</span>
{% else %}
<span class="badge bg-secondary">{{ item.extension|upper }}</span>
{% endif %}
</td>
<td class="text-muted">{{ item.size }}</td>
<td class="text-muted">{{ item.modified }}</td>
<td>
{% if item.is_dir %}
<a href="/admin/content-dir-rename?dir={{ item.path|url_encode }}" class="btn btn-sm btn-outline-secondary" title="Hernoemen">
<i class="bi bi-pencil"></i>
</a>
<a href="/admin/content-move?item={{ item.path|url_encode }}" class="btn btn-sm btn-outline-info" title="Verplaatsen">
<i class="bi bi-arrows-move"></i>
</a>
<form method="POST" action="/admin/content-dir-delete?dir={{ item.path|url_encode }}" class="d-inline" onsubmit="return confirm('Weet je zeker dat je deze map wilt verwijderen? De map moet leeg zijn.')">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<button type="submit" class="btn btn-sm btn-outline-danger" title="Verwijderen">
<i class="bi bi-trash"></i>
</button>
</form>
{% else %}
<a href="/admin/content-edit?file={{ item.path|url_encode }}" class="btn btn-sm btn-outline-primary" title="Bewerken">
<i class="bi bi-pencil"></i>
</a>
<a href="/admin/content-move?item={{ item.path|url_encode }}" class="btn btn-sm btn-outline-info" title="Verplaatsen">
<i class="bi bi-arrows-move"></i>
</a>
<form method="POST" action="/admin/content-delete?file={{ item.path|url_encode }}" class="d-inline" onsubmit="return confirm('Weet je zeker dat je dit bestand wilt verwijderen?')">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<button type="submit" class="btn btn-sm btn-outline-danger" title="Verwijderen">
<i class="bi bi-trash"></i>
</button>
</form>
{% endif %}
</td>
</tr>
{% endfor %}
{% endif %}
<tr id="contentFilterEmpty" class="d-none">
<td colspan="5" class="text-muted text-center py-4">Geen resultaten voor deze filter.</td>
</tr>
</tbody>
</table>
</div>
</div>
<script>
(function () {
var input = document.getElementById('contentFilter');
var counter = document.getElementById('contentFilterCount');
var emptyRow = document.getElementById('contentFilterEmpty');
if (!input) return;
var rows = Array.prototype.slice.call(document.querySelectorAll('tbody tr[data-name]'));
function apply() {
var q = input.value.trim().toLowerCase();
var shown = 0;
rows.forEach(function (row) {
var match = q === '' || row.getAttribute('data-name').indexOf(q) !== -1;
row.classList.toggle('d-none', !match);
if (match) shown++;
});
if (emptyRow) {
emptyRow.classList.toggle('d-none', shown > 0 || rows.length === 0);
}
if (counter) {
counter.textContent = q === '' ? rows.length + ' items' : shown + ' van ' + rows.length;
}
}
input.addEventListener('input', apply);
input.addEventListener('keydown', function (e) {
if (e.key === 'Escape') {
input.value = '';
apply();
}
});
apply();
})();
</script>
<!-- Create Directory Modal -->
<div class="modal fade" id="createDirModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<form method="POST" action="/admin/content-dir-create?dir={{ subdir|url_encode }}">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<div class="modal-header">
<h5 class="modal-title"><i class="bi bi-folder-plus"></i> Nieuwe map aanmaken</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label for="dirname" class="form-label">Mapnaam</label>
<input type="text" class="form-control" id="dirname" name="dirname" required autofocus>
<div class="form-text">Alleen letters, cijfers, punten, underscores en streepjes.</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Annuleren</button>
<button type="submit" class="btn btn-primary"><i class="bi bi-check-lg"></i> Aanmaken</button>
</div>
</form>
</div>
</div>
</div>
{% endblock %}
@@ -0,0 +1,219 @@
{% extends "layouts/admin.twig" %}
{% block title %}Dashboard - CodePress Admin{% endblock %}
{% block content %}
<h2 class="mb-4"><i class="bi bi-speedometer2"></i> Dashboard</h2>
<div class="alert alert-light border mb-4">
<strong>Welkom, {{ user.username }}</strong> — Ingelogd als <span class="badge bg-{{ user_role == 'admin' ? 'danger' : (user_role == 'content-manager' ? 'primary' : (user_role == 'bi-manager' ? 'success' : 'warning')) }}">{{ role_label(user_role) }}</span>
</div>
{# Analytics stats - only for roles with statistics permission #}
{% if has_permission('statistics') %}
<div class="row g-4 mb-4">
<div class="col-md-4">
<div class="card stat-card shadow-sm">
<div class="card-body d-flex justify-content-between align-items-center">
<div>
<h6 class="text-muted mb-1">Weergaven (30 dagen)</h6>
<h3 class="mb-0">{{ (analytics_summary.totals.views ?? 0)|number_format(0, ',', '.') }}</h3>
</div>
<i class="bi bi-eye stat-icon text-primary"></i>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card stat-card shadow-sm">
<div class="card-body d-flex justify-content-between align-items-center">
<div>
<h6 class="text-muted mb-1">Unieke bezoekers (30 dagen)</h6>
<h3 class="mb-0">{{ (analytics_summary.totals.uniques ?? 0)|number_format(0, ',', '.') }}</h3>
</div>
<i class="bi bi-people stat-icon text-success"></i>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card stat-card shadow-sm">
<div class="card-body d-flex justify-content-between align-items-center">
<div>
<h6 class="text-muted mb-1">Grootste land</h6>
<h3 class="mb-0">
{{ get_country_flag(analytics_summary.countries|keys|first) }}
<span class="fs-5">{{ get_country_name(analytics_summary.countries|keys|first) }}</span>
</h3>
</div>
<a href="/admin/statistics" class="btn btn-sm btn-outline-secondary">Statistieken →</a>
</div>
</div>
</div>
</div>
{% endif %}
{# Content stats - only for roles with content permission #}
{% if has_permission('content') %}
<div class="row g-4 mb-4">
<div class="col-md-3">
<div class="card stat-card shadow-sm">
<div class="card-body d-flex justify-content-between align-items-center">
<div>
<h6 class="text-muted mb-1">Pagina's</h6>
<h3 class="mb-0">{{ stats.pages }}</h3>
</div>
<i class="bi bi-file-earmark-text stat-icon text-primary"></i>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card stat-card shadow-sm">
<div class="card-body d-flex justify-content-between align-items-center">
<div>
<h6 class="text-muted mb-1">Mappen</h6>
<h3 class="mb-0">{{ stats.directories }}</h3>
</div>
<i class="bi bi-folder stat-icon text-warning"></i>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card stat-card shadow-sm">
<div class="card-body d-flex justify-content-between align-items-center">
<div>
<h6 class="text-muted mb-1">Content grootte</h6>
<h3 class="mb-0">{{ stats.content_size }}</h3>
</div>
<i class="bi bi-hdd stat-icon text-info"></i>
</div>
</div>
</div>
</div>
{% endif %}
{# System stats - only for admin/site-admin #}
{% if has_permission('plugins') or has_permission('config') %}
<div class="row g-4 mb-4">
{% if has_permission('plugins') %}
<div class="col-md-3">
<div class="card stat-card shadow-sm">
<div class="card-body d-flex justify-content-between align-items-center">
<div>
<h6 class="text-muted mb-1">Plugins</h6>
<h3 class="mb-0">{{ stats.plugins }}</h3>
</div>
<i class="bi bi-plug stat-icon text-success"></i>
</div>
</div>
</div>
{% endif %}
</div>
{% endif %}
<div class="row g-4">
{# Site information - only for admin #}
{% if has_permission('config') %}
<div class="col-md-6">
<div class="card shadow-sm">
<div class="card-header"><i class="bi bi-info-circle"></i> Site informatie</div>
<div class="card-body">
<table class="table table-sm mb-0">
<tr><td class="text-muted">Site titel</td><td>{{ site_config.site_title|default('CodePress') }}</td></tr>
<tr><td class="text-muted">Standaard taal</td><td>{{ site_config.language.default|default('nl') }}</td></tr>
<tr><td class="text-muted">Auteur</td><td>{{ site_config.author.name|default('-') }}</td></tr>
<tr><td class="text-muted">CodePress versie</td><td>{{ stats.cms_version }}</td></tr>
<tr><td class="text-muted">PHP versie</td><td>{{ stats.php_version }}</td></tr>
<tr><td class="text-muted">Besturingssysteem</td><td>{{ stats.os }}</td></tr>
<tr><td class="text-muted">Config geladen</td><td>{% if stats.config_exists %}<span class="badge bg-success">Ja</span>{% else %}<span class="badge bg-danger">Nee</span>{% endif %}</td></tr>
</table>
</div>
</div>
</div>
{% endif %}
{# Recent activity - only for roles with logs permission #}
{% if has_permission('logs') %}
<div class="col-md-6">
<div class="card shadow-sm">
<div class="card-header d-flex justify-content-between align-items-center">
<span><i class="bi bi-activity"></i> Recente activiteit</span>
<a href="/admin/logs?tab=admin" class="btn btn-sm btn-outline-secondary">Bekijk alle →</a>
</div>
<div class="card-body" style="max-height: 300px; overflow-y: auto;">
{% if recent_logs is empty %}
<p class="text-muted mb-0">Geen activiteit geregistreerd.</p>
{% else %}
<ul class="list-unstyled mb-0">
{% for log in recent_logs %}
<li class="mb-2 pb-2 border-bottom small">
<span class="text-muted">{{ log.time }}</span>
<span class="badge bg-{{ log.level == 'warning' ? 'warning text-dark' : (log.level == 'error' ? 'danger' : 'info') }} me-1">{{ log.level }}</span>
<code class="text-muted">{{ log.ip }}</code>
{{ log.message }}
</li>
{% endfor %}
</ul>
{% endif %}
</div>
</div>
</div>
<div class="col-md-6">
<div class="card shadow-sm">
<div class="card-header d-flex justify-content-between align-items-center">
<span><i class="bi bi-globe"></i> Recente requests</span>
<a href="/admin/logs?tab=requests" class="btn btn-sm btn-outline-secondary">Bekijk alle →</a>
</div>
<div class="card-body" style="max-height: 300px; overflow-y: auto;">
{% if recent_requests is empty %}
<p class="text-muted mb-0">Geen requests geregistreerd.</p>
{% else %}
<ul class="list-unstyled mb-0">
{% for log in recent_requests %}
<li class="mb-2 pb-2 border-bottom small d-flex justify-content-between align-items-center">
<div>
<span class="text-muted me-1">{{ log.time }}</span>
<code class="text-muted me-1">{{ log.ip }}</code>
<span class="fw-bold">{{ log.page }}</span>
</div>
{% if log.visitor_info is not empty %}
<span class="badge bg-{{ log.visitor_info.badge }}" title="{{ log.ua }}">
<i class="bi {{ log.visitor_info.icon }}"></i> {{ log.visitor_info.label }}
</span>
{% endif %}
</li>
{% endfor %}
</ul>
{% endif %}
</div>
</div>
</div>
{% endif %}
{# Quick actions - role-specific #}
<div class="col-md-6">
<div class="card shadow-sm">
<div class="card-header"><i class="bi bi-lightning"></i> Snelle acties</div>
<div class="card-body">
<div class="d-grid gap-2">
{% if has_permission('content') %}
<a href="/admin/content-new" class="btn btn-outline-primary"><i class="bi bi-plus-lg"></i> Nieuwe pagina</a>
<a href="/admin/content" class="btn btn-outline-info"><i class="bi bi-folder2-open"></i> Content beheren</a>
{% endif %}
{% if has_permission('config') %}
<a href="/admin/config" class="btn btn-outline-secondary"><i class="bi bi-sliders"></i> Configuratie bewerken</a>
{% endif %}
{% if has_permission('statistics') %}
<a href="/admin/statistics" class="btn btn-outline-secondary"><i class="bi bi-bar-chart"></i> Statistieken bekijken</a>
{% endif %}
{% if has_permission('theme') %}
<a href="/admin/theme" class="btn btn-outline-secondary"><i class="bi bi-palette"></i> Thema beheren</a>
{% endif %}
{% if has_permission('plugins') %}
<a href="/admin/plugins" class="btn btn-outline-secondary"><i class="bi bi-plug"></i> Plugins beheren</a>
{% endif %}
<a href="/" target="_blank" class="btn btn-outline-success"><i class="bi bi-box-arrow-up-right"></i> Website bekijken</a>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
@@ -0,0 +1,14 @@
{% extends "layouts/admin.twig" %}
{% block title %}{{ error_title|default('Fout') }} - CodePress Admin{% endblock %}
{% block content %}
<div class="text-center py-5">
<h1 class="display-1 text-muted">{{ error_code|default('404') }}</h1>
<h2 class="mb-3">{{ error_title|default('Pagina niet gevonden') }}</h2>
<p class="text-muted mb-4">{{ error_message|default('De gevraagde pagina kon niet worden gevonden.') }}</p>
<a href="/admin/dashboard" class="btn btn-primary">
<i class="bi bi-house"></i> Naar dashboard
</a>
</div>
{% endblock %}
@@ -0,0 +1,52 @@
{% extends "layouts/admin.twig" %}
{% block title %}{{ guide_page ? 'Handleiding - ' : '' }}Handleiding{% endblock %}
{% block content %}
<div class="row">
{% if guide_nav %}
<aside class="col-md-3 mb-3">
<div class="card shadow-sm">
<div class="card-header">
<h5 class="mb-0"><i class="bi bi-list-ul"></i> Navigatie</h5>
</div>
<div class="card-body">
{{ guide_nav|raw }}
</div>
</div>
</aside>
<div class="col-md-9">
{% else %}
<div class="col-12">
{% endif %}
<nav aria-label="breadcrumb" class="mb-4">
<ol class="breadcrumb">
<li class="breadcrumb-item {{ not guide_page ? 'active' : '' }}">
{% if guide_page %}
<a href="/admin/guide{% if guide_lang %}?lang={{ guide_lang }}{% endif %}">Handleiding</a>
{% else %}
Handleidingen
{% endif %}
</li>
{% if guide_breadcrumbs %}
{% for crumb in guide_breadcrumbs %}
{% if loop.last %}
<li class="breadcrumb-item active">{{ crumb.title }}</li>
{% else %}
<li class="breadcrumb-item">
<a href="/admin/guide?lang={{ guide_lang }}&page={{ crumb.url }}">{{ crumb.title }}</a>
</li>
{% endif %}
{% endfor %}
{% endif %}
</ol>
</nav>
<div class="guide-content card shadow-sm">
<div class="card-body">
{{ content|raw }}
</div>
</div>
</div>
</div>
{% endblock %}
+62
View File
@@ -0,0 +1,62 @@
{% extends "layouts/admin.twig" %}
{% block title %}Logs - CodePress Admin{% endblock %}
{% block content %}
<h2 class="mb-4"><i class="bi bi-journal-text"></i> Logs</h2>
<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-auto">
<div class="btn-group" role="group">
<a href="/admin/logs?tab=admin" class="btn btn-sm {{ tab == 'admin' ? 'btn-primary' : 'btn-outline-primary' }}">
<i class="bi bi-shield-check"></i> Admin
</a>
<a href="/admin/logs?tab=requests" class="btn btn-sm {{ tab == 'requests' ? 'btn-primary' : 'btn-outline-primary' }}">
<i class="bi bi-globe"></i> Requests
</a>
</div>
</div>
<div class="col-auto">
<a href="/admin/logs?tab={{ tab }}&download=1" class="btn btn-sm btn-outline-secondary">
<i class="bi bi-download"></i> Download
</a>
</div>
</form>
</div>
</div>
<div class="card shadow-sm">
<div class="card-body p-0" style="max-height: 600px; overflow-y: auto;">
<table class="table table-sm table-hover mb-0">
<thead class="sticky-top bg-white">
<tr>
<th>Tijd</th>
<th>Level</th>
<th>IP</th>
<th>Bericht</th>
</tr>
</thead>
<tbody>
{% for log in logs %}
<tr>
<td class="text-muted">{{ log.time }}</td>
<td>
<span class="badge bg-{{ log.level == 'warning' ? 'warning text-dark' : (log.level == 'error' ? 'danger' : 'info') }}">
{{ log.level }}
</span>
</td>
<td class="text-muted">{{ log.ip }}</td>
<td><code class="text-muted">{{ log.message }}</code></td>
</tr>
{% else %}
<tr>
<td colspan="4" class="text-muted text-center py-4">Geen logs gevonden.</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endblock %}
@@ -0,0 +1,52 @@
{% extends "layouts/admin.twig" %}
{% block title %}Media - CodePress Admin{% endblock %}
{% block content %}
<div class="d-flex justify-content-between align-items-center mb-4">
<h2><i class="bi bi-images"></i> Media</h2>
<button type="button" class="btn btn-primary btn-sm" data-bs-toggle="collapse" data-bs-target="#uploadForm">
<i class="bi bi-cloud-upload"></i> Uploaden
</button>
</div>
<div class="collapse mb-4" id="uploadForm">
<div class="card shadow-sm">
<div class="card-body">
<form method="POST" action="/admin/media?dir={{ subdir|url_encode }}" enctype="multipart/form-data">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<div class="mb-3">
<label for="file" class="form-label">Bestanden selecteren</label>
<input type="file" class="form-control" id="file" name="file[]" multiple accept="image/*,video/*,audio/*">
</div>
<button type="submit" class="btn btn-success">
<i class="bi bi-cloud-upload"></i> Uploaden
</button>
</form>
</div>
</div>
</div>
<div class="card shadow-sm">
<div class="card-body">
<div class="row g-4">
{% for item in items %}
{% if not item.is_dir and item.extension in ['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg'] %}
<div class="col-md-3">
<div class="card shadow-sm">
<img src="/content/{{ item.path|url_encode }}" class="card-img-top" alt="{{ item.name }}">
<div class="card-body p-2">
<p class="card-text small text-muted text-truncate">{{ item.name }}</p>
</div>
</div>
</div>
{% endif %}
{% else %}
<div class="col-12">
<p class="text-muted text-center">Geen media bestanden gevonden.</p>
</div>
{% endfor %}
</div>
</div>
</div>
{% endblock %}
@@ -0,0 +1,20 @@
{% extends "layouts/admin.twig" %}
{% block title %}Plugin Configuratie: {{ pluginName }} - CodePress Admin{% endblock %}
{% block content %}
<h2 class="mb-4"><i class="bi bi-plug"></i> Plugin Configuratie: {{ pluginName }}</h2>
<form method="post" class="card shadow-sm">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<div class="card-body">
<textarea name="config" id="config-textarea" class="form-control font-monospace" rows="20">{{ pluginConfig }}</textarea>
</div>
<div class="card-footer bg-white">
<button type="submit" class="btn btn-primary">
<i class="bi bi-check-lg"></i> Opslaan
</button>
<a href="/admin/plugins" class="btn btn-outline-secondary">Annuleren</a>
</div>
</form>
{% endblock %}
@@ -0,0 +1,38 @@
{% extends "layouts/admin.twig" %}
{% block title %}Plugin bewerken: {{ pluginName }} - CodePress Admin{% endblock %}
{% block extra_css %}
<link rel="stylesheet" href="/admin/assets/codemirror/codemirror.min.css">
<link rel="stylesheet" href="/admin/assets/css/editor.css">
{% endblock %}
{% block content %}
<div class="d-flex justify-content-between align-items-center mb-4">
<h2><i class="bi bi-pencil"></i> Plugin bewerken: {{ pluginName }}</h2>
<a href="/admin/plugins" class="btn btn-outline-secondary btn-sm">
<i class="bi bi-arrow-left"></i> Terug
</a>
</div>
<form method="POST" action="/admin/plugins-edit?plugin={{ pluginName|url_encode }}">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<div class="card shadow-sm">
<div class="card-body">
<div class="editor-toolbar" id="editor-toolbar"></div>
<div class="editor-wrapper">
<textarea name="content" id="editor-textarea" data-ext="php">{{ pluginContent }}</textarea>
</div>
</div>
</div>
<div class="d-flex gap-2 mt-3">
<button type="submit" class="btn btn-primary">
<i class="bi bi-check-lg"></i> Opslaan
</button>
<a href="/admin/plugins" class="btn btn-outline-secondary">Annuleren</a>
</div>
</form>
{% endblock %}
{% block extra_js %}
{% endblock %}
@@ -0,0 +1,24 @@
{% extends "layouts/admin.twig" %}
{% block title %}Nieuwe plugin - CodePress Admin{% endblock %}
{% block content %}
<h2 class="mb-4"><i class="bi bi-plug"></i> Nieuwe plugin aanmaken</h2>
<form method="post" class="card shadow-sm">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<div class="card-body">
<div class="mb-3">
<label for="name" class="form-label">Plugin naam</label>
<input type="text" class="form-control" id="name" name="name" required autofocus>
<small class="form-text text-muted">Alleen letters, cijfers, underscores en streepjes.</small>
</div>
</div>
<div class="card-footer bg-white">
<button type="submit" class="btn btn-primary">
<i class="bi bi-check-lg"></i> Aanmaken
</button>
<a href="/admin/plugins" class="btn btn-outline-secondary">Annuleren</a>
</div>
</form>
{% endblock %}
@@ -0,0 +1,70 @@
{% extends "layouts/admin.twig" %}
{% block title %}Plugins - CodePress Admin{% endblock %}
{% block content %}
<div class="d-flex justify-content-between align-items-center mb-4">
<h2><i class="bi bi-plug"></i> Plugins</h2>
<a href="/admin/plugins-new" class="btn btn-primary btn-sm">
<i class="bi bi-plus-lg"></i> Nieuwe plugin
</a>
</div>
<div class="row g-4">
{% for plugin in plugins %}
<div class="col-md-6 col-lg-4">
<div class="card shadow-sm h-100">
<div class="card-header d-flex justify-content-between align-items-center">
<span class="fw-bold">{{ plugin.name|default(plugin.name) }}</span>
<span class="badge bg-{{ plugin.enabled ? 'success' : 'secondary' }}">{{ plugin.enabled ? 'Actief' : 'Inactief' }}</span>
</div>
<div class="card-body">
<p class="card-text text-muted mb-2">{{ plugin.description|default('Geen beschrijving') }}</p>
<p class="small text-muted mb-3">
{% if plugin.version %}<span class="badge bg-info">v{{ plugin.version }}</span>{% endif %}
{% if plugin.author %}<span class="badge bg-secondary">{{ plugin.author }}</span>{% endif %}
</p>
</div>
<div class="card-footer bg-white">
<div class="btn-group w-100" role="group">
{% if plugin.protected %}
<span class="btn btn-sm btn-outline-secondary disabled" title="Essentiële plugin - kan niet worden bewerkt, gedeactiveerd of verwijderd">
<i class="bi bi-shield-check"></i> Essentieel
</span>
{% else %}
<a href="/admin/plugins-edit?plugin={{ plugin.name|url_encode }}" class="btn btn-sm btn-outline-primary">
<i class="bi bi-pencil"></i> Bewerken
</a>
{% if plugin.hasConfig %}
<a href="/admin/plugins-config?plugin={{ plugin.name|url_encode }}" class="btn btn-sm btn-outline-info">
<i class="bi bi-gear"></i> Config
</a>
{% endif %}
<form method="POST" action="/admin/plugins-toggle" class="d-inline">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<input type="hidden" name="plugin" value="{{ plugin.name }}">
<button type="submit" class="btn btn-sm btn-{{ plugin.enabled ? 'outline-warning' : 'outline-success' }}">
<i class="bi bi-{{ plugin.enabled ? 'pause' : 'play' }}"></i> {{ plugin.enabled ? 'Deactiveren' : 'Activeren' }}
</button>
</form>
<form method="POST" action="/admin/plugins-delete" class="d-inline" onsubmit="return confirm('Weet je zeker dat je deze plugin wilt verwijderen?')">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<input type="hidden" name="plugin" value="{{ plugin.name }}">
<button type="submit" class="btn btn-sm btn-outline-danger">
<i class="bi bi-trash"></i>
</button>
</form>
{% endif %}
</div>
</div>
</div>
</div>
{% else %}
<div class="col-12">
<div class="alert alert-info">
<i class="bi bi-info-circle"></i> Geen plugins gevonden. Maak een nieuwe plugin aan om te beginnen.
</div>
</div>
{% endfor %}
</div>
{% endblock %}
@@ -0,0 +1,44 @@
{% extends "layouts/admin.twig" %}
{% block title %}Beveiliging - CodePress Admin{% endblock %}
{% block content %}
<h2 class="mb-4"><i class="bi bi-shield-check"></i> Beveiliging & Bot Bescherming</h2>
<form method="POST" action="/admin/security">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<div class="card shadow-sm mb-4">
<div class="card-header">Bot Bescherming</div>
<div class="card-body">
<div class="form-check form-switch mb-3">
<input class="form-check-input" type="checkbox" id="botguard_enabled" name="botguard_enabled" {{ config.security.botguard_enabled ?? true ? 'checked' : '' }}>
<label class="form-check-label" for="botguard_enabled">BotGuard ingeschakeld</label>
</div>
<div class="form-check form-switch mb-3">
<input class="form-check-input" type="checkbox" id="block_bad_bots" name="block_bad_bots" {{ config.security.block_bad_bots ?? true ? 'checked' : '' }}>
<label class="form-check-label" for="block_bad_bots">Blokkeer slechte bots</label>
</div>
</div>
</div>
<div class="card shadow-sm mb-4">
<div class="card-header">Sessie Instellingen</div>
<div class="card-body">
<div class="mb-3">
<label for="session_timeout" class="form-label">Sessie timeout (seconden)</label>
<input type="number" class="form-control" id="session_timeout" name="session_timeout" value="{{ config.security.session_timeout ?? 3600 }}">
</div>
<div class="mb-3">
<label for="max_login_attempts" class="form-label">Maximale login pogingen</label>
<input type="number" class="form-control" id="max_login_attempts" name="max_login_attempts" value="{{ config.security.max_login_attempts ?? 5 }}">
</div>
</div>
</div>
<button type="submit" class="btn btn-primary">
<i class="bi bi-check-lg"></i> Opslaan
</button>
<a href="/admin/dashboard" class="btn btn-outline-secondary">Annuleren</a>
</form>
{% endblock %}
@@ -0,0 +1,80 @@
{% extends "layouts/admin.twig" %}
{% block title %}Statistieken - CodePress Admin{% endblock %}
{% block content %}
<h2 class="mb-4"><i class="bi bi-bar-chart"></i> Statistieken</h2>
<div class="row g-4 mb-4">
<div class="col-md-4">
<div class="card stat-card shadow-sm">
<div class="card-body d-flex justify-content-between align-items-center">
<div>
<h6 class="text-muted mb-1">Totaal aantal views</h6>
<h3 class="mb-0">{{ (stats.totals.views ?? 0)|number_format(0, ',', '.') }}</h3>
</div>
<i class="bi bi-eye stat-icon text-primary"></i>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card stat-card shadow-sm">
<div class="card-body d-flex justify-content-between align-items-center">
<div>
<h6 class="text-muted mb-1">Unieke bezoekers</h6>
<h3 class="mb-0">{{ (stats.totals.uniques ?? 0)|number_format(0, ',', '.') }}</h3>
</div>
<i class="bi bi-people stat-icon text-success"></i>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card stat-card shadow-sm">
<div class="card-body d-flex justify-content-between align-items-center">
<div>
<h6 class="text-muted mb-1">Pagina views</h6>
<h3 class="mb-0">{{ (stats.pages|length ?? 0) }}</h3>
</div>
<i class="bi bi-file-earmark-text stat-icon text-info"></i>
</div>
</div>
</div>
</div>
<div class="row g-4">
<div class="col-md-6">
<div class="card shadow-sm">
<div class="card-header"><i class="bi bi-globe"></i> Landen</div>
<div class="card-body">
<table class="table table-sm mb-0">
{% for country, count in stats.countries|slice(0, 10) %}
<tr>
<td>{{ get_country_flag(country) }} {{ get_country_name(country) }}</td>
<td class="text-end">{{ count|number_format(0, ',', '.') }}</td>
</tr>
{% else %}
<tr><td colspan="2" class="text-muted">Geen data</td></tr>
{% endfor %}
</table>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card shadow-sm">
<div class="card-header"><i class="bi bi-file-text"></i> Top pagina's</div>
<div class="card-body">
<table class="table table-sm mb-0">
{% for page, count in stats.pages|slice(0, 10) %}
<tr>
<td><code class="text-muted">{{ page }}</code></td>
<td class="text-end">{{ count|number_format(0, ',', '.') }}</td>
</tr>
{% else %}
<tr><td colspan="2" class="text-muted">Geen data</td></tr>
{% endfor %}
</table>
</div>
</div>
</div>
</div>
{% endblock %}
@@ -0,0 +1,24 @@
{% extends "layouts/admin.twig" %}
{% block title %}Nieuw thema - CodePress Admin{% endblock %}
{% block content %}
<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="{{ csrf_token }}">
<div class="card-body">
<div class="mb-3">
<label for="name" class="form-label">Thema naam</label>
<input type="text" class="form-control" id="name" name="name" required autofocus>
<small class="form-text text-muted">Alleen letters, cijfers, underscores en streepjes.</small>
</div>
</div>
<div class="card-footer bg-white">
<button type="submit" class="btn btn-primary">
<i class="bi bi-check-lg"></i> Aanmaken
</button>
<a href="/admin/theme" class="btn btn-outline-secondary">Annuleren</a>
</div>
</form>
{% endblock %}
@@ -0,0 +1,66 @@
{% extends "layouts/admin.twig" %}
{% block title %}Thema's - CodePress Admin{% endblock %}
{% block content %}
<div class="d-flex justify-content-between align-items-center mb-4">
<h2><i class="bi bi-palette"></i> Thema's</h2>
<div>
<form method="POST" action="/admin/theme" class="d-inline">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<button type="submit" name="activate_default" class="btn btn-primary btn-sm">
<i class="bi bi-check-lg"></i> Activeer Default
</button>
</form>
<a href="/admin/theme-new" class="btn btn-outline-primary btn-sm ms-2">
<i class="bi bi-plus-lg"></i> Nieuw thema
</a>
</div>
</div>
<div class="row g-4">
{% for theme in themes %}
<div class="col-md-6 col-lg-4">
<div class="card shadow-sm h-100 {{ theme.active ? 'border-primary border-2' : '' }}">
<div class="card-header d-flex justify-content-between align-items-center">
<span class="fw-bold">{{ theme.title|default(theme.name) }}</span>
{% if theme.active %}
<span class="badge bg-success">Actief</span>
{% endif %}
</div>
<div class="card-body">
<p class="text-muted small">Naam: {{ theme.name }}</p>
{% if theme.default_layout %}
<p class="text-muted small">Default layout: {{ theme.default_layout }}</p>
{% endif %}
</div>
<div class="card-footer bg-white">
{% if not theme.active %}
<form method="POST" action="/admin/theme" class="d-inline">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<input type="hidden" name="activate" value="{{ theme.name }}">
<button type="submit" class="btn btn-sm btn-outline-primary">
<i class="bi bi-check-lg"></i> Activeren
</button>
</form>
{% endif %}
<form method="POST" action="/admin/theme" class="d-inline">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<input type="hidden" name="compile_scss" value="1">
<input type="hidden" name="theme" value="{{ theme.name }}">
<button type="submit" class="btn btn-sm btn-outline-success">
<i class="bi bi-palette"></i> SCSS compileren
</button>
</form>
</div>
</div>
</div>
{% else %}
<div class="col-12">
<div class="alert alert-info">
<i class="bi bi-info-circle"></i> Geen thema's gevonden.
</div>
</div>
{% endfor %}
</div>
{% endblock %}
@@ -0,0 +1,35 @@
{% extends "layouts/admin.twig" %}
{% block title %}Update - CodePress Admin{% endblock %}
{% block content %}
<h2 class="mb-4"><i class="bi bi-cloud-arrow-down"></i> Systeem Update</h2>
{% if isGitWritable == false %}
<div class="alert alert-warning mb-4">
<i class="bi bi-exclamation-triangle-fill me-1"></i>
De .git map is niet beschrijfbaar. Automatische updates zijn niet mogelijk.
</div>
{% endif %}
<div class="card shadow-sm mb-4">
<div class="card-header">Huidige versie</div>
<div class="card-body">
<p class="mb-0">CodePress versie: <strong>{{ version }}</strong></p>
</div>
</div>
<div class="card shadow-sm">
<div class="card-header">Update opties</div>
<div class="card-body">
<div class="d-grid gap-2">
<button class="btn btn-primary" {{ isGitWritable ? '' : 'disabled' }}>
<i class="bi bi-cloud-arrow-down"></i> Controleer op updates
</button>
<button class="btn btn-outline-secondary" {{ isGitWritable ? '' : 'disabled' }}>
<i class="bi bi-arrow-repeat"></i> Update uitvoeren
</button>
</div>
</div>
</div>
{% endblock %}
+138
View File
@@ -0,0 +1,138 @@
{% extends "layouts/admin.twig" %}
{% block title %}Gebruikers - CodePress Admin{% endblock %}
{% block content %}
<h2 class="mb-4"><i class="bi bi-people"></i> Gebruikers</h2>
<div class="row g-4">
<div class="col-md-7">
<div class="card shadow-sm">
<div class="card-header">
<i class="bi bi-list"></i> Gebruikers
</div>
<div class="card-body p-0">
<table class="table table-hover mb-0">
<thead>
<tr>
<th>Gebruikersnaam</th>
<th>Rol</th>
<th>Aangemaakt</th>
<th>Acties</th>
</tr>
</thead>
<tbody>
{% for username, data in users %}
<tr>
<td>
<i class="bi bi-person-circle"></i>
{{ username }}
{% if username == user.username %}
<span class="badge bg-info">Jij</span>
{% endif %}
</td>
<td>
<span class="badge bg-{{ data.role == 'admin' ? 'danger' : (data.role == 'content-manager' ? 'primary' : (data.role == 'bi-manager' ? 'success' : 'warning')) }}">
{{ data.role_label|default(data.role) }}
</span>
</td>
<td class="text-muted">{{ data.created|default('Onbekend') }}</td>
<td>
{% if username != user.username %}
<button type="button" class="btn btn-sm btn-outline-secondary" data-bs-toggle="modal" data-bs-target="#roleModal-{{ username }}">
<i class="bi bi-person-gear"></i> Rol
</button>
<form method="POST" action="/admin/users" class="d-inline" onsubmit="return confirm('Weet je zeker dat je gebruiker {{ username }} wilt verwijderen?')">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<input type="hidden" name="action" value="delete">
<input type="hidden" name="delete_username" value="{{ username }}">
<button type="submit" class="btn btn-sm btn-outline-danger">
<i class="bi bi-trash"></i>
</button>
</form>
<!-- Role change modal -->
<div class="modal fade" id="roleModal-{{ username }}" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<form method="POST" action="/admin/users">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<input type="hidden" name="action" value="change_role">
<input type="hidden" name="role_username" value="{{ username }}">
<div class="modal-header">
<h5 class="modal-title"><i class="bi bi-person-gear"></i> Rol wijzigen: {{ username }}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label class="form-label">Huidige rol</label>
<p><span class="badge bg-secondary">{{ data.role_label|default(data.role) }}</span></p>
</div>
<div class="mb-3">
<label for="new_role-{{ username }}" class="form-label">Nieuwe rol</label>
<select class="form-select" id="new_role-{{ username }}" name="new_role">
{% for roleKey, roleLabel in roles %}
<option value="{{ roleKey }}" {{ data.role == roleKey ? 'selected' : '' }}>{{ roleLabel }}</option>
{% endfor %}
</select>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Annuleren</button>
<button type="submit" class="btn btn-primary"><i class="bi bi-check-lg"></i> Wijzigen</button>
</div>
</form>
</div>
</div>
</div>
{% else %}
<span class="text-muted small">Eigen account</span>
{% endif %}
</td>
</tr>
{% else %}
<tr>
<td colspan="4" class="text-muted text-center py-4">Geen gebruikers gevonden.</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
<div class="col-md-5">
<div class="card shadow-sm">
<div class="card-header">
<i class="bi bi-plus-circle"></i> Nieuwe gebruiker
</div>
<div class="card-body">
<form method="POST" action="/admin/users">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<input type="hidden" name="action" value="add">
<div class="mb-3">
<label for="new_username" class="form-label">Gebruikersnaam</label>
<input type="text" class="form-control" id="new_username" name="new_username" required autofocus>
</div>
<div class="mb-3">
<label for="new_password" class="form-label">Wachtwoord</label>
<input type="password" class="form-control" id="new_password" name="new_password" required>
<small class="form-text text-muted">Minimaal 8 tekens.</small>
</div>
<div class="mb-3">
<label for="new_role" class="form-label">Rol</label>
<select class="form-select" id="new_role" name="new_role">
{% for roleKey, roleLabel in roles %}
<option value="{{ roleKey }}" {{ roleKey == 'content-manager' ? 'selected' : '' }}>{{ roleLabel }}</option>
{% endfor %}
</select>
</div>
<button type="submit" class="btn btn-primary w-100">
<i class="bi bi-check-lg"></i> Gebruiker toevoegen
</button>
</form>
</div>
</div>
</div>
</div>
{% endblock %}