Spaces:
Runtime error
Runtime error
| /** | |
| * Dashboard animations and interactivity | |
| */ | |
| // Initialize animations when DOM is loaded | |
| document.addEventListener('DOMContentLoaded', () => { | |
| initializeAnimations(); | |
| initializeCharts(); | |
| initializeEventListeners(); | |
| }); | |
| // Animation utilities | |
| const animate = { | |
| fadeIn: (element, delay = 0) => { | |
| element.style.opacity = '0'; | |
| element.style.transform = 'translateY(20px)'; | |
| setTimeout(() => { | |
| element.style.transition = 'all 0.5s ease'; | |
| element.style.opacity = '1'; | |
| element.style.transform = 'translateY(0)'; | |
| }, delay); | |
| }, | |
| slideIn: (element, direction = 'left', delay = 0) => { | |
| const transforms = { | |
| left: 'translateX(-50px)', | |
| right: 'translateX(50px)', | |
| up: 'translateY(50px)', | |
| down: 'translateY(-50px)' | |
| }; | |
| element.style.opacity = '0'; | |
| element.style.transform = transforms[direction]; | |
| setTimeout(() => { | |
| element.style.transition = 'all 0.5s ease'; | |
| element.style.opacity = '1'; | |
| element.style.transform = 'translate(0)'; | |
| }, delay); | |
| }, | |
| pulse: (element) => { | |
| element.style.transform = 'scale(1)'; | |
| element.style.transition = 'transform 0.2s ease'; | |
| element.style.transform = 'scale(1.05)'; | |
| setTimeout(() => { | |
| element.style.transform = 'scale(1)'; | |
| }, 200); | |
| } | |
| }; | |
| // Initialize animations | |
| function initializeAnimations() { | |
| // Animate stat cards | |
| const statCards = document.querySelectorAll('.stat-card'); | |
| statCards.forEach((card, index) => { | |
| animate.fadeIn(card, index * 100); | |
| }); | |
| // Animate charts | |
| const charts = document.querySelectorAll('.chart-container'); | |
| charts.forEach((chart, index) => { | |
| animate.slideIn(chart, 'up', 300 + index * 100); | |
| }); | |
| // Animate session cards | |
| const sessionCards = document.querySelectorAll('.session-card'); | |
| sessionCards.forEach((card, index) => { | |
| animate.slideIn(card, 'right', 500 + index * 100); | |
| }); | |
| } | |
| // Initialize Charts | |
| function initializeCharts() { | |
| // Traffic Chart | |
| const trafficChart = document.getElementById('traffic-chart'); | |
| if (trafficChart) { | |
| new Chart(trafficChart, { | |
| type: 'line', | |
| data: { | |
| labels: getLastNHours(24), | |
| datasets: [{ | |
| label: 'Upload', | |
| data: generateRandomData(24), | |
| borderColor: '#4F46E5', | |
| tension: 0.4 | |
| }, { | |
| label: 'Download', | |
| data: generateRandomData(24), | |
| borderColor: '#10B981', | |
| tension: 0.4 | |
| }] | |
| }, | |
| options: { | |
| responsive: true, | |
| maintainAspectRatio: false, | |
| plugins: { | |
| legend: { | |
| position: 'top' | |
| } | |
| }, | |
| scales: { | |
| y: { | |
| beginAtZero: true | |
| } | |
| } | |
| } | |
| }); | |
| } | |
| // Protocol Distribution Chart | |
| const protocolChart = document.getElementById('protocol-chart'); | |
| if (protocolChart) { | |
| new Chart(protocolChart, { | |
| type: 'doughnut', | |
| data: { | |
| labels: ['Shadowsocks', 'IKEv2', 'L2TP', 'PPTP'], | |
| datasets: [{ | |
| data: [40, 30, 20, 10], | |
| backgroundColor: [ | |
| '#4F46E5', | |
| '#10B981', | |
| '#F59E0B', | |
| '#EF4444' | |
| ] | |
| }] | |
| }, | |
| options: { | |
| responsive: true, | |
| maintainAspectRatio: false, | |
| plugins: { | |
| legend: { | |
| position: 'bottom' | |
| } | |
| } | |
| } | |
| }); | |
| } | |
| } | |
| // Initialize Event Listeners | |
| function initializeEventListeners() { | |
| // Quick Action Buttons | |
| const quickActions = document.querySelectorAll('.quick-action'); | |
| quickActions.forEach(action => { | |
| action.addEventListener('mouseenter', () => { | |
| animate.pulse(action); | |
| }); | |
| }); | |
| // Responsive Sidebar Toggle | |
| const sidebarToggle = document.getElementById('sidebar-toggle'); | |
| const sidebar = document.querySelector('.dashboard-sidebar'); | |
| if (sidebarToggle && sidebar) { | |
| sidebarToggle.addEventListener('click', () => { | |
| sidebar.classList.toggle('active'); | |
| }); | |
| } | |
| // Live Data Updates | |
| setInterval(updateLiveStats, 5000); | |
| } | |
| // Utility Functions | |
| function getLastNHours(n) { | |
| const hours = []; | |
| for (let i = n - 1; i >= 0; i--) { | |
| const d = new Date(); | |
| d.setHours(d.getHours() - i); | |
| hours.push(d.getHours() + ':00'); | |
| } | |
| return hours; | |
| } | |
| function generateRandomData(n) { | |
| return Array.from({length: n}, () => Math.floor(Math.random() * 100)); | |
| } | |
| function formatBytes(bytes) { | |
| if (bytes === 0) return '0 B'; | |
| const k = 1024; | |
| const sizes = ['B', 'KB', 'MB', 'GB', 'TB']; | |
| const i = Math.floor(Math.log(bytes) / Math.log(k)); | |
| return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; | |
| } | |
| // Live Data Updates | |
| function updateLiveStats() { | |
| fetch('/api/stats') | |
| .then(response => response.json()) | |
| .then(data => { | |
| // Update connection status | |
| const statusIndicator = document.querySelector('.status-indicator'); | |
| if (statusIndicator) { | |
| statusIndicator.className = `status-indicator ${data.isConnected ? 'online' : 'offline'}`; | |
| statusIndicator.textContent = data.isConnected ? 'Connected' : 'Disconnected'; | |
| } | |
| // Update traffic stats | |
| const uploadStat = document.getElementById('upload-stat'); | |
| const downloadStat = document.getElementById('download-stat'); | |
| if (uploadStat) uploadStat.textContent = formatBytes(data.uploadTotal); | |
| if (downloadStat) downloadStat.textContent = formatBytes(data.downloadTotal); | |
| // Update active sessions | |
| updateSessionsList(data.sessions); | |
| }) | |
| .catch(console.error); | |
| } | |
| function updateSessionsList(sessions) { | |
| const sessionsList = document.getElementById('active-sessions'); | |
| if (!sessionsList) return; | |
| sessions.forEach(session => { | |
| const existingSession = document.getElementById(`session-${session.id}`); | |
| if (!existingSession) { | |
| // Create new session card | |
| const sessionCard = createSessionCard(session); | |
| animate.slideIn(sessionCard, 'right'); | |
| sessionsList.appendChild(sessionCard); | |
| } else { | |
| // Update existing session | |
| updateSessionCard(existingSession, session); | |
| } | |
| }); | |
| } | |
| function createSessionCard(session) { | |
| const card = document.createElement('div'); | |
| card.id = `session-${session.id}`; | |
| card.className = 'session-card dashboard-card'; | |
| card.innerHTML = ` | |
| <div class="session-info"> | |
| <h6 class="mb-1">${session.protocol}</h6> | |
| <small class="text-muted">${session.ip}</small> | |
| </div> | |
| <div class="session-stats"> | |
| <div class="mb-1">β ${formatBytes(session.upload)}</div> | |
| <div>β ${formatBytes(session.download)}</div> | |
| </div> | |
| `; | |
| return card; | |
| } | |
| function updateSessionCard(card, session) { | |
| const uploadEl = card.querySelector('.session-stats div:first-child'); | |
| const downloadEl = card.querySelector('.session-stats div:last-child'); | |
| uploadEl.textContent = `β ${formatBytes(session.upload)}`; | |
| downloadEl.textContent = `β ${formatBytes(session.download)}`; | |
| } | |