first commit

This commit is contained in:
Patrick
2026-05-01 18:46:17 +02:00
commit 61ae38701e
104 changed files with 20058 additions and 0 deletions
@@ -0,0 +1,583 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>MinePanel Dashboard - Themes</title>
<link rel="stylesheet" href="/panel.css">
<script src="/theme.js"></script>
</head>
<body>
<div class="layout">
<aside class="sidebar">
<div class="sidebar-brand">MinePanel</div>
<div id="me" class="sidebar-meta"></div>
<nav class="side-nav">
<a class="side-link" href="/dashboard/overview">Overview</a>
<button class="side-category-toggle" data-category="server" type="button">Server</button>
<div class="side-category-items" data-category-items="server">
<a class="side-link" href="/console">Console</a>
<a class="side-link" href="/dashboard/resources">Resources</a>
<a class="side-link" href="/dashboard/players">Players</a>
<a class="side-link" href="/dashboard/bans">Bans</a>
<a class="side-link" href="/dashboard/plugins">Plugins</a>
</div>
<button class="side-category-toggle" data-category="panel" type="button">Panel</button>
<div class="side-category-items" data-category-items="panel">
<a class="side-link" href="/dashboard/users">Users</a>
<a class="side-link" href="/dashboard/discord-webhook">Discord Webhook</a>
<a class="side-link active" href="/dashboard/themes">Themes</a>
</div>
</nav>
<button id="logout" class="secondary sidebar-logout">Logout</button>
</aside>
<main class="content-area">
<h1>Themes</h1>
<p>Pick a preset or customize panel colors.</p>
<section class="card spaced-top">
<h2>Presets</h2>
<div class="theme-preset-row">
<select id="themePreset">
<option value="default">Default (MinePanel)</option>
<option value="midnight">Midnight Blue</option>
<option value="forest">Forest Dark</option>
<option value="ember">Ember Dark</option>
</select>
<button id="applyPreset" type="button">Apply Preset</button>
</div>
</section>
<section class="card spaced-top">
<h2>Custom Colors</h2>
<div id="themeGrid" class="theme-grid"></div>
<div class="theme-actions">
<button id="saveTheme" type="button">Save Custom Theme</button>
<button id="resetTheme" class="secondary" type="button">Reset to Default</button>
</div>
<p id="themeStatus" class="action-status"></p>
</section>
</main>
</div>
<script>
function initSidebarCategories() {
const storageKey = 'minepanel.sidebar.categories';
let savedState = {};
try {
savedState = JSON.parse(sessionStorage.getItem(storageKey) || '{}') || {};
} catch (ignored) {
savedState = {};
}
function persistState() {
try {
sessionStorage.setItem(storageKey, JSON.stringify(savedState));
} catch (ignored) {
// Ignore unavailable sessionStorage.
}
}
document.querySelectorAll('.side-category-toggle').forEach(toggle => {
const category = toggle.dataset.category;
const items = document.querySelector(`.side-category-items[data-category-items="${category}"]`);
if (!items || !category) return;
const expanded = savedState[category] === true;
items.classList.toggle('expanded', expanded);
toggle.classList.toggle('expanded', expanded);
toggle.addEventListener('click', () => {
const expanded = !items.classList.contains('expanded');
items.classList.toggle('expanded', expanded);
toggle.classList.toggle('expanded', expanded);
savedState[category] = expanded;
persistState();
});
});
}
const STORAGE_KEY = window.MinePanelTheme ? window.MinePanelTheme.storageKey : 'minepanel.customTheme';
const THEME_KEYS = [
'--bg',
'--surface',
'--surface-2',
'--border',
'--text',
'--text-muted',
'--accent',
'--accent-strong',
'--danger-bg',
'--danger-text',
'--log-bg',
'--sidebar-bg',
'--sidebar-border',
'--sidebar-meta-bg',
'--sidebar-meta-border',
'--sidebar-toggle-text',
'--sidebar-toggle-icon',
'--sidebar-link-text',
'--sidebar-link-bg',
'--sidebar-link-border',
'--sidebar-link-hover-bg',
'--button-bg',
'--button-border',
'--button-hover-bg',
'--button-secondary-bg',
'--button-secondary-border',
'--input-bg',
'--focus-ring',
'--table-bg',
'--table-head-bg',
'--table-row-border',
'--table-row-hover-bg',
'--table-head-text',
'--table-cell-text',
'--log-border',
'--log-text',
'--chart-bg',
'--chart-border',
'--chart-grid',
'--chart-label',
'--player-box-bg',
'--player-box-border',
'--player-box-hover-bg',
'--player-box-active-bg',
'--player-box-active-border',
'--player-muted-bg',
'--player-muted-border'
];
const THEME_GROUPS = [
{
title: 'Core',
keys: [
'--bg',
'--surface',
'--surface-2',
'--border',
'--text',
'--text-muted',
'--accent',
'--accent-strong',
'--danger-bg',
'--danger-text'
]
},
{
title: 'Sidebar',
keys: [
'--sidebar-bg',
'--sidebar-border',
'--sidebar-meta-bg',
'--sidebar-meta-border',
'--sidebar-toggle-text',
'--sidebar-toggle-icon',
'--sidebar-link-text',
'--sidebar-link-bg',
'--sidebar-link-border',
'--sidebar-link-hover-bg'
]
},
{
title: 'Buttons and Inputs',
keys: [
'--button-bg',
'--button-border',
'--button-hover-bg',
'--button-secondary-bg',
'--button-secondary-border',
'--input-bg',
'--focus-ring'
]
},
{
title: 'Logs and Tables',
keys: [
'--log-bg',
'--log-border',
'--log-text',
'--table-bg',
'--table-head-bg',
'--table-row-border',
'--table-row-hover-bg',
'--table-head-text',
'--table-cell-text'
]
},
{
title: 'Charts',
keys: [
'--chart-bg',
'--chart-border',
'--chart-grid',
'--chart-label'
]
},
{
title: 'Players',
keys: [
'--player-box-bg',
'--player-box-border',
'--player-box-hover-bg',
'--player-box-active-bg',
'--player-box-active-border',
'--player-muted-bg',
'--player-muted-border'
]
}
];
const PRESETS = {
default: {
'--bg': '#0b1220',
'--surface': '#111a2e',
'--surface-2': '#16233d',
'--border': '#24324f',
'--text': '#e5ebf8',
'--text-muted': '#99a8c6',
'--accent': '#4f8cff',
'--accent-strong': '#3a75e8',
'--danger-bg': '#3d1d29',
'--danger-text': '#ffb7c7',
'--log-bg': '#0a101d',
'--sidebar-bg': '#060b16',
'--sidebar-border': '#1b2942',
'--sidebar-meta-bg': '#0d1528',
'--sidebar-meta-border': '#1f3050',
'--sidebar-toggle-text': '#7f93b8',
'--sidebar-toggle-icon': '#8fa6d0',
'--sidebar-link-text': '#c7d7f7',
'--sidebar-link-bg': '#0f1930',
'--sidebar-link-border': '#263a5d',
'--sidebar-link-hover-bg': '#162642',
'--button-bg': '#4f8cff',
'--button-border': '#3a75e8',
'--button-hover-bg': '#5a95ff',
'--button-secondary-bg': '#21314d',
'--button-secondary-border': '#344769',
'--input-bg': '#0f1930',
'--focus-ring': '#4f8cff',
'--table-bg': '#0e172b',
'--table-head-bg': '#13203b',
'--table-row-border': '#1f2d49',
'--table-row-hover-bg': '#111d35',
'--table-head-text': '#d7e2f8',
'--table-cell-text': '#c7d4ee',
'--log-border': '#213457',
'--log-text': '#d6e0f8',
'--chart-bg': '#0b1427',
'--chart-border': '#213457',
'--chart-grid': '#7896d2',
'--chart-label': '#8ea6d2',
'--player-box-bg': '#0f1930',
'--player-box-border': '#23365a',
'--player-box-hover-bg': '#162642',
'--player-box-active-bg': '#1b2c4a',
'--player-box-active-border': '#3f73cf',
'--player-muted-bg': '#101a2f',
'--player-muted-border': '#2a3f63'
},
midnight: {
'--bg': '#070b17',
'--surface': '#0f1a30',
'--surface-2': '#132445',
'--border': '#263f67',
'--text': '#e6f0ff',
'--text-muted': '#97abd2',
'--accent': '#4f8cff',
'--accent-strong': '#2c6fe2',
'--danger-bg': '#3b1a2f',
'--danger-text': '#ffb5c6',
'--log-bg': '#090f1d',
'--sidebar-bg': '#050915',
'--sidebar-border': '#1b3158',
'--sidebar-meta-bg': '#0c1730',
'--sidebar-meta-border': '#1f3a67',
'--sidebar-toggle-text': '#8aa1ca',
'--sidebar-toggle-icon': '#9cb4e2',
'--sidebar-link-text': '#d2e2ff',
'--sidebar-link-bg': '#11203b',
'--sidebar-link-border': '#2b4773',
'--sidebar-link-hover-bg': '#183055',
'--button-bg': '#4f8cff',
'--button-border': '#2c6fe2',
'--button-hover-bg': '#5f98ff',
'--button-secondary-bg': '#23385e',
'--button-secondary-border': '#35507e',
'--input-bg': '#11203b',
'--focus-ring': '#4f8cff',
'--table-bg': '#0f1a31',
'--table-head-bg': '#13274a',
'--table-row-border': '#2a4773',
'--table-row-hover-bg': '#183157',
'--table-head-text': '#d7e6ff',
'--table-cell-text': '#cddcff',
'--log-border': '#2b4a78',
'--log-text': '#d4e3ff',
'--chart-bg': '#0f1d38',
'--chart-border': '#2b4a78',
'--chart-grid': '#87a7df',
'--chart-label': '#a3b8df',
'--player-box-bg': '#11203b',
'--player-box-border': '#2b4773',
'--player-box-hover-bg': '#183055',
'--player-box-active-bg': '#223c69',
'--player-box-active-border': '#4b82de',
'--player-muted-bg': '#0f1b34',
'--player-muted-border': '#2a4268'
},
forest: {
'--bg': '#0b1612',
'--surface': '#11231b',
'--surface-2': '#173026',
'--border': '#29503f',
'--text': '#e7fff5',
'--text-muted': '#97c7b0',
'--accent': '#3fbf7f',
'--accent-strong': '#2ca866',
'--danger-bg': '#3d1d29',
'--danger-text': '#ffb7c7',
'--log-bg': '#0b1511',
'--sidebar-bg': '#08130f',
'--sidebar-border': '#1f3f33',
'--sidebar-meta-bg': '#10231c',
'--sidebar-meta-border': '#2a4f40',
'--sidebar-toggle-text': '#8db8a3',
'--sidebar-toggle-icon': '#9fceb8',
'--sidebar-link-text': '#d6f5e7',
'--sidebar-link-bg': '#133026',
'--sidebar-link-border': '#2f5f4b',
'--sidebar-link-hover-bg': '#1a3d31',
'--button-bg': '#3fbf7f',
'--button-border': '#2ca866',
'--button-hover-bg': '#4dce8d',
'--button-secondary-bg': '#24493b',
'--button-secondary-border': '#356652',
'--input-bg': '#133026',
'--focus-ring': '#3fbf7f',
'--table-bg': '#11271f',
'--table-head-bg': '#173629',
'--table-row-border': '#2e5b48',
'--table-row-hover-bg': '#1a3f31',
'--table-head-text': '#daf5e8',
'--table-cell-text': '#c8ebda',
'--log-border': '#2e5a49',
'--log-text': '#d9f4e8',
'--chart-bg': '#10261e',
'--chart-border': '#2e5a49',
'--chart-grid': '#7fb6a1',
'--chart-label': '#94c7b3',
'--player-box-bg': '#133026',
'--player-box-border': '#2f5f4b',
'--player-box-hover-bg': '#1a3d31',
'--player-box-active-bg': '#23503f',
'--player-box-active-border': '#43a379',
'--player-muted-bg': '#11271f',
'--player-muted-border': '#2c5342'
},
ember: {
'--bg': '#18100b',
'--surface': '#2a1a11',
'--surface-2': '#3a2416',
'--border': '#5a3a23',
'--text': '#fff0e8',
'--text-muted': '#d6b8a4',
'--accent': '#ff8f4f',
'--accent-strong': '#e67839',
'--danger-bg': '#4a1f22',
'--danger-text': '#ffb7b7',
'--log-bg': '#1a120d',
'--sidebar-bg': '#140d08',
'--sidebar-border': '#4c311f',
'--sidebar-meta-bg': '#23160e',
'--sidebar-meta-border': '#5e3b26',
'--sidebar-toggle-text': '#d1a78f',
'--sidebar-toggle-icon': '#e0b69e',
'--sidebar-link-text': '#ffe4d2',
'--sidebar-link-bg': '#382116',
'--sidebar-link-border': '#6f4228',
'--sidebar-link-hover-bg': '#4a2c1d',
'--button-bg': '#ff8f4f',
'--button-border': '#e67839',
'--button-hover-bg': '#ffa366',
'--button-secondary-bg': '#5a3622',
'--button-secondary-border': '#7a4a30',
'--input-bg': '#382116',
'--focus-ring': '#ff8f4f',
'--table-bg': '#2f1d13',
'--table-head-bg': '#432719',
'--table-row-border': '#6b4027',
'--table-row-hover-bg': '#4b2e1f',
'--table-head-text': '#ffe7da',
'--table-cell-text': '#f2d2bf',
'--log-border': '#70442a',
'--log-text': '#ffe4d5',
'--chart-bg': '#341f14',
'--chart-border': '#70442a',
'--chart-grid': '#c18e71',
'--chart-label': '#d2a98f',
'--player-box-bg': '#382116',
'--player-box-border': '#6f4228',
'--player-box-hover-bg': '#4a2c1d',
'--player-box-active-bg': '#603924',
'--player-box-active-border': '#ff9a63',
'--player-muted-bg': '#311d12',
'--player-muted-border': '#5b3521'
}
};
function setStatus(message, isError) {
const status = document.getElementById('themeStatus');
status.textContent = message || '';
status.className = isError ? 'action-status error-text' : 'action-status success-text';
}
function currentValueForKey(key) {
const value = getComputedStyle(document.documentElement).getPropertyValue(key).trim();
return value || PRESETS.default[key] || '#000000';
}
function normalizeHex(value) {
if (!value) return '#000000';
const trimmed = value.trim();
if (!trimmed.startsWith('#')) return '#000000';
if (trimmed.length === 7) return trimmed;
if (trimmed.length === 4) {
return `#${trimmed[1]}${trimmed[1]}${trimmed[2]}${trimmed[2]}${trimmed[3]}${trimmed[3]}`;
}
return '#000000';
}
function buildThemeField(key) {
const row = document.createElement('label');
row.className = 'theme-field';
row.setAttribute('for', `theme-${key.replace(/[^a-z0-9]/gi, '')}`);
const name = document.createElement('span');
name.textContent = key;
const input = document.createElement('input');
input.type = 'color';
input.id = `theme-${key.replace(/[^a-z0-9]/gi, '')}`;
input.dataset.themeKey = key;
input.value = normalizeHex(currentValueForKey(key));
input.addEventListener('input', () => {
document.documentElement.style.setProperty(key, input.value);
});
row.appendChild(name);
row.appendChild(input);
return row;
}
function buildThemeGroup(title, keys) {
const section = document.createElement('section');
section.className = 'theme-group';
const heading = document.createElement('h3');
heading.className = 'theme-group-title';
heading.textContent = title;
const content = document.createElement('div');
content.className = 'theme-group-grid';
keys.forEach(key => {
content.appendChild(buildThemeField(key));
});
section.appendChild(heading);
section.appendChild(content);
return section;
}
function buildThemeGrid() {
const grid = document.getElementById('themeGrid');
grid.innerHTML = '';
const usedKeys = new Set();
THEME_GROUPS.forEach(group => {
const validKeys = group.keys.filter(key => THEME_KEYS.includes(key));
if (validKeys.length === 0) {
return;
}
validKeys.forEach(key => usedKeys.add(key));
grid.appendChild(buildThemeGroup(group.title, validKeys));
});
const ungrouped = THEME_KEYS.filter(key => !usedKeys.has(key));
if (ungrouped.length > 0) {
grid.appendChild(buildThemeGroup('Other', ungrouped));
}
}
function collectThemeFromInputs() {
const theme = {};
document.querySelectorAll('input[data-theme-key]').forEach(input => {
theme[input.dataset.themeKey] = input.value;
});
return theme;
}
function applyTheme(theme) {
if (window.MinePanelTheme) {
window.MinePanelTheme.applyTheme(theme);
} else {
Object.entries(theme).forEach(([key, value]) => {
document.documentElement.style.setProperty(key, value);
});
}
buildThemeGrid();
}
async function api(url, options) {
const res = await fetch(url, options);
const data = await res.json();
if (!res.ok) throw new Error(data.error || 'Request failed');
return data;
}
async function loadMe() {
const data = await api('/api/me');
document.getElementById('me').textContent = `Logged in as ${data.user.username} (${data.user.role})`;
}
document.getElementById('applyPreset').addEventListener('click', () => {
const preset = document.getElementById('themePreset').value;
const theme = PRESETS[preset] || PRESETS.default;
applyTheme(theme);
setStatus(`Applied ${preset} preset. Save to keep it.`, false);
});
document.getElementById('saveTheme').addEventListener('click', () => {
const theme = collectThemeFromInputs();
localStorage.setItem(STORAGE_KEY, JSON.stringify(theme));
setStatus('Custom theme saved.', false);
});
document.getElementById('resetTheme').addEventListener('click', () => {
localStorage.removeItem(STORAGE_KEY);
applyTheme(PRESETS.default);
setStatus('Theme reset to default.', false);
});
document.getElementById('logout').addEventListener('click', async () => {
await api('/api/logout', { method: 'POST' });
window.location.href = '/';
});
(async () => {
try {
initSidebarCategories();
await loadMe();
buildThemeGrid();
} catch {
window.location.href = '/';
}
})();
</script>
</body>
</html>