mirror of
https://github.com/basicswap/basicswap.git
synced 2025-01-19 00:54:35 +00:00
ui: Chart update/fixes + wallets (prices) use TOR <> API.
This commit is contained in:
parent
8081f22e92
commit
934e809ac3
4 changed files with 647 additions and 507 deletions
|
@ -1,3 +1,5 @@
|
||||||
|
// Config
|
||||||
|
|
||||||
const coinNameToSymbol = {
|
const coinNameToSymbol = {
|
||||||
'Bitcoin': 'bitcoin',
|
'Bitcoin': 'bitcoin',
|
||||||
'Particl': 'particl',
|
'Particl': 'particl',
|
||||||
|
@ -6,8 +8,8 @@ const coinNameToSymbol = {
|
||||||
'Monero': 'monero',
|
'Monero': 'monero',
|
||||||
'Wownero': 'wownero',
|
'Wownero': 'wownero',
|
||||||
'Litecoin': 'litecoin',
|
'Litecoin': 'litecoin',
|
||||||
'Firo': 'zcoin',
|
'Firo': 'firo',
|
||||||
'Zcoin': 'zcoin',
|
'Zcoin': 'firo',
|
||||||
'Dash': 'dash',
|
'Dash': 'dash',
|
||||||
'PIVX': 'pivx',
|
'PIVX': 'pivx',
|
||||||
'Decred': 'decred',
|
'Decred': 'decred',
|
||||||
|
@ -52,7 +54,30 @@ function makePostRequest(url, headers = {}) {
|
||||||
|
|
||||||
const symbolToCoinName = {
|
const symbolToCoinName = {
|
||||||
...Object.fromEntries(Object.entries(coinNameToSymbol).map(([key, value]) => [value, key])),
|
...Object.fromEntries(Object.entries(coinNameToSymbol).map(([key, value]) => [value, key])),
|
||||||
'zcoin': 'Firo'
|
'zcoin': 'Firo',
|
||||||
|
'firo': 'Firo'
|
||||||
|
};
|
||||||
|
|
||||||
|
function getDisplayName(coinName) {
|
||||||
|
return coinNameToDisplayName[coinName] || coinName;
|
||||||
|
}
|
||||||
|
|
||||||
|
const coinNameToDisplayName = {
|
||||||
|
'Bitcoin': 'Bitcoin',
|
||||||
|
'Litecoin': 'Litecoin',
|
||||||
|
'Monero': 'Monero',
|
||||||
|
'Particl': 'Particl',
|
||||||
|
'Particl Blind': 'Particl Blind',
|
||||||
|
'Particl Anon': 'Particl Anon',
|
||||||
|
'PIVX': 'PIVX',
|
||||||
|
'Firo': 'Firo',
|
||||||
|
'Zcoin': 'Firo',
|
||||||
|
'Dash': 'Dash',
|
||||||
|
'Decred': 'Decred',
|
||||||
|
'Wownero': 'Wownero',
|
||||||
|
'Bitcoin Cash': 'Bitcoin Cash',
|
||||||
|
'Dogecoin': 'Dogecoin',
|
||||||
|
'Zano': 'Zano'
|
||||||
};
|
};
|
||||||
|
|
||||||
let latestPrices = null;
|
let latestPrices = null;
|
||||||
|
@ -136,7 +161,7 @@ const MIN_REFRESH_INTERVAL = 30; // Minimum refresh interval in seconds
|
||||||
const isSentOffers = window.offersTableConfig.isSentOffers;
|
const isSentOffers = window.offersTableConfig.isSentOffers;
|
||||||
|
|
||||||
let currentPage = 1;
|
let currentPage = 1;
|
||||||
const itemsPerPage = 50;
|
const itemsPerPage = 20;
|
||||||
|
|
||||||
const coinIdToName = {
|
const coinIdToName = {
|
||||||
1: 'particl', 2: 'bitcoin', 3: 'litecoin', 4: 'decred',
|
1: 'particl', 2: 'bitcoin', 3: 'litecoin', 4: 'decred',
|
||||||
|
@ -164,12 +189,20 @@ function getCoinSymbol(fullName) {
|
||||||
const symbolMap = {
|
const symbolMap = {
|
||||||
'Bitcoin': 'BTC', 'Litecoin': 'LTC', 'Monero': 'XMR',
|
'Bitcoin': 'BTC', 'Litecoin': 'LTC', 'Monero': 'XMR',
|
||||||
'Particl': 'PART', 'Particl Blind': 'PART', 'Particl Anon': 'PART',
|
'Particl': 'PART', 'Particl Blind': 'PART', 'Particl Anon': 'PART',
|
||||||
'PIVX': 'PIVX', 'Firo': 'FIRO', 'Dash': 'DASH',
|
'PIVX': 'PIVX', 'Firo': 'FIRO', 'Zcoin': 'FIRO',
|
||||||
'Decred': 'DCR', 'Wownero': 'WOW', 'Bitcoin Cash': 'BCH'
|
'Dash': 'DASH', 'Decred': 'DCR', 'Wownero': 'WOW',
|
||||||
|
'Bitcoin Cash': 'BCH'
|
||||||
};
|
};
|
||||||
return symbolMap[fullName] || fullName;
|
return symbolMap[fullName] || fullName;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getDisplayName(coinName) {
|
||||||
|
if (coinName.toLowerCase() === 'zcoin') {
|
||||||
|
return 'Firo';
|
||||||
|
}
|
||||||
|
return coinNameToDisplayName[coinName] || coinName;
|
||||||
|
}
|
||||||
|
|
||||||
function getValidOffers() {
|
function getValidOffers() {
|
||||||
if (isSentOffers) {
|
if (isSentOffers) {
|
||||||
return jsonData;
|
return jsonData;
|
||||||
|
@ -223,7 +256,7 @@ function setRefreshButtonLoading(isLoading) {
|
||||||
|
|
||||||
refreshButton.disabled = isLoading;
|
refreshButton.disabled = isLoading;
|
||||||
refreshIcon.classList.toggle('animate-spin', isLoading);
|
refreshIcon.classList.toggle('animate-spin', isLoading);
|
||||||
refreshText.textContent = isLoading ? 'Refreshing...' : 'Refresh';
|
refreshText.textContent = isLoading ? 'Refresh' : 'Refresh'; //to-do
|
||||||
}
|
}
|
||||||
|
|
||||||
function escapeHtml(unsafe) {
|
function escapeHtml(unsafe) {
|
||||||
|
@ -630,9 +663,28 @@ function filterAndSortData() {
|
||||||
|
|
||||||
const uniqueOffersMap = new Map();
|
const uniqueOffersMap = new Map();
|
||||||
|
|
||||||
|
const isFiroOrZcoin = (coin) => ['firo', 'zcoin'].includes(coin.toLowerCase());
|
||||||
|
const isParticlVariant = (coin) => ['particl', 'particl anon', 'particl blind'].includes(coin.toLowerCase());
|
||||||
|
|
||||||
|
const coinMatches = (offerCoin, filterCoin) => {
|
||||||
|
offerCoin = offerCoin.toLowerCase();
|
||||||
|
filterCoin = filterCoin.toLowerCase();
|
||||||
|
|
||||||
|
if (offerCoin === filterCoin) return true;
|
||||||
|
if (isFiroOrZcoin(offerCoin) && isFiroOrZcoin(filterCoin)) return true;
|
||||||
|
|
||||||
|
if (isParticlVariant(filterCoin)) {
|
||||||
|
return offerCoin === filterCoin;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filterCoin === 'particl' && isParticlVariant(offerCoin)) return true;
|
||||||
|
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
originalJsonData.forEach(offer => {
|
originalJsonData.forEach(offer => {
|
||||||
const coinFrom = (offer.coin_from || '').toLowerCase();
|
const coinFrom = (offer.coin_from || '');
|
||||||
const coinTo = (offer.coin_to || '').toLowerCase();
|
const coinTo = (offer.coin_to || '');
|
||||||
const isExpired = offer.expire_at <= currentTime;
|
const isExpired = offer.expire_at <= currentTime;
|
||||||
|
|
||||||
if (!isSentOffers && isExpired) {
|
if (!isSentOffers && isExpired) {
|
||||||
|
@ -641,10 +693,10 @@ function filterAndSortData() {
|
||||||
|
|
||||||
let passesFilter = true;
|
let passesFilter = true;
|
||||||
|
|
||||||
if (filters.coin_to !== 'any' && coinTo.toLowerCase() !== filters.coin_to.toLowerCase()) {
|
if (filters.coin_to !== 'any' && !coinMatches(coinTo, filters.coin_to)) {
|
||||||
passesFilter = false;
|
passesFilter = false;
|
||||||
}
|
}
|
||||||
if (filters.coin_from !== 'any' && coinFrom.toLowerCase() !== filters.coin_from.toLowerCase()) {
|
if (filters.coin_from !== 'any' && !coinMatches(coinFrom, filters.coin_from)) {
|
||||||
passesFilter = false;
|
passesFilter = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -747,12 +799,10 @@ function createTableRow(offer, isSentOffers) {
|
||||||
row.className = `opacity-100 text-gray-500 dark:text-gray-100 hover:bg-coolGray-200 dark:hover:bg-gray-600`;
|
row.className = `opacity-100 text-gray-500 dark:text-gray-100 hover:bg-coolGray-200 dark:hover:bg-gray-600`;
|
||||||
row.setAttribute('data-offer-id', `${offer.offer_id}_${offer.created_at}`);
|
row.setAttribute('data-offer-id', `${offer.offer_id}_${offer.created_at}`);
|
||||||
|
|
||||||
const coinFrom = offer.coin_from ? (symbolToCoinName[coinNameToSymbol[offer.coin_from]] || offer.coin_from) : 'Unknown';
|
const coinFromSymbol = coinNameToSymbol[offer.coin_from] || offer.coin_from.toLowerCase();
|
||||||
const coinTo = offer.coin_to ? (symbolToCoinName[coinNameToSymbol[offer.coin_to]] || offer.coin_to) : 'Unknown';
|
const coinToSymbol = coinNameToSymbol[offer.coin_to] || offer.coin_to.toLowerCase();
|
||||||
|
const coinFromDisplay = getDisplayName(offer.coin_from);
|
||||||
if (coinFrom === 'Unknown' || coinTo === 'Unknown') {
|
const coinToDisplay = getDisplayName(offer.coin_to);
|
||||||
console.warn(`Invalid coin data for offer ${offer.offer_id}: coinFrom=${coinFrom}, coinTo=${coinTo}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const postedTime = formatTimeAgo(offer.created_at);
|
const postedTime = formatTimeAgo(offer.created_at);
|
||||||
const expiresIn = formatTimeLeft(offer.expire_at);
|
const expiresIn = formatTimeLeft(offer.expire_at);
|
||||||
|
@ -760,7 +810,6 @@ function createTableRow(offer, isSentOffers) {
|
||||||
const currentTime = Math.floor(Date.now() / 1000);
|
const currentTime = Math.floor(Date.now() / 1000);
|
||||||
const isActuallyExpired = currentTime > offer.expire_at;
|
const isActuallyExpired = currentTime > offer.expire_at;
|
||||||
|
|
||||||
// Determine if this offer should be treated as a sent offer
|
|
||||||
const isOwnOffer = offer.is_own_offer;
|
const isOwnOffer = offer.is_own_offer;
|
||||||
|
|
||||||
const { buttonClass, buttonText } = getButtonProperties(isActuallyExpired, isSentOffers, isOwnOffer);
|
const { buttonClass, buttonText } = getButtonProperties(isActuallyExpired, isSentOffers, isOwnOffer);
|
||||||
|
@ -771,16 +820,16 @@ function createTableRow(offer, isSentOffers) {
|
||||||
row.innerHTML = `
|
row.innerHTML = `
|
||||||
${createTimeColumn(offer, postedTime, expiresIn)}
|
${createTimeColumn(offer, postedTime, expiresIn)}
|
||||||
${createDetailsColumn(offer)}
|
${createDetailsColumn(offer)}
|
||||||
${createTakerAmountColumn(offer, coinFrom, coinTo)}
|
${createTakerAmountColumn(offer, coinToDisplay, coinFromDisplay, coinFromSymbol)}
|
||||||
${createSwapColumn(offer, coinFrom, coinTo)}
|
${createSwapColumn(offer, coinToDisplay, coinFromDisplay, coinToSymbol, coinFromSymbol)}
|
||||||
${createOrderbookColumn(offer, coinTo, coinFrom)}
|
${createOrderbookColumn(offer, coinFromDisplay, coinToDisplay)}
|
||||||
${createRateColumn(offer, coinFrom, coinTo)}
|
${createRateColumn(offer, coinFromDisplay, coinToDisplay)}
|
||||||
${createPercentageColumn(offer)}
|
${createPercentageColumn(offer)}
|
||||||
${createActionColumn(offer, buttonClass, buttonText)}
|
${createActionColumn(offer, buttonClass, buttonText)}
|
||||||
${createTooltips(offer, isOwnOffer, coinFrom, coinTo, fromAmount, toAmount, postedTime, expiresIn, isActuallyExpired)}
|
${createTooltips(offer, isOwnOffer, coinFromDisplay, coinToDisplay, fromAmount, toAmount, postedTime, expiresIn, isActuallyExpired)}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
updateProfitLoss(row, coinFrom, coinTo, fromAmount, toAmount, isOwnOffer);
|
updateProfitLoss(row, coinFromDisplay, coinToDisplay, fromAmount, toAmount, isOwnOffer);
|
||||||
|
|
||||||
return row;
|
return row;
|
||||||
}
|
}
|
||||||
|
@ -966,8 +1015,16 @@ async function calculateProfitLoss(fromCoin, toCoin, fromAmount, toAmount, isOwn
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const fromSymbol = coinNameToSymbol[fromCoin] || fromCoin.toLowerCase();
|
const getPriceKey = (coin) => {
|
||||||
const toSymbol = coinNameToSymbol[toCoin] || toCoin.toLowerCase();
|
const lowerCoin = coin.toLowerCase();
|
||||||
|
if (lowerCoin === 'firo' || lowerCoin === 'zcoin') {
|
||||||
|
return 'zcoin';
|
||||||
|
}
|
||||||
|
return coinNameToSymbol[coin] || lowerCoin;
|
||||||
|
};
|
||||||
|
|
||||||
|
const fromSymbol = getPriceKey(fromCoin);
|
||||||
|
const toSymbol = getPriceKey(toCoin);
|
||||||
|
|
||||||
const fromPriceUSD = latestPrices[fromSymbol]?.usd;
|
const fromPriceUSD = latestPrices[fromSymbol]?.usd;
|
||||||
const toPriceUSD = latestPrices[toSymbol]?.usd;
|
const toPriceUSD = latestPrices[toSymbol]?.usd;
|
||||||
|
@ -993,7 +1050,6 @@ async function calculateProfitLoss(fromCoin, toCoin, fromAmount, toAmount, isOwn
|
||||||
|
|
||||||
return percentDiff;
|
return percentDiff;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getProfitColorClass(percentage) {
|
function getProfitColorClass(percentage) {
|
||||||
const numericPercentage = parseFloat(percentage);
|
const numericPercentage = parseFloat(percentage);
|
||||||
if (numericPercentage > 0) return 'text-green-500';
|
if (numericPercentage > 0) return 'text-green-500';
|
||||||
|
@ -1010,8 +1066,20 @@ function getMarketRate(fromCoin, toCoin) {
|
||||||
resolve(null);
|
resolve(null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const fromPrice = latestPrices[fromCoin.toLowerCase()]?.usd;
|
|
||||||
const toPrice = latestPrices[toCoin.toLowerCase()]?.usd;
|
const getPriceKey = (coin) => {
|
||||||
|
const lowerCoin = coin.toLowerCase();
|
||||||
|
if (lowerCoin === 'firo' || lowerCoin === 'zcoin') {
|
||||||
|
return 'zcoin';
|
||||||
|
}
|
||||||
|
return coinNameToSymbol[coin] || lowerCoin;
|
||||||
|
};
|
||||||
|
|
||||||
|
const fromSymbol = getPriceKey(fromCoin);
|
||||||
|
const toSymbol = getPriceKey(toCoin);
|
||||||
|
|
||||||
|
const fromPrice = latestPrices[fromSymbol]?.usd;
|
||||||
|
const toPrice = latestPrices[toSymbol]?.usd;
|
||||||
if (!fromPrice || !toPrice) {
|
if (!fromPrice || !toPrice) {
|
||||||
console.warn(`Missing price data for ${!fromPrice ? fromCoin : toCoin}`);
|
console.warn(`Missing price data for ${!fromPrice ? fromCoin : toCoin}`);
|
||||||
resolve(null);
|
resolve(null);
|
||||||
|
@ -1088,18 +1156,26 @@ function createTakerAmountColumn(offer, coinTo, coinFrom) {
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function createSwapColumn(offer, coinTo, coinFrom) {
|
function createSwapColumn(offer, coinToDisplay, coinFromDisplay, coinToSymbol, coinFromSymbol) {
|
||||||
|
const getImageFilename = (symbol, displayName) => {
|
||||||
|
if (displayName.toLowerCase() === 'zcoin' || displayName.toLowerCase() === 'firo') {
|
||||||
|
return 'firo.png';
|
||||||
|
}
|
||||||
|
return `${symbol.toLowerCase()}.png`;
|
||||||
|
};
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<td class="py-0 px-0 text-right text-sm">
|
<td class="py-0 px-0 text-right text-sm">
|
||||||
<a data-tooltip-target="tooltip-offer${offer.offer_id}" href="/offer/${offer.offer_id}">
|
<a data-tooltip-target="tooltip-offer${offer.offer_id}" href="/offer/${offer.offer_id}">
|
||||||
<div class="flex items-center justify-evenly monospace">
|
<div class="flex items-center justify-evenly monospace">
|
||||||
<span class="inline-flex mr-3 ml-3 align-middle items-center justify-center w-18 h-20 rounded">
|
<span class="inline-flex mr-3 ml-3 align-middle items-center justify-center w-18 h-20 rounded">
|
||||||
<img class="h-12" src="/static/images/coins/${coinFrom.replace(" ", "-")}.png" alt="${coinFrom}">
|
<img class="h-12" src="/static/images/coins/${getImageFilename(coinFromSymbol, coinFromDisplay)}" alt="${coinFromDisplay}">
|
||||||
</span>
|
</span>
|
||||||
<svg aria-hidden="true" class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
|
<svg aria-hidden="true" class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
|
||||||
<path fill-rule="evenodd " d="M12.293 5.293a1 1 0 011.414 0l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414-1.414L14.586 11H3a1 1 0 110-2h11.586l-2.293-2.293a1 1 0 010-1.414z " clip-rule="evenodd"></path></svg>
|
<path fill-rule="evenodd" d="M12.293 5.293a1 1 0 011.414 0l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414-1.414L14.586 11H3a1 1 0 110-2h11.586l-2.293-2.293a1 1 0 010-1.414z" clip-rule="evenodd"></path>
|
||||||
|
</svg>
|
||||||
<span class="inline-flex ml-3 mr-3 align-middle items-center justify-center w-18 h-20 rounded">
|
<span class="inline-flex ml-3 mr-3 align-middle items-center justify-center w-18 h-20 rounded">
|
||||||
<img class="h-12" src="/static/images/coins/${coinTo.replace(" ", "-")}.png" alt="${coinTo}">
|
<img class="h-12" src="/static/images/coins/${getImageFilename(coinToSymbol, coinToDisplay)}" alt="${coinToDisplay}">
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
|
@ -1134,8 +1210,13 @@ function createRateColumn(offer, coinFrom, coinTo) {
|
||||||
const fromSymbol = getCoinSymbol(coinFrom);
|
const fromSymbol = getCoinSymbol(coinFrom);
|
||||||
const toSymbol = getCoinSymbol(coinTo);
|
const toSymbol = getCoinSymbol(coinTo);
|
||||||
|
|
||||||
const fromPriceUSD = latestPrices[coinNameToSymbol[coinFrom]]?.usd || 0;
|
const getPriceKey = (coin) => {
|
||||||
const toPriceUSD = latestPrices[coinNameToSymbol[coinTo]]?.usd || 0;
|
const lowerCoin = coin.toLowerCase();
|
||||||
|
return lowerCoin === 'firo' || lowerCoin === 'zcoin' ? 'zcoin' : coinNameToSymbol[coin] || lowerCoin;
|
||||||
|
};
|
||||||
|
|
||||||
|
const fromPriceUSD = latestPrices[getPriceKey(coinFrom)]?.usd || 0;
|
||||||
|
const toPriceUSD = latestPrices[getPriceKey(coinTo)]?.usd || 0;
|
||||||
|
|
||||||
const rateInUSD = rate * toPriceUSD;
|
const rateInUSD = rate * toPriceUSD;
|
||||||
|
|
||||||
|
@ -1300,12 +1381,16 @@ function createTooltipContent(isSentOffers, coinFrom, coinTo, fromAmount, toAmou
|
||||||
<p>Invalid coin data.</p>`;
|
<p>Invalid coin data.</p>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure fromAmount and toAmount are numbers
|
|
||||||
fromAmount = parseFloat(fromAmount) || 0;
|
fromAmount = parseFloat(fromAmount) || 0;
|
||||||
toAmount = parseFloat(toAmount) || 0;
|
toAmount = parseFloat(toAmount) || 0;
|
||||||
|
|
||||||
const fromSymbol = coinNameToSymbol[coinFrom] || coinFrom.toLowerCase();
|
const getPriceKey = (coin) => {
|
||||||
const toSymbol = coinNameToSymbol[coinTo] || coinTo.toLowerCase();
|
const lowerCoin = coin.toLowerCase();
|
||||||
|
return lowerCoin === 'firo' || lowerCoin === 'zcoin' ? 'zcoin' : coinNameToSymbol[coin] || lowerCoin;
|
||||||
|
};
|
||||||
|
|
||||||
|
const fromSymbol = getPriceKey(coinFrom);
|
||||||
|
const toSymbol = getPriceKey(coinTo);
|
||||||
const fromPriceUSD = latestPrices[fromSymbol]?.usd;
|
const fromPriceUSD = latestPrices[fromSymbol]?.usd;
|
||||||
const toPriceUSD = latestPrices[toSymbol]?.usd;
|
const toPriceUSD = latestPrices[toSymbol]?.usd;
|
||||||
|
|
||||||
|
@ -1361,11 +1446,18 @@ function createTooltipContent(isSentOffers, coinFrom, coinTo, fromAmount, toAmou
|
||||||
<p><strong>Offer Rate:</strong> 1 ${coinFrom} = ${offerRate.toFixed(8)} ${coinTo}</p>
|
<p><strong>Offer Rate:</strong> 1 ${coinFrom} = ${offerRate.toFixed(8)} ${coinTo}</p>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function createCombinedRateTooltip(offer, coinFrom, coinTo, isSentOffers, treatAsSentOffer) {
|
function createCombinedRateTooltip(offer, coinFrom, coinTo, isSentOffers, treatAsSentOffer) {
|
||||||
const rate = parseFloat(offer.rate);
|
const rate = parseFloat(offer.rate);
|
||||||
const inverseRate = 1 / rate;
|
const inverseRate = 1 / rate;
|
||||||
const fromSymbol = getCoinSymbolLowercase(coinFrom);
|
|
||||||
const toSymbol = getCoinSymbolLowercase(coinTo);
|
const getPriceKey = (coin) => {
|
||||||
|
const lowerCoin = coin.toLowerCase();
|
||||||
|
return lowerCoin === 'firo' || lowerCoin === 'zcoin' ? 'zcoin' : getCoinSymbolLowercase(coin);
|
||||||
|
};
|
||||||
|
|
||||||
|
const fromSymbol = getPriceKey(coinFrom);
|
||||||
|
const toSymbol = getPriceKey(coinTo);
|
||||||
|
|
||||||
const fromPriceUSD = latestPrices[fromSymbol]?.usd || 0;
|
const fromPriceUSD = latestPrices[fromSymbol]?.usd || 0;
|
||||||
const toPriceUSD = latestPrices[toSymbol]?.usd || 0;
|
const toPriceUSD = latestPrices[toSymbol]?.usd || 0;
|
||||||
|
@ -1568,7 +1660,6 @@ function startRefreshCountdown() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function startCountdown() {
|
function startCountdown() {
|
||||||
// Clear any existing intervals
|
|
||||||
if (countdownInterval) {
|
if (countdownInterval) {
|
||||||
clearInterval(countdownInterval);
|
clearInterval(countdownInterval);
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,23 +2,23 @@
|
||||||
const config = {
|
const config = {
|
||||||
apiKeys: getAPIKeys(),
|
apiKeys: getAPIKeys(),
|
||||||
coins: [
|
coins: [
|
||||||
{ symbol: 'BTC', usesCryptoCompare: true, usesCoinGecko: false, historicalDays: 30 },
|
{ symbol: 'BTC', name: 'bitcoin', usesCryptoCompare: true, usesCoinGecko: true, historicalDays: 30 },
|
||||||
{ symbol: 'XMR', usesCryptoCompare: true, usesCoinGecko: false, historicalDays: 30 },
|
{ symbol: 'XMR', name: 'monero', usesCryptoCompare: true, usesCoinGecko: true, historicalDays: 30 },
|
||||||
{ symbol: 'PART', usesCryptoCompare: true, usesCoinGecko: false, historicalDays: 30 },
|
{ symbol: 'PART', name: 'particl', usesCryptoCompare: true, usesCoinGecko: true, historicalDays: 30 },
|
||||||
{ symbol: 'BCH', usesCryptoCompare: true, usesCoinGecko: false, historicalDays: 30 },
|
{ symbol: 'BCH', name: 'bitcoin-cash', usesCryptoCompare: true, usesCoinGecko: true, historicalDays: 30 },
|
||||||
{ symbol: 'PIVX', usesCryptoCompare: true, usesCoinGecko: false, historicalDays: 30 },
|
{ symbol: 'PIVX', name: 'pivx', usesCryptoCompare: true, usesCoinGecko: true, historicalDays: 30 },
|
||||||
{ symbol: 'FIRO', usesCryptoCompare: true, usesCoinGecko: false, historicalDays: 30 },
|
{ symbol: 'FIRO', name: 'zcoin', displayName: 'Firo', usesCryptoCompare: true, usesCoinGecko: true, historicalDays: 30 },
|
||||||
{ symbol: 'DASH', usesCryptoCompare: true, usesCoinGecko: false, historicalDays: 30 },
|
{ symbol: 'DASH', name: 'dash', usesCryptoCompare: true, usesCoinGecko: true, historicalDays: 30 },
|
||||||
{ symbol: 'LTC', usesCryptoCompare: true, usesCoinGecko: false, historicalDays: 30 },
|
{ symbol: 'LTC', name: 'litecoin', usesCryptoCompare: true, usesCoinGecko: true, historicalDays: 30 },
|
||||||
{ symbol: 'DOGE', usesCryptoCompare: true, usesCoinGecko: false, historicalDays: 30 },
|
{ symbol: 'DOGE', name: 'dogecoin', usesCryptoCompare: true, usesCoinGecko: true, historicalDays: 30 },
|
||||||
{ symbol: 'ETH', usesCryptoCompare: true, usesCoinGecko: false, historicalDays: 30 },
|
{ symbol: 'ETH', name: 'ethereum', usesCryptoCompare: true, usesCoinGecko: true, historicalDays: 30 },
|
||||||
{ symbol: 'DCR', usesCryptoCompare: true, usesCoinGecko: false, historicalDays: 30 },
|
{ symbol: 'DCR', name: 'decred', usesCryptoCompare: true, usesCoinGecko: true, historicalDays: 30 },
|
||||||
{ symbol: 'ZANO', usesCryptoCompare: true, usesCoinGecko: false, historicalDays: 30 },
|
{ symbol: 'ZANO', name: 'zano', usesCryptoCompare: true, usesCoinGecko: true, historicalDays: 30 },
|
||||||
{ symbol: 'WOW', usesCryptoCompare: false, usesCoinGecko: true, historicalDays: 30 }
|
{ symbol: 'WOW', name: 'wownero', usesCryptoCompare: false, usesCoinGecko: true, historicalDays: 30 }
|
||||||
],
|
],
|
||||||
apiEndpoints: {
|
apiEndpoints: {
|
||||||
cryptoCompare: 'https://min-api.cryptocompare.com/data/pricemultifull',
|
cryptoCompare: 'https://min-api.cryptocompare.com/data/pricemultifull',
|
||||||
coinGecko: 'https://api.coingecko.com/api/v3/coins',
|
coinGecko: 'https://api.coingecko.com/api/v3',
|
||||||
cryptoCompareHistorical: 'https://min-api.cryptocompare.com/data/v2/histoday'
|
cryptoCompareHistorical: 'https://min-api.cryptocompare.com/data/v2/histoday'
|
||||||
},
|
},
|
||||||
chartColors: {
|
chartColors: {
|
||||||
|
@ -28,13 +28,14 @@ const config = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
showVolume: false,
|
showVolume: false,
|
||||||
|
cacheTTL: 5 * 60 * 1000, // 5 minutes in milliseconds
|
||||||
specialCoins: [''],
|
specialCoins: [''],
|
||||||
resolutions: {
|
resolutions: {
|
||||||
month: { days: 30, interval: 'daily' },
|
year: { days: 365, interval: 'month' },
|
||||||
week: { days: 7, interval: 'daily' },
|
sixMonths: { days: 180, interval: 'daily' },
|
||||||
day: { days: 1, interval: 'hourly' }
|
day: { days: 1, interval: 'hourly' }
|
||||||
},
|
},
|
||||||
currentResolution: 'month'
|
currentResolution: 'year'
|
||||||
};
|
};
|
||||||
|
|
||||||
// Utils
|
// Utils
|
||||||
|
@ -125,98 +126,130 @@ const api = {
|
||||||
}));
|
}));
|
||||||
},
|
},
|
||||||
|
|
||||||
fetchCoinGeckoDataXHR: (coin) => {
|
fetchCoinGeckoDataXHR: async () => {
|
||||||
const coinConfig = config.coins.find(c => c.symbol === coin);
|
const cacheKey = 'coinGeckoOneLiner';
|
||||||
if (!coinConfig) {
|
let cachedData = cache.get(cacheKey);
|
||||||
logger.error(`No configuration found for coin: ${coin}`);
|
|
||||||
return Promise.reject(new AppError(`No configuration found for coin: ${coin}`));
|
if (cachedData) {
|
||||||
|
console.log('Using cached CoinGecko data');
|
||||||
|
return cachedData.value;
|
||||||
}
|
}
|
||||||
let coinId;
|
|
||||||
switch (coin) {
|
const coinIds = config.coins
|
||||||
case 'WOW':
|
.filter(coin => coin.usesCoinGecko)
|
||||||
coinId = 'wownero';
|
.map(coin => coin.name)
|
||||||
break;
|
.join(',');
|
||||||
default:
|
const url = `${config.apiEndpoints.coinGecko}/simple/price?ids=${coinIds}&vs_currencies=usd,btc&include_24hr_vol=true&include_24hr_change=true`;
|
||||||
coinId = coin.toLowerCase();
|
|
||||||
|
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 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}`);
|
const transformedData = Object.entries(data).map(([id, values]) => {
|
||||||
return api.makePostRequest(url)
|
const coinConfig = config.coins.find(coin => coin.name === id);
|
||||||
.then(data => {
|
return {
|
||||||
logger.log(`Raw CoinGecko data for ${coin}:`, data);
|
id,
|
||||||
if (!data.market_data || !data.market_data.current_price) {
|
symbol: coinConfig?.symbol.toLowerCase() || id,
|
||||||
throw new AppError(`Invalid data structure received for ${coin}`);
|
current_price: values.usd,
|
||||||
}
|
price_btc: values.btc,
|
||||||
return data;
|
total_volume: values.usd_24h_vol,
|
||||||
})
|
price_change_percentage_24h: values.usd_24h_change,
|
||||||
.catch(error => {
|
displayName: coinConfig?.displayName || coinConfig?.symbol || id
|
||||||
logger.error(`Error fetching CoinGecko data for ${coin}:`, error);
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`Transformed CoinGecko data:`, transformedData);
|
||||||
|
|
||||||
|
cache.set(cacheKey, transformedData);
|
||||||
|
|
||||||
|
return transformedData;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error fetching CoinGecko data:`, error);
|
||||||
return {
|
return {
|
||||||
error: error.message
|
error: error.message
|
||||||
};
|
};
|
||||||
});
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
fetchHistoricalDataXHR: (coinSymbol) => {
|
fetchHistoricalDataXHR: async (coinSymbols) => {
|
||||||
const coin = config.coins.find(c => c.symbol === coinSymbol);
|
if (!Array.isArray(coinSymbols)) {
|
||||||
if (!coin) {
|
coinSymbols = [coinSymbols];
|
||||||
logger.error(`No configuration found for coin: ${coinSymbol}`);
|
|
||||||
return Promise.reject(new AppError(`No configuration found for coin: ${coinSymbol}`));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log(`Fetching historical data for coins: ${coinSymbols.join(', ')}`);
|
||||||
|
|
||||||
|
const results = {};
|
||||||
|
|
||||||
|
const fetchPromises = coinSymbols.map(async coin => {
|
||||||
|
const coinConfig = config.coins.find(c => c.symbol === coin);
|
||||||
|
if (!coinConfig) {
|
||||||
|
console.error(`Coin configuration not found for ${coin}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (coin === 'WOW') {
|
||||||
|
const url = `${config.apiEndpoints.coinGecko}/coins/wownero/market_chart?vs_currency=usd&days=1`;
|
||||||
|
console.log(`CoinGecko URL for WOW: ${url}`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await api.makePostRequest(url);
|
||||||
|
if (response && response.prices) {
|
||||||
|
results[coin] = response.prices;
|
||||||
|
} else {
|
||||||
|
console.error(`Unexpected data structure for WOW:`, response);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error fetching CoinGecko data for WOW:`, error);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const resolution = config.resolutions[config.currentResolution];
|
||||||
let url;
|
let url;
|
||||||
const resolutionConfig = config.resolutions[config.currentResolution];
|
if (resolution.interval === 'hourly') {
|
||||||
|
url = `https://min-api.cryptocompare.com/data/v2/histohour?fsym=${coin}&tsym=USD&limit=${resolution.days * 24}&api_key=${config.apiKeys.cryptoCompare}`;
|
||||||
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 {
|
} else {
|
||||||
|
url = `${config.apiEndpoints.cryptoCompareHistorical}?fsym=${coin}&tsym=USD&limit=${resolution.days}&api_key=${config.apiKeys.cryptoCompare}`;
|
||||||
|
}
|
||||||
|
|
||||||
if (config.currentResolution === 'day') {
|
console.log(`CryptoCompare URL for ${coin}: ${url}`);
|
||||||
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') {
|
try {
|
||||||
url = `${config.apiEndpoints.cryptoCompareHistorical}?fsym=${coinSymbol}&tsym=USD&limit=7&aggregate=24&api_key=${config.apiKeys.cryptoCompare}`;
|
const response = await api.makePostRequest(url);
|
||||||
|
if (response.Response === "Error") {
|
||||||
|
console.error(`API Error for ${coin}:`, response.Message);
|
||||||
|
} else if (response.Data && response.Data.Data) {
|
||||||
|
results[coin] = response.Data;
|
||||||
} else {
|
} else {
|
||||||
url = `${config.apiEndpoints.cryptoCompareHistorical}?fsym=${coinSymbol}&tsym=USD&limit=30&aggregate=24&api_key=${config.apiKeys.cryptoCompare}`;
|
console.error(`Unexpected data structure for ${coin}:`, response);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error fetching CryptoCompare data for ${coin}:`, error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
|
||||||
});
|
});
|
||||||
},
|
|
||||||
|
|
||||||
|
await Promise.all(fetchPromises);
|
||||||
|
|
||||||
|
console.log('Final results object:', JSON.stringify(results, null, 2));
|
||||||
|
return results;
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// Cache
|
// Cache
|
||||||
const cache = {
|
const cache = {
|
||||||
ttl: 15 * 60 * 1000, // 15 minutes in milliseconds
|
|
||||||
set: (key, value, customTtl = null) => {
|
set: (key, value, customTtl = null) => {
|
||||||
const item = {
|
const item = {
|
||||||
value: value,
|
value: value,
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
expiresAt: Date.now() + (customTtl || cache.ttl)
|
expiresAt: Date.now() + (customTtl || app.cacheTTL)
|
||||||
};
|
};
|
||||||
localStorage.setItem(key, JSON.stringify(item));
|
localStorage.setItem(key, JSON.stringify(item));
|
||||||
|
console.log(`Cache set for ${key}, expires in ${(customTtl || app.cacheTTL) / 1000} seconds`);
|
||||||
},
|
},
|
||||||
get: (key) => {
|
get: (key) => {
|
||||||
const itemStr = localStorage.getItem(key);
|
const itemStr = localStorage.getItem(key);
|
||||||
|
@ -227,15 +260,17 @@ const cache = {
|
||||||
const item = JSON.parse(itemStr);
|
const item = JSON.parse(itemStr);
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
if (now < item.expiresAt) {
|
if (now < item.expiresAt) {
|
||||||
|
console.log(`Cache hit for ${key}, ${(item.expiresAt - now) / 1000} seconds remaining`);
|
||||||
return {
|
return {
|
||||||
value: item.value,
|
value: item.value,
|
||||||
remainingTime: item.expiresAt - now
|
remainingTime: item.expiresAt - now
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
|
console.log(`Cache expired for ${key}`);
|
||||||
localStorage.removeItem(key);
|
localStorage.removeItem(key);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error('Error parsing cache item:', e);
|
console.error('Error parsing cache item:', e);
|
||||||
localStorage.removeItem(key);
|
localStorage.removeItem(key);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
@ -245,10 +280,11 @@ const cache = {
|
||||||
},
|
},
|
||||||
clear: () => {
|
clear: () => {
|
||||||
Object.keys(localStorage).forEach(key => {
|
Object.keys(localStorage).forEach(key => {
|
||||||
if (key.startsWith('coinData_') || key.startsWith('chartData_')) {
|
if (key.startsWith('coinData_') || key.startsWith('chartData_') || key === 'coinGeckoOneLiner') {
|
||||||
localStorage.removeItem(key);
|
localStorage.removeItem(key);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
console.log('Cache cleared');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -280,29 +316,20 @@ const ui = {
|
||||||
if (data.error) {
|
if (data.error) {
|
||||||
throw new Error(data.error);
|
throw new Error(data.error);
|
||||||
}
|
}
|
||||||
if (coinConfig.usesCoinGecko) {
|
if (!data || !data.current_price) {
|
||||||
if (!data.market_data) {
|
|
||||||
throw new Error(`Invalid CoinGecko data structure for ${coin}`);
|
throw new Error(`Invalid CoinGecko data structure for ${coin}`);
|
||||||
}
|
}
|
||||||
priceUSD = data.market_data.current_price.usd;
|
priceUSD = data.current_price;
|
||||||
priceBTC = data.market_data.current_price.btc;
|
priceBTC = data.current_price / app.btcPriceUSD;
|
||||||
priceChange1d = data.market_data.price_change_percentage_24h;
|
priceChange1d = data.price_change_percentage_24h;
|
||||||
volume24h = data.market_data.total_volume.usd;
|
volume24h = data.total_volume;
|
||||||
} 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)) {
|
if (isNaN(priceUSD) || isNaN(priceBTC) || isNaN(volume24h)) {
|
||||||
throw new Error(`Invalid numeric values in data for ${coin}`);
|
throw new Error(`Invalid numeric values in data for ${coin}`);
|
||||||
}
|
}
|
||||||
updateUI(false);
|
updateUI(false);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`Error displaying data for ${coin}:`, error.message);
|
console.error(`Error displaying data for ${coin}:`, error.message);
|
||||||
updateUI(true);
|
updateUI(true);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -494,8 +521,8 @@ initChart: () => {
|
||||||
backgroundColor: gradient,
|
backgroundColor: gradient,
|
||||||
tension: 0.4,
|
tension: 0.4,
|
||||||
fill: true,
|
fill: true,
|
||||||
borderWidth: 3,
|
pointRadius: 2,
|
||||||
pointRadius: 0,
|
pointHoverRadius: 4,
|
||||||
}]
|
}]
|
||||||
},
|
},
|
||||||
options: {
|
options: {
|
||||||
|
@ -584,10 +611,11 @@ initChart: () => {
|
||||||
},
|
},
|
||||||
elements: {
|
elements: {
|
||||||
point: {
|
point: {
|
||||||
backgroundColor: 'transparent',
|
backgroundColor: 'rgba(77, 132, 240, 1)',
|
||||||
borderColor: 'rgba(77, 132, 240, 1)',
|
borderColor: 'rgba(77, 132, 240, 1)',
|
||||||
borderWidth: 2,
|
borderWidth: 1,
|
||||||
radius: 0,
|
radius: 2,
|
||||||
|
hoverRadius: 4,
|
||||||
hoverRadius: 4,
|
hoverRadius: 4,
|
||||||
hitRadius: 6,
|
hitRadius: 6,
|
||||||
hoverBorderWidth: 2
|
hoverBorderWidth: 2
|
||||||
|
@ -607,43 +635,33 @@ initChart: () => {
|
||||||
|
|
||||||
prepareChartData: (coinSymbol, data) => {
|
prepareChartData: (coinSymbol, data) => {
|
||||||
console.log(`Preparing chart data for ${coinSymbol}:`, JSON.stringify(data, null, 2));
|
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) {
|
if (!data) {
|
||||||
console.error(`Invalid data received for ${coinSymbol}:`, data);
|
console.error(`No data received for ${coinSymbol}`);
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let preparedData;
|
let preparedData;
|
||||||
if (coin.usesCoinGecko) {
|
|
||||||
if (!data.prices || !Array.isArray(data.prices)) {
|
if (data.Data && Array.isArray(data.Data)) {
|
||||||
throw new Error(`Invalid CoinGecko data structure for ${coinSymbol}`);
|
preparedData = data.Data.map(d => ({
|
||||||
}
|
x: new Date(d.time * 1000),
|
||||||
preparedData = data.prices.map(entry => ({
|
y: d.close
|
||||||
x: new Date(entry[0]),
|
|
||||||
y: entry[1]
|
|
||||||
}));
|
}));
|
||||||
|
} else if (data.Data && data.Data.Data && Array.isArray(data.Data.Data)) {
|
||||||
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 => ({
|
preparedData = data.Data.Data.map(d => ({
|
||||||
x: new Date(d.time * 1000),
|
x: new Date(d.time * 1000),
|
||||||
y: d.close
|
y: d.close
|
||||||
}));
|
}));
|
||||||
}
|
} else if (Array.isArray(data)) {
|
||||||
|
preparedData = data.map(([timestamp, price]) => ({
|
||||||
const expectedDataPoints = config.currentResolution === 'day' ? 24 : config.resolutions[config.currentResolution].days;
|
x: new Date(timestamp),
|
||||||
if (preparedData.length < expectedDataPoints) {
|
y: price
|
||||||
console.warn(`Insufficient data points for ${coinSymbol}. Expected ${expectedDataPoints}, got ${preparedData.length}`);
|
}));
|
||||||
|
} else {
|
||||||
|
console.error(`Unexpected data structure for ${coinSymbol}:`, data);
|
||||||
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`Prepared data for ${coinSymbol}:`, preparedData.slice(0, 5));
|
console.log(`Prepared data for ${coinSymbol}:`, preparedData.slice(0, 5));
|
||||||
|
@ -680,115 +698,73 @@ ensureHourlyData: (data) => {
|
||||||
chartModule.loadStartTime = Date.now();
|
chartModule.loadStartTime = Date.now();
|
||||||
|
|
||||||
const cacheKey = `chartData_${coinSymbol}_${config.currentResolution}`;
|
const cacheKey = `chartData_${coinSymbol}_${config.currentResolution}`;
|
||||||
const cacheDuration = 15 * 60 * 1000; // 15 minutes in milliseconds
|
|
||||||
let cachedData = !forceRefresh ? cache.get(cacheKey) : null;
|
let cachedData = !forceRefresh ? cache.get(cacheKey) : null;
|
||||||
let data;
|
let data;
|
||||||
|
|
||||||
if (cachedData) {
|
if (cachedData && Object.keys(cachedData.value).length > 0) {
|
||||||
data = cachedData.value;
|
data = cachedData.value;
|
||||||
console.log(`Using cached data for ${coinSymbol} (${config.currentResolution})`);
|
console.log(`Using cached data for ${coinSymbol} (${config.currentResolution})`);
|
||||||
} else {
|
} else {
|
||||||
console.log(`Fetching fresh data for ${coinSymbol} (${config.currentResolution})`);
|
console.log(`Fetching fresh data for ${coinSymbol} (${config.currentResolution})`);
|
||||||
data = await api.fetchHistoricalDataXHR(coinSymbol);
|
const allData = await api.fetchHistoricalDataXHR([coinSymbol]);
|
||||||
if (data.error) {
|
data = allData[coinSymbol];
|
||||||
throw new Error(data.error);
|
if (!data || Object.keys(data).length === 0) {
|
||||||
|
throw new Error(`No data returned for ${coinSymbol}`);
|
||||||
}
|
}
|
||||||
cache.set(cacheKey, data, cacheDuration);
|
console.log(`Caching new data for ${cacheKey}`);
|
||||||
|
cache.set(cacheKey, data, config.cacheTTL);
|
||||||
cachedData = null;
|
cachedData = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const chartData = chartModule.prepareChartData(coinSymbol, data);
|
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}`);
|
||||||
|
}
|
||||||
|
|
||||||
if (chartModule.chart) {
|
if (chartModule.chart) {
|
||||||
chartModule.chart.data.datasets[0].data = chartData;
|
chartModule.chart.data.datasets[0].data = chartData;
|
||||||
chartModule.chart.data.datasets[0].label = `${coinSymbol} Price (USD)`;
|
chartModule.chart.data.datasets[0].label = `${coinSymbol} Price (USD)`;
|
||||||
|
|
||||||
const coin = config.coins.find(c => c.symbol === coinSymbol);
|
// Special handling for Wownero
|
||||||
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') {
|
if (coinSymbol === 'WOW') {
|
||||||
chartModule.chart.options.scales.y.ticks.callback = (value) => {
|
chartModule.chart.options.scales.x.time.unit = 'hour';
|
||||||
return '$' + value.toFixed(4);
|
chartModule.chart.options.scales.x.ticks.maxTicksLimit = 24;
|
||||||
};
|
chartModule.chart.options.plugins.tooltip.callbacks.title = (tooltipItems) => {
|
||||||
|
const date = new Date(tooltipItems[0].parsed.x);
|
||||||
chartModule.chart.options.plugins.tooltip.callbacks.label = (context) => {
|
return date.toLocaleString('en-US', { month: 'short', day: 'numeric', hour: 'numeric', minute: 'numeric', hour12: true, timeZone: 'UTC' });
|
||||||
let label = context.dataset.label || '';
|
|
||||||
if (label) {
|
|
||||||
label += ': ';
|
|
||||||
}
|
|
||||||
if (context.parsed.y !== null) {
|
|
||||||
label += '$' + context.parsed.y.toFixed(4);
|
|
||||||
}
|
|
||||||
return label;
|
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
chartModule.chart.options.scales.y.ticks.callback = (value) => {
|
const resolution = config.resolutions[config.currentResolution] || config.resolutions.year;
|
||||||
return '$' + ui.formatPrice(coinSymbol, value);
|
chartModule.chart.options.scales.x.time.unit = resolution.interval === 'hourly' ? 'hour' : 'day';
|
||||||
};
|
|
||||||
|
|
||||||
chartModule.chart.options.plugins.tooltip.callbacks.label = (context) => {
|
if (config.currentResolution === 'year' || config.currentResolution === 'sixMonths') {
|
||||||
let label = context.dataset.label || '';
|
chartModule.chart.options.scales.x.time.unit = 'month';
|
||||||
if (label) {
|
|
||||||
label += ': ';
|
|
||||||
}
|
|
||||||
if (context.parsed.y !== null) {
|
|
||||||
label += '$' + ui.formatPrice(coinSymbol, context.parsed.y);
|
|
||||||
}
|
|
||||||
return label;
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (config.currentResolution === 'day') {
|
if (config.currentResolution === 'year') {
|
||||||
chartModule.chart.options.scales.x = {
|
chartModule.chart.options.scales.x.ticks.maxTicksLimit = 12; // One tick per month
|
||||||
type: 'time',
|
} else if (config.currentResolution === 'sixMonths') {
|
||||||
time: {
|
chartModule.chart.options.scales.x.ticks.maxTicksLimit = 6; // One tick every month
|
||||||
unit: 'hour',
|
} else if (config.currentResolution === 'day') {
|
||||||
displayFormats: {
|
chartModule.chart.options.scales.x.ticks.maxTicksLimit = 24; // One tick every hour
|
||||||
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';
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
};
|
chartModule.chart.options.plugins.tooltip.callbacks.title = (tooltipItems) => {
|
||||||
} else {
|
const date = new Date(tooltipItems[0].parsed.x);
|
||||||
chartModule.chart.options.scales.x = {
|
if (config.currentResolution === 'year' || config.currentResolution === 'sixMonths') {
|
||||||
type: 'time',
|
return date.toLocaleString('en-US', { year: 'numeric', month: 'short', day: 'numeric', timeZone: 'UTC' });
|
||||||
time: {
|
} else if (config.currentResolution === 'day') {
|
||||||
unit: 'day',
|
return date.toLocaleString('en-US', { month: 'short', day: 'numeric', hour: 'numeric', minute: 'numeric', hour12: true, timeZone: 'UTC' });
|
||||||
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');
|
chartModule.chart.update('active');
|
||||||
} else {
|
} else {
|
||||||
console.error('Chart object not initialized');
|
console.error('Chart object not initialized');
|
||||||
|
throw new Error('Chart object not initialized');
|
||||||
}
|
}
|
||||||
|
|
||||||
chartModule.currentCoin = coinSymbol;
|
chartModule.currentCoin = coinSymbol;
|
||||||
|
@ -797,11 +773,7 @@ ensureHourlyData: (data) => {
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Error updating chart for ${coinSymbol}:`, error);
|
console.error(`Error updating chart for ${coinSymbol}:`, error);
|
||||||
let errorMessage = `Failed to update chart for ${coinSymbol}`;
|
ui.displayErrorMessage(`Failed to update chart for ${coinSymbol}: ${error.message}`);
|
||||||
if (error.message) {
|
|
||||||
errorMessage += `: ${error.message}`;
|
|
||||||
}
|
|
||||||
ui.displayErrorMessage(errorMessage);
|
|
||||||
} finally {
|
} finally {
|
||||||
chartModule.hideChartLoader();
|
chartModule.hideChartLoader();
|
||||||
}
|
}
|
||||||
|
@ -858,20 +830,26 @@ const app = {
|
||||||
autoRefreshInterval: null,
|
autoRefreshInterval: null,
|
||||||
nextRefreshTime: null,
|
nextRefreshTime: null,
|
||||||
lastRefreshedTime: null,
|
lastRefreshedTime: null,
|
||||||
|
isRefreshing: false,
|
||||||
isAutoRefreshEnabled: localStorage.getItem('autoRefreshEnabled') !== 'false',
|
isAutoRefreshEnabled: localStorage.getItem('autoRefreshEnabled') !== 'false',
|
||||||
refreshTexts: {
|
refreshTexts: {
|
||||||
label: 'Auto-refresh in',
|
label: 'Auto-refresh in',
|
||||||
disabled: 'Auto-refresh: disabled',
|
disabled: 'Auto-refresh: disabled',
|
||||||
justRefreshed: 'Just refreshed',
|
justRefreshed: 'Just refreshed',
|
||||||
},
|
},
|
||||||
|
cacheTTL: 15 * 60 * 1000, // 15 minutes in milliseconds
|
||||||
|
minimumRefreshInterval: 60 * 1000, // 1 minute in milliseconds
|
||||||
|
|
||||||
init: () => {
|
init: () => {
|
||||||
|
console.log('Initializing app...');
|
||||||
window.addEventListener('load', app.onLoad);
|
window.addEventListener('load', app.onLoad);
|
||||||
app.loadLastRefreshedTime();
|
app.loadLastRefreshedTime();
|
||||||
app.updateAutoRefreshButton();
|
app.updateAutoRefreshButton();
|
||||||
|
console.log('App initialized');
|
||||||
},
|
},
|
||||||
|
|
||||||
onLoad: async () => {
|
onLoad: async () => {
|
||||||
|
console.log('App onLoad event triggered');
|
||||||
ui.showLoader();
|
ui.showLoader();
|
||||||
try {
|
try {
|
||||||
volumeToggle.init();
|
volumeToggle.init();
|
||||||
|
@ -884,17 +862,17 @@ const app = {
|
||||||
console.warn('Chart container not found, skipping chart initialization');
|
console.warn('Chart container not found, skipping chart initialization');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load all coin data immediately
|
console.log('Loading all coin data...');
|
||||||
await app.loadAllCoinData();
|
await app.loadAllCoinData();
|
||||||
|
|
||||||
if (chartModule.chart) {
|
if (chartModule.chart) {
|
||||||
config.currentResolution = 'month';
|
config.currentResolution = 'day';
|
||||||
await chartModule.updateChart('BTC');
|
await chartModule.updateChart('BTC');
|
||||||
app.updateResolutionButtons('BTC');
|
app.updateResolutionButtons('BTC');
|
||||||
}
|
}
|
||||||
ui.setActiveContainer('btc-container');
|
ui.setActiveContainer('btc-container');
|
||||||
|
|
||||||
// Set up event listeners and other initializations
|
console.log('Setting up event listeners and initializations...');
|
||||||
app.setupEventListeners();
|
app.setupEventListeners();
|
||||||
app.initializeSelectImages();
|
app.initializeSelectImages();
|
||||||
app.initAutoRefresh();
|
app.initAutoRefresh();
|
||||||
|
@ -907,20 +885,43 @@ const app = {
|
||||||
if (chartModule.chart) {
|
if (chartModule.chart) {
|
||||||
chartModule.hideChartLoader();
|
chartModule.hideChartLoader();
|
||||||
}
|
}
|
||||||
|
console.log('App onLoad completed');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
loadAllCoinData: async () => {
|
loadAllCoinData: async () => {
|
||||||
for (const coin of config.coins) {
|
console.log('Loading data for all coins...');
|
||||||
await app.loadCoinData(coin);
|
try {
|
||||||
|
const allCoinData = await api.fetchCoinGeckoDataXHR();
|
||||||
|
if (allCoinData.error) {
|
||||||
|
throw new Error(allCoinData.error);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (const coin of config.coins) {
|
||||||
|
const coinData = allCoinData.find(data => data.symbol.toUpperCase() === coin.symbol);
|
||||||
|
if (coinData) {
|
||||||
|
coinData.displayName = coin.displayName || coin.symbol;
|
||||||
|
ui.displayCoinData(coin.symbol, coinData);
|
||||||
|
const cacheKey = `coinData_${coin.symbol}`;
|
||||||
|
cache.set(cacheKey, coinData);
|
||||||
|
} else {
|
||||||
|
console.error(`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.');
|
||||||
|
}
|
||||||
|
console.log('All coin data loaded');
|
||||||
},
|
},
|
||||||
|
|
||||||
loadCoinData: async (coin) => {
|
loadCoinData: async (coin) => {
|
||||||
|
console.log(`Loading data for ${coin.symbol}...`);
|
||||||
const cacheKey = `coinData_${coin.symbol}`;
|
const cacheKey = `coinData_${coin.symbol}`;
|
||||||
let cachedData = cache.get(cacheKey);
|
let cachedData = cache.get(cacheKey);
|
||||||
let data;
|
let data;
|
||||||
if (cachedData) {
|
if (cachedData) {
|
||||||
|
console.log(`Using cached data for ${coin.symbol}`);
|
||||||
data = cachedData.value;
|
data = cachedData.value;
|
||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
|
@ -933,6 +934,7 @@ const app = {
|
||||||
if (data.error) {
|
if (data.error) {
|
||||||
throw new Error(data.error);
|
throw new Error(data.error);
|
||||||
}
|
}
|
||||||
|
console.log(`Caching new data for ${coin.symbol}`);
|
||||||
cache.set(cacheKey, data);
|
cache.set(cacheKey, data);
|
||||||
cachedData = null;
|
cachedData = null;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
@ -946,13 +948,16 @@ const app = {
|
||||||
}
|
}
|
||||||
ui.displayCoinData(coin.symbol, data);
|
ui.displayCoinData(coin.symbol, data);
|
||||||
ui.updateLoadTimeAndCache(0, cachedData);
|
ui.updateLoadTimeAndCache(0, cachedData);
|
||||||
|
console.log(`Data loaded for ${coin.symbol}`);
|
||||||
},
|
},
|
||||||
|
|
||||||
setupEventListeners: () => {
|
setupEventListeners: () => {
|
||||||
|
console.log('Setting up event listeners...');
|
||||||
config.coins.forEach(coin => {
|
config.coins.forEach(coin => {
|
||||||
const container = document.getElementById(`${coin.symbol.toLowerCase()}-container`);
|
const container = document.getElementById(`${coin.symbol.toLowerCase()}-container`);
|
||||||
if (container) {
|
if (container) {
|
||||||
container.addEventListener('click', () => {
|
container.addEventListener('click', () => {
|
||||||
|
console.log(`${coin.symbol} container clicked`);
|
||||||
ui.setActiveContainer(`${coin.symbol.toLowerCase()}-container`);
|
ui.setActiveContainer(`${coin.symbol.toLowerCase()}-container`);
|
||||||
if (chartModule.chart) {
|
if (chartModule.chart) {
|
||||||
if (coin.symbol === 'WOW') {
|
if (coin.symbol === 'WOW') {
|
||||||
|
@ -979,9 +984,11 @@ const app = {
|
||||||
if (closeErrorButton) {
|
if (closeErrorButton) {
|
||||||
closeErrorButton.addEventListener('click', ui.hideErrorMessage);
|
closeErrorButton.addEventListener('click', ui.hideErrorMessage);
|
||||||
}
|
}
|
||||||
|
console.log('Event listeners set up');
|
||||||
},
|
},
|
||||||
|
|
||||||
initAutoRefresh: () => {
|
initAutoRefresh: () => {
|
||||||
|
console.log('Initializing auto-refresh...');
|
||||||
const toggleAutoRefreshButton = document.getElementById('toggle-auto-refresh');
|
const toggleAutoRefreshButton = document.getElementById('toggle-auto-refresh');
|
||||||
if (toggleAutoRefreshButton) {
|
if (toggleAutoRefreshButton) {
|
||||||
toggleAutoRefreshButton.addEventListener('click', app.toggleAutoRefresh);
|
toggleAutoRefreshButton.addEventListener('click', app.toggleAutoRefresh);
|
||||||
|
@ -989,73 +996,105 @@ const app = {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (app.isAutoRefreshEnabled) {
|
if (app.isAutoRefreshEnabled) {
|
||||||
const storedNextRefreshTime = localStorage.getItem('nextRefreshTime');
|
console.log('Auto-refresh is enabled, scheduling next refresh');
|
||||||
if (storedNextRefreshTime) {
|
app.scheduleNextRefresh();
|
||||||
const nextRefreshTime = parseInt(storedNextRefreshTime);
|
|
||||||
if (nextRefreshTime > Date.now()) {
|
|
||||||
app.nextRefreshTime = nextRefreshTime;
|
|
||||||
app.startAutoRefresh();
|
|
||||||
} else {
|
} else {
|
||||||
app.startAutoRefresh(true);
|
console.log('Auto-refresh is disabled');
|
||||||
}
|
|
||||||
} else {
|
|
||||||
app.startAutoRefresh(true);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
startAutoRefresh: (resetTimer = false) => {
|
scheduleNextRefresh: () => {
|
||||||
app.stopAutoRefresh();
|
console.log('Scheduling next refresh...');
|
||||||
|
if (app.autoRefreshInterval) {
|
||||||
if (resetTimer || !app.nextRefreshTime) {
|
clearTimeout(app.autoRefreshInterval);
|
||||||
app.nextRefreshTime = Date.now() + cache.ttl;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const timeUntilNextRefresh = Math.max(0, app.nextRefreshTime - Date.now());
|
const now = Date.now();
|
||||||
|
let earliestExpiration = Infinity;
|
||||||
|
|
||||||
if (timeUntilNextRefresh === 0) {
|
Object.keys(localStorage).forEach(key => {
|
||||||
app.nextRefreshTime = Date.now() + cache.ttl;
|
if (key.startsWith('coinData_') || key.startsWith('chartData_') || key === 'coinGeckoOneLiner') {
|
||||||
|
try {
|
||||||
|
const cachedItem = JSON.parse(localStorage.getItem(key));
|
||||||
|
if (cachedItem && cachedItem.expiresAt) {
|
||||||
|
earliestExpiration = Math.min(earliestExpiration, cachedItem.expiresAt);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error parsing cached item ${key}:`, error);
|
||||||
|
// Remove corrupted cache item
|
||||||
|
localStorage.removeItem(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let nextRefreshTime;
|
||||||
|
if (earliestExpiration !== Infinity) {
|
||||||
|
nextRefreshTime = Math.max(earliestExpiration, now + app.minimumRefreshInterval);
|
||||||
|
} else {
|
||||||
|
nextRefreshTime = now + config.cacheTTL;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const timeUntilRefresh = nextRefreshTime - now;
|
||||||
|
console.log(`Next refresh scheduled in ${timeUntilRefresh / 1000} seconds`);
|
||||||
|
|
||||||
|
app.nextRefreshTime = nextRefreshTime;
|
||||||
app.autoRefreshInterval = setTimeout(() => {
|
app.autoRefreshInterval = setTimeout(() => {
|
||||||
|
console.log('Auto-refresh triggered');
|
||||||
app.refreshAllData();
|
app.refreshAllData();
|
||||||
app.startAutoRefresh(true);
|
}, timeUntilRefresh);
|
||||||
}, timeUntilNextRefresh);
|
|
||||||
|
|
||||||
localStorage.setItem('nextRefreshTime', app.nextRefreshTime.toString());
|
localStorage.setItem('nextRefreshTime', app.nextRefreshTime.toString());
|
||||||
app.updateNextRefreshTime();
|
app.updateNextRefreshTime();
|
||||||
app.isAutoRefreshEnabled = true;
|
|
||||||
localStorage.setItem('autoRefreshEnabled', 'true');
|
|
||||||
},
|
},
|
||||||
|
|
||||||
stopAutoRefresh: () => {
|
refreshAllData: async () => {
|
||||||
if (app.autoRefreshInterval) {
|
if (app.isRefreshing) {
|
||||||
clearTimeout(app.autoRefreshInterval);
|
console.log('Refresh already in progress, skipping...');
|
||||||
app.autoRefreshInterval = null;
|
return;
|
||||||
}
|
}
|
||||||
app.nextRefreshTime = null;
|
|
||||||
localStorage.removeItem('nextRefreshTime');
|
|
||||||
app.updateNextRefreshTime();
|
|
||||||
app.isAutoRefreshEnabled = false;
|
|
||||||
localStorage.setItem('autoRefreshEnabled', 'false');
|
|
||||||
},
|
|
||||||
|
|
||||||
toggleAutoRefresh: () => {
|
console.log('Refreshing all data...');
|
||||||
|
app.isRefreshing = true;
|
||||||
|
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();
|
||||||
|
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) {
|
if (app.isAutoRefreshEnabled) {
|
||||||
app.stopAutoRefresh();
|
app.scheduleNextRefresh();
|
||||||
} else {
|
}
|
||||||
app.startAutoRefresh();
|
|
||||||
}
|
}
|
||||||
app.updateAutoRefreshButton();
|
|
||||||
},
|
},
|
||||||
|
|
||||||
updateNextRefreshTime: () => {
|
updateNextRefreshTime: () => {
|
||||||
|
console.log('Updating next refresh time display');
|
||||||
const nextRefreshSpan = document.getElementById('next-refresh-time');
|
const nextRefreshSpan = document.getElementById('next-refresh-time');
|
||||||
const labelElement = document.getElementById('next-refresh-label');
|
const labelElement = document.getElementById('next-refresh-label');
|
||||||
const valueElement = document.getElementById('next-refresh-value');
|
const valueElement = document.getElementById('next-refresh-value');
|
||||||
|
|
||||||
if (nextRefreshSpan && labelElement && valueElement) {
|
if (nextRefreshSpan && labelElement && valueElement) {
|
||||||
if (app.nextRefreshTime) {
|
if (app.nextRefreshTime) {
|
||||||
|
if (app.updateNextRefreshTimeRAF) {
|
||||||
|
cancelAnimationFrame(app.updateNextRefreshTimeRAF);
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateDisplay = () => {
|
||||||
const timeUntilRefresh = Math.max(0, Math.ceil((app.nextRefreshTime - Date.now()) / 1000));
|
const timeUntilRefresh = Math.max(0, Math.ceil((app.nextRefreshTime - Date.now()) / 1000));
|
||||||
|
|
||||||
if (timeUntilRefresh === 0) {
|
if (timeUntilRefresh === 0) {
|
||||||
|
@ -1069,8 +1108,10 @@ const app = {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (timeUntilRefresh > 0) {
|
if (timeUntilRefresh > 0) {
|
||||||
setTimeout(app.updateNextRefreshTime, 1000);
|
app.updateNextRefreshTimeRAF = requestAnimationFrame(updateDisplay);
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
updateDisplay();
|
||||||
} else {
|
} else {
|
||||||
labelElement.textContent = '';
|
labelElement.textContent = '';
|
||||||
valueElement.textContent = app.refreshTexts.disabled;
|
valueElement.textContent = app.refreshTexts.disabled;
|
||||||
|
@ -1079,6 +1120,7 @@ const app = {
|
||||||
},
|
},
|
||||||
|
|
||||||
updateAutoRefreshButton: () => {
|
updateAutoRefreshButton: () => {
|
||||||
|
console.log('Updating auto-refresh button state');
|
||||||
const button = document.getElementById('toggle-auto-refresh');
|
const button = document.getElementById('toggle-auto-refresh');
|
||||||
if (button) {
|
if (button) {
|
||||||
if (app.isAutoRefreshEnabled) {
|
if (app.isAutoRefreshEnabled) {
|
||||||
|
@ -1095,6 +1137,7 @@ const app = {
|
||||||
},
|
},
|
||||||
|
|
||||||
startSpinAnimation: () => {
|
startSpinAnimation: () => {
|
||||||
|
console.log('Starting spin animation on auto-refresh button');
|
||||||
const svg = document.querySelector('#toggle-auto-refresh svg');
|
const svg = document.querySelector('#toggle-auto-refresh svg');
|
||||||
if (svg) {
|
if (svg) {
|
||||||
svg.classList.add('animate-spin');
|
svg.classList.add('animate-spin');
|
||||||
|
@ -1105,37 +1148,15 @@ const app = {
|
||||||
},
|
},
|
||||||
|
|
||||||
stopSpinAnimation: () => {
|
stopSpinAnimation: () => {
|
||||||
|
console.log('Stopping spin animation on auto-refresh button');
|
||||||
const svg = document.querySelector('#toggle-auto-refresh svg');
|
const svg = document.querySelector('#toggle-auto-refresh svg');
|
||||||
if (svg) {
|
if (svg) {
|
||||||
svg.classList.remove('animate-spin');
|
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: () => {
|
updateLastRefreshedTime: () => {
|
||||||
|
console.log('Updating last refreshed time');
|
||||||
const lastRefreshedElement = document.getElementById('last-refreshed-time');
|
const lastRefreshedElement = document.getElementById('last-refreshed-time');
|
||||||
if (lastRefreshedElement && app.lastRefreshedTime) {
|
if (lastRefreshedElement && app.lastRefreshedTime) {
|
||||||
const formattedTime = app.lastRefreshedTime.toLocaleTimeString();
|
const formattedTime = app.lastRefreshedTime.toLocaleTimeString();
|
||||||
|
@ -1144,6 +1165,7 @@ const app = {
|
||||||
},
|
},
|
||||||
|
|
||||||
loadLastRefreshedTime: () => {
|
loadLastRefreshedTime: () => {
|
||||||
|
console.log('Loading last refreshed time from storage');
|
||||||
const storedTime = localStorage.getItem('lastRefreshedTime');
|
const storedTime = localStorage.getItem('lastRefreshedTime');
|
||||||
if (storedTime) {
|
if (storedTime) {
|
||||||
app.lastRefreshedTime = new Date(parseInt(storedTime));
|
app.lastRefreshedTime = new Date(parseInt(storedTime));
|
||||||
|
@ -1152,13 +1174,14 @@ const app = {
|
||||||
},
|
},
|
||||||
|
|
||||||
updateBTCPrice: async () => {
|
updateBTCPrice: async () => {
|
||||||
|
console.log('Updating BTC price...');
|
||||||
try {
|
try {
|
||||||
const btcData = await api.fetchCryptoCompareDataXHR('BTC');
|
const btcData = await api.fetchCoinGeckoDataXHR('bitcoin');
|
||||||
if (btcData.error) {
|
if (btcData.error) {
|
||||||
console.error('Error fetching BTC price:', btcData.error);
|
console.error('Error fetching BTC price:', btcData.error);
|
||||||
app.btcPriceUSD = 0;
|
app.btcPriceUSD = 0;
|
||||||
} else if (btcData.RAW && btcData.RAW.BTC && btcData.RAW.BTC.USD) {
|
} else if (btcData[0] && btcData[0].current_price) {
|
||||||
app.btcPriceUSD = btcData.RAW.BTC.USD.PRICE;
|
app.btcPriceUSD = btcData[0].current_price;
|
||||||
} else {
|
} else {
|
||||||
console.error('Unexpected BTC data structure:', btcData);
|
console.error('Unexpected BTC data structure:', btcData);
|
||||||
app.btcPriceUSD = 0;
|
app.btcPriceUSD = 0;
|
||||||
|
@ -1326,6 +1349,26 @@ sortTable: (columnIndex) => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
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();
|
||||||
|
} else {
|
||||||
|
console.log('Auto-refresh disabled, clearing interval');
|
||||||
|
if (app.autoRefreshInterval) {
|
||||||
|
clearTimeout(app.autoRefreshInterval);
|
||||||
|
app.autoRefreshInterval = null;
|
||||||
|
}
|
||||||
|
app.nextRefreshTime = null;
|
||||||
|
localStorage.removeItem('nextRefreshTime');
|
||||||
|
}
|
||||||
|
app.updateAutoRefreshButton();
|
||||||
|
app.updateNextRefreshTime();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const resolutionButtons = document.querySelectorAll('.resolution-button');
|
const resolutionButtons = document.querySelectorAll('.resolution-button');
|
||||||
|
|
|
@ -74,15 +74,15 @@ function getAPIKeys() {
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<h2 class="text-xl font-bold dark:text-white" id="chart-title">Price Chart</h2>
|
<h2 class="text-xl font-bold dark:text-white" id="chart-title">Price Chart</h2>
|
||||||
<div class="flex items-center space-x-4">
|
<div class="flex items-center space-x-4">
|
||||||
<button id="resolution-month" class="ml-5 resolution-button">1Y</button>
|
<button id="resolution-year" class="ml-5 resolution-button">1Y</button>
|
||||||
<button id="resolution-week" class="resolution-button">1M</button>
|
<button id="resolution-sixMonths" class="resolution-button">6M</button>
|
||||||
<button id="resolution-day" class="resolution-button">24H</button>
|
<button id="resolution-day" class="resolution-button">24H</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<span id="load-time" class="mr-2 text-sm text-gray-600 dark:text-gray-300"></span>
|
<span id="load-time hidden" class="mr-2 text-sm text-gray-600 dark:text-gray-300"></span>
|
||||||
<span id="last-refreshed-time" class="mr-2 text-sm text-gray-600 dark:text-gray-300"></span>
|
<span id="last-refreshed-time hidden" class="mr-2 text-sm text-gray-600 dark:text-gray-300"></span>
|
||||||
<span id="cache-status" class="mr-4 text-sm text-gray-600 dark:text-gray-300"></span>
|
<span id="cache-status hidden" class="mr-4 text-sm text-gray-600 dark:text-gray-300"></span>
|
||||||
<span id="tor-status" class="mr-4 text-sm {% if tor_established %}text-green-500{% else %}text-red-500{% endif %}"> Tor <> API: {% if tor_established %}Connected{% else %}Not Connected{% endif %}
|
<span id="tor-status" class="mr-4 text-sm {% if tor_established %}text-green-500{% else %}text-red-500{% endif %}"> Tor <> API: {% if tor_established %}Connected{% else %}Not Connected{% endif %}
|
||||||
<a href="https://academy.particl.io/en/latest/basicswap-guides/basicswapguides_tor.html" target="_blank" rel="noopener noreferrer" class="underline">(?)</a>
|
<a href="https://academy.particl.io/en/latest/basicswap-guides/basicswapguides_tor.html" target="_blank" rel="noopener noreferrer" class="underline">(?)</a>
|
||||||
</span>
|
</span>
|
||||||
|
@ -388,7 +388,7 @@ function getAPIKeys() {
|
||||||
<p class="text-sm font-heading dark:text-gray-400 mr-4">Last refreshed:
|
<p class="text-sm font-heading dark:text-gray-400 mr-4">Last refreshed:
|
||||||
<span id="lastRefreshTime">Never</span>
|
<span id="lastRefreshTime">Never</span>
|
||||||
</p>
|
</p>
|
||||||
<p class="text-sm font-heading dark:text-gray-400 mr-4">Listings:
|
<p class="text-sm font-heading dark:text-gray-400 mr-4">Network Listings:
|
||||||
<span id="newEntriesCount"></span>
|
<span id="newEntriesCount"></span>
|
||||||
</p>
|
</p>
|
||||||
<p class="text-sm font-heading dark:text-gray-400 mr-4">Next refresh:
|
<p class="text-sm font-heading dark:text-gray-400 mr-4">Next refresh:
|
||||||
|
|
|
@ -207,6 +207,43 @@
|
||||||
{% include 'footer.html' %}
|
{% include 'footer.html' %}
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
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 Error('Request timed out'));
|
||||||
|
xhr.onload = () => {
|
||||||
|
console.log(`Response for ${url}:`, xhr.responseText);
|
||||||
|
if (xhr.status === 200) {
|
||||||
|
try {
|
||||||
|
const response = JSON.parse(xhr.responseText);
|
||||||
|
if (response.Error) {
|
||||||
|
console.error(`API Error for ${url}:`, response.Error);
|
||||||
|
reject(new Error(response.Error));
|
||||||
|
} else {
|
||||||
|
resolve(response);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Invalid JSON response for ${url}:`, xhr.responseText);
|
||||||
|
reject(new Error(`Invalid JSON response: ${error.message}`));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.error(`HTTP Error for ${url}: ${xhr.status} ${xhr.statusText}`);
|
||||||
|
reject(new Error(`HTTP Error: ${xhr.status} ${xhr.statusText}`));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
xhr.onerror = () => reject(new Error('Network error occurred'));
|
||||||
|
xhr.send(JSON.stringify({
|
||||||
|
url: url,
|
||||||
|
headers: headers
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
const coinNameToSymbol = {
|
const coinNameToSymbol = {
|
||||||
'Bitcoin': 'BTC',
|
'Bitcoin': 'BTC',
|
||||||
'Particl': 'PART',
|
'Particl': 'PART',
|
||||||
|
@ -222,18 +259,18 @@ const coinNameToSymbol = {
|
||||||
'Zano': 'ZANO',
|
'Zano': 'ZANO',
|
||||||
};
|
};
|
||||||
|
|
||||||
const getUsdValue = (cryptoValue, coinSymbol) => {
|
const getUsdValue = async (cryptoValue, coinSymbol) => {
|
||||||
let apiUrl;
|
let apiUrl;
|
||||||
|
|
||||||
if (coinSymbol === 'WOW') {
|
if (coinSymbol === 'WOW') {
|
||||||
apiUrl = `https://api.coingecko.com/api/v3/simple/price?ids=wownero&vs_currencies=usd`;
|
apiUrl = 'https://api.coingecko.com/api/v3/simple/price?ids=wownero&vs_currencies=usd';
|
||||||
} else {
|
} else {
|
||||||
apiUrl = `https://min-api.cryptocompare.com/data/price?fsym=${coinSymbol}&tsyms=USD`;
|
apiUrl = `https://min-api.cryptocompare.com/data/price?fsym=${coinSymbol}&tsyms=USD`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return fetch(apiUrl)
|
try {
|
||||||
.then(response => response.json())
|
const data = await api.makePostRequest(apiUrl);
|
||||||
.then(data => {
|
|
||||||
if (coinSymbol === 'WOW') {
|
if (coinSymbol === 'WOW') {
|
||||||
const exchangeRate = data.wownero.usd;
|
const exchangeRate = data.wownero.usd;
|
||||||
if (!isNaN(exchangeRate)) {
|
if (!isNaN(exchangeRate)) {
|
||||||
|
@ -255,40 +292,10 @@ const getUsdValue = (cryptoValue, coinSymbol) => {
|
||||||
throw new Error(`Invalid exchange rate for ${coinSymbol}`);
|
throw new Error(`Invalid exchange rate for ${coinSymbol}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateValues = async () => {
|
|
||||||
const coinNameValues = document.querySelectorAll('.coinname-value');
|
|
||||||
|
|
||||||
for (const coinNameValue of coinNameValues) {
|
|
||||||
const coinFullName = coinNameValue.getAttribute('data-coinname');
|
|
||||||
const cryptoValue = parseFloat(coinNameValue.textContent);
|
|
||||||
const coinSymbol = coinNameToSymbol[coinFullName];
|
|
||||||
|
|
||||||
if (coinSymbol) {
|
|
||||||
try {
|
|
||||||
const { usdValue, btcValue } = await getUsdValue(cryptoValue, coinSymbol);
|
|
||||||
|
|
||||||
const usdValueElement = coinNameValue.nextElementSibling.querySelector('.usd-value');
|
|
||||||
if (usdValueElement) {
|
|
||||||
usdValueElement.textContent = `$${usdValue.toFixed(2)}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const btcValueElement = coinNameValue.nextElementSibling.querySelector('.btc-value');
|
|
||||||
if (btcValueElement) {
|
|
||||||
btcValueElement.textContent = `${btcValue.toFixed(8)} BTC`;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Error retrieving exchange rate for ${coinSymbol}`);
|
console.error(`Error retrieving exchange rate for ${coinSymbol}:`, error);
|
||||||
|
throw error;
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
console.error(`Coin symbol not found for full name: ${coinFullName}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
calculateTotalUsdValue();
|
|
||||||
calculateTotalBtcValue();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const toggleUsdAmount = async (usdCell, isVisible) => {
|
const toggleUsdAmount = async (usdCell, isVisible) => {
|
||||||
|
@ -388,9 +395,8 @@ const calculateTotalBtcValue = async () => {
|
||||||
let totalBtcValue = 0;
|
let totalBtcValue = 0;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const btcToUsdRate = await fetch('https://min-api.cryptocompare.com/data/price?fsym=BTC&tsyms=USD')
|
const data = await api.makePostRequest('https://min-api.cryptocompare.com/data/price?fsym=BTC&tsyms=USD');
|
||||||
.then(response => response.json())
|
const btcToUsdRate = data.USD;
|
||||||
.then(data => data.USD);
|
|
||||||
|
|
||||||
for (const usdValueElement of usdValueElements) {
|
for (const usdValueElement of usdValueElements) {
|
||||||
const usdValue = parseFloat(usdValueElement.textContent.replace('$', ''));
|
const usdValue = parseFloat(usdValueElement.textContent.replace('$', ''));
|
||||||
|
@ -412,7 +418,7 @@ const calculateTotalBtcValue = async () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Error retrieving BTC to USD exchange rate: ${error}`);
|
console.error(`Error retrieving BTC to USD exchange rate:`, error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue