Really-amin's picture
Upload 325 files
b66240d verified
/**
* ═══════════════════════════════════════════════════════════════════
* HTS CRYPTO DASHBOARD - UNIFIED APPLICATION
* Complete JavaScript Logic with WebSocket & API Integration
* ═══════════════════════════════════════════════════════════════════
*/
// ═══════════════════════════════════════════════════════════════════
// CONFIGURATION
// ═══════════════════════════════════════════════════════════════════
// Auto-detect environment and set backend URLs
// Use relative URLs to avoid CORS issues - always use same origin
const getBackendURL = () => {
// Always use current origin to avoid CORS issues
return window.location.origin;
};
const getWebSocketURL = () => {
// Use current origin for WebSocket to avoid CORS issues
const protocol = window.location.protocol === "https:" ? "wss" : "ws";
const host = window.location.host;
return `${protocol}://${host}/ws`;
};
// Merge DASHBOARD_CONFIG if exists, but always use localhost detection for URLs
const baseConfig = window.DASHBOARD_CONFIG || {};
const backendURL = getBackendURL();
const wsURL = getWebSocketURL();
const CONFIG = {
...baseConfig,
// Always override URLs with localhost detection
BACKEND_URL: backendURL,
WS_URL: wsURL,
UPDATE_INTERVAL: baseConfig.UPDATE_INTERVAL || 30000, // 30 seconds
CACHE_TTL: baseConfig.CACHE_TTL || 60000, // 1 minute
};
// Always use current origin to avoid CORS issues
CONFIG.BACKEND_URL = window.location.origin;
const wsProtocol = window.location.protocol === "https:" ? "wss" : "ws";
CONFIG.WS_URL = `${wsProtocol}://${window.location.host}/ws`;
// Log configuration for debugging
console.log('[Config] Backend URL:', CONFIG.BACKEND_URL);
console.log('[Config] WebSocket URL:', CONFIG.WS_URL);
console.log('[Config] Current hostname:', window.location.hostname);
// ═══════════════════════════════════════════════════════════════════
// WEBSOCKET CLIENT
// ═══════════════════════════════════════════════════════════════════
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) {
// WebSocket error events don't provide detailed error info
// Check socket state to provide better error context
const socketState = this.socket ? this.socket.readyState : 'null';
const stateNames = {
0: 'CONNECTING',
1: 'OPEN',
2: 'CLOSING',
3: 'CLOSED'
};
const stateName = stateNames[socketState] || `UNKNOWN(${socketState})`;
// Only log error once to prevent spam
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;
// Reset error flag after a delay to allow logging if error persists
setTimeout(() => {
this._errorLogged = false;
}, 5000);
}
this.updateStatus('error');
// Attempt reconnection if not already scheduled
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();
// Clean up socket reference
if (this.socket) {
try {
// Remove event listeners to prevent memory leaks
this.socket.onopen = null;
this.socket.onclose = null;
this.socket.onerror = null;
this.socket.onmessage = null;
} catch (e) {
// Ignore errors during cleanup
}
// Don't nullify socket immediately - let it close naturally
// this.socket = null; // Set to null after a short delay
}
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() {
// Clear any existing heartbeat
if (this.heartbeatInterval) {
clearInterval(this.heartbeatInterval);
}
this.heartbeatInterval = setInterval(() => {
// Double-check connection state before sending heartbeat
if (this.socket && this.socket.readyState === WebSocket.OPEN) {
const sent = this.send({ type: 'ping' });
if (!sent) {
// If send failed, stop heartbeat and try to reconnect
this.stopHeartbeat();
if (this.reconnectAttempts < this.maxReconnectAttempts) {
this.scheduleReconnect();
}
}
} else {
// Connection is not open, stop heartbeat
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;
}
// Check if socket is in a valid state for sending
if (this.socket.readyState === WebSocket.OPEN) {
try {
this.socket.send(JSON.stringify(data));
return true;
} catch (error) {
console.error('[WS] Error sending message:', error);
// Mark as disconnected if send fails
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 {
// Check if socket is still open before closing
if (this.socket.readyState === WebSocket.OPEN || this.socket.readyState === WebSocket.CONNECTING) {
this.socket.close();
}
} catch (error) {
console.warn('[WS] Error during disconnect:', error);
} finally {
// Clean up after a brief delay to allow close to complete
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) {
// Ignore errors during cleanup
}
}, 100);
}
}
this.status = 'disconnected';
this.updateStatus('disconnected');
}
}
// ═══════════════════════════════════════════════════════════════════
// API CLIENT
// ═══════════════════════════════════════════════════════════════════
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}`;
// Check cache
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();
// Cache successful GET requests
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;
}
}
// Market Data
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 });
}
// News
async getNews(limit = 20) {
return this.request(`/api/news/latest?limit=${limit}`, { cache: true });
}
// Providers
async getProviders() {
return this.request('/api/providers', { cache: true });
}
// Chart Data
async getChartData(symbol, interval = '1h', limit = 100) {
return this.request(`/api/ohlcv?symbol=${symbol}&interval=${interval}&limit=${limit}`, { cache: true });
}
}
// ═══════════════════════════════════════════════════════════════════
// UTILITY FUNCTIONS
// ═══════════════════════════════════════════════════════════════════
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);
};
},
};
// ═══════════════════════════════════════════════════════════════════
// VIEW MANAGER
// ═══════════════════════════════════════════════════════════════════
class ViewManager {
constructor() {
this.currentView = 'overview';
this.views = new Map();
this.init();
}
init() {
// Desktop navigation
document.querySelectorAll('.nav-tab-btn').forEach(btn => {
btn.addEventListener('click', (e) => {
const view = btn.dataset.view;
this.switchView(view);
});
});
// Mobile navigation
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;
// Hide all views
document.querySelectorAll('.view-section').forEach(section => {
section.classList.remove('active');
});
// Show selected view
const viewSection = document.getElementById(`view-${viewName}`);
if (viewSection) {
viewSection.classList.add('active');
}
// Update navigation buttons
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);
// Trigger view-specific updates
this.triggerViewUpdate(viewName);
}
triggerViewUpdate(viewName) {
const event = new CustomEvent('viewChange', { detail: { view: viewName } });
document.dispatchEvent(event);
}
}
// ═══════════════════════════════════════════════════════════════════
// DASHBOARD APPLICATION
// ═══════════════════════════════════════════════════════════════════
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...');
// Connect WebSocket
this.ws.connect();
this.setupWebSocketHandlers();
// Setup UI handlers
this.setupUIHandlers();
// Load initial data
await this.loadInitialData();
// Start periodic updates
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() {
// Theme toggle
const themeToggle = document.getElementById('theme-toggle');
if (themeToggle) {
themeToggle.addEventListener('click', () => this.toggleTheme());
}
// Notifications
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');
});
}
// Refresh buttons
const refreshCoins = document.getElementById('refresh-coins');
if (refreshCoins) {
refreshCoins.addEventListener('click', () => this.loadMarketData());
}
// Floating stats minimize
const minimizeStats = document.getElementById('minimize-stats');
const floatingStats = document.getElementById('floating-stats');
if (minimizeStats && floatingStats) {
minimizeStats.addEventListener('click', () => {
floatingStats.classList.toggle('minimized');
});
}
// Global search
const globalSearch = document.getElementById('global-search');
if (globalSearch) {
globalSearch.addEventListener('input', Utils.debounce((e) => {
this.handleSearch(e.target.value);
}, 300));
}
// AI Tools
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();
// Transform backend format (value, classification) to frontend format (bullish, neutral, bearish)
const transformed = this.transformSentimentData(data);
this.data.sentiment = transformed;
this.renderSentiment(transformed);
} catch (error) {
console.error('[App] Error loading sentiment data:', error);
}
}
transformSentimentData(data) {
// Backend returns: { value: 0-100, classification: "extreme_fear"|"fear"|"neutral"|"greed"|"extreme_greed", ... }
// Frontend expects: { bullish: %, neutral: %, bearish: % }
if (!data) {
return { bullish: 0, neutral: 100, bearish: 0 };
}
const value = data.value || 50;
const classification = data.classification || 'neutral';
// Convert value (0-100) to bullish/neutral/bearish distribution
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 - distribute around center
neutral = 40 + Math.abs(50 - value) * 0.4;
const remaining = 100 - neutral;
bullish = remaining * (value / 100);
bearish = remaining - bullish;
}
// Ensure they sum to 100
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 // Keep original data for reference
};
}
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}%`;
// Update progress bars
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);
// Implement search functionality
}
viewCoinDetails(symbol) {
console.log('[App] View coin details:', symbol);
// Switch to charts view and load coin data
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');
}
}
}
// AI Tool Methods
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');
}
}
// ═══════════════════════════════════════════════════════════════════
// INITIALIZATION
// ═══════════════════════════════════════════════════════════════════
let app;
document.addEventListener('DOMContentLoaded', () => {
console.log('[Main] DOM loaded, initializing application...');
app = new DashboardApp();
app.init();
// Make app globally accessible for debugging
window.app = app;
console.log('[Main] Application ready');
});
// Cleanup on page unload
window.addEventListener('beforeunload', () => {
if (app) {
app.destroy();
}
});
// Handle visibility change to pause/resume updates
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 for module usage
export { DashboardApp, APIClient, WebSocketClient, Utils };