v2.6.0: Content backup/git versioning, plugin type system, docs update

New features:
- ContentBackup class with ZIP backup/restore and git versioning
- Admin backup & restore page (content-backup.twig) with git init/commit/log/restore
- Plugin type system: system (blue) vs content (green) with visual badges
- PluginAPIInterface + AdminPluginAPI for plugin architecture
- Essential plugin flag (cannot edit/deactivate/delete)

Improvements:
- Consolidated enabled_plugins config (removed plugins.enabled)
- Removed Analytics/Logging toggles from admin config page
- Fixed Dashboard plugin Twig comments rendered as text
- Updated 20 guide files (NL+EN): configuratie, plugins, plugin-development,
  core-classes, theme-json, layouts, scss-styling, admin-beheerder, nieuw-thema, architectuur
- Improved accessibility test script (grep -E, min/max checks)

Cleanup:
- Removed unused classes: ARIAComponents, AccessibilityManager, ContentSecurityPolicy, etc.
- Removed vendor packages: mustache/mustache, php-mqtt/client
- Removed old templates: logs.twig, statistics.twig (now plugins)
- Moved language files to language/ directory

Tests:
- Pentest: 30/30 passed, 0 vulnerabilities
- WCAG 2.1 AA: 25/25 passed, 100% compliance
This commit is contained in:
2026-08-15 19:21:04 +02:00
parent 3a55ea4db6
commit 20adea7544
208 changed files with 3763 additions and 22688 deletions
+21
View File
@@ -0,0 +1,21 @@
{
"users": [
{
"username": "admin",
"password_hash": "$2y$12$CV2uORTIiie6s3jf9ANaneL7VLNqA5smXyrIzkcJFjlS2Vxfw1/HK",
"role": "admin",
"email": "",
"author_name": "",
"author_email": "",
"created": "2025-01-01"
}
],
"security": {
"session_timeout": 1800,
"max_login_attempts": 5,
"lockout_duration": 900
},
"config": {
"default_page": "auto"
}
}
+39 -6
View File
@@ -16,7 +16,7 @@ class AdminAuth
*/
public const ROLE_PERMISSIONS = [
'admin' => ['*'],
'content-manager' => ['dashboard', 'content', 'content-edit', 'content-new', 'content-delete', 'content-dir-create', 'content-dir-rename', 'content-dir-delete', 'content-move', 'guide', 'logout'],
'content-manager' => ['dashboard', 'content', 'content-edit', 'content-new', 'content-delete', 'content-dir-create', 'content-dir-rename', 'content-dir-delete', 'content-move', 'content-backup', 'content-restore', 'content-git-init', 'content-git-commit', 'content-git-restore', 'guide', 'logout'],
'bi-manager' => ['dashboard', 'statistics', 'logs', 'guide', 'logout'],
'site-admin' => ['dashboard', 'theme', 'theme-new', 'plugins', 'plugins-new', 'plugins-edit', 'plugins-config', 'plugins-toggle', 'plugins-delete', 'statistics', 'logs', 'update', 'guide', 'logout'],
];
@@ -28,7 +28,7 @@ class AdminAuth
'admin' => 'Admin',
'content-manager' => 'Content Beheerder',
'bi-manager' => 'BI Beheerder',
'site-admin' => ' Site Admin',
'site-admin' => 'Site Admin',
];
public function __construct(array $appConfig)
@@ -170,10 +170,19 @@ class AdminAuth
if (!$this->isAuthenticated()) {
return null;
}
return [
'username' => $_SESSION['admin_user'],
$username = $_SESSION['admin_user'];
$userData = [
'username' => $username,
'role' => $_SESSION['admin_role'] ?? 'admin'
];
// Enrich with profile fields from admin.json
$userEntry = $this->findUser($username);
if ($userEntry) {
$userData['email'] = $userEntry['email'] ?? '';
$userData['author_name'] = $userEntry['author_name'] ?? '';
$userData['author_email'] = $userEntry['author_email'] ?? '';
}
return $userData;
}
/**
@@ -244,13 +253,16 @@ class AdminAuth
'username' => $u['username'],
'role' => $role,
'role_label' => self::getRoleLabel($role),
'created' => $u['created'] ?? ''
'created' => $u['created'] ?? '',
'email' => $u['email'] ?? '',
'author_name' => $u['author_name'] ?? '',
'author_email' => $u['author_email'] ?? '',
];
}
return $users;
}
public function addUser(string $username, string $password, string $role = 'admin'): array
public function addUser(string $username, string $password, string $role = 'admin', string $email = '', string $authorName = '', string $authorEmail = ''): array
{
if ($this->findUser($username)) {
return ['success' => false, 'message' => 'Gebruiker bestaat al.'];
@@ -266,6 +278,9 @@ class AdminAuth
'username' => $username,
'password_hash' => password_hash($password, PASSWORD_DEFAULT),
'role' => $role,
'email' => $email,
'author_name' => $authorName,
'author_email' => $authorEmail,
'created' => date('Y-m-d')
];
$this->saveAdminConfig();
@@ -273,6 +288,24 @@ class AdminAuth
return ['success' => true, 'message' => 'Gebruiker aangemaakt.'];
}
/**
* Update the profile (email, author_name, author_email) of a user.
*/
public function updateUserProfile(string $username, string $email = '', string $authorName = '', string $authorEmail = ''): array
{
foreach ($this->adminConfig['users'] as &$userEntry) {
if ($userEntry['username'] === $username) {
$userEntry['email'] = $email;
$userEntry['author_name'] = $authorName;
$userEntry['author_email'] = $authorEmail;
$this->saveAdminConfig();
$this->log('info', "Profiel bijgewerkt: {$username}");
return ['success' => true, 'message' => 'Profiel opgeslagen.'];
}
}
return ['success' => false, 'message' => 'Gebruiker niet gevonden.'];
}
/**
* Change the role of an existing user.
*/
+36 -39
View File
@@ -1,9 +1,9 @@
<!DOCTYPE html>
<html lang="nl">
<html lang="{{ admin_lang|default('nl') }}">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}CodePress Admin{% endblock %}</title>
<title>{% block title %}{{ ta.admin_title|default('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">
@@ -37,98 +37,95 @@
<body>
<nav class="admin-sidebar d-flex flex-column">
<div class="admin-brand">
<i class="bi bi-gear-fill"></i> CodePress Admin
<i class="bi bi-gear-fill"></i> {{ ta.admin_title|default('CodePress Admin') }}
</div>
<ul class="nav flex-column mt-2">
<li class="nav-section">Algemeen</li>
{# Algemene sectie: Dashboard (plugin) + plugin items met section=general #}
{% set general_plugins = plugin_admin_menu|filter(item => (item.section|default('general')) == 'general') %}
{% if general_plugins is not empty or has_permission('dashboard') or true %}
<li class="nav-section">{{ ta.section_general|default('Algemeen') }}</li>
{% for item in general_plugins %}
<li class="nav-item">
<a class="nav-link {{ route == 'dashboard' or route == '' ? 'active' : '' }}" href="/admin/dashboard">
<i class="bi bi-speedometer2"></i> Dashboard
<a class="nav-link {{ route == item.route or route starts with item.route ~ '/' ? 'active' : '' }}" href="/admin/{{ item.route }}">
<i class="bi {{ item.icon|default('bi-puzzle') }}"></i> {{ item.label|default(item.route) }}
</a>
</li>
{% endfor %}
{% endif %}
{% if has_permission('content') %}
<li class="nav-section">Content</li>
<li class="nav-section">{{ ta.section_content|default('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
<i class="bi bi-file-earmark-text"></i> {{ ta.content|default('Content') }}
</a>
</li>
{% endif %}
{% if has_permission('config') or has_permission('theme') or has_permission('security') %}
<li class="nav-section">Instellingen</li>
<li class="nav-section">{{ ta.section_settings|default('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
<i class="bi bi-sliders"></i> {{ ta.configuration|default('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
<i class="bi bi-palette"></i> {{ ta.theme|default('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
<i class="bi bi-shield-check"></i> {{ ta.security|default('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>
{# Systeem sectie: core admin items + plugin items met section=system #}
{% set system_plugins = plugin_admin_menu|filter(item => (item.section|default('general')) == 'system') %}
{% if has_permission('plugins') or has_permission('users') or has_permission('update') or system_plugins is not empty %}
<li class="nav-section">{{ ta.section_system|default('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
<i class="bi bi-plug"></i> {{ ta.plugins|default('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
<i class="bi bi-people"></i> {{ ta.users|default('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
<i class="bi bi-cloud-arrow-down"></i> {{ ta.update|default('Update') }}
</a>
</li>
{% endif %}
{% for item in system_plugins %}
<li class="nav-item">
<a class="nav-link {{ route == item.route or route starts with item.route ~ '/' ? 'active' : '' }}" href="/admin/{{ item.route }}">
<i class="bi {{ item.icon|default('bi-puzzle') }}"></i> {{ item.label|default(item.route) }}
</a>
</li>
{% endfor %}
{% endif %}
{% if has_permission('guide') %}
<li class="nav-section">Help</li>
<li class="nav-section">{{ ta.section_help|default('Help') }}</li>
<li class="nav-item">
<a class="nav-link {{ route == 'guide' ? 'active' : '' }}" href="/admin/guide">
<i class="bi bi-book"></i> Handleiding
<i class="bi bi-book"></i> {{ ta.guide|default('Handleiding') }}
</a>
</li>
{% endif %}
@@ -137,12 +134,12 @@
<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
<i class="bi bi-box-arrow-up-right"></i> {{ ta.view_website|default('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
<i class="bi bi-box-arrow-left"></i> {{ ta.logout|default('Uitloggen') }}
</a>
</li>
</ul>
+8 -8
View File
@@ -1,9 +1,9 @@
<!DOCTYPE html>
<html lang="nl">
<html lang="{{ admin_lang|default('nl') }}">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>CodePress Admin - Login</title>
<title>{{ ta.login_title|default('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">
@@ -17,7 +17,7 @@
<div class="container">
<div class="login-card">
<div class="login-header">
<h4><i class="bi bi-shield-lock"></i> CodePress Admin</h4>
<h4><i class="bi bi-shield-lock"></i> {{ ta.admin_title|default('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">
@@ -30,14 +30,14 @@
<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>
<label for="username" class="form-label">{{ ta.username_label|default('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>
<label for="password" class="form-label">{{ ta.password_label|default('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>
@@ -45,17 +45,17 @@
</div>
<div class="d-grid">
<button type="submit" class="btn btn-primary">
<i class="bi bi-box-arrow-in-right"></i> Inloggen
<i class="bi bi-box-arrow-in-right"></i> {{ ta.login_btn|default('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>
<a href="/" class="text-decoration-none"><i class="bi bi-arrow-left"></i> {{ ta.back_to_website|default('Terug naar website') }}</a>
</p>
</div>
</div>
<script src="/admin/assets/js/bootstrap.bundle.min.js"></script>
</body>
</html>
</html>
+87 -39
View File
@@ -1,63 +1,111 @@
{% extends "layouts/admin.twig" %}
{% block title %}Configuratie - CodePress Admin{% endblock %}
{% block title %}{{ ta.configuration|default('Configuratie') }} - {{ ta.admin_title|default('CodePress Admin') }}{% endblock %}
{% block content %}
<h2 class="mb-4"><i class="bi bi-sliders"></i> Configuratie</h2>
<h2 class="mb-4"><i class="bi bi-sliders"></i> {{ ta.configuration|default('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-header">{{ ta.section_settings|default('Instellingen') }}</div>
<div class="card-body">
<div class="mb-3">
<label for="site_title" class="form-label">Site titel</label>
<label for="site_title" class="form-label">{{ ta.site_title|default('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">
<label for="admin_language" class="form-label">{{ ta.admin_language|default('Admin taal') }}</label>
<select class="form-select" id="admin_language" name="admin_language">
<option value="nl" {{ config.admin_language|default('nl') == 'nl' ? 'selected' : '' }}>Nederlands</option>
<option value="en" {{ config.admin_language == 'en' ? 'selected' : '' }}>English</option>
<option value="de" {{ config.admin_language == 'de' ? 'selected' : '' }}>Deutsch</option>
</select>
<div class="form-text">{{ ta.admin_language_help|default('Taal van het admin-paneel (menu, knoppen, labels).') }}</div>
</div>
<div class="mb-3">
<label for="content_language" class="form-label">{{ ta.content_language|default('Content taal') }}</label>
<select class="form-select" id="content_language" name="content_language">
<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>
<option value="en" {{ config.language.default == 'en' ? 'selected' : '' }}>English</option>
<option value="de" {{ config.language.default == 'de' ? 'selected' : '' }}>Deutsch</option>
<option value="fr" {{ config.language.default == 'fr' ? 'selected' : '' }}>Français</option>
</select>
<div class="form-text">{{ ta.content_language_help|default('Standaardtaal van de website-content (fallback als er geen taal in de URL staat).') }}</div>
</div>
</div>
</div>
<div class="card shadow-sm mb-4">
<div class="card-header">{{ ta.homepage|default('Homepage') }}</div>
<div class="card-body">
<p class="text-muted small mb-3">{{ ta.homepage_help|default('Bepaal welke pagina getoond wordt op de homepage.') }}</p>
<div class="mb-3">
<label class="form-label">{{ ta.homepage_mode|default('Homepage-modus') }}</label>
<div class="form-check">
<input class="form-check-input" type="radio" name="default_page" id="dp_auto" value="auto" {{ current_default_page == 'auto' ? 'checked' : '' }}>
<label class="form-check-label" for="dp_auto">{{ ta.homepage_auto|default('Automatisch — eerste beschikbare pagina') }}</label>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" name="default_page" id="dp_newest" value="newest" {{ current_default_page == 'newest' ? 'checked' : '' }}>
<label class="form-check-label" for="dp_newest">{{ ta.homepage_newest|default('Meest recent aangepaste pagina') }}</label>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" name="default_page" id="dp_specific" value="specific" {{ current_default_page not in ['auto', 'newest'] ? 'checked' : '' }}>
<label class="form-check-label" for="dp_specific">{{ ta.homepage_specific|default('Specifieke pagina') }}</label>
</div>
</div>
<div class="mb-3" id="specific_page_wrapper" style="display: none;">
<label for="default_page_specific" class="form-label">{{ ta.homepage_select|default('Selecteer pagina') }}</label>
<select class="form-select" id="default_page_specific" name="default_page_specific">
{% for pageKey, pageLabel in content_pages %}
<option value="{{ pageKey }}" {{ current_default_page == pageKey ? 'selected' : '' }}>{{ pageLabel }}</option>
{% else %}
<option value="" disabled>{{ ta.no_pages_found|default('Geen pagina\'s gevonden in content/') }}</option>
{% endfor %}
</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
<i class="bi bi-check-lg"></i> {{ ta.save|default('Opslaan') }}
</button>
<a href="/admin/dashboard" class="btn btn-outline-secondary">Annuleren</a>
<a href="/admin/dashboard" class="btn btn-outline-secondary">{{ ta.cancel|default('Annuleren') }}</a>
</form>
<script>
(function () {
var radios = document.querySelectorAll('input[name="default_page"]');
var wrapper = document.getElementById('specific_page_wrapper');
var specificSelect = document.getElementById('default_page_specific');
function toggleWrapper() {
var checked = document.querySelector('input[name="default_page"]:checked');
if (checked && checked.value === 'specific') {
wrapper.style.display = '';
} else {
wrapper.style.display = 'none';
}
}
radios.forEach(function (r) {
r.addEventListener('change', toggleWrapper);
});
// If the specific select changes but the radio isn't on "specific", switch to it
if (specificSelect) {
specificSelect.addEventListener('change', function () {
var specRadio = document.getElementById('dp_specific');
if (specRadio && !specRadio.checked) {
specRadio.checked = true;
toggleWrapper();
}
});
}
toggleWrapper();
})();
</script>
{% endblock %}
@@ -0,0 +1,128 @@
{% extends "layouts/admin.twig" %}
{% block title %}{{ ta.backup_restore|default('Backup & Restore') }} - {{ ta.admin_title|default('CodePress Admin') }}{% endblock %}
{% block content %}
<div class="d-flex justify-content-between align-items-center mb-4">
<h2><i class="bi bi-archive"></i> {{ ta.backup_restore|default('Backup & Restore') }}</h2>
<a href="/admin/content" class="btn btn-outline-secondary btn-sm">
<i class="bi bi-arrow-left"></i> {{ ta.back_to_content|default('Terug naar content') }}
</a>
</div>
{% if message %}
<div class="alert alert-{{ message_type }} alert-dismissible fade show" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
{% endif %}
<div class="row g-4">
<div class="col-md-6">
<div class="card shadow-sm">
<div class="card-header"><i class="bi bi-file-zip"></i> {{ ta.zip_backup|default('ZIP Backup') }}</div>
<div class="card-body">
<p class="text-muted">{{ ta.zip_backup_help|default('Download de volledige content-map als ZIP bestand.') }}</p>
<form method="POST" action="/admin/content-backup">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<input type="hidden" name="action" value="download_zip">
<button type="submit" class="btn btn-primary">
<i class="bi bi-download"></i> {{ ta.download_zip|default('Download ZIP') }}
</button>
</form>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card shadow-sm">
<div class="card-header"><i class="bi bi-upload"></i> {{ ta.restore_from_zip|default('Herstellen uit ZIP') }}</div>
<div class="card-body">
<p class="text-muted">{{ ta.restore_help|default('Upload een eerder gedownloade ZIP bestand om content te herstellen. De huidige content wordt eerst geback-upt.') }}</p>
<form method="POST" action="/admin/content-restore" enctype="multipart/form-data">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<div class="mb-3">
<label for="zipfile" class="form-label">{{ ta.select_zip|default('ZIP bestand selecteren') }}</label>
<input type="file" class="form-control" id="zipfile" name="zipfile" accept=".zip" required>
</div>
<button type="submit" class="btn btn-warning" onclick="return confirm('{{ ta.confirm_restore|default('Weet je zeker dat je de content wilt herstellen? Huidige content wordt geback-upt.') }}')">
<i class="bi bi-arrow-counterclockwise"></i> {{ ta.restore|default('Herstellen') }}
</button>
</form>
</div>
</div>
</div>
</div>
<hr class="my-4">
<div class="card shadow-sm">
<div class="card-header d-flex justify-content-between align-items-center">
<span><i class="bi bi-git"></i> {{ ta.git_versioning|default('Git Versioning') }}</span>
{% if git_available and not has_git_repo %}
<form method="POST" action="/admin/content-git-init" class="d-inline">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<button type="submit" class="btn btn-sm btn-outline-success">
<i class="bi bi-check2-circle"></i> {{ ta.git_init|default('Git init') }}
</button>
</form>
{% endif %}
</div>
<div class="card-body">
{% if not git_available %}
<div class="alert alert-secondary mb-0">
<i class="bi bi-info-circle"></i> {{ ta.git_not_available|default('Git is niet beschikbaar op deze server. Gebruik de ZIP backup/restore opties hierboven.') }}
</div>
{% elseif not has_git_repo %}
<div class="alert alert-info mb-0">
<i class="bi bi-info-circle"></i> {{ ta.git_not_initialized|default('Geen git repository in content/. Klik op "Git init" om versiebeheer te starten.') }}
</div>
{% else %}
<form method="POST" action="/admin/content-git-commit" class="mb-4">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<div class="input-group">
<input type="text" class="form-control" name="commit_message" placeholder="{{ ta.commit_message_placeholder|default('Commit bericht...') }}" maxlength="200">
<button type="submit" class="btn btn-success">
<i class="bi bi-check-lg"></i> {{ ta.commit|default('Commit') }}
</button>
</div>
</form>
{% if git_commits is empty %}
<p class="text-muted">{{ ta.no_commits|default('Nog geen commits. Maak een eerste commit aan.') }}</p>
{% else %}
<div class="table-responsive">
<table class="table table-sm table-hover">
<thead>
<tr>
<th>{{ ta.commit_short|default('Commit') }}</th>
<th>{{ ta.date|default('Datum') }}</th>
<th>{{ ta.message|default('Bericht') }}</th>
<th>{{ ta.actions|default('Acties') }}</th>
</tr>
</thead>
<tbody>
{% for commit in git_commits %}
<tr>
<td><code>{{ commit.short }}</code></td>
<td class="text-muted small">{{ commit.date }}</td>
<td>{{ commit.message }}</td>
<td>
<form method="POST" action="/admin/content-git-restore" class="d-inline" onsubmit="return confirm('{{ ta.confirm_git_restore|default('Weet je zeker dat je de content wilt herstellen naar deze commit?') }}')">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<input type="hidden" name="commit" value="{{ commit.hash }}">
<button type="submit" class="btn btn-sm btn-outline-warning" title="{{ ta.restore_this|default('Herstellen naar deze commit') }}">
<i class="bi bi-arrow-counterclockwise"></i>
</button>
</form>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endif %}
{% endif %}
</div>
</div>
{% endblock %}
@@ -1,23 +1,23 @@
{% extends "layouts/admin.twig" %}
{% block title %}Map hernoemen - CodePress Admin{% endblock %}
{% block title %}{{ ta.rename_folder|default('Map hernoemen') }} - {{ ta.admin_title|default('CodePress Admin') }}{% endblock %}
{% block content %}
<h2 class="mb-4"><i class="bi bi-pencil"></i> Map hernoemen</h2>
<h2 class="mb-4"><i class="bi bi-pencil"></i> {{ ta.rename_folder|default('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>
<label for="newname" class="form-label">{{ ta.new_name_for|default('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
<i class="bi bi-check-lg"></i> {{ ta.rename|default('Hernoemen') }}
</button>
<a href="/admin/content?dir={{ dir|url_encode }}" class="btn btn-outline-secondary">Annuleren</a>
<a href="/admin/content?dir={{ dir|url_encode }}" class="btn btn-outline-secondary">{{ ta.cancel|default('Annuleren') }}</a>
</div>
</form>
{% endblock %}
{% endblock %}
@@ -1,6 +1,6 @@
{% extends "layouts/admin.twig" %}
{% block title %}{{ fileName }} - CodePress Admin{% endblock %}
{% block title %}{{ fileName }} - {{ ta.admin_title|default('CodePress Admin') }}{% endblock %}
{% block content %}
<h2 class="mb-4"><i class="bi bi-pencil"></i> {{ fileName }}</h2>
@@ -11,16 +11,16 @@
<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>
<label for="filename" class="form-label">{{ ta.filename|default('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>
<small class="form-text text-muted">{{ ta.name_help|default('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>
<label for="layout" class="form-label">{{ ta.template_layout|default('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>
@@ -29,7 +29,7 @@
</div>
{% if availablePlugins is not empty %}
<div class="col-md-4">
<label class="form-label d-block">Zichtbare plugins</label>
<label class="form-label d-block">{{ ta.visible_plugins|default('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' : '' }}>
@@ -47,15 +47,15 @@
</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 type="submit" class="btn btn-primary" title="{{ ta.save_ctrl_s|default('Opslaan (Ctrl+S)') }}">
<i class="bi bi-check-lg"></i> {{ ta.save|default('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 href="/{{ currentLang }}{% if fileDir %}/{{ fileDir }}{% endif %}/{{ fileName|replace({'.md': '', '.php': '', '.html': ''}) }}" target="_blank" class="btn btn-outline-info" title="{{ ta.open_new_tab|default('Open in nieuw tabblad') }}">
<i class="bi bi-eye"></i> {{ ta.preview|default('Preview') }}
</a>
{% endif %}
<a href="/admin/content?dir={{ fileDir|url_encode }}" class="btn btn-outline-secondary" id="back-btn">Terug</a>
<a href="/admin/content?dir={{ fileDir|url_encode }}" class="btn btn-outline-secondary" id="back-btn">{{ ta.back|default('Terug') }}</a>
</div>
</form>
</div>
@@ -121,4 +121,4 @@ document.addEventListener('DOMContentLoaded', function () {
window.__onContentChange = markChanged;
});
</script>
{% endblock %}
{% endblock %}
@@ -1,18 +1,18 @@
{% extends "layouts/admin.twig" %}
{% block title %}{{ isDir ? 'Map' : 'Bestand' }} verplaatsen - CodePress Admin{% endblock %}
{% block title %}{% if isDir %}{{ ta.folder|default('Map') }}{% else %}{{ ta.file|default('Bestand') }}{% endif %} {{ ta.move_suffix|default('verplaatsen') }} - {{ ta.admin_title|default('CodePress Admin') }}{% endblock %}
{% block content %}
<h2 class="mb-4"><i class="bi bi-arrows-move"></i> {{ isDir ? 'Map' : 'Bestand' }} verplaatsen</h2>
<h2 class="mb-4"><i class="bi bi-arrows-move"></i> {% if isDir %}{{ ta.folder|default('Map') }}{% else %}{{ ta.file|default('Bestand') }}{% endif %} {{ ta.move_suffix|default('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:
{{ ta.move_prefix|default('Verplaats') }} <strong>{{ itemName }}</strong> {{ ta.move_to|default('naar:') }}
</div>
<div class="mb-3">
<label for="destination" class="form-label">Doelmap</label>
<label for="destination" class="form-label">{{ ta.target_folder|default('Doelmap') }}</label>
<select class="form-select" id="destination" name="destination" required>
{% for directory in directories %}
<option value="{{ directory }}">{{ directory }}</option>
@@ -22,9 +22,9 @@
</div>
<div class="card-footer bg-white">
<button type="submit" class="btn btn-primary">
<i class="bi bi-check-lg"></i> Verplaatsen
<i class="bi bi-check-lg"></i> {{ ta.move|default('Verplaatsen') }}
</button>
<a href="/admin/content?dir={{ itemDir|url_encode }}" class="btn btn-outline-secondary">Annuleren</a>
<a href="/admin/content?dir={{ itemDir|url_encode }}" class="btn btn-outline-secondary">{{ ta.cancel|default('Annuleren') }}</a>
</div>
</form>
{% endblock %}
{% endblock %}
@@ -1,21 +1,21 @@
{% extends "layouts/admin.twig" %}
{% block title %}Nieuwe pagina - CodePress Admin{% endblock %}
{% block title %}{{ ta.new_page|default('Nieuwe pagina') }} - {{ ta.admin_title|default('CodePress Admin') }}{% endblock %}
{% block content %}
<h2 class="mb-4"><i class="bi bi-plus-lg"></i> Nieuwe pagina</h2>
<h2 class="mb-4"><i class="bi bi-plus-lg"></i> {{ ta.new_page|default('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>
<label for="filename" class="form-label">{{ ta.filename|default('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>
<small class="form-text text-muted">{{ ta.name_help|default('Alleen letters, cijfers, punten, underscores en streepjes.') }}</small>
</div>
<div class="mb-3">
<label for="extension" class="form-label">Bestandstype</label>
<label for="extension" class="form-label">{{ ta.file_type|default('Bestandstype') }}</label>
<select class="form-select" id="extension" name="extension">
{% for ext, label in availableExtensions %}
<option value="{{ ext }}">{{ label }}</option>
@@ -23,7 +23,7 @@
</select>
</div>
<div class="mb-3">
<label for="layout" class="form-label">Sjabloon / Layout <small class="text-muted">({{ activeThemeName|default('default') }} theme)</small></label>
<label for="layout" class="form-label">{{ ta.template_layout|default('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>
@@ -32,11 +32,11 @@
</div>
<div class="d-flex gap-2">
<button type="submit" class="btn btn-primary">
<i class="bi bi-check-lg"></i> Aanmaken
<i class="bi bi-check-lg"></i> {{ ta.create|default('Aanmaken') }}
</button>
<a href="/admin/content?dir={{ dir|url_encode }}" class="btn btn-outline-secondary">Annuleren</a>
<a href="/admin/content?dir={{ dir|url_encode }}" class="btn btn-outline-secondary">{{ ta.cancel|default('Annuleren') }}</a>
</div>
</form>
</div>
</div>
{% endblock %}
{% endblock %}
+34 -31
View File
@@ -1,19 +1,22 @@
{% extends "layouts/admin.twig" %}
{% block title %}Content - CodePress Admin{% endblock %}
{% block title %}{{ ta.content|default('Content') }} - {{ ta.admin_title|default('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>
<h2><i class="bi bi-file-earmark-text"></i> {{ ta.content|default('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
<i class="bi bi-cloud-upload"></i> {{ ta.upload|default('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
<i class="bi bi-folder-plus"></i> {{ ta.new_folder|default('Nieuwe map') }}
</button>
<a href="/admin/content-backup" class="btn btn-outline-info btn-sm me-1">
<i class="bi bi-archive"></i> {{ ta.backup|default('Backup') }}
</a>
<a href="/admin/content-new?dir={{ subdir|url_encode }}" class="btn btn-primary btn-sm">
<i class="bi bi-plus-lg"></i> Nieuw bestand
<i class="bi bi-plus-lg"></i> {{ ta.new_file|default('Nieuw bestand') }}
</a>
</div>
</div>
@@ -24,12 +27,12 @@
<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>
<label for="file" class="form-label">{{ ta.select_files|default('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>
<small class="form-text text-muted">{{ ta.allowed_types|default('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
<i class="bi bi-cloud-upload"></i> {{ ta.upload_to_folder|default('Uploaden naar deze map') }}
</button>
</form>
</div>
@@ -60,7 +63,7 @@
<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">
<input type="search" id="contentFilter" class="form-control border-start-0" placeholder="{{ ta.filter_placeholder|default('Filter op bestands- of mapnaam…') }}" autocomplete="off" aria-label="{{ ta.filter_content|default('Filter content') }}">
<span class="input-group-text bg-white text-muted" id="contentFilterCount"></span>
</div>
</div>
@@ -68,16 +71,16 @@
<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>
<th>{{ ta.col_name|default('Naam') }}</th>
<th>{{ ta.col_type|default('Type') }}</th>
<th>{{ ta.col_size|default('Grootte') }}</th>
<th>{{ ta.col_modified|default('Gewijzigd') }}</th>
<th style="width: 200px;">{{ ta.col_actions|default('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>
<tr><td colspan="5" class="text-muted text-center py-4">{{ ta.no_files_found|default('Geen bestanden gevonden.') }}</td></tr>
{% else %}
{% for item in items %}
<tr data-name="{{ item.name|lower }}">
@@ -95,7 +98,7 @@
</td>
<td>
{% if item.is_dir %}
<span class="badge bg-warning text-dark">Map</span>
<span class="badge bg-warning text-dark">{{ ta.folder_badge|default('Map') }}</span>
{% else %}
<span class="badge bg-secondary">{{ item.extension|upper }}</span>
{% endif %}
@@ -104,28 +107,28 @@
<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">
<a href="/admin/content-dir-rename?dir={{ item.path|url_encode }}" class="btn btn-sm btn-outline-secondary" title="{{ ta.rename|default('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">
<a href="/admin/content-move?item={{ item.path|url_encode }}" class="btn btn-sm btn-outline-info" title="{{ ta.move|default('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.')">
<form method="POST" action="/admin/content-dir-delete?dir={{ item.path|url_encode }}" class="d-inline" onsubmit="return confirm('{{ ta.confirm_delete_folder|default('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">
<button type="submit" class="btn btn-sm btn-outline-danger" title="{{ ta.delete|default('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">
<a href="/admin/content-edit?file={{ item.path|url_encode }}" class="btn btn-sm btn-outline-primary" title="{{ ta.edit|default('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">
<a href="/admin/content-move?item={{ item.path|url_encode }}" class="btn btn-sm btn-outline-info" title="{{ ta.move|default('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?')">
<form method="POST" action="/admin/content-delete?file={{ item.path|url_encode }}" class="d-inline" onsubmit="return confirm('{{ ta.confirm_delete_file|default('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">
<button type="submit" class="btn btn-sm btn-outline-danger" title="{{ ta.delete|default('Verwijderen') }}">
<i class="bi bi-trash"></i>
</button>
</form>
@@ -135,7 +138,7 @@
{% endfor %}
{% endif %}
<tr id="contentFilterEmpty" class="d-none">
<td colspan="5" class="text-muted text-center py-4">Geen resultaten voor deze filter.</td>
<td colspan="5" class="text-muted text-center py-4">{{ ta.no_filter_results|default('Geen resultaten voor deze filter.') }}</td>
</tr>
</tbody>
</table>
@@ -188,22 +191,22 @@
<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>
<h5 class="modal-title"><i class="bi bi-folder-plus"></i> {{ ta.new_folder_title|default('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>
<label for="dirname" class="form-label">{{ ta.folder_name|default('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 class="form-text">{{ ta.name_help|default('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>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{{ ta.cancel|default('Annuleren') }}</button>
<button type="submit" class="btn btn-primary"><i class="bi bi-check-lg"></i> {{ ta.create|default('Aanmaken') }}</button>
</div>
</form>
</div>
</div>
</div>
{% endblock %}
{% endblock %}
+50 -191
View File
@@ -1,219 +1,78 @@
{% extends "layouts/admin.twig" %}
{% block title %}Dashboard - CodePress Admin{% endblock %}
{% block title %}{{ ta.dashboard|default('Dashboard') }} - {{ ta.admin_title|default('CodePress Admin') }}{% endblock %}
{% block content %}
<h2 class="mb-4"><i class="bi bi-speedometer2"></i> Dashboard</h2>
<h2 class="mb-4"><i class="bi bi-speedometer2"></i> {{ ta.dashboard|default('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>
<strong>{{ ta.welcome|default('Welkom,') }} {{ user.username }}</strong> — {{ ta.logged_in_as|default('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') %}
{# Site information #}
<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-header"><i class="bi bi-info-circle"></i> {{ ta.site_info|default('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>
<tr><td class="text-muted">{{ ta.site_title|default('Site titel') }}</td><td>{{ site_config.site_title|default('CodePress') }}</td></tr>
<tr><td class="text-muted">{{ ta.default_lang|default('Standaard taal') }}</td><td>{{ site_config.language.default|default('nl') }}</td></tr>
<tr><td class="text-muted">{{ ta.cms_version|default('CodePress versie') }}</td><td>{{ stats.cms_version }}</td></tr>
<tr><td class="text-muted">{{ ta.php_version|default('PHP versie') }}</td><td>{{ stats.php_version }}</td></tr>
<tr><td class="text-muted">{{ ta.os|default('Besturingssysteem') }}</td><td>{{ stats.os }}</td></tr>
<tr><td class="text-muted">{{ ta.config_loaded|default('Config geladen') }}</td><td>{% if stats.config_exists %}<span class="badge bg-success">{{ ta.yes|default('Ja') }}</span>{% else %}<span class="badge bg-danger">{{ ta.no|default('Nee') }}</span>{% endif %}</td></tr>
</table>
</div>
</div>
</div>
{# Content information #}
{% if has_permission('content') %}
<div class="col-md-6">
<div class="card shadow-sm">
<div class="card-header"><i class="bi bi-folder2-open"></i> {{ ta.content_info|default('Content informatie') }}</div>
<div class="card-body">
<table class="table table-sm mb-0">
<tr><td class="text-muted">{{ ta.pages|default('Pagina\'s') }}</td><td><span class="badge bg-primary">{{ stats.pages }}</span></td></tr>
<tr><td class="text-muted">{{ ta.folders|default('Mappen') }}</td><td><span class="badge bg-warning text-dark">{{ stats.directories }}</span></td></tr>
<tr><td class="text-muted">{{ ta.content_size|default('Content grootte') }}</td><td>{{ stats.content_size }}</td></tr>
</table>
</div>
</div>
</div>
{% endif %}
{# Recent activity - only for roles with logs permission #}
{% if has_permission('logs') %}
{# Plugins overview #}
{% if has_permission('plugins') %}
<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>
<span><i class="bi bi-plug"></i> {{ ta.plugins|default('Plugins') }}</span>
<a href="/admin/plugins" class="btn btn-sm btn-outline-secondary">{{ ta.manage|default('Beheren') }}</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 class="card-body">
<table class="table table-sm mb-0">
{% for pluginName, pluginInfo in plugin_overview %}
<tr>
<td>
<i class="bi bi-plug-fill"></i> {{ pluginName }}
</td>
<td>
{% if pluginInfo.enabled %}
<span class="badge bg-success">{{ ta.active|default('Actief') }}</span>
{% else %}
<span class="badge bg-secondary">{{ ta.inactive|default('Inactief') }}</span>
{% endif %}
</td>
</tr>
{% else %}
<tr><td colspan="2" class="text-muted text-center">{{ ta.no_plugins|default('Geen plugins gevonden.') }}</td></tr>
{% endfor %}
</table>
</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 %}
{% endblock %}
+4 -4
View File
@@ -1,14 +1,14 @@
{% extends "layouts/admin.twig" %}
{% block title %}{{ error_title|default('Fout') }} - CodePress Admin{% endblock %}
{% block title %}{{ error_title|default(ta.error|default('Fout')) }} - {{ ta.admin_title|default('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>
<h2 class="mb-3">{{ error_title|default(ta.page_not_found|default('Pagina niet gevonden')) }}</h2>
<p class="text-muted mb-4">{{ error_message|default(ta.page_not_found_msg|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
<i class="bi bi-house"></i> {{ ta.to_dashboard|default('Naar dashboard') }}
</a>
</div>
{% endblock %}
+4 -4
View File
@@ -1,6 +1,6 @@
{% extends "layouts/admin.twig" %}
{% block title %}{{ guide_page ? 'Handleiding - ' : '' }}Handleiding{% endblock %}
{% block title %}{{ guide_page ? (ta.guide|default('Handleiding')) ~ ' - ' : '' }}{{ ta.guide|default('Handleiding') }}{% endblock %}
{% block content %}
<div class="row">
@@ -8,7 +8,7 @@
<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>
<h5 class="mb-0"><i class="bi bi-list-ul"></i> {{ ta.navigation|default('Navigatie') }}</h5>
</div>
<div class="card-body">
{{ guide_nav|raw }}
@@ -23,9 +23,9 @@
<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>
<a href="/admin/guide{% if guide_lang %}?lang={{ guide_lang }}{% endif %}">{{ ta.guide|default('Handleiding') }}</a>
{% else %}
Handleidingen
{{ ta.manuals|default('Handleidingen') }}
{% endif %}
</li>
{% if guide_breadcrumbs %}
-62
View File
@@ -1,62 +0,0 @@
{% 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 %}
+7 -7
View File
@@ -1,12 +1,12 @@
{% extends "layouts/admin.twig" %}
{% block title %}Media - CodePress Admin{% endblock %}
{% block title %}{{ ta.media|default('Media') }} - {{ ta.admin_title|default('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>
<h2><i class="bi bi-images"></i> {{ ta.media|default('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
<i class="bi bi-cloud-upload"></i> {{ ta.upload|default('Upload') }}
</button>
</div>
@@ -16,11 +16,11 @@
<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>
<label for="file" class="form-label">{{ ta.select_files|default('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
<i class="bi bi-cloud-upload"></i> {{ ta.upload|default('Uploaden') }}
</button>
</form>
</div>
@@ -43,10 +43,10 @@
{% endif %}
{% else %}
<div class="col-12">
<p class="text-muted text-center">Geen media bestanden gevonden.</p>
<p class="text-muted text-center">{{ ta.no_media|default('Geen media bestanden gevonden.') }}</p>
</div>
{% endfor %}
</div>
</div>
</div>
{% endblock %}
{% endblock %}
@@ -1,9 +1,9 @@
{% extends "layouts/admin.twig" %}
{% block title %}Plugin Configuratie: {{ pluginName }} - CodePress Admin{% endblock %}
{% block title %}{{ ta.plugin_config|default('Plugin Configuratie: ') }}{{ pluginName }} - {{ ta.admin_title|default('CodePress Admin') }}{% endblock %}
{% block content %}
<h2 class="mb-4"><i class="bi bi-plug"></i> Plugin Configuratie: {{ pluginName }}</h2>
<h2 class="mb-4"><i class="bi bi-plug"></i> {{ ta.plugin_config|default('Plugin Configuratie: ') }}{{ pluginName }}</h2>
<form method="post" class="card shadow-sm">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
@@ -12,9 +12,9 @@
</div>
<div class="card-footer bg-white">
<button type="submit" class="btn btn-primary">
<i class="bi bi-check-lg"></i> Opslaan
<i class="bi bi-check-lg"></i> {{ ta.save|default('Opslaan') }}
</button>
<a href="/admin/plugins" class="btn btn-outline-secondary">Annuleren</a>
<a href="/admin/plugins" class="btn btn-outline-secondary">{{ ta.cancel|default('Annuleren') }}</a>
</div>
</form>
{% endblock %}
{% endblock %}
@@ -0,0 +1,7 @@
{% extends "layouts/admin.twig" %}
{% block title %}{{ plugin_title|default('Plugin') }} - {{ ta.admin_title|default('CodePress Admin') }}{% endblock %}
{% block content %}
{{ plugin_content|raw }}
{% endblock %}
@@ -1,6 +1,6 @@
{% extends "layouts/admin.twig" %}
{% block title %}Plugin bewerken: {{ pluginName }} - CodePress Admin{% endblock %}
{% block title %}{{ ta.plugin_edit|default('Plugin bewerken: ') }}{{ pluginName }} - {{ ta.admin_title|default('CodePress Admin') }}{% endblock %}
{% block extra_css %}
<link rel="stylesheet" href="/admin/assets/codemirror/codemirror.min.css">
@@ -9,9 +9,9 @@
{% 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>
<h2><i class="bi bi-pencil"></i> {{ ta.plugin_edit|default('Plugin bewerken: ') }}{{ pluginName }}</h2>
<a href="/admin/plugins" class="btn btn-outline-secondary btn-sm">
<i class="bi bi-arrow-left"></i> Terug
<i class="bi bi-arrow-left"></i> {{ ta.back|default('Terug') }}
</a>
</div>
@@ -27,12 +27,12 @@
</div>
<div class="d-flex gap-2 mt-3">
<button type="submit" class="btn btn-primary">
<i class="bi bi-check-lg"></i> Opslaan
<i class="bi bi-check-lg"></i> {{ ta.save|default('Opslaan') }}
</button>
<a href="/admin/plugins" class="btn btn-outline-secondary">Annuleren</a>
<a href="/admin/plugins" class="btn btn-outline-secondary">{{ ta.cancel|default('Annuleren') }}</a>
</div>
</form>
{% endblock %}
{% block extra_js %}
{% endblock %}
{% endblock %}
@@ -1,24 +1,24 @@
{% extends "layouts/admin.twig" %}
{% block title %}Nieuwe plugin - CodePress Admin{% endblock %}
{% block title %}{{ ta.new_plugin|default('Nieuwe plugin') }} - {{ ta.admin_title|default('CodePress Admin') }}{% endblock %}
{% block content %}
<h2 class="mb-4"><i class="bi bi-plug"></i> Nieuwe plugin aanmaken</h2>
<h2 class="mb-4"><i class="bi bi-plug"></i> {{ ta.new_plugin_title|default('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>
<label for="name" class="form-label">{{ ta.plugin_name|default('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>
<small class="form-text text-muted">{{ ta.plugin_name_help|default('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
<i class="bi bi-check-lg"></i> {{ ta.create|default('Aanmaken') }}
</button>
<a href="/admin/plugins" class="btn btn-outline-secondary">Annuleren</a>
<a href="/admin/plugins" class="btn btn-outline-secondary">{{ ta.cancel|default('Annuleren') }}</a>
</div>
</form>
{% endblock %}
{% endblock %}
+28 -16
View File
@@ -1,25 +1,37 @@
{% extends "layouts/admin.twig" %}
{% block title %}Plugins - CodePress Admin{% endblock %}
{% block title %}{{ ta.plugins|default('Plugins') }} - {{ ta.admin_title|default('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>
<h2><i class="bi bi-plug"></i> {{ ta.plugins|default('Plugins') }}</h2>
<a href="/admin/plugins-new" class="btn btn-primary btn-sm">
<i class="bi bi-plus-lg"></i> Nieuwe plugin
<i class="bi bi-plus-lg"></i> {{ ta.new_plugin|default('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 shadow-sm h-100 {% if plugin.type == 'system' %}border-primary{% elseif plugin.type == 'content' %}border-success{% endif %}">
<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>
<span class="fw-bold">
{% if plugin.type == 'system' %}<i class="bi bi-gear-fill text-primary"></i>
{% elseif plugin.type == 'content' %}<i class="bi bi-puzzle-fill text-success"></i>
{% else %}<i class="bi bi-plug-fill"></i>{% endif %}
{{ plugin.name|default(plugin.name) }}
</span>
<div class="d-flex gap-1">
{% if plugin.type == 'system' %}
<span class="badge bg-primary">{{ ta.plugin_type_system|default('Systeem') }}</span>
{% elseif plugin.type == 'content' %}
<span class="badge bg-success">{{ ta.plugin_type_content|default('Content') }}</span>
{% endif %}
<span class="badge bg-{{ plugin.enabled ? 'success' : 'secondary' }}">{{ plugin.enabled ? ta.active|default('Actief') : ta.inactive|default('Inactief') }}</span>
</div>
</div>
<div class="card-body">
<p class="card-text text-muted mb-2">{{ plugin.description|default('Geen beschrijving') }}</p>
<p class="card-text text-muted mb-2">{{ plugin.description|default(ta.no_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 %}
@@ -27,27 +39,27 @@
</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
{% if plugin.protected or plugin.essential %}
<span class="btn btn-sm btn-outline-secondary disabled" title="{{ ta.essential_title|default('Essentiële plugin - kan niet worden bewerkt, gedeactiveerd of verwijderd') }}">
<i class="bi bi-shield-check"></i> {{ ta.essential|default('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
<i class="bi bi-pencil"></i> {{ ta.edit|default('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
<i class="bi bi-gear"></i> {{ ta.config|default('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' }}
<i class="bi bi-{{ plugin.enabled ? 'pause' : 'play' }}"></i> {{ plugin.enabled ? ta.deactivate|default('Deactiveren') : ta.activate|default('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?')">
<form method="POST" action="/admin/plugins-delete" class="d-inline" onsubmit="return confirm('{{ ta.confirm_delete_plugin|default('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">
@@ -62,9 +74,9 @@
{% 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.
<i class="bi bi-info-circle"></i> {{ ta.no_plugins|default('Geen plugins gevonden. Maak een nieuwe plugin aan om te beginnen.') }}
</div>
</div>
{% endfor %}
</div>
{% endblock %}
{% endblock %}
+11 -11
View File
@@ -1,44 +1,44 @@
{% extends "layouts/admin.twig" %}
{% block title %}Beveiliging - CodePress Admin{% endblock %}
{% block title %}{{ ta.security|default('Beveiliging') }} - {{ ta.admin_title|default('CodePress Admin') }}{% endblock %}
{% block content %}
<h2 class="mb-4"><i class="bi bi-shield-check"></i> Beveiliging & Bot Bescherming</h2>
<h2 class="mb-4"><i class="bi bi-shield-check"></i> {{ ta.security_bots|default('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-header">{{ ta.bot_protection|default('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>
<label class="form-check-label" for="botguard_enabled">{{ ta.botguard_enabled|default('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>
<label class="form-check-label" for="block_bad_bots">{{ ta.block_bad_bots|default('Blokkeer slechte bots') }}</label>
</div>
</div>
</div>
<div class="card shadow-sm mb-4">
<div class="card-header">Sessie Instellingen</div>
<div class="card-header">{{ ta.session_settings|default('Sessie Instellingen') }}</div>
<div class="card-body">
<div class="mb-3">
<label for="session_timeout" class="form-label">Sessie timeout (seconden)</label>
<label for="session_timeout" class="form-label">{{ ta.session_timeout|default('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>
<label for="max_login_attempts" class="form-label">{{ ta.max_login_attempts|default('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
<i class="bi bi-check-lg"></i> {{ ta.save|default('Opslaan') }}
</button>
<a href="/admin/dashboard" class="btn btn-outline-secondary">Annuleren</a>
<a href="/admin/dashboard" class="btn btn-outline-secondary">{{ ta.cancel|default('Annuleren') }}</a>
</form>
{% endblock %}
{% endblock %}
@@ -1,80 +0,0 @@
{% 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 %}
@@ -1,24 +1,24 @@
{% extends "layouts/admin.twig" %}
{% block title %}Nieuw thema - CodePress Admin{% endblock %}
{% block title %}{{ ta.new_theme|default('Nieuw thema') }} - {{ ta.admin_title|default('CodePress Admin') }}{% endblock %}
{% block content %}
<h2 class="mb-4"><i class="bi bi-palette"></i> Nieuw thema aanmaken</h2>
<h2 class="mb-4"><i class="bi bi-palette"></i> {{ ta.new_theme_title|default('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>
<label for="name" class="form-label">{{ ta.theme_name|default('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>
<small class="form-text text-muted">{{ ta.theme_name_help|default('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
<i class="bi bi-check-lg"></i> {{ ta.create|default('Aanmaken') }}
</button>
<a href="/admin/theme" class="btn btn-outline-secondary">Annuleren</a>
<a href="/admin/theme" class="btn btn-outline-secondary">{{ ta.cancel|default('Annuleren') }}</a>
</div>
</form>
{% endblock %}
{% endblock %}
+11 -11
View File
@@ -1,19 +1,19 @@
{% extends "layouts/admin.twig" %}
{% block title %}Thema's - CodePress Admin{% endblock %}
{% block title %}{{ ta.themes|default('Thema\'s') }} - {{ ta.admin_title|default('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>
<h2><i class="bi bi-palette"></i> {{ ta.themes|default('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
<i class="bi bi-check-lg"></i> {{ ta.activate_default|default('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
<i class="bi bi-plus-lg"></i> {{ ta.new_theme|default('Nieuw thema') }}
</a>
</div>
</div>
@@ -25,13 +25,13 @@
<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>
<span class="badge bg-success">{{ ta.active|default('Actief') }}</span>
{% endif %}
</div>
<div class="card-body">
<p class="text-muted small">Naam: {{ theme.name }}</p>
<p class="text-muted small">{{ ta.name_label|default('Naam: ') }}{{ theme.name }}</p>
{% if theme.default_layout %}
<p class="text-muted small">Default layout: {{ theme.default_layout }}</p>
<p class="text-muted small">{{ ta.default_layout_label|default('Default layout: ') }}{{ theme.default_layout }}</p>
{% endif %}
</div>
<div class="card-footer bg-white">
@@ -40,7 +40,7 @@
<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
<i class="bi bi-check-lg"></i> {{ ta.activate|default('Activeren') }}
</button>
</form>
{% endif %}
@@ -49,7 +49,7 @@
<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
<i class="bi bi-palette"></i> {{ ta.compile_scss|default('SCSS compileren') }}
</button>
</form>
</div>
@@ -58,9 +58,9 @@
{% else %}
<div class="col-12">
<div class="alert alert-info">
<i class="bi bi-info-circle"></i> Geen thema's gevonden.
<i class="bi bi-info-circle"></i> {{ ta.no_themes|default('Geen thema\'s gevonden.') }}
</div>
</div>
{% endfor %}
</div>
{% endblock %}
{% endblock %}
+9 -9
View File
@@ -1,35 +1,35 @@
{% extends "layouts/admin.twig" %}
{% block title %}Update - CodePress Admin{% endblock %}
{% block title %}{{ ta.update|default('Update') }} - {{ ta.admin_title|default('CodePress Admin') }}{% endblock %}
{% block content %}
<h2 class="mb-4"><i class="bi bi-cloud-arrow-down"></i> Systeem Update</h2>
<h2 class="mb-4"><i class="bi bi-cloud-arrow-down"></i> {{ ta.system_update|default('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.
{{ ta.git_not_writable|default('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-header">{{ ta.current_version|default('Huidige versie') }}</div>
<div class="card-body">
<p class="mb-0">CodePress versie: <strong>{{ version }}</strong></p>
<p class="mb-0">{{ ta.cms_version_label|default('CodePress versie: ') }}<strong>{{ version }}</strong></p>
</div>
</div>
<div class="card shadow-sm">
<div class="card-header">Update opties</div>
<div class="card-header">{{ ta.update_options|default('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
<i class="bi bi-cloud-arrow-down"></i> {{ ta.check_updates|default('Controleer op updates') }}
</button>
<button class="btn btn-outline-secondary" {{ isGitWritable ? '' : 'disabled' }}>
<i class="bi bi-arrow-repeat"></i> Update uitvoeren
<i class="bi bi-arrow-repeat"></i> {{ ta.run_update|default('Update uitvoeren') }}
</button>
</div>
</div>
</div>
{% endblock %}
{% endblock %}
+73 -24
View File
@@ -1,24 +1,24 @@
{% extends "layouts/admin.twig" %}
{% block title %}Gebruikers - CodePress Admin{% endblock %}
{% block title %}{{ ta.users|default('Gebruikers') }} - {{ ta.admin_title|default('CodePress Admin') }}{% endblock %}
{% block content %}
<h2 class="mb-4"><i class="bi bi-people"></i> Gebruikers</h2>
<h2 class="mb-4"><i class="bi bi-people"></i> {{ ta.users|default('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
<i class="bi bi-list"></i> {{ ta.users|default('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>
<th>{{ ta.username|default('Gebruikersnaam') }}</th>
<th>{{ ta.col_role|default('Rol') }}</th>
<th>{{ ta.col_created|default('Aangemaakt') }}</th>
<th>{{ ta.col_actions|default('Acties') }}</th>
</tr>
</thead>
<tbody>
@@ -28,7 +28,7 @@
<i class="bi bi-person-circle"></i>
{{ username }}
{% if username == user.username %}
<span class="badge bg-info">Jij</span>
<span class="badge bg-info">{{ ta.you|default('Jij') }}</span>
{% endif %}
</td>
<td>
@@ -36,13 +36,13 @@
{{ data.role_label|default(data.role) }}
</span>
</td>
<td class="text-muted">{{ data.created|default('Onbekend') }}</td>
<td class="text-muted">{{ data.created|default(ta.unknown|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
<i class="bi bi-person-gear"></i> {{ ta.col_role|default('Rol') }}
</button>
<form method="POST" action="/admin/users" class="d-inline" onsubmit="return confirm('Weet je zeker dat je gebruiker {{ username }} wilt verwijderen?')">
<form method="POST" action="/admin/users" class="d-inline" onsubmit="return confirm('{{ ta.confirm_delete_user|default('Weet je zeker dat je deze gebruiker 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 }}">
@@ -60,16 +60,16 @@
<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>
<h5 class="modal-title"><i class="bi bi-person-gear"></i> {{ ta.change_role|default('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>
<label class="form-label">{{ ta.current_role|default('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>
<label for="new_role-{{ username }}" class="form-label">{{ ta.new_role|default('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>
@@ -78,21 +78,23 @@
</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>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{{ ta.cancel|default('Annuleren') }}</button>
<button type="submit" class="btn btn-primary"><i class="bi bi-check-lg"></i> {{ ta.change|default('Wijzigen') }}</button>
</div>
</form>
</div>
</div>
</div>
{% else %}
<span class="text-muted small">Eigen account</span>
<button type="button" class="btn btn-sm btn-outline-primary" data-bs-toggle="modal" data-bs-target="#profileModal">
<i class="bi bi-pencil"></i> {{ ta.edit_profile|default('Profiel bewerken') }}
</button>
{% endif %}
</td>
</tr>
{% else %}
<tr>
<td colspan="4" class="text-muted text-center py-4">Geen gebruikers gevonden.</td>
<td colspan="4" class="text-muted text-center py-4">{{ ta.no_users|default('Geen gebruikers gevonden.') }}</td>
</tr>
{% endfor %}
</tbody>
@@ -102,25 +104,72 @@
</div>
<div class="col-md-5">
<!-- Profile edit card for current user -->
<div class="card shadow-sm mb-4">
<div class="card-header">
<i class="bi bi-person-circle"></i> {{ ta.my_profile|default('Mijn profiel') }}
</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="profile">
<input type="hidden" name="profile_username" value="{{ user.username }}">
<div class="mb-3">
<label class="form-label">{{ ta.username|default('Gebruikersnaam') }}</label>
<input type="text" class="form-control" value="{{ user.username }}" disabled>
</div>
<div class="mb-3">
<label for="profile_email" class="form-label">{{ ta.login_email|default('Login e-mail') }}</label>
<input type="email" class="form-control" id="profile_email" name="profile_email" value="{{ user.email|default('') }}">
</div>
<div class="mb-3">
<label for="profile_author_name" class="form-label">{{ ta.author_name|default('Auteur naam') }}</label>
<input type="text" class="form-control" id="profile_author_name" name="profile_author_name" value="{{ user.author_name|default('') }}">
<small class="form-text text-muted">{{ ta.author_name_help|default('Getoond als auteur op de website.') }}</small>
</div>
<div class="mb-3">
<label for="profile_author_email" class="form-label">{{ ta.author_email|default('Auteur e-mail') }}</label>
<input type="email" class="form-control" id="profile_author_email" name="profile_author_email" value="{{ user.author_email|default('') }}">
</div>
<button type="submit" class="btn btn-primary w-100">
<i class="bi bi-check-lg"></i> {{ ta.save|default('Opslaan') }}
</button>
</form>
</div>
</div>
<!-- New user card -->
<div class="card shadow-sm">
<div class="card-header">
<i class="bi bi-plus-circle"></i> Nieuwe gebruiker
<i class="bi bi-plus-circle"></i> {{ ta.new_user|default('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>
<label for="new_username" class="form-label">{{ ta.username|default('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>
<label for="new_password" class="form-label">{{ ta.password|default('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>
<small class="form-text text-muted">{{ ta.password_help|default('Minimaal 8 tekens.') }}</small>
</div>
<div class="mb-3">
<label for="new_role" class="form-label">Rol</label>
<label for="new_email" class="form-label">{{ ta.login_email|default('Login e-mail') }}</label>
<input type="email" class="form-control" id="new_email" name="new_email">
</div>
<div class="mb-3">
<label for="new_author_name" class="form-label">{{ ta.author_name|default('Auteur naam') }}</label>
<input type="text" class="form-control" id="new_author_name" name="new_author_name">
</div>
<div class="mb-3">
<label for="new_author_email" class="form-label">{{ ta.author_email|default('Auteur e-mail') }}</label>
<input type="email" class="form-control" id="new_author_email" name="new_author_email">
</div>
<div class="mb-3">
<label for="new_role" class="form-label">{{ ta.col_role|default('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>
@@ -128,7 +177,7 @@
</select>
</div>
<button type="submit" class="btn btn-primary w-100">
<i class="bi bi-check-lg"></i> Gebruiker toevoegen
<i class="bi bi-check-lg"></i> {{ ta.add_user|default('Gebruiker toevoegen') }}
</button>
</form>
</div>