mirror of
https://github.com/yggdrasil-network/yggdrasil-go.git
synced 2025-08-24 16:05:07 +03:00
Expose global state and update peer display logic in WebUI
This commit is contained in:
parent
675e2e71a5
commit
2b3b4c39d2
6 changed files with 312 additions and 162 deletions
|
@ -3,9 +3,9 @@
|
||||||
* Integrates admin API with the user interface
|
* Integrates admin API with the user interface
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// Global state
|
// Global state - expose to window for access from other scripts
|
||||||
let nodeInfo = null;
|
window.nodeInfo = null;
|
||||||
let peersData = null;
|
window.peersData = null;
|
||||||
let isLoading = false;
|
let isLoading = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -14,7 +14,7 @@ let isLoading = false;
|
||||||
async function loadNodeInfo() {
|
async function loadNodeInfo() {
|
||||||
try {
|
try {
|
||||||
const info = await window.yggAPI.getSelf();
|
const info = await window.yggAPI.getSelf();
|
||||||
nodeInfo = info;
|
window.nodeInfo = info;
|
||||||
updateNodeInfoDisplay(info);
|
updateNodeInfoDisplay(info);
|
||||||
return info;
|
return info;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
@ -30,7 +30,7 @@ async function loadNodeInfo() {
|
||||||
async function loadPeers() {
|
async function loadPeers() {
|
||||||
try {
|
try {
|
||||||
const data = await window.yggAPI.getPeers();
|
const data = await window.yggAPI.getPeers();
|
||||||
peersData = data;
|
window.peersData = data;
|
||||||
updatePeersDisplay(data);
|
updatePeersDisplay(data);
|
||||||
return data;
|
return data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
@ -64,6 +64,13 @@ function updatePeersDisplay(data) {
|
||||||
|
|
||||||
peersContainer.innerHTML = '';
|
peersContainer.innerHTML = '';
|
||||||
|
|
||||||
|
// Always update peer counts, even if no peers
|
||||||
|
const peersCount = data.peers ? data.peers.length : 0;
|
||||||
|
const onlineCount = data.peers ? data.peers.filter(p => p.up).length : 0;
|
||||||
|
|
||||||
|
updateElementText('peers-count', peersCount.toString());
|
||||||
|
updateElementText('peers-online', onlineCount.toString());
|
||||||
|
|
||||||
if (!data.peers || data.peers.length === 0) {
|
if (!data.peers || data.peers.length === 0) {
|
||||||
peersContainer.innerHTML = '<div class="no-data">No peers connected</div>';
|
peersContainer.innerHTML = '<div class="no-data">No peers connected</div>';
|
||||||
return;
|
return;
|
||||||
|
@ -73,12 +80,12 @@ function updatePeersDisplay(data) {
|
||||||
const peerElement = createPeerElement(peer);
|
const peerElement = createPeerElement(peer);
|
||||||
peersContainer.appendChild(peerElement);
|
peersContainer.appendChild(peerElement);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Update peer count
|
|
||||||
updateElementText('peers-count', data.peers.length.toString());
|
|
||||||
updateElementText('peers-online', data.peers.filter(p => p.up).length.toString());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Expose update functions to window for access from other scripts
|
||||||
|
window.updateNodeInfoDisplay = updateNodeInfoDisplay;
|
||||||
|
window.updatePeersDisplay = updatePeersDisplay;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create HTML element for a single peer
|
* Create HTML element for a single peer
|
||||||
*/
|
*/
|
||||||
|
@ -232,6 +239,11 @@ async function initializeApp() {
|
||||||
}
|
}
|
||||||
|
|
||||||
isLoading = true;
|
isLoading = true;
|
||||||
|
|
||||||
|
// Initialize peer counts to 0 immediately to replace "Loading..." text
|
||||||
|
updateElementText('peers-count', '0');
|
||||||
|
updateElementText('peers-online', '0');
|
||||||
|
|
||||||
showInfo('Loading dashboard...');
|
showInfo('Loading dashboard...');
|
||||||
|
|
||||||
// Load initial data
|
// Load initial data
|
||||||
|
|
|
@ -7,9 +7,10 @@
|
||||||
<title>Yggdrasil Web Interface</title>
|
<title>Yggdrasil Web Interface</title>
|
||||||
<link rel="stylesheet" href="static/style.css">
|
<link rel="stylesheet" href="static/style.css">
|
||||||
<script src="static/api.js"></script>
|
<script src="static/api.js"></script>
|
||||||
|
<script src="static/main.js"></script>
|
||||||
<script src="static/app.js"></script>
|
<script src="static/app.js"></script>
|
||||||
<script src="static/lang/ru.js"></script>
|
<script src="static/lang/ru.js"></script>
|
||||||
<script srс="static/lang/en.js"></script>
|
<script src="static/lang/en.js"></script>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
@ -145,152 +146,7 @@
|
||||||
<!-- Notifications container -->
|
<!-- Notifications container -->
|
||||||
<div class="notifications-container" id="notifications-container"></div>
|
<div class="notifications-container" id="notifications-container"></div>
|
||||||
|
|
||||||
<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';
|
|
||||||
applyTheme();
|
|
||||||
localStorage.setItem('yggdrasil-theme', currentTheme);
|
|
||||||
}
|
|
||||||
|
|
||||||
function applyTheme() {
|
|
||||||
document.documentElement.setAttribute('data-theme', currentTheme);
|
|
||||||
const themeBtn = document.getElementById('theme-btn');
|
|
||||||
if (themeBtn) {
|
|
||||||
const icon = themeBtn.querySelector('.theme-icon');
|
|
||||||
if (icon) {
|
|
||||||
icon.textContent = currentTheme === 'light' ? '🌙' : '☀️';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function switchLanguage(lang) {
|
|
||||||
currentLanguage = lang;
|
|
||||||
localStorage.setItem('yggdrasil-language', lang);
|
|
||||||
|
|
||||||
// Update button states
|
|
||||||
document.querySelectorAll('.lang-btn').forEach(btn => btn.classList.remove('active'));
|
|
||||||
document.getElementById('lang-' + lang).classList.add('active');
|
|
||||||
|
|
||||||
// Update all texts
|
|
||||||
updateTexts();
|
|
||||||
}
|
|
||||||
|
|
||||||
function showSection(sectionName) {
|
|
||||||
// Hide all sections
|
|
||||||
const sections = document.querySelectorAll('.content-section');
|
|
||||||
sections.forEach(section => section.classList.remove('active'));
|
|
||||||
|
|
||||||
// Remove active class from all nav items
|
|
||||||
const navItems = document.querySelectorAll('.nav-item');
|
|
||||||
navItems.forEach(item => item.classList.remove('active'));
|
|
||||||
|
|
||||||
// Show selected section
|
|
||||||
const targetSection = document.getElementById(sectionName + '-section');
|
|
||||||
if (targetSection) {
|
|
||||||
targetSection.classList.add('active');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add active class to clicked nav item
|
|
||||||
event.target.closest('.nav-item').classList.add('active');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Notification system (shared with app.js)
|
|
||||||
let notificationId = 0;
|
|
||||||
|
|
||||||
function showNotification(message, type = 'info', title = null, duration = 5000) {
|
|
||||||
const container = document.getElementById('notifications-container');
|
|
||||||
const id = ++notificationId;
|
|
||||||
|
|
||||||
const icons = {
|
|
||||||
success: '✅',
|
|
||||||
error: '❌',
|
|
||||||
warning: '⚠️',
|
|
||||||
info: 'ℹ️'
|
|
||||||
};
|
|
||||||
|
|
||||||
const titles = {
|
|
||||||
success: window.translations[currentLanguage]['notification_success'] || 'Success',
|
|
||||||
error: window.translations[currentLanguage]['notification_error'] || 'Error',
|
|
||||||
warning: window.translations[currentLanguage]['notification_warning'] || 'Warning',
|
|
||||||
info: window.translations[currentLanguage]['notification_info'] || 'Information'
|
|
||||||
};
|
|
||||||
|
|
||||||
const notification = document.createElement('div');
|
|
||||||
notification.className = `notification ${type}`;
|
|
||||||
notification.id = `notification-${id}`;
|
|
||||||
|
|
||||||
notification.innerHTML = `
|
|
||||||
<div class="notification-icon">${icons[type] || icons.info}</div>
|
|
||||||
<div class="notification-content">
|
|
||||||
<div class="notification-title">${title || titles[type]}</div>
|
|
||||||
<div class="notification-message">${message}</div>
|
|
||||||
</div>
|
|
||||||
<button class="notification-close" onclick="removeNotification(${id})">×</button>
|
|
||||||
`;
|
|
||||||
|
|
||||||
container.appendChild(notification);
|
|
||||||
|
|
||||||
// Auto remove after duration
|
|
||||||
if (duration > 0) {
|
|
||||||
setTimeout(() => {
|
|
||||||
removeNotification(id);
|
|
||||||
}, duration);
|
|
||||||
}
|
|
||||||
|
|
||||||
return id;
|
|
||||||
}
|
|
||||||
|
|
||||||
function removeNotification(id) {
|
|
||||||
const notification = document.getElementById(`notification-${id}`);
|
|
||||||
if (notification) {
|
|
||||||
notification.classList.add('removing');
|
|
||||||
setTimeout(() => {
|
|
||||||
if (notification.parentNode) {
|
|
||||||
notification.parentNode.removeChild(notification);
|
|
||||||
}
|
|
||||||
}, 300);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function showSuccess(message, title = null) {
|
|
||||||
return showNotification(message, 'success', title);
|
|
||||||
}
|
|
||||||
|
|
||||||
function showError(message, title = null) {
|
|
||||||
return showNotification(message, 'error', title);
|
|
||||||
}
|
|
||||||
|
|
||||||
function showWarning(message, title = null) {
|
|
||||||
return showNotification(message, 'warning', title);
|
|
||||||
}
|
|
||||||
|
|
||||||
function showInfo(message, title = null) {
|
|
||||||
return showNotification(message, 'info', title);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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>
|
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
|
@ -8,19 +8,28 @@ window.translations.en = {
|
||||||
'nav_config': 'Configuration',
|
'nav_config': 'Configuration',
|
||||||
'status_title': 'Node Status',
|
'status_title': 'Node Status',
|
||||||
'status_active': 'Active',
|
'status_active': 'Active',
|
||||||
'status_description': 'WebUI is running and accessible',
|
'status_description': 'Information about your Yggdrasil node current status',
|
||||||
|
'node_info': 'Node Information',
|
||||||
|
'public_key': 'Public Key',
|
||||||
|
'version': 'Version',
|
||||||
|
'routing_entries': 'Routing Entries',
|
||||||
|
'loading': 'Loading...',
|
||||||
'network_info': 'Network Information',
|
'network_info': 'Network Information',
|
||||||
'address': 'Address',
|
'address': 'Address',
|
||||||
'subnet': 'Subnet',
|
'subnet': 'Subnet',
|
||||||
'statistics': 'Statistics',
|
'statistics': 'Peer Statistics',
|
||||||
|
'total_peers': 'Total Peers',
|
||||||
|
'online_peers': 'Online Peers',
|
||||||
'uptime': 'Uptime',
|
'uptime': 'Uptime',
|
||||||
'connections': 'Active connections',
|
'connections': 'Active connections',
|
||||||
'peers_title': 'Peer Management',
|
'peers_title': 'Peer Management',
|
||||||
'peers_description': 'View and manage peer connections',
|
'peers_description': 'View and manage peer connections',
|
||||||
|
'connected_peers': 'Connected Peers',
|
||||||
'active_peers': 'Active Peers',
|
'active_peers': 'Active Peers',
|
||||||
'active_connections': 'Number of active connections',
|
'active_connections': 'Number of active connections',
|
||||||
'add_peer': 'Add Peer',
|
'add_peer': 'Add Peer',
|
||||||
'add_peer_description': 'Connect to a new node',
|
'add_peer_description': 'Connect to a new node',
|
||||||
|
'add_peer_btn': 'Add Peer',
|
||||||
'config_title': 'Configuration',
|
'config_title': 'Configuration',
|
||||||
'config_description': 'Node settings and network parameters',
|
'config_description': 'Node settings and network parameters',
|
||||||
'basic_settings': 'Basic Settings',
|
'basic_settings': 'Basic Settings',
|
||||||
|
@ -37,7 +46,6 @@ window.translations.en = {
|
||||||
'access_dashboard': 'Access Dashboard',
|
'access_dashboard': 'Access Dashboard',
|
||||||
'error_invalid_password': 'Invalid password. Please try again.',
|
'error_invalid_password': 'Invalid password. Please try again.',
|
||||||
'error_too_many_attempts': 'Too many failed attempts. Please wait 1 minute before trying again.',
|
'error_too_many_attempts': 'Too many failed attempts. Please wait 1 minute before trying again.',
|
||||||
'add_peer_btn': 'Add Peer',
|
|
||||||
'notification_success': 'Success',
|
'notification_success': 'Success',
|
||||||
'notification_error': 'Error',
|
'notification_error': 'Error',
|
||||||
'notification_warning': 'Warning',
|
'notification_warning': 'Warning',
|
||||||
|
|
|
@ -8,19 +8,28 @@ window.translations.ru = {
|
||||||
'nav_config': 'Конфигурация',
|
'nav_config': 'Конфигурация',
|
||||||
'status_title': 'Состояние узла',
|
'status_title': 'Состояние узла',
|
||||||
'status_active': 'Активен',
|
'status_active': 'Активен',
|
||||||
'status_description': 'WebUI запущен и доступен',
|
'status_description': 'Информация о текущем состоянии вашего узла Yggdrasil',
|
||||||
|
'node_info': 'Информация об узле',
|
||||||
|
'public_key': 'Публичный ключ',
|
||||||
|
'version': 'Версия',
|
||||||
|
'routing_entries': 'Записей маршрутизации',
|
||||||
|
'loading': 'Загрузка...',
|
||||||
'network_info': 'Сетевая информация',
|
'network_info': 'Сетевая информация',
|
||||||
'address': 'Адрес',
|
'address': 'Адрес',
|
||||||
'subnet': 'Подсеть',
|
'subnet': 'Подсеть',
|
||||||
'statistics': 'Статистика',
|
'statistics': 'Статистика пиров',
|
||||||
|
'total_peers': 'Всего пиров',
|
||||||
|
'online_peers': 'Онлайн пиров',
|
||||||
'uptime': 'Время работы',
|
'uptime': 'Время работы',
|
||||||
'connections': 'Активных соединений',
|
'connections': 'Активных соединений',
|
||||||
'peers_title': 'Управление пирами',
|
'peers_title': 'Управление пирами',
|
||||||
'peers_description': 'Просмотр и управление соединениями с пирами',
|
'peers_description': 'Просмотр и управление соединениями с пирами',
|
||||||
|
'connected_peers': 'Подключенные пиры',
|
||||||
'active_peers': 'Активные пиры',
|
'active_peers': 'Активные пиры',
|
||||||
'active_connections': 'Количество активных соединений',
|
'active_connections': 'Количество активных соединений',
|
||||||
'add_peer': 'Добавить пир',
|
'add_peer': 'Добавить пир',
|
||||||
'add_peer_description': 'Подключение к новому узлу',
|
'add_peer_description': 'Подключение к новому узлу',
|
||||||
|
'add_peer_btn': 'Добавить пир',
|
||||||
'config_title': 'Конфигурация',
|
'config_title': 'Конфигурация',
|
||||||
'config_description': 'Настройки узла и параметры сети',
|
'config_description': 'Настройки узла и параметры сети',
|
||||||
'basic_settings': 'Основные настройки',
|
'basic_settings': 'Основные настройки',
|
||||||
|
@ -37,7 +46,6 @@ window.translations.ru = {
|
||||||
'access_dashboard': 'Войти в панель',
|
'access_dashboard': 'Войти в панель',
|
||||||
'error_invalid_password': 'Неверный пароль. Попробуйте снова.',
|
'error_invalid_password': 'Неверный пароль. Попробуйте снова.',
|
||||||
'error_too_many_attempts': 'Слишком много неудачных попыток. Подождите 1 минуту перед повторной попыткой.',
|
'error_too_many_attempts': 'Слишком много неудачных попыток. Подождите 1 минуту перед повторной попыткой.',
|
||||||
'add_peer_btn': 'Добавить пир',
|
|
||||||
'notification_success': 'Успешно',
|
'notification_success': 'Успешно',
|
||||||
'notification_error': 'Ошибка',
|
'notification_error': 'Ошибка',
|
||||||
'notification_warning': 'Предупреждение',
|
'notification_warning': 'Предупреждение',
|
||||||
|
|
266
src/webui/static/main.js
Normal file
266
src/webui/static/main.js
Normal file
|
@ -0,0 +1,266 @@
|
||||||
|
/**
|
||||||
|
* Main JavaScript logic for Yggdrasil Web Interface
|
||||||
|
* Handles language switching, theme management, notifications, and UI interactions
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Global state variables
|
||||||
|
let currentLanguage = localStorage.getItem('yggdrasil-language') || 'ru';
|
||||||
|
let currentTheme = localStorage.getItem('yggdrasil-theme') || 'light';
|
||||||
|
|
||||||
|
// Elements that should not be overwritten by translations when they contain data
|
||||||
|
const dataElements = [
|
||||||
|
'node-key', 'node-version', 'routing-entries', 'node-address',
|
||||||
|
'node-subnet', 'peers-count', 'peers-online'
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if an element contains actual data (not just loading text or empty)
|
||||||
|
*/
|
||||||
|
function hasDataContent(element) {
|
||||||
|
const text = element.textContent.trim();
|
||||||
|
const loadingTexts = ['Loading...', 'Загрузка...', 'N/A', ''];
|
||||||
|
return !loadingTexts.includes(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update all text elements based on current language
|
||||||
|
*/
|
||||||
|
function updateTexts() {
|
||||||
|
const elements = document.querySelectorAll('[data-key]');
|
||||||
|
elements.forEach(element => {
|
||||||
|
const key = element.getAttribute('data-key');
|
||||||
|
const elementId = element.id;
|
||||||
|
|
||||||
|
// Skip data elements that already have content loaded
|
||||||
|
if (elementId && dataElements.includes(elementId) && hasDataContent(element)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (window.translations && window.translations[currentLanguage] && window.translations[currentLanguage][key]) {
|
||||||
|
element.textContent = window.translations[currentLanguage][key];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Refresh displayed data after language change
|
||||||
|
*/
|
||||||
|
function refreshDataDisplay() {
|
||||||
|
// If we have node info, refresh its display
|
||||||
|
if (window.nodeInfo) {
|
||||||
|
window.updateNodeInfoDisplay(window.nodeInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we have peers data, refresh its display
|
||||||
|
if (window.peersData) {
|
||||||
|
window.updatePeersDisplay(window.peersData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggle between light and dark theme
|
||||||
|
*/
|
||||||
|
function toggleTheme() {
|
||||||
|
currentTheme = currentTheme === 'light' ? 'dark' : 'light';
|
||||||
|
applyTheme();
|
||||||
|
localStorage.setItem('yggdrasil-theme', currentTheme);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply the current theme to the document
|
||||||
|
*/
|
||||||
|
function applyTheme() {
|
||||||
|
document.documentElement.setAttribute('data-theme', currentTheme);
|
||||||
|
const themeBtn = document.getElementById('theme-btn');
|
||||||
|
if (themeBtn) {
|
||||||
|
const icon = themeBtn.querySelector('.theme-icon');
|
||||||
|
if (icon) {
|
||||||
|
icon.textContent = currentTheme === 'light' ? '🌙' : '☀️';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Switch application language
|
||||||
|
* @param {string} lang - Language code (ru, en)
|
||||||
|
*/
|
||||||
|
function switchLanguage(lang) {
|
||||||
|
currentLanguage = lang;
|
||||||
|
localStorage.setItem('yggdrasil-language', lang);
|
||||||
|
|
||||||
|
// Update button states
|
||||||
|
document.querySelectorAll('.lang-btn').forEach(btn => btn.classList.remove('active'));
|
||||||
|
document.getElementById('lang-' + lang).classList.add('active');
|
||||||
|
|
||||||
|
// Update all texts
|
||||||
|
updateTexts();
|
||||||
|
|
||||||
|
// Refresh data display to preserve loaded data
|
||||||
|
refreshDataDisplay();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show a specific content section and hide others
|
||||||
|
* @param {string} sectionName - Name of the section to show
|
||||||
|
*/
|
||||||
|
function showSection(sectionName) {
|
||||||
|
// Hide all sections
|
||||||
|
const sections = document.querySelectorAll('.content-section');
|
||||||
|
sections.forEach(section => section.classList.remove('active'));
|
||||||
|
|
||||||
|
// Remove active class from all nav items
|
||||||
|
const navItems = document.querySelectorAll('.nav-item');
|
||||||
|
navItems.forEach(item => item.classList.remove('active'));
|
||||||
|
|
||||||
|
// Show selected section
|
||||||
|
const targetSection = document.getElementById(sectionName + '-section');
|
||||||
|
if (targetSection) {
|
||||||
|
targetSection.classList.add('active');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add active class to clicked nav item
|
||||||
|
if (event && event.target) {
|
||||||
|
event.target.closest('.nav-item').classList.add('active');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Logout function (placeholder)
|
||||||
|
*/
|
||||||
|
function logout() {
|
||||||
|
if (confirm('Are you sure you want to logout?')) {
|
||||||
|
// Clear stored preferences
|
||||||
|
localStorage.removeItem('yggdrasil-language');
|
||||||
|
localStorage.removeItem('yggdrasil-theme');
|
||||||
|
|
||||||
|
// Redirect or refresh
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Notification system
|
||||||
|
let notificationId = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show a notification to the user
|
||||||
|
* @param {string} message - Notification message
|
||||||
|
* @param {string} type - Notification type (info, success, error, warning)
|
||||||
|
* @param {string} title - Optional custom title
|
||||||
|
* @param {number} duration - Auto-hide duration in milliseconds (0 = no auto-hide)
|
||||||
|
* @returns {number} Notification ID
|
||||||
|
*/
|
||||||
|
function showNotification(message, type = 'info', title = null, duration = 5000) {
|
||||||
|
const container = document.getElementById('notifications-container');
|
||||||
|
const id = ++notificationId;
|
||||||
|
|
||||||
|
const icons = {
|
||||||
|
success: '✅',
|
||||||
|
error: '❌',
|
||||||
|
warning: '⚠️',
|
||||||
|
info: 'ℹ️'
|
||||||
|
};
|
||||||
|
|
||||||
|
const titles = {
|
||||||
|
success: window.translations[currentLanguage]['notification_success'] || 'Success',
|
||||||
|
error: window.translations[currentLanguage]['notification_error'] || 'Error',
|
||||||
|
warning: window.translations[currentLanguage]['notification_warning'] || 'Warning',
|
||||||
|
info: window.translations[currentLanguage]['notification_info'] || 'Information'
|
||||||
|
};
|
||||||
|
|
||||||
|
const notification = document.createElement('div');
|
||||||
|
notification.className = `notification ${type}`;
|
||||||
|
notification.id = `notification-${id}`;
|
||||||
|
|
||||||
|
notification.innerHTML = `
|
||||||
|
<div class="notification-icon">${icons[type] || icons.info}</div>
|
||||||
|
<div class="notification-content">
|
||||||
|
<div class="notification-title">${title || titles[type]}</div>
|
||||||
|
<div class="notification-message">${message}</div>
|
||||||
|
</div>
|
||||||
|
<button class="notification-close" onclick="removeNotification(${id})">×</button>
|
||||||
|
`;
|
||||||
|
|
||||||
|
container.appendChild(notification);
|
||||||
|
|
||||||
|
// Auto remove after duration
|
||||||
|
if (duration > 0) {
|
||||||
|
setTimeout(() => {
|
||||||
|
removeNotification(id);
|
||||||
|
}, duration);
|
||||||
|
}
|
||||||
|
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove a notification by ID
|
||||||
|
* @param {number} id - Notification ID to remove
|
||||||
|
*/
|
||||||
|
function removeNotification(id) {
|
||||||
|
const notification = document.getElementById(`notification-${id}`);
|
||||||
|
if (notification) {
|
||||||
|
notification.classList.add('removing');
|
||||||
|
setTimeout(() => {
|
||||||
|
if (notification.parentNode) {
|
||||||
|
notification.parentNode.removeChild(notification);
|
||||||
|
}
|
||||||
|
}, 300);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show success notification
|
||||||
|
* @param {string} message - Success message
|
||||||
|
* @param {string} title - Optional custom title
|
||||||
|
* @returns {number} Notification ID
|
||||||
|
*/
|
||||||
|
function showSuccess(message, title = null) {
|
||||||
|
return showNotification(message, 'success', title);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show error notification
|
||||||
|
* @param {string} message - Error message
|
||||||
|
* @param {string} title - Optional custom title
|
||||||
|
* @returns {number} Notification ID
|
||||||
|
*/
|
||||||
|
function showError(message, title = null) {
|
||||||
|
return showNotification(message, 'error', title);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show warning notification
|
||||||
|
* @param {string} message - Warning message
|
||||||
|
* @param {string} title - Optional custom title
|
||||||
|
* @returns {number} Notification ID
|
||||||
|
*/
|
||||||
|
function showWarning(message, title = null) {
|
||||||
|
return showNotification(message, 'warning', title);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show info notification
|
||||||
|
* @param {string} message - Info message
|
||||||
|
* @param {string} title - Optional custom title
|
||||||
|
* @returns {number} Notification ID
|
||||||
|
*/
|
||||||
|
function showInfo(message, title = null) {
|
||||||
|
return showNotification(message, 'info', title);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the application when DOM is loaded
|
||||||
|
*/
|
||||||
|
function initializeMain() {
|
||||||
|
// Set active language button
|
||||||
|
document.getElementById('lang-' + currentLanguage).classList.add('active');
|
||||||
|
|
||||||
|
// Update all texts
|
||||||
|
updateTexts();
|
||||||
|
|
||||||
|
// Apply saved theme
|
||||||
|
applyTheme();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize when DOM is ready
|
||||||
|
document.addEventListener('DOMContentLoaded', initializeMain);
|
|
@ -524,7 +524,7 @@ header p {
|
||||||
|
|
||||||
.info-grid {
|
.info-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
grid-template-columns: 1fr;
|
||||||
gap: 20px;
|
gap: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue