init
This commit is contained in:
47
public/index.html
Normal file
47
public/index.html
Normal file
@@ -0,0 +1,47 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>PostgreSQL SensoLab Panel</title>
|
||||
<link rel="icon" type="image/svg+xml" href="favicon.svg">
|
||||
|
||||
<!-- Tailwind CSS -->
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script>
|
||||
tailwind.config = {
|
||||
darkMode: 'class',
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
primary: 'var(--color-primary)',
|
||||
'primary-dark': 'var(--color-primary-dark)',
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- Lucide Icons -->
|
||||
<script src="https://unpkg.com/lucide@latest"></script>
|
||||
|
||||
<!-- Fonts -->
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
|
||||
|
||||
<!-- Main Styles -->
|
||||
<link rel="stylesheet" href="/styles/main.css">
|
||||
<link rel="stylesheet" href="/styles/theme.css">
|
||||
<link rel="stylesheet" href="/styles/responsive.css">
|
||||
<link rel="stylesheet" href="/styles/animations.css">
|
||||
</head>
|
||||
<body class="bg-white dark:bg-slate-950 text-slate-800 dark:text-slate-100 transition-colors duration-300">
|
||||
<div id="app"></div>
|
||||
|
||||
<!-- Scripts -->
|
||||
<script src="/js/theme.js"></script>
|
||||
<script src="/js/api.js"></script>
|
||||
<script src="/js/auth.js"></script>
|
||||
<script src="/js/router.js"></script>
|
||||
<script src="/js/app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
145
public/js/api.js
Normal file
145
public/js/api.js
Normal file
@@ -0,0 +1,145 @@
|
||||
// API Helper - Centralized API calls
|
||||
class API {
|
||||
constructor() {
|
||||
this.baseURL = '/api';
|
||||
this.timeout = 10000;
|
||||
}
|
||||
|
||||
async request(endpoint, options = {}) {
|
||||
const {
|
||||
method = 'GET',
|
||||
headers = {},
|
||||
body = null,
|
||||
timeout = this.timeout
|
||||
} = options;
|
||||
|
||||
const url = `${this.baseURL}${endpoint}`;
|
||||
const config = {
|
||||
method,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...headers
|
||||
}
|
||||
};
|
||||
|
||||
if (body) {
|
||||
config.body = JSON.stringify(body);
|
||||
}
|
||||
|
||||
try {
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
||||
|
||||
const response = await fetch(url, {
|
||||
...config,
|
||||
signal: controller.signal
|
||||
});
|
||||
|
||||
clearTimeout(timeoutId);
|
||||
|
||||
if (!response.ok) {
|
||||
if (response.status === 401) {
|
||||
// Redirect to login if unauthorized
|
||||
window.location.hash = '#login';
|
||||
throw new Error('Unauthorized. Please login again.');
|
||||
}
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error('API Error:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Auth endpoints
|
||||
async login(email, password) {
|
||||
return this.request('/auth/login', {
|
||||
method: 'POST',
|
||||
body: { email, password }
|
||||
});
|
||||
}
|
||||
|
||||
async register(name, email, password) {
|
||||
return this.request('/auth/register', {
|
||||
method: 'POST',
|
||||
body: { name, email, password }
|
||||
});
|
||||
}
|
||||
|
||||
async logout() {
|
||||
return this.request('/auth/logout', { method: 'POST' });
|
||||
}
|
||||
|
||||
async getCurrentUser() {
|
||||
return this.request('/auth/me');
|
||||
}
|
||||
|
||||
// Users endpoints
|
||||
async getUsers() {
|
||||
return this.request('/users');
|
||||
}
|
||||
|
||||
async createUser(userData) {
|
||||
return this.request('/users', {
|
||||
method: 'POST',
|
||||
body: userData
|
||||
});
|
||||
}
|
||||
|
||||
async updateUser(userId, userData) {
|
||||
return this.request(`/users/${userId}`, {
|
||||
method: 'PATCH',
|
||||
body: userData
|
||||
});
|
||||
}
|
||||
|
||||
async deleteUser(userId) {
|
||||
return this.request(`/users/${userId}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
}
|
||||
|
||||
// Database endpoints
|
||||
async getTables() {
|
||||
return this.request('/db/tables');
|
||||
}
|
||||
|
||||
async getTableData(tableName, limit = 100, offset = 0) {
|
||||
return this.request(`/db/tables/${tableName}/data?limit=${limit}&offset=${offset}`);
|
||||
}
|
||||
|
||||
async executeQuery(sql) {
|
||||
return this.request('/db/query', {
|
||||
method: 'POST',
|
||||
body: { sql }
|
||||
});
|
||||
}
|
||||
|
||||
async getDatabaseStats() {
|
||||
return this.request('/db/stats');
|
||||
}
|
||||
|
||||
// Admin endpoints
|
||||
async getSystemStats() {
|
||||
return this.request('/admin/stats');
|
||||
}
|
||||
|
||||
async getLogs(limit = 100) {
|
||||
return this.request(`/admin/logs?limit=${limit}`);
|
||||
}
|
||||
|
||||
async getBackups() {
|
||||
return this.request('/admin/backups');
|
||||
}
|
||||
|
||||
async createBackup() {
|
||||
return this.request('/admin/backups', {
|
||||
method: 'POST'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Global API instance
|
||||
const api = new API();
|
||||
228
public/js/app.js
Normal file
228
public/js/app.js
Normal file
@@ -0,0 +1,228 @@
|
||||
// Main Application Handler
|
||||
class Application {
|
||||
constructor() {
|
||||
this.init();
|
||||
}
|
||||
|
||||
async init() {
|
||||
// Wait for auth check
|
||||
const isAuth = await auth.checkAuth();
|
||||
|
||||
if (isAuth) {
|
||||
// Redirect to dashboard
|
||||
if (window.location.hash === '' || window.location.hash === '#login') {
|
||||
window.location.hash = '#dashboard';
|
||||
}
|
||||
} else {
|
||||
// Redirect to login
|
||||
window.location.hash = '#login';
|
||||
}
|
||||
}
|
||||
|
||||
loadDashboardData() {
|
||||
this.loadStats();
|
||||
this.loadTablesList();
|
||||
}
|
||||
|
||||
async loadStats() {
|
||||
try {
|
||||
const stats = await api.getDatabaseStats();
|
||||
document.getElementById('statsTableCount').textContent = stats.tableCount || 0;
|
||||
document.getElementById('statsRecordCount').textContent = stats.recordCount || 0;
|
||||
document.getElementById('statsUserCount').textContent = stats.userCount || 0;
|
||||
document.getElementById('statsDbSize').textContent = stats.dbSize || '-';
|
||||
} catch (error) {
|
||||
console.error('Failed to load stats:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async loadTablesList() {
|
||||
try {
|
||||
const tables = await api.getTables();
|
||||
const tablesList = document.getElementById('tablesList');
|
||||
|
||||
if (tables.length === 0) {
|
||||
tablesList.innerHTML = '<p class="text-slate-500 dark:text-slate-400 text-sm">Таблицы не найдены</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
tablesList.innerHTML = tables.map(table => `
|
||||
<div class="flex items-center justify-between p-3 bg-slate-50 dark:bg-slate-800 rounded-lg hover:bg-slate-100 dark:hover:bg-slate-700 transition-colors cursor-pointer" onclick="window.location.hash='#tables'">
|
||||
<div class="flex items-center gap-2">
|
||||
<i data-lucide="table" class="w-4 h-4 text-blue-600 dark:text-blue-400"></i>
|
||||
<span class="font-medium text-slate-900 dark:text-white">${table.name}</span>
|
||||
</div>
|
||||
<span class="text-xs text-slate-500 dark:text-slate-400">${table.recordCount || 0} записей</span>
|
||||
</div>
|
||||
`).join('');
|
||||
|
||||
if (typeof lucide !== 'undefined') {
|
||||
lucide.createIcons();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load tables:', error);
|
||||
}
|
||||
}
|
||||
|
||||
loadAdminPanel() {
|
||||
this.loadUsers();
|
||||
}
|
||||
|
||||
async loadUsers() {
|
||||
try {
|
||||
const users = await api.getUsers();
|
||||
const usersList = document.getElementById('usersList');
|
||||
|
||||
if (!users || users.length === 0) {
|
||||
usersList.innerHTML = '<tr><td colspan="5" class="py-8 px-4 text-center text-slate-500 dark:text-slate-400">Пользователи не найдены</td></tr>';
|
||||
return;
|
||||
}
|
||||
|
||||
usersList.innerHTML = users.map(user => `
|
||||
<tr>
|
||||
<td class="py-3 px-4 font-medium text-slate-900 dark:text-white">${user.name}</td>
|
||||
<td class="py-3 px-4 text-slate-600 dark:text-slate-400 hidden sm:table-cell text-sm">${user.email}</td>
|
||||
<td class="py-3 px-4 text-slate-600 dark:text-slate-400">
|
||||
<span class="px-2 py-1 rounded bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300 text-xs font-medium">${this.getRoleName(user.role)}</span>
|
||||
</td>
|
||||
<td class="py-3 px-4">
|
||||
<span class="inline-flex items-center gap-1 px-2 py-1 rounded-full text-xs font-semibold ${user.active ? 'bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-400' : 'bg-slate-100 dark:bg-slate-800 text-slate-700 dark:text-slate-400'}">
|
||||
<i data-lucide="circle" class="w-2 h-2"></i>
|
||||
${user.active ? 'Активен' : 'Неактивен'}
|
||||
</span>
|
||||
</td>
|
||||
<td class="py-3 px-4 text-right">
|
||||
<div class="flex items-center justify-end gap-2">
|
||||
<button onclick="app.editUser(${user.id})" class="p-1 hover:bg-blue-100 dark:hover:bg-blue-900/30 rounded text-blue-600 dark:text-blue-400" title="Редактировать">
|
||||
<i data-lucide="edit" class="w-4 h-4"></i>
|
||||
</button>
|
||||
<button onclick="app.deleteUser(${user.id})" class="p-1 hover:bg-red-100 dark:hover:bg-red-900/30 rounded text-red-600 dark:text-red-400" title="Удалить">
|
||||
<i data-lucide="trash" class="w-4 h-4"></i>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
`).join('');
|
||||
|
||||
if (typeof lucide !== 'undefined') {
|
||||
lucide.createIcons();
|
||||
}
|
||||
|
||||
this.setupAdminPanelHandlers();
|
||||
} catch (error) {
|
||||
console.error('Failed to load users:', error);
|
||||
}
|
||||
}
|
||||
|
||||
setupAdminPanelHandlers() {
|
||||
// Add user button
|
||||
const addUserBtn = document.getElementById('addUserBtn');
|
||||
if (addUserBtn) {
|
||||
addUserBtn.onclick = () => this.showUserModal(null);
|
||||
}
|
||||
|
||||
// User modal controls
|
||||
const userModal = document.getElementById('userModal');
|
||||
const closeBtn = document.getElementById('closeUserModal');
|
||||
const cancelBtn = document.getElementById('cancelUserEdit');
|
||||
const userForm = document.getElementById('userForm');
|
||||
|
||||
if (closeBtn) closeBtn.onclick = () => userModal.classList.add('hidden');
|
||||
if (cancelBtn) cancelBtn.onclick = () => userModal.classList.add('hidden');
|
||||
|
||||
if (userForm) {
|
||||
userForm.onsubmit = (e) => this.handleUserFormSubmit(e);
|
||||
}
|
||||
}
|
||||
|
||||
showUserModal(userId) {
|
||||
const userModal = document.getElementById('userModal');
|
||||
const userForm = document.getElementById('userForm');
|
||||
const userModalTitle = document.getElementById('userModalTitle');
|
||||
|
||||
userForm.reset();
|
||||
userForm.dataset.userId = userId || '';
|
||||
|
||||
if (userId) {
|
||||
userModalTitle.textContent = 'Редактировать пользователя';
|
||||
// Load user data
|
||||
// TODO: implement
|
||||
} else {
|
||||
userModalTitle.textContent = 'Добавить пользователя';
|
||||
}
|
||||
|
||||
userModal.classList.remove('hidden');
|
||||
}
|
||||
|
||||
async handleUserFormSubmit(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const formData = {
|
||||
name: document.getElementById('userNameInput').value,
|
||||
email: document.getElementById('userEmailInput').value,
|
||||
role: document.getElementById('userRoleSelect').value,
|
||||
password: document.getElementById('userPasswordInput').value
|
||||
};
|
||||
|
||||
const userId = e.target.dataset.userId;
|
||||
|
||||
try {
|
||||
if (userId) {
|
||||
await api.updateUser(userId, formData);
|
||||
} else {
|
||||
await api.createUser(formData);
|
||||
}
|
||||
|
||||
document.getElementById('userModal').classList.add('hidden');
|
||||
this.loadUsers();
|
||||
} catch (error) {
|
||||
alert('Ошибка: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
async editUser(userId) {
|
||||
// TODO: implement
|
||||
this.showUserModal(userId);
|
||||
}
|
||||
|
||||
async deleteUser(userId) {
|
||||
if (confirm('Вы уверены, что хотите удалить этого пользователя?')) {
|
||||
try {
|
||||
await api.deleteUser(userId);
|
||||
this.loadUsers();
|
||||
} catch (error) {
|
||||
alert('Ошибка: ' + error.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getRoleName(role) {
|
||||
const names = {
|
||||
'superadmin': 'Суперадминистратор',
|
||||
'admin': 'Администратор',
|
||||
'moderator': 'Модератор',
|
||||
'viewer': 'Только просмотр'
|
||||
};
|
||||
return names[role] || role;
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize application
|
||||
const app = new Application();
|
||||
|
||||
// Update content when route changes
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
window.addEventListener('hashchange', () => {
|
||||
const currentRoute = window.location.hash.slice(1);
|
||||
|
||||
// Load dashboard data
|
||||
if (currentRoute.includes('dashboard') || currentRoute === '') {
|
||||
app.loadDashboardData();
|
||||
}
|
||||
|
||||
// Load admin panel
|
||||
if (currentRoute.includes('admin')) {
|
||||
app.loadAdminPanel();
|
||||
}
|
||||
});
|
||||
});
|
||||
212
public/js/auth.js
Normal file
212
public/js/auth.js
Normal file
@@ -0,0 +1,212 @@
|
||||
// Authentication Handler
|
||||
class Auth {
|
||||
constructor() {
|
||||
this.user = null;
|
||||
this.isAuthenticated = false;
|
||||
this.checkAuth();
|
||||
}
|
||||
|
||||
async checkAuth() {
|
||||
try {
|
||||
const response = await api.getCurrentUser();
|
||||
if (response.success) {
|
||||
this.user = response.user;
|
||||
this.isAuthenticated = true;
|
||||
return true;
|
||||
}
|
||||
} catch (error) {
|
||||
this.logout();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
async login(email, password) {
|
||||
try {
|
||||
const response = await api.login(email, password);
|
||||
if (response.success) {
|
||||
this.user = response.user;
|
||||
this.isAuthenticated = true;
|
||||
return { success: true, user: this.user };
|
||||
}
|
||||
return { success: false, error: response.message };
|
||||
} catch (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
async register(name, email, password) {
|
||||
try {
|
||||
const response = await api.register(name, email, password);
|
||||
if (response.success) {
|
||||
return { success: true, message: 'Регистрация успешна. Теперь вы можете войти.' };
|
||||
}
|
||||
return { success: false, error: response.message };
|
||||
} catch (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
async logout() {
|
||||
try {
|
||||
await api.logout();
|
||||
} catch (error) {
|
||||
console.error('Logout error:', error);
|
||||
}
|
||||
this.user = null;
|
||||
this.isAuthenticated = false;
|
||||
window.location.hash = '#login';
|
||||
}
|
||||
|
||||
hasRole(role) {
|
||||
return this.user && this.user.role === role;
|
||||
}
|
||||
|
||||
hasPermission(permission) {
|
||||
if (!this.user) return false;
|
||||
return this.user.permissions && this.user.permissions.includes(permission);
|
||||
}
|
||||
|
||||
getUser() {
|
||||
return this.user;
|
||||
}
|
||||
|
||||
isAdmin() {
|
||||
return this.user && (this.user.role === 'superadmin' || this.user.role === 'admin');
|
||||
}
|
||||
|
||||
isSuperAdmin() {
|
||||
return this.user && this.user.role === 'superadmin';
|
||||
}
|
||||
}
|
||||
|
||||
// Global auth instance
|
||||
const auth = new Auth();
|
||||
|
||||
// Login/Register form handler
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// Tab switching
|
||||
const authTabBtns = document.querySelectorAll('.auth-tab-btn');
|
||||
const authForms = document.querySelectorAll('.auth-form');
|
||||
|
||||
authTabBtns.forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
const tab = btn.dataset.tab;
|
||||
|
||||
// Update active tab
|
||||
authTabBtns.forEach(b => b.classList.remove('active'));
|
||||
btn.classList.add('active');
|
||||
|
||||
// Show/hide forms
|
||||
authForms.forEach(form => form.classList.add('hidden'));
|
||||
if (tab === 'login') {
|
||||
document.getElementById('loginForm').classList.remove('hidden');
|
||||
} else {
|
||||
document.getElementById('registerForm').classList.remove('hidden');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Login form
|
||||
const loginForm = document.getElementById('loginForm');
|
||||
if (loginForm) {
|
||||
loginForm.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const email = document.getElementById('loginEmail').value;
|
||||
const password = document.getElementById('loginPassword').value;
|
||||
|
||||
const loginText = document.querySelector('.login-text');
|
||||
const loginLoader = document.querySelector('.login-loader');
|
||||
|
||||
loginText.style.display = 'none';
|
||||
loginLoader.classList.remove('hidden');
|
||||
|
||||
const result = await auth.login(email, password);
|
||||
|
||||
if (result.success) {
|
||||
// Redirect to dashboard
|
||||
setTimeout(() => {
|
||||
window.location.hash = '#dashboard';
|
||||
}, 500);
|
||||
} else {
|
||||
showAuthError(result.error);
|
||||
loginText.style.display = 'block';
|
||||
loginLoader.classList.add('hidden');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Register form
|
||||
const registerForm = document.getElementById('registerForm');
|
||||
if (registerForm) {
|
||||
registerForm.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const name = document.getElementById('registerName').value;
|
||||
const email = document.getElementById('registerEmail').value;
|
||||
const password = document.getElementById('registerPassword').value;
|
||||
const confirmPassword = document.getElementById('registerPasswordConfirm').value;
|
||||
|
||||
if (password !== confirmPassword) {
|
||||
showAuthError('Пароли не совпадают');
|
||||
return;
|
||||
}
|
||||
|
||||
const registerText = document.querySelector('.register-text');
|
||||
const registerLoader = document.querySelector('.register-loader');
|
||||
|
||||
registerText.style.display = 'none';
|
||||
registerLoader.classList.remove('hidden');
|
||||
|
||||
const result = await auth.register(name, email, password);
|
||||
|
||||
if (result.success) {
|
||||
showAuthError(result.message, 'success');
|
||||
// Clear form and switch to login
|
||||
registerForm.reset();
|
||||
document.querySelector('[data-tab="login"]').click();
|
||||
} else {
|
||||
showAuthError(result.error);
|
||||
}
|
||||
|
||||
registerText.style.display = 'block';
|
||||
registerLoader.classList.add('hidden');
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
function showAuthError(message, type = 'error') {
|
||||
const errorDiv = document.getElementById('authError');
|
||||
const errorText = document.getElementById('authErrorText');
|
||||
|
||||
if (type === 'success') {
|
||||
errorDiv.classList.remove('bg-red-50', 'border-red-200', 'text-red-600', 'dark:bg-red-900/20', 'dark:border-red-800', 'dark:text-red-400');
|
||||
errorDiv.classList.add('bg-green-50', 'border-green-200', 'text-green-600', 'dark:bg-green-900/20', 'dark:border-green-800', 'dark:text-green-400');
|
||||
const icon = errorDiv.querySelector('i');
|
||||
if (icon) {
|
||||
icon.setAttribute('data-lucide', 'check-circle');
|
||||
}
|
||||
} else {
|
||||
errorDiv.classList.remove('bg-green-50', 'border-green-200', 'text-green-600', 'dark:bg-green-900/20', 'dark:border-green-800', 'dark:text-green-400');
|
||||
errorDiv.classList.add('bg-red-50', 'border-red-200', 'text-red-600', 'dark:bg-red-900/20', 'dark:border-red-800', 'dark:text-red-400');
|
||||
const icon = errorDiv.querySelector('i');
|
||||
if (icon) {
|
||||
icon.setAttribute('data-lucide', 'alert-circle');
|
||||
}
|
||||
}
|
||||
|
||||
errorText.textContent = message;
|
||||
errorDiv.classList.remove('hidden');
|
||||
|
||||
// Auto-hide success messages
|
||||
if (type === 'success') {
|
||||
setTimeout(() => {
|
||||
errorDiv.classList.add('hidden');
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
// Update Lucide icons
|
||||
if (typeof lucide !== 'undefined') {
|
||||
lucide.createIcons();
|
||||
}
|
||||
}
|
||||
205
public/js/router.js
Normal file
205
public/js/router.js
Normal file
@@ -0,0 +1,205 @@
|
||||
// Router - Single Page Application routing
|
||||
class Router {
|
||||
constructor() {
|
||||
this.routes = {
|
||||
'login': { module: 'authModule', handler: this.handleLogin.bind(this) },
|
||||
'register': { module: 'authModule', handler: this.handleLogin.bind(this) },
|
||||
'dashboard': { module: 'dashboardModule', requireAuth: true, handler: this.handleDashboard.bind(this) },
|
||||
'databases': { module: 'dashboardModule', requireAuth: true, handler: this.handleDatabases.bind(this) },
|
||||
'tables': { module: 'dashboardModule', requireAuth: true, handler: this.handleTables.bind(this) },
|
||||
'queries': { module: 'dashboardModule', requireAuth: true, handler: this.handleQueries.bind(this) },
|
||||
'admin-users': { module: 'adminModule', requireAuth: true, requireAdmin: true, handler: this.handleAdminUsers.bind(this) },
|
||||
'admin-roles': { module: 'adminModule', requireAuth: true, requireAdmin: true, handler: this.handleAdminRoles.bind(this) },
|
||||
'admin-settings': { module: 'adminModule', requireAuth: true, requireAdmin: true, handler: this.handleAdminSettings.bind(this) },
|
||||
'admin-logs': { module: 'adminModule', requireAuth: true, requireAdmin: true, handler: this.handleAdminLogs.bind(this) },
|
||||
'admin-database': { module: 'adminModule', requireAuth: true, requireAdmin: true, handler: this.handleAdminDatabase.bind(this) },
|
||||
'admin-backups': { module: 'adminModule', requireAuth: true, requireAdmin: true, handler: this.handleAdminBackups.bind(this) },
|
||||
};
|
||||
|
||||
this.currentRoute = null;
|
||||
this.modules = {};
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
async init() {
|
||||
// Load modules
|
||||
await this.loadModules();
|
||||
|
||||
// Setup hash change listener
|
||||
window.addEventListener('hashchange', () => this.navigate());
|
||||
|
||||
// Initial navigation
|
||||
this.navigate();
|
||||
}
|
||||
|
||||
async loadModules() {
|
||||
const modules = ['auth', 'dashboard', 'admin'];
|
||||
|
||||
for (const module of modules) {
|
||||
try {
|
||||
const response = await fetch(`/modules/${module}/${module === 'auth' ? 'login' : module === 'admin' ? 'admin-panel' : 'dashboard'}.html`);
|
||||
if (response.ok) {
|
||||
const html = await response.text();
|
||||
const container = document.createElement('div');
|
||||
container.innerHTML = html;
|
||||
document.getElementById('app').appendChild(container);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Failed to load ${module} module:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
// Update Lucide icons after loading modules
|
||||
if (typeof lucide !== 'undefined') {
|
||||
lucide.createIcons();
|
||||
}
|
||||
}
|
||||
|
||||
navigate() {
|
||||
const hash = window.location.hash.slice(1) || 'login';
|
||||
const route = this.routes[hash];
|
||||
|
||||
if (!route) {
|
||||
window.location.hash = '#login';
|
||||
return;
|
||||
}
|
||||
|
||||
// Check authentication
|
||||
if (route.requireAuth && !auth.isAuthenticated) {
|
||||
window.location.hash = '#login';
|
||||
return;
|
||||
}
|
||||
|
||||
// Check admin permission
|
||||
if (route.requireAdmin && !auth.isAdmin()) {
|
||||
window.location.hash = '#dashboard';
|
||||
return;
|
||||
}
|
||||
|
||||
// Hide all modules
|
||||
document.querySelectorAll('[id$="Module"]').forEach(mod => {
|
||||
mod.classList.add('hidden');
|
||||
});
|
||||
|
||||
// Show target module
|
||||
const module = document.getElementById(route.module);
|
||||
if (module) {
|
||||
module.classList.remove('hidden');
|
||||
}
|
||||
|
||||
// Close sidebars on mobile when navigating
|
||||
const sidebars = document.querySelectorAll('[id$="Sidebar"]');
|
||||
sidebars.forEach(sidebar => {
|
||||
sidebar.classList.add('hidden');
|
||||
});
|
||||
|
||||
// Call route handler
|
||||
if (route.handler) {
|
||||
route.handler();
|
||||
}
|
||||
|
||||
// Setup navigation event listeners for current route
|
||||
this.setupNavigation();
|
||||
|
||||
// Update Lucide icons
|
||||
if (typeof lucide !== 'undefined') {
|
||||
lucide.createIcons();
|
||||
}
|
||||
|
||||
this.currentRoute = hash;
|
||||
}
|
||||
|
||||
setupNavigation() {
|
||||
// Sidebar toggle
|
||||
const toggleBtn = document.getElementById('toggleSidebar');
|
||||
const sidebar = document.getElementById('sidebar');
|
||||
if (toggleBtn && sidebar) {
|
||||
toggleBtn.onclick = () => sidebar.classList.toggle('-translate-x-full');
|
||||
}
|
||||
|
||||
// Admin sidebar toggle
|
||||
const toggleAdminBtn = document.getElementById('toggleAdminSidebar');
|
||||
const adminSidebar = document.getElementById('adminSidebar');
|
||||
if (toggleAdminBtn && adminSidebar) {
|
||||
toggleAdminBtn.onclick = () => adminSidebar.classList.toggle('-translate-x-full');
|
||||
}
|
||||
|
||||
// Logout buttons
|
||||
const logoutBtn = document.getElementById('logoutBtn');
|
||||
if (logoutBtn) {
|
||||
logoutBtn.onclick = () => {
|
||||
if (confirm('Вы уверены, что хотите выйти?')) {
|
||||
auth.logout();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const adminLogoutBtn = document.getElementById('adminLogoutBtn');
|
||||
if (adminLogoutBtn) {
|
||||
adminLogoutBtn.onclick = () => {
|
||||
if (confirm('Вы уверены, что хотите выйти?')) {
|
||||
auth.logout();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Update user info
|
||||
if (auth.isAuthenticated && auth.user) {
|
||||
const userName = document.getElementById('userName');
|
||||
const userRole = document.getElementById('userRole');
|
||||
const avatarCircle = document.getElementById('avatarCircle');
|
||||
|
||||
if (userName) userName.textContent = auth.user.name || 'User';
|
||||
if (userRole) userRole.textContent = this.getRoleName(auth.user.role);
|
||||
if (avatarCircle) avatarCircle.textContent = (auth.user.name || 'A').charAt(0).toUpperCase();
|
||||
}
|
||||
|
||||
// Navigation items
|
||||
const navItems = document.querySelectorAll('.nav-item');
|
||||
navItems.forEach(item => {
|
||||
item.classList.remove('active');
|
||||
|
||||
if (item.getAttribute('href') === `#${this.currentRoute}`) {
|
||||
item.classList.add('active');
|
||||
}
|
||||
|
||||
item.addEventListener('click', () => {
|
||||
navItems.forEach(n => n.classList.remove('active'));
|
||||
item.classList.add('active');
|
||||
|
||||
// Close sidebar on mobile
|
||||
const sidebar = document.getElementById('sidebar') || document.getElementById('adminSidebar');
|
||||
if (sidebar) {
|
||||
sidebar.classList.add('-translate-x-full');
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
getRoleName(role) {
|
||||
const names = {
|
||||
'superadmin': 'Суперадминистратор',
|
||||
'admin': 'Администратор',
|
||||
'moderator': 'Модератор',
|
||||
'viewer': 'Только просмотр'
|
||||
};
|
||||
return names[role] || role;
|
||||
}
|
||||
|
||||
// Route handlers
|
||||
handleLogin() { }
|
||||
handleDashboard() { }
|
||||
handleDatabases() { }
|
||||
handleTables() { }
|
||||
handleQueries() { }
|
||||
handleAdminUsers() { }
|
||||
handleAdminRoles() { }
|
||||
handleAdminSettings() { }
|
||||
handleAdminLogs() { }
|
||||
handleAdminDatabase() { }
|
||||
handleAdminBackups() { }
|
||||
}
|
||||
|
||||
// Initialize router
|
||||
const router = new Router();
|
||||
76
public/js/theme.js
Normal file
76
public/js/theme.js
Normal file
@@ -0,0 +1,76 @@
|
||||
// Theme Management
|
||||
class ThemeManager {
|
||||
constructor() {
|
||||
this.storageKey = 'pgadmin-theme';
|
||||
this.darkClass = 'dark';
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
const savedTheme = localStorage.getItem(this.storageKey);
|
||||
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
const isDark = savedTheme ? savedTheme === 'dark' : prefersDark;
|
||||
|
||||
if (isDark) {
|
||||
this.setDark();
|
||||
} else {
|
||||
this.setLight();
|
||||
}
|
||||
|
||||
// Listen for system theme changes
|
||||
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => {
|
||||
if (!localStorage.getItem(this.storageKey)) {
|
||||
e.matches ? this.setDark() : this.setLight();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
toggle() {
|
||||
const html = document.documentElement;
|
||||
if (html.classList.contains(this.darkClass)) {
|
||||
this.setLight();
|
||||
} else {
|
||||
this.setDark();
|
||||
}
|
||||
}
|
||||
|
||||
setDark() {
|
||||
document.documentElement.classList.add(this.darkClass);
|
||||
localStorage.setItem(this.storageKey, 'dark');
|
||||
this.updateIcons();
|
||||
}
|
||||
|
||||
setLight() {
|
||||
document.documentElement.classList.remove(this.darkClass);
|
||||
localStorage.setItem(this.storageKey, 'light');
|
||||
this.updateIcons();
|
||||
}
|
||||
|
||||
isDark() {
|
||||
return document.documentElement.classList.contains(this.darkClass);
|
||||
}
|
||||
|
||||
updateIcons() {
|
||||
// Update Lucide icons if needed
|
||||
if (typeof lucide !== 'undefined') {
|
||||
lucide.createIcons();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize theme manager
|
||||
const themeManager = new ThemeManager();
|
||||
|
||||
// Theme toggle button handlers
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const themeButtons = [
|
||||
document.getElementById('toggleTheme'),
|
||||
document.getElementById('toggleAdminTheme')
|
||||
];
|
||||
|
||||
themeButtons.forEach(btn => {
|
||||
if (btn) {
|
||||
btn.addEventListener('click', () => themeManager.toggle());
|
||||
}
|
||||
});
|
||||
});
|
||||
164
public/modules/admin/admin-panel.html
Normal file
164
public/modules/admin/admin-panel.html
Normal file
@@ -0,0 +1,164 @@
|
||||
<!-- Admin Panel Module -->
|
||||
<div id="adminModule" class="admin-module hidden h-screen flex flex-col">
|
||||
<!-- Header (same as dashboard) -->
|
||||
<header class="bg-white dark:bg-slate-900 border-b border-slate-200 dark:border-slate-800 h-16 flex items-center justify-between px-4 sm:px-6 shadow-sm z-10">
|
||||
<div class="flex items-center gap-4">
|
||||
<button id="toggleAdminSidebar" class="p-2 hover:bg-slate-100 dark:hover:bg-slate-800 rounded-lg md:hidden">
|
||||
<i data-lucide="menu" class="w-6 h-6"></i>
|
||||
</button>
|
||||
<div class="flex items-center gap-2 text-blue-600 dark:text-blue-400">
|
||||
<i data-lucide="shield-admin" class="w-6 h-6"></i>
|
||||
<span class="font-bold text-lg hidden sm:inline">Администрирование</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-3">
|
||||
<button id="toggleAdminTheme" class="p-2 hover:bg-slate-100 dark:hover:bg-slate-800 rounded-lg transition-colors" title="Переключить тему">
|
||||
<i data-lucide="moon" class="w-5 h-5 dark:hidden"></i>
|
||||
<i data-lucide="sun" class="w-5 h-5 hidden dark:block"></i>
|
||||
</button>
|
||||
<div class="h-6 w-px bg-slate-200 dark:bg-slate-700 hidden sm:block"></div>
|
||||
<button id="adminLogoutBtn" class="p-2 hover:bg-red-50 dark:hover:bg-red-900/20 rounded-lg transition-colors" title="Выход">
|
||||
<i data-lucide="log-out" class="w-5 h-5 text-red-500"></i>
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="flex flex-1 overflow-hidden">
|
||||
<!-- Admin Sidebar -->
|
||||
<aside id="adminSidebar" class="fixed md:static inset-y-16 left-0 w-64 bg-white dark:bg-slate-900 border-r border-slate-200 dark:border-slate-800 overflow-y-auto transform -translate-x-full md:translate-x-0 transition-transform z-40">
|
||||
<nav class="p-4 space-y-2">
|
||||
<h3 class="px-4 py-2 text-xs font-semibold text-slate-500 dark:text-slate-400 uppercase tracking-wider">Управление</h3>
|
||||
|
||||
<a href="#admin-overview" class="nav-item active flex items-center gap-3 px-4 py-3 rounded-lg font-medium transition-colors">
|
||||
<i data-lucide="layout-dashboard" class="w-5 h-5"></i>
|
||||
<span>Обзор</span>
|
||||
</a>
|
||||
|
||||
<a href="#admin-users" class="nav-item flex items-center gap-3 px-4 py-3 rounded-lg font-medium transition-colors">
|
||||
<i data-lucide="users" class="w-5 h-5"></i>
|
||||
<span>Пользователи</span>
|
||||
</a>
|
||||
|
||||
<a href="#admin-roles" class="nav-item flex items-center gap-3 px-4 py-3 rounded-lg font-medium transition-colors">
|
||||
<i data-lucide="shield" class="w-5 h-5"></i>
|
||||
<span>Роли и права</span>
|
||||
</a>
|
||||
|
||||
<a href="#admin-logs" class="nav-item flex items-center gap-3 px-4 py-3 rounded-lg font-medium transition-colors">
|
||||
<i data-lucide="file-text" class="w-5 h-5"></i>
|
||||
<span>Логи</span>
|
||||
</a>
|
||||
|
||||
<div class="my-4 h-px bg-slate-200 dark:bg-slate-700"></div>
|
||||
|
||||
<h3 class="px-4 py-2 text-xs font-semibold text-slate-500 dark:text-slate-400 uppercase tracking-wider">Система</h3>
|
||||
|
||||
<a href="#admin-database" class="nav-item flex items-center gap-3 px-4 py-3 rounded-lg font-medium transition-colors">
|
||||
<i data-lucide="database" class="w-5 h-5"></i>
|
||||
<span>База данных</span>
|
||||
</a>
|
||||
|
||||
<a href="#admin-settings" class="nav-item flex items-center gap-3 px-4 py-3 rounded-lg font-medium transition-colors">
|
||||
<i data-lucide="settings" class="w-5 h-5"></i>
|
||||
<span>Настройки</span>
|
||||
</a>
|
||||
|
||||
<a href="#admin-backups" class="nav-item flex items-center gap-3 px-4 py-3 rounded-lg font-medium transition-colors">
|
||||
<i data-lucide="download" class="w-5 h-5"></i>
|
||||
<span>Резервные копии</span>
|
||||
</a>
|
||||
|
||||
<a href="#dashboard" class="nav-item flex items-center gap-3 px-4 py-3 rounded-lg font-medium transition-colors text-blue-600 dark:text-blue-400">
|
||||
<i data-lucide="arrow-left" class="w-5 h-5"></i>
|
||||
<span>К панели</span>
|
||||
</a>
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
<!-- Admin Content -->
|
||||
<main id="adminContent" class="flex-1 overflow-auto">
|
||||
<div class="p-4 sm:p-6 space-y-6">
|
||||
<!-- Users Section -->
|
||||
<section id="usersSection" class="space-y-4">
|
||||
<div class="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4 mb-4">
|
||||
<div>
|
||||
<h2 class="text-2xl font-bold text-slate-900 dark:text-white flex items-center gap-2">
|
||||
<i data-lucide="users" class="w-6 h-6"></i>
|
||||
Управление пользователями
|
||||
</h2>
|
||||
<p class="text-slate-500 dark:text-slate-400 text-sm mt-1">Добавляйте, редактируйте и удаляйте администраторов</p>
|
||||
</div>
|
||||
<button id="addUserBtn" class="btn btn-primary whitespace-nowrap flex items-center gap-2">
|
||||
<i data-lucide="plus" class="w-5 h-5"></i>
|
||||
<span class="hidden sm:inline">Добавить пользователя</span>
|
||||
<span class="sm:hidden">Добавить</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Users Table -->
|
||||
<div class="card overflow-x-auto">
|
||||
<table class="w-full text-sm">
|
||||
<thead class="border-b border-slate-200 dark:border-slate-700">
|
||||
<tr>
|
||||
<th class="text-left py-3 px-4 font-semibold text-slate-600 dark:text-slate-300">Имя</th>
|
||||
<th class="text-left py-3 px-4 font-semibold text-slate-600 dark:text-slate-300 hidden sm:table-cell">Email</th>
|
||||
<th class="text-left py-3 px-4 font-semibold text-slate-600 dark:text-slate-300">Роль</th>
|
||||
<th class="text-left py-3 px-4 font-semibold text-slate-600 dark:text-slate-300">Статус</th>
|
||||
<th class="text-right py-3 px-4 font-semibold text-slate-600 dark:text-slate-300">Действия</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="usersList" class="divide-y divide-slate-200 dark:divide-slate-700">
|
||||
<tr>
|
||||
<td colspan="5" class="py-8 px-4 text-center text-slate-500 dark:text-slate-400">
|
||||
Загрузка пользователей...
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Add/Edit User Modal -->
|
||||
<div id="userModal" class="hidden fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/50">
|
||||
<div class="bg-white dark:bg-slate-900 rounded-2xl shadow-xl max-w-md w-full max-h-[90vh] overflow-y-auto">
|
||||
<div class="sticky top-0 bg-white dark:bg-slate-900 p-6 border-b border-slate-200 dark:border-slate-700 flex items-center justify-between">
|
||||
<h3 class="text-xl font-bold text-slate-900 dark:text-white" id="userModalTitle">Добавить пользователя</h3>
|
||||
<button id="closeUserModal" class="p-1 hover:bg-slate-100 dark:hover:bg-slate-800 rounded-lg">
|
||||
<i data-lucide="x" class="w-6 h-6"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<form id="userForm" class="p-6 space-y-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">Полное имя</label>
|
||||
<input type="text" id="userNameInput" class="input-field" required>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">Email</label>
|
||||
<input type="email" id="userEmailInput" class="input-field" required>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">Роль</label>
|
||||
<select id="userRoleSelect" class="input-field">
|
||||
<option value="superadmin">Суперадминистратор</option>
|
||||
<option value="admin">Администратор</option>
|
||||
<option value="moderator">Модератор</option>
|
||||
<option value="viewer">Только просмотр</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">Пароль</label>
|
||||
<input type="password" id="userPasswordInput" class="input-field" placeholder="Оставить пусто, чтобы оставить без изменений">
|
||||
</div>
|
||||
<div class="flex gap-3 pt-4">
|
||||
<button type="submit" class="flex-1 btn btn-primary">Сохранить</button>
|
||||
<button type="button" id="cancelUserEdit" class="flex-1 btn btn-secondary">Отмена</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
98
public/modules/auth/login.html
Normal file
98
public/modules/auth/login.html
Normal file
@@ -0,0 +1,98 @@
|
||||
<!-- Auth Module - Login/Register Screen -->
|
||||
<div id="authModule" class="auth-module hidden">
|
||||
<!-- Login Screen -->
|
||||
<div id="loginScreen" class="fixed inset-0 z-50 flex items-center justify-center">
|
||||
<div class="absolute inset-0 bg-gradient-to-br from-slate-900 via-blue-900 to-slate-900 dark:from-slate-950 dark:via-blue-950 dark:to-slate-950"></div>
|
||||
|
||||
<div class="relative w-full max-w-md px-4 sm:px-0">
|
||||
<div class="glass-panel glass-panel-light dark:glass-panel-dark rounded-2xl shadow-2xl p-8">
|
||||
<!-- Header -->
|
||||
<div class="text-center mb-8">
|
||||
<div class="inline-flex items-center justify-center w-16 h-16 bg-gradient-to-br from-blue-600 to-blue-700 rounded-2xl mb-4 shadow-lg shadow-blue-600/30">
|
||||
<i data-lucide="database" class="w-8 h-8 text-white"></i>
|
||||
</div>
|
||||
<h1 class="text-2xl sm:text-3xl font-bold text-slate-900 dark:text-white">PostgreSQL SensoLab</h1>
|
||||
<p class="text-slate-500 dark:text-slate-400 mt-2 text-sm sm:text-base">Управление базой данных</p>
|
||||
</div>
|
||||
|
||||
<!-- Tab Navigation -->
|
||||
<div class="flex gap-2 mb-8 bg-slate-100 dark:bg-slate-800 p-1 rounded-lg">
|
||||
<button class="auth-tab-btn active flex-1 py-2 px-4 rounded-md font-medium transition-all text-sm sm:text-base" data-tab="login">
|
||||
Вход
|
||||
</button>
|
||||
<button class="auth-tab-btn flex-1 py-2 px-4 rounded-md font-medium transition-all text-sm sm:text-base" data-tab="register">
|
||||
Регистрация
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Login Form -->
|
||||
<form id="loginForm" class="auth-form space-y-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
|
||||
Email или логин
|
||||
</label>
|
||||
<input type="text" id="loginEmail" class="input-field" placeholder="admin@example.com" required>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
|
||||
Пароль
|
||||
</label>
|
||||
<input type="password" id="loginPassword" class="input-field" placeholder="••••••••" required>
|
||||
</div>
|
||||
<button type="submit" class="w-full btn btn-primary mt-6">
|
||||
<span class="login-text">Войти</span>
|
||||
<div class="login-loader hidden" style="width: 20px; height: 20px; border: 2px solid rgba(255,255,255,0.3); border-top: 2px solid white; border-radius: 50%; animation: spin 1s linear infinite;"></div>
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<!-- Register Form -->
|
||||
<form id="registerForm" class="auth-form hidden space-y-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
|
||||
Полное имя
|
||||
</label>
|
||||
<input type="text" id="registerName" class="input-field" placeholder="Иван Петров" required>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
|
||||
Email
|
||||
</label>
|
||||
<input type="email" id="registerEmail" class="input-field" placeholder="admin@example.com" required>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
|
||||
Пароль
|
||||
</label>
|
||||
<input type="password" id="registerPassword" class="input-field" placeholder="••••••••" required>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
|
||||
Подтверждение пароля
|
||||
</label>
|
||||
<input type="password" id="registerPasswordConfirm" class="input-field" placeholder="••••••••" required>
|
||||
</div>
|
||||
<button type="submit" class="w-full btn btn-primary mt-6">
|
||||
<span class="register-text">Создать аккаунт</span>
|
||||
<div class="register-loader hidden" style="width: 20px; height: 20px; border: 2px solid rgba(255,255,255,0.3); border-top: 2px solid white; border-radius: 50%; animation: spin 1s linear infinite;"></div>
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<!-- Error Message -->
|
||||
<div id="authError" class="mt-4 p-4 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg text-red-600 dark:text-red-400 text-sm hidden flex items-center gap-2">
|
||||
<i data-lucide="alert-circle" class="w-4 h-4"></i>
|
||||
<span id="authErrorText"></span>
|
||||
</div>
|
||||
|
||||
<!-- Demo Info -->
|
||||
<div class="mt-6 p-4 bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg text-blue-600 dark:text-blue-400 text-xs sm:text-sm">
|
||||
<p class="font-semibold mb-2 flex items-center gap-2">
|
||||
<i data-lucide="info" class="w-4 h-4"></i>
|
||||
Демо учетные данные:
|
||||
</p>
|
||||
<p>Email: <code class="bg-white/50 dark:bg-slate-900 px-2 py-1 rounded text-xs">admin@example.com</code></p>
|
||||
<p>Пароль: <code class="bg-white/50 dark:bg-slate-900 px-2 py-1 rounded text-xs">admin123</code></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
146
public/modules/dashboard/dashboard.html
Normal file
146
public/modules/dashboard/dashboard.html
Normal file
@@ -0,0 +1,146 @@
|
||||
<!-- Dashboard Module -->
|
||||
<div id="dashboardModule" class="dashboard-module hidden h-screen flex flex-col">
|
||||
<!-- Header -->
|
||||
<header class="bg-white dark:bg-slate-900 border-b border-slate-200 dark:border-slate-800 h-16 flex items-center justify-between px-4 sm:px-6 shadow-sm z-10">
|
||||
<div class="flex items-center gap-2 sm:gap-4">
|
||||
<button id="toggleSidebar" class="p-2 hover:bg-slate-100 dark:hover:bg-slate-800 rounded-lg md:hidden">
|
||||
<i data-lucide="menu" class="w-6 h-6"></i>
|
||||
</button>
|
||||
<div class="flex items-center gap-2 text-blue-600 dark:text-blue-400">
|
||||
<i data-lucide="database" class="w-6 h-6"></i>
|
||||
<span class="font-bold text-lg hidden sm:inline">SensoLab</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-3">
|
||||
<button id="toggleTheme" class="p-2 hover:bg-slate-100 dark:hover:bg-slate-800 rounded-lg transition-colors" title="Переключить тему">
|
||||
<i data-lucide="moon" class="w-5 h-5 dark:hidden"></i>
|
||||
<i data-lucide="sun" class="w-5 h-5 hidden dark:block"></i>
|
||||
</button>
|
||||
<div class="h-6 w-px bg-slate-200 dark:bg-slate-700 hidden sm:block"></div>
|
||||
<div class="flex items-center gap-2 text-sm">
|
||||
<div class="w-8 h-8 bg-blue-600 rounded-full flex items-center justify-center text-white font-semibold hidden sm:flex" id="avatarCircle">A</div>
|
||||
<div class="hidden sm:block">
|
||||
<p class="font-semibold text-slate-900 dark:text-white text-sm" id="userName">Admin</p>
|
||||
<p class="text-xs text-slate-500 dark:text-slate-400" id="userRole">Администратор</p>
|
||||
</div>
|
||||
</div>
|
||||
<button id="logoutBtn" class="p-2 hover:bg-red-50 dark:hover:bg-red-900/20 rounded-lg transition-colors" title="Выход">
|
||||
<i data-lucide="log-out" class="w-5 h-5 text-red-500"></i>
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="flex flex-1 overflow-hidden">
|
||||
<!-- Sidebar -->
|
||||
<aside id="sidebar" class="fixed md:static inset-y-16 left-0 w-64 bg-white dark:bg-slate-900 border-r border-slate-200 dark:border-slate-800 overflow-y-auto transform -translate-x-full md:translate-x-0 transition-transform z-40">
|
||||
<nav class="p-4 space-y-2">
|
||||
<h3 class="px-4 py-2 text-xs font-semibold text-slate-500 dark:text-slate-400 uppercase tracking-wider">Меню</h3>
|
||||
|
||||
<a href="#dashboard" class="nav-item active flex items-center gap-3 px-4 py-3 rounded-lg font-medium transition-colors">
|
||||
<i data-lucide="home" class="w-5 h-5"></i>
|
||||
<span>Панель управления</span>
|
||||
</a>
|
||||
|
||||
<a href="#databases" class="nav-item flex items-center gap-3 px-4 py-3 rounded-lg font-medium transition-colors">
|
||||
<i data-lucide="database" class="w-5 h-5"></i>
|
||||
<span>Базы данных</span>
|
||||
</a>
|
||||
|
||||
<a href="#tables" class="nav-item flex items-center gap-3 px-4 py-3 rounded-lg font-medium transition-colors">
|
||||
<i data-lucide="table" class="w-5 h-5"></i>
|
||||
<span>Таблицы</span>
|
||||
</a>
|
||||
|
||||
<a href="#queries" class="nav-item flex items-center gap-3 px-4 py-3 rounded-lg font-medium transition-colors">
|
||||
<i data-lucide="terminal" class="w-5 h-5"></i>
|
||||
<span>SQL Запросы</span>
|
||||
</a>
|
||||
|
||||
<div class="my-4 h-px bg-slate-200 dark:bg-slate-700"></div>
|
||||
|
||||
<h3 class="px-4 py-2 text-xs font-semibold text-slate-500 dark:text-slate-400 uppercase tracking-wider">Администрирование</h3>
|
||||
|
||||
<a href="#admin-users" class="nav-item flex items-center gap-3 px-4 py-3 rounded-lg font-medium transition-colors">
|
||||
<i data-lucide="users" class="w-5 h-5"></i>
|
||||
<span>Пользователи</span>
|
||||
</a>
|
||||
|
||||
<a href="#admin-settings" class="nav-item flex items-center gap-3 px-4 py-3 rounded-lg font-medium transition-colors">
|
||||
<i data-lucide="settings" class="w-5 h-5"></i>
|
||||
<span>Настройки</span>
|
||||
</a>
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
<!-- Main Content -->
|
||||
<main class="flex-1 overflow-auto">
|
||||
<div id="dashboardContent" class="p-4 sm:p-6 space-y-6">
|
||||
<!-- Stats Row -->
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
<div class="card">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-sm text-slate-500 dark:text-slate-400">Таблиц</p>
|
||||
<p class="text-2xl font-bold text-slate-900 dark:text-white" id="statsTableCount">0</p>
|
||||
</div>
|
||||
<div class="p-3 bg-blue-100 dark:bg-blue-900/30 rounded-lg">
|
||||
<i data-lucide="table" class="w-6 h-6 text-blue-600 dark:text-blue-400"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-sm text-slate-500 dark:text-slate-400">Записей</p>
|
||||
<p class="text-2xl font-bold text-slate-900 dark:text-white" id="statsRecordCount">0</p>
|
||||
</div>
|
||||
<div class="p-3 bg-green-100 dark:bg-green-900/30 rounded-lg">
|
||||
<i data-lucide="database" class="w-6 h-6 text-green-600 dark:text-green-400"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-sm text-slate-500 dark:text-slate-400">Пользователей</p>
|
||||
<p class="text-2xl font-bold text-slate-900 dark:text-white" id="statsUserCount">0</p>
|
||||
</div>
|
||||
<div class="p-3 bg-purple-100 dark:bg-purple-900/30 rounded-lg">
|
||||
<i data-lucide="users" class="w-6 h-6 text-purple-600 dark:text-purple-400"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-sm text-slate-500 dark:text-slate-400">Размер БД</p>
|
||||
<p class="text-2xl font-bold text-slate-900 dark:text-white" id="statsDbSize">-</p>
|
||||
</div>
|
||||
<div class="p-3 bg-orange-100 dark:bg-orange-900/30 rounded-lg">
|
||||
<i data-lucide="hard-drive" class="w-6 h-6 text-orange-600 dark:text-orange-400"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tables Preview -->
|
||||
<div class="card">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h2 class="text-lg font-semibold text-slate-900 dark:text-white flex items-center gap-2">
|
||||
<i data-lucide="table" class="w-5 h-5"></i>
|
||||
Таблицы
|
||||
</h2>
|
||||
<a href="#tables" class="text-sm text-blue-600 dark:text-blue-400 hover:underline">Все таблицы</a>
|
||||
</div>
|
||||
<div id="tablesList" class="space-y-2">
|
||||
<p class="text-slate-500 dark:text-slate-400 text-sm">Загрузка...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
275
public/styles/animations.css
Normal file
275
public/styles/animations.css
Normal file
@@ -0,0 +1,275 @@
|
||||
/* Animations */
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeOut {
|
||||
from {
|
||||
opacity: 1;
|
||||
}
|
||||
to {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideInLeft {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateX(-30px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideInRight {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateX(30px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideInDown {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-30px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(30px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% {
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes bounce {
|
||||
0%, 100% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
50% {
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes shimmer {
|
||||
0% {
|
||||
background-position: -1000px 0;
|
||||
}
|
||||
100% {
|
||||
background-position: 1000px 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes float {
|
||||
0%, 100% {
|
||||
transform: translateY(0px);
|
||||
}
|
||||
50% {
|
||||
transform: translateY(-20px);
|
||||
}
|
||||
}
|
||||
|
||||
/* Animation classes */
|
||||
|
||||
.animate-fade-in {
|
||||
animation: fadeIn 0.3s ease-in;
|
||||
}
|
||||
|
||||
.animate-fade-out {
|
||||
animation: fadeOut 0.3s ease-out;
|
||||
}
|
||||
|
||||
.animate-slide-in-left {
|
||||
animation: slideInLeft 0.3s ease-out;
|
||||
}
|
||||
|
||||
.animate-slide-in-right {
|
||||
animation: slideInRight 0.3s ease-out;
|
||||
}
|
||||
|
||||
.animate-slide-in-down {
|
||||
animation: slideInDown 0.3s ease-out;
|
||||
}
|
||||
|
||||
.animate-slide-in-up {
|
||||
animation: slideInUp 0.3s ease-out;
|
||||
}
|
||||
|
||||
.animate-pulse {
|
||||
animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
||||
}
|
||||
|
||||
.animate-bounce {
|
||||
animation: bounce 1s infinite;
|
||||
}
|
||||
|
||||
.animate-spin {
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
.animate-shimmer {
|
||||
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
|
||||
background-size: 1000px 100%;
|
||||
animation: shimmer 2s infinite;
|
||||
}
|
||||
|
||||
.animate-float {
|
||||
animation: float 3s ease-in-out infinite;
|
||||
}
|
||||
|
||||
/* Transition classes */
|
||||
|
||||
.transition-all {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.transition-colors {
|
||||
transition: color 0.3s ease, background-color 0.3s ease, border-color 0.3s ease;
|
||||
}
|
||||
|
||||
.transition-transform {
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.transition-opacity {
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.transition-fast {
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.transition-slow {
|
||||
transition: all 0.5s ease;
|
||||
}
|
||||
|
||||
/* Hover effects */
|
||||
|
||||
.hover-lift {
|
||||
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
||||
}
|
||||
|
||||
.hover-lift:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.hover-scale:hover {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.hover-brighten:hover {
|
||||
filter: brightness(1.1);
|
||||
}
|
||||
|
||||
/* Transform utilities */
|
||||
|
||||
.scale-95 {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
.scale-100 {
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
.scale-105 {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.scale-110 {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
/* Opacity utilities */
|
||||
|
||||
.opacity-0 {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.opacity-50 {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.opacity-75 {
|
||||
opacity: 0.75;
|
||||
}
|
||||
|
||||
.opacity-100 {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Loading skeleton */
|
||||
|
||||
.skeleton {
|
||||
background: linear-gradient(90deg, #e0e0e0 25%, #f0f0f0 50%, #e0e0e0 75%);
|
||||
background-size: 200% 100%;
|
||||
animation: loading 1.5s infinite;
|
||||
}
|
||||
|
||||
.dark .skeleton {
|
||||
background: linear-gradient(90deg, #333 25%, #444 50%, #333 75%);
|
||||
background-size: 200% 100%;
|
||||
}
|
||||
|
||||
@keyframes loading {
|
||||
0% {
|
||||
background-position: 200% 0;
|
||||
}
|
||||
100% {
|
||||
background-position: -200% 0;
|
||||
}
|
||||
}
|
||||
|
||||
.skeleton-text {
|
||||
height: 1rem;
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
|
||||
.skeleton-circle {
|
||||
border-radius: 50%;
|
||||
height: 2.5rem;
|
||||
width: 2.5rem;
|
||||
}
|
||||
|
||||
.skeleton-card {
|
||||
border-radius: 0.75rem;
|
||||
height: 100%;
|
||||
}
|
||||
266
public/styles/main.css
Normal file
266
public/styles/main.css
Normal file
@@ -0,0 +1,266 @@
|
||||
/* Main Styles */
|
||||
|
||||
:root {
|
||||
--color-primary: #3b82f6;
|
||||
--color-primary-dark: #1e40af;
|
||||
--color-success: #10b981;
|
||||
--color-warning: #f59e0b;
|
||||
--color-danger: #ef4444;
|
||||
--transition-speed: 0.3s;
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Inter', sans-serif;
|
||||
}
|
||||
|
||||
.font-mono {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
}
|
||||
|
||||
/* Common Components */
|
||||
|
||||
.card {
|
||||
@apply bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-800 rounded-xl shadow-sm hover:shadow-md transition-shadow;
|
||||
}
|
||||
|
||||
.glass-panel {
|
||||
@apply backdrop-filter backdrop-blur-md border;
|
||||
}
|
||||
|
||||
.glass-panel-light {
|
||||
@apply bg-white/95 border-slate-200/50;
|
||||
}
|
||||
|
||||
.glass-panel-dark {
|
||||
@apply bg-slate-900/95 dark:bg-slate-900/95 border-slate-700/50;
|
||||
}
|
||||
|
||||
/* Buttons */
|
||||
|
||||
.btn {
|
||||
@apply px-4 py-2 rounded-lg font-medium transition-all duration-200 flex items-center justify-center gap-2;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
@apply bg-blue-600 hover:bg-blue-700 text-white shadow-lg shadow-blue-600/30 hover:shadow-blue-600/50 active:scale-95;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
@apply bg-slate-100 dark:bg-slate-800 text-slate-700 dark:text-slate-300 hover:bg-slate-200 dark:hover:bg-slate-700 active:scale-95;
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
@apply bg-red-600 hover:bg-red-700 text-white shadow-lg shadow-red-600/30 active:scale-95;
|
||||
}
|
||||
|
||||
/* Input Fields */
|
||||
|
||||
.input-field {
|
||||
@apply w-full px-4 py-2.5 border border-slate-300 dark:border-slate-600 rounded-lg bg-white dark:bg-slate-800 text-slate-900 dark:text-white placeholder-slate-400 dark:placeholder-slate-500 focus:ring-2 focus:ring-blue-500 focus:border-transparent dark:focus:ring-blue-400 outline-none transition-all;
|
||||
}
|
||||
|
||||
.input-field:disabled {
|
||||
@apply bg-slate-100 dark:bg-slate-900 cursor-not-allowed opacity-50;
|
||||
}
|
||||
|
||||
/* Navigation Items */
|
||||
|
||||
.nav-item {
|
||||
@apply text-slate-600 dark:text-slate-400 hover:bg-slate-100 dark:hover:bg-slate-800 hover:text-slate-900 dark:hover:text-white;
|
||||
}
|
||||
|
||||
.nav-item.active {
|
||||
@apply bg-blue-50 dark:bg-blue-900/30 text-blue-600 dark:text-blue-400 font-semibold;
|
||||
}
|
||||
|
||||
/* Auth Tab Buttons */
|
||||
|
||||
.auth-tab-btn {
|
||||
@apply text-slate-700 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white transition-all;
|
||||
}
|
||||
|
||||
.auth-tab-btn.active {
|
||||
@apply bg-white dark:bg-slate-900 text-slate-900 dark:text-white shadow-sm;
|
||||
}
|
||||
|
||||
/* Animations */
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateX(-20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% {
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.fade-in {
|
||||
animation: fadeIn var(--transition-speed) ease-in;
|
||||
}
|
||||
|
||||
.slide-in {
|
||||
animation: slideIn var(--transition-speed) ease-in;
|
||||
}
|
||||
|
||||
/* Scrollbar Styling */
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: #f1f5f9;
|
||||
}
|
||||
|
||||
.dark ::-webkit-scrollbar-track {
|
||||
background: #1e293b;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #cbd5e1;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: #94a3b8;
|
||||
}
|
||||
|
||||
.dark ::-webkit-scrollbar-thumb {
|
||||
background: #475569;
|
||||
}
|
||||
|
||||
.dark ::-webkit-scrollbar-thumb:hover {
|
||||
background: #64748b;
|
||||
}
|
||||
|
||||
/* Status Indicators */
|
||||
|
||||
.status-badge {
|
||||
@apply inline-flex items-center gap-1 px-3 py-1 rounded-full text-xs font-semibold;
|
||||
}
|
||||
|
||||
.status-online {
|
||||
@apply bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-400;
|
||||
}
|
||||
|
||||
.status-offline {
|
||||
@apply bg-slate-100 dark:bg-slate-800 text-slate-700 dark:text-slate-400;
|
||||
}
|
||||
|
||||
.status-warning {
|
||||
@apply bg-yellow-100 dark:bg-yellow-900/30 text-yellow-700 dark:text-yellow-400;
|
||||
}
|
||||
|
||||
.status-error {
|
||||
@apply bg-red-100 dark:bg-red-900/30 text-red-700 dark:text-red-400;
|
||||
}
|
||||
|
||||
/* Loading Spinner */
|
||||
|
||||
.loader {
|
||||
display: inline-block;
|
||||
border: 3px solid rgba(100, 116, 139, 0.2);
|
||||
border-top-color: #3b82f6;
|
||||
border-radius: 50%;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
animation: spin 0.8s linear infinite;
|
||||
}
|
||||
|
||||
/* Tables */
|
||||
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
thead {
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.dark thead {
|
||||
background: #1e293b;
|
||||
}
|
||||
|
||||
tbody tr {
|
||||
@apply hover:bg-slate-50 dark:hover:bg-slate-800 transition-colors;
|
||||
}
|
||||
|
||||
td, th {
|
||||
padding: 0.75rem;
|
||||
}
|
||||
|
||||
/* Modal Overlay */
|
||||
|
||||
.modal-overlay {
|
||||
@apply fixed inset-0 bg-black/50 backdrop-blur-sm z-50 flex items-center justify-center;
|
||||
}
|
||||
|
||||
/* Utility Classes */
|
||||
|
||||
.text-truncate {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.line-clamp-2 {
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.shadow-sm {
|
||||
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
/* Focus visible for accessibility */
|
||||
|
||||
:focus-visible {
|
||||
outline: 2px solid var(--color-primary);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
input:focus-visible,
|
||||
button:focus-visible,
|
||||
select:focus-visible {
|
||||
outline: none !important;
|
||||
}
|
||||
279
public/styles/responsive.css
Normal file
279
public/styles/responsive.css
Normal file
@@ -0,0 +1,279 @@
|
||||
/* Responsive Design - Mobile First */
|
||||
|
||||
/* Extra Small Devices (< 640px) */
|
||||
@media (max-width: 639px) {
|
||||
body {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.card {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 0.625rem 1rem;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.btn-icon {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
/* Hide elements on small screens */
|
||||
.hidden-mobile {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Stack grid on mobile */
|
||||
.grid-responsive {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
/* Full width modals */
|
||||
.modal {
|
||||
margin: 1rem;
|
||||
}
|
||||
|
||||
/* Navigation sidebar to drawer on mobile */
|
||||
#sidebar {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 4rem;
|
||||
height: calc(100vh - 4rem);
|
||||
width: 100%;
|
||||
max-width: 256px;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
#sidebar.hidden {
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
|
||||
/* Adjust table for small screens */
|
||||
table {
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
thead th {
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
tbody td {
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
/* Stack table rows */
|
||||
.table-responsive {
|
||||
display: block;
|
||||
overflow-x: auto;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Adjust forms */
|
||||
.form-group {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
/* Responsive header */
|
||||
header {
|
||||
padding: 0.5rem 1rem;
|
||||
}
|
||||
|
||||
.header-title {
|
||||
font-size: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Small Devices (640px - 768px) */
|
||||
@media (min-width: 640px) and (max-width: 767px) {
|
||||
.card {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.grid-responsive {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
table {
|
||||
font-size: 0.9375rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Medium Devices (768px - 1024px) && ipads */
|
||||
@media (min-width: 768px) {
|
||||
.hidden-md-down {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.hidden-md-up {
|
||||
display: block;
|
||||
}
|
||||
|
||||
#sidebar {
|
||||
transform: translateX(0) !important;
|
||||
position: static;
|
||||
}
|
||||
|
||||
.grid-responsive {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
/* Large Devices (1024px+) && desktops */
|
||||
@media (min-width: 1024px) {
|
||||
.grid-responsive {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
|
||||
.grid-responsive.grid-4 {
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
}
|
||||
|
||||
#sidebar {
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
.hidden-lg-down {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.hidden-lg-up {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Show more columns on desktop */
|
||||
.table-hidden-mobile {
|
||||
display: table-cell;
|
||||
}
|
||||
}
|
||||
|
||||
/* Extra Large Devices (1280px+) */
|
||||
@media (min-width: 1280px) {
|
||||
body {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.container-xl {
|
||||
max-width: 1280px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.grid-responsive.grid-4 {
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
}
|
||||
|
||||
.grid-responsive.grid-5 {
|
||||
grid-template-columns: repeat(5, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
/* Portrait vs Landscape */
|
||||
@media (orientation: portrait) {
|
||||
.landscape-hidden {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (orientation: landscape) {
|
||||
.portrait-hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.h-screen {
|
||||
max-height: 100vh;
|
||||
}
|
||||
}
|
||||
|
||||
/* Touch devices optimization */
|
||||
@media (hover: none) and (pointer: coarse) {
|
||||
.btn,
|
||||
.nav-item {
|
||||
padding: 1rem;
|
||||
min-height: 44px;
|
||||
min-width: 44px;
|
||||
}
|
||||
|
||||
/* Remove hover effects on touch devices */
|
||||
.btn:hover {
|
||||
transform: none;
|
||||
}
|
||||
|
||||
button:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
}
|
||||
|
||||
/* Print styles */
|
||||
@media print {
|
||||
header,
|
||||
nav,
|
||||
.no-print {
|
||||
display: none;
|
||||
}
|
||||
|
||||
body {
|
||||
background: white;
|
||||
color: black;
|
||||
}
|
||||
|
||||
.card {
|
||||
page-break-inside: avoid;
|
||||
border: 1px solid #ccc;
|
||||
}
|
||||
|
||||
.btn {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* High DPI screens (Retina) */
|
||||
@media (-webkit-min-device-pixel-ratio: 2), (min-resolution: 192dpi) {
|
||||
/* Optimize images and borders */
|
||||
body {
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
}
|
||||
|
||||
/* Reduced motion preference */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
* {
|
||||
animation-duration: 0.01ms !important;
|
||||
animation-iteration-count: 1 !important;
|
||||
transition-duration: 0.01ms !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Dark mode media query */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
color-scheme: dark;
|
||||
}
|
||||
}
|
||||
|
||||
/* Light mode media query */
|
||||
@media (prefers-color-scheme: light) {
|
||||
:root {
|
||||
color-scheme: light;
|
||||
}
|
||||
}
|
||||
192
public/styles/theme.css
Normal file
192
public/styles/theme.css
Normal file
@@ -0,0 +1,192 @@
|
||||
/* Theme Support - Light/Dark Mode */
|
||||
|
||||
:root {
|
||||
--bg-primary: #ffffff;
|
||||
--bg-secondary: #f8fafc;
|
||||
--bg-tertiary: #f1f5f9;
|
||||
--text-primary: #1e293b;
|
||||
--text-secondary: #64748b;
|
||||
--text-tertiary: #94a3b8;
|
||||
--border-color: #e2e8f0;
|
||||
--shadow-color: rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
:root.dark {
|
||||
--bg-primary: #0f172a;
|
||||
--bg-secondary: #1e293b;
|
||||
--bg-tertiary: #334155;
|
||||
--text-primary: #f1f5f9;
|
||||
--text-secondary: #cbd5e1;
|
||||
--text-tertiary: #94a3b8;
|
||||
--border-color: #475569;
|
||||
--shadow-color: rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
/* Smooth transitions for theme switching */
|
||||
|
||||
html {
|
||||
transition: background-color 0.3s ease, color 0.3s ease;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: var(--bg-primary);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.dark body {
|
||||
background-color: var(--bg-secondary);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
/* Background gradients for different themes */
|
||||
|
||||
.bg-gradient-light {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
}
|
||||
|
||||
.bg-gradient-dark {
|
||||
background: linear-gradient(135deg, #1e3a8a 0%, #1e40af 100%);
|
||||
}
|
||||
|
||||
/* Text colors that change with theme */
|
||||
|
||||
.text-muted {
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.text-muted-dark {
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
|
||||
/* Shadow adjustments */
|
||||
|
||||
.shadow-light {
|
||||
box-shadow: 0 4px 6px -1px var(--shadow-color);
|
||||
}
|
||||
|
||||
.shadow-dark {
|
||||
box-shadow: 0 10px 15px -3px var(--shadow-color);
|
||||
}
|
||||
|
||||
/* Input field styling for dark mode */
|
||||
|
||||
.input-field {
|
||||
background-color: var(--bg-primary);
|
||||
border-color: var(--border-color);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.input-field::placeholder {
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
|
||||
/* Card styling for dark mode */
|
||||
|
||||
.card {
|
||||
background-color: var(--bg-primary);
|
||||
border-color: var(--border-color);
|
||||
}
|
||||
|
||||
.dark .card {
|
||||
background-color: var(--bg-secondary);
|
||||
border-color: var(--border-color);
|
||||
}
|
||||
|
||||
/* Header and navigation */
|
||||
|
||||
header {
|
||||
background-color: var(--bg-primary);
|
||||
border-color: var(--border-color);
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
background-color: var(--bg-primary);
|
||||
border-color: var(--border-color);
|
||||
}
|
||||
|
||||
/* Table styling */
|
||||
|
||||
thead {
|
||||
background-color: var(--bg-secondary);
|
||||
border-color: var(--border-color);
|
||||
}
|
||||
|
||||
tbody {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.glass-panel {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
backdrop-filter: blur(10px);
|
||||
border-color: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.dark .glass-panel {
|
||||
background: rgba(15, 23, 42, 0.95);
|
||||
backdrop-filter: blur(10px);
|
||||
border-color: rgba(148, 163, 184, 0.1);
|
||||
}
|
||||
|
||||
/* Hover states */
|
||||
|
||||
.hover-light:hover {
|
||||
background-color: var(--bg-tertiary);
|
||||
}
|
||||
|
||||
.dark .hover-light:hover {
|
||||
background-color: var(--bg-tertiary);
|
||||
}
|
||||
|
||||
/* Color overlays */
|
||||
|
||||
.color-overlay-blue {
|
||||
background: linear-gradient(135deg, rgba(59, 130, 246, 0.1), rgba(59, 130, 246, 0.05));
|
||||
}
|
||||
|
||||
.dark .color-overlay-blue {
|
||||
background: linear-gradient(135deg, rgba(59, 130, 246, 0.2), rgba(59, 130, 246, 0.1));
|
||||
}
|
||||
|
||||
/* Auth screen theme */
|
||||
|
||||
#authModule {
|
||||
background: linear-gradient(to bottom right, #1e3a8a, #1e40af);
|
||||
}
|
||||
|
||||
.dark #authModule {
|
||||
background: linear-gradient(to bottom right, #0f172a, #1e293b);
|
||||
}
|
||||
|
||||
/* Status colors */
|
||||
|
||||
.status-success {
|
||||
color: #10b981;
|
||||
}
|
||||
|
||||
.dark .status-success {
|
||||
color: #6ee7b7;
|
||||
}
|
||||
|
||||
.status-warning {
|
||||
color: #f59e0b;
|
||||
}
|
||||
|
||||
.dark .status-warning {
|
||||
color: #fbbf24;
|
||||
}
|
||||
|
||||
.status-error {
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.dark .status-error {
|
||||
color: #f87171;
|
||||
}
|
||||
|
||||
.status-info {
|
||||
color: #3b82f6;
|
||||
}
|
||||
|
||||
.dark .status-info {
|
||||
color: #60a5fa;
|
||||
}
|
||||
Reference in New Issue
Block a user