From 2e4be0274ac96dd11a55b1754c7909387b81e013 Mon Sep 17 00:00:00 2001 From: gerlofvanek Date: Tue, 19 Nov 2024 20:04:04 +0100 Subject: [PATCH] ui: Refactor JS offers page / Added filter Expired/Revoked.. / Various Fixes. --- basicswap/static/js/offerstable.js | 3185 +++++++++++++--------------- basicswap/templates/offers.html | 34 +- 2 files changed, 1505 insertions(+), 1714 deletions(-) diff --git a/basicswap/static/js/offerstable.js b/basicswap/static/js/offerstable.js index 530a837..c570147 100644 --- a/basicswap/static/js/offerstable.js +++ b/basicswap/static/js/offerstable.js @@ -1,176 +1,69 @@ -// Config - -const coinNameToSymbol = { - 'Bitcoin': 'bitcoin', - 'Particl': 'particl', - 'Particl Blind': 'particl', - 'Particl Anon': 'particl', - 'Monero': 'monero', - 'Wownero': 'wownero', - 'Litecoin': 'litecoin', - 'Firo': 'firo', - 'Zcoin': 'firo', - 'Dash': 'dash', - 'PIVX': 'pivx', - 'Decred': 'decred', - 'Zano': 'zano', - 'Dogecoin': 'dogecoin', - 'Bitcoin Cash': 'bitcoin-cash' -}; - -function 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 symbolToCoinName = { - ...Object.fromEntries(Object.entries(coinNameToSymbol).map(([key, value]) => [value, key])), - '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 lastRefreshTime = null; +let newEntriesCount = 0; +let nextRefreshCountdown = 60; +let currentPage = 1; +const itemsPerPage = 100; +let lastAppliedFilters = {}; const CACHE_KEY = 'latestPricesCache'; -const CACHE_DURATION = 10 * 60 * 1000; // 10 minutes in milliseconds +const CACHE_DURATION = 10 * 60 * 1000; // 10 min +const MIN_REFRESH_INTERVAL = 30; // 30 sec -async function fetchLatestPrices() { - console.log('Checking for cached prices...'); - const cachedData = getCachedPrices(); - - if (cachedData) { - console.log('Using cached price data'); - latestPrices = cachedData; - return cachedData; - } - - console.log('Fetching latest prices...'); - const url = 'https://api.coingecko.com/api/v3/simple/price?ids=bitcoin,bitcoin-cash,dash,dogecoin,decred,litecoin,particl,pivx,monero,zano,wownero,zcoin&vs_currencies=USD,BTC'; - - try { - const data = await makePostRequest(url); - console.log('Fetched price data:', data); - - latestPrices = data; - setCachedPrices(data); - return data; - } catch (error) { - console.error('Error fetching price data:', error.message); - return null; - } -} - -function getCachedPrices() { - const cachedItem = localStorage.getItem(CACHE_KEY); - if (cachedItem) { - const { data, timestamp } = JSON.parse(cachedItem); - if (Date.now() - timestamp < CACHE_DURATION) { - return data; - } - } - return null; -} - -function setCachedPrices(data) { - const cacheItem = { - data: data, - timestamp: Date.now() - }; - localStorage.setItem(CACHE_KEY, JSON.stringify(cacheItem)); -} - -const getUsdValue = async (cryptoValue, coinSymbol) => { - try { - const prices = await fetchLatestPrices(); - const apiSymbol = coinNameToSymbol[coinSymbol] || coinSymbol.toLowerCase(); - const coinData = prices[apiSymbol]; - if (coinData && coinData.usd) { - return cryptoValue * coinData.usd; - } else { - throw new Error(`Price data not available for ${coinSymbol}`); - } - } catch (error) { - console.error(`Error getting USD value for ${coinSymbol}:`, error); - throw error; - } -}; - -// Global let jsonData = []; let originalJsonData = []; let isInitialLoad = true; let tableRateModule; - -let lastRefreshTime = null; -let newEntriesCount = 0; - -let nextRefreshCountdown = 60; // Default to 60 seconds -const MIN_REFRESH_INTERVAL = 30; // Minimum refresh interval in seconds - const isSentOffers = window.offersTableConfig.isSentOffers; -let currentPage = 1; -const itemsPerPage = 100; - -const coinIdToName = { - 1: 'particl', 2: 'bitcoin', 3: 'litecoin', 4: 'decred', - 6: 'monero', 7: 'particl blind', 8: 'particl anon', - 9: 'wownero', 11: 'pivx', 13: 'firo', 17: 'bitcoincash' +const coinNameToSymbol = { + 'Bitcoin': 'bitcoin', + 'Particl': 'particl', + 'Particl Blind': 'particl', + 'Particl Anon': 'particl', + 'Monero': 'monero', + 'Wownero': 'wownero', + 'Litecoin': 'litecoin', + 'Firo': 'firo', + 'Zcoin': 'firo', + 'Dash': 'dash', + 'PIVX': 'pivx', + 'Decred': 'decred', + 'Zano': 'zano', + 'Dogecoin': 'dogecoin', + 'Bitcoin Cash': 'bitcoin-cash' +}; + +const symbolToCoinName = { + ...Object.fromEntries(Object.entries(coinNameToSymbol).map(([key, value]) => [value, key])), + 'zcoin': 'Firo', + 'firo': 'Firo' +}; + +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' +}; + +const coinIdToName = { + 1: 'particl', 2: 'bitcoin', 3: 'litecoin', 4: 'decred', + 6: 'monero', 7: 'particl blind', 8: 'particl anon', + 9: 'wownero', 11: 'pivx', 13: 'firo', 17: 'bitcoincash' }; -// DOM const toggleButton = document.getElementById('toggleView'); const tableView = document.getElementById('tableView'); const jsonView = document.getElementById('jsonView'); @@ -185,57 +78,311 @@ const lastRefreshTimeSpan = document.getElementById('lastRefreshTime'); const newEntriesCountSpan = document.getElementById('newEntriesCount'); const nextRefreshTimeSpan = document.getElementById('nextRefreshTime'); -// Utility +window.tableRateModule = { + coinNameToSymbol: { + 'Bitcoin': 'BTC', + 'Particl': 'PART', + 'Particl Blind': 'PART', + 'Particl Anon': 'PART', + 'Monero': 'XMR', + 'Wownero': 'WOW', + 'Litecoin': 'LTC', + 'Firo': 'FIRO', + 'Dash': 'DASH', + 'PIVX': 'PIVX', + 'Decred': 'DCR', + 'Zano': 'ZANO', + 'Bitcoin Cash': 'BCH', + 'Dogecoin': 'DOGE' + }, + + cache: {}, + processedOffers: new Set(), + + getCachedValue(key) { + const cachedItem = localStorage.getItem(key); + if (cachedItem) { + const parsedItem = JSON.parse(cachedItem); + if (Date.now() < parsedItem.expiry) { + return parsedItem.value; + } else { + localStorage.removeItem(key); + } + } + return null; + }, + + setCachedValue(key, value, ttl = 900000) { + const item = { + value: value, + expiry: Date.now() + ttl, + }; + localStorage.setItem(key, JSON.stringify(item)); + }, + + setFallbackValue(coinSymbol, value) { + this.setCachedValue(`fallback_${coinSymbol}_usd`, value, 24 * 60 * 60 * 1000); + }, + + isNewOffer(offerId) { + if (this.processedOffers.has(offerId)) { + return false; + } + this.processedOffers.add(offerId); + return true; + }, + + formatUSD(value) { + if (Math.abs(value) < 0.000001) { + return value.toExponential(8) + ' USD'; + } else if (Math.abs(value) < 0.01) { + return value.toFixed(8) + ' USD'; + } else { + return value.toFixed(2) + ' USD'; + } + }, + + formatNumber(value, decimals) { + if (Math.abs(value) < 0.000001) { + return value.toExponential(decimals); + } else if (Math.abs(value) < 0.01) { + return value.toFixed(decimals); + } else { + return value.toFixed(Math.min(2, decimals)); + } + }, + + getFallbackValue(coinSymbol) { + const value = localStorage.getItem(`fallback_${coinSymbol}_usd`); + return value ? parseFloat(value) : null; + }, + + initializeTable() { + document.querySelectorAll('.coinname-value').forEach(coinNameValue => { + const coinFullNameOrSymbol = coinNameValue.getAttribute('data-coinname'); + if (!coinFullNameOrSymbol || coinFullNameOrSymbol === 'Unknown') { + console.warn('Missing or unknown coin name/symbol in data-coinname attribute'); + return; + } + coinNameValue.classList.remove('hidden'); + if (!coinNameValue.textContent.trim()) { + coinNameValue.textContent = 'N/A'; + } + }); + + document.querySelectorAll('.usd-value').forEach(usdValue => { + if (!usdValue.textContent.trim()) { + usdValue.textContent = 'N/A'; + } + }); + + document.querySelectorAll('.profit-loss').forEach(profitLoss => { + if (!profitLoss.textContent.trim() || profitLoss.textContent === 'Calculating...') { + profitLoss.textContent = 'N/A'; + } + }); + }, + + init() { + console.log('Initializing TableRateModule'); + this.initializeTable(); + } +}; + +function 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 + })); + }); +} + +function escapeHtml(unsafe) { + if (typeof unsafe !== 'string') { + console.warn('escapeHtml received a non-string value:', unsafe); + return ''; + } + return unsafe + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """) + .replace(/'/g, "'"); +} + +function formatTimeDifference(timestamp) { + const now = Math.floor(Date.now() / 1000); + const diff = Math.abs(now - timestamp); + + if (diff < 60) return `${diff} seconds`; + if (diff < 3600) return `${Math.floor(diff / 60)} minutes`; + if (diff < 86400) return `${Math.floor(diff / 3600)} hours`; + if (diff < 2592000) return `${Math.floor(diff / 86400)} days`; + if (diff < 31536000) return `${Math.floor(diff / 2592000)} months`; + return `${Math.floor(diff / 31536000)} years`; +} + +function formatTimeAgo(timestamp) { + return `${formatTimeDifference(timestamp)} ago`; +} + +function formatTimeLeft(timestamp) { + const now = Math.floor(Date.now() / 1000); + if (timestamp <= now) return "Expired"; + return formatTimeDifference(timestamp); +} + function getCoinSymbol(fullName) { - const symbolMap = { - 'Bitcoin': 'BTC', 'Litecoin': 'LTC', 'Monero': 'XMR', - 'Particl': 'PART', 'Particl Blind': 'PART', 'Particl Anon': 'PART', - 'PIVX': 'PIVX', 'Firo': 'FIRO', 'Zcoin': 'FIRO', - 'Dash': 'DASH', 'Decred': 'DCR', 'Wownero': 'WOW', - 'Bitcoin Cash': 'BCH' - }; - return symbolMap[fullName] || fullName; + const symbolMap = { + 'Bitcoin': 'BTC', 'Litecoin': 'LTC', 'Monero': 'XMR', + 'Particl': 'PART', 'Particl Blind': 'PART', 'Particl Anon': 'PART', + 'PIVX': 'PIVX', 'Firo': 'FIRO', 'Zcoin': 'FIRO', + 'Dash': 'DASH', 'Decred': 'DCR', 'Wownero': 'WOW', + 'Bitcoin Cash': 'BCH' + }; + return symbolMap[fullName] || fullName; } function getDisplayName(coinName) { - if (coinName.toLowerCase() === 'zcoin') { - return 'Firo'; - } - return coinNameToDisplayName[coinName] || coinName; + if (coinName.toLowerCase() === 'zcoin') { + return 'Firo'; + } + return coinNameToDisplayName[coinName] || coinName; } -function getValidOffers() { - if (isSentOffers) { - return jsonData; +function getCoinSymbolLowercase(coin) { + if (typeof coin === 'string') { + if (coin.toLowerCase() === 'bitcoin cash') { + return 'bitcoin-cash'; + } + return (coinNameToSymbol[coin] || coin).toLowerCase(); + } else if (coin && typeof coin === 'object' && coin.symbol) { + return coin.symbol.toLowerCase(); } else { - const currentTime = Math.floor(Date.now() / 1000); - return jsonData.filter(offer => offer.expire_at > currentTime); + console.warn('Invalid coin input:', coin); + return 'unknown'; } } -function removeExpiredOffers() { - if (isSentOffers) { - return false; - } - const currentTime = Math.floor(Date.now() / 1000); - const initialLength = jsonData.length; - jsonData = jsonData.filter(offer => offer.expire_at > currentTime); +function coinMatches(offerCoin, filterCoin) { + if (!offerCoin || !filterCoin) return false; - if (jsonData.length < initialLength) { - console.log(`Removed ${initialLength - jsonData.length} expired offers`); + offerCoin = offerCoin.toLowerCase(); + filterCoin = filterCoin.toLowerCase(); + + if (offerCoin === filterCoin) return true; + + if ((offerCoin === 'firo' || offerCoin === 'zcoin') && + (filterCoin === 'firo' || filterCoin === 'zcoin')) { return true; } + + if ((offerCoin === 'bitcoincash' && filterCoin === 'bitcoin cash') || + (offerCoin === 'bitcoin cash' && filterCoin === 'bitcoincash')) { + return true; + } + + const particlVariants = ['particl', 'particl anon', 'particl blind']; + if (filterCoin === 'particl' && particlVariants.includes(offerCoin)) { + return true; + } + + if (particlVariants.includes(filterCoin)) { + return offerCoin === filterCoin; + } + return false; } -function handleNoOffersScenario() { - offersBody.innerHTML = 'No active offers available. Refreshing data...'; - fetchOffers(true); +function getCachedPrices() { + const cachedItem = localStorage.getItem(CACHE_KEY); + if (cachedItem) { + const { data, timestamp } = JSON.parse(cachedItem); + if (Date.now() - timestamp < CACHE_DURATION) { + return data; + } + } + return null; } -function logOfferStatus() { - const validOffers = getValidOffers(); - console.log(`Total offers: ${jsonData.length}, Valid offers: ${validOffers.length}, Current page: ${currentPage}, Total pages: ${Math.ceil(validOffers.length / itemsPerPage)}`); +function setCachedPrices(data) { + const cacheItem = { + data: data, + timestamp: Date.now() + }; + localStorage.setItem(CACHE_KEY, JSON.stringify(cacheItem)); +} + +function getButtonProperties(isActuallyExpired, isSentOffers, isTreatedAsSentOffer, isRevoked) { + if (isRevoked) { + return { + buttonClass: 'bg-red-500 text-white hover:bg-red-600 transition duration-200', + buttonText: 'Revoked' + }; + } else if (isActuallyExpired && isSentOffers) { + return { + buttonClass: 'bg-gray-400 text-white dark:border-gray-300 text-white hover:bg-red-700 transition duration-200', + buttonText: 'Expired' + }; + } else if (isTreatedAsSentOffer) { + return { + buttonClass: 'bg-gray-300 bold text-white bold hover:bg-green-600 transition duration-200', + buttonText: 'Edit' + }; + } else { + return { + buttonClass: 'bg-blue-500 text-white hover:bg-green-600 transition duration-200', + buttonText: 'Swap' + }; + } +} + +function getTimerColor(offer) { + const now = Math.floor(Date.now() / 1000); + const timeLeft = offer.expire_at - now; + + if (timeLeft <= 300) { // 5 min or less + return "#9CA3AF"; // Grey + } else if (timeLeft <= 1800) { // 5-30 min + return "#3B82F6"; // Blue + } else { // More than 30 min + return "#10B981"; // Green + } +} + +function getProfitColorClass(percentage) { + const numericPercentage = parseFloat(percentage); + if (numericPercentage > 0) return 'text-green-500'; + if (numericPercentage < 0) return 'text-red-500'; + if (numericPercentage === 0) return 'text-yellow-400'; + return 'text-white'; } function isOfferExpired(offer) { @@ -250,51 +397,6 @@ function isOfferExpired(offer) { return isExpired; } -function setRefreshButtonLoading(isLoading) { - const refreshButton = document.getElementById('refreshOffers'); - const refreshIcon = document.getElementById('refreshIcon'); - const refreshText = document.getElementById('refreshText'); - - refreshButton.disabled = isLoading; - refreshIcon.classList.toggle('animate-spin', isLoading); - refreshText.textContent = isLoading ? 'Refresh' : 'Refresh'; //to-do -} - -function escapeHtml(unsafe) { - if (typeof unsafe !== 'string') { - console.warn('escapeHtml received a non-string value:', unsafe); - return ''; - } - return unsafe - .replace(/&/g, "&") - .replace(//g, ">") - .replace(/"/g, """) - .replace(/'/g, "'"); -} - -function formatTimeDifference(timestamp) { - const now = Math.floor(Date.now() / 1000); - const diff = Math.abs(now - timestamp); - - if (diff < 60) return `${diff} seconds`; - if (diff < 3600) return `${Math.floor(diff / 60)} minutes`; - if (diff < 86400) return `${Math.floor(diff / 3600)} hours`; - if (diff < 2592000) return `${Math.floor(diff / 86400)} days`; - if (diff < 31536000) return `${Math.floor(diff / 2592000)} months`; - return `${Math.floor(diff / 31536000)} years`; -} - -function formatTimeAgo(timestamp) { - return `${formatTimeDifference(timestamp)} ago`; -} - -function formatTimeLeft(timestamp) { - const now = Math.floor(Date.now() / 1000); - if (timestamp <= now) return "Expired"; - return formatTimeDifference(timestamp); -} - function getTimeUntilNextExpiration() { const currentTime = Math.floor(Date.now() / 1000); const nextExpiration = jsonData.reduce((earliest, offer) => { @@ -305,1332 +407,70 @@ function getTimeUntilNextExpiration() { return Math.max(MIN_REFRESH_INTERVAL, Math.min(nextExpiration, 300)); } -function getNoOffersMessage() { - const formData = new FormData(filterForm); - const filters = Object.fromEntries(formData); - let message = 'No offers available'; - if (filters.coin_to !== 'any') { - const coinToName = coinIdToName[filters.coin_to] || filters.coin_to; - message += ` for bids to ${coinToName}`; - } - if (filters.coin_from !== 'any') { - const coinFromName = coinIdToName[filters.coin_from] || filters.coin_from; - message += ` for offers from ${coinFromName}`; - } - if (isSentOffers && filters.active && filters.active !== 'any') { - message += ` with status: ${filters.active}`; - } - return message; -} - -window.tableRateModule = { - coinNameToSymbol: { - 'Bitcoin': 'BTC', - 'Particl': 'PART', - 'Particl Blind': 'PART', - 'Particl Anon': 'PART', - 'Monero': 'XMR', - 'Wownero': 'WOW', - 'Litecoin': 'LTC', - 'Firo': 'FIRO', - 'Dash': 'DASH', - 'PIVX': 'PIVX', - 'Decred': 'DCR', - 'Zano': 'ZANO', - 'Bitcoin Cash': 'BCH', - 'Dogecoin': 'DOGE' - }, - - cache: {}, - processedOffers: new Set(), - - getCachedValue(key) { - const cachedItem = localStorage.getItem(key); - if (cachedItem) { - const parsedItem = JSON.parse(cachedItem); - if (Date.now() < parsedItem.expiry) { - return parsedItem.value; - } else { - localStorage.removeItem(key); - } - } - return null; - }, - - setCachedValue(key, value, ttl = 900000) { - const item = { - value: value, - expiry: Date.now() + ttl, - }; - localStorage.setItem(key, JSON.stringify(item)); - }, - - setFallbackValue(coinSymbol, value) { - this.setCachedValue(`fallback_${coinSymbol}_usd`, value, 24 * 60 * 60 * 1000); - }, - - isNewOffer(offerId) { - if (this.processedOffers.has(offerId)) { - return false; - } - this.processedOffers.add(offerId); - return true; - }, - - formatUSD(value) { - if (Math.abs(value) < 0.000001) { - return value.toExponential(8) + ' USD'; - } else if (Math.abs(value) < 0.01) { - return value.toFixed(8) + ' USD'; - } else { - return value.toFixed(2) + ' USD'; - } - }, - - formatNumber(value, decimals) { - if (Math.abs(value) < 0.000001) { - return value.toExponential(decimals); - } else if (Math.abs(value) < 0.01) { - return value.toFixed(decimals); - } else { - return value.toFixed(Math.min(2, decimals)); - } - }, - - getFallbackValue(coinSymbol) { - const value = localStorage.getItem(`fallback_${coinSymbol}_usd`); - return value ? parseFloat(value) : null; - }, - - initializeTable() { - console.log('Initializing table'); - document.querySelectorAll('.coinname-value').forEach(coinNameValue => { - const coinFullNameOrSymbol = coinNameValue.getAttribute('data-coinname'); - console.log('Processing coin:', coinFullNameOrSymbol); - if (!coinFullNameOrSymbol || coinFullNameOrSymbol === 'Unknown') { - console.warn('Missing or unknown coin name/symbol in data-coinname attribute'); - return; - } - coinNameValue.classList.remove('hidden'); - if (!coinNameValue.textContent.trim()) { - coinNameValue.textContent = 'N/A'; - } - }); - - document.querySelectorAll('.usd-value').forEach(usdValue => { - if (!usdValue.textContent.trim()) { - usdValue.textContent = 'N/A'; - } - }); - - document.querySelectorAll('.profit-loss').forEach(profitLoss => { - if (!profitLoss.textContent.trim() || profitLoss.textContent === 'Calculating...') { - profitLoss.textContent = 'N/A'; - } - }); - }, - - init() { - console.log('Initializing TableRateModule'); - this.initializeTable(); - } -}; - -function updateProfitLoss(row, fromCoin, toCoin, fromAmount, toAmount, isOwnOffer) { - const profitLossElement = row.querySelector('.profit-loss'); - if (!profitLossElement) { - console.warn('Profit loss element not found in row'); - return; - } - - calculateProfitLoss(fromCoin, toCoin, fromAmount, toAmount, isOwnOffer) - .then(percentDiff => { - if (percentDiff === null) { - profitLossElement.textContent = 'N/A'; - profitLossElement.className = 'profit-loss text-lg font-bold text-gray-500'; - console.log(`Unable to calculate profit/loss for ${fromCoin} to ${toCoin}`); - return; - } - - const formattedPercentDiff = percentDiff.toFixed(2); - const percentDiffDisplay = formattedPercentDiff === "0.00" ? "0.00" : - (percentDiff > 0 ? `+${formattedPercentDiff}` : formattedPercentDiff); - - const colorClass = getProfitColorClass(percentDiff); - profitLossElement.textContent = `${percentDiffDisplay}%`; - profitLossElement.className = `profit-loss text-lg font-bold ${colorClass}`; - - const tooltipId = `percentage-tooltip-${row.getAttribute('data-offer-id')}`; - const tooltipElement = document.getElementById(tooltipId); - if (tooltipElement) { - const tooltipContent = createTooltipContent(isOwnOffer, fromCoin, toCoin, fromAmount, toAmount); - tooltipElement.innerHTML = ` -
- ${tooltipContent} -
-
- `; - } - - console.log(`Updated profit/loss display: ${profitLossElement.textContent}, isOwnOffer: ${isOwnOffer}`); - }) - .catch(error => { - console.error('Error in updateProfitLoss:', error); - profitLossElement.textContent = 'Error'; - profitLossElement.className = 'profit-loss text-lg font-bold text-red-500'; - }); -} - -function fetchOffers(manualRefresh = false) { - return new Promise((resolve, reject) => { - const endpoint = isSentOffers ? '/json/sentoffers' : '/json/offers'; - console.log(`Fetching offers from: ${endpoint}`); - - if (newEntriesCountSpan) { - newEntriesCountSpan.textContent = 'Loading...'; - } - - if (manualRefresh) { - offersBody.innerHTML = 'Refreshing offers...'; - } - - setRefreshButtonLoading(true); - - const requestBody = { - with_extra_info: true - }; - - fetch(endpoint, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(requestBody) - }) - .then(response => { - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } - return response.json(); - }) - .then(data => { - console.log('Raw data received:', data.length, 'offers'); - - let newData = Array.isArray(data) ? data : Object.values(data); - console.log('Processed data length before filtering:', newData.length); - - newData = newData.map(offer => ({ - ...offer, - offer_id: String(offer.offer_id || ''), - swap_type: String(offer.swap_type || 'N/A'), - addr_from: String(offer.addr_from || ''), - coin_from: String(offer.coin_from || ''), - coin_to: String(offer.coin_to || ''), - amount_from: String(offer.amount_from || '0'), - amount_to: String(offer.amount_to || '0'), - rate: String(offer.rate || '0'), - created_at: Number(offer.created_at || 0), - expire_at: Number(offer.expire_at || 0), - is_own_offer: Boolean(offer.is_own_offer), - amount_negotiable: Boolean(offer.amount_negotiable), - unique_id: `${offer.offer_id}_${offer.created_at}_${offer.coin_from}_${offer.coin_to}` - })); - - if (!isSentOffers) { - const currentTime = Math.floor(Date.now() / 1000); - const beforeFilterCount = newData.length; - newData = newData.filter(offer => { - const keepOffer = !isOfferExpired(offer); - if (!keepOffer) { - console.log('Filtered out expired offer:', offer.offer_id); - } - return keepOffer; - }); - console.log(`Filtered out ${beforeFilterCount - newData.length} expired offers`); - } - - console.log('Processed data length after filtering:', newData.length); - - if (isInitialLoad || manualRefresh) { - console.log('Initial load or manual refresh - replacing all data'); - jsonData = newData; - originalJsonData = [...newData]; - isInitialLoad = false; - } else { - console.log('Updating existing data'); - console.log('Current jsonData length:', jsonData.length); - - const mergedData = [...jsonData]; - newData.forEach(newOffer => { - const existingIndex = mergedData.findIndex(existing => existing.offer_id === newOffer.offer_id); - if (existingIndex !== -1) { - mergedData[existingIndex] = newOffer; - } else { - mergedData.push(newOffer); - } - }); - - jsonData = isSentOffers ? mergedData : mergedData.filter(offer => !isOfferExpired(offer)); - } - - console.log('Final jsonData length:', jsonData.length); - - if (!isSentOffers) { - removeExpiredOffers(); - } - - const validOffers = getValidOffers(); - const validItemCount = validOffers.length; - if (newEntriesCountSpan) { - newEntriesCountSpan.textContent = validItemCount; - } - console.log('Valid offers count:', validItemCount); - - lastRefreshTime = Date.now(); - nextRefreshCountdown = getTimeUntilNextExpiration(); - updateLastRefreshTime(); - updateNextRefreshTime(); - - updateOffersTable(); - updateJsonView(); - updatePaginationInfo(); - - if (validItemCount === 0) { - handleNoOffersScenario(); - } - - if (manualRefresh) { - console.log('Offers refreshed successfully'); - } - - resolve(); - }) - .catch(error => { - console.error(`Error fetching ${isSentOffers ? 'sent offers' : 'offers'}:`, error); - - let errorMessage = 'An error occurred while fetching offers. '; - if (error.message.includes('HTTP error')) { - errorMessage += 'The server returned an error. '; - } else if (error.message.includes('empty data')) { - errorMessage += 'No offer data was received. '; - } else if (error.name === 'TypeError') { - errorMessage += 'There was a problem parsing the response. '; - } else { - errorMessage += 'Please check your network connection. '; - } - errorMessage += 'Please try again later.'; - - if (typeof ui !== 'undefined' && ui.displayErrorMessage) { - ui.displayErrorMessage(errorMessage); - } else { - console.error(errorMessage); - offersBody.innerHTML = `${escapeHtml(errorMessage)}`; - } - - isInitialLoad = false; - updateOffersTable(); - updateJsonView(); - updatePaginationInfo(); - - if (newEntriesCountSpan) { - newEntriesCountSpan.textContent = '0'; - } - - reject(error); - }) - .finally(() => { - setRefreshButtonLoading(false); - }); - }); -} - -function applyFilters() { - console.log('Applying filters'); - jsonData = filterAndSortData(); - updateOffersTable(); - updateJsonView(); - updatePaginationInfo(); - console.log('Filters applied, table updated'); -} - -function filterAndSortData() { - console.log('Filtering and sorting data'); - - const formData = new FormData(filterForm); - const filters = Object.fromEntries(formData); - console.log('Processed filters:', filters); - - if (filters.coin_to !== 'any') { - filters.coin_to = coinIdToName[filters.coin_to] || filters.coin_to; - } - if (filters.coin_from !== 'any') { - filters.coin_from = coinIdToName[filters.coin_from] || filters.coin_from; - } - - console.log('Processed filters:', filters); - - const currentTime = Math.floor(Date.now() / 1000); - - 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 (filterCoin === 'bitcoincash' && offerCoin === 'bitcoin cash') return true; - if (filterCoin === 'bitcoin cash' && offerCoin === 'bitcoincash') return true; - - if (isParticlVariant(filterCoin)) { - return offerCoin === filterCoin; - } - - if (filterCoin === 'particl' && isParticlVariant(offerCoin)) return true; - - return false; -}; - - originalJsonData.forEach(offer => { - const coinFrom = (offer.coin_from || ''); - const coinTo = (offer.coin_to || ''); - const isExpired = offer.expire_at <= currentTime; - - if (!isSentOffers && isExpired) { - return; - } - - let passesFilter = true; - - if (filters.coin_to !== 'any' && !coinMatches(coinTo, filters.coin_to)) { - passesFilter = false; - } - if (filters.coin_from !== 'any' && !coinMatches(coinFrom, filters.coin_from)) { - passesFilter = false; - } - - if (isSentOffers && filters.active && filters.active !== 'any') { - const offerState = isExpired ? 'expired' : 'active'; - if (filters.active !== offerState) { - passesFilter = false; - } - } - - if (passesFilter) { - uniqueOffersMap.set(offer.unique_id, offer); - } - }); - - let filteredData = Array.from(uniqueOffersMap.values()); - - console.log('Filtered data length:', filteredData.length); - - const sortBy = filters.sort_by || 'created_at'; - const sortDir = filters.sort_dir || 'desc'; - - filteredData.sort((a, b) => { - let aValue, bValue; - - switch (sortBy) { - case 'created_at': - aValue = a.created_at; - bValue = b.created_at; - break; - case 'rate': - aValue = parseFloat(a.rate); - bValue = parseFloat(b.rate); - break; - default: - aValue = a.created_at; - bValue = b.created_at; - } - - if (sortDir === 'asc') { - return aValue - bValue; - } else { - return bValue - aValue; - } - }); - - console.log(`Sorted offers by ${sortBy} in ${sortDir} order`); - - return filteredData; -} - -function updateProfitLoss(row, fromCoin, toCoin, fromAmount, toAmount, isOwnOffer) { - const profitLossElement = row.querySelector('.profit-loss'); - if (!profitLossElement) { - console.warn('Profit loss element not found in row'); - return; - } - - calculateProfitLoss(fromCoin, toCoin, fromAmount, toAmount, isOwnOffer) - .then(percentDiff => { - if (percentDiff === null) { - profitLossElement.textContent = 'N/A'; - profitLossElement.className = 'profit-loss text-lg font-bold text-gray-500'; - console.log(`Unable to calculate profit/loss for ${fromCoin} to ${toCoin}`); - return; - } - - // Format the percentage to two decimal places - const formattedPercentDiff = percentDiff.toFixed(2); - const percentDiffDisplay = formattedPercentDiff === "0.00" ? "0.00" : - (percentDiff > 0 ? `+${formattedPercentDiff}` : formattedPercentDiff); - - const colorClass = getProfitColorClass(percentDiff); - profitLossElement.textContent = `${percentDiffDisplay}%`; - profitLossElement.className = `profit-loss text-lg font-bold ${colorClass}`; - - const tooltipId = `percentage-tooltip-${row.getAttribute('data-offer-id')}`; - const tooltipElement = document.getElementById(tooltipId); - if (tooltipElement) { - const tooltipContent = createTooltipContent(isOwnOffer, fromCoin, toCoin, fromAmount, toAmount); - tooltipElement.innerHTML = ` -
- ${tooltipContent} -
-
- `; - } - - console.log(`Updated profit/loss display: ${profitLossElement.textContent}, isOwnOffer: ${isOwnOffer}`); - }) - .catch(error => { - console.error('Error in updateProfitLoss:', error); - profitLossElement.textContent = 'Error'; - profitLossElement.className = 'profit-loss text-lg font-bold text-red-500'; - }); -} - -function createTableRow(offer, isSentOffers) { - const row = document.createElement('tr'); - const uniqueId = `${offer.offer_id}_${offer.created_at}`; - 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', uniqueId); - - const coinFrom = offer.coin_from; - const coinTo = offer.coin_to; - const coinFromSymbol = coinNameToSymbol[coinFrom] || coinFrom.toLowerCase(); - const coinToSymbol = coinNameToSymbol[coinTo] || coinTo.toLowerCase(); - const coinFromDisplay = getDisplayName(coinFrom); - const coinToDisplay = getDisplayName(coinTo); - - const postedTime = formatTimeAgo(offer.created_at); - const expiresIn = formatTimeLeft(offer.expire_at); - - const currentTime = Math.floor(Date.now() / 1000); - const isActuallyExpired = currentTime > offer.expire_at; - - const isOwnOffer = offer.is_own_offer; - - const { buttonClass, buttonText } = getButtonProperties(isActuallyExpired, isSentOffers, isOwnOffer); - - const fromAmount = parseFloat(offer.amount_from) || 0; - const toAmount = parseFloat(offer.amount_to) || 0; - - row.innerHTML = ` - ${createTimeColumn(offer, postedTime, expiresIn)} - ${createDetailsColumn(offer)} - ${createTakerAmountColumn(offer, coinTo, coinFrom)} - ${createSwapColumn(offer, coinFromDisplay, coinToDisplay, coinFromSymbol, coinToSymbol)} - ${createOrderbookColumn(offer, coinFrom, coinTo)} - ${createRateColumn(offer, coinFrom, coinTo)} - ${createPercentageColumn(offer)} - ${createActionColumn(offer, buttonClass, buttonText)} - ${createTooltips(offer, isOwnOffer, coinFrom, coinTo, fromAmount, toAmount, postedTime, expiresIn, isActuallyExpired)} - `; - - updateTooltipTargets(row, uniqueId); - - updateProfitLoss(row, coinFrom, coinTo, fromAmount, toAmount, isOwnOffer); - - return row; -} - -function prepareOfferData(offer, isSentOffers) { - const coinFrom = offer.coin_from; - const coinTo = offer.coin_to; - - const postedTime = formatTimeAgo(offer.created_at); - const expiresIn = formatTimeLeft(offer.expire_at); - - const currentTime = Math.floor(Date.now() / 1000); - const isActuallyExpired = currentTime > offer.expire_at; - - const { buttonClass, buttonText } = getButtonProperties(isActuallyExpired, isSentOffers, offer.is_own_offer); - - const clockColor = isActuallyExpired ? "#9CA3AF" : "#3B82F6"; - - return { - coinFrom, coinTo, - postedTime, expiresIn, isActuallyExpired, - buttonClass, buttonText, clockColor - }; -} - -// to-do revoked -function getButtonProperties(isActuallyExpired, isSentOffers, isTreatedAsSentOffer, isRevoked) { - if (isRevoked) { - return { - buttonClass: 'bg-red-500 text-white hover:bg-red-600 transition duration-200', - buttonText: 'Revoked' - }; - } else if (isActuallyExpired && isSentOffers) { - return { - buttonClass: 'bg-gray-400 text-white dark:border-gray-300 text-white hover:bg-red-700 transition duration-200', - buttonText: 'Expired' - }; - } else if (isTreatedAsSentOffer) { - return { - buttonClass: 'bg-gray-300 bold text-white bold hover:bg-green-600 transition duration-200', - buttonText: 'Edit' - }; - } else { - return { - buttonClass: 'bg-blue-500 text-white hover:bg-green-600 transition duration-200', - buttonText: 'Swap' - }; - } -} - -async function updateOffersTable() { - console.log('Starting updateOffersTable function'); - console.log(`Is Sent Offers page: ${isSentOffers}`); - - try { - const priceData = await fetchLatestPrices(); - if (!priceData) { - console.error('Failed to fetch latest prices. Using last known prices or proceeding without price data.'); - } else { - console.log('Latest prices fetched successfully'); - latestPrices = priceData; - } - - let validOffers = getValidOffers(); - console.log(`Valid offers: ${validOffers.length}`); - - if (validOffers.length === 0) { - console.log('No valid offers found. Handling no offers scenario.'); - handleNoOffersScenario(); - return; - } - - const totalPages = Math.max(1, Math.ceil(validOffers.length / itemsPerPage)); - currentPage = Math.min(currentPage, totalPages); - - const startIndex = (currentPage - 1) * itemsPerPage; - const endIndex = startIndex + itemsPerPage; - const itemsToDisplay = validOffers.slice(startIndex, endIndex); - - console.log(`Displaying offers ${startIndex + 1} to ${endIndex} of ${validOffers.length}`); - - offersBody.innerHTML = ''; - - for (const offer of itemsToDisplay) { - const row = createTableRow(offer, isSentOffers); - if (row) { - offersBody.appendChild(row); - try { - const fromAmount = parseFloat(offer.amount_from); - const toAmount = parseFloat(offer.amount_to); - await updateProfitLoss(row, offer.coin_from, offer.coin_to, fromAmount, toAmount, offer.is_own_offer); - } catch (error) { - console.error(`Error updating profit/loss for offer ${offer.offer_id}:`, error); - } - } - } - - updateRowTimes(); - initializeFlowbiteTooltips(); - updatePaginationInfo(); - - if (tableRateModule && typeof tableRateModule.initializeTable === 'function') { - tableRateModule.initializeTable(); - } - - logOfferStatus(); - - lastRefreshTime = Date.now(); - nextRefreshCountdown = getTimeUntilNextExpiration(); - updateLastRefreshTime(); - updateNextRefreshTime(); - - if (newEntriesCountSpan) { - newEntriesCountSpan.textContent = validOffers.length; - } - - } catch (error) { - console.error('Error updating offers table:', error); - offersBody.innerHTML = `An error occurred while updating the offers table. Please try again later.`; - } finally { - setRefreshButtonLoading(false); - } -} - -function updateProfitLoss(row, fromCoin, toCoin, fromAmount, toAmount, isOwnOffer) { - const profitLossElement = row.querySelector('.profit-loss'); - if (!profitLossElement) { - console.warn('Profit loss element not found in row'); - return; - } - - if (!fromCoin || !toCoin) { - console.error(`Invalid coin names: fromCoin=${fromCoin}, toCoin=${toCoin}`); - profitLossElement.textContent = 'Error'; - profitLossElement.className = 'profit-loss text-lg font-bold text-red-500'; - return; - } - - calculateProfitLoss(fromCoin, toCoin, fromAmount, toAmount, isOwnOffer) - .then(percentDiff => { - if (percentDiff === null) { - profitLossElement.textContent = 'N/A'; - profitLossElement.className = 'profit-loss text-lg font-bold text-gray-500'; - console.log(`Unable to calculate profit/loss for ${fromCoin} to ${toCoin}`); - return; - } - - // Format the percentage to two decimal places - const formattedPercentDiff = percentDiff.toFixed(2); - const percentDiffDisplay = formattedPercentDiff === "0.00" ? "0.00" : - (percentDiff > 0 ? `+${formattedPercentDiff}` : formattedPercentDiff); - - const colorClass = getProfitColorClass(percentDiff); - profitLossElement.textContent = `${percentDiffDisplay}%`; - profitLossElement.className = `profit-loss text-lg font-bold ${colorClass}`; - - const tooltipId = `percentage-tooltip-${row.getAttribute('data-offer-id')}`; - const tooltipElement = document.getElementById(tooltipId); - if (tooltipElement) { - const tooltipContent = createTooltipContent(isSentOffers || isOwnOffer, fromCoin, toCoin, fromAmount, toAmount); - tooltipElement.innerHTML = ` -
- ${tooltipContent} -
-
- `; - } - - console.log(`Updated profit/loss display: ${profitLossElement.textContent}, isSentOffers: ${isSentOffers}, isOwnOffer: ${isOwnOffer}`); - }) - .catch(error => { - console.error('Error in updateProfitLoss:', error); - profitLossElement.textContent = 'Error'; - profitLossElement.className = 'profit-loss text-lg font-bold text-red-500'; - }); -} - -async function calculateProfitLoss(fromCoin, toCoin, fromAmount, toAmount, isOwnOffer) { - console.log(`Calculating profit/loss for ${fromAmount} ${fromCoin} to ${toAmount} ${toCoin}, isOwnOffer: ${isOwnOffer}`); - - if (!latestPrices) { - console.error('Latest prices not available. Unable to calculate profit/loss.'); - return null; - } - - const getPriceKey = (coin) => { - const lowerCoin = coin.toLowerCase(); - if (lowerCoin === 'firo' || lowerCoin === 'zcoin') { - return 'zcoin'; - } - if (lowerCoin === 'bitcoin cash') { - return 'bitcoin-cash'; - } - return coinNameToSymbol[coin] || lowerCoin; - }; - - const fromSymbol = getPriceKey(fromCoin); - const toSymbol = getPriceKey(toCoin); - - const fromPriceUSD = latestPrices[fromSymbol]?.usd; - const toPriceUSD = latestPrices[toSymbol]?.usd; - - if (!fromPriceUSD || !toPriceUSD) { - console.error(`Price data missing for ${fromSymbol} or ${toSymbol}`); - return null; - } - - const fromValueUSD = fromAmount * fromPriceUSD; - const toValueUSD = toAmount * toPriceUSD; - - let percentDiff; - if (isOwnOffer) { - percentDiff = ((toValueUSD / fromValueUSD) - 1) * 100; - } else { - percentDiff = ((fromValueUSD / toValueUSD) - 1) * 100; - } - - console.log(`Percent difference: ${percentDiff.toFixed(2)}%`); - return percentDiff; -} - -function getProfitColorClass(percentage) { - const numericPercentage = parseFloat(percentage); - if (numericPercentage > 0) return 'text-green-500'; - if (numericPercentage < 0) return 'text-red-500'; - if (numericPercentage === 0) return 'text-yellow-400'; - return 'text-white'; -} - -function getMarketRate(fromCoin, toCoin) { - return new Promise((resolve) => { - console.log(`Attempting to get market rate for ${fromCoin} to ${toCoin}`); - if (!latestPrices) { - console.warn('Latest prices object is not available'); - resolve(null); - return; - } - - const getPriceKey = (coin) => { - const lowerCoin = coin.toLowerCase(); - if (lowerCoin === 'firo' || lowerCoin === 'zcoin') { - return 'zcoin'; - } - if (lowerCoin === 'bitcoin cash') { - return 'bitcoin-cash'; - } - 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) { - console.warn(`Missing price data for ${!fromPrice ? fromCoin : toCoin}`); - resolve(null); - return; - } - const rate = toPrice / fromPrice; - console.log(`Market rate calculated: ${rate} ${toCoin}/${fromCoin}`); - resolve(rate); - }); -} - -function getTimerColor(offer) { - const now = Math.floor(Date.now() / 1000); - const timeLeft = offer.expire_at - now; - - if (timeLeft <= 300) { // 5 minutes or less - return "#9CA3AF"; // Grey - } else if (timeLeft <= 1800) { // 5-30 minutes - return "#3B82F6"; // Blue - } else { // More than 30 minutes - return "#10B981"; // Green/Turquoise - } -} - -function createTimeColumn(offer, postedTime, expiresIn) { - const timerColor = getTimerColor(offer); - - return ` - -
-
- - - - - - -
- -
- - `; -} - -function createDetailsColumn(offer) { - const addrFrom = offer.addr_from || ''; - return ` - - - Recipient: ${escapeHtml(addrFrom.substring(0, 10))}... - - - - `; -} - -function createSwapColumn(offer, coinFromDisplay, coinToDisplay, coinFromSymbol, coinToSymbol) { - const getImageFilename = (symbol, displayName) => { - if (displayName.toLowerCase() === 'zcoin' || displayName.toLowerCase() === 'firo') { - return 'Firo.png'; - } - return `${displayName.replace(' ', '-')}.png`; - }; - - return ` - - -
- - ${coinToDisplay} - - - - ${coinFromDisplay} - -
-
- - `; -} - -function createTakerAmountColumn(offer, coinFrom, coinTo) { - const fromAmount = parseFloat(offer.amount_to); - const fromSymbol = getCoinSymbol(coinFrom); - return ` - -
- -
-
${fromAmount.toFixed(4)}
-
${coinFrom}
-
-
-
- - `; -} - -function createOrderbookColumn(offer, coinTo, coinFrom) { - const toAmount = parseFloat(offer.amount_from); - const toSymbol = getCoinSymbol(coinTo); - return ` - -
- -
-
${toAmount.toFixed(4)}
-
${coinTo}
-
-
-
- - `; -} - function calculateInverseRate(rate) { return (1 / parseFloat(rate)).toFixed(8); } -function createRateColumn(offer, coinFrom, coinTo) { - const rate = parseFloat(offer.rate); - const inverseRate = 1 / rate; - const fromSymbol = getCoinSymbol(coinFrom); - const toSymbol = getCoinSymbol(coinTo); - - const getPriceKey = (coin) => { - const lowerCoin = coin.toLowerCase(); - if (lowerCoin === 'firo' || lowerCoin === 'zcoin') { - return 'zcoin'; - } - if (lowerCoin === 'bitcoin cash') { - return 'bitcoin-cash'; - } - return coinNameToSymbol[coin] || lowerCoin; - }; - - const fromPriceUSD = latestPrices[getPriceKey(coinFrom)]?.usd || 0; - const toPriceUSD = latestPrices[getPriceKey(coinTo)]?.usd || 0; - - const rateInUSD = rate * toPriceUSD; - - // Add logging for debugging - console.log(`Rate calculation for ${fromSymbol} to ${toSymbol}:`); - console.log(`Using price keys: ${getPriceKey(coinFrom)}, ${getPriceKey(coinTo)}`); - console.log(`Got prices: $${fromPriceUSD}, $${toPriceUSD}`); - console.log(`Rate: ${rate} ${toSymbol}/${fromSymbol}`); - console.log(`Rate in USD: $${rateInUSD.toFixed(2)}`); - - return ` - -
-
- - $${rateInUSD.toFixed(2)} USD - - - ${rate.toFixed(8)} ${toSymbol}/${fromSymbol} - - - ${inverseRate.toFixed(8)} ${fromSymbol}/${toSymbol} - -
-
- - `; -} - -function createPercentageColumn(offer) { - return ` - -
-
- - Calculating... - -
-
- - `; -} - -function createActionColumn(offer, buttonClass, buttonText) { - return ` - -
- - ${buttonText} - -
- - `; -} - -function createTooltips(offer, treatAsSentOffer, coinFrom, coinTo, fromAmount, toAmount, postedTime, expiresIn, isActuallyExpired) { - const rate = parseFloat(offer.rate); - const fromSymbol = getCoinSymbolLowercase(coinFrom); - const toSymbol = getCoinSymbolLowercase(coinTo); - - const fromPriceUSD = latestPrices[fromSymbol]?.usd || 0; - const toPriceUSD = latestPrices[toSymbol]?.usd || 0; - const rateInUSD = rate * toPriceUSD; - - const combinedRateTooltip = createCombinedRateTooltip(offer, coinFrom, coinTo, treatAsSentOffer); - - const percentageTooltipContent = createTooltipContent(treatAsSentOffer, coinFrom, coinTo, fromAmount, toAmount); - - const uniqueId = `${offer.offer_id}_${offer.created_at}`; - - return ` - +function hasActiveFilters() { + const formData = new FormData(filterForm); + const filters = Object.fromEntries(formData); - - - - - - - - - + console.log('Current filters:', filters); - - `; + const hasFilters = filters.coin_to !== 'any' || + filters.coin_from !== 'any' || + (filters.status && filters.status !== 'any'); + + console.log('Has active filters:', hasFilters); + + return hasFilters; } -function updateTooltipTargets(row, uniqueId) { - row.querySelector('[data-tooltip-target^="tooltip-active"]').setAttribute('data-tooltip-target', `tooltip-active-${uniqueId}`); - row.querySelector('[data-tooltip-target^="tooltip-recipient"]').setAttribute('data-tooltip-target', `tooltip-recipient-${uniqueId}`); - row.querySelector('[data-tooltip-target^="tooltip-wallet"]').setAttribute('data-tooltip-target', `tooltip-wallet-${uniqueId}`); - row.querySelector('[data-tooltip-target^="tooltip-offer"]').setAttribute('data-tooltip-target', `tooltip-offer-${uniqueId}`); - row.querySelector('[data-tooltip-target^="tooltip-wallet-maker"]').setAttribute('data-tooltip-target', `tooltip-wallet-maker-${uniqueId}`); - row.querySelector('[data-tooltip-target^="tooltip-rate"]').setAttribute('data-tooltip-target', `tooltip-rate-${uniqueId}`); - row.querySelector('[data-tooltip-target^="percentage-tooltip"]').setAttribute('data-tooltip-target', `percentage-tooltip-${uniqueId}`); -} - -function getCoinSymbolLowercase(coin) { - if (typeof coin === 'string') { - if (coin.toLowerCase() === 'bitcoin cash') { - return 'bitcoin-cash'; +function updateClearFiltersButton() { + const clearButton = document.getElementById('clearFilters'); + if (clearButton) { + clearButton.classList.toggle('opacity-50', !hasActiveFilters()); + clearButton.disabled = !hasActiveFilters(); } - return (coinNameToSymbol[coin] || coin).toLowerCase(); - } else if (coin && typeof coin === 'object' && coin.symbol) { - return coin.symbol.toLowerCase(); - } else { - console.warn('Invalid coin input:', coin); - return 'unknown'; - } } -function createTooltipContent(isSentOffers, coinFrom, coinTo, fromAmount, toAmount, isOwnOffer) { - if (!coinFrom || !coinTo) { - console.error(`Invalid coin names: coinFrom=${coinFrom}, coinTo=${coinTo}`); - return `

Unable to calculate profit/loss

-

Invalid coin data.

`; - } +function setRefreshButtonLoading(isLoading) { + const refreshButton = document.getElementById('refreshOffers'); + const refreshIcon = document.getElementById('refreshIcon'); + const refreshText = document.getElementById('refreshText'); - fromAmount = parseFloat(fromAmount) || 0; - toAmount = parseFloat(toAmount) || 0; - - const getPriceKey = (coin) => { - 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 toPriceUSD = latestPrices[toSymbol]?.usd; - - if (!fromPriceUSD || !toPriceUSD) { - return `

Unable to calculate profit/loss

-

Price data is missing for one or both coins.

`; - } - - const fromValueUSD = fromAmount * fromPriceUSD; - const toValueUSD = toAmount * toPriceUSD; - const profitUSD = toValueUSD - fromValueUSD; - - const marketRate = fromPriceUSD / toPriceUSD; - const offerRate = toAmount / fromAmount; - let percentDiff; - - if (isSentOffers || isOwnOffer) { - percentDiff = ((toValueUSD / fromValueUSD) - 1) * 100; - } else { - percentDiff = ((fromValueUSD / toValueUSD) - 1) * 100; - } - - const formattedPercentDiff = percentDiff.toFixed(2); - const percentDiffDisplay = formattedPercentDiff === "0.00" ? "0.00" : - (percentDiff > 0 ? `+${formattedPercentDiff}` : formattedPercentDiff); - - const profitLabel = (isSentOffers || isOwnOffer) ? "Max Profit" : "Max Loss"; - const actionLabel = (isSentOffers || isOwnOffer) ? "selling" : "buying"; - const directionLabel = (isSentOffers || isOwnOffer) ? "receiving" : "paying"; - - return ` -

Profit/Loss Calculation:

-

You are ${actionLabel} ${fromAmount.toFixed(8)} ${coinFrom} ($${fromValueUSD.toFixed(2)} USD)
and ${directionLabel} ${toAmount.toFixed(8)} ${coinTo} ($${toValueUSD.toFixed(2)} USD).

-

Percentage difference: ${percentDiffDisplay}%

-

${profitLabel}: ${profitUSD > 0 ? '' : '-'}$${profitUSD.toFixed(2)} USD

-

Calculation:

-

Percentage = ${(isSentOffers || isOwnOffer) ? - "((To Amount in USD / From Amount in USD) - 1) * 100" : - "((From Amount in USD / To Amount in USD) - 1) * 100"}

-

USD ${profitLabel} = To Amount in USD - From Amount in USD

-

Interpretation:

- ${(isSentOffers || isOwnOffer) ? ` -

Positive percentage: You're selling above market rate (profitable)

-

Negative percentage: You're selling below market rate (loss)

- ` : ` -

Positive percentage: You're buying below market rate (savings)

-

Negative percentage: You're buying above market rate (premium)

- `} -

Note: ${(isSentOffers || isOwnOffer) ? - "As a seller, a positive percentage means
you're selling for more than the current market value." : - "As a buyer, a positive percentage indicates
potential savings compared to current market rates."}

-

Market Rate: 1 ${coinFrom} = ${marketRate.toFixed(8)} ${coinTo}

-

Offer Rate: 1 ${coinFrom} = ${offerRate.toFixed(8)} ${coinTo}

- `; + refreshButton.disabled = isLoading; + refreshIcon.classList.toggle('animate-spin', isLoading); + refreshText.textContent = isLoading ? 'Refresh' : 'Refresh'; } -function createCombinedRateTooltip(offer, coinFrom, coinTo, isSentOffers, treatAsSentOffer) { - const rate = parseFloat(offer.rate); - const inverseRate = 1 / rate; - - const getPriceKey = (coin) => { - const lowerCoin = coin.toLowerCase(); - if (lowerCoin === 'firo' || lowerCoin === 'zcoin') { - return 'zcoin'; - } - if (lowerCoin === 'bitcoin cash') { - return 'bitcoin-cash'; - } - return coinNameToSymbol[coin] || lowerCoin; - }; - - const fromSymbol = getPriceKey(coinFrom); - const toSymbol = getPriceKey(coinTo); - - const fromPriceUSD = latestPrices[fromSymbol]?.usd || 0; - const toPriceUSD = latestPrices[toSymbol]?.usd || 0; - const rateInUSD = rate * toPriceUSD; - - const marketRate = fromPriceUSD / toPriceUSD; - - const percentDiff = ((rate - marketRate) / marketRate) * 100; - const formattedPercentDiff = percentDiff.toFixed(2); - const percentDiffDisplay = formattedPercentDiff === "0.00" ? "0.00" : - (percentDiff > 0 ? `+${formattedPercentDiff}` : formattedPercentDiff); - const aboveOrBelow = percentDiff > 0 ? "above" : percentDiff < 0 ? "below" : "at"; - - const action = isSentOffers || treatAsSentOffer ? "selling" : "buying"; - - return ` -

Exchange Rate Explanation:

-

This offer is ${action} ${coinFrom} for ${coinTo}
at a rate that is ${percentDiffDisplay}% ${aboveOrBelow} market price.

-

Exchange Rates:

-

1 ${coinFrom} = ${rate.toFixed(8)} ${coinTo}

-

1 ${coinTo} = ${inverseRate.toFixed(8)} ${coinFrom}

-

USD Equivalent:

-

1 ${coinFrom} = $${rateInUSD.toFixed(2)} USD

-

Current market prices:

-

${coinFrom}: $${fromPriceUSD.toFixed(2)} USD

-

${coinTo}: $${toPriceUSD.toFixed(2)} USD

-

Market rate: 1 ${coinFrom} = ${marketRate.toFixed(8)} ${coinTo}

- `; -} - -function updatePaginationInfo() { - const validOffers = getValidOffers(); - const validItemCount = validOffers.length; - const totalPages = Math.max(1, Math.ceil(validItemCount / itemsPerPage)); - - currentPage = Math.max(1, Math.min(currentPage, totalPages)); - - currentPageSpan.textContent = currentPage; - totalPagesSpan.textContent = totalPages; - - prevPageButton.classList.toggle('invisible', currentPage === 1 || validItemCount === 0); - nextPageButton.classList.toggle('invisible', currentPage === totalPages || validItemCount === 0); - - prevPageButton.style.display = currentPage === 1 ? 'none' : 'inline-flex'; - nextPageButton.style.display = (currentPage === totalPages || validItemCount === 0) ? 'none' : 'inline-flex'; - - if (lastRefreshTime) { - lastRefreshTimeSpan.textContent = new Date(lastRefreshTime).toLocaleTimeString(); - } - - const newEntriesCountSpan = document.getElementById('newEntriesCount'); - if (newEntriesCountSpan) { - newEntriesCountSpan.textContent = validItemCount; - } - - logOfferStatus(); -} - -function updateJsonView() { - jsonContent.textContent = JSON.stringify(jsonData, null, 2); -} - -function updateLastRefreshTime() { - lastRefreshTimeSpan.textContent = new Date(lastRefreshTime).toLocaleTimeString(); -} - -function updateNextRefreshTime() { - if (isSentOffers) return; - - const nextRefreshTimeSpan = document.getElementById('nextRefreshTime'); - if (!nextRefreshTimeSpan) { - console.warn('nextRefreshTime element not found'); +function initializeFlowbiteTooltips() { + if (typeof Tooltip === 'undefined') { + console.warn('Tooltip is not defined. Make sure the required library is loaded.'); return; } - const minutes = Math.floor(nextRefreshCountdown / 60); - const seconds = nextRefreshCountdown % 60; - - nextRefreshTimeSpan.textContent = `${minutes}m ${seconds.toString().padStart(2, '0')}s`; - // console.log(`Next refresh in: ${minutes}m ${seconds}s`); + const tooltipElements = document.querySelectorAll('[data-tooltip-target]'); + tooltipElements.forEach((el) => { + const tooltipId = el.getAttribute('data-tooltip-target'); + const tooltipElement = document.getElementById(tooltipId); + if (tooltipElement) { + new Tooltip(tooltipElement, el); + } + }); } -function updateRowTimes() { - const currentTime = Math.floor(Date.now() / 1000); - const rows = document.querySelectorAll('[data-offer-id]'); - - rows.forEach(row => { - const offerId = row.getAttribute('data-offer-id'); - const offer = jsonData.find(o => o.offer_id === offerId); - if (!offer) return; +function initializeFooter() { + if (isSentOffers) { + const nextRefreshContainer = document.getElementById('nextRefreshContainer'); + if (nextRefreshContainer) { + nextRefreshContainer.style.display = 'none'; + } - const timeColumn = row.querySelector('td:first-child'); - if (!timeColumn) return; - - const postedTime = formatTimeAgo(offer.created_at); - const expiresIn = formatTimeLeft(offer.expire_at); - const timerColor = getTimerColor(offer); - - const svg = timeColumn.querySelector('svg'); - if (svg) { - svg.querySelector('g').setAttribute('stroke', timerColor); - svg.querySelector('polyline').setAttribute('stroke', timerColor); + if (typeof nextRefreshCountdown !== 'undefined') { + clearInterval(nextRefreshCountdown); + } } - - const textContainer = timeColumn.querySelector('.xl\\:block'); - if (textContainer) { - const postedElement = textContainer.querySelector('.text-xs:first-child'); - const expiresElement = textContainer.querySelector('.text-xs:last-child'); - - if (postedElement) postedElement.textContent = `Posted: ${postedTime}`; - if (expiresElement) expiresElement.textContent = `Expires in: ${expiresIn}`; - } - - const tooltipElement = document.getElementById(`tooltip-active${offerId}`); - if (tooltipElement) { - const tooltipContent = tooltipElement.querySelector('.active-revoked-expired'); - if (tooltipContent) { - const postedElement = tooltipContent.querySelector('.text-xs:first-child'); - const expiresElement = tooltipContent.querySelector('.text-xs:last-child'); - - if (postedElement) postedElement.textContent = `Posted: ${postedTime}`; - if (expiresElement) expiresElement.textContent = `Expires in: ${expiresIn}`; - } - } - }); } function updateCoinFilterImages() { @@ -1656,66 +496,1074 @@ function updateCoinFilterImages() { updateButtonImage(coinFromSelect, coinFromButton); } -function updateCoinFilterOptions() { - const coinToSelect = document.getElementById('coin_to'); - const coinFromSelect = document.getElementById('coin_from'); - - const updateOptions = (select) => { - const options = select.options; - for (let i = 0; i < options.length; i++) { - const option = options[i]; - if (option.value !== 'any') { - const displayName = symbolToCoinName[option.value] || option.value; - option.textContent = displayName; - } - } - }; +function applyFilters() { + console.log('Applying filters'); - updateOptions(coinToSelect); - updateOptions(coinFromSelect); + setTimeout(() => { + jsonData = filterAndSortData(); + updateOffersTable(); + updateJsonView(); + updatePaginationInfo(); + updateClearFiltersButton(); + console.log('Filters applied, table updated'); + }, 100); } -function initializeFlowbiteTooltips() { - if (typeof Tooltip === 'undefined') { - console.warn('Tooltip is not defined. Make sure the required library is loaded.'); +function updateRowTimes() { + const rows = document.querySelectorAll('[data-offer-id]'); + rows.forEach(row => { + const offerId = row.getAttribute('data-offer-id'); + const offer = jsonData.find(o => o.offer_id === offerId); + if (!offer) return; + + const timeColumn = row.querySelector('td:first-child'); + if (!timeColumn) return; + + const postedTime = formatTimeAgo(offer.created_at); + const expiresIn = formatTimeLeft(offer.expire_at); + const timerColor = getTimerColor(offer); + + const svg = timeColumn.querySelector('svg'); + if (svg) { + svg.querySelector('g').setAttribute('stroke', timerColor); + svg.querySelector('polyline').setAttribute('stroke', timerColor); + } + + const textContainer = timeColumn.querySelector('.xl\\:block'); + if (textContainer) { + const postedElement = textContainer.querySelector('.text-xs:first-child'); + const expiresElement = textContainer.querySelector('.text-xs:last-child'); + + if (postedElement) postedElement.textContent = `Posted: ${postedTime}`; + if (expiresElement) expiresElement.textContent = `Expires in: ${expiresIn}`; + } + }); +} + +function updateJsonView() { + jsonContent.textContent = JSON.stringify(jsonData, null, 2); +} + +function updateLastRefreshTime() { + if (lastRefreshTimeSpan) { + lastRefreshTimeSpan.textContent = lastRefreshTime ? new Date(lastRefreshTime).toLocaleTimeString() : 'Never'; + } +} + +function updateNextRefreshTime() { + if (isSentOffers) return; + + const nextRefreshTimeSpan = document.getElementById('nextRefreshTime'); + if (!nextRefreshTimeSpan) { + console.warn('nextRefreshTime element not found'); return; } - const tooltipElements = document.querySelectorAll('[data-tooltip-target]'); - tooltipElements.forEach((el) => { - const tooltipId = el.getAttribute('data-tooltip-target'); - const tooltipElement = document.getElementById(tooltipId); - if (tooltipElement) { - new Tooltip(tooltipElement, el); + const minutes = Math.floor(nextRefreshCountdown / 60); + const seconds = nextRefreshCountdown % 60; + nextRefreshTimeSpan.textContent = `${minutes}m ${seconds.toString().padStart(2, '0')}s`; +} + +function updatePaginationInfo() { + const validOffers = getValidOffers(); + const totalItems = validOffers.length; + const totalPages = Math.max(1, Math.ceil(totalItems / itemsPerPage)); + + currentPage = Math.max(1, Math.min(currentPage, totalPages)); + + currentPageSpan.textContent = currentPage; + totalPagesSpan.textContent = totalPages; + + const showPrev = currentPage > 1; + const showNext = currentPage < totalPages && totalItems > 0; + + prevPageButton.style.display = showPrev ? 'inline-flex' : 'none'; + nextPageButton.style.display = showNext ? 'inline-flex' : 'none'; + + if (lastRefreshTime) { + lastRefreshTimeSpan.textContent = new Date(lastRefreshTime).toLocaleTimeString(); + } + + if (newEntriesCountSpan) { + newEntriesCountSpan.textContent = totalItems; + } +} + +function updatePaginationControls(totalPages) { + prevPageButton.style.display = currentPage > 1 ? 'inline-flex' : 'none'; + nextPageButton.style.display = currentPage < totalPages ? 'inline-flex' : 'none'; + currentPageSpan.textContent = currentPage; + totalPagesSpan.textContent = totalPages; +} + +function checkExpiredAndFetchNew() { + if (isSentOffers) return Promise.resolve(); + + console.log('Starting checkExpiredAndFetchNew'); + + return fetch('/json/offers') + .then(response => response.json()) + .then(data => { + let newListings = Array.isArray(data) ? data : Object.values(data); + + newListings = newListings.map(offer => ({ + ...offer, + offer_id: String(offer.offer_id || ''), + swap_type: String(offer.swap_type || 'N/A'), + addr_from: String(offer.addr_from || ''), + coin_from: String(offer.coin_from || ''), + coin_to: String(offer.coin_to || ''), + amount_from: String(offer.amount_from || '0'), + amount_to: String(offer.amount_to || '0'), + rate: String(offer.rate || '0'), + created_at: Number(offer.created_at || 0), + expire_at: Number(offer.expire_at || 0), + is_own_offer: Boolean(offer.is_own_offer), + amount_negotiable: Boolean(offer.amount_negotiable), + unique_id: `${offer.offer_id}_${offer.created_at}_${offer.coin_from}_${offer.coin_to}` + })); + + newListings = newListings.filter(offer => !isOfferExpired(offer)); + originalJsonData = newListings; + + const currentFilters = new FormData(filterForm); + const hasActiveFilters = currentFilters.get('coin_to') !== 'any' || + currentFilters.get('coin_from') !== 'any'; + + if (hasActiveFilters) { + jsonData = filterAndSortData(); + } else { + jsonData = [...newListings]; + } + + updateOffersTable(); + updateJsonView(); + updatePaginationInfo(); + + if (jsonData.length === 0) { + handleNoOffersScenario(); + } + + nextRefreshCountdown = getTimeUntilNextExpiration(); + console.log(`Next refresh in ${nextRefreshCountdown} seconds`); + + return jsonData.length; + }) + .catch(error => { + console.error('Error fetching new listings:', error); + nextRefreshCountdown = 60; + return Promise.reject(error); + }); +} + +function createTimeColumn(offer, postedTime, expiresIn) { + const timerColor = getTimerColor(offer); + return ` + +
+
+ + + + + + +
+ +
+ + `; +} + +function createDetailsColumn(offer) { + const addrFrom = offer.addr_from || ''; + return ` + + + Recipient: ${escapeHtml(addrFrom.substring(0, 10))}... + + + `; +} + +function createTakerAmountColumn(offer, coinTo, coinFrom) { + const fromAmount = parseFloat(offer.amount_to); + const fromSymbol = getCoinSymbol(coinFrom); + return ` + +
+ +
+
${fromAmount.toFixed(4)}
+
${coinFrom}
+
+
+
+ + `; +} + +function createSwapColumn(offer, coinFromDisplay, coinToDisplay, coinFromSymbol, coinToSymbol) { + const getImageFilename = (symbol, displayName) => { + if (displayName.toLowerCase() === 'zcoin' || displayName.toLowerCase() === 'firo') { + return 'Firo.png'; + } + return `${displayName.replace(' ', '-')}.png`; + }; + + return ` + + +
+ + ${coinToDisplay} + + + + ${coinFromDisplay} + +
+
+ + `; +} + +function createOrderbookColumn(offer, coinFrom, coinTo) { + const toAmount = parseFloat(offer.amount_from); + const toSymbol = getCoinSymbol(coinTo); + return ` + +
+ +
+
${toAmount.toFixed(4)}
+
${coinTo}
+
+
+
+ + `; +} + +function createRateColumn(offer, coinFrom, coinTo) { + const rate = parseFloat(offer.rate); + const inverseRate = 1 / rate; + const fromSymbol = getCoinSymbol(coinFrom); + const toSymbol = getCoinSymbol(coinTo); + + const getPriceKey = (coin) => { + const lowerCoin = coin.toLowerCase(); + if (lowerCoin === 'firo' || lowerCoin === 'zcoin') { + return 'zcoin'; + } + if (lowerCoin === 'bitcoin cash') { + return 'bitcoin-cash'; + } + return coinNameToSymbol[coin] || lowerCoin; + }; + + const fromPriceUSD = latestPrices[getPriceKey(coinFrom)]?.usd || 0; + const toPriceUSD = latestPrices[getPriceKey(coinTo)]?.usd || 0; + const rateInUSD = rate * toPriceUSD; + + return ` + +
+
+ + $${rateInUSD.toFixed(2)} USD + + + ${rate.toFixed(8)} ${toSymbol}/${fromSymbol} + + + ${inverseRate.toFixed(8)} ${fromSymbol}/${toSymbol} + +
+
+ + `; +} + +function createPercentageColumn(offer) { + return ` + +
+
+ + Calculating... + +
+
+ + `; +} + +function createActionColumn(offer, buttonClass, buttonText) { + return ` + +
+ + ${buttonText} + +
+ + `; +} + +function updateProfitLoss(row, fromCoin, toCoin, fromAmount, toAmount, isOwnOffer) { + const profitLossElement = row.querySelector('.profit-loss'); + if (!profitLossElement) { + console.warn('Profit loss element not found in row'); + return; + } + + if (!fromCoin || !toCoin) { + console.error(`Invalid coin names: fromCoin=${fromCoin}, toCoin=${toCoin}`); + profitLossElement.textContent = 'Error'; + profitLossElement.className = 'profit-loss text-lg font-bold text-red-500'; + return; + } + + calculateProfitLoss(fromCoin, toCoin, fromAmount, toAmount, isOwnOffer) + .then(percentDiff => { + if (percentDiff === null) { + profitLossElement.textContent = 'N/A'; + profitLossElement.className = 'profit-loss text-lg font-bold text-gray-400'; + return; + } + + const formattedPercentDiff = percentDiff.toFixed(2); + const percentDiffDisplay = formattedPercentDiff === "0.00" ? "0.00" : + (percentDiff > 0 ? `+${formattedPercentDiff}` : formattedPercentDiff); + + const colorClass = getProfitColorClass(percentDiff); + profitLossElement.textContent = `${percentDiffDisplay}%`; + profitLossElement.className = `profit-loss text-lg font-bold ${colorClass}`; + + const tooltipId = `percentage-tooltip-${row.getAttribute('data-offer-id')}`; + const tooltipElement = document.getElementById(tooltipId); + if (tooltipElement) { + const tooltipContent = createTooltipContent(isSentOffers || isOwnOffer, fromCoin, toCoin, fromAmount, toAmount); + tooltipElement.innerHTML = ` +
+ ${tooltipContent} +
+
+ `; + } + }) + .catch(error => { + console.error('Error in updateProfitLoss:', error); + profitLossElement.textContent = 'Error'; + profitLossElement.className = 'profit-loss text-lg font-bold text-red-500'; + }); +} + +function createTooltips(offer, treatAsSentOffer, coinFrom, coinTo, fromAmount, toAmount, postedTime, expiresIn, isActuallyExpired, isRevoked) { + const rate = parseFloat(offer.rate); + const fromSymbol = getCoinSymbolLowercase(coinFrom); + const toSymbol = getCoinSymbolLowercase(coinTo); + const uniqueId = `${offer.offer_id}_${offer.created_at}`; + + const fromPriceUSD = latestPrices[fromSymbol]?.usd || 0; + const toPriceUSD = latestPrices[toSymbol]?.usd || 0; + const rateInUSD = rate * toPriceUSD; + + const combinedRateTooltip = createCombinedRateTooltip(offer, coinFrom, coinTo, treatAsSentOffer); + const percentageTooltipContent = createTooltipContent(treatAsSentOffer, coinFrom, coinTo, fromAmount, toAmount); + + return ` + + + + + + + + + + + + + + `; +} + +function createTooltipContent(isSentOffers, coinFrom, coinTo, fromAmount, toAmount, isOwnOffer) { + if (!coinFrom || !coinTo) { + console.error(`Invalid coin names: coinFrom=${coinFrom}, coinTo=${coinTo}`); + return `

Unable to calculate profit/loss

+

Invalid coin data.

`; + } + + fromAmount = parseFloat(fromAmount) || 0; + toAmount = parseFloat(toAmount) || 0; + + const getPriceKey = (coin) => { + 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 toPriceUSD = latestPrices[toSymbol]?.usd; + + if (!fromPriceUSD || !toPriceUSD) { + return `

Unable to calculate profit/loss

+

Price data is missing for one or both coins.

`; + } + + const fromValueUSD = fromAmount * fromPriceUSD; + const toValueUSD = toAmount * toPriceUSD; + const profitUSD = toValueUSD - fromValueUSD; + + const marketRate = fromPriceUSD / toPriceUSD; + const offerRate = toAmount / fromAmount; + let percentDiff; + + if (isSentOffers || isOwnOffer) { + percentDiff = ((toValueUSD / fromValueUSD) - 1) * 100; + } else { + percentDiff = ((fromValueUSD / toValueUSD) - 1) * 100; + } + + const formattedPercentDiff = percentDiff.toFixed(2); + const percentDiffDisplay = formattedPercentDiff === "0.00" ? "0.00" : + (percentDiff > 0 ? `+${formattedPercentDiff}` : formattedPercentDiff); + + const profitLabel = (isSentOffers || isOwnOffer) ? "Max Profit" : "Max Loss"; + const actionLabel = (isSentOffers || isOwnOffer) ? "selling" : "buying"; + const directionLabel = (isSentOffers || isOwnOffer) ? "receiving" : "paying"; + + return ` +

Profit/Loss Calculation:

+

You are ${actionLabel} ${fromAmount.toFixed(8)} ${coinFrom} ($${fromValueUSD.toFixed(2)} USD)
and ${directionLabel} ${toAmount.toFixed(8)} ${coinTo} ($${toValueUSD.toFixed(2)} USD).

+

Percentage difference: ${percentDiffDisplay}%

+

${profitLabel}: ${profitUSD > 0 ? '' : '-'}$${Math.abs(profitUSD).toFixed(2)} USD

+

Calculation:

+

Percentage = ${(isSentOffers || isOwnOffer) ? + "((To Amount in USD / From Amount in USD) - 1) * 100" : + "((From Amount in USD / To Amount in USD) - 1) * 100"}

+

USD ${profitLabel} = To Amount in USD - From Amount in USD

+

Interpretation:

+ ${(isSentOffers || isOwnOffer) ? ` +

Positive percentage: You're selling above market rate (profitable)

+

Negative percentage: You're selling below market rate (loss)

+ ` : ` +

Positive percentage: You're buying below market rate (savings)

+

Negative percentage: You're buying above market rate (premium)

+ `} +

Note: ${(isSentOffers || isOwnOffer) ? + "As a seller, a positive percentage means
you're selling for more than the current market value." : + "As a buyer, a positive percentage indicates
potential savings compared to current market rates."}

+

Market Rate: 1 ${coinFrom} = ${marketRate.toFixed(8)} ${coinTo}

+

Offer Rate: 1 ${coinFrom} = ${offerRate.toFixed(8)} ${coinTo}

+ `; +} + +function createCombinedRateTooltip(offer, coinFrom, coinTo, isSentOffers, treatAsSentOffer) { + const rate = parseFloat(offer.rate); + const inverseRate = 1 / rate; + + const getPriceKey = (coin) => { + const lowerCoin = coin.toLowerCase(); + if (lowerCoin === 'firo' || lowerCoin === 'zcoin') { + return 'zcoin'; + } + if (lowerCoin === 'bitcoin cash') { + return 'bitcoin-cash'; + } + return coinNameToSymbol[coin] || lowerCoin; + }; + + const fromSymbol = getPriceKey(coinFrom); + const toSymbol = getPriceKey(coinTo); + + const fromPriceUSD = latestPrices[fromSymbol]?.usd || 0; + const toPriceUSD = latestPrices[toSymbol]?.usd || 0; + const rateInUSD = rate * toPriceUSD; + + const marketRate = fromPriceUSD / toPriceUSD; + + const percentDiff = ((rate - marketRate) / marketRate) * 100; + const formattedPercentDiff = percentDiff.toFixed(2); + const percentDiffDisplay = formattedPercentDiff === "0.00" ? "0.00" : + (percentDiff > 0 ? `+${formattedPercentDiff}` : formattedPercentDiff); + const aboveOrBelow = percentDiff > 0 ? "above" : percentDiff < 0 ? "below" : "at"; + + const action = isSentOffers || treatAsSentOffer ? "selling" : "buying"; + + return ` +

Exchange Rate Explanation:

+

This offer is ${action} ${coinFrom} for ${coinTo}
at a rate that is ${percentDiffDisplay}% ${aboveOrBelow} market price.

+

Exchange Rates:

+

1 ${coinFrom} = ${rate.toFixed(8)} ${coinTo}

+

1 ${coinTo} = ${inverseRate.toFixed(8)} ${coinFrom}

+

USD Equivalent:

+

1 ${coinFrom} = $${rateInUSD.toFixed(2)} USD

+

Current market prices:

+

${coinFrom}: $${fromPriceUSD.toFixed(2)} USD

+

${coinTo}: $${toPriceUSD.toFixed(2)} USD

+

Market rate: 1 ${coinFrom} = ${marketRate.toFixed(8)} ${coinTo}

+ `; +} + +function updateTooltipTargets(row, uniqueId) { + const tooltipElements = [ + { prefix: 'tooltip-active', selector: '[data-tooltip-target^="tooltip-active"]' }, + { prefix: 'tooltip-recipient', selector: '[data-tooltip-target^="tooltip-recipient"]' }, + { prefix: 'tooltip-wallet', selector: '[data-tooltip-target^="tooltip-wallet"]' }, + { prefix: 'tooltip-offer', selector: '[data-tooltip-target^="tooltip-offer"]' }, + { prefix: 'tooltip-wallet-maker', selector: '[data-tooltip-target^="tooltip-wallet-maker"]' }, + { prefix: 'tooltip-rate', selector: '[data-tooltip-target^="tooltip-rate"]' }, + { prefix: 'percentage-tooltip', selector: '[data-tooltip-target^="percentage-tooltip"]' } + ]; + + tooltipElements.forEach(({ prefix, selector }) => { + const element = row.querySelector(selector); + if (element) { + element.setAttribute('data-tooltip-target', `${prefix}-${uniqueId}`); } }); } +function clearFilters() { + filterForm.reset(); + const statusSelect = document.getElementById('status'); + if (statusSelect) { + statusSelect.value = 'any'; + } + jsonData = [...originalJsonData]; + currentPage = 1; + updateOffersTable(); + updateJsonView(); + updateCoinFilterImages(); + updateClearFiltersButton(); +} + +async function fetchLatestPrices() { + const MAX_RETRIES = 3; + const BASE_DELAY = 1000; + + const cachedData = getCachedPrices(); + if (cachedData) { + latestPrices = cachedData; + return cachedData; + } + + const url = 'https://api.coingecko.com/api/v3/simple/price?ids=bitcoin,bitcoin-cash,dash,dogecoin,decred,litecoin,particl,pivx,monero,zano,wownero,zcoin&vs_currencies=USD,BTC'; + + for (let attempt = 0; attempt < MAX_RETRIES; attempt++) { + try { + const data = await makePostRequest(url); + if (data && Object.keys(data).length > 0) { + latestPrices = data; + setCachedPrices(data); + // Store as fallback values with 24h expiry + Object.entries(data).forEach(([coin, prices]) => { + tableRateModule.setFallbackValue(coin, prices.usd); + }); + return data; + } + } catch (error) { + const delay = Math.min(BASE_DELAY * Math.pow(2, attempt), 10000); + await new Promise(resolve => setTimeout(resolve, delay)); + } + } + + return getFallbackPrices(); +} + +function getFallbackPrices() { + const fallbacks = {}; + const coins = ['bitcoin', 'bitcoin-cash', 'dash', 'dogecoin', 'decred', 'litecoin', + 'particl', 'pivx', 'monero', 'zano', 'wownero', 'zcoin']; + + for (const coin of coins) { + const fallbackValue = tableRateModule.getFallbackValue(coin); + if (fallbackValue) { + fallbacks[coin] = { usd: fallbackValue }; + } + } + + return Object.keys(fallbacks).length > 0 ? fallbacks : null; +} + +async function fetchOffers(manualRefresh = false) { + return new Promise((resolve, reject) => { + const endpoint = isSentOffers ? '/json/sentoffers' : '/json/offers'; + console.log(`[Debug] Fetching from endpoint: ${endpoint}`); + + fetch(endpoint, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + with_extra_info: true, + limit: 1000 + }) + }) + .then(response => response.json()) + .then(data => { + console.log('[Debug] Raw data received:', data); + + let newData = Array.isArray(data) ? data : Object.values(data); + + newData = newData.map(offer => ({ + ...offer, + offer_id: String(offer.offer_id || ''), + swap_type: String(offer.swap_type || 'N/A'), + addr_from: String(offer.addr_from || ''), + coin_from: String(offer.coin_from || ''), + coin_to: String(offer.coin_to || ''), + amount_from: String(offer.amount_from || '0'), + amount_to: String(offer.amount_to || '0'), + rate: String(offer.rate || '0'), + created_at: Number(offer.created_at || 0), + expire_at: Number(offer.expire_at || 0), + is_own_offer: Boolean(offer.is_own_offer), + amount_negotiable: Boolean(offer.amount_negotiable), + is_revoked: Boolean(offer.is_revoked), + unique_id: `${offer.offer_id}_${offer.created_at}_${offer.coin_from}_${offer.coin_to}` + })); + + if (isInitialLoad || manualRefresh) { + jsonData = newData; + originalJsonData = [...newData]; + } else { + const mergedData = mergeSentOffers(jsonData, newData); + jsonData = mergedData; + originalJsonData = [...mergedData]; + } + + if (newEntriesCountSpan) { + newEntriesCountSpan.textContent = jsonData.length; + } + + updateOffersTable(); + updateJsonView(); + updatePaginationInfo(); + + resolve(); + }) + .catch(error => { + console.error('[Debug] Error fetching offers:', error); + reject(error); + }); + }); +} + +function mergeSentOffers(existingOffers, newOffers) { + console.log('[Debug] Merging offers:', { + existing: existingOffers.length, + new: newOffers.length + }); + + const offerMap = new Map(); + existingOffers.forEach(offer => { + offerMap.set(offer.offer_id, offer); + }); + + newOffers.forEach(offer => { + offerMap.set(offer.offer_id, offer); + }); + + const mergedOffers = Array.from(offerMap.values()); + console.log('[Debug] After merge:', mergedOffers.length); + + return mergedOffers; +} + +function getValidOffers() { + if (!jsonData) { + console.warn('jsonData is undefined or null'); + return []; + } + + const filteredData = filterAndSortData(); + console.log(`getValidOffers: Found ${filteredData.length} valid offers`); + return filteredData; +} + +function filterAndSortData() { + console.log('[Debug] Starting filter with data length:', originalJsonData.length); + + const formData = new FormData(filterForm); + const filters = Object.fromEntries(formData); + console.log('[Debug] Active filters:', filters); + + if (filters.coin_to !== 'any') { + filters.coin_to = coinIdToName[filters.coin_to] || filters.coin_to; + } + if (filters.coin_from !== 'any') { + filters.coin_from = coinIdToName[filters.coin_from] || filters.coin_from; + } + + let filteredData = [...originalJsonData]; + + filteredData = filteredData.filter(offer => { + if (!isSentOffers && isOfferExpired(offer)) { + return false; + } + + const coinFrom = (offer.coin_from || '').toLowerCase(); + const coinTo = (offer.coin_to || '').toLowerCase(); + + if (filters.coin_to !== 'any') { + if (!coinMatches(coinTo, filters.coin_to)) { + return false; + } + } + + if (filters.coin_from !== 'any') { + if (!coinMatches(coinFrom, filters.coin_from)) { + return false; + } + } + + if (isSentOffers && filters.status && filters.status !== 'any') { + const currentTime = Math.floor(Date.now() / 1000); + const isExpired = offer.expire_at <= currentTime; + const isRevoked = Boolean(offer.is_revoked); + + switch (filters.status) { + case 'active': + return !isExpired && !isRevoked; + case 'expired': + return isExpired && !isRevoked; + case 'revoked': + return isRevoked; + default: + return true; + } + } + + return true; + }); + + return filteredData; +} + +async function calculateProfitLoss(fromCoin, toCoin, fromAmount, toAmount, isOwnOffer) { + console.log(`Calculating profit/loss for ${fromAmount} ${fromCoin} to ${toAmount} ${toCoin}, isOwnOffer: ${isOwnOffer}`); + + if (!latestPrices) { + console.error('Latest prices not available. Unable to calculate profit/loss.'); + return null; + } + + const getPriceKey = (coin) => { + const lowerCoin = coin.toLowerCase(); + if (lowerCoin === 'firo' || lowerCoin === 'zcoin') { + return 'zcoin'; + } + if (lowerCoin === 'bitcoin cash') { + return 'bitcoin-cash'; + } + return coinNameToSymbol[coin] || lowerCoin; + }; + + const fromSymbol = getPriceKey(fromCoin); + const toSymbol = getPriceKey(toCoin); + + const fromPriceUSD = latestPrices[fromSymbol]?.usd; + const toPriceUSD = latestPrices[toSymbol]?.usd; + + if (!fromPriceUSD || !toPriceUSD) { + console.error(`Price data missing for ${fromSymbol} or ${toSymbol}`); + return null; + } + + const fromValueUSD = fromAmount * fromPriceUSD; + const toValueUSD = toAmount * toPriceUSD; + + let percentDiff; + if (isOwnOffer) { + percentDiff = ((toValueUSD / fromValueUSD) - 1) * 100; + } else { + percentDiff = ((fromValueUSD / toValueUSD) - 1) * 100; + } + + console.log(`Percent difference: ${percentDiff.toFixed(2)}%`); + return percentDiff; +} + +async function getMarketRate(fromCoin, toCoin) { + return new Promise((resolve) => { + console.log(`Attempting to get market rate for ${fromCoin} to ${toCoin}`); + if (!latestPrices) { + console.warn('Latest prices object is not available'); + resolve(null); + return; + } + + const getPriceKey = (coin) => { + const lowerCoin = coin.toLowerCase(); + if (lowerCoin === 'firo' || lowerCoin === 'zcoin') { + return 'zcoin'; + } + if (lowerCoin === 'bitcoin cash') { + return 'bitcoin-cash'; + } + 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) { + console.warn(`Missing price data for ${!fromPrice ? fromCoin : toCoin}`); + resolve(null); + return; + } + const rate = toPrice / fromPrice; + console.log(`Market rate calculated: ${rate} ${toCoin}/${fromCoin}`); + resolve(rate); + }); +} + +function handleNoOffersScenario() { + const formData = new FormData(filterForm); + const filters = Object.fromEntries(formData); + const hasActiveFilters = filters.coin_to !== 'any' || + filters.coin_from !== 'any' || + (filters.status && filters.status !== 'any'); + + if (hasActiveFilters) { + offersBody.innerHTML = ` + + + No offers match the selected filters. Try different filter options or + + + `; + } else { + offersBody.innerHTML = ` + + + No active offers available. ${!isSentOffers ? 'Refreshing data...' : ''} + + `; + if (!isSentOffers) { + setTimeout(() => fetchOffers(true), 2000); + } + } +} + +function createTableRow(offer, isSentOffers) { + const row = document.createElement('tr'); + const uniqueId = `${offer.offer_id}_${offer.created_at}`; + 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', uniqueId); + + const coinFrom = offer.coin_from; + const coinTo = offer.coin_to; + const coinFromSymbol = coinNameToSymbol[coinFrom] || coinFrom.toLowerCase(); + const coinToSymbol = coinNameToSymbol[coinTo] || coinTo.toLowerCase(); + const coinFromDisplay = getDisplayName(coinFrom); + const coinToDisplay = getDisplayName(coinTo); + + const postedTime = formatTimeAgo(offer.created_at); + const expiresIn = formatTimeLeft(offer.expire_at); + + const currentTime = Math.floor(Date.now() / 1000); + const isActuallyExpired = currentTime > offer.expire_at; + + const isOwnOffer = offer.is_own_offer; + const isRevoked = Boolean(offer.is_revoked); + + const { buttonClass, buttonText } = getButtonProperties(isActuallyExpired, isSentOffers, isOwnOffer, isRevoked); + + const fromAmount = parseFloat(offer.amount_from) || 0; + const toAmount = parseFloat(offer.amount_to) || 0; + + row.innerHTML = ` + ${createTimeColumn(offer, postedTime, expiresIn)} + ${createDetailsColumn(offer)} + ${createTakerAmountColumn(offer, coinTo, coinFrom)} + ${createSwapColumn(offer, coinFromDisplay, coinToDisplay, coinFromSymbol, coinToSymbol)} + ${createOrderbookColumn(offer, coinFrom, coinTo)} + ${createRateColumn(offer, coinFrom, coinTo)} + ${createPercentageColumn(offer)} + ${createActionColumn(offer, buttonClass, buttonText)} + ${createTooltips(offer, isOwnOffer, coinFrom, coinTo, fromAmount, toAmount, postedTime, expiresIn, isActuallyExpired, isRevoked)} + `; + + updateTooltipTargets(row, uniqueId); + updateProfitLoss(row, coinFrom, coinTo, fromAmount, toAmount, isOwnOffer); + + return row; +} + +async function updateOffersTable(isPaginationChange = false) { + console.log('[Debug] Starting updateOffersTable function'); + + try { + if (!isPaginationChange) { + const priceData = await fetchLatestPrices(); + if (!priceData) { + console.error('Failed to fetch latest prices'); + } else { + console.log('Latest prices fetched successfully'); + latestPrices = priceData; + } + } + + let validOffers = getValidOffers(); + console.log('[Debug] Valid offers:', validOffers.length); + + if (validOffers.length === 0) { + handleNoOffersScenario(); + return; + } + + const totalPages = Math.max(1, Math.ceil(validOffers.length / itemsPerPage)); + currentPage = Math.min(currentPage, totalPages); + + const startIndex = (currentPage - 1) * itemsPerPage; + const endIndex = Math.min(startIndex + itemsPerPage, validOffers.length); + const itemsToDisplay = validOffers.slice(startIndex, endIndex); + + offersBody.innerHTML = ''; + + for (const offer of itemsToDisplay) { + const row = createTableRow(offer, isSentOffers); + if (row) { + offersBody.appendChild(row); + } + } + + updateRowTimes(); + initializeFlowbiteTooltips(); + updatePaginationControls(totalPages); + + if (tableRateModule?.initializeTable) { + tableRateModule.initializeTable(); + } + + if (!isPaginationChange) { + lastRefreshTime = Date.now(); + if (newEntriesCountSpan) { + const displayCount = isSentOffers ? jsonData.length : validOffers.length; + newEntriesCountSpan.textContent = displayCount; + } + if (!isSentOffers) { + nextRefreshCountdown = getTimeUntilNextExpiration(); + updateLastRefreshTime(); + updateNextRefreshTime(); + } + } + + } catch (error) { + console.error('[Debug] Error in updateOffersTable:', error); + offersBody.innerHTML = `An error occurred while updating the offers table. Please try again later.`; + } finally { + setRefreshButtonLoading(false); + } +} + function startRefreshCountdown() { if (isSentOffers) return; - console.log('Starting refresh countdown'); - let refreshTimeout; let countdownInterval; function refreshCycle() { - console.log(`Refresh cycle started. Current countdown: ${nextRefreshCountdown}`); - checkExpiredAndFetchNew().then(() => { - console.log(`Refresh cycle completed. Next refresh in ${nextRefreshCountdown} seconds`); - startCountdown(); - }); + checkExpiredAndFetchNew() + .then(() => { + startCountdown(); + }) + .catch(error => { + console.error('Error in refresh cycle:', error); + nextRefreshCountdown = 60; + startCountdown(); + }); } function startCountdown() { - if (countdownInterval) { - clearInterval(countdownInterval); - } - if (refreshTimeout) { - clearTimeout(refreshTimeout); - } + if (countdownInterval) clearInterval(countdownInterval); + if (refreshTimeout) clearTimeout(refreshTimeout); - nextRefreshCountdown = getTimeUntilNextExpiration(); updateNextRefreshTime(); countdownInterval = setInterval(() => { @@ -1739,118 +1587,6 @@ function startRefreshCountdown() { }); } -function checkExpiredAndFetchNew() { - if (isSentOffers) return Promise.resolve(); - console.log('Starting checkExpiredAndFetchNew'); - const currentTime = Math.floor(Date.now() / 1000); - const expiredOffers = jsonData.filter(offer => offer.expire_at <= currentTime); - - console.log(`Checking for expired offers. Current time: ${currentTime}`); - console.log(`Found ${expiredOffers.length} expired offers.`); - - jsonData = jsonData.filter(offer => offer.expire_at > currentTime); - - console.log('Fetching new offers...'); - - return fetch('/json/offers') - .then(response => response.json()) - .then(data => { - let newListings = Array.isArray(data) ? data : Object.values(data); - newListings = newListings.filter(offer => !isOfferExpired(offer)); - - const brandNewListings = newListings.filter(newOffer => - !jsonData.some(existingOffer => existingOffer.offer_id === newOffer.offer_id) - ); - - console.log(`Found ${brandNewListings.length} new listings to add.`); - jsonData = [...jsonData, ...brandNewListings]; - newEntriesCount += brandNewListings.length; - - updateOffersTable(); - updateJsonView(); - updatePaginationInfo(); - - if (jsonData.length === 0) { - console.log('No active offers. Displaying message to user.'); - handleNoOffersScenario(); - } - - const timeUntilNextExpiration = getTimeUntilNextExpiration(); - nextRefreshCountdown = Math.max(MIN_REFRESH_INTERVAL, Math.min(timeUntilNextExpiration, 300)); // Between 30 seconds and 5 minutes - console.log(`Next check scheduled in ${nextRefreshCountdown} seconds`); - }) - .catch(error => { - console.error('Error fetching new listings:', error); - nextRefreshCountdown = 60; - console.log(`Error occurred. Next check scheduled in ${nextRefreshCountdown} seconds`); - }); -} - -// Event -toggleButton.addEventListener('click', () => { - tableView.classList.toggle('hidden'); - jsonView.classList.toggle('hidden'); - toggleButton.textContent = tableView.classList.contains('hidden') ? 'Show Table View' : 'Show JSON View'; -}); - -filterForm.addEventListener('submit', (e) => { - e.preventDefault(); - applyFilters(); -}); - -filterForm.addEventListener('change', applyFilters); - -document.getElementById('coin_to').addEventListener('change', (event) => { - console.log('Coin To filter changed:', event.target.value); - applyFilters(); -}); - -document.getElementById('coin_from').addEventListener('change', (event) => { - console.log('Coin From filter changed:', event.target.value); - applyFilters(); -}); - -prevPageButton.addEventListener('click', () => { - if (currentPage > 1) { - currentPage--; - updateOffersTable(); - updatePaginationInfo(); - } -}); - -nextPageButton.addEventListener('click', () => { - const validOffers = getValidOffers(); - const totalPages = Math.ceil(validOffers.length / itemsPerPage); - if (currentPage < totalPages && validOffers.length > 0) { - currentPage++; - updateOffersTable(); - updatePaginationInfo(); - console.log(`Moved to page ${currentPage} of ${totalPages}`); - } else { - console.log('No more pages or all offers have expired'); - nextPageButton.classList.add('invisible'); - nextPageButton.style.display = 'none'; - if (validOffers.length === 0) { - handleNoOffersScenario(); - } - } -}); - -document.getElementById('clearFilters').addEventListener('click', () => { - filterForm.reset(); - jsonData = [...originalJsonData]; - currentPage = 1; - updateOffersTable(); - updateJsonView(); - updateCoinFilterImages(); -}); - -document.getElementById('refreshOffers').addEventListener('click', () => { - console.log('Refresh button clicked'); - fetchOffers(true); -}); - -// Init function initializeTableRateModule() { if (typeof window.tableRateModule !== 'undefined') { tableRateModule = window.tableRateModule; @@ -1865,15 +1601,20 @@ function initializeTableRateModule() { function continueInitialization() { if (typeof volumeToggle !== 'undefined' && volumeToggle.init) { volumeToggle.init(); - } else { - console.warn('volumeToggle is not defined or does not have an init method'); } updateCoinFilterImages(); fetchOffers().then(() => { applyFilters(); - startRefreshCountdown(); + if (!isSentOffers) { + startRefreshCountdown(); + } }); + + const listingLabel = document.querySelector('span[data-listing-label]'); + if (listingLabel) { + listingLabel.textContent = isSentOffers ? 'Total Listings: ' : 'Network Listings: '; + } function updateTimesLoop() { updateRowTimes(); @@ -1882,13 +1623,15 @@ function continueInitialization() { requestAnimationFrame(updateTimesLoop); setInterval(updateRowTimes, 900000); - console.log('Initialization completed'); } document.addEventListener('DOMContentLoaded', () => { console.log('DOM content loaded, initializing...'); + initializeFooter(); + updateClearFiltersButton(); + if (initializeTableRateModule()) { continueInitialization(); } else { @@ -1900,7 +1643,7 @@ document.addEventListener('DOMContentLoaded', () => { clearInterval(retryInterval); continueInitialization(); } else if (retryCount >= maxRetries) { - console.error('Failed to load tableRateModule after multiple attempts. Some functionality may be limited.'); + console.error('Failed to load tableRateModule after multiple attempts'); clearInterval(retryInterval); continueInitialization(); } @@ -1914,16 +1657,54 @@ document.addEventListener('DOMContentLoaded', () => { filterForm.addEventListener('change', applyFilters); - document.getElementById('coin_to').addEventListener('change', applyFilters); - document.getElementById('coin_from').addEventListener('change', applyFilters); + const coinToSelect = document.getElementById('coin_to'); + const coinFromSelect = document.getElementById('coin_from'); + + coinToSelect.addEventListener('change', applyFilters); + coinFromSelect.addEventListener('change', applyFilters); document.getElementById('clearFilters').addEventListener('click', () => { filterForm.reset(); + const statusSelect = document.getElementById('status'); + if (statusSelect) { + statusSelect.value = 'any'; + } jsonData = [...originalJsonData]; currentPage = 1; applyFilters(); updateCoinFilterImages(); }); + + document.getElementById('refreshOffers').addEventListener('click', () => { + console.log('Refresh button clicked'); + fetchOffers(true); + }); + + toggleButton.addEventListener('click', () => { + tableView.classList.toggle('hidden'); + jsonView.classList.toggle('hidden'); + toggleButton.textContent = tableView.classList.contains('hidden') ? 'Show Table View' : 'Show JSON View'; + }); + + prevPageButton.addEventListener('click', () => { + if (currentPage > 1) { + currentPage--; + const validOffers = getValidOffers(); + const totalPages = Math.ceil(validOffers.length / itemsPerPage); + updateOffersTable(true); + updatePaginationControls(totalPages); + } + }); + + nextPageButton.addEventListener('click', () => { + const validOffers = getValidOffers(); + const totalPages = Math.ceil(validOffers.length / itemsPerPage); + if (currentPage < totalPages) { + currentPage++; + updateOffersTable(true); + updatePaginationControls(totalPages); + } + }); }); console.log('Offers Table Module fully initialized'); diff --git a/basicswap/templates/offers.html b/basicswap/templates/offers.html index f78ca78..d8d957d 100644 --- a/basicswap/templates/offers.html +++ b/basicswap/templates/offers.html @@ -266,11 +266,26 @@ function getAPIKeys() { + {% if sent_offers %} +
+
+
+ {{ input_arrow_down_svg | safe }} + +
+
+
+ {% endif %}
-
@@ -384,17 +399,12 @@ function getAPIKeys() {
-
-

Last refreshed: - Never -

-

Network Listings: - -

-

Next refresh: - -

-
+
+

Last refreshed: Never

+

Network Listings:

+

Next refresh: +

+