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:
parent
bf3e55a967
commit
a1021a0f84
20 changed files with 5281 additions and 5471 deletions
117
assets/global.js
Normal file
117
assets/global.js
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
/* === DARK MODE === */
|
||||
(function() {
|
||||
const saved = localStorage.getItem('theme') || 'light';
|
||||
document.documentElement.setAttribute('data-theme', saved);
|
||||
})();
|
||||
|
||||
function toggleDarkMode() {
|
||||
const html = document.documentElement;
|
||||
const current = html.getAttribute('data-theme') || 'light';
|
||||
const next = current === 'dark' ? 'light' : 'dark';
|
||||
html.setAttribute('data-theme', next);
|
||||
localStorage.setItem('theme', next);
|
||||
updateDarkModeBtn();
|
||||
}
|
||||
|
||||
function updateDarkModeBtn() {
|
||||
const btn = document.getElementById('darkModeBtn');
|
||||
if (!btn) return;
|
||||
const isDark = document.documentElement.getAttribute('data-theme') === 'dark';
|
||||
btn.textContent = isDark ? '☀️' : '🌙';
|
||||
btn.title = isDark ? 'Light Mode' : 'Dark Mode';
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', updateDarkModeBtn);
|
||||
|
||||
/* === TOAST NOTIFICATIONS === */
|
||||
(function() {
|
||||
let container = null;
|
||||
|
||||
function getContainer() {
|
||||
if (!container) {
|
||||
container = document.getElementById('toast-container');
|
||||
if (!container) {
|
||||
container = document.createElement('div');
|
||||
container.id = 'toast-container';
|
||||
document.body.appendChild(container);
|
||||
}
|
||||
}
|
||||
return container;
|
||||
}
|
||||
|
||||
const icons = {
|
||||
success: '✓',
|
||||
error: '✕',
|
||||
info: 'ℹ',
|
||||
warning: '⚠'
|
||||
};
|
||||
|
||||
window.showToast = function(type, title, message, duration) {
|
||||
duration = duration || 4000;
|
||||
const c = getContainer();
|
||||
const el = document.createElement('div');
|
||||
el.className = 'toast ' + type;
|
||||
el.innerHTML = `
|
||||
<span class="toast-icon">${icons[type] || 'ℹ'}</span>
|
||||
<div class="toast-body">
|
||||
<div class="toast-title">${title}</div>
|
||||
${message ? `<div class="toast-msg">${message}</div>` : ''}
|
||||
</div>
|
||||
<button class="toast-close" onclick="this.parentElement.remove()">×</button>
|
||||
`;
|
||||
c.appendChild(el);
|
||||
requestAnimationFrame(() => {
|
||||
requestAnimationFrame(() => el.classList.add('show'));
|
||||
});
|
||||
setTimeout(() => {
|
||||
el.classList.remove('show');
|
||||
setTimeout(() => el.remove(), 350);
|
||||
}, duration);
|
||||
return el;
|
||||
};
|
||||
})();
|
||||
|
||||
/* === MOBILE SIDEBAR === */
|
||||
function toggleMobileSidebar() {
|
||||
const sidebar = document.querySelector('.sidebar');
|
||||
const overlay = document.querySelector('.sidebar-overlay');
|
||||
if (!sidebar) return;
|
||||
sidebar.classList.toggle('mobile-open');
|
||||
if (overlay) overlay.classList.toggle('active');
|
||||
}
|
||||
|
||||
function closeMobileSidebar() {
|
||||
const sidebar = document.querySelector('.sidebar');
|
||||
const overlay = document.querySelector('.sidebar-overlay');
|
||||
if (sidebar) sidebar.classList.remove('mobile-open');
|
||||
if (overlay) overlay.classList.remove('active');
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const overlay = document.querySelector('.sidebar-overlay');
|
||||
if (overlay) overlay.addEventListener('click', closeMobileSidebar);
|
||||
|
||||
document.addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Escape') closeMobileSidebar();
|
||||
});
|
||||
});
|
||||
|
||||
/* === LANGUAGE SWITCHER === */
|
||||
function switchLanguage(lang) {
|
||||
fetch('?set_lang=' + lang, { method: 'GET' }).then(() => location.reload());
|
||||
}
|
||||
|
||||
/* === CLIPBOARD === */
|
||||
function copyToClipboard(text, successMsg) {
|
||||
navigator.clipboard.writeText(text).then(() => {
|
||||
showToast('success', successMsg || 'Kopiert!', '');
|
||||
}).catch(() => {
|
||||
const ta = document.createElement('textarea');
|
||||
ta.value = text;
|
||||
document.body.appendChild(ta);
|
||||
ta.select();
|
||||
document.execCommand('copy');
|
||||
document.body.removeChild(ta);
|
||||
showToast('success', successMsg || 'Kopiert!', '');
|
||||
});
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue