Improved dashboard and music queue
This commit is contained in:
+131
@@ -0,0 +1,131 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>MumBullet Dashboard</title>
|
||||
<link rel="stylesheet" href="/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<h1>MumBullet Dashboard</h1>
|
||||
<nav>
|
||||
<button id="queueTab" class="tab-button active">Queue</button>
|
||||
<button id="usersTab" class="tab-button">Users</button>
|
||||
<button id="cacheTab" class="tab-button">Cache</button>
|
||||
<button id="logoutButton">Logout</button>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<section id="queueSection" class="tab-content active">
|
||||
<h2>Music Queue</h2>
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3>Now Playing</h3>
|
||||
</div>
|
||||
<div class="card-content" id="nowPlaying">
|
||||
<p>Nothing playing</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3>Queue</h3>
|
||||
<button id="clearQueueButton" class="danger-button">Clear Queue</button>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<ul id="queueList" class="queue-list">
|
||||
<li class="empty-message">Queue is empty</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="usersSection" class="tab-content">
|
||||
<h2>User Management</h2>
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3>Users</h3>
|
||||
<button id="addUserButton">Add User</button>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<table id="usersTable" class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Username</th>
|
||||
<th>Permission Level</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr class="empty-message">
|
||||
<td colspan="3">No users found</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="addUserModal" class="modal">
|
||||
<div class="modal-content">
|
||||
<h3>Add User</h3>
|
||||
<form id="addUserForm">
|
||||
<div class="form-group">
|
||||
<label for="newUsername">Username</label>
|
||||
<input type="text" id="newUsername" name="username" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="newPermissionLevel">Permission Level</label>
|
||||
<select id="newPermissionLevel" name="permissionLevel" required>
|
||||
<option value="0">None</option>
|
||||
<option value="1">View Only</option>
|
||||
<option value="2">Read/Write</option>
|
||||
<option value="3">Admin</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-actions">
|
||||
<button type="button" id="cancelAddUser">Cancel</button>
|
||||
<button type="submit">Add User</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="cacheSection" class="tab-content">
|
||||
<h2>Cache Management</h2>
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3>Cache Statistics</h3>
|
||||
<button id="clearCacheButton" class="danger-button">Clear Cache</button>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<div id="cacheStats" class="stats-grid">
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">Songs</span>
|
||||
<span class="stat-value" id="cacheSongCount">0</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">Size</span>
|
||||
<span class="stat-value" id="cacheSize">0 MB</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">Max Size</span>
|
||||
<span class="stat-value" id="cacheMaxSize">0 MB</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">Usage</span>
|
||||
<div class="progress-bar">
|
||||
<div id="cacheUsage" class="progress-value" style="width: 0%"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<script src="/script.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,85 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Login - MumBullet Dashboard</title>
|
||||
<link rel="stylesheet" href="/style.css">
|
||||
</head>
|
||||
<body class="login-page">
|
||||
<div class="login-container">
|
||||
<h1>MumBullet Dashboard</h1>
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h2>Login</h2>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<!--ERROR-->
|
||||
<form id="loginForm" action="/api/login" method="post">
|
||||
<div class="form-group">
|
||||
<label for="username">Username</label>
|
||||
<input type="text" id="username" name="username" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="password">Password</label>
|
||||
<input type="password" id="password" name="password" required>
|
||||
</div>
|
||||
<div id="loginError" class="error" style="display: none;">
|
||||
Invalid username or password.
|
||||
</div>
|
||||
<div class="form-actions">
|
||||
<button type="submit">Login</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<script>
|
||||
// Check if there's an error parameter in the URL
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
if (urlParams.has('error') && urlParams.get('error') === 'invalid') {
|
||||
document.getElementById('loginError').style.display = 'block';
|
||||
}
|
||||
|
||||
// Handle form submission with fetch API instead of traditional form submission
|
||||
document.getElementById('loginForm').addEventListener('submit', async function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const username = document.getElementById('username').value;
|
||||
const password = document.getElementById('password').value;
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/login', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
},
|
||||
body: `username=${encodeURIComponent(username)}&password=${encodeURIComponent(password)}`,
|
||||
// Important for cookies to work
|
||||
credentials: 'include'
|
||||
});
|
||||
|
||||
if (response.redirected) {
|
||||
// Follow the redirect manually
|
||||
window.location.href = response.url;
|
||||
return;
|
||||
}
|
||||
|
||||
if (response.ok) {
|
||||
// Successful login, redirect to dashboard
|
||||
window.location.href = '/';
|
||||
} else {
|
||||
// Show error message
|
||||
document.getElementById('loginError').style.display = 'block';
|
||||
console.error('Login failed:', await response.text());
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Login error:', error);
|
||||
document.getElementById('loginError').textContent = 'An error occurred. Please try again.';
|
||||
document.getElementById('loginError').style.display = 'block';
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
+287
@@ -0,0 +1,287 @@
|
||||
// DOM Elements
|
||||
const queueTab = document.getElementById('queueTab');
|
||||
const usersTab = document.getElementById('usersTab');
|
||||
const cacheTab = document.getElementById('cacheTab');
|
||||
const logoutButton = document.getElementById('logoutButton');
|
||||
|
||||
const queueSection = document.getElementById('queueSection');
|
||||
const usersSection = document.getElementById('usersSection');
|
||||
const cacheSection = document.getElementById('cacheSection');
|
||||
|
||||
const nowPlaying = document.getElementById('nowPlaying');
|
||||
const queueList = document.getElementById('queueList');
|
||||
const clearQueueButton = document.getElementById('clearQueueButton');
|
||||
|
||||
const usersTable = document.getElementById('usersTable');
|
||||
const addUserButton = document.getElementById('addUserButton');
|
||||
const addUserModal = document.getElementById('addUserModal');
|
||||
const addUserForm = document.getElementById('addUserForm');
|
||||
const cancelAddUser = document.getElementById('cancelAddUser');
|
||||
|
||||
const cacheSongCount = document.getElementById('cacheSongCount');
|
||||
const cacheSize = document.getElementById('cacheSize');
|
||||
const cacheMaxSize = document.getElementById('cacheMaxSize');
|
||||
const cacheUsage = document.getElementById('cacheUsage');
|
||||
const clearCacheButton = document.getElementById('clearCacheButton');
|
||||
|
||||
// Tab switching
|
||||
function switchTab(tab, section) {
|
||||
// Remove active class from all tabs and sections
|
||||
[queueTab, usersTab, cacheTab].forEach(t => t.classList.remove('active'));
|
||||
[queueSection, usersSection, cacheSection].forEach(s => s.classList.remove('active'));
|
||||
|
||||
// Add active class to selected tab and section
|
||||
tab.classList.add('active');
|
||||
section.classList.add('active');
|
||||
}
|
||||
|
||||
queueTab.addEventListener('click', () => switchTab(queueTab, queueSection));
|
||||
usersTab.addEventListener('click', () => switchTab(usersTab, usersSection));
|
||||
cacheTab.addEventListener('click', () => switchTab(cacheTab, cacheSection));
|
||||
|
||||
// Logout
|
||||
logoutButton.addEventListener('click', async () => {
|
||||
try {
|
||||
await fetch('/api/logout', {
|
||||
method: 'POST',
|
||||
credentials: 'include', // Important for cookies to work
|
||||
});
|
||||
window.location.href = '/login';
|
||||
} catch (error) {
|
||||
console.error('Logout failed:', error);
|
||||
}
|
||||
});
|
||||
|
||||
// Queue management
|
||||
function formatDuration(seconds) {
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
const secs = seconds % 60;
|
||||
return `${minutes}:${secs.toString().padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
async function loadQueue() {
|
||||
try {
|
||||
const response = await fetch('/api/queue', {
|
||||
credentials: 'include', // Important for cookies to work
|
||||
});
|
||||
const responseData = await response.json();
|
||||
|
||||
// Update now playing
|
||||
if (responseData.current_song) {
|
||||
nowPlaying.innerHTML = `
|
||||
<div class="song-info">
|
||||
<h4>${responseData.current_song.title}</h4>
|
||||
<p>${formatDuration(responseData.current_song.duration)}</p>
|
||||
</div>
|
||||
<div class="song-status">
|
||||
<span class="badge">${responseData.state}</span>
|
||||
</div>
|
||||
`;
|
||||
} else {
|
||||
nowPlaying.innerHTML = '<p>Nothing playing</p>';
|
||||
}
|
||||
|
||||
// Update queue
|
||||
if (responseData.queue && responseData.queue.length > 0) {
|
||||
queueList.innerHTML = responseData.queue.map((songItem, idx) => {
|
||||
if (songItem.is_current) {
|
||||
return `
|
||||
<li class="now-playing">
|
||||
<div class="song-info">
|
||||
<strong>${songItem.title}</strong>
|
||||
<span>${formatDuration(songItem.duration)}</span>
|
||||
</div>
|
||||
<div class="song-status">
|
||||
<span class="badge">Now Playing</span>
|
||||
</div>
|
||||
</li>
|
||||
`;
|
||||
} else {
|
||||
return `
|
||||
<li>
|
||||
<div class="song-info">
|
||||
<strong>${idx + 1}. ${songItem.title}</strong>
|
||||
<span>${formatDuration(songItem.duration)}</span>
|
||||
</div>
|
||||
</li>
|
||||
`;
|
||||
}
|
||||
}).join('');
|
||||
} else {
|
||||
queueList.innerHTML = '<li class="empty-message">Queue is empty</li>';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load queue:', error);
|
||||
queueList.innerHTML = '<li class="empty-message">Failed to load queue</li>';
|
||||
}
|
||||
}
|
||||
|
||||
clearQueueButton.addEventListener('click', async () => {
|
||||
if (!confirm('Are you sure you want to clear the queue?')) return;
|
||||
|
||||
try {
|
||||
await fetch('/api/queue', {
|
||||
method: 'DELETE',
|
||||
credentials: 'include', // Important for cookies to work
|
||||
});
|
||||
loadQueue();
|
||||
} catch (error) {
|
||||
console.error('Failed to clear queue:', error);
|
||||
alert('Failed to clear queue');
|
||||
}
|
||||
});
|
||||
|
||||
// User management
|
||||
async function loadUsers() {
|
||||
try {
|
||||
const response = await fetch('/api/users', {
|
||||
credentials: 'include', // Important for cookies to work
|
||||
});
|
||||
const responseData = await response.json();
|
||||
|
||||
if (responseData.users && responseData.users.length > 0) {
|
||||
const tbody = usersTable.querySelector('tbody');
|
||||
tbody.innerHTML = responseData.users.map(userItem => `
|
||||
<tr>
|
||||
<td>${userItem.username}</td>
|
||||
<td>
|
||||
<select class="permission-select" data-username="${userItem.username}">
|
||||
<option value="0" ${userItem.permission_level == 0 ? 'selected' : ''}>None</option>
|
||||
<option value="1" ${userItem.permission_level == 1 ? 'selected' : ''}>View Only</option>
|
||||
<option value="2" ${userItem.permission_level == 2 ? 'selected' : ''}>Read/Write</option>
|
||||
<option value="3" ${userItem.permission_level == 3 ? 'selected' : ''}>Admin</option>
|
||||
</select>
|
||||
</td>
|
||||
<td>
|
||||
<button class="save-permission" data-username="${userItem.username}">Save</button>
|
||||
</td>
|
||||
</tr>
|
||||
`).join('');
|
||||
|
||||
// Add event listeners to save buttons
|
||||
document.querySelectorAll('.save-permission').forEach(button => {
|
||||
button.addEventListener('click', async () => {
|
||||
const username = button.dataset.username;
|
||||
const select = document.querySelector(`.permission-select[data-username="${username}"]`);
|
||||
const permissionLevel = parseInt(select.value);
|
||||
|
||||
try {
|
||||
await fetch(`/api/users/${username}/permissions`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ permission_level: permissionLevel }),
|
||||
credentials: 'include', // Important for cookies to work
|
||||
});
|
||||
alert(`Permission updated for ${username}`);
|
||||
} catch (error) {
|
||||
console.error('Failed to update permission:', error);
|
||||
alert('Failed to update permission');
|
||||
}
|
||||
});
|
||||
});
|
||||
} else {
|
||||
usersTable.querySelector('tbody').innerHTML = '<tr class="empty-message"><td colspan="3">No users found</td></tr>';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load users:', error);
|
||||
usersTable.querySelector('tbody').innerHTML = '<tr class="empty-message"><td colspan="3">Failed to load users</td></tr>';
|
||||
}
|
||||
}
|
||||
|
||||
// Add user modal
|
||||
addUserButton.addEventListener('click', () => {
|
||||
addUserModal.classList.add('active');
|
||||
});
|
||||
|
||||
cancelAddUser.addEventListener('click', () => {
|
||||
addUserModal.classList.remove('active');
|
||||
addUserForm.reset();
|
||||
});
|
||||
|
||||
addUserForm.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const username = document.getElementById('newUsername').value;
|
||||
const permissionLevel = parseInt(document.getElementById('newPermissionLevel').value);
|
||||
|
||||
try {
|
||||
await fetch('/api/users', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ username, permission_level: permissionLevel }),
|
||||
credentials: 'include', // Important for cookies to work
|
||||
});
|
||||
|
||||
addUserModal.classList.remove('active');
|
||||
addUserForm.reset();
|
||||
loadUsers();
|
||||
} catch (error) {
|
||||
console.error('Failed to add user:', error);
|
||||
alert('Failed to add user');
|
||||
}
|
||||
});
|
||||
|
||||
// Cache management
|
||||
async function loadCacheStats() {
|
||||
try {
|
||||
const response = await fetch('/api/cache', {
|
||||
credentials: 'include', // Important for cookies to work
|
||||
headers: {
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
});
|
||||
const stats = await response.json();
|
||||
|
||||
cacheSongCount.textContent = stats.songCount;
|
||||
cacheSize.textContent = `${stats.totalSizeMb} MB`;
|
||||
cacheMaxSize.textContent = `${stats.maxSizeMb} MB`;
|
||||
|
||||
const usagePercent = stats.maxSizeMb > 0
|
||||
? (stats.totalSizeMb / stats.maxSizeMb) * 100
|
||||
: 0;
|
||||
|
||||
cacheUsage.style.width = `${Math.min(usagePercent, 100)}%`;
|
||||
|
||||
// Change color based on usage
|
||||
if (usagePercent > 90) {
|
||||
cacheUsage.style.backgroundColor = 'var(--danger-color)';
|
||||
} else if (usagePercent > 70) {
|
||||
cacheUsage.style.backgroundColor = 'var(--accent-color)';
|
||||
} else {
|
||||
cacheUsage.style.backgroundColor = 'var(--primary-color)';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load cache stats:', error);
|
||||
}
|
||||
}
|
||||
|
||||
clearCacheButton.addEventListener('click', async () => {
|
||||
if (!confirm('Are you sure you want to clear the cache? This will delete all downloaded audio files.')) return;
|
||||
|
||||
try {
|
||||
await fetch('/api/cache', {
|
||||
method: 'DELETE',
|
||||
credentials: 'include', // Important for cookies to work
|
||||
});
|
||||
loadCacheStats();
|
||||
} catch (error) {
|
||||
console.error('Failed to clear cache:', error);
|
||||
alert('Failed to clear cache');
|
||||
}
|
||||
});
|
||||
|
||||
// Initial load
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
loadQueue();
|
||||
loadUsers();
|
||||
loadCacheStats();
|
||||
|
||||
// Refresh data periodically
|
||||
setInterval(loadQueue, 10000); // Every 10 seconds
|
||||
setInterval(loadCacheStats, 30000); // Every 30 seconds
|
||||
});
|
||||
+319
@@ -0,0 +1,319 @@
|
||||
/* Base styles */
|
||||
:root {
|
||||
--primary-color: #4a6da7;
|
||||
--primary-dark: #345a96;
|
||||
--secondary-color: #f5f5f5;
|
||||
--accent-color: #ff9800;
|
||||
--danger-color: #f44336;
|
||||
--text-color: #333333;
|
||||
--text-light: #ffffff;
|
||||
--border-color: #dddddd;
|
||||
--card-bg: #ffffff;
|
||||
--bg-color: #f0f2f5;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
|
||||
line-height: 1.6;
|
||||
color: var(--text-color);
|
||||
background-color: var(--bg-color);
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--primary-color);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
button {
|
||||
cursor: pointer;
|
||||
padding: 8px 16px;
|
||||
background-color: var(--primary-color);
|
||||
color: var(--text-light);
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background-color: var(--primary-dark);
|
||||
}
|
||||
|
||||
.danger-button {
|
||||
background-color: var(--danger-color);
|
||||
}
|
||||
|
||||
.danger-button:hover {
|
||||
background-color: #d32f2f;
|
||||
}
|
||||
|
||||
/* Layout */
|
||||
header {
|
||||
background-color: var(--primary-color);
|
||||
color: var(--text-light);
|
||||
padding: 1rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
header h1 {
|
||||
font-size: 1.5rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
nav {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
main {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
/* Cards */
|
||||
.card {
|
||||
background-color: var(--card-bg);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
margin-bottom: 1.5rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
padding: 1rem;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.card-header h3 {
|
||||
margin: 0;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.card-content {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
/* Tabs */
|
||||
.tab-button {
|
||||
background: transparent;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
border: none;
|
||||
padding: 8px 16px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.tab-button.active {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.tab-content.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Queue */
|
||||
.queue-list {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.queue-list li {
|
||||
padding: 10px;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.queue-list li:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.queue-list .now-playing {
|
||||
background-color: rgba(74, 109, 167, 0.1);
|
||||
border-left: 4px solid var(--primary-color);
|
||||
}
|
||||
|
||||
/* Tables */
|
||||
.data-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.data-table th, .data-table td {
|
||||
padding: 10px;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.data-table th {
|
||||
background-color: var(--secondary-color);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Forms */
|
||||
.form-group {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.form-group input, .form-group select {
|
||||
width: 100%;
|
||||
padding: 8px;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 4px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
/* Modal */
|
||||
.modal {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
z-index: 1000;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.modal.active {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background-color: var(--card-bg);
|
||||
border-radius: 8px;
|
||||
width: 90%;
|
||||
max-width: 500px;
|
||||
padding: 20px;
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.modal-content h3 {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
/* Stats */
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Progress bar */
|
||||
.progress-bar {
|
||||
width: 100%;
|
||||
height: 10px;
|
||||
background-color: #e0e0e0;
|
||||
border-radius: 5px;
|
||||
overflow: hidden;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.progress-value {
|
||||
height: 100%;
|
||||
background-color: var(--primary-color);
|
||||
}
|
||||
|
||||
/* Login page */
|
||||
.login-page {
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.login-container {
|
||||
width: 90%;
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
.login-container h1 {
|
||||
text-align: center;
|
||||
margin-bottom: 20px;
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.error {
|
||||
background-color: rgba(244, 67, 54, 0.1);
|
||||
color: var(--danger-color);
|
||||
padding: 10px;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 15px;
|
||||
border-left: 4px solid var(--danger-color);
|
||||
}
|
||||
|
||||
/* Empty states */
|
||||
.empty-message {
|
||||
text-align: center;
|
||||
color: #999;
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 768px) {
|
||||
header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
nav {
|
||||
margin-top: 10px;
|
||||
width: 100%;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user