Spaces:
Running
Running
| // CryptoVista Dashboard Pro - Enhanced JavaScript | |
| // API Configuration with fallbacks | |
| const COINGECKO_API = 'https://api.coingecko.com/api/v3'; | |
| const CRYPTO_NEWS_API = 'https://min-api.cryptocompare.com/data/v2/news/'; | |
| const ALTERNATIVE_API = 'https://api.coincap.io/v2/assets?limit=5'; | |
| // Global State with enhanced monitoring | |
| let cryptoData = { | |
| marketData: null, | |
| topGainers: [], | |
| news: [], | |
| userPreferences: { | |
| currency: 'usd', | |
| theme: 'dark', | |
| refreshInterval: 30000, | |
| notifications: true | |
| }, | |
| performance: { | |
| lastUpdate: null, | |
| apiLatency: {}, | |
| errors: [] | |
| } | |
| }; | |
| let selectedCurrency = localStorage.getItem('preferredCurrency') || 'usd'; | |
| let theme = localStorage.getItem('theme') || 'dark'; | |
| let isInitialized = false; | |
| // Performance monitoring | |
| const perfMonitor = { | |
| start: (label) => { | |
| if (window.performance) { | |
| perfMonitor[label] = performance.now(); | |
| } | |
| }, | |
| end: (label) => { | |
| if (window.performance && perfMonitor[label]) { | |
| const duration = performance.now() - perfMonitor[label]; | |
| console.log(`β±οΈ ${label}: ${duration.toFixed(2)}ms`); | |
| return duration; | |
| } | |
| return 0; | |
| } | |
| }; | |
| // Initialize Dashboard with enhanced UX | |
| async function initializeDashboard() { | |
| if (isInitialized) { | |
| console.warn('Dashboard already initialized'); | |
| return; | |
| } | |
| perfMonitor.start('init'); | |
| try { | |
| console.log('π Initializing CryptoVista Dashboard Pro...'); | |
| // Set initial theme | |
| applyTheme(theme); | |
| // Show loading state | |
| showLoadingState(); | |
| // Load critical data first | |
| await Promise.allSettled([ | |
| loadTopGainers(), | |
| loadNews(), | |
| loadMarketData() | |
| ]); | |
| // Setup enhanced auto-refresh | |
| setupAutoRefresh(); | |
| // Setup enhanced event listeners | |
| setupEventListeners(); | |
| // Initialize UI components | |
| initializeUIComponents(); | |
| // Mark as initialized | |
| isInitialized = true; | |
| // Hide loading state | |
| hideLoadingState(); | |
| perfMonitor.end('init'); | |
| // Show welcome message | |
| setTimeout(() => { | |
| showToast('Dashboard loaded successfully!', 'success'); | |
| }, 500); | |
| console.log('β Dashboard initialized successfully'); | |
| } catch (error) { | |
| console.error('β Dashboard initialization failed:', error); | |
| showError('global', 'Failed to initialize dashboard. Please refresh.'); | |
| hideLoadingState(); | |
| } | |
| } | |
| // Load Top Gainers from CoinGecko API | |
| async function loadTopGainers() { | |
| try { | |
| const response = await fetch(`${COINGECKO_API}/coins/markets?vs_currency=${selectedCurrency}&order=market_cap_desc&per_page=5&sparkline=false&price_change_percentage=24h`); | |
| const gainers = await response.json(); | |
| const container = document.getElementById('topGainers'); | |
| container.innerHTML = ''; | |
| gainers.forEach((coin, index) => { | |
| const isPositive = coin.price_change_percentage_24h > 0; | |
| const changeClass = isPositive ? 'positive' : 'negative'; | |
| const coinElement = ` | |
| <div class="flex items-center justify-between p-4 bg-dark-900 rounded-xl hover:bg-dark-700 transition-colors cursor-pointer" | |
| onclick="window.location.href='/coin/${coin.id}'"> | |
| <div class="flex items-center gap-3"> | |
| <div class="w-10 h-10 rounded-full bg-gradient-to-r from-primary-600 to-secondary-600 flex items-center justify-center"> | |
| <span class="font-bold">${coin.symbol.toUpperCase()}</span> | |
| </div> | |
| <div> | |
| <p class="font-medium">${coin.name}</p> | |
| <p class="text-sm text-gray-400">$${coin.current_price.toLocaleString()}</p> | |
| </div> | |
| </div> | |
| <div class="text-right"> | |
| <span class="price-change ${changeClass}"> | |
| ${isPositive ? '+' : ''}${coin.price_change_percentage_24h?.toFixed(2) || 0}% | |
| </span> | |
| </div> | |
| </div> | |
| `; | |
| container.innerHTML += coinElement; | |
| }); | |
| } catch (error) { | |
| console.error('Error loading top gainers:', error); | |
| showError('topGainers', 'Failed to load top gainers data'); | |
| } | |
| } | |
| // Load Crypto News from CryptoCompare API | |
| async function loadNews() { | |
| try { | |
| // Note: In production, you'd use your actual API key | |
| const response = await fetch(CRYPTO_NEWS_API); | |
| const data = await response.json(); | |
| const news = data.Data?.slice(0, 3) || []; | |
| const container = document.getElementById('newsFeed'); | |
| container.innerHTML = ''; | |
| news.forEach((article, index) => { | |
| const imageUrl = article.imageurl || `http://static.photos/technology/640x360/${index + 1}`; | |
| const newsElement = ` | |
| <a href="${article.url}" target="_blank" rel="noopener noreferrer" | |
| class="block bg-dark-900 rounded-xl overflow-hidden hover-lift group"> | |
| <div class="relative overflow-hidden"> | |
| <img src="${imageUrl}" | |
| alt="${article.title}" | |
| class="w-full h-48 object-cover group-hover:scale-105 transition-transform duration-300"> | |
| <div class="absolute inset-0 bg-gradient-to-t from-dark-900/80 to-transparent"></div> | |
| </div> | |
| <div class="p-4"> | |
| <h4 class="font-semibold mb-2 line-clamp-2 group-hover:text-primary-400 transition-colors"> | |
| ${article.title} | |
| </h4> | |
| <div class="flex items-center justify-between text-sm text-gray-400"> | |
| <span>${article.source}</span> | |
| <span>${formatTimeAgo(article.published_on)}</span> | |
| </div> | |
| </div> | |
| </a> | |
| `; | |
| container.innerHTML += newsElement; | |
| }); | |
| } catch (error) { | |
| console.error('Error loading news:', error); | |
| showError('newsFeed', 'Failed to load news feed'); | |
| // Fallback to static news | |
| const container = document.getElementById('newsFeed'); | |
| container.innerHTML = ` | |
| <div class="col-span-3 text-center py-12"> | |
| <i data-feather="wifi-off" class="w-12 h-12 text-gray-600 mx-auto mb-4"></i> | |
| <p class="text-gray-400">News feed temporarily unavailable</p> | |
| <p class="text-sm text-gray-500">Check back soon for updates</p> | |
| </div> | |
| `; | |
| feather.replace(); | |
| } | |
| } | |
| // Load Market Overview Data | |
| async function loadMarketData() { | |
| try { | |
| const response = await fetch(`${COINGECKO_API}/global`); | |
| const data = await response.json(); | |
| cryptoData.marketData = data.data; | |
| // Update market stats | |
| updateMarketStats(data.data); | |
| } catch (error) { | |
| console.error('Error loading market data:', error); | |
| } | |
| } | |
| // Update Market Stats Display | |
| function updateMarketStats(marketData) { | |
| const elements = { | |
| totalMarketCap: document.querySelector('[data-market-cap]'), | |
| totalVolume: document.querySelector('[data-volume]'), | |
| btcDominance: document.querySelector('[data-btc-dominance]'), | |
| activeCryptos: document.querySelector('[data-active-cryptos]') | |
| }; | |
| if (marketData && marketData.total_market_cap) { | |
| Object.keys(elements).forEach(key => { | |
| if (elements[key] && marketData[key]) { | |
| const value = marketData[key]; | |
| let formattedValue; | |
| switch(key) { | |
| case 'total_market_cap': | |
| case 'total_volume': | |
| formattedValue = `$${(value.usd / 1e12).toFixed(2)}T`; | |
| break; | |
| case 'btc_dominance': | |
| formattedValue = `${value.toFixed(2)}%`; | |
| break; | |
| case 'active_cryptocurrencies': | |
| formattedValue = value.toLocaleString(); | |
| break; | |
| default: | |
| formattedValue = value; | |
| } | |
| elements[key].textContent = formattedValue; | |
| } | |
| }); | |
| } | |
| } | |
| // Setup Auto Refresh | |
| function setupAutoRefresh() { | |
| // Refresh specific components at different intervals | |
| setInterval(() => { | |
| loadTopGainers(); | |
| console.log('π Refreshed top gainers'); | |
| }, 30000); // 30 seconds | |
| setInterval(() => { | |
| loadMarketData(); | |
| console.log('π Refreshed market data'); | |
| }, 60000); // 1 minute | |
| setInterval(() => { | |
| loadNews(); | |
| console.log('π° Refreshed news feed'); | |
| }, 120000); // 2 minutes | |
| } | |
| // Setup Event Listeners | |
| function setupEventListeners() { | |
| // Currency selector | |
| const currencyButtons = document.querySelectorAll('[data-currency]'); | |
| currencyButtons.forEach(button => { | |
| button.addEventListener('click', () => { | |
| selectedCurrency = button.dataset.currency; | |
| currencyButtons.forEach(btn => btn.classList.remove('active')); | |
| button.classList.add('active'); | |
| loadTopGainers(); | |
| }); | |
| }); | |
| // Theme toggle | |
| const themeToggle = document.getElementById('themeToggle'); | |
| if (themeToggle) { | |
| themeToggle.addEventListener('click', toggleTheme); | |
| } | |
| // Search functionality | |
| const searchInput = document.getElementById('cryptoSearch'); | |
| if (searchInput) { | |
| searchInput.addEventListener('input', debounce(handleSearch, 300)); | |
| } | |
| // Time range buttons | |
| const timeRangeButtons = document.querySelectorAll('[data-time-range]'); | |
| timeRangeButtons.forEach(button => { | |
| button.addEventListener('click', () => { | |
| timeRangeButtons.forEach(btn => btn.classList.remove('active')); | |
| button.classList.add('active'); | |
| // In production, update chart time range here | |
| }); | |
| }); | |
| // Mobile menu toggle | |
| const mobileMenuButton = document.getElementById('mobileMenuButton'); | |
| const mobileMenu = document.getElementById('mobileMenu'); | |
| if (mobileMenuButton && mobileMenu) { | |
| mobileMenuButton.addEventListener('click', () => { | |
| mobileMenu.classList.toggle('hidden'); | |
| }); | |
| } | |
| } | |
| // Handle Search Functionality | |
| async function handleSearch(event) { | |
| const query = event.target.value.toLowerCase().trim(); | |
| if (query.length < 2) { | |
| hideSearchResults(); | |
| return; | |
| } | |
| try { | |
| const response = await fetch(`${COINGECKO_API}/search?query=${query}`); | |
| const data = await response.json(); | |
| displaySearchResults(data.coins || []); | |
| } catch (error) { | |
| console.error('Search error:', error); | |
| } | |
| } | |
| // Display Search Results | |
| function displaySearchResults(coins) { | |
| const container = document.getElementById('searchResults'); | |
| if (!container) return; | |
| container.innerHTML = coins.slice(0, 5).map(coin => ` | |
| <a href="/coin/${coin.id}" class="flex items-center gap-3 p-3 hover:bg-dark-700 rounded-lg transition-colors"> | |
| <img src="${coin.thumb}" alt="${coin.name}" class="w-8 h-8 rounded-full"> | |
| <div> | |
| <p class="font-medium">${coin.name}</p> | |
| <p class="text-sm text-gray-400">${coin.symbol.toUpperCase()}</p> | |
| </div> | |
| </a> | |
| `).join(''); | |
| container.classList.remove('hidden'); | |
| } | |
| // Hide Search Results | |
| function hideSearchResults() { | |
| const container = document.getElementById('searchResults'); | |
| if (container) { | |
| container.classList.add('hidden'); | |
| } | |
| } | |
| // Toggle Theme | |
| function toggleTheme() { | |
| const html = document.documentElement; | |
| if (theme === 'dark') { | |
| theme = 'light'; | |
| html.classList.remove('dark'); | |
| html.classList.add('light'); | |
| } else { | |
| theme = 'dark'; | |
| html.classList.remove('light'); | |
| html.classList.add('dark'); | |
| } | |
| // Save preference | |
| localStorage.setItem('theme', theme); | |
| // Update UI | |
| updateThemeUI(); | |
| } | |
| // Update Theme UI | |
| function updateThemeUI() { | |
| const themeToggle = document.getElementById('themeToggle'); | |
| if (!themeToggle) return; | |
| const icon = themeToggle.querySelector('i[data-feather]'); | |
| if (icon) { | |
| const newIcon = theme === 'dark' ? 'sun' : 'moon'; | |
| icon.setAttribute('data-feather', newIcon); | |
| feather.replace(); | |
| } | |
| } | |
| // Show Error Message | |
| function showError(elementId, message) { | |
| const element = document.getElementById(elementId); | |
| if (element) { | |
| element.innerHTML = ` | |
| <div class="text-center py-8"> | |
| <i data-feather="alert-circle" class="w-12 h-12 text-red-500 mx-auto mb-4"></i> | |
| <p class="text-gray-400">${message}</p> | |
| </div> | |
| `; | |
| feather.replace(); | |
| } | |
| } | |
| // Format Time Ago | |
| function formatTimeAgo(timestamp) { | |
| const seconds = Math.floor((Date.now() - timestamp * 1000) / 1000); | |
| const intervals = [ | |
| { label: 'year', seconds: 31536000 }, | |
| { label: 'month', seconds: 2592000 }, | |
| { label: 'day', seconds: 86400 }, | |
| { label: 'hour', seconds: 3600 }, | |
| { label: 'minute', seconds: 60 }, | |
| { label: 'second', seconds: 1 } | |
| ]; | |
| for (const interval of intervals) { | |
| const count = Math.floor(seconds / interval.seconds); | |
| if (count >= 1) { | |
| return `${count} ${interval.label}${count !== 1 ? 's' : ''} ago`; | |
| } | |
| } | |
| return 'just now'; | |
| } | |
| // Debounce Function | |
| function debounce(func, wait) { | |
| let timeout; | |
| return function executedFunction(...args) { | |
| const later = () => { | |
| clearTimeout(timeout); | |
| func(...args); | |
| }; | |
| clearTimeout(timeout); | |
| timeout = setTimeout(later, wait); | |
| }; | |
| } | |
| // Load saved preferences | |
| function loadPreferences() { | |
| const savedTheme = localStorage.getItem('theme'); | |
| const savedCurrency = localStorage.getItem('currency'); | |
| if (savedTheme) { | |
| theme = savedTheme; | |
| document.documentElement.classList.add(theme); | |
| } | |
| if (savedCurrency) { | |
| selectedCurrency = savedCurrency; | |
| } | |
| } | |
| // Format Currency | |
| function formatCurrency(amount, currency = 'usd') { | |
| return new Intl.NumberFormat('en-US', { | |
| style: 'currency', | |
| currency: currency.toUpperCase(), | |
| minimumFractionDigits: 2, | |
| maximumFractionDigits: 6 | |
| }).format(amount); | |
| } | |
| // Copy to Clipboard | |
| function copyToClipboard(text) { | |
| navigator.clipboard.writeText(text).then(() => { | |
| showToast('Copied to clipboard!', 'success'); | |
| }).catch(err => { | |
| console.error('Copy failed:', err); | |
| showToast('Copy failed', 'error'); | |
| }); | |
| } | |
| // Show Toast Notification | |
| function showToast(message, type = 'info') { | |
| const toast = document.createElement('div'); | |
| toast.className = `fixed bottom-4 right-4 px-6 py-3 rounded-lg shadow-lg z-50 transition-all duration-300 transform translate-y-0 opacity-100 ${ | |
| type === 'success' ? 'bg-green-900 text-green-100' : | |
| type === 'error' ? 'bg-red-900 text-red-100' : | |
| 'bg-dark-800 text-gray-100' | |
| }`; | |
| toast.textContent = message; | |
| document.body.appendChild(toast); | |
| setTimeout(() => { | |
| toast.style.transform = 'translateY(100%)'; | |
| toast.style.opacity = '0'; | |
| setTimeout(() => document.body.removeChild(toast), 300); | |
| }, 3000); | |
| } | |
| // Initialize when DOM is loaded | |
| document.addEventListener('DOMContentLoaded', () => { | |
| loadPreferences(); | |
| initializeDashboard(); | |
| // Handle clicks outside search to close results | |
| document.addEventListener('click', (event) => { | |
| if (!event.target.closest('#searchContainer')) { | |
| hideSearchResults(); | |
| } | |
| }); | |
| }); | |
| // Export functions for use in templates | |
| window.cryptoUtils = { | |
| formatCurrency, | |
| formatTimeAgo, | |
| copyToClipboard, | |
| showToast, | |
| loadTopGainers, | |
| loadNews | |
| }; | |
| // Simplex noise for chart background (placeholder) | |
| class CryptoChartSimulator { | |
| constructor(canvasId) { | |
| this.canvas = document.getElementById(canvasId); | |
| if (!this.canvas) return; | |
| this.ctx = this.canvas.getContext('2d'); | |
| this.width = this.canvas.width; | |
| this.height = this.canvas.height; | |
| this.setupChart(); | |
| this.animate(); | |
| } | |
| setupChart() { | |
| // Chart setup logic would go here | |
| this.ctx.lineWidth = 2; | |
| this.ctx.strokeStyle = '#0ea5e9'; | |
| } | |
| animate() { | |
| // Animation logic for demo chart | |
| requestAnimationFrame(() => this.animate()); | |
| } | |
| } |