mirror of
https://github.com/yggdrasil-network/yggdrasil-go.git
synced 2025-08-24 16:05:07 +03:00
Refactor static file serving in WebUI to allow CSS and JS access without authentication, and implement theme toggle functionality in login and main pages
This commit is contained in:
parent
fc354865ea
commit
3187114780
7 changed files with 408 additions and 126 deletions
|
@ -15,11 +15,20 @@ import (
|
||||||
|
|
||||||
// setupStaticHandler configures static file serving for development (files from disk)
|
// setupStaticHandler configures static file serving for development (files from disk)
|
||||||
func setupStaticHandler(mux *http.ServeMux, server *WebUIServer) {
|
func setupStaticHandler(mux *http.ServeMux, server *WebUIServer) {
|
||||||
// Serve static files from disk for development - with auth
|
// Serve static files from disk for development
|
||||||
staticHandler := http.StripPrefix("/static/", http.FileServer(http.Dir("src/webui/static/")))
|
staticHandler := http.StripPrefix("/static/", http.FileServer(http.Dir("src/webui/static/")))
|
||||||
mux.HandleFunc("/static/", server.authMiddleware(func(rw http.ResponseWriter, r *http.Request) {
|
mux.HandleFunc("/static/", func(rw http.ResponseWriter, r *http.Request) {
|
||||||
staticHandler.ServeHTTP(rw, r)
|
// Allow access to CSS and JS files without auth (needed for login page)
|
||||||
}))
|
path := strings.TrimPrefix(r.URL.Path, "/static/")
|
||||||
|
if strings.HasSuffix(path, ".css") || strings.HasSuffix(path, ".js") {
|
||||||
|
staticHandler.ServeHTTP(rw, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// For other static files, require auth
|
||||||
|
server.authMiddleware(func(rw http.ResponseWriter, r *http.Request) {
|
||||||
|
staticHandler.ServeHTTP(rw, r)
|
||||||
|
})(rw, r)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// serveFile serves any file from disk or returns 404 if not found
|
// serveFile serves any file from disk or returns 404 if not found
|
||||||
|
|
|
@ -25,12 +25,22 @@ func setupStaticHandler(mux *http.ServeMux, server *WebUIServer) {
|
||||||
panic("failed to get embedded static files: " + err.Error())
|
panic("failed to get embedded static files: " + err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Serve static files from embedded FS - with auth
|
// Serve static files from embedded FS
|
||||||
staticHandler := http.FileServer(http.FS(staticFS))
|
staticHandler := http.FileServer(http.FS(staticFS))
|
||||||
mux.HandleFunc("/static/", server.authMiddleware(func(rw http.ResponseWriter, r *http.Request) {
|
mux.HandleFunc("/static/", func(rw http.ResponseWriter, r *http.Request) {
|
||||||
// Strip the /static/ prefix before serving
|
// Allow access to CSS and JS files without auth (needed for login page)
|
||||||
http.StripPrefix("/static/", staticHandler).ServeHTTP(rw, r)
|
path := strings.TrimPrefix(r.URL.Path, "/static/")
|
||||||
}))
|
if strings.HasSuffix(path, ".css") || strings.HasSuffix(path, ".js") {
|
||||||
|
// Strip the /static/ prefix before serving
|
||||||
|
http.StripPrefix("/static/", staticHandler).ServeHTTP(rw, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// For other static files, require auth
|
||||||
|
server.authMiddleware(func(rw http.ResponseWriter, r *http.Request) {
|
||||||
|
// Strip the /static/ prefix before serving
|
||||||
|
http.StripPrefix("/static/", staticHandler).ServeHTTP(rw, r)
|
||||||
|
})(rw, r)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// serveFile serves any file from embedded files or returns 404 if not found
|
// serveFile serves any file from embedded files or returns 404 if not found
|
||||||
|
|
|
@ -19,7 +19,10 @@
|
||||||
<p data-key="subtitle">Network mesh management dashboard</p>
|
<p data-key="subtitle">Network mesh management dashboard</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="header-actions">
|
<div class="header-actions">
|
||||||
<div class="language-switcher">
|
<div class="controls-group">
|
||||||
|
<button onclick="toggleTheme()" class="theme-btn" id="theme-btn">
|
||||||
|
<span class="theme-icon">🌙</span>
|
||||||
|
</button>
|
||||||
<button onclick="switchLanguage('ru')" class="lang-btn" id="lang-ru">RU</button>
|
<button onclick="switchLanguage('ru')" class="lang-btn" id="lang-ru">RU</button>
|
||||||
<button onclick="switchLanguage('en')" class="lang-btn" id="lang-en">EN</button>
|
<button onclick="switchLanguage('en')" class="lang-btn" id="lang-en">EN</button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -126,6 +129,7 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
let currentLanguage = localStorage.getItem('yggdrasil-language') || 'ru';
|
let currentLanguage = localStorage.getItem('yggdrasil-language') || 'ru';
|
||||||
|
let currentTheme = localStorage.getItem('yggdrasil-theme') || 'light';
|
||||||
|
|
||||||
function updateTexts() {
|
function updateTexts() {
|
||||||
const elements = document.querySelectorAll('[data-key]');
|
const elements = document.querySelectorAll('[data-key]');
|
||||||
|
@ -137,6 +141,28 @@
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toggleTheme() {
|
||||||
|
currentTheme = currentTheme === 'light' ? 'dark' : 'light';
|
||||||
|
localStorage.setItem('yggdrasil-theme', currentTheme);
|
||||||
|
applyTheme();
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyTheme() {
|
||||||
|
const body = document.body;
|
||||||
|
const themeIcon = document.querySelector('.theme-icon');
|
||||||
|
const themeBtn = document.getElementById('theme-btn');
|
||||||
|
|
||||||
|
if (currentTheme === 'dark') {
|
||||||
|
body.setAttribute('data-theme', 'dark');
|
||||||
|
themeIcon.textContent = '☀️';
|
||||||
|
themeBtn.title = window.translations[currentLanguage]['theme_light'] || 'Light theme';
|
||||||
|
} else {
|
||||||
|
body.removeAttribute('data-theme');
|
||||||
|
themeIcon.textContent = '🌙';
|
||||||
|
themeBtn.title = window.translations[currentLanguage]['theme_dark'] || 'Dark theme';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function switchLanguage(lang) {
|
function switchLanguage(lang) {
|
||||||
currentLanguage = lang;
|
currentLanguage = lang;
|
||||||
localStorage.setItem('yggdrasil-language', lang);
|
localStorage.setItem('yggdrasil-language', lang);
|
||||||
|
@ -146,6 +172,7 @@
|
||||||
document.getElementById('lang-' + lang).classList.add('active');
|
document.getElementById('lang-' + lang).classList.add('active');
|
||||||
|
|
||||||
updateTexts();
|
updateTexts();
|
||||||
|
applyTheme(); // Update theme button tooltip
|
||||||
}
|
}
|
||||||
|
|
||||||
function logout() {
|
function logout() {
|
||||||
|
@ -174,12 +201,14 @@
|
||||||
event.target.closest('.nav-item').classList.add('active');
|
event.target.closest('.nav-item').classList.add('active');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize language on page load
|
// Initialize language and theme on page load
|
||||||
document.addEventListener('DOMContentLoaded', function () {
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
// Set active language button
|
// Set active language button
|
||||||
document.getElementById('lang-' + currentLanguage).classList.add('active');
|
document.getElementById('lang-' + currentLanguage).classList.add('active');
|
||||||
// Update all texts
|
// Update all texts
|
||||||
updateTexts();
|
updateTexts();
|
||||||
|
// Apply saved theme
|
||||||
|
applyTheme();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|
|
@ -29,5 +29,12 @@ window.translations.en = {
|
||||||
'network_settings_description': 'Network interaction parameters',
|
'network_settings_description': 'Network interaction parameters',
|
||||||
'coming_soon': 'Coming soon...',
|
'coming_soon': 'Coming soon...',
|
||||||
'footer_text': 'Yggdrasil Network • Minimal WebUI v1.0',
|
'footer_text': 'Yggdrasil Network • Minimal WebUI v1.0',
|
||||||
'logout_confirm': 'Are you sure you want to logout?'
|
'logout_confirm': 'Are you sure you want to logout?',
|
||||||
|
'theme_light': 'Light theme',
|
||||||
|
'theme_dark': 'Dark theme',
|
||||||
|
'login_subtitle': 'Enter password to access the web interface',
|
||||||
|
'password_label': 'Password:',
|
||||||
|
'access_dashboard': 'Access Dashboard',
|
||||||
|
'error_invalid_password': 'Invalid password. Please try again.',
|
||||||
|
'error_too_many_attempts': 'Too many failed attempts. Please wait 1 minute before trying again.'
|
||||||
};
|
};
|
|
@ -29,5 +29,12 @@ window.translations.ru = {
|
||||||
'network_settings_description': 'Параметры сетевого взаимодействия',
|
'network_settings_description': 'Параметры сетевого взаимодействия',
|
||||||
'coming_soon': 'Функция в разработке...',
|
'coming_soon': 'Функция в разработке...',
|
||||||
'footer_text': 'Yggdrasil Network • Minimal WebUI v1.0',
|
'footer_text': 'Yggdrasil Network • Minimal WebUI v1.0',
|
||||||
'logout_confirm': 'Вы уверены, что хотите выйти?'
|
'logout_confirm': 'Вы уверены, что хотите выйти?',
|
||||||
|
'theme_light': 'Светлая тема',
|
||||||
|
'theme_dark': 'Темная тема',
|
||||||
|
'login_subtitle': 'Введите пароль для доступа к веб-интерфейсу',
|
||||||
|
'password_label': 'Пароль:',
|
||||||
|
'access_dashboard': 'Войти в панель',
|
||||||
|
'error_invalid_password': 'Неверный пароль. Попробуйте снова.',
|
||||||
|
'error_too_many_attempts': 'Слишком много неудачных попыток. Подождите 1 минуту перед повторной попыткой.'
|
||||||
};
|
};
|
|
@ -6,7 +6,18 @@
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Yggdrasil Web Interface - Login</title>
|
<title>Yggdrasil Web Interface - Login</title>
|
||||||
<link rel="stylesheet" href="static/style.css">
|
<link rel="stylesheet" href="static/style.css">
|
||||||
|
<script src="static/lang/ru.js"></script>
|
||||||
|
<script src="static/lang/en.js"></script>
|
||||||
<style>
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||||
|
background: linear-gradient(135deg, var(--bg-primary) 0%, var(--bg-secondary) 100%);
|
||||||
|
min-height: 100vh;
|
||||||
|
color: var(--text-primary);
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.login-container {
|
.login-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
@ -16,24 +27,27 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-form {
|
.login-form {
|
||||||
background: white;
|
background: var(--bg-info-card);
|
||||||
border-radius: 12px;
|
border-radius: 4px;
|
||||||
padding: 40px;
|
padding: 40px;
|
||||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
|
box-shadow: 0 2px 8px var(--shadow-dark);
|
||||||
|
border: 1px solid var(--border-card);
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 400px;
|
max-width: 400px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-form h1 {
|
.login-form h1 {
|
||||||
color: #495057;
|
color: var(--text-heading);
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
font-size: 2rem;
|
font-size: 2rem;
|
||||||
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-form p {
|
.login-form p {
|
||||||
color: #6c757d;
|
color: var(--text-body);
|
||||||
margin-bottom: 30px;
|
margin-bottom: 30px;
|
||||||
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-group {
|
.form-group {
|
||||||
|
@ -44,95 +58,164 @@
|
||||||
.form-group label {
|
.form-group label {
|
||||||
display: block;
|
display: block;
|
||||||
margin-bottom: 5px;
|
margin-bottom: 5px;
|
||||||
color: #495057;
|
color: var(--text-heading);
|
||||||
font-weight: 500;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-group input {
|
.form-group input {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
border: 1px solid #ced4da;
|
border: 1px solid var(--border-card);
|
||||||
border-radius: 6px;
|
border-radius: 2px;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
transition: border-color 0.2s ease;
|
transition: border-color 0.2s ease;
|
||||||
|
background: var(--bg-status-card);
|
||||||
|
color: var(--text-body);
|
||||||
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-group input:focus {
|
.form-group input:focus {
|
||||||
outline: none;
|
outline: none;
|
||||||
border-color: #667eea;
|
border-color: var(--border-hover);
|
||||||
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
|
box-shadow: 0 1px 4px var(--shadow-light);
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-group input:disabled {
|
.form-group input:disabled {
|
||||||
background-color: #f8f9fa;
|
background-color: var(--bg-nav-item);
|
||||||
border-color: #e9ecef;
|
border-color: var(--border-nav-item);
|
||||||
color: #6c757d;
|
color: var(--text-muted);
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-button {
|
.login-button {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
background: var(--bg-nav-active);
|
||||||
color: white;
|
color: var(--text-white);
|
||||||
border: none;
|
border: 1px solid var(--bg-nav-active-border);
|
||||||
padding: 12px;
|
padding: 12px 16px;
|
||||||
border-radius: 6px;
|
border-radius: 2px;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
font-weight: 500;
|
font-weight: 600;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
transition: background 0.2s ease, box-shadow 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-button:hover {
|
.login-button:hover {
|
||||||
transform: translateY(-1px);
|
background: var(--bg-nav-active-border);
|
||||||
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3);
|
box-shadow: 0 2px 8px var(--shadow-medium);
|
||||||
}
|
|
||||||
|
|
||||||
.login-button:active {
|
|
||||||
transform: translateY(0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.error-message {
|
.error-message {
|
||||||
background: #f8d7da;
|
background: var(--bg-status-card);
|
||||||
color: #721c24;
|
color: var(--bg-logout);
|
||||||
border: 1px solid #f1aeb5;
|
border: 1px solid var(--border-logout);
|
||||||
border-radius: 6px;
|
border-radius: 2px;
|
||||||
padding: 10px;
|
padding: 12px;
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
display: none;
|
display: none;
|
||||||
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
.lock-icon {
|
.lock-icon {
|
||||||
font-size: 3rem;
|
font-size: 3rem;
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
color: #667eea;
|
color: var(--bg-nav-active);
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-header {
|
||||||
|
position: absolute;
|
||||||
|
top: 20px;
|
||||||
|
right: 20px;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-container {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
<div class="login-header">
|
||||||
|
<div class="controls-group">
|
||||||
|
<button onclick="toggleTheme()" class="theme-btn" id="theme-btn">
|
||||||
|
<span class="theme-icon">🌙</span>
|
||||||
|
</button>
|
||||||
|
<button onclick="switchLanguage('ru')" class="lang-btn" id="lang-ru">RU</button>
|
||||||
|
<button onclick="switchLanguage('en')" class="lang-btn" id="lang-en">EN</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="login-container">
|
<div class="login-container">
|
||||||
<form class="login-form" onsubmit="return handleLogin(event)">
|
<form class="login-form" onsubmit="return handleLogin(event)">
|
||||||
<div class="lock-icon">🔒</div>
|
<div class="lock-icon">🔒</div>
|
||||||
<h1>🌳 Yggdrasil</h1>
|
<h1 data-key="title">🌳 Yggdrasil</h1>
|
||||||
<p>Enter password to access the web interface</p>
|
<p data-key="login_subtitle">Enter password to access the web interface</p>
|
||||||
|
|
||||||
<div class="error-message" id="errorMessage">
|
<div class="error-message" id="errorMessage">
|
||||||
Invalid password. Please try again.
|
<span data-key="error_invalid_password">Invalid password. Please try again.</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="password">Password:</label>
|
<label for="password" data-key="password_label">Password:</label>
|
||||||
<input type="password" id="password" name="password" required autofocus>
|
<input type="password" id="password" name="password" required autofocus>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button type="submit" class="login-button">
|
<button type="submit" class="login-button" data-key="access_dashboard">
|
||||||
Access Dashboard
|
Access Dashboard
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
let currentLanguage = localStorage.getItem('yggdrasil-language') || 'ru';
|
||||||
|
let currentTheme = localStorage.getItem('yggdrasil-theme') || 'light';
|
||||||
|
|
||||||
|
function updateTexts() {
|
||||||
|
const elements = document.querySelectorAll('[data-key]');
|
||||||
|
elements.forEach(element => {
|
||||||
|
const key = element.getAttribute('data-key');
|
||||||
|
if (window.translations && window.translations[currentLanguage] && window.translations[currentLanguage][key]) {
|
||||||
|
element.textContent = window.translations[currentLanguage][key];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleTheme() {
|
||||||
|
currentTheme = currentTheme === 'light' ? 'dark' : 'light';
|
||||||
|
localStorage.setItem('yggdrasil-theme', currentTheme);
|
||||||
|
applyTheme();
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyTheme() {
|
||||||
|
const body = document.body;
|
||||||
|
const themeIcon = document.querySelector('.theme-icon');
|
||||||
|
const themeBtn = document.getElementById('theme-btn');
|
||||||
|
|
||||||
|
if (currentTheme === 'dark') {
|
||||||
|
body.setAttribute('data-theme', 'dark');
|
||||||
|
themeIcon.textContent = '☀️';
|
||||||
|
themeBtn.title = window.translations[currentLanguage]['theme_light'] || 'Light theme';
|
||||||
|
} else {
|
||||||
|
body.removeAttribute('data-theme');
|
||||||
|
themeIcon.textContent = '🌙';
|
||||||
|
themeBtn.title = window.translations[currentLanguage]['theme_dark'] || 'Dark theme';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function switchLanguage(lang) {
|
||||||
|
currentLanguage = lang;
|
||||||
|
localStorage.setItem('yggdrasil-language', lang);
|
||||||
|
|
||||||
|
// Update active button
|
||||||
|
document.querySelectorAll('.lang-btn').forEach(btn => btn.classList.remove('active'));
|
||||||
|
document.getElementById('lang-' + lang).classList.add('active');
|
||||||
|
|
||||||
|
updateTexts();
|
||||||
|
applyTheme(); // Update theme button tooltip
|
||||||
|
}
|
||||||
|
|
||||||
async function handleLogin(event) {
|
async function handleLogin(event) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
|
@ -153,7 +236,7 @@
|
||||||
window.location.href = '/';
|
window.location.href = '/';
|
||||||
} else if (response.status === 429) {
|
} else if (response.status === 429) {
|
||||||
// Too many requests - IP blocked
|
// Too many requests - IP blocked
|
||||||
errorMessage.textContent = 'Too many failed attempts. Please wait 1 minute before trying again.';
|
errorMessage.querySelector('span').textContent = window.translations[currentLanguage]['error_too_many_attempts'] || 'Too many failed attempts. Please wait 1 minute before trying again.';
|
||||||
errorMessage.style.display = 'block';
|
errorMessage.style.display = 'block';
|
||||||
document.getElementById('password').value = '';
|
document.getElementById('password').value = '';
|
||||||
document.getElementById('password').disabled = true;
|
document.getElementById('password').disabled = true;
|
||||||
|
@ -166,7 +249,7 @@
|
||||||
}, 60000);
|
}, 60000);
|
||||||
} else {
|
} else {
|
||||||
// Invalid password
|
// Invalid password
|
||||||
errorMessage.textContent = 'Invalid password. Please try again.';
|
errorMessage.querySelector('span').textContent = window.translations[currentLanguage]['error_invalid_password'] || 'Invalid password. Please try again.';
|
||||||
errorMessage.style.display = 'block';
|
errorMessage.style.display = 'block';
|
||||||
document.getElementById('password').value = '';
|
document.getElementById('password').value = '';
|
||||||
document.getElementById('password').focus();
|
document.getElementById('password').focus();
|
||||||
|
@ -183,6 +266,16 @@
|
||||||
document.getElementById('password').addEventListener('input', function () {
|
document.getElementById('password').addEventListener('input', function () {
|
||||||
document.getElementById('errorMessage').style.display = 'none';
|
document.getElementById('errorMessage').style.display = 'none';
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Initialize language and theme on page load
|
||||||
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
|
// Set active language button
|
||||||
|
document.getElementById('lang-' + currentLanguage).classList.add('active');
|
||||||
|
// Update all texts
|
||||||
|
updateTexts();
|
||||||
|
// Apply saved theme
|
||||||
|
applyTheme();
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,91 @@
|
||||||
|
/* Light theme (default) */
|
||||||
|
:root {
|
||||||
|
/* Background colors */
|
||||||
|
--bg-primary: #667eea;
|
||||||
|
--bg-secondary: #764ba2;
|
||||||
|
--bg-sidebar: #ffffff;
|
||||||
|
--bg-nav-item: #f8f9fa;
|
||||||
|
--bg-nav-hover: #e9ecef;
|
||||||
|
--bg-nav-active: #3498db;
|
||||||
|
--bg-nav-active-border: #2980b9;
|
||||||
|
--bg-main-content: #fafafa;
|
||||||
|
--bg-status-card: #f5f5f5;
|
||||||
|
--bg-info-card: #ffffff;
|
||||||
|
--bg-logout: #dc3545;
|
||||||
|
--bg-logout-hover: #c82333;
|
||||||
|
--bg-lang-switcher: rgba(255, 255, 255, 0.2);
|
||||||
|
--bg-lang-btn-hover: rgba(255, 255, 255, 0.3);
|
||||||
|
--bg-lang-btn-active: rgba(255, 255, 255, 0.4);
|
||||||
|
|
||||||
|
/* Border colors */
|
||||||
|
--border-sidebar: #e0e0e0;
|
||||||
|
--border-nav-item: #dee2e6;
|
||||||
|
--border-main: #e0e0e0;
|
||||||
|
--border-card: #e0e0e0;
|
||||||
|
--border-hover: #3498db;
|
||||||
|
--border-footer: #dee2e6;
|
||||||
|
--border-logout: #c82333;
|
||||||
|
--border-lang: rgba(255, 255, 255, 0.3);
|
||||||
|
|
||||||
|
/* Text colors */
|
||||||
|
--text-primary: #333;
|
||||||
|
--text-white: white;
|
||||||
|
--text-nav: #495057;
|
||||||
|
--text-heading: #343a40;
|
||||||
|
--text-body: #495057;
|
||||||
|
--text-muted: #6c757d;
|
||||||
|
|
||||||
|
/* Shadow colors */
|
||||||
|
--shadow-light: rgba(0, 0, 0, 0.1);
|
||||||
|
--shadow-medium: rgba(0, 0, 0, 0.15);
|
||||||
|
--shadow-dark: rgba(0, 0, 0, 0.2);
|
||||||
|
--shadow-heavy: rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dark theme */
|
||||||
|
[data-theme="dark"] {
|
||||||
|
/* Background colors */
|
||||||
|
--bg-primary: #2c3e50;
|
||||||
|
--bg-secondary: #34495e;
|
||||||
|
--bg-sidebar: #37474f;
|
||||||
|
--bg-nav-item: #455a64;
|
||||||
|
--bg-nav-hover: #546e7a;
|
||||||
|
--bg-nav-active: #3498db;
|
||||||
|
--bg-nav-active-border: #2980b9;
|
||||||
|
--bg-main-content: #37474f;
|
||||||
|
--bg-status-card: #455a64;
|
||||||
|
--bg-info-card: #455a64;
|
||||||
|
--bg-logout: #dc3545;
|
||||||
|
--bg-logout-hover: #c82333;
|
||||||
|
--bg-lang-switcher: rgba(0, 0, 0, 0.3);
|
||||||
|
--bg-lang-btn-hover: rgba(255, 255, 255, 0.1);
|
||||||
|
--bg-lang-btn-active: rgba(255, 255, 255, 0.2);
|
||||||
|
|
||||||
|
/* Border colors */
|
||||||
|
--border-sidebar: #455a64;
|
||||||
|
--border-nav-item: #546e7a;
|
||||||
|
--border-main: #455a64;
|
||||||
|
--border-card: #546e7a;
|
||||||
|
--border-hover: #3498db;
|
||||||
|
--border-footer: #546e7a;
|
||||||
|
--border-logout: #c82333;
|
||||||
|
--border-lang: rgba(255, 255, 255, 0.3);
|
||||||
|
|
||||||
|
/* Text colors */
|
||||||
|
--text-primary: #333;
|
||||||
|
--text-white: white;
|
||||||
|
--text-nav: #eceff1;
|
||||||
|
--text-heading: #eceff1;
|
||||||
|
--text-body: #cfd8dc;
|
||||||
|
--text-muted: #b0bec5;
|
||||||
|
|
||||||
|
/* Shadow colors */
|
||||||
|
--shadow-light: rgba(0, 0, 0, 0.2);
|
||||||
|
--shadow-medium: rgba(0, 0, 0, 0.3);
|
||||||
|
--shadow-dark: rgba(0, 0, 0, 0.4);
|
||||||
|
--shadow-heavy: rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
* {
|
* {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
@ -5,10 +93,10 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
background: linear-gradient(135deg, var(--bg-primary) 0%, var(--bg-secondary) 100%);
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
color: #333;
|
color: var(--text-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.container {
|
.container {
|
||||||
|
@ -19,7 +107,7 @@ body {
|
||||||
|
|
||||||
header {
|
header {
|
||||||
margin-bottom: 40px;
|
margin-bottom: 40px;
|
||||||
color: white;
|
color: var(--text-white);
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-content {
|
.header-content {
|
||||||
|
@ -35,33 +123,58 @@ header {
|
||||||
gap: 15px;
|
gap: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.language-switcher {
|
.controls-group {
|
||||||
display: flex;
|
display: flex;
|
||||||
background: rgba(255, 255, 255, 0.15);
|
background: var(--bg-lang-switcher);
|
||||||
border-radius: 8px;
|
border-radius: 2px;
|
||||||
padding: 4px;
|
padding: 2px;
|
||||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
border: 1px solid var(--border-lang);
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-btn {
|
||||||
|
background: transparent;
|
||||||
|
color: var(--text-white);
|
||||||
|
border: none;
|
||||||
|
padding: 6px 12px;
|
||||||
|
border-radius: 1px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
transition: background 0.2s ease;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-btn:hover {
|
||||||
|
background: var(--bg-lang-btn-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-icon {
|
||||||
|
display: block;
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.lang-btn {
|
.lang-btn {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
color: white;
|
color: var(--text-white);
|
||||||
border: none;
|
border: none;
|
||||||
padding: 6px 12px;
|
padding: 6px 12px;
|
||||||
border-radius: 6px;
|
border-radius: 1px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
font-weight: 500;
|
font-weight: 600;
|
||||||
transition: all 0.2s ease;
|
transition: background 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.lang-btn:hover {
|
.lang-btn:hover {
|
||||||
background: rgba(255, 255, 255, 0.2);
|
background: var(--bg-lang-btn-hover);
|
||||||
}
|
}
|
||||||
|
|
||||||
.lang-btn.active {
|
.lang-btn.active {
|
||||||
background: rgba(255, 255, 255, 0.3);
|
background: var(--bg-lang-btn-active);
|
||||||
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-content > div:first-child {
|
.header-content > div:first-child {
|
||||||
|
@ -70,9 +183,11 @@ header {
|
||||||
}
|
}
|
||||||
|
|
||||||
header h1 {
|
header h1 {
|
||||||
font-size: 2.5rem;
|
font-size: 2.2rem;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
text-shadow: 2px 2px 4px rgba(0,0,0,0.3);
|
text-shadow: 1px 1px 2px var(--shadow-heavy);
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: -0.5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
header p {
|
header p {
|
||||||
|
@ -83,20 +198,20 @@ header p {
|
||||||
.sidebar-footer {
|
.sidebar-footer {
|
||||||
margin-top: 20px;
|
margin-top: 20px;
|
||||||
padding-top: 20px;
|
padding-top: 20px;
|
||||||
border-top: 1px solid rgba(255, 255, 255, 0.2);
|
border-top: 1px solid var(--border-footer);
|
||||||
}
|
}
|
||||||
|
|
||||||
.logout-btn {
|
.logout-btn {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
background: rgba(220, 53, 69, 0.2);
|
background: var(--bg-logout);
|
||||||
color: white;
|
color: var(--text-white);
|
||||||
border: 1px solid rgba(220, 53, 69, 0.3);
|
border: 1px solid var(--border-logout);
|
||||||
padding: 12px 16px;
|
padding: 10px 16px;
|
||||||
border-radius: 10px;
|
border-radius: 2px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-weight: 500;
|
font-weight: 600;
|
||||||
transition: all 0.3s ease;
|
transition: background 0.2s ease;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
@ -104,9 +219,7 @@ header p {
|
||||||
}
|
}
|
||||||
|
|
||||||
.logout-btn:hover {
|
.logout-btn:hover {
|
||||||
background: rgba(220, 53, 69, 0.3);
|
background: var(--bg-logout-hover);
|
||||||
transform: translateY(-2px);
|
|
||||||
box-shadow: 0 4px 15px rgba(220, 53, 69, 0.2);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.logout-btn:before {
|
.logout-btn:before {
|
||||||
|
@ -122,12 +235,11 @@ header p {
|
||||||
|
|
||||||
.sidebar {
|
.sidebar {
|
||||||
min-width: 250px;
|
min-width: 250px;
|
||||||
background: rgba(255, 255, 255, 0.15);
|
background: var(--bg-sidebar);
|
||||||
backdrop-filter: blur(10px);
|
border-radius: 4px;
|
||||||
border-radius: 12px;
|
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
box-shadow: 0 8px 32px rgba(0,0,0,0.1);
|
box-shadow: 0 2px 8px var(--shadow-heavy);
|
||||||
border: 1px solid rgba(255, 255, 255, 0.18);
|
border: 1px solid var(--border-sidebar);
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
@ -143,25 +255,24 @@ header p {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
padding: 15px 18px;
|
padding: 12px 16px;
|
||||||
background: rgba(255, 255, 255, 0.1);
|
background: var(--bg-nav-item);
|
||||||
border-radius: 10px;
|
border-radius: 2px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.3s ease;
|
transition: background 0.2s ease;
|
||||||
color: white;
|
color: var(--text-nav);
|
||||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
border: 1px solid var(--border-nav-item);
|
||||||
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-item:hover {
|
.nav-item:hover {
|
||||||
background: rgba(255, 255, 255, 0.2);
|
background: var(--bg-nav-hover);
|
||||||
transform: translateY(-2px);
|
|
||||||
box-shadow: 0 4px 15px rgba(0,0,0,0.2);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-item.active {
|
.nav-item.active {
|
||||||
background: rgba(255, 255, 255, 0.25);
|
background: var(--bg-nav-active);
|
||||||
border-color: rgba(255, 255, 255, 0.4);
|
border-color: var(--bg-nav-active-border);
|
||||||
box-shadow: 0 4px 15px rgba(0,0,0,0.2);
|
color: var(--text-white);
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-icon {
|
.nav-icon {
|
||||||
|
@ -175,12 +286,11 @@ header p {
|
||||||
|
|
||||||
.main-content {
|
.main-content {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
background: rgba(255, 255, 255, 0.95);
|
background: var(--bg-main-content);
|
||||||
border-radius: 12px;
|
border-radius: 4px;
|
||||||
padding: 30px;
|
padding: 30px;
|
||||||
box-shadow: 0 8px 32px rgba(0,0,0,0.1);
|
box-shadow: 0 2px 8px var(--shadow-dark);
|
||||||
backdrop-filter: blur(10px);
|
border: 1px solid var(--border-main);
|
||||||
border: 1px solid rgba(255, 255, 255, 0.18);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.content-section {
|
.content-section {
|
||||||
|
@ -192,13 +302,25 @@ header p {
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-card {
|
.status-card {
|
||||||
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
|
background: var(--bg-status-card);
|
||||||
border-radius: 15px;
|
border-radius: 4px;
|
||||||
padding: 25px;
|
padding: 20px;
|
||||||
margin-bottom: 30px;
|
margin-bottom: 30px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
box-shadow: 0 4px 20px rgba(0,0,0,0.08);
|
box-shadow: 0 1px 4px var(--shadow-light);
|
||||||
border: 1px solid rgba(255, 255, 255, 0.5);
|
border: 1px solid var(--border-card);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-card h2 {
|
||||||
|
color: var(--text-heading);
|
||||||
|
font-weight: 700;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
font-size: 1.3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-card p {
|
||||||
|
color: var(--text-body);
|
||||||
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-indicator {
|
.status-indicator {
|
||||||
|
@ -213,11 +335,11 @@ header p {
|
||||||
width: 12px;
|
width: 12px;
|
||||||
height: 12px;
|
height: 12px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
background: #6c757d;
|
background: var(--text-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-dot.active {
|
.status-dot.active {
|
||||||
background: #28a745;
|
background: var(--bg-nav-active);
|
||||||
animation: pulse 2s infinite;
|
animation: pulse 2s infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -234,39 +356,44 @@ header p {
|
||||||
}
|
}
|
||||||
|
|
||||||
.info-card {
|
.info-card {
|
||||||
background: linear-gradient(135deg, #ffffff 0%, #f8f9fa 100%);
|
background: var(--bg-info-card);
|
||||||
border: 1px solid rgba(233, 236, 239, 0.6);
|
border: 1px solid var(--border-card);
|
||||||
border-radius: 15px;
|
border-radius: 4px;
|
||||||
padding: 25px;
|
padding: 20px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
transition: all 0.3s ease;
|
transition: box-shadow 0.2s ease;
|
||||||
box-shadow: 0 2px 10px rgba(0,0,0,0.05);
|
box-shadow: 0 1px 4px var(--shadow-light);
|
||||||
}
|
}
|
||||||
|
|
||||||
.info-card:hover {
|
.info-card:hover {
|
||||||
transform: translateY(-5px);
|
box-shadow: 0 2px 8px var(--shadow-medium);
|
||||||
box-shadow: 0 8px 25px rgba(0,0,0,0.15);
|
border-color: var(--border-hover);
|
||||||
border-color: rgba(102, 126, 234, 0.3);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.info-card h3 {
|
.info-card h3 {
|
||||||
color: #495057;
|
color: var(--text-heading);
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 1.1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.info-card p {
|
.info-card p {
|
||||||
color: #6c757d;
|
color: var(--text-body);
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
.info-card small {
|
.info-card small {
|
||||||
color: #adb5bd;
|
color: var(--text-muted);
|
||||||
font-style: italic;
|
font-weight: 500;
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
footer {
|
footer {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
color: white;
|
color: var(--text-white);
|
||||||
opacity: 0.8;
|
opacity: 0.8;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -310,7 +437,7 @@ footer {
|
||||||
margin-left: 15px;
|
margin-left: 15px;
|
||||||
padding-top: 0;
|
padding-top: 0;
|
||||||
border-top: none;
|
border-top: none;
|
||||||
border-left: 1px solid rgba(255, 255, 255, 0.2);
|
border-left: 1px solid var(--border-footer);
|
||||||
padding-left: 15px;
|
padding-left: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue