// Config
const config = {
apiKeys: getAPIKeys(),
coins: [
{ symbol: 'BTC', usesCryptoCompare: true, usesCoinGecko: false, historicalDays: 30 },
{ symbol: 'XMR', usesCryptoCompare: true, usesCoinGecko: false, historicalDays: 30 },
{ symbol: 'PART', usesCryptoCompare: true, usesCoinGecko: false, historicalDays: 30 },
{ symbol: 'PIVX', usesCryptoCompare: true, usesCoinGecko: false, historicalDays: 30 },
{ symbol: 'FIRO', usesCryptoCompare: true, usesCoinGecko: false, historicalDays: 30 },
{ symbol: 'DASH', usesCryptoCompare: true, usesCoinGecko: false, historicalDays: 30 },
{ symbol: 'LTC', usesCryptoCompare: true, usesCoinGecko: false, historicalDays: 30 },
{ symbol: 'DOGE', usesCryptoCompare: true, usesCoinGecko: false, historicalDays: 30 },
{ symbol: 'ETH', usesCryptoCompare: true, usesCoinGecko: false, historicalDays: 30 },
{ symbol: 'DCR', usesCryptoCompare: true, usesCoinGecko: false, historicalDays: 30 },
{ symbol: 'ZANO', usesCryptoCompare: true, usesCoinGecko: false, historicalDays: 30 },
{ symbol: 'WOW', usesCryptoCompare: false, usesCoinGecko: true, historicalDays: 30 },
{ symbol: 'BCH', usesCryptoCompare: true, usesCoinGecko: false, historicalDays: 30 }
],
apiEndpoints: {
cryptoCompare: 'https://min-api.cryptocompare.com/data/pricemultifull',
coinGecko: 'https://api.coingecko.com/api/v3/coins',
cryptoCompareHistorical: 'https://min-api.cryptocompare.com/data/v2/histoday'
},
chartColors: {
default: {
lineColor: 'rgba(77, 132, 240, 1)',
backgroundColor: 'rgba(77, 132, 240, 0.1)'
}
},
showVolume: false,
specialCoins: [''],
resolutions: {
month: { days: 30, interval: 'daily' },
week: { days: 7, interval: 'daily' },
day: { days: 1, interval: 'hourly' }
},
currentResolution: 'month'
};
// Utils
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 = {
day: { hour: '2-digit', minute: '2-digit', hour12: true },
week: { month: 'short', day: 'numeric' },
month: { year: 'numeric', month: 'short', day: 'numeric' }
};
return date.toLocaleString('en-US', { ...options[resolution], timeZone: 'UTC' });
},
debounce: (func, delay) => {
let timeoutId;
return (...args) => {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => func(...args), delay);
};
}
};
// Error
class AppError extends Error {
constructor(message, type = 'AppError') {
super(message);
this.name = type;
}
}
// Log
const logger = {
log: (message) => console.log(`[AppLog] ${new Date().toISOString()}: ${message}`),
warn: (message) => console.warn(`[AppWarn] ${new Date().toISOString()}: ${message}`),
error: (message) => console.error(`[AppError] ${new Date().toISOString()}: ${message}`)
};
// API
const api = {
makePostRequest: (url, headers = {}) => {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open('POST', '/json/readurl');
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.timeout = 30000;
xhr.ontimeout = () => reject(new AppError('Request timed out'));
xhr.onload = () => {
logger.log(`Response for ${url}:`, xhr.responseText);
if (xhr.status === 200) {
try {
const response = JSON.parse(xhr.responseText);
if (response.Error) {
logger.error(`API Error for ${url}:`, response.Error);
reject(new AppError(response.Error, 'APIError'));
} else {
resolve(response);
}
} catch (error) {
logger.error(`Invalid JSON response for ${url}:`, xhr.responseText);
reject(new AppError(`Invalid JSON response: ${error.message}`, 'ParseError'));
}
} else {
logger.error(`HTTP Error for ${url}: ${xhr.status} ${xhr.statusText}`);
reject(new AppError(`HTTP Error: ${xhr.status} ${xhr.statusText}`, 'HTTPError'));
}
};
xhr.onerror = () => reject(new AppError('Network error occurred', 'NetworkError'));
xhr.send(JSON.stringify({
url: url,
headers: headers
}));
});
},
fetchCryptoCompareDataXHR: (coin) => {
const url = `${config.apiEndpoints.cryptoCompare}?fsyms=${coin}&tsyms=USD,BTC&api_key=${config.apiKeys.cryptoCompare}`;
const headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
'Accept-Language': 'en-US,en;q=0.5',
};
return api.makePostRequest(url, headers).catch(error => ({
error: error.message
}));
},
fetchCoinGeckoDataXHR: (coin) => {
const coinConfig = config.coins.find(c => c.symbol === coin);
if (!coinConfig) {
logger.error(`No configuration found for coin: ${coin}`);
return Promise.reject(new AppError(`No configuration found for coin: ${coin}`));
}
let coinId;
switch (coin) {
case 'WOW':
coinId = 'wownero';
break;
default:
coinId = coin.toLowerCase();
}
const url = `${config.apiEndpoints.coinGecko}/${coinId}?localization=false&tickers=false&market_data=true&community_data=false&developer_data=false&sparkline=false`;
logger.log(`Fetching data for ${coin} from CoinGecko: ${url}`);
return api.makePostRequest(url)
.then(data => {
logger.log(`Raw CoinGecko data for ${coin}:`, data);
if (!data.market_data || !data.market_data.current_price) {
throw new AppError(`Invalid data structure received for ${coin}`);
}
return data;
})
.catch(error => {
logger.error(`Error fetching CoinGecko data for ${coin}:`, error);
return {
error: error.message
};
});
},
fetchHistoricalDataXHR: (coinSymbol) => {
const coin = config.coins.find(c => c.symbol === coinSymbol);
if (!coin) {
logger.error(`No configuration found for coin: ${coinSymbol}`);
return Promise.reject(new AppError(`No configuration found for coin: ${coinSymbol}`));
}
let url;
const resolutionConfig = config.resolutions[config.currentResolution];
if (coin.usesCoinGecko) {
let coinId;
switch (coinSymbol) {
case 'ZANO':
coinId = 'zano';
break;
case 'WOW':
coinId = 'wownero';
break;
default:
coinId = coinSymbol.toLowerCase();
}
url = `${config.apiEndpoints.coinGecko}/${coinId}/market_chart?vs_currency=usd&days=2`;
} else {
if (config.currentResolution === 'day') {
url = `https://min-api.cryptocompare.com/data/v2/histohour?fsym=${coinSymbol}&tsym=USD&limit=24&api_key=${config.apiKeys.cryptoCompare}`;
} else if (config.currentResolution === 'week') {
url = `${config.apiEndpoints.cryptoCompareHistorical}?fsym=${coinSymbol}&tsym=USD&limit=7&aggregate=24&api_key=${config.apiKeys.cryptoCompare}`;
} else {
url = `${config.apiEndpoints.cryptoCompareHistorical}?fsym=${coinSymbol}&tsym=USD&limit=30&aggregate=24&api_key=${config.apiKeys.cryptoCompare}`;
}
}
logger.log(`Fetching historical data for ${coinSymbol}:`, url);
return api.makePostRequest(url)
.then(response => {
logger.log(`Received historical data for ${coinSymbol}:`, JSON.stringify(response, null, 2));
return response;
})
.catch(error => {
logger.error(`Error fetching historical data for ${coinSymbol}:`, error);
throw error;
});
},
};
// Cache
const cache = {
ttl: 15 * 60 * 1000, // 15 minutes in milliseconds
set: (key, value, customTtl = null) => {
const item = {
value: value,
timestamp: Date.now(),
expiresAt: Date.now() + (customTtl || cache.ttl)
};
localStorage.setItem(key, JSON.stringify(item));
},
get: (key) => {
const itemStr = localStorage.getItem(key);
if (!itemStr) {
return null;
}
try {
const item = JSON.parse(itemStr);
const now = Date.now();
if (now < item.expiresAt) {
return {
value: item.value,
remainingTime: item.expiresAt - now
};
} else {
localStorage.removeItem(key);
}
} catch (e) {
logger.error('Error parsing cache item:', e);
localStorage.removeItem(key);
}
return null;
},
isValid: (key) => {
return cache.get(key) !== null;
},
clear: () => {
Object.keys(localStorage).forEach(key => {
if (key.startsWith('coinData_') || key.startsWith('chartData_')) {
localStorage.removeItem(key);
}
});
}
};
// UI
const ui = {
displayCoinData: (coin, data) => {
const coinConfig = config.coins.find(c => c.symbol === coin);
let priceUSD, priceBTC, priceChange1d, volume24h;
const updateUI = (isError = false) => {
const priceUsdElement = document.querySelector(`#${coin.toLowerCase()}-price-usd`);
const volumeDiv = document.querySelector(`#${coin.toLowerCase()}-volume-div`);
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 && coin !== 'BTC') {
priceBtcElement.textContent = isError ? 'N/A' : `${priceBTC.toFixed(8)} BTC`;
btcPriceDiv.style.display = 'flex';
}
ui.updatePriceChangeContainer(coin, isError ? null : priceChange1d);
};
try {
if (data.error) {
throw new Error(data.error);
}
if (coinConfig.usesCoinGecko) {
if (!data.market_data) {
throw new Error(`Invalid CoinGecko data structure for ${coin}`);
}
priceUSD = data.market_data.current_price.usd;
priceBTC = data.market_data.current_price.btc;
priceChange1d = data.market_data.price_change_percentage_24h;
volume24h = data.market_data.total_volume.usd;
} else if (coinConfig.usesCryptoCompare) {
if (!data.RAW || !data.RAW[coin] || !data.RAW[coin].USD) {
throw new Error(`Invalid CryptoCompare data structure for ${coin}`);
}
priceUSD = data.RAW[coin].USD.PRICE;
priceBTC = data.RAW[coin].BTC.PRICE;
priceChange1d = data.RAW[coin].USD.CHANGEPCT24HOUR;
volume24h = data.RAW[coin].USD.TOTALVOLUME24HTO;
}
if (isNaN(priceUSD) || isNaN(priceBTC) || isNaN(volume24h)) {
throw new Error(`Invalid numeric values in data for ${coin}`);
}
updateUI(false);
} catch (error) {
logger.error(`Error displaying data for ${coin}:`, error.message);
updateUI(true);
}
},
showLoader: () => {
const loader = document.getElementById('loader');
if (loader) {
loader.classList.remove('hidden');
}
},
hideLoader: () => {
const loader = document.getElementById('loader');
if (loader) {
loader.classList.add('hidden');
}
},
showCoinLoader: (coinSymbol) => {
const loader = document.getElementById(`${coinSymbol.toLowerCase()}-loader`);
if (loader) {
loader.classList.remove('hidden');
}
},
hideCoinLoader: (coinSymbol) => {
const loader = document.getElementById(`${coinSymbol.toLowerCase()}-loader`);
if (loader) {
loader.classList.add('hidden');
}
},
updateCacheStatus: (isCached) => {
const cacheStatusElement = document.getElementById('cache-status');
if (cacheStatusElement) {
cacheStatusElement.textContent = isCached ? 'Cached' : 'Live';
cacheStatusElement.classList.toggle('text-green-500', isCached);
cacheStatusElement.classList.toggle('text-blue-500', !isCached);
}
},
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);
cacheStatusElement.textContent = `Cached: ${remainingMinutes} min left`;
cacheStatusElement.classList.add('text-green-500');
cacheStatusElement.classList.remove('text-blue-500');
} else {
cacheStatusElement.textContent = 'Live';
cacheStatusElement.classList.add('text-blue-500');
cacheStatusElement.classList.remove('text-green-500');
}
}
ui.updateLastRefreshedTime();
},
updatePriceChangeContainer: (coin, priceChange) => {
const container = document.querySelector(`#${coin.toLowerCase()}-price-change-container`);
if (container) {
container.innerHTML = priceChange !== null ?
(priceChange >= 0 ? ui.positivePriceChangeHTML(priceChange) : ui.negativePriceChangeHTML(priceChange)) :
'N/A';
}
},
updateLastRefreshedTime: () => {
const lastRefreshedElement = document.getElementById('last-refreshed-time');
if (lastRefreshedElement && app.lastRefreshedTime) {
const formattedTime = app.lastRefreshedTime.toLocaleTimeString();
lastRefreshedElement.textContent = `Last Refreshed: ${formattedTime}`;
}
},
positivePriceChangeHTML: (value) => `
`,
negativePriceChangeHTML: (value) => `
${Math.abs(value).toFixed(2)}%
`,
formatPrice: (coin, price) => {
if (typeof price !== 'number' || isNaN(price)) {
logger.error(`Invalid price for ${coin}:`, price);
return 'N/A';
}
if (price < 0.000001) return price.toExponential(2);
if (price < 0.001) return price.toFixed(8);
if (price < 1) return price.toFixed(4);
if (price < 1000) return price.toFixed(2);
return price.toFixed(1);
},
setActiveContainer: (containerId) => {
const containerIds = ['btc', 'xmr', 'part', 'pivx', 'firo', 'dash', 'ltc', 'doge', 'eth', 'dcr', 'zano', 'wow', 'bch'].map(id => `${id}-container`);
containerIds.forEach(id => {
const container = document.getElementById(id);
if (container) {
const innerDiv = container.querySelector('div');
innerDiv.classList.toggle('active-container', id === containerId);
}
});
},
displayErrorMessage: (message) => {
const errorOverlay = document.getElementById('error-overlay');
const errorMessage = document.getElementById('error-message');
const chartContainer = document.querySelector('.container-to-blur');
if (errorOverlay && errorMessage && chartContainer) {
errorOverlay.classList.remove('hidden');
errorMessage.textContent = message;
chartContainer.classList.add('blurred');
}
},
hideErrorMessage: () => {
const errorOverlay = document.getElementById('error-overlay');
const containersToBlur = document.querySelectorAll('.container-to-blur');
if (errorOverlay) {
errorOverlay.classList.add('hidden');
containersToBlur.forEach(container => container.classList.remove('blurred'));
}
}
};
// Chart
const chartModule = {
chart: null,
currentCoin: 'BTC',
loadStartTime: 0,
verticalLinePlugin: {
id: 'verticalLine',
beforeDraw: (chart, args, options) => {
if (chart.tooltip._active && chart.tooltip._active.length) {
const activePoint = chart.tooltip._active[0];
const ctx = chart.ctx;
const x = activePoint.element.x;
const topY = chart.scales.y.top;
const bottomY = chart.scales.y.bottom;
ctx.save();
ctx.beginPath();
ctx.moveTo(x, topY);
ctx.lineTo(x, bottomY);
ctx.lineWidth = options.lineWidth || 1;
ctx.strokeStyle = options.lineColor || 'rgba(77, 132, 240, 0.5)';
ctx.stroke();
ctx.restore();
}
}
},
initChart: () => {
const ctx = document.getElementById('coin-chart').getContext('2d');
if (!ctx) {
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: {
datasets: [{
label: 'Price',
data: [],
borderColor: 'rgba(77, 132, 240, 1)',
backgroundColor: gradient,
tension: 0.4,
fill: true,
borderWidth: 3,
pointRadius: 0,
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
interaction: {
intersect: false,
mode: 'index'
},
scales: {
x: {
type: 'time',
time: {
unit: 'day',
displayFormats: {
hour: 'ha',
day: 'MMM d'
}
},
ticks: {
source: 'data',
maxTicksLimit: 10,
font: {
size: 12,
family: "'Inter', sans-serif"
},
color: 'rgba(156, 163, 175, 1)'
},
grid: {
display: false
}
},
y: {
beginAtZero: false,
ticks: {
font: {
size: 12,
family: "'Inter', sans-serif"
},
color: 'rgba(156, 163, 175, 1)',
callback: (value) => '$' + value.toLocaleString()
},
grid: {
display: false
}
}
},
plugins: {
legend: {
display: false
},
tooltip: {
mode: 'index',
intersect: false,
backgroundColor: 'rgba(255, 255, 255, 0.9)',
titleColor: 'rgba(17, 24, 39, 1)',
bodyColor: 'rgba(55, 65, 81, 1)',
borderColor: 'rgba(226, 232, 240, 1)',
borderWidth: 1,
cornerRadius: 4,
padding: 8,
displayColors: false,
callbacks: {
title: (tooltipItems) => {
const date = new Date(tooltipItems[0].parsed.x);
return date.toLocaleString('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric',
hour: 'numeric',
minute: 'numeric',
hour12: true,
timeZone: 'UTC'
});
},
label: (item) => {
const value = item.parsed.y;
return `${chartModule.currentCoin}: $${value.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 8 })}`;
}
}
},
verticalLine: {
lineWidth: 1,
lineColor: 'rgba(77, 132, 240, 0.5)'
}
},
elements: {
point: {
backgroundColor: 'transparent',
borderColor: 'rgba(77, 132, 240, 1)',
borderWidth: 2,
radius: 0,
hoverRadius: 4,
hitRadius: 6,
hoverBorderWidth: 2
},
line: {
backgroundColor: gradient,
borderColor: 'rgba(77, 132, 240, 1)',
fill: true
}
}
},
plugins: [chartModule.verticalLinePlugin]
});
console.log('Chart initialized:', chartModule.chart);
},
prepareChartData: (coinSymbol, data) => {
console.log(`Preparing chart data for ${coinSymbol}:`, JSON.stringify(data, null, 2));
const coin = config.coins.find(c => c.symbol === coinSymbol);
if (!data || typeof data !== 'object' || data.error) {
console.error(`Invalid data received for ${coinSymbol}:`, data);
return [];
}
try {
let preparedData;
if (coin.usesCoinGecko) {
if (!data.prices || !Array.isArray(data.prices)) {
throw new Error(`Invalid CoinGecko data structure for ${coinSymbol}`);
}
preparedData = data.prices.map(entry => ({
x: new Date(entry[0]),
y: entry[1]
}));
if (config.currentResolution === 'day') {
preparedData = chartModule.ensureHourlyData(preparedData);
} else {
preparedData = preparedData.filter((_, index) => index % 24 === 0);
}
} else {
if (!data.Data || !data.Data.Data || !Array.isArray(data.Data.Data)) {
throw new Error(`Invalid CryptoCompare data structure for ${coinSymbol}`);
}
preparedData = data.Data.Data.map(d => ({
x: new Date(d.time * 1000),
y: d.close
}));
}
const expectedDataPoints = config.currentResolution === 'day' ? 24 : config.resolutions[config.currentResolution].days;
if (preparedData.length < expectedDataPoints) {
console.warn(`Insufficient data points for ${coinSymbol}. Expected ${expectedDataPoints}, got ${preparedData.length}`);
}
console.log(`Prepared data for ${coinSymbol}:`, preparedData.slice(0, 5));
return preparedData;
} catch (error) {
console.error(`Error preparing chart data for ${coinSymbol}:`, error);
return [];
}
},
ensureHourlyData: (data) => {
const now = new Date();
const twentyFourHoursAgo = new Date(now.getTime() - 24 * 60 * 60 * 1000);
const hourlyData = [];
for (let i = 0; i < 24; i++) {
const targetTime = new Date(twentyFourHoursAgo.getTime() + i * 60 * 60 * 1000);
const closestDataPoint = data.reduce((prev, curr) =>
Math.abs(curr.x - targetTime) < Math.abs(prev.x - targetTime) ? curr : prev
);
hourlyData.push({
x: targetTime,
y: closestDataPoint.y
});
}
return hourlyData;
},
updateChart: async (coinSymbol, forceRefresh = false) => {
try {
chartModule.showChartLoader();
chartModule.loadStartTime = Date.now();
const cacheKey = `chartData_${coinSymbol}_${config.currentResolution}`;
const cacheDuration = 15 * 60 * 1000; // 15 minutes in milliseconds
let cachedData = !forceRefresh ? cache.get(cacheKey) : null;
let data;
if (cachedData) {
data = cachedData.value;
console.log(`Using cached data for ${coinSymbol} (${config.currentResolution})`);
} else {
console.log(`Fetching fresh data for ${coinSymbol} (${config.currentResolution})`);
data = await api.fetchHistoricalDataXHR(coinSymbol);
if (data.error) {
throw new Error(data.error);
}
cache.set(cacheKey, data, cacheDuration);
cachedData = null;
}
const chartData = chartModule.prepareChartData(coinSymbol, data);
if (chartModule.chart) {
chartModule.chart.data.datasets[0].data = chartData;
chartModule.chart.data.datasets[0].label = `${coinSymbol} Price (USD)`;
const coin = config.coins.find(c => c.symbol === coinSymbol);
let apiSource = coin.usesCoinGecko ? 'CoinGecko' : 'CryptoCompare';
let currency = 'USD';
const chartTitle = document.getElementById('chart-title');
if (chartTitle) {
chartTitle.textContent = `${coinSymbol} Price Chart`;
}
chartModule.chart.options.scales.y.title = {
display: true,
text: `Price (${currency}) - ${coinSymbol} - ${apiSource}`
};
if (coinSymbol === 'WOW') {
chartModule.chart.options.scales.y.ticks.callback = (value) => {
return '$' + value.toFixed(4);
};
chartModule.chart.options.plugins.tooltip.callbacks.label = (context) => {
let label = context.dataset.label || '';
if (label) {
label += ': ';
}
if (context.parsed.y !== null) {
label += '$' + context.parsed.y.toFixed(4);
}
return label;
};
} else {
chartModule.chart.options.scales.y.ticks.callback = (value) => {
return '$' + ui.formatPrice(coinSymbol, value);
};
chartModule.chart.options.plugins.tooltip.callbacks.label = (context) => {
let label = context.dataset.label || '';
if (label) {
label += ': ';
}
if (context.parsed.y !== null) {
label += '$' + ui.formatPrice(coinSymbol, context.parsed.y);
}
return label;
};
}
if (config.currentResolution === 'day') {
chartModule.chart.options.scales.x = {
type: 'time',
time: {
unit: 'hour',
displayFormats: {
hour: 'HH:mm'
},
tooltipFormat: 'MMM d, yyyy HH:mm'
},
ticks: {
source: 'data',
maxTicksLimit: 24,
callback: function(value, index, values) {
const date = new Date(value);
return date.getUTCHours().toString().padStart(2, '0') + ':00';
}
}
};
} else {
chartModule.chart.options.scales.x = {
type: 'time',
time: {
unit: 'day',
displayFormats: {
day: 'MMM d'
},
tooltipFormat: 'MMM d, yyyy'
},
ticks: {
source: 'data',
maxTicksLimit: 10
}
};
}
console.log('Updating chart with data:', chartData.slice(0, 5));
chartModule.chart.update('active');
} else {
console.error('Chart object not initialized');
}
chartModule.currentCoin = coinSymbol;
const loadTime = Date.now() - chartModule.loadStartTime;
ui.updateLoadTimeAndCache(loadTime, cachedData);
} catch (error) {
console.error(`Error updating chart for ${coinSymbol}:`, error);
let errorMessage = `Failed to update chart for ${coinSymbol}`;
if (error.message) {
errorMessage += `: ${error.message}`;
}
ui.displayErrorMessage(errorMessage);
} finally {
chartModule.hideChartLoader();
}
},
showChartLoader: () => {
document.getElementById('chart-loader').classList.remove('hidden');
document.getElementById('coin-chart').classList.add('hidden');
},
hideChartLoader: () => {
document.getElementById('chart-loader').classList.add('hidden');
document.getElementById('coin-chart').classList.remove('hidden');
}
};
Chart.register(chartModule.verticalLinePlugin);
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');
}
}
};
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,
autoRefreshInterval: null,
nextRefreshTime: null,
lastRefreshedTime: null,
isAutoRefreshEnabled: localStorage.getItem('autoRefreshEnabled') !== 'false',
refreshTexts: {
label: 'Auto-refresh in',
disabled: 'Auto-refresh: disabled',
justRefreshed: 'Just refreshed',
},
init: () => {
window.addEventListener('load', app.onLoad);
app.loadLastRefreshedTime();
app.updateAutoRefreshButton();
},
onLoad: async () => {
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');
}
// Load all coin data immediately
await app.loadAllCoinData();
if (chartModule.chart) {
config.currentResolution = 'month';
await chartModule.updateChart('BTC');
app.updateResolutionButtons('BTC');
}
ui.setActiveContainer('btc-container');
// Set up event listeners and other 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();
}
}
},
loadAllCoinData: async () => {
for (const coin of config.coins) {
await app.loadCoinData(coin);
}
},
loadCoinData: async (coin) => {
const cacheKey = `coinData_${coin.symbol}`;
let cachedData = cache.get(cacheKey);
let data;
if (cachedData) {
data = cachedData.value;
} else {
try {
ui.showCoinLoader(coin.symbol);
if (coin.usesCoinGecko) {
data = await api.fetchCoinGeckoDataXHR(coin.symbol);
} else {
data = await api.fetchCryptoCompareDataXHR(coin.symbol);
}
if (data.error) {
throw new Error(data.error);
}
cache.set(cacheKey, data);
cachedData = null;
} catch (error) {
console.error(`Error fetching ${coin.symbol} data:`, error.message);
data = {
error: error.message
};
} finally {
ui.hideCoinLoader(coin.symbol);
}
}
ui.displayCoinData(coin.symbol, data);
ui.updateLoadTimeAndCache(0, cachedData);
},
setupEventListeners: () => {
config.coins.forEach(coin => {
const container = document.getElementById(`${coin.symbol.toLowerCase()}-container`);
if (container) {
container.addEventListener('click', () => {
ui.setActiveContainer(`${coin.symbol.toLowerCase()}-container`);
if (chartModule.chart) {
if (coin.symbol === 'WOW') {
config.currentResolution = 'day';
}
chartModule.updateChart(coin.symbol);
app.updateResolutionButtons(coin.symbol);
}
});
}
});
const refreshAllButton = document.getElementById('refresh-all');
if (refreshAllButton) {
refreshAllButton.addEventListener('click', app.refreshAllData);
}
const headers = document.querySelectorAll('th');
headers.forEach((header, index) => {
header.addEventListener('click', () => app.sortTable(index, header.classList.contains('disabled')));
});
const closeErrorButton = document.getElementById('close-error');
if (closeErrorButton) {
closeErrorButton.addEventListener('click', ui.hideErrorMessage);
}
},
initAutoRefresh: () => {
const toggleAutoRefreshButton = document.getElementById('toggle-auto-refresh');
if (toggleAutoRefreshButton) {
toggleAutoRefreshButton.addEventListener('click', app.toggleAutoRefresh);
app.updateAutoRefreshButton();
}
if (app.isAutoRefreshEnabled) {
const storedNextRefreshTime = localStorage.getItem('nextRefreshTime');
if (storedNextRefreshTime) {
const nextRefreshTime = parseInt(storedNextRefreshTime);
if (nextRefreshTime > Date.now()) {
app.nextRefreshTime = nextRefreshTime;
app.startAutoRefresh();
} else {
app.startAutoRefresh(true);
}
} else {
app.startAutoRefresh(true);
}
}
},
startAutoRefresh: (resetTimer = false) => {
app.stopAutoRefresh();
if (resetTimer || !app.nextRefreshTime) {
app.nextRefreshTime = Date.now() + cache.ttl;
}
const timeUntilNextRefresh = Math.max(0, app.nextRefreshTime - Date.now());
if (timeUntilNextRefresh === 0) {
app.nextRefreshTime = Date.now() + cache.ttl;
}
app.autoRefreshInterval = setTimeout(() => {
app.refreshAllData();
app.startAutoRefresh(true);
}, timeUntilNextRefresh);
localStorage.setItem('nextRefreshTime', app.nextRefreshTime.toString());
app.updateNextRefreshTime();
app.isAutoRefreshEnabled = true;
localStorage.setItem('autoRefreshEnabled', 'true');
},
stopAutoRefresh: () => {
if (app.autoRefreshInterval) {
clearTimeout(app.autoRefreshInterval);
app.autoRefreshInterval = null;
}
app.nextRefreshTime = null;
localStorage.removeItem('nextRefreshTime');
app.updateNextRefreshTime();
app.isAutoRefreshEnabled = false;
localStorage.setItem('autoRefreshEnabled', 'false');
},
toggleAutoRefresh: () => {
if (app.isAutoRefreshEnabled) {
app.stopAutoRefresh();
} else {
app.startAutoRefresh();
}
app.updateAutoRefreshButton();
},
updateNextRefreshTime: () => {
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) {
const timeUntilRefresh = Math.max(0, Math.ceil((app.nextRefreshTime - Date.now()) / 1000));
if (timeUntilRefresh === 0) {
labelElement.textContent = '';
valueElement.textContent = app.refreshTexts.justRefreshed;
} else {
const minutes = Math.floor(timeUntilRefresh / 60);
const seconds = timeUntilRefresh % 60;
labelElement.textContent = `${app.refreshTexts.label}: `;
valueElement.textContent = `${minutes}:${seconds.toString().padStart(2, '0')}`;
}
if (timeUntilRefresh > 0) {
setTimeout(app.updateNextRefreshTime, 1000);
}
} else {
labelElement.textContent = '';
valueElement.textContent = app.refreshTexts.disabled;
}
}
},
updateAutoRefreshButton: () => {
const button = document.getElementById('toggle-auto-refresh');
if (button) {
if (app.isAutoRefreshEnabled) {
button.classList.remove('text-gray-600', 'dark:text-gray-400');
button.classList.add('text-green-500', 'dark:text-green-400');
app.startSpinAnimation();
} else {
button.classList.remove('text-green-500', 'dark:text-green-400');
button.classList.add('text-gray-600', 'dark:text-gray-400');
app.stopSpinAnimation();
}
button.title = app.isAutoRefreshEnabled ? 'Disable Auto-Refresh' : 'Enable Auto-Refresh';
}
},
startSpinAnimation: () => {
const svg = document.querySelector('#toggle-auto-refresh svg');
if (svg) {
svg.classList.add('animate-spin');
setTimeout(() => {
svg.classList.remove('animate-spin');
}, 2000);
}
},
stopSpinAnimation: () => {
const svg = document.querySelector('#toggle-auto-refresh svg');
if (svg) {
svg.classList.remove('animate-spin');
}
},
refreshAllData: async () => {
ui.showLoader();
chartModule.showChartLoader();
try {
cache.clear();
await app.updateBTCPrice();
await app.loadAllCoinData();
if (chartModule.currentCoin) {
await chartModule.updateChart(chartModule.currentCoin, true);
}
app.lastRefreshedTime = new Date();
localStorage.setItem('lastRefreshedTime', app.lastRefreshedTime.getTime().toString());
ui.updateLastRefreshedTime();
} catch (error) {
console.error('Error refreshing all data:', error);
ui.displayErrorMessage('Failed to refresh all data. Please try again.');
} finally {
ui.hideLoader();
chartModule.hideChartLoader();
}
},
updateLastRefreshedTime: () => {
const lastRefreshedElement = document.getElementById('last-refreshed-time');
if (lastRefreshedElement && app.lastRefreshedTime) {
const formattedTime = app.lastRefreshedTime.toLocaleTimeString();
lastRefreshedElement.textContent = `Last Refreshed: ${formattedTime}`;
}
},
loadLastRefreshedTime: () => {
const storedTime = localStorage.getItem('lastRefreshedTime');
if (storedTime) {
app.lastRefreshedTime = new Date(parseInt(storedTime));
ui.updateLastRefreshedTime();
}
},
updateBTCPrice: async () => {
try {
const btcData = await api.fetchCryptoCompareDataXHR('BTC');
if (btcData.error) {
console.error('Error fetching BTC price:', btcData.error);
app.btcPriceUSD = 0;
} else if (btcData.RAW && btcData.RAW.BTC && btcData.RAW.BTC.USD) {
app.btcPriceUSD = btcData.RAW.BTC.USD.PRICE;
} else {
console.error('Unexpected BTC data structure:', btcData);
app.btcPriceUSD = 0;
}
} catch (error) {
console.error('Error fetching BTC price:', error);
app.btcPriceUSD = 0;
}
console.log('Current BTC price:', app.btcPriceUSD);
},
sortTable: (columnIndex) => {
const sortableColumns = [5, 6];
if (!sortableColumns.includes(columnIndex)) return;
const table = document.querySelector('table');
if (!table) {
console.error("Table not found for sorting.");
return;
}
const rows = Array.from(table.querySelectorAll('tbody tr'));
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 ? '↑' : '↓';
rows.sort((a, b) => {
const aValue = a.cells[columnIndex]?.textContent.trim() || '';
const bValue = b.cells[columnIndex]?.textContent.trim() || '';
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.");
}
},
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 selectedOption = select.options[select.selectedIndex];
const imageURL = selectedOption?.getAttribute('data-image');
requestAnimationFrame(() => {
if (imageURL) {
button.style.backgroundImage = `url('${imageURL}')`;
button.style.backgroundSize = '25px 25px';
button.style.backgroundPosition = 'center';
button.style.backgroundRepeat = 'no-repeat';
} else {
button.style.backgroundImage = 'none';
}
button.style.minWidth = '25px';
button.style.minHeight = '25px';
});
};
const handleSelectChange = (event) => {
updateSelectedImage(event.target.id);
};
['coin_to', 'coin_from'].forEach(selectId => {
const select = document.getElementById(selectId);
if (select) {
select.addEventListener('change', handleSelectChange);
updateSelectedImage(selectId);
} else {
console.error(`Select element not found for ${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;
}
});
},
};
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);
}
});
});
app.init();