feat: comprehensive UI/UX and feature improvements

- Dark mode: CSS custom properties (global.css) + toggle button, persisted in localStorage
- i18n: German/English language switcher (lang/de.php, lang/en.php, includes/I18n.php)
- Mobile-responsive admin layout: hamburger menu, sidebar overlay (global.js + global.css)
- Shared admin navigation include (includes/admin_nav.php) used across all admin pages
- Toast notifications system globally available via global.js
- Voucher templates/profiles: CRUD UI at admin/templates.php with voucher_templates DB table
- Bulk voucher creation: create 1-20 vouchers at once with multi-print layout on index.php
- Configurable voucher defaults: expire time, device limit, max limit in admin settings
- Template quick-select on voucher form: auto-fills max_uses and expire_minutes
- Password reset flow: forgot_password.php + reset_password.php with token-based reset
- Audit log UI: admin/audit_log.php with filter, pagination, audit_log DB table
- Audit logging on login, user create/edit/delete, site create/edit/delete
- Admin pages updated: index, vouchers, users, sites all use admin_nav.php + dark mode + i18n
- Voucher admin: live search input added alongside existing status filter + pagination
- Users admin: password-reset-link button per user row (when SMTP enabled)
- Login page: i18n, dark mode, language switcher, forgot password link

https://claude.ai/code/session_01YN6Bcm1VSi8mpDeyKpyrdJ
This commit is contained in:
Claude 2026-05-08 17:59:17 +00:00
parent bf3e55a967
commit a1021a0f84
No known key found for this signature in database
20 changed files with 5281 additions and 5471 deletions

53
includes/I18n.php Normal file
View file

@ -0,0 +1,53 @@
<?php
class I18n {
private static array $translations = [];
private static string $language = 'de';
private static bool $initialized = false;
public static function init(): void {
if (self::$initialized) return;
if (!isset($_SESSION)) {
if (session_status() === PHP_SESSION_NONE) session_start();
}
if (isset($_GET['set_lang']) && in_array($_GET['set_lang'], ['de', 'en'], true)) {
$_SESSION['language'] = $_GET['set_lang'];
}
self::$language = $_SESSION['language'] ?? 'de';
$langFile = __DIR__ . '/../lang/' . self::$language . '.php';
if (file_exists($langFile)) {
self::$translations = require $langFile;
} else {
$fallback = __DIR__ . '/../lang/de.php';
if (file_exists($fallback)) {
self::$translations = require $fallback;
}
}
self::$initialized = true;
}
public static function t(string $key, array $replace = []): string {
$text = self::$translations[$key] ?? $key;
foreach ($replace as $k => $v) {
$text = str_replace('{' . $k . '}', (string)$v, $text);
}
return $text;
}
public static function getLanguage(): string {
return self::$language;
}
public static function getAvailable(): array {
return ['de' => 'Deutsch', 'en' => 'English'];
}
}
function __($key, array $replace = []): string {
return I18n::t($key, $replace);
}