diff --git a/basicswap/static/js/offerstable.js b/basicswap/static/js/offerstable.js index 2652780..2a595d6 100644 --- a/basicswap/static/js/offerstable.js +++ b/basicswap/static/js/offerstable.js @@ -449,33 +449,49 @@ const CacheManager = { try { this.cleanup(); + if (!value) { + console.warn('Attempted to cache null/undefined value for key:', key); + return false; + } + const item = { value: value, timestamp: Date.now(), expiresAt: Date.now() + (customTtl || CACHE_DURATION) }; - const itemSize = new Blob([JSON.stringify(item)]).size; - if (itemSize > this.maxSize) { - //console.error(`Cache item exceeds maximum size (${(itemSize/1024/1024).toFixed(2)}MB)`); + try { + JSON.stringify(item); + } catch (e) { + console.error('Failed to serialize cache item:', e); return false; } - localStorage.setItem(key, JSON.stringify(item)); - return true; + const itemSize = new Blob([JSON.stringify(item)]).size; + if (itemSize > this.maxSize) { + console.warn(`Cache item exceeds maximum size (${(itemSize/1024/1024).toFixed(2)}MB)`); + return false; + } + + try { + localStorage.setItem(key, JSON.stringify(item)); + return true; + } catch (storageError) { + if (storageError.name === 'QuotaExceededError') { + this.cleanup(true); + try { + localStorage.setItem(key, JSON.stringify(item)); + return true; + } catch (retryError) { + console.error('Storage quota exceeded even after cleanup:', retryError); + return false; + } + } + throw storageError; + } } catch (error) { - if (error.name === 'QuotaExceededError') { - this.cleanup(true); // Aggressive cleanup - try { - localStorage.setItem(key, JSON.stringify(item)); - return true; - } catch (error) { - console.error('Storage quota exceeded even after cleanup:', error.message); - return false; - } - } - //console.error('Cache set error:', error); + console.error('Cache set error:', error); return false; } }, @@ -483,11 +499,26 @@ const CacheManager = { get: function(key) { try { const itemStr = localStorage.getItem(key); - if (!itemStr) return null; + if (!itemStr) { + return null; + } + + let item; + try { + item = JSON.parse(itemStr); + } catch (parseError) { + console.error('Failed to parse cached item:', parseError); + localStorage.removeItem(key); + return null; + } + + if (!item || typeof item.expiresAt !== 'number' || !item.hasOwnProperty('value')) { + console.warn('Invalid cache item structure for key:', key); + localStorage.removeItem(key); + return null; + } - const item = JSON.parse(itemStr); const now = Date.now(); - if (now < item.expiresAt) { return { value: item.value, @@ -496,11 +527,17 @@ const CacheManager = { } localStorage.removeItem(key); + return null; + } catch (error) { - console.error("An error occured:", error.message); - localStorage.removeItem(key); + console.error("Cache retrieval error:", error); + try { + localStorage.removeItem(key); + } catch (removeError) { + console.error("Failed to remove invalid cache entry:", removeError); + } + return null; } - return null; }, cleanup: function(aggressive = false) { @@ -533,7 +570,7 @@ const CacheManager = { totalSize += size; itemCount++; } catch (error) { - console.error("An error occured:", error.message); + console.error("Error processing cache item:", error); localStorage.removeItem(key); } } @@ -543,11 +580,21 @@ const CacheManager = { while ((totalSize > this.maxSize || itemCount > this.maxItems) && items.length > 0) { const item = items.pop(); - localStorage.removeItem(item.key); - totalSize -= item.size; - itemCount--; + try { + localStorage.removeItem(item.key); + totalSize -= item.size; + itemCount--; + } catch (error) { + console.error("Error removing cache item:", error); + } } } + + return { + totalSize, + itemCount, + cleaned: items.length + }; }, clear: function() { @@ -559,7 +606,13 @@ const CacheManager = { } } - keys.forEach(key => localStorage.removeItem(key)); + keys.forEach(key => { + try { + localStorage.removeItem(key); + } catch (error) { + console.error("Error clearing cache item:", error); + } + }); }, getStats: function() { @@ -584,7 +637,7 @@ const CacheManager = { expiredCount++; } } catch (error) { - console.error("An error occured:", error.message); + console.error("Error getting cache stats:", error); } } @@ -597,6 +650,8 @@ const CacheManager = { } }; +window.CacheManager = CacheManager; + // Identity cache management const IdentityManager = { cache: new Map(), @@ -939,15 +994,44 @@ function filterAndSortData() { comparison = a.offer_id.localeCompare(b.offer_id); break; } - return currentSortDirection === 'desc' ? -comparison : comparison; }); } - //console.log(`[Debug] Filtered data length: ${filteredData.length}`); return filteredData; } +function getPriceWithFallback(coin, latestPrices) { + const getPriceKey = (coin) => { + const lowerCoin = coin.toLowerCase(); + if (lowerCoin === 'firo' || lowerCoin === 'zcoin') { + return 'zcoin'; + } + if (lowerCoin === 'bitcoin cash') { + return 'bitcoin-cash'; + } + if (lowerCoin === 'particl anon' || lowerCoin === 'particl blind') { + return 'particl'; + } + return coinNameToSymbol[coin] || lowerCoin; + }; + + const priceKey = getPriceKey(coin); + const livePrice = latestPrices[priceKey]?.usd; + if (livePrice !== undefined && livePrice !== null) { + return livePrice; + } + + if (window.tableRateModule) { + const fallback = window.tableRateModule.getFallbackValue(priceKey); + if (fallback !== null) { + return fallback; + } + } + + return null; +} + async function calculateProfitLoss(fromCoin, toCoin, fromAmount, toAmount, isOwnOffer) { return new Promise((resolve) => { if (!latestPrices) { @@ -972,26 +1056,33 @@ async function calculateProfitLoss(fromCoin, toCoin, fromAmount, toAmount, isOwn const fromSymbol = getPriceKey(fromCoin); const toSymbol = getPriceKey(toCoin); + let fromPriceUSD = latestPrices[fromSymbol]?.usd; + let toPriceUSD = latestPrices[toSymbol]?.usd; - const fromPriceUSD = latestPrices[fromSymbol]?.usd; - const toPriceUSD = latestPrices[toSymbol]?.usd; - - if (fromPriceUSD === null || toPriceUSD === null || - fromPriceUSD === undefined || toPriceUSD === undefined) { + if (!fromPriceUSD || !toPriceUSD) { + fromPriceUSD = tableRateModule.getFallbackValue(fromSymbol); + toPriceUSD = tableRateModule.getFallbackValue(toSymbol); + } + if (!fromPriceUSD || !toPriceUSD || isNaN(fromPriceUSD) || isNaN(toPriceUSD)) { resolve(null); return; } - const fromValueUSD = fromAmount * fromPriceUSD; const toValueUSD = toAmount * toPriceUSD; - + if (isNaN(fromValueUSD) || isNaN(toValueUSD) || fromValueUSD === 0 || toValueUSD === 0) { + resolve(null); + return; + } let percentDiff; if (isOwnOffer) { percentDiff = ((toValueUSD / fromValueUSD) - 1) * 100; } else { percentDiff = ((fromValueUSD / toValueUSD) - 1) * 100; } - + if (isNaN(percentDiff)) { + resolve(null); + return; + } resolve(percentDiff); }); } @@ -1015,94 +1106,75 @@ function getEmptyPriceData() { async function fetchLatestPrices() { const PRICES_CACHE_KEY = 'prices_coingecko'; - const RETRY_DELAY = 5000; - const MAX_RETRIES = 3; - const cachedData = CacheManager.get(PRICES_CACHE_KEY); - if (cachedData && cachedData.remainingTime > 30000) { - console.log('Using cached price data'); - latestPrices = cachedData.value; - return cachedData.value; + if (!window.isManualRefresh) { + const cachedData = CacheManager.get(PRICES_CACHE_KEY); + if (cachedData && cachedData.remainingTime > 60000) { + console.log('Using cached price data'); + latestPrices = cachedData.value; + Object.entries(cachedData.value).forEach(([coin, prices]) => { + if (prices.usd) { + tableRateModule.setFallbackValue(coin, prices.usd); + } + }); + return cachedData.value; + } } + const url = `${offersConfig.apiEndpoints.coinGecko}/simple/price?ids=bitcoin,bitcoin-cash,dash,dogecoin,decred,litecoin,particl,pivx,monero,zano,wownero,zcoin&vs_currencies=USD,BTC&api_key=${offersConfig.apiKeys.coinGecko}`; - const baseUrl = `${offersConfig.apiEndpoints.coinGecko}/simple/price?ids=bitcoin,bitcoin-cash,dash,dogecoin,decred,litecoin,particl,pivx,monero,zcoin,zano,wownero&vs_currencies=USD,BTC`; + try { + console.log('Initiating fresh price data fetch...'); + const response = await fetch('/json/readurl', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + url: url, + headers: {} + }) + }); - let retryCount = 0; - let data = null; - - while (!data && retryCount < MAX_RETRIES) { - if (retryCount > 0) { - const delay = RETRY_DELAY * Math.pow(2, retryCount - 1); - console.log(`Waiting ${delay}ms before retry ${retryCount + 1}...`); - await new Promise(resolve => setTimeout(resolve, delay)); + if (!response.ok) { + throw new Error(`HTTP Error: ${response.status} ${response.statusText}`); } - try { - console.log('Attempting price fetch with API key...'); - const urlWithKey = `${baseUrl}&api_key=${offersConfig.apiKeys.coinGecko}`; + const data = await response.json(); - const response = await fetch('/json/readurl', { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ - url: urlWithKey, - headers: {} - }) + if (data.Error) { + console.error('API Error:', data.Error); + throw new Error(data.Error); + } + + if (data && Object.keys(data).length > 0) { + console.log('Processing fresh price data...'); + latestPrices = data; + CacheManager.set(PRICES_CACHE_KEY, data, CACHE_DURATION); + + Object.entries(data).forEach(([coin, prices]) => { + if (prices.usd) { + tableRateModule.setFallbackValue(coin, prices.usd); + } }); - const responseData = await response.json(); - - if (responseData.error) { - if (responseData.error.includes('429')) { - console.log('Rate limited, retrying...'); - } else { - console.warn('Invalid price data received:', responseData); - } - retryCount++; - continue; - } - - const hasValidPrices = Object.values(responseData).some(coin => - coin && typeof coin === 'object' && - typeof coin.usd === 'number' && - !isNaN(coin.usd) - ); - - if (!hasValidPrices) { - console.warn('No valid price data found in response'); - retryCount++; - continue; - } - - data = responseData; - break; - - } catch (error) { - console.warn('Error fetching prices:', error); - retryCount++; + return data; + } else { + console.warn('No price data received'); + return null; } + } catch (error) { + console.error('Price Fetch Error:', error); + const fallbackPrices = {}; + Object.keys(getEmptyPriceData()).forEach(coin => { + const fallbackValue = tableRateModule.getFallbackValue(coin); + if (fallbackValue !== null) { + fallbackPrices[coin] = { usd: fallbackValue, btc: null }; + } + }); + return Object.keys(fallbackPrices).length > 0 ? fallbackPrices : null; + } finally { + window.isManualRefresh = false; } - - if (!data) { - console.warn('All price fetch attempts failed, using empty price data'); - const naData = getEmptyPriceData(); - latestPrices = naData; - return naData; - } - - console.log('Successfully fetched fresh price data'); - latestPrices = data; - CacheManager.set(PRICES_CACHE_KEY, data, CACHE_DURATION); - - Object.entries(data).forEach(([coin, prices]) => { - if (prices && typeof prices.usd === 'number' && !isNaN(prices.usd)) { - tableRateModule.setFallbackValue(coin, prices.usd); - } - }); - - return data; } async function fetchOffers() { @@ -1275,20 +1347,18 @@ function updatePaginationControls(totalPages) { function updateProfitLoss(row, fromCoin, toCoin, fromAmount, toAmount, isOwnOffer) { const profitLossElement = row.querySelector('.profit-loss'); if (!profitLossElement) { - //console.warn('Profit loss element not found in row'); return; } if (!fromCoin || !toCoin) { - //console.error(`Invalid coin names: fromCoin=${fromCoin}, toCoin=${toCoin}`); - profitLossElement.textContent = 'Error'; - profitLossElement.className = 'profit-loss text-lg font-bold text-red-500'; + profitLossElement.textContent = 'N/A'; + profitLossElement.className = 'profit-loss text-lg font-bold text-gray-300'; return; } calculateProfitLoss(fromCoin, toCoin, fromAmount, toAmount, isOwnOffer) .then(percentDiff => { - if (percentDiff === null) { + if (percentDiff === null || isNaN(percentDiff)) { profitLossElement.textContent = 'N/A'; profitLossElement.className = 'profit-loss text-lg font-bold text-gray-300'; return; @@ -1302,6 +1372,7 @@ function updateProfitLoss(row, fromCoin, toCoin, fromAmount, toAmount, isOwnOffe profitLossElement.textContent = `${percentDiffDisplay}%`; profitLossElement.className = `profit-loss text-lg font-bold ${colorClass}`; + // Update tooltip if it exists const tooltipId = `percentage-tooltip-${row.getAttribute('data-offer-id')}`; const tooltipElement = document.getElementById(tooltipId); if (tooltipElement) { @@ -1316,8 +1387,8 @@ function updateProfitLoss(row, fromCoin, toCoin, fromAmount, toAmount, isOwnOffe }) .catch(error => { console.error('Error in updateProfitLoss:', error); - profitLossElement.textContent = 'Error'; - profitLossElement.className = 'profit-loss text-lg font-bold text-red-500'; + profitLossElement.textContent = 'N/A'; + profitLossElement.className = 'profit-loss text-lg font-bold text-gray-300'; }); } @@ -1802,18 +1873,27 @@ function createRateColumn(offer, coinFrom, coinTo) { if (lowerCoin === 'bitcoin cash') { return 'bitcoin-cash'; } + if (lowerCoin === 'particl anon' || lowerCoin === 'particl blind') { + return 'particl'; + } return coinNameToSymbol[coin] || lowerCoin; }; - const toPriceUSD = latestPrices[getPriceKey(coinTo)]?.usd || 0; - const rateInUSD = rate * toPriceUSD; + const toSymbolKey = getPriceKey(coinTo); + let toPriceUSD = latestPrices[toSymbolKey]?.usd; + + if (!toPriceUSD || isNaN(toPriceUSD)) { + toPriceUSD = tableRateModule.getFallbackValue(toSymbolKey); + } + + const rateInUSD = toPriceUSD && !isNaN(toPriceUSD) && !isNaN(rate) ? rate * toPriceUSD : null; return ` <td class="py-3 semibold monospace text-xs text-right items-center rate-table-info"> <div class="relative"> <div class="flex flex-col items-end pr-3" data-tooltip-target="tooltip-rate-${offer.offer_id}"> <span class="text-sm bold text-gray-700 dark:text-white"> - $${rateInUSD.toFixed(2)} USD + ${rateInUSD !== null ? `$${rateInUSD.toFixed(2)} USD` : 'N/A'} </span> <span class="bold text-gray-700 dark:text-white"> ${rate.toFixed(8)} ${toSymbol}/${fromSymbol} @@ -2443,49 +2523,76 @@ function initializeTableEvents() { }); } - const refreshButton = document.getElementById('refreshOffers'); - if (refreshButton) { - EventManager.add(refreshButton, 'click', async () => { - console.log('Manual refresh initiated'); - const refreshIcon = document.getElementById('refreshIcon'); - const refreshText = document.getElementById('refreshText'); +const refreshButton = document.getElementById('refreshOffers'); +if (refreshButton) { + EventManager.add(refreshButton, 'click', async () => { + console.log('Manual refresh initiated'); + const refreshIcon = document.getElementById('refreshIcon'); + const refreshText = document.getElementById('refreshText'); - refreshButton.disabled = true; - refreshIcon.classList.add('animate-spin'); - refreshText.textContent = 'Refreshing...'; - refreshButton.classList.add('opacity-75', 'cursor-wait'); + refreshButton.disabled = true; + refreshIcon.classList.add('animate-spin'); + refreshText.textContent = 'Refreshing...'; + refreshButton.classList.add('opacity-75', 'cursor-wait'); - try { - const endpoint = isSentOffers ? '/json/sentoffers' : '/json/offers'; - const response = await fetch(endpoint); - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } - const newData = await response.json(); - - const processedNewData = Array.isArray(newData) ? newData : Object.values(newData); - console.log('Fetched offers:', processedNewData.length); - - jsonData = formatInitialData(processedNewData); - originalJsonData = [...jsonData]; - - await updateOffersTable(); - updateJsonView(); - updatePaginationInfo(); - - console.log('Manual refresh completed successfully'); - - } catch (error) { - console.error('Error during manual refresh:', error); - ui.displayErrorMessage('Failed to refresh offers. Please try again later.'); - } finally { - refreshButton.disabled = false; - refreshIcon.classList.remove('animate-spin'); - refreshText.textContent = 'Refresh'; - refreshButton.classList.remove('opacity-75', 'cursor-wait'); + try { + const PRICES_CACHE_KEY = 'prices_coingecko'; + localStorage.removeItem(PRICES_CACHE_KEY); + CacheManager.clear(); + window.isManualRefresh = true; + const endpoint = isSentOffers ? '/json/sentoffers' : '/json/offers'; + const response = await fetch(endpoint); + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); } - }); - } + const newData = await response.json(); + const processedNewData = Array.isArray(newData) ? newData : Object.values(newData); + console.log('Fetched offers:', processedNewData.length); + + jsonData = formatInitialData(processedNewData); + originalJsonData = [...jsonData]; + + const url = `${offersConfig.apiEndpoints.coinGecko}/simple/price?ids=bitcoin,bitcoin-cash,dash,dogecoin,decred,litecoin,particl,pivx,monero,zano,wownero,zcoin&vs_currencies=USD,BTC&api_key=${offersConfig.apiKeys.coinGecko}`; + console.log('Fetching fresh prices...'); + const priceResponse = await fetch('/json/readurl', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ url: url, headers: {} }) + }); + + if (priceResponse.ok) { + const priceData = await priceResponse.json(); + if (priceData && Object.keys(priceData).length > 0) { + console.log('Updating with fresh price data'); + latestPrices = priceData; + CacheManager.set(PRICES_CACHE_KEY, priceData, CACHE_DURATION); + Object.entries(priceData).forEach(([coin, prices]) => { + if (prices.usd) { + tableRateModule.setFallbackValue(coin, prices.usd); + } + }); + } + } + + await updateOffersTable(); + updateJsonView(); + updatePaginationInfo(); + lastRefreshTime = Date.now(); + updateLastRefreshTime(); + + console.log('Manual refresh completed successfully'); + } catch (error) { + console.error('Error during manual refresh:', error); + ui.displayErrorMessage('Failed to refresh offers. Please try again later.'); + } finally { + window.isManualRefresh = false; + refreshButton.disabled = false; + refreshIcon.classList.remove('animate-spin'); + refreshText.textContent = 'Refresh'; + refreshButton.classList.remove('opacity-75', 'cursor-wait'); + } + }); +} document.querySelectorAll('th[data-sortable="true"]').forEach(header => { EventManager.add(header, 'click', () => { @@ -2718,11 +2825,6 @@ async function cleanup() { cleanupTable(); debug.addStep('Table cleanup completed', `Cleaned up ${rowCount} rows`); - debug.addStep('Starting cache cleanup'); - const cacheStats = CacheManager.getStats(); - CacheManager.clear(); - debug.addStep('Cache cleanup completed', `Cleared ${cacheStats.itemCount} cached items`); - debug.addStep('Resetting global state'); const globals = { currentPage: currentPage, diff --git a/basicswap/static/js/pricechart.js b/basicswap/static/js/pricechart.js index a4c63f2..0cc9163 100644 --- a/basicswap/static/js/pricechart.js +++ b/basicswap/static/js/pricechart.js @@ -42,7 +42,6 @@ const config = { const utils = { formatNumber: (number, decimals = 2) => number.toFixed(decimals).replace(/\B(?=(\d{3})+(?!\d))/g, ','), - formatDate: (timestamp, resolution) => { const date = new Date(timestamp); const options = { @@ -80,7 +79,6 @@ const logger = { // API const api = { makePostRequest: (url, headers = {}) => { - // unused // const apiKeys = getAPIKeys(); return new Promise((resolve, reject) => { const xhr = new XMLHttpRequest(); xhr.open('POST', '/json/readurl'); @@ -141,17 +139,13 @@ const api = { .map(coin => coin.name) .join(','); const url = `${config.apiEndpoints.coinGecko}/simple/price?ids=${coinIds}&vs_currencies=usd,btc&include_24hr_vol=true&include_24hr_change=true&api_key=${config.apiKeys.coinGecko}`; - //console.log(`Fetching data for multiple coins from CoinGecko: ${url}`); - try { const data = await api.makePostRequest(url); //console.log(`Raw CoinGecko data:`, data); - if (typeof data !== 'object' || data === null) { throw new AppError(`Invalid data structure received from CoinGecko`); } - const transformedData = {}; Object.entries(data).forEach(([id, values]) => { const coinConfig = config.coins.find(coin => coin.name === id); @@ -164,7 +158,6 @@ const api = { displayName: coinConfig?.displayName || coinConfig?.symbol || id }; }); - //console.log(`Transformed CoinGecko data:`, transformedData); cache.set(cacheKey, transformedData); return transformedData; @@ -212,9 +205,7 @@ const api = { } else { url = `${config.apiEndpoints.cryptoCompareHistorical}?fsym=${coin}&tsym=USD&limit=${resolution.days}&api_key=${config.apiKeys.cryptoCompare}`; } - //console.log(`CryptoCompare URL for ${coin}: ${url}`); - try { const response = await api.makePostRequest(url); if (response.Response === "Error") { @@ -229,9 +220,7 @@ const api = { } } }); - await Promise.all(fetchPromises); - //console.log('Final results object:', JSON.stringify(results, null, 2)); return results; } @@ -295,16 +284,13 @@ displayCoinData: (coin, data) => { const volumeElement = document.querySelector(`#${coin.toLowerCase()}-volume-24h`); const btcPriceDiv = document.querySelector(`#${coin.toLowerCase()}-btc-price-div`); const priceBtcElement = document.querySelector(`#${coin.toLowerCase()}-price-btc`); - if (priceUsdElement) { priceUsdElement.textContent = isError ? 'N/A' : `$ ${ui.formatPrice(coin, priceUSD)}`; } - if (volumeDiv && volumeElement) { volumeElement.textContent = isError ? 'N/A' : `${utils.formatNumber(volume24h, 0)} USD`; volumeDiv.style.display = volumeToggle.isVisible ? 'flex' : 'none'; } - if (btcPriceDiv && priceBtcElement) { if (coin === 'BTC') { btcPriceDiv.style.display = 'none'; @@ -313,10 +299,8 @@ displayCoinData: (coin, data) => { btcPriceDiv.style.display = 'flex'; } } - ui.updatePriceChangeContainer(coin, isError ? null : priceChange1d); }; - try { if (data.error) { throw new Error(data.error); @@ -324,19 +308,15 @@ displayCoinData: (coin, data) => { if (!data || !data.current_price) { throw new Error(`Invalid CoinGecko data structure for ${coin}`); } - priceUSD = data.current_price; priceBTC = coin === 'BTC' ? 1 : data.price_btc || (data.current_price / app.btcPriceUSD); priceChange1d = data.price_change_percentage_24h; volume24h = data.total_volume; - if (isNaN(priceUSD) || isNaN(priceBTC) || isNaN(volume24h)) { throw new Error(`Invalid numeric values in data for ${coin}`); } - updateUI(false); } catch (error) { - console.error(`Error displaying data for ${coin}:`, error.message); updateUI(true); } }, @@ -381,11 +361,9 @@ displayCoinData: (coin, data) => { updateLoadTimeAndCache: (loadTime, cachedData) => { const loadTimeElement = document.getElementById('load-time'); const cacheStatusElement = document.getElementById('cache-status'); - if (loadTimeElement) { loadTimeElement.textContent = `Load time: ${loadTime}ms`; } - if (cacheStatusElement) { if (cachedData && cachedData.remainingTime) { const remainingMinutes = Math.ceil(cachedData.remainingTime / 60000); @@ -398,7 +376,6 @@ displayCoinData: (coin, data) => { cacheStatusElement.classList.remove('text-green-500'); } } - ui.updateLastRefreshedTime(); }, @@ -486,13 +463,6 @@ const chartModule = { chart: null, currentCoin: 'BTC', loadStartTime: 0, - - cleanup: () => { - if (chartModule.chart) { - chartModule.chart.destroy(); - chartModule.chart = null; - } - }, verticalLinePlugin: { id: 'verticalLine', beforeDraw: (chart, args, options) => { @@ -520,11 +490,9 @@ const chartModule = { logger.error('Failed to get chart context. Make sure the canvas element exists.'); return; } - const gradient = ctx.createLinearGradient(0, 0, 0, 400); gradient.addColorStop(0, 'rgba(77, 132, 240, 0.2)'); gradient.addColorStop(1, 'rgba(77, 132, 240, 0)'); - chartModule.chart = new Chart(ctx, { type: 'line', data: { @@ -674,12 +642,10 @@ const chartModule = { plugins: [chartModule.verticalLinePlugin] }); }, - prepareChartData: (coinSymbol, data) => { if (!data) { return []; } - try { let preparedData; @@ -688,13 +654,10 @@ const chartModule = { endTime.setUTCMinutes(0, 0, 0); const endUnix = endTime.getTime(); const startUnix = endUnix - (24 * 3600000); - const hourlyPoints = []; - for (let hourUnix = startUnix; hourUnix <= endUnix; hourUnix += 3600000) { const targetHour = new Date(hourUnix); targetHour.setUTCMinutes(0, 0, 0); - const closestPoint = data.reduce((prev, curr) => { const prevTime = new Date(prev[0]); const currTime = new Date(curr[0]); @@ -737,13 +700,12 @@ const chartModule = { } else { return []; } - return preparedData.map(point => ({ x: new Date(point.x).getTime(), y: point.y })); } catch (error) { - console.error("An error occured:", error.message); + console.error(`Error preparing chart data for ${coinSymbol}:`, error); return []; } }, @@ -760,13 +722,11 @@ const chartModule = { Math.abs(new Date(curr.x).getTime() - targetTime.getTime()) < Math.abs(new Date(prev.x).getTime() - targetTime.getTime()) ? curr : prev ); - hourlyData.push({ x: targetTime.getTime(), y: closestDataPoint.y }); } - return hourlyData; }, @@ -774,24 +734,26 @@ const chartModule = { try { chartModule.showChartLoader(); chartModule.loadStartTime = Date.now(); - const cacheKey = `chartData_${coinSymbol}_${config.currentResolution}`; let cachedData = !forceRefresh ? cache.get(cacheKey) : null; let data; - if (cachedData && Object.keys(cachedData.value).length > 0) { data = cachedData.value; + //console.log(`Using cached data for ${coinSymbol} (${config.currentResolution})`); } else { + //console.log(`Fetching fresh data for ${coinSymbol} (${config.currentResolution})`); const allData = await api.fetchHistoricalDataXHR([coinSymbol]); data = allData[coinSymbol]; if (!data || Object.keys(data).length === 0) { throw new Error(`No data returned for ${coinSymbol}`); } + //console.log(`Caching new data for ${cacheKey}`); cache.set(cacheKey, data, config.cacheTTL); cachedData = null; } const chartData = chartModule.prepareChartData(coinSymbol, data); + //console.log(`Prepared chart data for ${coinSymbol}:`, chartData.slice(0, 5)); if (chartData.length === 0) { throw new Error(`No valid chart data for ${coinSymbol}`); @@ -807,11 +769,9 @@ const chartModule = { } else { const resolution = config.resolutions[config.currentResolution]; chartModule.chart.options.scales.x.time.unit = resolution.interval === 'hourly' ? 'hour' : 'day'; - if (config.currentResolution === 'year' || config.currentResolution === 'sixMonths') { chartModule.chart.options.scales.x.time.unit = 'month'; } - if (config.currentResolution === 'year') { chartModule.chart.options.scales.x.ticks.maxTicksLimit = 12; } else if (config.currentResolution === 'sixMonths') { @@ -823,6 +783,7 @@ const chartModule = { chartModule.chart.update('active'); } else { + //console.error('Chart object not initialized'); throw new Error('Chart object not initialized'); } @@ -831,6 +792,7 @@ const chartModule = { ui.updateLoadTimeAndCache(loadTime, cachedData); } catch (error) { + //console.error(`Error updating chart for ${coinSymbol}:`, error); ui.displayErrorMessage(`Failed to update chart for ${coinSymbol}: ${error.message}`); } finally { chartModule.hideChartLoader(); @@ -840,11 +802,10 @@ const chartModule = { showChartLoader: () => { const loader = document.getElementById('chart-loader'); const chart = document.getElementById('coin-chart'); - if (!loader || !chart) { + //console.warn('Chart loader or chart container elements not found'); return; } - loader.classList.remove('hidden'); chart.classList.add('hidden'); }, @@ -852,63 +813,49 @@ const chartModule = { hideChartLoader: () => { const loader = document.getElementById('chart-loader'); const chart = document.getElementById('coin-chart'); - if (!loader || !chart) { + //console.warn('Chart loader or chart container elements not found'); return; } - loader.classList.add('hidden'); chart.classList.remove('hidden'); - } + }, }; Chart.register(chartModule.verticalLinePlugin); -const volumeToggle = { - isVisible: localStorage.getItem('volumeToggleState') === 'true', - - cleanup: () => { - const toggleButton = document.getElementById('toggle-volume'); - if (toggleButton) { - toggleButton.removeEventListener('click', volumeToggle.toggle); - } - }, - - init: () => { - volumeToggle.cleanup(); - - const toggleButton = document.getElementById('toggle-volume'); - if (toggleButton) { - toggleButton.addEventListener('click', volumeToggle.toggle); + const volumeToggle = { + isVisible: localStorage.getItem('volumeToggleState') === 'true', + init: () => { + const toggleButton = document.getElementById('toggle-volume'); + if (toggleButton) { + toggleButton.addEventListener('click', volumeToggle.toggle); + volumeToggle.updateVolumeDisplay(); + } + }, + toggle: () => { + volumeToggle.isVisible = !volumeToggle.isVisible; + localStorage.setItem('volumeToggleState', volumeToggle.isVisible.toString()); volumeToggle.updateVolumeDisplay(); + }, + updateVolumeDisplay: () => { + const volumeDivs = document.querySelectorAll('[id$="-volume-div"]'); + volumeDivs.forEach(div => { + div.style.display = volumeToggle.isVisible ? 'flex' : 'none'; + }); + const toggleButton = document.getElementById('toggle-volume'); + if (toggleButton) { + updateButtonStyles(toggleButton, volumeToggle.isVisible, 'green'); + } } - }, + }; - toggle: () => { - volumeToggle.isVisible = !volumeToggle.isVisible; - localStorage.setItem('volumeToggleState', volumeToggle.isVisible.toString()); - volumeToggle.updateVolumeDisplay(); - }, - - updateVolumeDisplay: () => { - const volumeDivs = document.querySelectorAll('[id$="-volume-div"]'); - volumeDivs.forEach(div => { - div.style.display = volumeToggle.isVisible ? 'flex' : 'none'; - }); - - const toggleButton = document.getElementById('toggle-volume'); - if (toggleButton) { - updateButtonStyles(toggleButton, volumeToggle.isVisible, 'green'); - } + function updateButtonStyles(button, isActive, color) { + button.classList.toggle('text-' + color + '-500', isActive); + button.classList.toggle('text-gray-600', !isActive); + button.classList.toggle('dark:text-' + color + '-400', isActive); + button.classList.toggle('dark:text-gray-400', !isActive); } -}; - -function updateButtonStyles(button, isActive, color) { - button.classList.toggle('text-' + color + '-500', isActive); - button.classList.toggle('text-gray-600', !isActive); - button.classList.toggle('dark:text-' + color + '-400', isActive); - button.classList.toggle('dark:text-gray-400', !isActive); -} const app = { btcPriceUSD: 0, @@ -924,177 +871,90 @@ const app = { }, cacheTTL: 5 * 60 * 1000, // 5 minutes minimumRefreshInterval: 60 * 1000, // 1 minute - eventListeners: new Map(), - visibilityCleanup: null, - - cleanup: () => { - if (app.autoRefreshInterval) { - clearTimeout(app.autoRefreshInterval); - app.autoRefreshInterval = null; - } - - if (app.updateNextRefreshTimeRAF) { - cancelAnimationFrame(app.updateNextRefreshTimeRAF); - app.updateNextRefreshTimeRAF = null; - } - - if (typeof app.visibilityCleanup === 'function') { - app.visibilityCleanup(); - app.visibilityCleanup = null; - } - - volumeToggle.cleanup(); - - app.removeEventListeners(); - - if (chartModule.chart) { - chartModule.chart.destroy(); - chartModule.chart = null; - } - - cache.clear(); - }, - - removeEventListeners: () => { - app.eventListeners.forEach((listener, element) => { - if (element && typeof element.removeEventListener === 'function') { - element.removeEventListener(listener.type, listener.fn); - } - }); - app.eventListeners.clear(); - }, - - addEventListenerWithCleanup: (element, type, fn) => { - if (element && typeof element.addEventListener === 'function') { - element.addEventListener(type, fn); - app.eventListeners.set(element, { type, fn }); - } - }, - - initResolutionButtons: () => { - const resolutionButtons = document.querySelectorAll('.resolution-button'); - resolutionButtons.forEach(button => { - // Remove existing listeners first - const oldListener = button.getAttribute('data-resolution-listener'); - if (oldListener && window[oldListener]) { - button.removeEventListener('click', window[oldListener]); - delete window[oldListener]; - } - - const listener = () => { - const resolution = button.id.split('-')[1]; - const currentCoin = chartModule.currentCoin; - - if (currentCoin !== 'WOW' || resolution === 'day') { - config.currentResolution = resolution; - chartModule.updateChart(currentCoin, true); - app.updateResolutionButtons(currentCoin); - } - }; - - const listenerName = `resolutionListener_${button.id}`; - window[listenerName] = listener; - button.setAttribute('data-resolution-listener', listenerName); - button.addEventListener('click', listener); - }); - }, - - setupVisibilityHandler: () => { - const cleanup = () => { - if (window.visibilityHandler) { - document.removeEventListener('visibilitychange', window.visibilityHandler); - delete window.visibilityHandler; - } - }; - - cleanup(); - - window.visibilityHandler = () => { - if (!document.hidden && chartModule.chart) { - chartModule.updateChart(chartModule.currentCoin, true); - } - }; - - document.addEventListener('visibilitychange', window.visibilityHandler); - return cleanup; -}, init: () => { console.log('Initializing app...'); - app.cleanup(); window.addEventListener('load', app.onLoad); app.loadLastRefreshedTime(); app.updateAutoRefreshButton(); - app.initResolutionButtons(); - app.setupVisibilityHandler(); console.log('App initialized'); }, onLoad: async () => { - console.log('App onLoad event triggered'); - ui.showLoader(); - try { - volumeToggle.init(); - await app.updateBTCPrice(); - const chartContainer = document.getElementById('coin-chart'); - if (chartContainer) { - chartModule.initChart(); - chartModule.showChartLoader(); - } - - console.log('Loading all coin data...'); - await app.loadAllCoinData(); - - if (chartModule.chart) { - config.currentResolution = 'day'; - await chartModule.updateChart('BTC'); - app.updateResolutionButtons('BTC'); - } - ui.setActiveContainer('btc-container'); - - app.setupEventListeners(); - app.initializeSelectImages(); - app.initAutoRefresh(); - - } catch (error) { - console.error("An error occured:", error.message); - ui.displayErrorMessage('Failed to initialize the dashboard. Please try refreshing the page.'); - } finally { - ui.hideLoader(); - if (chartModule.chart) { - chartModule.hideChartLoader(); - } - console.log('App onLoad completed'); + console.log('App onLoad event triggered'); + ui.showLoader(); + try { + volumeToggle.init(); + await app.updateBTCPrice(); + const chartContainer = document.getElementById('coin-chart'); + if (chartContainer) { + chartModule.initChart(); + chartModule.showChartLoader(); + } else { + //console.warn('Chart container not found, skipping chart initialization'); } - }, - loadAllCoinData: async () => { - try { - const allCoinData = await api.fetchCoinGeckoDataXHR(); - if (allCoinData.error) { - throw new Error(allCoinData.error); - } + console.log('Loading all coin data...'); + await app.loadAllCoinData(); - for (const coin of config.coins) { - const coinData = allCoinData[coin.symbol.toLowerCase()]; - if (coinData) { - coinData.displayName = coin.displayName || coin.symbol; - ui.displayCoinData(coin.symbol, coinData); - const cacheKey = `coinData_${coin.symbol}`; - cache.set(cacheKey, coinData); + if (chartModule.chart) { + config.currentResolution = 'day'; + await chartModule.updateChart('BTC'); + app.updateResolutionButtons('BTC'); + } + ui.setActiveContainer('btc-container'); + + //console.log('Setting up event listeners and initializations...'); + app.setupEventListeners(); + app.initializeSelectImages(); + app.initAutoRefresh(); + + } catch (error) { + //console.error('Error during initialization:', error); + ui.displayErrorMessage('Failed to initialize the dashboard. Please try refreshing the page.'); + } finally { + ui.hideLoader(); + if (chartModule.chart) { + chartModule.hideChartLoader(); + } + console.log('App onLoad completed'); + } +}, + + loadAllCoinData: async () => { + //console.log('Loading data for all coins...'); + try { + const allCoinData = await api.fetchCoinGeckoDataXHR(); + if (allCoinData.error) { + throw new Error(allCoinData.error); + } + + for (const coin of config.coins) { + const coinData = allCoinData[coin.symbol.toLowerCase()]; + if (coinData) { + coinData.displayName = coin.displayName || coin.symbol; + ui.displayCoinData(coin.symbol, coinData); + const cacheKey = `coinData_${coin.symbol}`; + cache.set(cacheKey, coinData); + } else { + //console.warn(`No data found for ${coin.symbol}`); + } + } + } catch (error) { + //console.error('Error loading all coin data:', error); + ui.displayErrorMessage('Failed to load coin data. Please try refreshing the page.'); + } finally { + //console.log('All coin data loaded'); } - } - } catch (error) { - console.error("An error occured:", error.message); - ui.displayErrorMessage('Failed to load coin data. Please try refreshing the page.'); - } - }, + }, loadCoinData: async (coin) => { + //console.log(`Loading data for ${coin.symbol}...`); const cacheKey = `coinData_${coin.symbol}`; let cachedData = cache.get(cacheKey); let data; if (cachedData) { + //console.log(`Using cached data for ${coin.symbol}`); data = cachedData.value; } else { try { @@ -1107,9 +967,11 @@ const app = { if (data.error) { throw new Error(data.error); } + //console.log(`Caching new data for ${coin.symbol}`); cache.set(cacheKey, data); cachedData = null; } catch (error) { + //console.error(`Error fetching ${coin.symbol} data:`, error.message); data = { error: error.message }; @@ -1119,13 +981,16 @@ const app = { } ui.displayCoinData(coin.symbol, data); ui.updateLoadTimeAndCache(0, cachedData); + //console.log(`Data loaded for ${coin.symbol}`); }, setupEventListeners: () => { + //console.log('Setting up event listeners...'); config.coins.forEach(coin => { const container = document.getElementById(`${coin.symbol.toLowerCase()}-container`); if (container) { - app.addEventListenerWithCleanup(container, 'click', () => { + container.addEventListener('click', () => { + //console.log(`${coin.symbol} container clicked`); ui.setActiveContainer(`${coin.symbol.toLowerCase()}-container`); if (chartModule.chart) { if (coin.symbol === 'WOW') { @@ -1140,27 +1005,26 @@ const app = { const refreshAllButton = document.getElementById('refresh-all'); if (refreshAllButton) { - app.addEventListenerWithCleanup(refreshAllButton, 'click', app.refreshAllData); + refreshAllButton.addEventListener('click', app.refreshAllData); } const headers = document.querySelectorAll('th'); headers.forEach((header, index) => { - app.addEventListenerWithCleanup(header, 'click', () => - app.sortTable(index, header.classList.contains('disabled')) - ); + header.addEventListener('click', () => app.sortTable(index, header.classList.contains('disabled'))); }); const closeErrorButton = document.getElementById('close-error'); if (closeErrorButton) { - app.addEventListenerWithCleanup(closeErrorButton, 'click', ui.hideErrorMessage); + closeErrorButton.addEventListener('click', ui.hideErrorMessage); } + //console.log('Event listeners set up'); }, initAutoRefresh: () => { console.log('Initializing auto-refresh...'); const toggleAutoRefreshButton = document.getElementById('toggle-auto-refresh'); if (toggleAutoRefreshButton) { - app.addEventListenerWithCleanup(toggleAutoRefreshButton, 'click', app.toggleAutoRefresh); + toggleAutoRefreshButton.addEventListener('click', app.toggleAutoRefresh); app.updateAutoRefreshButton(); } @@ -1189,7 +1053,7 @@ const app = { earliestExpiration = Math.min(earliestExpiration, cachedItem.expiresAt); } } catch (error) { - console.error("An error occured:", error.message); + //console.error(`Error parsing cached item ${key}:`, error); localStorage.removeItem(key); } } @@ -1214,65 +1078,72 @@ const app = { localStorage.setItem('nextRefreshTime', app.nextRefreshTime.toString()); app.updateNextRefreshTime(); }, - - refreshAllData: async () => { - if (app.isRefreshing) { - console.log('Refresh already in progress, skipping...'); - return; - } - - console.log('Refreshing all data...'); - app.isRefreshing = true; - ui.showLoader(); - chartModule.showChartLoader(); - - try { - cache.clear(); - await app.updateBTCPrice(); - - const allCoinData = await api.fetchCoinGeckoDataXHR(); - if (allCoinData.error) { - throw new Error(allCoinData.error); - } - - for (const coin of config.coins) { - const symbol = coin.symbol.toLowerCase(); - const coinData = allCoinData[symbol]; - if (coinData) { - coinData.displayName = coin.displayName || coin.symbol; - ui.displayCoinData(coin.symbol, coinData); - const cacheKey = `coinData_${coin.symbol}`; - cache.set(cacheKey, coinData); + + refreshAllData: async () => { + if (app.isRefreshing) { + console.log('Refresh already in progress, skipping...'); + return; } - } - if (chartModule.currentCoin) { - await chartModule.updateChart(chartModule.currentCoin, true); - } + console.log('Refreshing all data...'); + app.isRefreshing = true; + ui.showLoader(); + chartModule.showChartLoader(); + + try { - app.lastRefreshedTime = new Date(); - localStorage.setItem('lastRefreshedTime', app.lastRefreshedTime.getTime().toString()); - ui.updateLastRefreshedTime(); + cache.clear(); + + await app.updateBTCPrice(); + + const allCoinData = await api.fetchCoinGeckoDataXHR(); + if (allCoinData.error) { + throw new Error(allCoinData.error); + } + + for (const coin of config.coins) { + const symbol = coin.symbol.toLowerCase(); + const coinData = allCoinData[symbol]; + if (coinData) { + coinData.displayName = coin.displayName || coin.symbol; - } catch (error) { - console.error("An error occured:", error.message); - ui.displayErrorMessage('Failed to refresh all data. Please try again.'); - } finally { - ui.hideLoader(); - chartModule.hideChartLoader(); - app.isRefreshing = false; - if (app.isAutoRefreshEnabled) { - app.scheduleNextRefresh(); - } - } - }, + ui.displayCoinData(coin.symbol, coinData); + + const cacheKey = `coinData_${coin.symbol}`; + cache.set(cacheKey, coinData); + } else { + //console.error(`No data found for ${coin.symbol}`); + } + } + + if (chartModule.currentCoin) { + await chartModule.updateChart(chartModule.currentCoin, true); + } + + app.lastRefreshedTime = new Date(); + localStorage.setItem('lastRefreshedTime', app.lastRefreshedTime.getTime().toString()); + ui.updateLastRefreshedTime(); + + console.log('All data refreshed successfully'); + + } catch (error) { + //console.error('Error refreshing all data:', error); + ui.displayErrorMessage('Failed to refresh all data. Please try again.'); + } finally { + ui.hideLoader(); + chartModule.hideChartLoader(); + app.isRefreshing = false; + if (app.isAutoRefreshEnabled) { + app.scheduleNextRefresh(); + } + } + }, updateNextRefreshTime: () => { console.log('Updating next refresh time display'); const nextRefreshSpan = document.getElementById('next-refresh-time'); const labelElement = document.getElementById('next-refresh-label'); const valueElement = document.getElementById('next-refresh-value'); - if (nextRefreshSpan && labelElement && valueElement) { if (app.nextRefreshTime) { if (app.updateNextRefreshTimeRAF) { @@ -1296,7 +1167,6 @@ const app = { app.updateNextRefreshTimeRAF = requestAnimationFrame(updateDisplay); } }; - updateDisplay(); } else { labelElement.textContent = ''; @@ -1323,6 +1193,7 @@ const app = { }, startSpinAnimation: () => { + //console.log('Starting spin animation on auto-refresh button'); const svg = document.querySelector('#toggle-auto-refresh svg'); if (svg) { svg.classList.add('animate-spin'); @@ -1333,6 +1204,7 @@ const app = { }, stopSpinAnimation: () => { + //console.log('Stopping spin animation on auto-refresh button'); const svg = document.querySelector('#toggle-auto-refresh svg'); if (svg) { svg.classList.remove('animate-spin'); @@ -1340,6 +1212,7 @@ const app = { }, updateLastRefreshedTime: () => { + //console.log('Updating last refreshed time'); const lastRefreshedElement = document.getElementById('last-refreshed-time'); if (lastRefreshedElement && app.lastRefreshedTime) { const formattedTime = app.lastRefreshedTime.toLocaleTimeString(); @@ -1356,127 +1229,135 @@ const app = { } }, - updateBTCPrice: async () => { - try { - const priceData = await api.fetchCoinGeckoDataXHR(); - if (priceData.error) { - app.btcPriceUSD = 0; - } else if (priceData.btc && priceData.btc.current_price) { - app.btcPriceUSD = priceData.btc.current_price; - } else { - app.btcPriceUSD = 0; - } - } catch (error) { - console.error("An error occured:", error.message); - app.btcPriceUSD = 0; - } - }, + updateBTCPrice: async () => { + //console.log('Updating BTC price...'); + try { + const priceData = await api.fetchCoinGeckoDataXHR(); + if (priceData.error) { + //console.error('Error fetching BTC price:', priceData.error); + app.btcPriceUSD = 0; + } else if (priceData.btc && priceData.btc.current_price) { - sortTable: (columnIndex) => { - const sortableColumns = [0, 5, 6, 7]; // 0: Time, 5: Rate, 6: Market +/-, 7: Trade - if (!sortableColumns.includes(columnIndex)) { - return; - } - - const table = document.querySelector('table'); - if (!table) { - return; - } - - const rows = Array.from(table.querySelectorAll('tbody tr')); - const sortIcon = document.getElementById(`sort-icon-${columnIndex}`); - if (!sortIcon) { - return; - } - - const sortOrder = sortIcon.textContent === '↓' ? 1 : -1; - sortIcon.textContent = sortOrder === 1 ? '↑' : '↓'; - - const getSafeTextContent = (element) => element ? element.textContent.trim() : ''; - - rows.sort((a, b) => { - let aValue, bValue; - switch (columnIndex) { - case 1: // Time column - aValue = getSafeTextContent(a.querySelector('td:first-child .text-xs:first-child')); - bValue = getSafeTextContent(b.querySelector('td:first-child .text-xs:first-child')); - - const parseTime = (timeStr) => { - const [value, unit] = timeStr.split(' '); - const numValue = parseFloat(value); - switch(unit) { - case 'seconds': return numValue; - case 'minutes': return numValue * 60; - case 'hours': return numValue * 3600; - case 'days': return numValue * 86400; - default: return 0; + app.btcPriceUSD = priceData.btc.current_price; + } else { + //console.error('Unexpected BTC data structure:', priceData); + app.btcPriceUSD = 0; } - }; - return (parseTime(bValue) - parseTime(aValue)) * sortOrder; + } catch (error) { + //console.error('Error fetching BTC price:', error); + app.btcPriceUSD = 0; + } + //console.log('Current BTC price:', app.btcPriceUSD); + }, - case 5: // Rate - case 6: // Market +/- - aValue = getSafeTextContent(a.cells[columnIndex]); - bValue = getSafeTextContent(b.cells[columnIndex]); +sortTable: (columnIndex) => { + //console.log(`Sorting column: ${columnIndex}`); + const sortableColumns = [0, 5, 6, 7]; // 0: Time, 5: Rate, 6: Market +/-, 7: Trade + if (!sortableColumns.includes(columnIndex)) { + //console.log(`Column ${columnIndex} is not sortable`); + return; + } + const table = document.querySelector('table'); + if (!table) { + //console.error("Table not found for sorting."); + return; + } + const rows = Array.from(table.querySelectorAll('tbody tr')); + console.log(`Found ${rows.length} rows to sort`); + const sortIcon = document.getElementById(`sort-icon-${columnIndex}`); + if (!sortIcon) { + //console.error("Sort icon not found."); + return; + } + const sortOrder = sortIcon.textContent === '↓' ? 1 : -1; + sortIcon.textContent = sortOrder === 1 ? '↑' : '↓'; - aValue = parseFloat(aValue.replace(/[^\d.-]/g, '') || '0'); - bValue = parseFloat(bValue.replace(/[^\d.-]/g, '') || '0'); - return (aValue - bValue) * sortOrder; + const getSafeTextContent = (element) => element ? element.textContent.trim() : ''; - case 7: // Trade - const aCell = a.cells[columnIndex]; - const bCell = b.cells[columnIndex]; + rows.sort((a, b) => { + let aValue, bValue; + switch (columnIndex) { + case 1: // Time column + aValue = getSafeTextContent(a.querySelector('td:first-child .text-xs:first-child')); + bValue = getSafeTextContent(b.querySelector('td:first-child .text-xs:first-child')); + //console.log(`Comparing times: "${aValue}" vs "${bValue}"`); - aValue = getSafeTextContent(aCell.querySelector('a')) || - getSafeTextContent(aCell.querySelector('button')) || - getSafeTextContent(aCell); - bValue = getSafeTextContent(bCell.querySelector('a')) || - getSafeTextContent(bCell.querySelector('button')) || - getSafeTextContent(bCell); + const parseTime = (timeStr) => { + const [value, unit] = timeStr.split(' '); + const numValue = parseFloat(value); + switch(unit) { + case 'seconds': return numValue; + case 'minutes': return numValue * 60; + case 'hours': return numValue * 3600; + case 'days': return numValue * 86400; + default: return 0; + } + }; + return (parseTime(bValue) - parseTime(aValue)) * sortOrder; + + case 5: // Rate + case 6: // Market +/- + aValue = getSafeTextContent(a.cells[columnIndex]); + bValue = getSafeTextContent(b.cells[columnIndex]); + //console.log(`Comparing values: "${aValue}" vs "${bValue}"`); - aValue = aValue.toLowerCase(); - bValue = bValue.toLowerCase(); - - if (aValue === bValue) return 0; - if (aValue === "swap") return -1 * sortOrder; - if (bValue === "swap") return 1 * sortOrder; - return aValue.localeCompare(bValue) * sortOrder; - - default: - aValue = getSafeTextContent(a.cells[columnIndex]); - bValue = getSafeTextContent(b.cells[columnIndex]); - return aValue.localeCompare(bValue, undefined, { - numeric: true, - sensitivity: 'base' - }) * sortOrder; - } - }); - - const tbody = table.querySelector('tbody'); - if (tbody) { - const fragment = document.createDocumentFragment(); - rows.forEach(row => fragment.appendChild(row)); - tbody.appendChild(fragment); + aValue = parseFloat(aValue.replace(/[^\d.-]/g, '') || '0'); + bValue = parseFloat(bValue.replace(/[^\d.-]/g, '') || '0'); + return (aValue - bValue) * sortOrder; + + case 7: // Trade + const aCell = a.cells[columnIndex]; + const bCell = b.cells[columnIndex]; + //console.log('aCell:', aCell ? aCell.outerHTML : 'null'); + //console.log('bCell:', bCell ? bCell.outerHTML : 'null'); + + aValue = getSafeTextContent(aCell.querySelector('a')) || + getSafeTextContent(aCell.querySelector('button')) || + getSafeTextContent(aCell); + bValue = getSafeTextContent(bCell.querySelector('a')) || + getSafeTextContent(bCell.querySelector('button')) || + getSafeTextContent(bCell); + + aValue = aValue.toLowerCase(); + bValue = bValue.toLowerCase(); + + //console.log(`Comparing trade actions: "${aValue}" vs "${bValue}"`); + + if (aValue === bValue) return 0; + if (aValue === "swap") return -1 * sortOrder; + if (bValue === "swap") return 1 * sortOrder; + return aValue.localeCompare(bValue) * sortOrder; + + default: + aValue = getSafeTextContent(a.cells[columnIndex]); + bValue = getSafeTextContent(b.cells[columnIndex]); + //console.log(`Comparing default values: "${aValue}" vs "${bValue}"`); + return aValue.localeCompare(bValue, undefined, { + numeric: true, + sensitivity: 'base' + }) * sortOrder; } - }, + }); + const tbody = table.querySelector('tbody'); + if (tbody) { + rows.forEach(row => tbody.appendChild(row)); + } else { + //console.error("Table body not found."); + } + //console.log('Sorting completed'); +}, + initializeSelectImages: () => { const updateSelectedImage = (selectId) => { const select = document.getElementById(selectId); const button = document.getElementById(`${selectId}_button`); if (!select || !button) { + //console.error(`Elements not found for ${selectId}`); return; } - - const oldListener = select.getAttribute('data-change-listener'); - if (oldListener && window[oldListener]) { - select.removeEventListener('change', window[oldListener]); - delete window[oldListener]; - } - const selectedOption = select.options[select.selectedIndex]; const imageURL = selectedOption?.getAttribute('data-image'); - requestAnimationFrame(() => { if (imageURL) { button.style.backgroundImage = `url('${imageURL}')`; @@ -1490,50 +1371,46 @@ const app = { button.style.minHeight = '25px'; }); }; - + const handleSelectChange = (event) => { + updateSelectedImage(event.target.id); + }; ['coin_to', 'coin_from'].forEach(selectId => { const select = document.getElementById(selectId); if (select) { - - const listenerName = `selectChangeListener_${selectId}`; - window[listenerName] = () => updateSelectedImage(selectId); - - select.setAttribute('data-change-listener', listenerName); - - select.addEventListener('change', window[listenerName]); - + select.addEventListener('change', handleSelectChange); updateSelectedImage(selectId); - } - }); - }, - - updateResolutionButtons: (coinSymbol) => { - const resolutionButtons = document.querySelectorAll('.resolution-button'); - resolutionButtons.forEach(button => { - const resolution = button.id.split('-')[1]; - if (coinSymbol === 'WOW') { - if (resolution === 'day') { - button.classList.remove('text-gray-400', 'cursor-not-allowed', 'opacity-50', 'outline-none'); - button.classList.add('active'); - button.disabled = false; - } else { - button.classList.add('text-gray-400', 'cursor-not-allowed', 'opacity-50', 'outline-none'); - button.classList.remove('active'); - button.disabled = true; - } } else { - button.classList.remove('text-gray-400', 'cursor-not-allowed', 'opacity-50', 'outline-none'); - button.classList.toggle('active', resolution === config.currentResolution); - button.disabled = false; + //console.error(`Select element not found for ${selectId}`); } }); }, - toggleAutoRefresh: () => { +updateResolutionButtons: (coinSymbol) => { + const resolutionButtons = document.querySelectorAll('.resolution-button'); + resolutionButtons.forEach(button => { + const resolution = button.id.split('-')[1]; + if (coinSymbol === 'WOW') { + if (resolution === 'day') { + button.classList.remove('text-gray-400', 'cursor-not-allowed', 'opacity-50', 'outline-none'); + button.classList.add('active'); + button.disabled = false; + } else { + button.classList.add('text-gray-400', 'cursor-not-allowed', 'opacity-50', 'outline-none'); + button.classList.remove('active'); + button.disabled = true; + } + } else { + button.classList.remove('text-gray-400', 'cursor-not-allowed', 'opacity-50', 'outline-none'); + button.classList.toggle('active', resolution === config.currentResolution); + button.disabled = false; + } + }); +}, + + toggleAutoRefresh: () => { console.log('Toggling auto-refresh'); app.isAutoRefreshEnabled = !app.isAutoRefreshEnabled; localStorage.setItem('autoRefreshEnabled', app.isAutoRefreshEnabled.toString()); - if (app.isAutoRefreshEnabled) { console.log('Auto-refresh enabled, scheduling next refresh'); app.scheduleNextRefresh(); @@ -1546,18 +1423,24 @@ const app = { app.nextRefreshTime = null; localStorage.removeItem('nextRefreshTime'); } - app.updateAutoRefreshButton(); app.updateNextRefreshTime(); } }; +const resolutionButtons = document.querySelectorAll('.resolution-button'); +resolutionButtons.forEach(button => { + button.addEventListener('click', () => { + const resolution = button.id.split('-')[1]; + const currentCoin = chartModule.currentCoin; + + if (currentCoin !== 'WOW' || resolution === 'day') { + config.currentResolution = resolution; + chartModule.updateChart(currentCoin, true); + app.updateResolutionButtons(currentCoin); + } + }); +}); // LOAD app.init(); -app.visibilityCleanup = app.setupVisibilityHandler(); - -window.addEventListener('beforeunload', () => { - console.log('Page unloading, cleaning up...'); - app.cleanup(); -});