| |
| |
| |
| |
| |
| |
|
|
| |
| |
| |
|
|
| |
| |
| const getBackendURL = () => { |
| |
| return window.location.origin; |
| }; |
|
|
| const getWebSocketURL = () => { |
| |
| const protocol = window.location.protocol === "https:" ? "wss" : "ws"; |
| const host = window.location.host; |
| return `${protocol}://${host}/ws`; |
| }; |
|
|
| |
| const baseConfig = window.DASHBOARD_CONFIG || {}; |
| const backendURL = getBackendURL(); |
| const wsURL = getWebSocketURL(); |
| const CONFIG = { |
| ...baseConfig, |
| |
| BACKEND_URL: backendURL, |
| WS_URL: wsURL, |
| UPDATE_INTERVAL: baseConfig.UPDATE_INTERVAL || 30000, |
| CACHE_TTL: baseConfig.CACHE_TTL || 60000, |
| }; |
|
|
| |
| CONFIG.BACKEND_URL = window.location.origin; |
| const wsProtocol = window.location.protocol === "https:" ? "wss" : "ws"; |
| CONFIG.WS_URL = `${wsProtocol}://${window.location.host}/ws`; |
|
|
| |
| console.log('[Config] Backend URL:', CONFIG.BACKEND_URL); |
| console.log('[Config] WebSocket URL:', CONFIG.WS_URL); |
| console.log('[Config] Current hostname:', window.location.hostname); |
|
|
| |
| |
| |
|
|
| class WebSocketClient { |
| constructor(url) { |
| this.url = url; |
| this.socket = null; |
| this.status = 'disconnected'; |
| this.reconnectAttempts = 0; |
| this.maxReconnectAttempts = 5; |
| this.reconnectDelay = 3000; |
| this.listeners = new Map(); |
| this.heartbeatInterval = null; |
| } |
|
|
| connect() { |
| if (this.socket && this.socket.readyState === WebSocket.OPEN) { |
| console.log('[WS] Already connected'); |
| return; |
| } |
|
|
| try { |
| console.log('[WS] Connecting to:', this.url); |
| this.socket = new WebSocket(this.url); |
| |
| this.socket.onopen = this.handleOpen.bind(this); |
| this.socket.onmessage = this.handleMessage.bind(this); |
| this.socket.onerror = this.handleError.bind(this); |
| this.socket.onclose = this.handleClose.bind(this); |
| |
| this.updateStatus('connecting'); |
| } catch (error) { |
| console.error('[WS] Connection error:', error); |
| this.scheduleReconnect(); |
| } |
| } |
|
|
| handleOpen() { |
| console.log('[WS] Connected successfully'); |
| this.status = 'connected'; |
| this.reconnectAttempts = 0; |
| this.updateStatus('connected'); |
| this.startHeartbeat(); |
| this.emit('connected', true); |
| } |
|
|
| handleMessage(event) { |
| try { |
| const data = JSON.parse(event.data); |
| console.log('[WS] Message received:', data.type); |
| |
| if (data.type === 'heartbeat') { |
| this.send({ type: 'pong' }); |
| return; |
| } |
| |
| this.emit(data.type, data); |
| this.emit('message', data); |
| } catch (error) { |
| console.error('[WS] Message parse error:', error); |
| } |
| } |
|
|
| handleError(error) { |
| |
| |
| const socketState = this.socket ? this.socket.readyState : 'null'; |
| const stateNames = { |
| 0: 'CONNECTING', |
| 1: 'OPEN', |
| 2: 'CLOSING', |
| 3: 'CLOSED' |
| }; |
| |
| const stateName = stateNames[socketState] || `UNKNOWN(${socketState})`; |
| |
| |
| if (!this._errorLogged) { |
| console.error('[WS] Connection error:', { |
| url: this.url, |
| state: stateName, |
| readyState: socketState, |
| message: 'WebSocket connection failed. Check if server is running and URL is correct.' |
| }); |
| this._errorLogged = true; |
| |
| |
| setTimeout(() => { |
| this._errorLogged = false; |
| }, 5000); |
| } |
| |
| this.updateStatus('error'); |
| |
| |
| if (this.socket && this.socket.readyState === WebSocket.CLOSED && |
| this.reconnectAttempts < this.maxReconnectAttempts) { |
| this.scheduleReconnect(); |
| } |
| } |
|
|
| handleClose() { |
| console.log('[WS] Connection closed'); |
| this.status = 'disconnected'; |
| this.updateStatus('disconnected'); |
| this.stopHeartbeat(); |
| |
| |
| if (this.socket) { |
| try { |
| |
| this.socket.onopen = null; |
| this.socket.onclose = null; |
| this.socket.onerror = null; |
| this.socket.onmessage = null; |
| } catch (e) { |
| |
| } |
| |
| |
| } |
| |
| this.emit('connected', false); |
| this.scheduleReconnect(); |
| } |
|
|
| scheduleReconnect() { |
| if (this.reconnectAttempts >= this.maxReconnectAttempts) { |
| console.error('[WS] Max reconnection attempts reached'); |
| return; |
| } |
|
|
| this.reconnectAttempts++; |
| console.log(`[WS] Reconnecting in ${this.reconnectDelay}ms (attempt ${this.reconnectAttempts})`); |
| |
| setTimeout(() => this.connect(), this.reconnectDelay); |
| } |
|
|
| startHeartbeat() { |
| |
| if (this.heartbeatInterval) { |
| clearInterval(this.heartbeatInterval); |
| } |
| |
| this.heartbeatInterval = setInterval(() => { |
| |
| if (this.socket && this.socket.readyState === WebSocket.OPEN) { |
| const sent = this.send({ type: 'ping' }); |
| if (!sent) { |
| |
| this.stopHeartbeat(); |
| if (this.reconnectAttempts < this.maxReconnectAttempts) { |
| this.scheduleReconnect(); |
| } |
| } |
| } else { |
| |
| this.stopHeartbeat(); |
| } |
| }, 30000); |
| } |
|
|
| stopHeartbeat() { |
| if (this.heartbeatInterval) { |
| clearInterval(this.heartbeatInterval); |
| this.heartbeatInterval = null; |
| } |
| } |
|
|
| send(data) { |
| if (!this.socket) { |
| console.warn('[WS] Cannot send - socket is null'); |
| return false; |
| } |
| |
| |
| if (this.socket.readyState === WebSocket.OPEN) { |
| try { |
| this.socket.send(JSON.stringify(data)); |
| return true; |
| } catch (error) { |
| console.error('[WS] Error sending message:', error); |
| |
| if (error.message && (error.message.includes('close') || error.message.includes('send'))) { |
| this.handleClose(); |
| } |
| return false; |
| } |
| } |
| |
| console.warn('[WS] Cannot send - socket state:', this.socket.readyState); |
| return false; |
| } |
|
|
| on(event, callback) { |
| if (!this.listeners.has(event)) { |
| this.listeners.set(event, []); |
| } |
| this.listeners.get(event).push(callback); |
| } |
|
|
| emit(event, data) { |
| if (this.listeners.has(event)) { |
| this.listeners.get(event).forEach(callback => callback(data)); |
| } |
| } |
|
|
| updateStatus(status) { |
| this.status = status; |
| |
| const statusBar = document.getElementById('connection-status-bar'); |
| const statusDot = document.getElementById('ws-status-dot'); |
| const statusText = document.getElementById('ws-status-text'); |
| |
| if (statusBar && statusDot && statusText) { |
| if (status === 'connected') { |
| statusBar.classList.remove('disconnected'); |
| statusText.textContent = 'متصل'; |
| } else if (status === 'disconnected' || status === 'error') { |
| statusBar.classList.add('disconnected'); |
| statusText.textContent = status === 'error' ? 'خطا در اتصال' : 'قطع شده'; |
| } else { |
| statusText.textContent = 'در حال اتصال...'; |
| } |
| } |
| } |
|
|
| isConnected() { |
| return this.socket && this.socket.readyState === WebSocket.OPEN; |
| } |
|
|
| disconnect() { |
| this.stopHeartbeat(); |
| |
| if (this.socket) { |
| try { |
| |
| if (this.socket.readyState === WebSocket.OPEN || this.socket.readyState === WebSocket.CONNECTING) { |
| this.socket.close(); |
| } |
| } catch (error) { |
| console.warn('[WS] Error during disconnect:', error); |
| } finally { |
| |
| setTimeout(() => { |
| try { |
| if (this.socket) { |
| this.socket.onopen = null; |
| this.socket.onclose = null; |
| this.socket.onerror = null; |
| this.socket.onmessage = null; |
| this.socket = null; |
| } |
| } catch (e) { |
| |
| } |
| }, 100); |
| } |
| } |
| |
| this.status = 'disconnected'; |
| this.updateStatus('disconnected'); |
| } |
| } |
|
|
| |
| |
| |
|
|
| class APIClient { |
| constructor(baseURL) { |
| this.baseURL = baseURL; |
| this.cache = new Map(); |
| } |
|
|
| async request(endpoint, options = {}) { |
| const url = `${this.baseURL}${endpoint}`; |
| const cacheKey = `${options.method || 'GET'}:${url}`; |
|
|
| |
| if (options.cache && this.cache.has(cacheKey)) { |
| const cached = this.cache.get(cacheKey); |
| if (Date.now() - cached.timestamp < CONFIG.CACHE_TTL) { |
| console.log('[API] Cache hit:', endpoint); |
| return cached.data; |
| } |
| } |
|
|
| try { |
| console.log('[API] Request:', endpoint); |
| const response = await fetch(url, { |
| method: options.method || 'GET', |
| headers: { |
| 'Content-Type': 'application/json', |
| ...options.headers, |
| }, |
| body: options.body ? JSON.stringify(options.body) : undefined, |
| }); |
|
|
| if (!response.ok) { |
| throw new Error(`HTTP ${response.status}: ${response.statusText}`); |
| } |
|
|
| const data = await response.json(); |
|
|
| |
| if (!options.method || options.method === 'GET') { |
| this.cache.set(cacheKey, { |
| data, |
| timestamp: Date.now(), |
| }); |
| } |
|
|
| return data; |
| } catch (error) { |
| console.error('[API] Error:', endpoint, error); |
| throw error; |
| } |
| } |
|
|
| |
| async getMarket() { |
| return this.request('/api/market', { cache: true }); |
| } |
|
|
| async getTrending() { |
| return this.request('/api/trending', { cache: true }); |
| } |
|
|
| async getSentiment() { |
| return this.request('/api/sentiment', { cache: true }); |
| } |
|
|
| async getStats() { |
| return this.request('/api/market/stats', { cache: true }); |
| } |
|
|
| |
| async getNews(limit = 20) { |
| return this.request(`/api/news/latest?limit=${limit}`, { cache: true }); |
| } |
|
|
| |
| async getProviders() { |
| return this.request('/api/providers', { cache: true }); |
| } |
|
|
| |
| async getChartData(symbol, interval = '1h', limit = 100) { |
| return this.request(`/api/ohlcv?symbol=${symbol}&interval=${interval}&limit=${limit}`, { cache: true }); |
| } |
| } |
|
|
| |
| |
| |
|
|
| const Utils = { |
| formatCurrency(value) { |
| if (value === null || value === undefined || isNaN(value)) { |
| return '—'; |
| } |
| const num = Number(value); |
| if (Math.abs(num) >= 1e12) { |
| return `$${(num / 1e12).toFixed(2)}T`; |
| } |
| if (Math.abs(num) >= 1e9) { |
| return `$${(num / 1e9).toFixed(2)}B`; |
| } |
| if (Math.abs(num) >= 1e6) { |
| return `$${(num / 1e6).toFixed(2)}M`; |
| } |
| if (Math.abs(num) >= 1e3) { |
| return `$${(num / 1e3).toFixed(2)}K`; |
| } |
| return `$${num.toLocaleString(undefined, { |
| minimumFractionDigits: 2, |
| maximumFractionDigits: 2 |
| })}`; |
| }, |
|
|
| formatPercent(value) { |
| if (value === null || value === undefined || isNaN(value)) { |
| return '—'; |
| } |
| const num = Number(value); |
| const sign = num >= 0 ? '+' : ''; |
| return `${sign}${num.toFixed(2)}%`; |
| }, |
|
|
| formatNumber(value) { |
| if (value === null || value === undefined || isNaN(value)) { |
| return '—'; |
| } |
| return Number(value).toLocaleString(); |
| }, |
|
|
| formatDate(timestamp) { |
| const date = new Date(timestamp); |
| return date.toLocaleDateString('fa-IR', { |
| year: 'numeric', |
| month: 'long', |
| day: 'numeric', |
| hour: '2-digit', |
| minute: '2-digit', |
| }); |
| }, |
|
|
| getChangeClass(value) { |
| if (value > 0) return 'positive'; |
| if (value < 0) return 'negative'; |
| return 'neutral'; |
| }, |
|
|
| showLoader(element) { |
| if (element) { |
| element.innerHTML = ` |
| <div class="loading-cell"> |
| <div class="loader"></div> |
| در حال بارگذاری... |
| </div> |
| `; |
| } |
| }, |
|
|
| showError(element, message) { |
| if (element) { |
| element.innerHTML = ` |
| <div class="error-message"> |
| <i class="fas fa-exclamation-circle"></i> |
| ${message} |
| </div> |
| `; |
| } |
| }, |
|
|
| debounce(func, wait) { |
| let timeout; |
| return function executedFunction(...args) { |
| const later = () => { |
| clearTimeout(timeout); |
| func(...args); |
| }; |
| clearTimeout(timeout); |
| timeout = setTimeout(later, wait); |
| }; |
| }, |
| }; |
|
|
| |
| |
| |
|
|
| class ViewManager { |
| constructor() { |
| this.currentView = 'overview'; |
| this.views = new Map(); |
| this.init(); |
| } |
|
|
| init() { |
| |
| document.querySelectorAll('.nav-tab-btn').forEach(btn => { |
| btn.addEventListener('click', (e) => { |
| const view = btn.dataset.view; |
| this.switchView(view); |
| }); |
| }); |
|
|
| |
| document.querySelectorAll('.mobile-nav-tab-btn').forEach(btn => { |
| btn.addEventListener('click', (e) => { |
| const view = btn.dataset.view; |
| this.switchView(view); |
| }); |
| }); |
| } |
|
|
| switchView(viewName) { |
| if (this.currentView === viewName) return; |
|
|
| |
| document.querySelectorAll('.view-section').forEach(section => { |
| section.classList.remove('active'); |
| }); |
|
|
| |
| const viewSection = document.getElementById(`view-${viewName}`); |
| if (viewSection) { |
| viewSection.classList.add('active'); |
| } |
|
|
| |
| document.querySelectorAll('.nav-tab-btn, .mobile-nav-tab-btn').forEach(btn => { |
| btn.classList.remove('active'); |
| if (btn.dataset.view === viewName) { |
| btn.classList.add('active'); |
| } |
| }); |
|
|
| this.currentView = viewName; |
| console.log('[View] Switched to:', viewName); |
|
|
| |
| this.triggerViewUpdate(viewName); |
| } |
|
|
| triggerViewUpdate(viewName) { |
| const event = new CustomEvent('viewChange', { detail: { view: viewName } }); |
| document.dispatchEvent(event); |
| } |
| } |
|
|
| |
| |
| |
|
|
| class DashboardApp { |
| constructor() { |
| this.ws = new WebSocketClient(CONFIG.WS_URL); |
| this.api = new APIClient(CONFIG.BACKEND_URL); |
| this.viewManager = new ViewManager(); |
| this.updateInterval = null; |
| this.data = { |
| market: null, |
| sentiment: null, |
| trending: null, |
| news: [], |
| }; |
| } |
|
|
| async init() { |
| console.log('[App] Initializing dashboard...'); |
|
|
| |
| this.ws.connect(); |
| this.setupWebSocketHandlers(); |
|
|
| |
| this.setupUIHandlers(); |
|
|
| |
| await this.loadInitialData(); |
|
|
| |
| this.startPeriodicUpdates(); |
|
|
| console.log('[App] Dashboard initialized successfully'); |
| } |
|
|
| setupWebSocketHandlers() { |
| this.ws.on('connected', (isConnected) => { |
| console.log('[App] WebSocket connection status:', isConnected); |
| if (isConnected) { |
| this.ws.send({ type: 'subscribe', groups: ['market', 'sentiment'] }); |
| } |
| }); |
|
|
| this.ws.on('market_update', (data) => { |
| console.log('[App] Market update received'); |
| this.handleMarketUpdate(data); |
| }); |
|
|
| this.ws.on('sentiment_update', (data) => { |
| console.log('[App] Sentiment update received'); |
| this.handleSentimentUpdate(data); |
| }); |
|
|
| this.ws.on('stats_update', (data) => { |
| console.log('[App] Stats update received'); |
| this.updateOnlineUsers(data.active_connections || 0); |
| }); |
| } |
|
|
| setupUIHandlers() { |
| |
| const themeToggle = document.getElementById('theme-toggle'); |
| if (themeToggle) { |
| themeToggle.addEventListener('click', () => this.toggleTheme()); |
| } |
|
|
| |
| const notificationsBtn = document.getElementById('notifications-btn'); |
| const notificationsPanel = document.getElementById('notifications-panel'); |
| const closeNotifications = document.getElementById('close-notifications'); |
|
|
| if (notificationsBtn && notificationsPanel) { |
| notificationsBtn.addEventListener('click', () => { |
| notificationsPanel.classList.toggle('active'); |
| }); |
| } |
|
|
| if (closeNotifications && notificationsPanel) { |
| closeNotifications.addEventListener('click', () => { |
| notificationsPanel.classList.remove('active'); |
| }); |
| } |
|
|
| |
| const refreshCoins = document.getElementById('refresh-coins'); |
| if (refreshCoins) { |
| refreshCoins.addEventListener('click', () => this.loadMarketData()); |
| } |
|
|
| |
| const minimizeStats = document.getElementById('minimize-stats'); |
| const floatingStats = document.getElementById('floating-stats'); |
| if (minimizeStats && floatingStats) { |
| minimizeStats.addEventListener('click', () => { |
| floatingStats.classList.toggle('minimized'); |
| }); |
| } |
|
|
| |
| const globalSearch = document.getElementById('global-search'); |
| if (globalSearch) { |
| globalSearch.addEventListener('input', Utils.debounce((e) => { |
| this.handleSearch(e.target.value); |
| }, 300)); |
| } |
|
|
| |
| this.setupAIToolHandlers(); |
| } |
|
|
| setupAIToolHandlers() { |
| const sentimentBtn = document.getElementById('sentiment-analysis-btn'); |
| const summaryBtn = document.getElementById('news-summary-btn'); |
| const predictionBtn = document.getElementById('price-prediction-btn'); |
| const patternBtn = document.getElementById('pattern-detection-btn'); |
|
|
| if (sentimentBtn) { |
| sentimentBtn.addEventListener('click', () => this.runSentimentAnalysis()); |
| } |
|
|
| if (summaryBtn) { |
| summaryBtn.addEventListener('click', () => this.runNewsSummary()); |
| } |
|
|
| if (predictionBtn) { |
| predictionBtn.addEventListener('click', () => this.runPricePrediction()); |
| } |
|
|
| if (patternBtn) { |
| patternBtn.addEventListener('click', () => this.runPatternDetection()); |
| } |
|
|
| const clearResults = document.getElementById('clear-results'); |
| const aiResults = document.getElementById('ai-results'); |
| if (clearResults && aiResults) { |
| clearResults.addEventListener('click', () => { |
| aiResults.style.display = 'none'; |
| }); |
| } |
| } |
|
|
| async loadInitialData() { |
| this.showLoadingOverlay(true); |
|
|
| try { |
| await Promise.all([ |
| this.loadMarketData(), |
| this.loadSentimentData(), |
| this.loadTrendingData(), |
| this.loadNewsData(), |
| ]); |
| } catch (error) { |
| console.error('[App] Error loading initial data:', error); |
| } |
|
|
| this.showLoadingOverlay(false); |
| } |
|
|
| async loadMarketData() { |
| try { |
| const data = await this.api.getMarket(); |
| this.data.market = data; |
| this.renderMarketStats(data); |
| this.renderCoinsTable(data.cryptocurrencies || []); |
| } catch (error) { |
| console.error('[App] Error loading market data:', error); |
| } |
| } |
|
|
| async loadSentimentData() { |
| try { |
| const data = await this.api.getSentiment(); |
| |
| const transformed = this.transformSentimentData(data); |
| this.data.sentiment = transformed; |
| this.renderSentiment(transformed); |
| } catch (error) { |
| console.error('[App] Error loading sentiment data:', error); |
| } |
| } |
|
|
| transformSentimentData(data) { |
| |
| |
| if (!data) { |
| return { bullish: 0, neutral: 100, bearish: 0 }; |
| } |
|
|
| const value = data.value || 50; |
| const classification = data.classification || 'neutral'; |
|
|
| |
| let bullish = 0; |
| let neutral = 0; |
| let bearish = 0; |
|
|
| if (classification.includes('extreme_greed') || classification.includes('greed')) { |
| bullish = Math.max(60, value); |
| neutral = Math.max(20, 100 - value); |
| bearish = 100 - bullish - neutral; |
| } else if (classification.includes('extreme_fear') || classification.includes('fear')) { |
| bearish = Math.max(60, 100 - value); |
| neutral = Math.max(20, value); |
| bullish = 100 - bearish - neutral; |
| } else { |
| |
| neutral = 40 + Math.abs(50 - value) * 0.4; |
| const remaining = 100 - neutral; |
| bullish = remaining * (value / 100); |
| bearish = remaining - bullish; |
| } |
|
|
| |
| const total = bullish + neutral + bearish; |
| if (total > 0) { |
| bullish = Math.round((bullish / total) * 100); |
| neutral = Math.round((neutral / total) * 100); |
| bearish = 100 - bullish - neutral; |
| } |
|
|
| return { |
| bullish, |
| neutral, |
| bearish, |
| ...data |
| }; |
| } |
|
|
| async loadTrendingData() { |
| try { |
| const data = await this.api.getTrending(); |
| this.data.trending = data; |
| } catch (error) { |
| console.error('[App] Error loading trending data:', error); |
| } |
| } |
|
|
| async loadNewsData() { |
| try { |
| const data = await this.api.getNews(20); |
| this.data.news = data.news || []; |
| this.renderNews(this.data.news); |
| } catch (error) { |
| console.error('[App] Error loading news data:', error); |
| } |
| } |
|
|
| renderMarketStats(data) { |
| const totalMarketCap = document.getElementById('total-market-cap'); |
| const btcDominance = document.getElementById('btc-dominance'); |
| const volume24h = document.getElementById('volume-24h'); |
|
|
| if (totalMarketCap && data.total_market_cap) { |
| totalMarketCap.textContent = Utils.formatCurrency(data.total_market_cap); |
| } |
|
|
| if (btcDominance && data.btc_dominance) { |
| btcDominance.textContent = `${data.btc_dominance.toFixed(1)}%`; |
| } |
|
|
| if (volume24h && data.total_volume_24h) { |
| volume24h.textContent = Utils.formatCurrency(data.total_volume_24h); |
| } |
| } |
|
|
| renderCoinsTable(coins) { |
| const tbody = document.getElementById('coins-table-body'); |
| if (!tbody) return; |
|
|
| if (!coins || coins.length === 0) { |
| tbody.innerHTML = '<tr><td colspan="7">دادهای یافت نشد</td></tr>'; |
| return; |
| } |
|
|
| tbody.innerHTML = coins.slice(0, 20).map((coin, index) => ` |
| <tr> |
| <td>${index + 1}</td> |
| <td> |
| <div style="display: flex; align-items: center; gap: 8px;"> |
| <strong>${coin.symbol}</strong> |
| <span style="color: var(--text-muted); font-size: 0.875rem;">${coin.name}</span> |
| </div> |
| </td> |
| <td style="font-family: var(--font-mono);">${Utils.formatCurrency(coin.current_price)}</td> |
| <td> |
| <span class="stat-change ${Utils.getChangeClass(coin.price_change_percentage_24h)}"> |
| ${Utils.formatPercent(coin.price_change_percentage_24h)} |
| </span> |
| </td> |
| <td>${Utils.formatCurrency(coin.total_volume)}</td> |
| <td>${Utils.formatCurrency(coin.market_cap)}</td> |
| <td> |
| <button class="btn-ghost" onclick="app.viewCoinDetails('${coin.symbol}')"> |
| <i class="fas fa-chart-line"></i> |
| </button> |
| </td> |
| </tr> |
| `).join(''); |
| } |
|
|
| renderSentiment(data) { |
| if (!data) return; |
|
|
| const bullish = data.bullish || 0; |
| const neutral = data.neutral || 0; |
| const bearish = data.bearish || 0; |
|
|
| const bullishPercent = document.getElementById('bullish-percent'); |
| const neutralPercent = document.getElementById('neutral-percent'); |
| const bearishPercent = document.getElementById('bearish-percent'); |
|
|
| if (bullishPercent) bullishPercent.textContent = `${bullish}%`; |
| if (neutralPercent) neutralPercent.textContent = `${neutral}%`; |
| if (bearishPercent) bearishPercent.textContent = `${bearish}%`; |
|
|
| |
| const progressBars = document.querySelectorAll('.sentiment-progress-bar'); |
| progressBars.forEach(bar => { |
| if (bar.classList.contains('bullish')) { |
| bar.style.width = `${bullish}%`; |
| } else if (bar.classList.contains('neutral')) { |
| bar.style.width = `${neutral}%`; |
| } else if (bar.classList.contains('bearish')) { |
| bar.style.width = `${bearish}%`; |
| } |
| }); |
| } |
|
|
| renderNews(news) { |
| const newsGrid = document.getElementById('news-grid'); |
| if (!newsGrid) return; |
|
|
| if (!news || news.length === 0) { |
| newsGrid.innerHTML = '<p>خبری یافت نشد</p>'; |
| return; |
| } |
|
|
| newsGrid.innerHTML = news.map(item => ` |
| <div class="news-card"> |
| ${item.image ? `<img src="${item.image}" alt="${item.title}" class="news-card-image">` : ''} |
| <div class="news-card-content"> |
| <h3 class="news-card-title">${item.title}</h3> |
| <div class="news-card-meta"> |
| <span><i class="fas fa-clock"></i> ${Utils.formatDate(item.published_at || Date.now())}</span> |
| <span><i class="fas fa-newspaper"></i> ${item.source || 'Unknown'}</span> |
| </div> |
| <p class="news-card-excerpt">${item.description || item.summary || ''}</p> |
| </div> |
| </div> |
| `).join(''); |
| } |
|
|
| handleMarketUpdate(data) { |
| if (data.data) { |
| this.renderMarketStats(data.data); |
| if (data.data.cryptocurrencies) { |
| this.renderCoinsTable(data.data.cryptocurrencies); |
| } |
| } |
| } |
|
|
| handleSentimentUpdate(data) { |
| if (data.data) { |
| this.renderSentiment(data.data); |
| } |
| } |
|
|
| updateOnlineUsers(count) { |
| const activeUsersCount = document.getElementById('active-users-count'); |
| if (activeUsersCount) { |
| activeUsersCount.textContent = count; |
| } |
| } |
|
|
| startPeriodicUpdates() { |
| this.updateInterval = setInterval(() => { |
| console.log('[App] Periodic update triggered'); |
| this.loadMarketData(); |
| this.loadSentimentData(); |
| }, CONFIG.UPDATE_INTERVAL); |
| } |
|
|
| stopPeriodicUpdates() { |
| if (this.updateInterval) { |
| clearInterval(this.updateInterval); |
| this.updateInterval = null; |
| } |
| } |
|
|
| toggleTheme() { |
| document.body.classList.toggle('light-theme'); |
| const icon = document.querySelector('#theme-toggle i'); |
| if (icon) { |
| icon.classList.toggle('fa-moon'); |
| icon.classList.toggle('fa-sun'); |
| } |
| } |
|
|
| handleSearch(query) { |
| console.log('[App] Search query:', query); |
| |
| } |
|
|
| viewCoinDetails(symbol) { |
| console.log('[App] View coin details:', symbol); |
| |
| this.viewManager.switchView('charts'); |
| } |
|
|
| showLoadingOverlay(show) { |
| const overlay = document.getElementById('loading-overlay'); |
| if (overlay) { |
| if (show) { |
| overlay.classList.add('active'); |
| } else { |
| overlay.classList.remove('active'); |
| } |
| } |
| } |
|
|
| |
| async runSentimentAnalysis() { |
| const aiResults = document.getElementById('ai-results'); |
| const aiResultsContent = document.getElementById('ai-results-content'); |
| |
| if (!aiResults || !aiResultsContent) return; |
|
|
| aiResults.style.display = 'block'; |
| aiResultsContent.innerHTML = '<div class="loader"></div> در حال تحلیل...'; |
|
|
| try { |
| const data = await this.api.getSentiment(); |
| |
| aiResultsContent.innerHTML = ` |
| <div class="ai-result-card"> |
| <h4>نتایج تحلیل احساسات</h4> |
| <div class="sentiment-summary"> |
| <div class="sentiment-summary-item"> |
| <div class="summary-label">صعودی</div> |
| <div class="summary-value bullish">${data.bullish}%</div> |
| </div> |
| <div class="sentiment-summary-item"> |
| <div class="summary-label">خنثی</div> |
| <div class="summary-value neutral">${data.neutral}%</div> |
| </div> |
| <div class="sentiment-summary-item"> |
| <div class="summary-label">نزولی</div> |
| <div class="summary-value bearish">${data.bearish}%</div> |
| </div> |
| </div> |
| <p style="margin-top: 1rem; color: var(--text-muted);"> |
| ${data.summary || 'تحلیل احساسات بازار بر اساس دادههای جمعآوری شده از منابع مختلف'} |
| </p> |
| </div> |
| `; |
| } catch (error) { |
| aiResultsContent.innerHTML = ` |
| <div class="error-message"> |
| <i class="fas fa-exclamation-circle"></i> |
| خطا در تحلیل: ${error.message} |
| </div> |
| `; |
| } |
| } |
|
|
| async runNewsSummary() { |
| const aiResults = document.getElementById('ai-results'); |
| const aiResultsContent = document.getElementById('ai-results-content'); |
| |
| if (!aiResults || !aiResultsContent) return; |
|
|
| aiResults.style.display = 'block'; |
| aiResultsContent.innerHTML = '<div class="loader"></div> در حال خلاصهسازی...'; |
|
|
| setTimeout(() => { |
| aiResultsContent.innerHTML = ` |
| <div class="ai-result-card"> |
| <h4>خلاصه اخبار</h4> |
| <p>قابلیت خلاصهسازی اخبار به زودی اضافه خواهد شد.</p> |
| <p style="color: var(--text-muted); font-size: 0.875rem;"> |
| این قابلیت از مدلهای Hugging Face برای خلاصهسازی متن استفاده میکند. |
| </p> |
| </div> |
| `; |
| }, 1000); |
| } |
|
|
| async runPricePrediction() { |
| const aiResults = document.getElementById('ai-results'); |
| const aiResultsContent = document.getElementById('ai-results-content'); |
| |
| if (!aiResults || !aiResultsContent) return; |
|
|
| aiResults.style.display = 'block'; |
| aiResultsContent.innerHTML = '<div class="loader"></div> در حال پیشبینی...'; |
|
|
| setTimeout(() => { |
| aiResultsContent.innerHTML = ` |
| <div class="ai-result-card"> |
| <h4>پیشبینی قیمت</h4> |
| <p>قابلیت پیشبینی قیمت به زودی اضافه خواهد شد.</p> |
| <p style="color: var(--text-muted); font-size: 0.875rem;"> |
| این قابلیت از مدلهای یادگیری ماشین برای پیشبینی روند قیمت استفاده میکند. |
| </p> |
| </div> |
| `; |
| }, 1000); |
| } |
|
|
| async runPatternDetection() { |
| const aiResults = document.getElementById('ai-results'); |
| const aiResultsContent = document.getElementById('ai-results-content'); |
| |
| if (!aiResults || !aiResultsContent) return; |
|
|
| aiResults.style.display = 'block'; |
| aiResultsContent.innerHTML = '<div class="loader"></div> در حال تشخیص الگو...'; |
|
|
| setTimeout(() => { |
| aiResultsContent.innerHTML = ` |
| <div class="ai-result-card"> |
| <h4>تشخیص الگو</h4> |
| <p>قابلیت تشخیص الگو به زودی اضافه خواهد شد.</p> |
| <p style="color: var(--text-muted); font-size: 0.875rem;"> |
| این قابلیت الگوهای کندل استیک و تحلیل تکنیکال را شناسایی میکند. |
| </p> |
| </div> |
| `; |
| }, 1000); |
| } |
|
|
| destroy() { |
| this.stopPeriodicUpdates(); |
| this.ws.disconnect(); |
| console.log('[App] Dashboard destroyed'); |
| } |
| } |
|
|
| |
| |
| |
|
|
| let app; |
|
|
| document.addEventListener('DOMContentLoaded', () => { |
| console.log('[Main] DOM loaded, initializing application...'); |
| |
| app = new DashboardApp(); |
| app.init(); |
|
|
| |
| window.app = app; |
| |
| console.log('[Main] Application ready'); |
| }); |
|
|
| |
| window.addEventListener('beforeunload', () => { |
| if (app) { |
| app.destroy(); |
| } |
| }); |
|
|
| |
| document.addEventListener('visibilitychange', () => { |
| if (document.hidden) { |
| console.log('[Main] Page hidden, pausing updates'); |
| app.stopPeriodicUpdates(); |
| } else { |
| console.log('[Main] Page visible, resuming updates'); |
| app.startPeriodicUpdates(); |
| app.loadMarketData(); |
| } |
| }); |
|
|
| |
| export { DashboardApp, APIClient, WebSocketClient, Utils }; |
|
|