mirror of
https://github.com/basicswap/basicswap.git
synced 2026-02-28 16:45:11 +01:00
2803 lines
105 KiB
JavaScript
2803 lines
105 KiB
JavaScript
let latestPrices = null;
|
||
let lastRefreshTime = null;
|
||
let currentPage = 1;
|
||
let jsonData = [];
|
||
let originalJsonData = [];
|
||
let currentSortColumn = 0;
|
||
let currentSortDirection = 'desc';
|
||
let isPaginationInProgress = false;
|
||
let autoRefreshInterval = null;
|
||
|
||
const isSentOffers = window.offersTableConfig.isSentOffers;
|
||
const CACHE_DURATION = window.config.cacheConfig.defaultTTL;
|
||
const wsPort = window.config.wsPort;
|
||
const itemsPerPage = window.config.itemsPerPage;
|
||
|
||
const offersBody = document.getElementById('offers-body');
|
||
const filterForm = document.getElementById('filterForm');
|
||
const prevPageButton = document.getElementById('prevPage');
|
||
const nextPageButton = document.getElementById('nextPage');
|
||
const currentPageSpan = document.getElementById('currentPage');
|
||
const totalPagesSpan = document.getElementById('totalPages');
|
||
const lastRefreshTimeSpan = document.getElementById('lastRefreshTime');
|
||
const newEntriesCountSpan = document.getElementById('newEntriesCount');
|
||
|
||
window.tableRateModule = {
|
||
|
||
cache: {},
|
||
processedOffers: new Set(),
|
||
|
||
getCachedValue(key) {
|
||
if (window.CacheManager) {
|
||
return window.CacheManager.get(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, resourceType = null) {
|
||
if (window.CacheManager) {
|
||
const ttl = resourceType ?
|
||
window.config.cacheConfig.ttlSettings[resourceType] ||
|
||
window.config.cacheConfig.defaultTTL :
|
||
900000;
|
||
window.CacheManager.set(key, value, ttl);
|
||
return;
|
||
}
|
||
const ttl = resourceType ?
|
||
window.config.cacheConfig.ttlSettings[resourceType] ||
|
||
window.config.cacheConfig.defaultTTL :
|
||
900000;
|
||
|
||
const item = {
|
||
value: value,
|
||
expiry: Date.now() + ttl,
|
||
};
|
||
localStorage.setItem(key, JSON.stringify(item));
|
||
},
|
||
|
||
setFallbackValue(coinSymbol, value) {
|
||
this.setCachedValue(`fallback_${coinSymbol}_usd`, value, 'fallback');
|
||
},
|
||
|
||
isNewOffer(offerId) {
|
||
if (this.processedOffers.has(offerId)) {
|
||
return false;
|
||
}
|
||
this.processedOffers.add(offerId);
|
||
return true;
|
||
},
|
||
|
||
getFallbackValue(coinSymbol) {
|
||
if (!coinSymbol) return null;
|
||
const normalizedSymbol = coinSymbol.toLowerCase() === 'part' ? 'particl' : coinSymbol.toLowerCase();
|
||
const cachedValue = this.getCachedValue(`fallback_${normalizedSymbol}_usd`);
|
||
return cachedValue;
|
||
},
|
||
|
||
initializeTable() {
|
||
document.querySelectorAll('.coinname-value').forEach(coinNameValue => {
|
||
const coinFullNameOrSymbol = coinNameValue.getAttribute('data-coinname');
|
||
if (!coinFullNameOrSymbol || coinFullNameOrSymbol === 'Unknown') {
|
||
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() {
|
||
this.initializeTable();
|
||
}
|
||
};
|
||
|
||
function initializeTableRateModule() {
|
||
if (typeof window.tableRateModule !== 'undefined') {
|
||
tableRateModule = window.tableRateModule;
|
||
return true;
|
||
} else {
|
||
return false;
|
||
}
|
||
}
|
||
|
||
function continueInitialization() {
|
||
fetchOffers().then(() => {
|
||
applyFilters();
|
||
if (!isSentOffers) {
|
||
return;
|
||
}
|
||
});
|
||
|
||
const listingLabel = document.querySelector('span[data-listing-label]');
|
||
if (listingLabel) {
|
||
listingLabel.textContent = isSentOffers ? 'Total Listings: ' : 'Network Listings: ';
|
||
}
|
||
}
|
||
|
||
function initializeTooltips() {
|
||
if (window.TooltipManager) {
|
||
window.TooltipManager.initializeTooltips();
|
||
}
|
||
}
|
||
|
||
function initializeTooltipsInBatches() {
|
||
if (!window.TooltipManager) return;
|
||
|
||
const tooltipElements = document.querySelectorAll('[data-tooltip-target]');
|
||
const BATCH_SIZE = 5;
|
||
let currentIndex = 0;
|
||
|
||
function processBatch() {
|
||
const endIndex = Math.min(currentIndex + BATCH_SIZE, tooltipElements.length);
|
||
|
||
for (let i = currentIndex; i < endIndex; i++) {
|
||
const element = tooltipElements[i];
|
||
const targetId = element.getAttribute('data-tooltip-target');
|
||
if (!targetId) continue;
|
||
|
||
const tooltipContent = document.getElementById(targetId);
|
||
if (tooltipContent) {
|
||
window.TooltipManager.create(element, tooltipContent.innerHTML, {
|
||
placement: element.getAttribute('data-tooltip-placement') || 'top'
|
||
});
|
||
}
|
||
}
|
||
|
||
currentIndex = endIndex;
|
||
|
||
if (currentIndex < tooltipElements.length) {
|
||
CleanupManager.setTimeout(processBatch, 0);
|
||
}
|
||
}
|
||
|
||
if (tooltipElements.length > 0) {
|
||
CleanupManager.setTimeout(processBatch, 0);
|
||
}
|
||
}
|
||
|
||
function getValidOffers() {
|
||
if (!jsonData) {
|
||
return [];
|
||
}
|
||
|
||
return jsonData;
|
||
}
|
||
|
||
function saveFilterSettings() {
|
||
const storageKey = isSentOffers ? 'sentOffersTableSettings' : 'networkOffersTableSettings';
|
||
|
||
const selectedCoinTo = getSelectedCoins('coin_to');
|
||
const selectedCoinFrom = getSelectedCoins('coin_from');
|
||
|
||
const statusSelect = document.getElementById('status');
|
||
const sentFromSelect = document.getElementById('sent_from');
|
||
const autoAcceptTypeSelect = document.getElementById('auto_accept_type');
|
||
|
||
localStorage.setItem(storageKey, JSON.stringify({
|
||
coin_to: selectedCoinTo,
|
||
coin_from: selectedCoinFrom,
|
||
status: statusSelect ? statusSelect.value : 'any',
|
||
sent_from: sentFromSelect ? sentFromSelect.value : 'any',
|
||
auto_accept_type: autoAcceptTypeSelect ? autoAcceptTypeSelect.value : 'any',
|
||
sortColumn: currentSortColumn,
|
||
sortDirection: currentSortDirection
|
||
}));
|
||
}
|
||
|
||
function getSelectedCoins(filterType) {
|
||
|
||
const dropdown = document.getElementById(`${filterType}_dropdown`);
|
||
if (!dropdown) {
|
||
return ['any'];
|
||
}
|
||
|
||
const allCheckboxes = dropdown.querySelectorAll('input[type="checkbox"]');
|
||
|
||
const selected = [];
|
||
allCheckboxes.forEach((cb) => {
|
||
if (cb.checked && cb.value !== 'any') {
|
||
selected.push(cb.value);
|
||
}
|
||
});
|
||
|
||
return selected.length > 0 ? selected : ['any'];
|
||
}
|
||
|
||
function getCoinNameFromValue(value, filterType) {
|
||
if (value === 'any') {
|
||
return 'any';
|
||
}
|
||
|
||
const dropdown = document.getElementById(`${filterType}_dropdown`);
|
||
if (!dropdown) {
|
||
return value;
|
||
}
|
||
|
||
const checkbox = dropdown.querySelector(`input[value="${value}"]`);
|
||
if (checkbox) {
|
||
const label = checkbox.closest('label');
|
||
const spans = label.querySelectorAll('span');
|
||
|
||
const coinSpan = spans[spans.length - 1];
|
||
if (coinSpan) {
|
||
const text = coinSpan.textContent.trim();
|
||
|
||
const cleanText = text.replace(/\s*\(clear all\)\s*/, '');
|
||
return cleanText;
|
||
}
|
||
}
|
||
|
||
return value;
|
||
}
|
||
|
||
function getCoinImage(coinName) {
|
||
return window.CoinManager.getCoinIcon(coinName);
|
||
}
|
||
|
||
function updateFilterButtonText(filterType) {
|
||
const selected = getSelectedCoins(filterType);
|
||
const button = document.getElementById(`${filterType}_button`);
|
||
const textSpan = document.getElementById(`${filterType}_text`);
|
||
|
||
if (!button || !textSpan) return;
|
||
|
||
if (selected.length === 0 || (selected.length === 1 && selected[0] === 'any')) {
|
||
|
||
const defaultText = filterType === 'coin_to' ?
|
||
(isSentOffers ? 'Filter Receiving' : 'Filter Bids') :
|
||
(isSentOffers ? 'Filter Sending' : 'Filter Offers');
|
||
textSpan.textContent = defaultText;
|
||
} else {
|
||
const filterLabel = filterType === 'coin_to' ?
|
||
(isSentOffers ? 'Receiving' : 'Bids') :
|
||
(isSentOffers ? 'Sending' : 'Offers');
|
||
textSpan.textContent = `Filter ${filterLabel} (${selected.length} selected)`;
|
||
}
|
||
|
||
button.style.width = '210px';
|
||
}
|
||
|
||
function updateCoinBadges(filterType) {
|
||
const selected = getSelectedCoins(filterType);
|
||
const badgesContainer = document.getElementById(`${filterType}_badges`);
|
||
const mainContainer = document.getElementById('selected_coins_container');
|
||
|
||
if (!badgesContainer) return;
|
||
|
||
badgesContainer.innerHTML = '';
|
||
|
||
if (selected.length > 0 && !(selected.length === 1 && selected[0] === 'any')) {
|
||
selected.forEach(coinValue => {
|
||
const coinName = getCoinNameFromValue(coinValue, filterType);
|
||
const badge = document.createElement('span');
|
||
|
||
const isBidsFilter = filterType === 'coin_to' && !isSentOffers;
|
||
const isOffersFilter = filterType === 'coin_from' && !isSentOffers;
|
||
const isReceivingFilter = filterType === 'coin_to' && isSentOffers;
|
||
const isSendingFilter = filterType === 'coin_from' && isSentOffers;
|
||
|
||
let badgeClass = 'inline-flex items-center px-3 py-2 rounded-full text-sm font-medium mr-2 mb-1';
|
||
if (isBidsFilter || isReceivingFilter) {
|
||
badgeClass += ' bg-blue-500 text-white dark:bg-blue-600 dark:text-white';
|
||
} else {
|
||
badgeClass += ' bg-green-400 text-white dark:bg-green-500 dark:text-white';
|
||
}
|
||
|
||
badge.className = badgeClass + ' cursor-pointer hover:opacity-80';
|
||
|
||
const coinImage = getCoinImage(coinName);
|
||
|
||
badge.innerHTML = `
|
||
<img src="/static/images/coins/${coinImage}" class="w-4 h-4 mr-1" alt="${coinName}" onerror="this.style.display='none'">
|
||
<span>${coinName}</span>
|
||
<button type="button" class="ml-1 text-current hover:text-red-500 focus:outline-none remove-coin-btn">
|
||
<span class="sr-only">Remove ${coinName}</span>
|
||
×
|
||
</button>
|
||
`;
|
||
|
||
CleanupManager.addListener(badge, 'click', (e) => {
|
||
e.preventDefault();
|
||
e.stopPropagation();
|
||
removeCoinFilter(filterType, coinValue);
|
||
});
|
||
|
||
const closeBtn = badge.querySelector('.remove-coin-btn');
|
||
if (closeBtn) {
|
||
CleanupManager.addListener(closeBtn, 'click', (e) => {
|
||
e.preventDefault();
|
||
e.stopPropagation();
|
||
removeCoinFilter(filterType, coinValue);
|
||
});
|
||
}
|
||
|
||
badgesContainer.appendChild(badge);
|
||
});
|
||
|
||
if (mainContainer) {
|
||
mainContainer.style.display = 'block';
|
||
}
|
||
} else {
|
||
|
||
const otherType = filterType === 'coin_to' ? 'coin_from' : 'coin_to';
|
||
const otherSelected = getSelectedCoins(otherType);
|
||
if (otherSelected.length === 0 || (otherSelected.length === 1 && otherSelected[0] === 'any')) {
|
||
if (mainContainer) {
|
||
mainContainer.style.display = 'none';
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
function coinMatches(offerCoin, filterCoins) {
|
||
if (!offerCoin || !filterCoins) return true;
|
||
|
||
if (typeof filterCoins === 'string') {
|
||
filterCoins = [filterCoins];
|
||
}
|
||
|
||
if (filterCoins.includes('any') || filterCoins.length === 0) return true;
|
||
|
||
const normalizedOfferCoin = offerCoin.toLowerCase().trim();
|
||
|
||
return filterCoins.some(filterCoin => {
|
||
if (!filterCoin) return false;
|
||
|
||
const normalizedFilterCoin = filterCoin.toLowerCase().trim();
|
||
|
||
if (normalizedOfferCoin === normalizedFilterCoin) {
|
||
return true;
|
||
}
|
||
|
||
if (window.CoinUtils && window.CoinUtils.isSameCoin(normalizedOfferCoin, normalizedFilterCoin)) {
|
||
return true;
|
||
}
|
||
|
||
const particlVariants = ['particl', 'particl anon', 'particl blind'];
|
||
if (normalizedFilterCoin === 'particl' && particlVariants.includes(normalizedOfferCoin)) {
|
||
return true;
|
||
}
|
||
|
||
if (particlVariants.includes(normalizedFilterCoin)) {
|
||
return normalizedOfferCoin === normalizedFilterCoin;
|
||
}
|
||
|
||
if (normalizedFilterCoin.includes(' ') || normalizedOfferCoin.includes(' ')) {
|
||
const filterFirstWord = normalizedFilterCoin.split(' ')[0];
|
||
const offerFirstWord = normalizedOfferCoin.split(' ')[0];
|
||
|
||
if (filterFirstWord === 'bitcoin' && offerFirstWord === 'bitcoin') {
|
||
const filterHasCash = normalizedFilterCoin.includes('cash');
|
||
const offerHasCash = normalizedOfferCoin.includes('cash');
|
||
return filterHasCash === offerHasCash;
|
||
}
|
||
|
||
if (filterFirstWord === offerFirstWord && filterFirstWord.length > 4) {
|
||
return true;
|
||
}
|
||
}
|
||
|
||
return false;
|
||
});
|
||
}
|
||
|
||
function toggleDropdown(filterType) {
|
||
const dropdown = document.getElementById(`${filterType}_dropdown`);
|
||
const button = document.getElementById(`${filterType}_button`);
|
||
const container = dropdown?.closest('.dropdown-container');
|
||
|
||
if (dropdown && button) {
|
||
const isVisible = dropdown.style.display !== 'none';
|
||
hideAllDropdowns();
|
||
|
||
if (!isVisible) {
|
||
const buttonRect = button.getBoundingClientRect();
|
||
dropdown.style.position = 'fixed';
|
||
dropdown.style.top = (buttonRect.bottom + 4) + 'px';
|
||
dropdown.style.left = buttonRect.left + 'px';
|
||
dropdown.style.width = buttonRect.width + 'px';
|
||
dropdown.style.display = 'block';
|
||
|
||
if (container) {
|
||
container.classList.add('open');
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
function hideDropdown(filterType) {
|
||
const dropdown = document.getElementById(`${filterType}_dropdown`);
|
||
const container = dropdown?.closest('.dropdown-container');
|
||
|
||
if (dropdown) {
|
||
dropdown.style.display = 'none';
|
||
if (container) {
|
||
container.classList.remove('open');
|
||
}
|
||
}
|
||
}
|
||
|
||
function hideAllDropdowns() {
|
||
hideDropdown('coin_to');
|
||
hideDropdown('coin_from');
|
||
}
|
||
|
||
function handleCoinCheckboxChange(filterType, checkbox) {
|
||
if (checkbox.value === 'any') {
|
||
|
||
if (checkbox.checked) {
|
||
const dropdown = document.getElementById(`${filterType}_dropdown`);
|
||
if (dropdown) {
|
||
const otherCheckboxes = dropdown.querySelectorAll('input[type="checkbox"]:not([value="any"])');
|
||
otherCheckboxes.forEach(cb => cb.checked = false);
|
||
}
|
||
}
|
||
} else {
|
||
|
||
if (checkbox.checked) {
|
||
const dropdown = document.getElementById(`${filterType}_dropdown`);
|
||
if (dropdown) {
|
||
const anyCheckbox = dropdown.querySelector('input[value="any"]');
|
||
if (anyCheckbox) anyCheckbox.checked = false;
|
||
}
|
||
}
|
||
}
|
||
|
||
updateFilterButtonText(filterType);
|
||
updateCoinBadges(filterType);
|
||
applyFilters();
|
||
updateClearFiltersButton();
|
||
}
|
||
|
||
function removeCoinFilter(filterType, coinValue) {
|
||
|
||
const dropdown = document.getElementById(`${filterType}_dropdown`);
|
||
if (!dropdown) {
|
||
return;
|
||
}
|
||
|
||
const checkbox = dropdown.querySelector(`input[value="${coinValue}"]`);
|
||
if (checkbox) {
|
||
checkbox.checked = false;
|
||
|
||
updateFilterButtonText(filterType);
|
||
updateCoinBadges(filterType);
|
||
applyFilters();
|
||
updateClearFiltersButton();
|
||
}
|
||
}
|
||
|
||
window.removeCoinFilter = removeCoinFilter;
|
||
|
||
function filterAndSortData() {
|
||
saveFilterSettings();
|
||
|
||
let filteredData = [...originalJsonData];
|
||
|
||
const statusSelect = document.getElementById('status');
|
||
const sentFromSelect = document.getElementById('sent_from');
|
||
const autoAcceptTypeSelect = document.getElementById('auto_accept_type');
|
||
const selectedCoinTo = getSelectedCoins('coin_to');
|
||
const selectedCoinFrom = getSelectedCoins('coin_from');
|
||
|
||
const sentFromFilter = sentFromSelect ? sentFromSelect.value : 'any';
|
||
filteredData = filteredData.filter(offer => {
|
||
const isMatch = sentFromFilter === 'public' ? offer.is_public :
|
||
sentFromFilter === 'private' ? !offer.is_public :
|
||
true;
|
||
return isMatch;
|
||
});
|
||
|
||
const autoAcceptTypeFilter = autoAcceptTypeSelect ? autoAcceptTypeSelect.value : 'any';
|
||
if (autoAcceptTypeFilter !== 'any') {
|
||
filteredData = filteredData.filter(offer => {
|
||
const offerAutoAcceptType = offer.auto_accept_type !== undefined ? offer.auto_accept_type : 0;
|
||
return offerAutoAcceptType === parseInt(autoAcceptTypeFilter);
|
||
});
|
||
}
|
||
|
||
filteredData = filteredData.filter(offer => {
|
||
if (!isSentOffers && isOfferExpired(offer)) {
|
||
return false;
|
||
}
|
||
|
||
if (selectedCoinTo.length > 0 && !(selectedCoinTo.length === 1 && selectedCoinTo[0] === 'any')) {
|
||
const coinNames = selectedCoinTo.map(value => getCoinNameFromValue(value, 'coin_to'));
|
||
const matches = coinMatches(offer.coin_to, coinNames);
|
||
if (!matches) {
|
||
return false;
|
||
}
|
||
}
|
||
|
||
if (selectedCoinFrom.length > 0 && !(selectedCoinFrom.length === 1 && selectedCoinFrom[0] === 'any')) {
|
||
const coinNames = selectedCoinFrom.map(value => getCoinNameFromValue(value, 'coin_from'));
|
||
const matches = coinMatches(offer.coin_from, coinNames);
|
||
if (!matches) {
|
||
return false;
|
||
}
|
||
}
|
||
|
||
if (isSentOffers && statusSelect && statusSelect.value !== 'any') {
|
||
const isExpired = offer.expire_at <= Math.floor(Date.now() / 1000);
|
||
const isRevoked = Boolean(offer.is_revoked);
|
||
|
||
let statusMatch = false;
|
||
switch (statusSelect.value) {
|
||
case 'active':
|
||
statusMatch = !isExpired && !isRevoked;
|
||
break;
|
||
case 'expired':
|
||
statusMatch = isExpired && !isRevoked;
|
||
break;
|
||
case 'revoked':
|
||
statusMatch = isRevoked;
|
||
break;
|
||
}
|
||
|
||
if (!statusMatch) {
|
||
return false;
|
||
}
|
||
}
|
||
|
||
return true;
|
||
});
|
||
|
||
if (currentSortColumn === 7) {
|
||
const offersWithPercentages = [];
|
||
|
||
for (const offer of filteredData) {
|
||
const fromAmount = parseFloat(offer.amount_from) || 0;
|
||
const toAmount = parseFloat(offer.amount_to) || 0;
|
||
const coinFrom = offer.coin_from;
|
||
const coinTo = offer.coin_to;
|
||
|
||
const fromSymbol = getPriceKey(coinFrom);
|
||
const toSymbol = getPriceKey(coinTo);
|
||
|
||
const fromPriceUSD = latestPrices && fromSymbol ? latestPrices[fromSymbol]?.usd : null;
|
||
const toPriceUSD = latestPrices && toSymbol ? latestPrices[toSymbol]?.usd : null;
|
||
|
||
let percentDiff = null;
|
||
|
||
if (fromPriceUSD && toPriceUSD && !isNaN(fromPriceUSD) && !isNaN(toPriceUSD)) {
|
||
const fromValueUSD = fromAmount * fromPriceUSD;
|
||
const toValueUSD = toAmount * toPriceUSD;
|
||
|
||
if (fromValueUSD && toValueUSD) {
|
||
if (offer.is_own_offer || isSentOffers) {
|
||
percentDiff = ((toValueUSD / fromValueUSD) - 1) * 100;
|
||
} else {
|
||
percentDiff = (-((toValueUSD / fromValueUSD) - 1)) * 100;
|
||
}
|
||
}
|
||
}
|
||
|
||
offersWithPercentages.push({
|
||
offer: offer,
|
||
percentDiff: percentDiff
|
||
});
|
||
}
|
||
|
||
const validOffers = offersWithPercentages.filter(item => item.percentDiff !== null);
|
||
const nullOffers = offersWithPercentages.filter(item => item.percentDiff === null);
|
||
|
||
validOffers.sort((a, b) => {
|
||
return currentSortDirection === 'asc' ? a.percentDiff - b.percentDiff : b.percentDiff - a.percentDiff;
|
||
});
|
||
|
||
filteredData = [...validOffers.map(item => item.offer), ...nullOffers.map(item => item.offer)];
|
||
} else if (currentSortColumn !== null && currentSortDirection !== null) {
|
||
filteredData.sort((a, b) => {
|
||
let aValue, bValue;
|
||
|
||
switch(currentSortColumn) {
|
||
case 0:
|
||
aValue = a.created_at;
|
||
bValue = b.created_at;
|
||
break;
|
||
case 1:
|
||
aValue = a.addr_from || '';
|
||
bValue = b.addr_from || '';
|
||
break;
|
||
case 2:
|
||
aValue = parseFloat(a.amount_to) || 0;
|
||
bValue = parseFloat(b.amount_to) || 0;
|
||
break;
|
||
case 3:
|
||
aValue = a.coin_from + a.coin_to;
|
||
bValue = b.coin_from + b.coin_to;
|
||
break;
|
||
case 4:
|
||
aValue = parseFloat(a.amount_from) || 0;
|
||
bValue = parseFloat(b.amount_from) || 0;
|
||
break;
|
||
case 5:
|
||
aValue = parseFloat(a.rate) || 0;
|
||
bValue = parseFloat(b.rate) || 0;
|
||
break;
|
||
case 6:
|
||
const aSymbol = getPriceKey(a.coin_to);
|
||
const bSymbol = getPriceKey(b.coin_to);
|
||
const aRate = parseFloat(a.rate) || 0;
|
||
const bRate = parseFloat(b.rate) || 0;
|
||
const aPriceUSD = latestPrices && aSymbol ? latestPrices[aSymbol]?.usd : null;
|
||
const bPriceUSD = latestPrices && bSymbol ? latestPrices[bSymbol]?.usd : null;
|
||
|
||
aValue = aPriceUSD && !isNaN(aPriceUSD) ? aRate * aPriceUSD : 0;
|
||
bValue = bPriceUSD && !isNaN(bPriceUSD) ? bRate * bPriceUSD : 0;
|
||
break;
|
||
default:
|
||
aValue = a.created_at;
|
||
bValue = b.created_at;
|
||
}
|
||
|
||
if (typeof aValue === 'number' && typeof bValue === 'number') {
|
||
return currentSortDirection === 'asc' ? aValue - bValue : bValue - aValue;
|
||
}
|
||
|
||
if (typeof aValue === 'string' && typeof bValue === 'string') {
|
||
return currentSortDirection === 'asc'
|
||
? aValue.localeCompare(bValue)
|
||
: bValue.localeCompare(aValue);
|
||
}
|
||
|
||
return currentSortDirection === 'asc' ? (aValue > bValue ? 1 : -1) : (bValue > aValue ? 1 : -1);
|
||
});
|
||
}
|
||
|
||
return filteredData;
|
||
}
|
||
|
||
async function calculateProfitLoss(fromCoin, toCoin, fromAmount, toAmount, isOwnOffer) {
|
||
return new Promise((resolve) => {
|
||
if (!fromCoin || !toCoin) {
|
||
resolve(null);
|
||
return;
|
||
}
|
||
|
||
const getPriceForCoin = (coin) => {
|
||
if (!coin) return null;
|
||
|
||
let normalizedCoin = coin.toLowerCase();
|
||
|
||
if (window.CoinManager) {
|
||
normalizedCoin = window.CoinManager.getPriceKey(coin) || normalizedCoin;
|
||
} else {
|
||
if (window.CoinUtils) {
|
||
normalizedCoin = window.CoinUtils.normalizeCoinName(normalizedCoin, latestPrices);
|
||
}
|
||
}
|
||
let price = null;
|
||
if (latestPrices && latestPrices[normalizedCoin]) {
|
||
price = latestPrices[normalizedCoin].usd;
|
||
}
|
||
return price;
|
||
};
|
||
|
||
const fromPriceUSD = getPriceForCoin(fromCoin);
|
||
const toPriceUSD = getPriceForCoin(toCoin);
|
||
|
||
if (!fromPriceUSD || !toPriceUSD || isNaN(fromPriceUSD) || isNaN(toPriceUSD)) {
|
||
resolve(null);
|
||
return;
|
||
}
|
||
|
||
const fromValueUSD = fromAmount * fromPriceUSD;
|
||
const toValueUSD = toAmount * toPriceUSD;
|
||
|
||
if (isNaN(fromValueUSD) || isNaN(toValueUSD) || fromValueUSD === 0 || toValueUSD === 0) {
|
||
resolve(null);
|
||
return;
|
||
}
|
||
|
||
let percentDiff;
|
||
if (isOwnOffer) {
|
||
percentDiff = ((toValueUSD / fromValueUSD) - 1) * 100;
|
||
} else {
|
||
percentDiff = (-((toValueUSD / fromValueUSD) - 1)) * 100;
|
||
}
|
||
|
||
if (isNaN(percentDiff)) {
|
||
resolve(null);
|
||
return;
|
||
}
|
||
|
||
resolve(percentDiff);
|
||
});
|
||
}
|
||
|
||
function getEmptyPriceData() {
|
||
return window.config.utils.getEmptyPriceData();
|
||
}
|
||
|
||
async function fetchLatestPrices() {
|
||
try {
|
||
|
||
const prices = await window.PriceManager.getPrices(window.isManualRefresh);
|
||
window.isManualRefresh = false;
|
||
return prices;
|
||
} catch (error) {
|
||
console.error('Error fetching prices:', error);
|
||
return getEmptyPriceData();
|
||
}
|
||
}
|
||
|
||
async function fetchPricesAsync() {
|
||
try {
|
||
const prices = await window.PriceManager.getPrices(false);
|
||
return prices;
|
||
} catch (error) {
|
||
console.error('Error fetching prices asynchronously:', error);
|
||
return null;
|
||
}
|
||
}
|
||
|
||
async function fetchOffers() {
|
||
const refreshButton = document.getElementById('refreshOffers');
|
||
const refreshIcon = document.getElementById('refreshIcon');
|
||
const refreshText = document.getElementById('refreshText');
|
||
|
||
const fetchWithRetry = async (url, maxRetries = 3) => {
|
||
for (let i = 0; i < maxRetries; i++) {
|
||
try {
|
||
const response = await fetch(url);
|
||
if (!response.ok) {
|
||
throw new Error(`HTTP error! status: ${response.status}`);
|
||
}
|
||
return response;
|
||
} catch (error) {
|
||
if (i === maxRetries - 1) throw error;
|
||
console.log(`Fetch retry ${i + 1}/${maxRetries} for ${url}`);
|
||
|
||
await new Promise(resolve => CleanupManager.setTimeout(resolve, 100 * Math.pow(2, i)));
|
||
}
|
||
}
|
||
};
|
||
|
||
try {
|
||
if (!NetworkManager.isOnline()) {
|
||
throw new Error('Network is offline');
|
||
}
|
||
|
||
if (refreshButton) {
|
||
refreshButton.disabled = true;
|
||
refreshIcon.classList.add('animate-spin');
|
||
refreshText.textContent = 'Refreshing...';
|
||
refreshButton.classList.add('opacity-75', 'cursor-wait');
|
||
}
|
||
|
||
const offersResponse = await fetchWithRetry(isSentOffers ? '/json/sentoffers' : '/json/offers');
|
||
const data = await offersResponse.json();
|
||
|
||
if (data.error) {
|
||
if (data.locked) {
|
||
if (typeof ui !== 'undefined' && ui.displayErrorMessage) {
|
||
ui.displayErrorMessage(data.error);
|
||
} else {
|
||
offersBody.innerHTML = `
|
||
<tr>
|
||
<td colspan="9" class="text-center py-8">
|
||
<div class="flex flex-col items-center justify-center text-yellow-600 dark:text-yellow-400">
|
||
<svg class="w-8 h-8 mb-2" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L4.082 16.5c-.77.833.192 2.5 1.732 2.5z"></path>
|
||
</svg>
|
||
<span class="font-medium">${data.error}</span>
|
||
</div>
|
||
</td>
|
||
</tr>`;
|
||
}
|
||
return;
|
||
} else {
|
||
throw new Error(data.error);
|
||
}
|
||
}
|
||
const processedData = Array.isArray(data) ? data : Object.values(data);
|
||
|
||
jsonData = formatInitialData(processedData);
|
||
originalJsonData = [...jsonData];
|
||
|
||
CacheManager.set('offers_cached', jsonData, 'offers');
|
||
|
||
applyFilters();
|
||
updatePaginationInfo();
|
||
|
||
fetchPricesAsync().then(prices => {
|
||
if (prices) {
|
||
latestPrices = prices;
|
||
updateProfitLossDisplays();
|
||
}
|
||
}).catch(error => {
|
||
console.error('Error fetching prices after offers refresh:', error);
|
||
});
|
||
|
||
} catch (error) {
|
||
console.error('[Debug] Error fetching offers:', error);
|
||
NetworkManager.handleNetworkError(error);
|
||
|
||
const cachedOffers = CacheManager.get('offers_cached');
|
||
if (cachedOffers?.value) {
|
||
jsonData = cachedOffers.value;
|
||
originalJsonData = [...jsonData];
|
||
applyFilters();
|
||
}
|
||
ui.displayErrorMessage('Failed to fetch offers. Please try again later.');
|
||
} finally {
|
||
if (refreshButton) {
|
||
refreshButton.disabled = false;
|
||
refreshIcon.classList.remove('animate-spin');
|
||
refreshText.textContent = 'Refresh';
|
||
refreshButton.classList.remove('opacity-75', 'cursor-wait');
|
||
}
|
||
}
|
||
}
|
||
|
||
function formatInitialData(data) {
|
||
return data.map(offer => ({
|
||
offer_id: String(offer.offer_id || ''),
|
||
swap_type: String(offer.swap_type || 'N/A'),
|
||
addr_from: String(offer.addr_from || ''),
|
||
addr_to: String(offer.addr_to || ''),
|
||
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),
|
||
is_public: offer.is_public !== undefined ? Boolean(offer.is_public) : false,
|
||
auto_accept_type: offer.auto_accept_type !== undefined ? Number(offer.auto_accept_type) : 0,
|
||
unique_id: `${offer.offer_id}_${offer.created_at}_${offer.coin_from}_${offer.coin_to}`
|
||
}));
|
||
}
|
||
|
||
function updateConnectionStatus(status) {
|
||
const dot = document.getElementById('status-dot');
|
||
const text = document.getElementById('status-text');
|
||
|
||
if (!dot || !text) {
|
||
return;
|
||
}
|
||
|
||
switch(status) {
|
||
case 'connected':
|
||
dot.className = 'w-2.5 h-2.5 rounded-full bg-green-500 mr-2';
|
||
text.textContent = 'Connected';
|
||
text.className = 'text-sm text-green-500';
|
||
break;
|
||
case 'disconnected':
|
||
dot.className = 'w-2.5 h-2.5 rounded-full bg-red-500 mr-2';
|
||
text.textContent = 'Disconnected - Reconnecting...';
|
||
text.className = 'text-sm text-red-500';
|
||
break;
|
||
case 'error':
|
||
dot.className = 'w-2.5 h-2.5 rounded-full bg-yellow-500 mr-2';
|
||
text.textContent = 'Connection Error';
|
||
text.className = 'text-sm text-yellow-500';
|
||
break;
|
||
default:
|
||
dot.className = 'w-2.5 h-2.5 rounded-full bg-gray-500 mr-2';
|
||
text.textContent = 'Connecting...';
|
||
text.className = 'text-sm text-gray-500';
|
||
}
|
||
}
|
||
|
||
function updateRowTimes() {
|
||
|
||
const rows = document.querySelectorAll('[data-offer-id]');
|
||
const updates = [];
|
||
|
||
rows.forEach(row => {
|
||
const offerId = row.getAttribute('data-offer-id');
|
||
const offer = jsonData.find(o => o.offer_id === offerId);
|
||
if (!offer) return;
|
||
|
||
const newPostedTime = formatTime(offer.created_at, true);
|
||
const newExpiresIn = formatTimeLeft(offer.expire_at);
|
||
|
||
const postedElement = row.querySelector('.text-xs:first-child');
|
||
const expiresElement = row.querySelector('.text-xs:last-child');
|
||
|
||
updates.push({
|
||
postedElement,
|
||
expiresElement,
|
||
newPostedTime,
|
||
newExpiresIn
|
||
});
|
||
});
|
||
|
||
updates.forEach(({ postedElement, expiresElement, newPostedTime, newExpiresIn }) => {
|
||
if (postedElement && postedElement.textContent !== `Posted: ${newPostedTime}`) {
|
||
postedElement.textContent = `Posted: ${newPostedTime}`;
|
||
}
|
||
if (expiresElement && expiresElement.textContent !== `Expires in: ${newExpiresIn}`) {
|
||
expiresElement.textContent = `Expires in: ${newExpiresIn}`;
|
||
}
|
||
});
|
||
}
|
||
|
||
function updateLastRefreshTime() {
|
||
if (lastRefreshTimeSpan) {
|
||
lastRefreshTimeSpan.textContent = lastRefreshTime ? new Date(lastRefreshTime).toLocaleTimeString() : 'Never';
|
||
}
|
||
}
|
||
|
||
function stopRefreshAnimation() {
|
||
const refreshButton = document.getElementById('refreshOffers');
|
||
const refreshIcon = document.getElementById('refreshIcon');
|
||
const refreshText = document.getElementById('refreshText');
|
||
|
||
if (refreshButton) {
|
||
refreshButton.disabled = false;
|
||
refreshButton.classList.remove('opacity-75', 'cursor-wait');
|
||
}
|
||
if (refreshIcon) {
|
||
refreshIcon.classList.remove('animate-spin');
|
||
}
|
||
if (refreshText) {
|
||
refreshText.textContent = 'Refresh';
|
||
}
|
||
}
|
||
|
||
function updatePaginationInfo() {
|
||
const validOffers = getValidOffers();
|
||
const totalItems = validOffers.length;
|
||
const totalPages = Math.max(1, Math.ceil(totalItems / itemsPerPage));
|
||
const previousPage = currentPage;
|
||
|
||
if (previousPage !== currentPage) {
|
||
debugPaginationChange('updatePaginationInfo', previousPage, currentPage);
|
||
}
|
||
|
||
if (currentPageSpan) currentPageSpan.textContent = totalPages > 0 ? currentPage : 0;
|
||
if (totalPagesSpan) totalPagesSpan.textContent = totalPages;
|
||
|
||
const showPrev = currentPage > 1;
|
||
const showNext = currentPage < totalPages;
|
||
|
||
if (prevPageButton) {
|
||
prevPageButton.style.display = showPrev ? 'inline-flex' : 'none';
|
||
prevPageButton.disabled = !showPrev;
|
||
}
|
||
|
||
if (nextPageButton) {
|
||
nextPageButton.style.display = showNext ? 'inline-flex' : 'none';
|
||
nextPageButton.disabled = !showNext;
|
||
}
|
||
|
||
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 updateProfitLoss(row, fromCoin, toCoin, fromAmount, toAmount, isOwnOffer) {
|
||
const profitLossElement = row.querySelector('.profit-loss');
|
||
if (!profitLossElement) {
|
||
return;
|
||
}
|
||
|
||
if (!fromCoin || !toCoin) {
|
||
profitLossElement.textContent = 'N/A';
|
||
profitLossElement.className = 'profit-loss text-lg font-bold text-gray-300';
|
||
return;
|
||
}
|
||
|
||
calculateProfitLoss(fromCoin, toCoin, fromAmount, toAmount, isOwnOffer)
|
||
.then(percentDiff => {
|
||
if (percentDiff === null || isNaN(percentDiff)) {
|
||
profitLossElement.textContent = 'N/A';
|
||
profitLossElement.className = 'profit-loss text-lg font-bold text-gray-300';
|
||
return;
|
||
}
|
||
|
||
const percentDiffDisplay = percentDiff === "0.00" ? "0.00" :
|
||
(percentDiff > 0 ? percentDiff : -percentDiff);
|
||
|
||
const colorClass = getProfitColorClass(percentDiff);
|
||
profitLossElement.textContent = `${percentDiffDisplay.toFixed(2)}%`;
|
||
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 = `
|
||
<div class="tooltip-content">
|
||
${tooltipContent}
|
||
</div>
|
||
<div class="tooltip-arrow" data-popper-arrow></div>
|
||
`;
|
||
}
|
||
})
|
||
.catch(error => {
|
||
profitLossElement.textContent = 'N/A';
|
||
profitLossElement.className = 'profit-loss text-lg font-bold text-gray-300';
|
||
});
|
||
}
|
||
|
||
function updateCoinFilterImages() {
|
||
return;
|
||
}
|
||
|
||
function updateClearFiltersButton() {
|
||
const clearButton = document.getElementById('clearFilters');
|
||
if (clearButton) {
|
||
const hasFilters = hasActiveFilters();
|
||
clearButton.classList.toggle('opacity-50', !hasFilters);
|
||
clearButton.disabled = !hasFilters;
|
||
|
||
if (hasFilters) {
|
||
clearButton.classList.add('hover:bg-green-600', 'hover:text-white');
|
||
clearButton.classList.remove('cursor-not-allowed');
|
||
} else {
|
||
clearButton.classList.remove('hover:bg-green-600', 'hover:text-white');
|
||
clearButton.classList.add('cursor-not-allowed');
|
||
}
|
||
}
|
||
}
|
||
|
||
function cleanupRow(row) {
|
||
if (!row) return;
|
||
|
||
const tooltipTriggers = row.querySelectorAll('[data-tooltip-trigger-id]');
|
||
tooltipTriggers.forEach(trigger => {
|
||
if (window.TooltipManager) {
|
||
window.TooltipManager.destroy(trigger);
|
||
}
|
||
});
|
||
|
||
CleanupManager.removeListenersByElement(row);
|
||
|
||
while (row.attributes && row.attributes.length > 0) {
|
||
row.removeAttribute(row.attributes[0].name);
|
||
}
|
||
|
||
while (row.firstChild) {
|
||
const child = row.firstChild;
|
||
row.removeChild(child);
|
||
}
|
||
}
|
||
|
||
function cleanupTable() {
|
||
if (!offersBody) return;
|
||
|
||
const existingRows = Array.from(offersBody.querySelectorAll('tr'));
|
||
|
||
existingRows.forEach(row => cleanupRow(row));
|
||
|
||
offersBody.innerHTML = '';
|
||
|
||
if (window.TooltipManager) {
|
||
window.TooltipManager.cleanup();
|
||
}
|
||
}
|
||
|
||
function handleNoOffersScenario() {
|
||
const hasFilters = hasActiveFilters();
|
||
|
||
stopRefreshAnimation();
|
||
|
||
const existingRows = offersBody.querySelectorAll('tr');
|
||
existingRows.forEach(row => {
|
||
cleanupRow(row);
|
||
});
|
||
|
||
if (hasFilters) {
|
||
offersBody.innerHTML = `
|
||
<tr>
|
||
<td colspan="9" class="text-center py-8">
|
||
<div class="flex items-center justify-center text-gray-500 dark:text-white">
|
||
No offers match the selected filters. Try different filter options or
|
||
<button onclick="clearFilters()" class="ml-1 text-blue-500 hover:text-blue-700 font-semibold">
|
||
clear filters
|
||
</button>
|
||
</div>
|
||
</td>
|
||
</tr>`;
|
||
} else {
|
||
offersBody.innerHTML = `
|
||
<tr>
|
||
<td colspan="9" class="text-center py-8 text-gray-500 dark:text-white">
|
||
No active offers available.
|
||
</td>
|
||
</tr>`;
|
||
}
|
||
}
|
||
|
||
async function updateOffersTable(options = {}) {
|
||
try {
|
||
|
||
if (isPaginationInProgress && !options.fromPaginationClick) {
|
||
console.log('Skipping table update during pagination operation');
|
||
return;
|
||
}
|
||
|
||
const isIncrementalUpdate = options.incremental === true;
|
||
if (!options.skipSkeleton && !isIncrementalUpdate && offersBody) {
|
||
offersBody.innerHTML = '<tr><td colspan="10" class="text-center py-8 text-gray-500 dark:text-gray-400"><div class="animate-pulse">Loading offers...</div></td></tr>';
|
||
}
|
||
|
||
if (window.TooltipManager) {
|
||
requestAnimationFrame(() => window.TooltipManager.cleanup());
|
||
}
|
||
|
||
const validOffers = getValidOffers();
|
||
if (validOffers.length === 0) {
|
||
handleNoOffersScenario();
|
||
return;
|
||
}
|
||
|
||
const totalPages = Math.ceil(validOffers.length / itemsPerPage);
|
||
|
||
if (!options.fromPaginationClick) {
|
||
const oldPage = currentPage;
|
||
currentPage = Math.max(1, Math.min(currentPage, totalPages));
|
||
if (oldPage !== currentPage) {
|
||
debugPaginationChange('updateOffersTable auto-adjust', oldPage, currentPage);
|
||
}
|
||
}
|
||
|
||
const startIndex = (currentPage - 1) * itemsPerPage;
|
||
const endIndex = Math.min(startIndex + itemsPerPage, validOffers.length);
|
||
const itemsToDisplay = validOffers.slice(startIndex, endIndex);
|
||
|
||
const fragment = document.createDocumentFragment();
|
||
|
||
const BATCH_SIZE = 10;
|
||
for (let i = 0; i < itemsToDisplay.length; i += BATCH_SIZE) {
|
||
const batch = itemsToDisplay.slice(i, i + BATCH_SIZE);
|
||
|
||
const batchPromises = batch.map(offer =>
|
||
offer.addr_from ? IdentityManager.getIdentityData(offer.addr_from) : Promise.resolve(null)
|
||
);
|
||
|
||
const batchIdentities = await Promise.all(batchPromises);
|
||
|
||
batch.forEach((offer, index) => {
|
||
const row = createTableRow(offer, batchIdentities[index]);
|
||
if (row) fragment.appendChild(row);
|
||
});
|
||
|
||
}
|
||
|
||
if (offersBody) {
|
||
if (isIncrementalUpdate && offersBody.children.length > 0) {
|
||
|
||
const existingRows = Array.from(offersBody.querySelectorAll('tr[data-offer-id]'));
|
||
const newRows = Array.from(fragment.querySelectorAll('tr[data-offer-id]'));
|
||
|
||
const existingMap = new Map(existingRows.map(row => [row.getAttribute('data-offer-id'), row]));
|
||
const newMap = new Map(newRows.map(row => [row.getAttribute('data-offer-id'), row]));
|
||
|
||
existingRows.forEach(row => {
|
||
const offerId = row.getAttribute('data-offer-id');
|
||
if (!newMap.has(offerId)) {
|
||
cleanupRow(row);
|
||
row.remove();
|
||
}
|
||
});
|
||
|
||
newRows.forEach((newRow, index) => {
|
||
const offerId = newRow.getAttribute('data-offer-id');
|
||
const existingRow = existingMap.get(offerId);
|
||
|
||
if (existingRow) {
|
||
|
||
const currentIndex = Array.from(offersBody.children).indexOf(existingRow);
|
||
if (currentIndex !== index) {
|
||
|
||
if (index >= offersBody.children.length) {
|
||
offersBody.appendChild(existingRow);
|
||
} else {
|
||
offersBody.insertBefore(existingRow, offersBody.children[index]);
|
||
}
|
||
}
|
||
|
||
} else {
|
||
|
||
if (index >= offersBody.children.length) {
|
||
offersBody.appendChild(newRow);
|
||
} else {
|
||
offersBody.insertBefore(newRow, offersBody.children[index]);
|
||
}
|
||
}
|
||
});
|
||
} else {
|
||
|
||
const existingRows = offersBody.querySelectorAll('tr');
|
||
existingRows.forEach(row => cleanupRow(row));
|
||
offersBody.textContent = '';
|
||
offersBody.appendChild(fragment);
|
||
}
|
||
}
|
||
|
||
initializeTooltipsInBatches();
|
||
|
||
CleanupManager.setTimeout(() => {
|
||
updateRowTimes();
|
||
updatePaginationInfo();
|
||
updateProfitLossDisplays();
|
||
}, 10);
|
||
|
||
CleanupManager.setTimeout(() => {
|
||
if (tableRateModule?.initializeTable) {
|
||
tableRateModule.initializeTable();
|
||
}
|
||
}, 50);
|
||
|
||
lastRefreshTime = Date.now();
|
||
updateLastRefreshTime();
|
||
|
||
} catch (error) {
|
||
console.error('[Debug] Error in updateOffersTable:', error);
|
||
handleTableError();
|
||
}
|
||
}
|
||
|
||
function updateProfitLossDisplays() {
|
||
|
||
const rows = document.querySelectorAll('[data-offer-id]');
|
||
const updates = [];
|
||
|
||
rows.forEach(row => {
|
||
const offerId = row.getAttribute('data-offer-id');
|
||
const offer = jsonData.find(o => o.offer_id === offerId);
|
||
if (!offer) return;
|
||
|
||
const fromAmount = parseFloat(offer.amount_from) || 0;
|
||
const toAmount = parseFloat(offer.amount_to) || 0;
|
||
|
||
updates.push({
|
||
row,
|
||
offerId,
|
||
offer,
|
||
fromAmount,
|
||
toAmount
|
||
});
|
||
});
|
||
|
||
updates.forEach(({ row, offerId, offer, fromAmount, toAmount }) => {
|
||
updateProfitLoss(row, offer.coin_from, offer.coin_to, fromAmount, toAmount, offer.is_own_offer);
|
||
|
||
const rateTooltipId = `tooltip-rate-${offerId}`;
|
||
const rateTooltip = document.getElementById(rateTooltipId);
|
||
if (rateTooltip) {
|
||
const tooltipContent = createCombinedRateTooltip(offer, offer.coin_from, offer.coin_to, offer.is_own_offer);
|
||
rateTooltip.innerHTML = tooltipContent;
|
||
}
|
||
});
|
||
}
|
||
|
||
function handleTableError() {
|
||
offersBody.innerHTML = `
|
||
<tr>
|
||
<td colspan="8" class="text-center py-4 text-gray-500">
|
||
<div class="flex flex-col items-center justify-center gap-2">
|
||
<span>An error occurred while updating the table.</span>
|
||
<span class="text-sm">The table will continue to function with cached data.</span>
|
||
</div>
|
||
</td>
|
||
</tr>`;
|
||
}
|
||
|
||
function getIdentityInfo(address, identity) {
|
||
if (!identity) {
|
||
return {
|
||
displayAddr: address ? `${address.substring(0, 10)}...` : 'Unspecified',
|
||
fullAddress: address || '',
|
||
label: '',
|
||
note: '',
|
||
automationOverride: 0,
|
||
stats: {
|
||
sentBidsSuccessful: 0,
|
||
recvBidsSuccessful: 0,
|
||
sentBidsRejected: 0,
|
||
recvBidsRejected: 0,
|
||
sentBidsFailed: 0,
|
||
recvBidsFailed: 0
|
||
}
|
||
};
|
||
}
|
||
|
||
return {
|
||
displayAddr: address ? `${address.substring(0, 10)}...` : 'Unspecified',
|
||
fullAddress: address || '',
|
||
label: identity.label || '',
|
||
note: identity.note || '',
|
||
automationOverride: identity.automation_override || 0,
|
||
stats: {
|
||
sentBidsSuccessful: identity.num_sent_bids_successful || 0,
|
||
recvBidsSuccessful: identity.num_recv_bids_successful || 0,
|
||
sentBidsRejected: identity.num_sent_bids_rejected || 0,
|
||
recvBidsRejected: identity.num_recv_bids_rejected || 0,
|
||
sentBidsFailed: identity.num_sent_bids_failed || 0,
|
||
recvBidsFailed: identity.num_recv_bids_failed || 0
|
||
}
|
||
};
|
||
}
|
||
|
||
function createTableRow(offer, identity = null) {
|
||
const row = document.createElement('tr');
|
||
const uniqueId = `${offer.offer_id}_${offer.created_at}`;
|
||
|
||
row.className = 'relative 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 {
|
||
coin_from: coinFrom,
|
||
coin_to: coinTo,
|
||
created_at: createdAt,
|
||
expire_at: expireAt,
|
||
amount_from: amountFrom,
|
||
amount_to: amountTo,
|
||
is_own_offer: isOwnOffer,
|
||
is_revoked: isRevoked,
|
||
is_public: isPublic
|
||
} = offer;
|
||
|
||
let coinFromSymbol, coinToSymbol;
|
||
|
||
if (window.CoinManager) {
|
||
coinFromSymbol = window.CoinManager.getSymbol(coinFrom) || coinFrom.toLowerCase();
|
||
coinToSymbol = window.CoinManager.getSymbol(coinTo) || coinTo.toLowerCase();
|
||
} else {
|
||
coinFromSymbol = coinFrom.toLowerCase();
|
||
coinToSymbol = coinTo.toLowerCase();
|
||
}
|
||
|
||
let coinFromDisplay, coinToDisplay;
|
||
|
||
if (window.CoinManager) {
|
||
coinFromDisplay = window.CoinManager.getDisplayName(coinFrom) || coinFrom;
|
||
coinToDisplay = window.CoinManager.getDisplayName(coinTo) || coinTo;
|
||
} else {
|
||
coinFromDisplay = coinFrom;
|
||
coinToDisplay = coinTo;
|
||
if (coinFromDisplay.toLowerCase() === 'zcoin') coinFromDisplay = 'Firo';
|
||
if (coinToDisplay.toLowerCase() === 'zcoin') coinToDisplay = 'Firo';
|
||
}
|
||
|
||
const postedTime = formatTime(createdAt, true);
|
||
const expiresIn = formatTime(expireAt);
|
||
const currentTime = Math.floor(Date.now() / 1000);
|
||
const isActuallyExpired = currentTime > expireAt;
|
||
const fromAmount = parseFloat(amountFrom) || 0;
|
||
const toAmount = parseFloat(amountTo) || 0;
|
||
|
||
row.innerHTML = `
|
||
${!isPublic ? createPrivateIndicator() : '<td class="w-0 p-0 m-0"></td>'}
|
||
${createTimeColumn(offer, postedTime, expiresIn)}
|
||
${createDetailsColumn(offer, identity)}
|
||
${createTakerAmountColumn(offer, coinTo, coinFrom)}
|
||
${createSwapColumn(offer, coinFromDisplay, coinToDisplay, coinFromSymbol, coinToSymbol)}
|
||
${createOrderbookColumn(offer, coinFrom)}
|
||
${createRateColumn(offer, coinFrom, coinTo)}
|
||
${createPercentageColumn(offer)}
|
||
${createActionColumn(offer, isActuallyExpired)}
|
||
${createTooltips(
|
||
offer,
|
||
isOwnOffer,
|
||
coinFrom,
|
||
coinTo,
|
||
fromAmount,
|
||
toAmount,
|
||
postedTime,
|
||
expiresIn,
|
||
isActuallyExpired,
|
||
Boolean(isRevoked),
|
||
identity
|
||
)}
|
||
`;
|
||
|
||
updateTooltipTargets(row, uniqueId);
|
||
updateProfitLoss(row, coinFrom, coinTo, fromAmount, toAmount, isOwnOffer);
|
||
|
||
return row;
|
||
}
|
||
|
||
function createPrivateIndicator() {
|
||
return `<td class="relative w-0 p-0 m-0">
|
||
<div class="absolute top-0 bottom-0 left-0 w-1 bg-red-700" style="min-height: 100%;"></div>
|
||
</td>`;
|
||
}
|
||
|
||
function createTimeColumn(offer, postedTime, expiresIn) {
|
||
const now = Math.floor(Date.now() / 1000);
|
||
const timeLeft = offer.expire_at - now;
|
||
|
||
let strokeColor = '#10B981';
|
||
if (timeLeft <= 300) {
|
||
strokeColor = '#9CA3AF';
|
||
} else if (timeLeft <= 1800) {
|
||
strokeColor = '#3B82F6';
|
||
}
|
||
|
||
return `
|
||
<td class="py-3 pl-1 pr-2 text-xs whitespace-nowrap">
|
||
<div class="flex items-center">
|
||
<div class="relative" data-tooltip-target="tooltip-active${escapeHtml(offer.offer_id)}">
|
||
<svg alt="" class="w-5 h-5 rounded-full mr-4 cursor-pointer" xmlns="http://www.w3.org/2000/svg" height="20" width="20" viewBox="0 0 24 24">
|
||
<g stroke-linecap="round" stroke-width="2" fill="none" stroke="${strokeColor}" stroke-linejoin="round">
|
||
<circle cx="12" cy="12" r="11"></circle>
|
||
<polyline points="12,6 12,12 18,12" stroke="${strokeColor}"></polyline>
|
||
</g>
|
||
</svg>
|
||
</div>
|
||
<div class="flex flex-col hidden xl:block">
|
||
<div class="text-xs whitespace-nowrap"><span class="bold">Posted:</span> ${escapeHtml(postedTime)}</div>
|
||
<div class="text-xs whitespace-nowrap"><span class="bold">Expires in:</span> ${escapeHtml(expiresIn)}</div>
|
||
</div>
|
||
</div>
|
||
</td>
|
||
`;
|
||
}
|
||
|
||
function truncateText(text, maxLength = 15) {
|
||
if (typeof text !== 'string') return '';
|
||
return text.length > maxLength
|
||
? text.slice(0, maxLength) + '...'
|
||
: text;
|
||
}
|
||
|
||
function createDetailsColumn(offer, identity = null) {
|
||
const addrFrom = offer.addr_from || '';
|
||
const identityInfo = getIdentityInfo(addrFrom, identity);
|
||
|
||
const showPublicPrivateTags = originalJsonData.some(o => o.is_public !== offer.is_public);
|
||
|
||
const tagClass = offer.is_public
|
||
? 'bg-green-600 dark:bg-green-600'
|
||
: 'bg-red-500 dark:bg-red-500';
|
||
const tagText = offer.is_public ? 'Public' : 'Private';
|
||
|
||
const displayIdentifier = truncateText(
|
||
identityInfo.label || addrFrom || 'Unspecified'
|
||
);
|
||
|
||
const identifierTextClass = identityInfo.label
|
||
? 'dark:text-white'
|
||
: 'monospace';
|
||
|
||
return `
|
||
<td class="py-8 px-4 text-xs text-left hidden xl:block">
|
||
<div class="flex flex-col gap-2 relative">
|
||
${showPublicPrivateTags ? `<span class="inline-flex pl-6 pr-6 py-1 justify-center text-[10px] w-1/4 font-medium text-gray-100 rounded-md ${tagClass}">${tagText}</span>
|
||
` : ''}
|
||
|
||
<a data-tooltip-target="tooltip-recipient-${escapeHtml(offer.offer_id)}" href="/identity/${escapeHtml(addrFrom)}" class="flex items-center">
|
||
<svg class="w-4 h-4 mr-2 text-gray-400 dark:text-white" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
|
||
<path fill-rule="evenodd" d="M10 9a3 3 0 100-6 3 3 0 000 6zm-7 9a7 7 0 1114 0H3z" clip-rule="evenodd"></path>
|
||
</svg>
|
||
<span class="${identifierTextClass}">
|
||
${escapeHtml(displayIdentifier)}
|
||
</span>
|
||
</a>
|
||
</div>
|
||
</td>
|
||
`;
|
||
}
|
||
|
||
function createTakerAmountColumn(offer, coinTo) {
|
||
const fromAmount = parseFloat(offer.amount_to);
|
||
const toSymbol = getCoinSymbol(coinTo);
|
||
return `
|
||
<td class="py-0">
|
||
<div class="py-3 px-4 text-left">
|
||
<a data-tooltip-target="tooltip-wallet${escapeHtml(offer.offer_id)}" href="/wallet/${escapeHtml(toSymbol)}" class="items-center monospace">
|
||
<div class="pr-2">
|
||
<div class="text-sm font-semibold">${fromAmount.toFixed(4)}</div>
|
||
<div class="text-sm text-gray-500 dark:text-gray-400">${coinTo}</div>
|
||
</div>
|
||
</a>
|
||
</div>
|
||
</td>
|
||
`;
|
||
}
|
||
|
||
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 `
|
||
<td class="py-0 px-0 text-right text-sm">
|
||
<a data-tooltip-target="tooltip-offer${offer.offer_id}" href="/offer/${offer.offer_id}">
|
||
<div class="flex items-center justify-evenly monospace">
|
||
<span class="inline-flex mr-3 ml-3 align-middle items-center justify-center w-18 h-20 rounded">
|
||
<img class="h-12" src="/static/images/coins/${getImageFilename(coinToSymbol, coinToDisplay)}" alt="${coinToDisplay}">
|
||
</span>
|
||
<svg aria-hidden="true" class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
|
||
<path fill-rule="evenodd" d="M12.293 5.293a1 1 0 011.414 0l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414-1.414L14.586 11H3a1 1 0 110-2h11.586l-2.293-2.293a1 1 0 010-1.414z" clip-rule="evenodd"></path>
|
||
</svg>
|
||
<span class="inline-flex ml-3 mr-3 align-middle items-center justify-center w-18 h-20 rounded">
|
||
<img class="h-12" src="/static/images/coins/${getImageFilename(coinFromSymbol, coinFromDisplay)}" alt="${coinFromDisplay}">
|
||
</span>
|
||
</div>
|
||
</a>
|
||
</td>
|
||
`;
|
||
}
|
||
|
||
function createOrderbookColumn(offer, coinFrom) {
|
||
const toAmount = parseFloat(offer.amount_from);
|
||
const fromSymbol = getCoinSymbol(coinFrom);
|
||
return `
|
||
<td class="p-0">
|
||
<div class="py-3 px-4 text-right">
|
||
<a data-tooltip-target="tooltip-wallet-maker${escapeHtml(offer.offer_id)}" href="/wallet/${escapeHtml(fromSymbol)}" class="items-center monospace">
|
||
<div class="pr-2">
|
||
<div class="text-sm font-semibold">${toAmount.toFixed(4)}</div>
|
||
<div class="text-sm text-gray-500 dark:text-gray-400">${coinFrom}</div>
|
||
</div>
|
||
</a>
|
||
</div>
|
||
</td>
|
||
`;
|
||
}
|
||
|
||
function createRateColumn(offer, coinFrom, coinTo) {
|
||
const rate = parseFloat(offer.rate) || 0;
|
||
const inverseRate = rate ? (1 / rate) : 0;
|
||
|
||
const toSymbolKey = getPriceKey(coinTo);
|
||
let toPriceUSD = latestPrices && latestPrices[toSymbolKey] ? latestPrices[toSymbolKey].usd : null;
|
||
|
||
if (!toPriceUSD || isNaN(toPriceUSD)) {
|
||
toPriceUSD = tableRateModule.getFallbackValue(toSymbolKey);
|
||
}
|
||
|
||
const rateInUSD = toPriceUSD && !isNaN(toPriceUSD) && !isNaN(rate) ? rate * toPriceUSD : null;
|
||
const fromSymbol = getCoinSymbol(coinFrom);
|
||
const toSymbol = getCoinSymbol(coinTo);
|
||
|
||
return `
|
||
<td class="py-3 semibold monospace text-xs text-right items-center rate-table-info">
|
||
<div class="relative">
|
||
<div class="flex flex-col items-end pr-3" data-tooltip-target="tooltip-rate-${offer.offer_id}">
|
||
<span class="text-sm bold text-gray-700 dark:text-white">
|
||
${rateInUSD !== null ? `$${rateInUSD.toFixed(2)} USD` : 'N/A'}
|
||
</span>
|
||
<span class="bold text-gray-700 dark:text-white">
|
||
${rate.toFixed(8)} ${toSymbol}/${fromSymbol}
|
||
</span>
|
||
<span class="semibold text-gray-400 dark:text-gray-300">
|
||
${inverseRate.toFixed(8)} ${fromSymbol}/${toSymbol}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</td>
|
||
`;
|
||
}
|
||
|
||
function createPercentageColumn(offer) {
|
||
return `
|
||
<td class="py-3 px-2 bold text-sm text-center monospace items-center rate-table-info">
|
||
<div class="relative" data-tooltip-target="percentage-tooltip-${offer.offer_id}">
|
||
<div class="profittype">
|
||
<span class="profit-loss text-lg font-bold" data-offer-id="${offer.offer_id}">
|
||
Calculating...
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</td>
|
||
`;
|
||
}
|
||
|
||
function createActionColumn(offer, isActuallyExpired = false) {
|
||
const isRevoked = Boolean(offer.is_revoked);
|
||
const isTreatedAsSentOffer = offer.is_own_offer;
|
||
|
||
let buttonClass, buttonText;
|
||
|
||
if (isRevoked) {
|
||
buttonClass = 'bg-red-500 text-white hover:bg-red-600 transition duration-200';
|
||
buttonText = 'Revoked';
|
||
} else if (isActuallyExpired && isSentOffers) {
|
||
buttonClass = 'bg-gray-400 text-white dark:border-gray-300 text-white hover:bg-red-700 transition duration-200';
|
||
buttonText = 'Expired';
|
||
} else if (isTreatedAsSentOffer) {
|
||
buttonClass = 'bg-gray-300 bold text-white bold hover:bg-green-600 transition duration-200';
|
||
buttonText = 'Edit';
|
||
} else {
|
||
buttonClass = 'bg-blue-500 text-white hover:bg-green-600 transition duration-200';
|
||
buttonText = 'Swap';
|
||
}
|
||
|
||
return `
|
||
<td class="py-6 px-2 text-center">
|
||
<div class="flex justify-center items-center h-full">
|
||
<a class="inline-block w-20 py-1 px-2 font-medium text-center text-sm rounded-md ${buttonClass}"
|
||
href="/offer/${offer.offer_id}">
|
||
${buttonText}
|
||
</a>
|
||
</div>
|
||
</td>
|
||
`;
|
||
}
|
||
|
||
function createTooltips(offer, treatAsSentOffer, coinFrom, coinTo, fromAmount, toAmount, postedTime, expiresIn, isActuallyExpired, isRevoked, identity = null) {
|
||
const uniqueId = `${offer.offer_id}_${offer.created_at}`;
|
||
|
||
const addrFrom = offer.addr_from || '';
|
||
const identityInfo = getIdentityInfo(addrFrom, identity);
|
||
|
||
const totalBids = identity ? (
|
||
identityInfo.stats.sentBidsSuccessful +
|
||
identityInfo.stats.recvBidsSuccessful +
|
||
identityInfo.stats.sentBidsFailed +
|
||
identityInfo.stats.recvBidsFailed +
|
||
identityInfo.stats.sentBidsRejected +
|
||
identityInfo.stats.recvBidsRejected
|
||
) : 0;
|
||
|
||
const successRate = totalBids ? (
|
||
((identityInfo.stats.sentBidsSuccessful + identityInfo.stats.recvBidsSuccessful) / totalBids) * 100
|
||
).toFixed(1) : 0;
|
||
|
||
const combinedRateTooltip = createCombinedRateTooltip(offer, coinFrom, coinTo, treatAsSentOffer);
|
||
const percentageTooltipContent = createTooltipContent(treatAsSentOffer, coinFrom, coinTo, fromAmount, toAmount);
|
||
|
||
return `
|
||
<div id="tooltip-active-${uniqueId}" role="tooltip" class="inline-block absolute hidden z-50 py-2 px-3 text-sm font-medium text-white bg-gray-400 rounded-lg shadow-sm opacity-0 transition-opacity duration-300 tooltip">
|
||
<div class="active-revoked-expired">
|
||
<span class="bold">
|
||
<div class="text-xs"><span class="bold">Posted:</span> ${postedTime}</div>
|
||
<div class="text-xs"><span class="bold">Expires in:</span> ${expiresIn}</div>
|
||
${isRevoked ? '<div class="text-xs text-red-300"><span class="bold">Status:</span> Revoked</div>' : ''}
|
||
</span>
|
||
</div>
|
||
<div class="mt-5 text-xs">
|
||
<p class="font-bold mb-3">Time Indicator Colors:</p>
|
||
<p class="flex items-center">
|
||
<svg class="w-5 h-5 mr-3" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||
<g stroke-linecap="round" stroke-width="2" fill="none" stroke="#10B981" stroke-linejoin="round">
|
||
<circle cx="12" cy="12" r="11"></circle>
|
||
<polyline points="12,6 12,12 18,12" stroke="#10B981"></polyline>
|
||
</g>
|
||
</svg>
|
||
Green: More than 30 minutes left
|
||
</p>
|
||
<p class="flex items-center mt-3">
|
||
<svg class="w-5 h-5 mr-3" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||
<g stroke-linecap="round" stroke-width="2" fill="none" stroke="#3B82F6" stroke-linejoin="round">
|
||
<circle cx="12" cy="12" r="11"></circle>
|
||
<polyline points="12,6 12,12 18,12" stroke="#3B82F6"></polyline>
|
||
</g>
|
||
</svg>
|
||
Blue: Between 5 and 30 minutes left
|
||
</p>
|
||
<p class="flex items-center mt-3 mb-3">
|
||
<svg class="w-5 h-5 mr-3" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||
<g stroke-linecap="round" stroke-width="2" fill="none" stroke="#9CA3AF" stroke-linejoin="round">
|
||
<circle cx="12" cy="12" r="11"></circle>
|
||
<polyline points="12,6 12,12 18,12" stroke="#9CA3AF"></polyline>
|
||
</g>
|
||
</svg>
|
||
Grey: Less than 5 minutes left or expired
|
||
</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="tooltip-wallet-${uniqueId}" role="tooltip" class="inline-block absolute hidden z-50 py-2 px-3 text-sm font-medium text-white bg-gray-400 rounded-lg shadow-sm opacity-0 transition-opacity duration-300 tooltip">
|
||
<div class="active-revoked-expired">
|
||
<span class="bold">${treatAsSentOffer ? 'My' : ''} ${coinTo} Wallet</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="tooltip-offer-${uniqueId}" role="tooltip" class="inline-block absolute hidden z-50 py-2 px-3 text-sm font-medium text-white ${isRevoked ? 'bg-red-500' : (offer.is_own_offer ? 'bg-gray-300' : 'bg-green-700')} rounded-lg shadow-sm opacity-0 transition-opacity duration-300 tooltip">
|
||
<div class="active-revoked-expired">
|
||
<span class="bold">
|
||
${isRevoked ? 'Offer Revoked' : (offer.is_own_offer ? 'Edit Offer' : `Buy ${coinFrom}`)}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="tooltip-wallet-maker-${uniqueId}" role="tooltip" class="inline-block absolute hidden z-50 py-2 px-3 text-sm font-medium text-white bg-gray-400 rounded-lg shadow-sm opacity-0 transition-opacity duration-300 tooltip">
|
||
<div class="active-revoked-expired">
|
||
<span class="bold">${treatAsSentOffer ? 'My' : ''} ${coinFrom} Wallet</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="tooltip-rate-${uniqueId}" role="tooltip" class="inline-block absolute hidden z-50 py-2 px-3 text-sm font-medium text-white bg-gray-400 rounded-lg shadow-sm opacity-0 transition-opacity duration-300 tooltip">
|
||
<div class="tooltip-content">
|
||
${combinedRateTooltip}
|
||
</div>
|
||
</div>
|
||
|
||
<div id="percentage-tooltip-${uniqueId}" role="tooltip" class="inline-block absolute hidden z-50 py-2 px-3 text-sm font-medium text-white bg-gray-400 rounded-lg shadow-sm opacity-0 transition-opacity duration-300 tooltip">
|
||
<div class="tooltip-content">
|
||
${percentageTooltipContent}
|
||
</div>
|
||
</div>
|
||
|
||
${createRecipientTooltip(uniqueId, identityInfo, identity, successRate, totalBids)}
|
||
`;
|
||
}
|
||
|
||
function createRecipientTooltip(uniqueId, identityInfo, identity, successRate, totalBids) {
|
||
|
||
const getSuccessRateColor = (rate) => {
|
||
if (rate >= 80) return 'text-green-600';
|
||
if (rate >= 60) return 'text-yellow-600';
|
||
return 'text-red-600';
|
||
};
|
||
|
||
const truncateText = (text, maxLength) => {
|
||
if (text.length <= maxLength) return text;
|
||
return text.substring(0, maxLength) + '...';
|
||
};
|
||
|
||
return `
|
||
<div id="tooltip-recipient-${uniqueId}" role="tooltip"
|
||
class="fixed z-50 py-3 px-4 text-sm font-medium text-white bg-gray-400 rounded-lg shadow-sm opacity-0 transition-opacity duration-300 tooltip max-w-sm pointer-events-none">
|
||
<div class="identity-info space-y-2">
|
||
${identityInfo.label ? `
|
||
<div class="border-b border-gray-400 pb-2">
|
||
<div class="text-white text-xs tracking-wide font-semibold">Label:</div>
|
||
<div class="text-white">${escapeHtml(identityInfo.label)}</div>
|
||
</div>
|
||
` : ''}
|
||
|
||
<div class="space-y-1">
|
||
<div class="text-white text-xs tracking-wide font-semibold">Recipient Address:</div>
|
||
<div class="monospace text-xs break-all bg-gray-500 p-2 rounded-md text-white">
|
||
${escapeHtml(identityInfo.fullAddress)}
|
||
</div>
|
||
</div>
|
||
|
||
${identityInfo.note ? `
|
||
<div class="space-y-1 hidden">
|
||
<div class="text-white text-xs tracking-wide font-semibold">Note:</div>
|
||
<div class="text-white text-sm italic" title="${escapeHtml(identityInfo.note)}">
|
||
${escapeHtml(truncateText(identityInfo.note, 150))}
|
||
</div>
|
||
</div>
|
||
` : ''}
|
||
|
||
${identity ? `
|
||
<div class= pt-2 mt-2">
|
||
<div class="text-white text-xs tracking-wide font-semibold mb-2">Swap History:</div>
|
||
<div class="grid grid-cols-2 gap-2">
|
||
<div class="text-center p-2 bg-gray-500 rounded-md">
|
||
<div class="text-lg font-bold ${getSuccessRateColor(successRate)}">${successRate}%</div>
|
||
<div class="text-xs text-white">Success Rate</div>
|
||
</div>
|
||
<div class="text-center p-2 bg-gray-500 rounded-md">
|
||
<div class="text-lg font-bold text-blue-500">${totalBids}</div>
|
||
<div class="text-xs text-white">Total Trades</div>
|
||
</div>
|
||
</div>
|
||
<div class="grid grid-cols-3 gap-2 mt-2 text-center text-xs">
|
||
<div>
|
||
<div class="text-green-600 font-semibold">
|
||
${identityInfo.stats.sentBidsSuccessful + identityInfo.stats.recvBidsSuccessful}
|
||
</div>
|
||
<div class="text-white">Successful</div>
|
||
</div>
|
||
<div>
|
||
<div class="text-yellow-600 font-semibold">
|
||
${identityInfo.stats.sentBidsRejected + identityInfo.stats.recvBidsRejected}
|
||
</div>
|
||
<div class="text-white">Rejected</div>
|
||
</div>
|
||
<div>
|
||
<div class="text-red-600 font-semibold">
|
||
${identityInfo.stats.sentBidsFailed + identityInfo.stats.recvBidsFailed}
|
||
</div>
|
||
<div class="text-white">Failed</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
` : ''}
|
||
</div>
|
||
<div class="tooltip-arrow" data-popper-arrow></div>
|
||
</div>`;
|
||
}
|
||
|
||
function createTooltipContent(isSentOffers, coinFrom, coinTo, fromAmount, toAmount, isOwnOffer) {
|
||
if (!coinFrom || !coinTo) {
|
||
return `<p class="font-bold mb-1">Unable to calculate profit/loss</p>
|
||
<p>Invalid coin data.</p>`;
|
||
}
|
||
|
||
fromAmount = parseFloat(fromAmount) || 0;
|
||
toAmount = parseFloat(toAmount) || 0;
|
||
|
||
const getPriceKey = (coin) => {
|
||
if (!coin) return null;
|
||
if (window.CoinUtils) {
|
||
return window.CoinUtils.normalizeCoinName(coin, latestPrices);
|
||
}
|
||
return coin.toLowerCase();
|
||
};
|
||
|
||
const fromSymbol = getPriceKey(coinFrom);
|
||
const toSymbol = getPriceKey(coinTo);
|
||
|
||
let fromPriceUSD = latestPrices && fromSymbol ? latestPrices[fromSymbol]?.usd : null;
|
||
let toPriceUSD = latestPrices && toSymbol ? latestPrices[toSymbol]?.usd : null;
|
||
|
||
if (!fromPriceUSD && window.tableRateModule) {
|
||
fromPriceUSD = tableRateModule.getFallbackValue(fromSymbol);
|
||
}
|
||
if (!toPriceUSD && window.tableRateModule) {
|
||
toPriceUSD = tableRateModule.getFallbackValue(toSymbol);
|
||
}
|
||
|
||
if (fromPriceUSD === null || toPriceUSD === null ||
|
||
fromPriceUSD === undefined || toPriceUSD === undefined ||
|
||
isNaN(fromPriceUSD) || isNaN(toPriceUSD)) {
|
||
return `<p class="font-bold mb-1">Price Information Unavailable</p>
|
||
<p>Current market prices are temporarily unavailable.</p>
|
||
<p class="mt-2">You are ${isSentOffers ? 'selling' : 'buying'} ${fromAmount.toFixed(8)} ${coinFrom}
|
||
for ${toAmount.toFixed(8)} ${coinTo}.</p>
|
||
<p class="font-bold mt-2">Note:</p>
|
||
<p>Profit/loss calculations will be available when price data is restored.</p>`;
|
||
}
|
||
|
||
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 = (-((toValueUSD / fromValueUSD) - 1)) * 100;
|
||
}
|
||
|
||
const percentDiffDisplay = percentDiff === "0.00" ? "0.00" :
|
||
(percentDiff > 0 ? percentDiff : -percentDiff);
|
||
|
||
const profitLabel = percentDiff > 0 ? "Max Profit" : "Max Loss";
|
||
const actionLabel = (isSentOffers || isOwnOffer) ? "selling" : "buying";
|
||
const directionLabel = (isSentOffers || isOwnOffer) ? "receiving" : "paying";
|
||
|
||
return `
|
||
<p class="font-bold mb-1">Profit/Loss Calculation:</p>
|
||
<p>You are ${actionLabel} ${fromAmount.toFixed(8)} ${coinFrom} ($${fromValueUSD.toFixed(2)} USD) <br/> and ${directionLabel} ${toAmount.toFixed(8)} ${coinTo} ($${toValueUSD.toFixed(2)} USD).</p>
|
||
<p class="mt-1">Percentage difference: ${percentDiffDisplay.toFixed(2)}%</p>
|
||
<p>${profitLabel}: ${Math.abs(profitUSD).toFixed(2)} USD</p>
|
||
<p class="font-bold mt-2">Calculation:</p>
|
||
<p>Percentage = ${(isSentOffers || isOwnOffer) ?
|
||
"((To Amount in USD / From Amount in USD) - 1) * 100" :
|
||
"(-((To Amount in USD / From Amount in USD) - 1)) * 100"}</p>
|
||
<p>USD ${profitLabel} = To Amount in USD - From Amount in USD</p>
|
||
<p class="font-bold mt-1">Interpretation:</p>
|
||
${(isSentOffers || isOwnOffer) ? `
|
||
<p><span class="text-green-500">Green:</span> You're selling above market rate (profitable)</p>
|
||
<p><span class="text-red-500">Red:</span> You're selling below market rate (loss)</p>
|
||
` : `
|
||
<p><span class="text-green-500">Green:</span> You're buying below market rate (savings)</p>
|
||
<p><span class="text-red-500">Red:</span> You're buying above market rate (premium)</p>
|
||
`}
|
||
<p class="mt-1"><strong>Note:</strong> ${(isSentOffers || isOwnOffer) ?
|
||
"As a seller, a green percentage means <br/> you're selling for more than the current market rate." :
|
||
"As a buyer, a green percentage indicates </br> potential savings compared to current market rate."}</p>
|
||
<p class="mt-1"><strong>Market Rate:</strong> 1 ${coinFrom} = ${marketRate.toFixed(8)} ${coinTo}</p>
|
||
<p><strong>Offer Rate:</strong> 1 ${coinFrom} = ${offerRate.toFixed(8)} ${coinTo}</p>
|
||
`;
|
||
}
|
||
|
||
function createCombinedRateTooltip(offer, coinFrom, coinTo, treatAsSentOffer) {
|
||
const rate = parseFloat(offer.rate) || 0;
|
||
const inverseRate = rate ? (1 / rate) : 0;
|
||
|
||
const getPriceKey = (coin) => {
|
||
if (!coin) return null;
|
||
if (window.CoinUtils) {
|
||
return window.CoinUtils.normalizeCoinName(coin, latestPrices);
|
||
}
|
||
return coin.toLowerCase();
|
||
};
|
||
|
||
const fromSymbol = getPriceKey(coinFrom);
|
||
const toSymbol = getPriceKey(coinTo);
|
||
|
||
let fromPriceUSD = latestPrices && fromSymbol ? latestPrices[fromSymbol]?.usd : null;
|
||
let toPriceUSD = latestPrices && toSymbol ? latestPrices[toSymbol]?.usd : null;
|
||
|
||
if (!fromPriceUSD && window.tableRateModule) {
|
||
fromPriceUSD = tableRateModule.getFallbackValue(fromSymbol);
|
||
}
|
||
if (!toPriceUSD && window.tableRateModule) {
|
||
toPriceUSD = tableRateModule.getFallbackValue(toSymbol);
|
||
}
|
||
|
||
if (fromPriceUSD === null || toPriceUSD === null ||
|
||
fromPriceUSD === undefined || toPriceUSD === undefined ||
|
||
isNaN(fromPriceUSD) || isNaN(toPriceUSD)) {
|
||
return `
|
||
<p class="font-bold mb-1">Exchange Rate Information</p>
|
||
<p>Market price data is temporarily unavailable.</p>
|
||
<p class="font-bold mt-2">Current Offer Rates:</p>
|
||
<p>1 ${coinFrom} = ${rate.toFixed(8)} ${coinTo}</p>
|
||
<p>1 ${coinTo} = ${inverseRate.toFixed(8)} ${coinFrom}</p>
|
||
<p class="font-bold mt-2">Note:</p>
|
||
<p>Market comparison will be available when price data is restored.</p>
|
||
`;
|
||
}
|
||
|
||
const rateInUSD = rate * toPriceUSD;
|
||
const marketRate = fromPriceUSD / toPriceUSD;
|
||
const percentDiff = marketRate ? ((rate - marketRate) / marketRate) * 100 : 0;
|
||
const percentDiffDisplay = percentDiff === "0.00" ? "0.00" :
|
||
(percentDiff > 0 ? percentDiff : -percentDiff);
|
||
const aboveOrBelow = percentDiff > 0 ? "above" : percentDiff < 0 ? "below" : "at";
|
||
const action = treatAsSentOffer ? "selling" : "buying";
|
||
|
||
return `
|
||
<p class="font-bold mb-1">Exchange Rate Explanation:</p>
|
||
<p>This offer is ${action} ${coinFrom} for ${coinTo} <br/>at a rate that is ${percentDiffDisplay.toFixed(2)}% ${aboveOrBelow} market price.</p>
|
||
<p class="font-bold mt-1">Exchange Rates:</p>
|
||
<p>1 ${coinFrom} = ${rate.toFixed(8)} ${coinTo}</p>
|
||
<p>1 ${coinTo} = ${inverseRate.toFixed(8)} ${coinFrom}</p>
|
||
<p class="font-bold mt-2">USD Equivalent:</p>
|
||
<p>1 ${coinFrom} = $${rateInUSD.toFixed(2)} USD</p>
|
||
<p class="font-bold mt-2">Current market prices:</p>
|
||
<p>${coinFrom}: $${fromPriceUSD.toFixed(2)} USD</p>
|
||
<p>${coinTo}: $${toPriceUSD.toFixed(2)} USD</p>
|
||
<p class="mt-1">Market rate: 1 ${coinFrom} = ${marketRate.toFixed(8)} ${coinTo}</p>
|
||
`;
|
||
}
|
||
|
||
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 applyFilters(options = {}) {
|
||
if (window.filterTimeout) {
|
||
clearTimeout(window.filterTimeout);
|
||
window.filterTimeout = null;
|
||
}
|
||
|
||
try {
|
||
window.filterTimeout = CleanupManager.setTimeout(() => {
|
||
currentPage = 1;
|
||
jsonData = filterAndSortData();
|
||
updateOffersTable(options);
|
||
updateClearFiltersButton();
|
||
window.filterTimeout = null;
|
||
}, 250);
|
||
} catch (error) {
|
||
console.error('Error in filter timeout:', error);
|
||
window.filterTimeout = null;
|
||
}
|
||
}
|
||
|
||
function clearFilters() {
|
||
document.querySelectorAll('.coin-to-checkbox, .coin-from-checkbox').forEach(checkbox => {
|
||
checkbox.checked = checkbox.value === 'any';
|
||
});
|
||
|
||
const statusSelect = document.getElementById('status');
|
||
if (statusSelect) {
|
||
statusSelect.value = 'any';
|
||
}
|
||
|
||
const sentFromSelect = document.getElementById('sent_from');
|
||
if (sentFromSelect) {
|
||
sentFromSelect.value = 'any';
|
||
}
|
||
|
||
const autoAcceptTypeSelect = document.getElementById('auto_accept_type');
|
||
if (autoAcceptTypeSelect) {
|
||
autoAcceptTypeSelect.value = 'any';
|
||
}
|
||
|
||
updateFilterButtonText('coin_to');
|
||
updateFilterButtonText('coin_from');
|
||
updateCoinBadges('coin_to');
|
||
updateCoinBadges('coin_from');
|
||
hideAllDropdowns();
|
||
|
||
jsonData = [...originalJsonData];
|
||
currentPage = 1;
|
||
|
||
const storageKey = isSentOffers ? 'sentOffersTableSettings' : 'networkOffersTableSettings';
|
||
localStorage.removeItem(storageKey);
|
||
|
||
updateOffersTable();
|
||
updateClearFiltersButton();
|
||
}
|
||
|
||
function hasActiveFilters() {
|
||
const selectedCoinTo = getSelectedCoins('coin_to');
|
||
const selectedCoinFrom = getSelectedCoins('coin_from');
|
||
|
||
const hasCoinToFilter = selectedCoinTo.length > 0 && !(selectedCoinTo.length === 1 && selectedCoinTo[0] === 'any');
|
||
const hasCoinFromFilter = selectedCoinFrom.length > 0 && !(selectedCoinFrom.length === 1 && selectedCoinFrom[0] === 'any');
|
||
|
||
const statusSelect = document.getElementById('status');
|
||
const sentFromSelect = document.getElementById('sent_from');
|
||
const autoAcceptTypeSelect = document.getElementById('auto_accept_type');
|
||
|
||
const hasStatusFilter = statusSelect && statusSelect.value !== 'any';
|
||
const hasSentFromFilter = sentFromSelect && sentFromSelect.value !== 'any';
|
||
const hasAutoAcceptTypeFilter = autoAcceptTypeSelect && autoAcceptTypeSelect.value !== 'any';
|
||
|
||
return hasCoinToFilter || hasCoinFromFilter || hasStatusFilter || hasSentFromFilter || hasAutoAcceptTypeFilter;
|
||
}
|
||
|
||
function formatTimeLeft(timestamp) {
|
||
return window.config.utils.formatTimeLeft(timestamp);
|
||
}
|
||
|
||
function getDisplayName(coinName) {
|
||
if (window.CoinManager && window.CoinManager.getDisplayName) {
|
||
return window.CoinManager.getDisplayName(coinName) || coinName;
|
||
}
|
||
return coinName;
|
||
}
|
||
|
||
function getCoinSymbolLowercase(coin) {
|
||
if (typeof coin === 'string') {
|
||
if (coin.toLowerCase() === 'bitcoin cash') {
|
||
return 'bitcoin-cash';
|
||
}
|
||
return (window.config.coinMappings.nameToSymbol[coin] || coin).toLowerCase();
|
||
} else if (coin && typeof coin === 'object' && coin.symbol) {
|
||
return coin.symbol.toLowerCase();
|
||
} else {
|
||
return 'unknown';
|
||
}
|
||
}
|
||
|
||
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) {
|
||
if (isSentOffers) {
|
||
return false;
|
||
}
|
||
const currentTime = Math.floor(Date.now() / 1000);
|
||
const isExpired = offer.expire_at <= currentTime;
|
||
return isExpired;
|
||
}
|
||
|
||
function formatTime(timestamp, addAgoSuffix = false) {
|
||
return window.config.utils.formatTime(timestamp, addAgoSuffix);
|
||
}
|
||
|
||
function escapeHtml(unsafe) {
|
||
return window.config.utils.escapeHtml(unsafe);
|
||
}
|
||
|
||
function getPriceKey(coin) {
|
||
if (window.CoinManager) {
|
||
return window.CoinManager.getPriceKey(coin);
|
||
}
|
||
if (window.CoinUtils) {
|
||
return window.CoinUtils.normalizeCoinName(coin);
|
||
}
|
||
return coin ? coin.toLowerCase() : null;
|
||
}
|
||
|
||
function getCoinSymbol(fullName) {
|
||
if (window.CoinManager) {
|
||
return window.CoinManager.getSymbol(fullName) || fullName;
|
||
}
|
||
if (window.CoinUtils) {
|
||
return window.CoinUtils.getCoinSymbol(fullName);
|
||
}
|
||
return fullName;
|
||
}
|
||
|
||
function initializeTableEvents() {
|
||
const filterForm = document.getElementById('filterForm');
|
||
if (filterForm) {
|
||
CleanupManager.addListener(filterForm, 'submit', (e) => {
|
||
e.preventDefault();
|
||
applyFilters();
|
||
});
|
||
|
||
CleanupManager.addListener(filterForm, 'change', () => {
|
||
applyFilters();
|
||
updateClearFiltersButton();
|
||
});
|
||
}
|
||
|
||
const coinToButton = document.getElementById('coin_to_button');
|
||
const coinFromButton = document.getElementById('coin_from_button');
|
||
const coinToDropdown = document.getElementById('coin_to_dropdown');
|
||
const coinFromDropdown = document.getElementById('coin_from_dropdown');
|
||
const statusSelect = document.getElementById('status');
|
||
const sentFromSelect = document.getElementById('sent_from');
|
||
|
||
if (coinToButton && coinToDropdown) {
|
||
CleanupManager.addListener(coinToButton, 'click', (e) => {
|
||
e.stopPropagation();
|
||
toggleDropdown('coin_to');
|
||
});
|
||
|
||
const coinToCheckboxes = coinToDropdown.querySelectorAll('.coin-to-checkbox');
|
||
coinToCheckboxes.forEach(checkbox => {
|
||
CleanupManager.addListener(checkbox, 'change', (e) => {
|
||
handleCoinCheckboxChange('coin_to', e.target);
|
||
});
|
||
});
|
||
}
|
||
|
||
if (coinFromButton && coinFromDropdown) {
|
||
CleanupManager.addListener(coinFromButton, 'click', (e) => {
|
||
e.stopPropagation();
|
||
toggleDropdown('coin_from');
|
||
});
|
||
|
||
const coinFromCheckboxes = coinFromDropdown.querySelectorAll('.coin-from-checkbox');
|
||
coinFromCheckboxes.forEach(checkbox => {
|
||
CleanupManager.addListener(checkbox, 'change', (e) => {
|
||
handleCoinCheckboxChange('coin_from', e.target);
|
||
});
|
||
});
|
||
}
|
||
|
||
CleanupManager.addListener(window, 'resize', () => {
|
||
hideAllDropdowns();
|
||
});
|
||
|
||
CleanupManager.addListener(window, 'scroll', () => {
|
||
hideAllDropdowns();
|
||
});
|
||
|
||
if (statusSelect) {
|
||
CleanupManager.addListener(statusSelect, 'change', () => {
|
||
applyFilters();
|
||
});
|
||
}
|
||
|
||
if (sentFromSelect) {
|
||
CleanupManager.addListener(sentFromSelect, 'change', () => {
|
||
applyFilters();
|
||
});
|
||
}
|
||
|
||
const autoAcceptTypeSelect = document.getElementById('auto_accept_type');
|
||
if (autoAcceptTypeSelect) {
|
||
CleanupManager.addListener(autoAcceptTypeSelect, 'change', () => {
|
||
applyFilters();
|
||
});
|
||
}
|
||
|
||
const clearFiltersBtn = document.getElementById('clearFilters');
|
||
if (clearFiltersBtn) {
|
||
CleanupManager.addListener(clearFiltersBtn, 'click', () => {
|
||
clearFilters();
|
||
});
|
||
}
|
||
|
||
const refreshButton = document.getElementById('refreshOffers');
|
||
if (refreshButton) {
|
||
let lastRefreshTime = 0;
|
||
const REFRESH_COOLDOWN = 6000;
|
||
let countdownInterval;
|
||
|
||
CleanupManager.addListener(refreshButton, 'click', async () => {
|
||
const now = Date.now();
|
||
if (now - lastRefreshTime < REFRESH_COOLDOWN) {
|
||
console.log('Refresh rate limited. Please wait before refreshing again.');
|
||
const startTime = now;
|
||
const refreshText = document.getElementById('refreshText');
|
||
|
||
refreshButton.classList.remove('bg-blue-600', 'hover:bg-green-600', 'border-blue-500', 'hover:border-green-600');
|
||
refreshButton.classList.add('bg-red-600', 'border-red-500', 'cursor-not-allowed');
|
||
|
||
if (window.countdownInterval) clearInterval(window.countdownInterval);
|
||
|
||
window.countdownInterval = CleanupManager.setInterval(() => {
|
||
const currentTime = Date.now();
|
||
const elapsedTime = currentTime - startTime;
|
||
const remainingTime = Math.ceil((REFRESH_COOLDOWN - elapsedTime) / 1000);
|
||
|
||
if (remainingTime <= 0) {
|
||
clearInterval(window.countdownInterval);
|
||
window.countdownInterval = null;
|
||
refreshText.textContent = 'Refresh';
|
||
|
||
refreshButton.classList.remove('bg-red-600', 'border-red-500', 'cursor-not-allowed');
|
||
refreshButton.classList.add('bg-blue-600', 'hover:bg-green-600', 'border-blue-500', 'hover:border-green-600');
|
||
} else {
|
||
refreshText.textContent = `Refresh (${remainingTime}s)`;
|
||
}
|
||
}, 100);
|
||
return;
|
||
}
|
||
|
||
lastRefreshTime = now;
|
||
const refreshIcon = document.getElementById('refreshIcon');
|
||
const refreshText = document.getElementById('refreshText');
|
||
refreshButton.disabled = true;
|
||
refreshIcon.classList.add('animate-spin');
|
||
refreshText.textContent = 'Refreshing...';
|
||
refreshButton.classList.add('opacity-75', 'cursor-wait');
|
||
|
||
try {
|
||
const cachedPrices = CacheManager.get('prices_coingecko');
|
||
const previousPrices = cachedPrices ? cachedPrices.value : null;
|
||
CacheManager.clear();
|
||
window.isManualRefresh = true;
|
||
const endpoint = isSentOffers ? '/json/sentoffers' : '/json/offers';
|
||
const response = await fetch(endpoint);
|
||
if (!response.ok) {
|
||
throw new Error(`HTTP error! status: ${response.status}`);
|
||
}
|
||
const newData = await response.json();
|
||
const processedNewData = Array.isArray(newData) ? newData : Object.values(newData);
|
||
jsonData = formatInitialData(processedNewData);
|
||
originalJsonData = [...jsonData];
|
||
const priceData = await fetchLatestPrices();
|
||
if (!priceData && previousPrices) {
|
||
console.log('Using previous price data after failed refresh');
|
||
latestPrices = previousPrices;
|
||
applyFilters({ incremental: false });
|
||
} else if (priceData) {
|
||
latestPrices = priceData;
|
||
applyFilters({ incremental: false });
|
||
} else {
|
||
throw new Error('Unable to fetch price data');
|
||
}
|
||
updatePaginationInfo();
|
||
lastRefreshTime = now;
|
||
updateLastRefreshTime();
|
||
|
||
} catch (error) {
|
||
console.error('Error during manual refresh:', error);
|
||
NetworkManager.handleNetworkError(error);
|
||
ui.displayErrorMessage('Unable to refresh data. Previous data will be preserved.');
|
||
|
||
const cachedData = CacheManager.get('prices_coingecko');
|
||
if (cachedData?.value) {
|
||
latestPrices = cachedData.value;
|
||
applyFilters();
|
||
}
|
||
} finally {
|
||
window.isManualRefresh = false;
|
||
refreshButton.disabled = false;
|
||
refreshIcon.classList.remove('animate-spin');
|
||
refreshText.textContent = 'Refresh';
|
||
refreshButton.classList.remove('opacity-75', 'cursor-wait');
|
||
|
||
refreshButton.classList.remove('bg-red-600', 'border-red-500', 'cursor-not-allowed');
|
||
refreshButton.classList.add('bg-blue-600', 'hover:bg-green-600', 'border-blue-500', 'hover:border-green-600');
|
||
|
||
if (countdownInterval) {
|
||
clearInterval(countdownInterval);
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
const prevPageButton = document.getElementById('prevPage');
|
||
const nextPageButton = document.getElementById('nextPage');
|
||
|
||
if (prevPageButton) {
|
||
CleanupManager.addListener(prevPageButton, 'click', async () => {
|
||
if (currentPage > 1 && !isPaginationInProgress) {
|
||
try {
|
||
isPaginationInProgress = true;
|
||
const oldPage = currentPage;
|
||
currentPage--;
|
||
await updateOffersTable({ fromPaginationClick: true });
|
||
updatePaginationInfo();
|
||
} finally {
|
||
CleanupManager.setTimeout(() => {
|
||
isPaginationInProgress = false;
|
||
}, 100);
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
if (nextPageButton) {
|
||
CleanupManager.addListener(nextPageButton, 'click', async () => {
|
||
const validOffers = getValidOffers();
|
||
const totalPages = Math.ceil(validOffers.length / itemsPerPage);
|
||
if (currentPage < totalPages && !isPaginationInProgress) {
|
||
try {
|
||
isPaginationInProgress = true;
|
||
const oldPage = currentPage;
|
||
currentPage++;
|
||
await updateOffersTable({ fromPaginationClick: true });
|
||
updatePaginationInfo();
|
||
} finally {
|
||
CleanupManager.setTimeout(() => {
|
||
isPaginationInProgress = false;
|
||
}, 100);
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
document.querySelectorAll('th[data-sortable="true"]').forEach(header => {
|
||
CleanupManager.addListener(header, 'click', () => {
|
||
const columnIndex = parseInt(header.getAttribute('data-column-index'));
|
||
handleTableSort(columnIndex, header);
|
||
});
|
||
});
|
||
}
|
||
|
||
function handleTableSort(columnIndex, header) {
|
||
if (currentSortColumn === columnIndex) {
|
||
if (currentSortDirection === null) {
|
||
currentSortDirection = 'desc';
|
||
} else if (currentSortDirection === 'desc') {
|
||
currentSortDirection = 'asc';
|
||
} else if (currentSortDirection === 'asc') {
|
||
currentSortColumn = null;
|
||
currentSortDirection = null;
|
||
}
|
||
} else {
|
||
currentSortColumn = columnIndex;
|
||
currentSortDirection = 'desc';
|
||
}
|
||
|
||
document.querySelectorAll('th[data-sortable="true"]').forEach(th => {
|
||
const columnSpan = th.querySelector('span:not(.sort-icon)');
|
||
const icon = th.querySelector('.sort-icon');
|
||
const thisColumnIndex = parseInt(th.getAttribute('data-column-index'));
|
||
|
||
if (thisColumnIndex === columnIndex) {
|
||
if (columnSpan) {
|
||
columnSpan.classList.remove('text-gray-600', 'dark:text-gray-300');
|
||
columnSpan.classList.add('text-blue-500', 'dark:text-blue-500');
|
||
}
|
||
if (icon) {
|
||
icon.classList.remove('text-gray-600', 'dark:text-gray-400');
|
||
icon.classList.add('text-blue-500', 'dark:text-blue-500');
|
||
|
||
if (currentSortDirection === 'desc') {
|
||
icon.textContent = '↓';
|
||
} else if (currentSortDirection === 'asc') {
|
||
icon.textContent = '↑';
|
||
} else {
|
||
icon.textContent = '↓';
|
||
columnSpan.classList.remove('text-blue-500', 'dark:text-blue-500');
|
||
columnSpan.classList.add('text-gray-600', 'dark:text-gray-300');
|
||
icon.classList.remove('text-blue-500', 'dark:text-blue-500');
|
||
icon.classList.add('text-gray-600', 'dark:text-gray-400');
|
||
}
|
||
}
|
||
} else {
|
||
if (columnSpan) {
|
||
columnSpan.classList.remove('text-blue-500', 'dark:text-blue-500');
|
||
columnSpan.classList.add('text-gray-600', 'dark:text-gray-300');
|
||
}
|
||
if (icon) {
|
||
icon.classList.remove('text-blue-500', 'dark:text-blue-500');
|
||
icon.classList.add('text-gray-600', 'dark:text-gray-400');
|
||
icon.textContent = '↓';
|
||
}
|
||
}
|
||
});
|
||
|
||
saveFilterSettings();
|
||
|
||
if (window.sortTimeout) {
|
||
clearTimeout(window.sortTimeout);
|
||
}
|
||
|
||
window.sortTimeout = CleanupManager.setTimeout(() => {
|
||
applyFilters();
|
||
}, 100);
|
||
}
|
||
|
||
function startAutoRefresh() {
|
||
const REFRESH_INTERVAL = 2 * 60 * 1000; // 2 minutes
|
||
|
||
if (autoRefreshInterval) {
|
||
clearInterval(autoRefreshInterval);
|
||
}
|
||
|
||
autoRefreshInterval = CleanupManager.setInterval(async () => {
|
||
try {
|
||
|
||
const response = await fetch(isSentOffers ? '/json/sentoffers' : '/json/offers');
|
||
if (response.ok) {
|
||
|
||
}
|
||
} catch (error) {
|
||
console.error('[Auto-refresh] Error during background refresh:', error);
|
||
}
|
||
}, REFRESH_INTERVAL);
|
||
}
|
||
|
||
function stopAutoRefresh() {
|
||
if (autoRefreshInterval) {
|
||
clearInterval(autoRefreshInterval);
|
||
autoRefreshInterval = null;
|
||
}
|
||
}
|
||
|
||
async function initializeTableAndData() {
|
||
loadSavedSettings();
|
||
updateClearFiltersButton();
|
||
initializeTableEvents();
|
||
initializeTooltips();
|
||
|
||
updateFilterButtonText('coin_to');
|
||
updateFilterButtonText('coin_from');
|
||
updateCoinBadges('coin_to');
|
||
updateCoinBadges('coin_from');
|
||
|
||
try {
|
||
await fetchOffers();
|
||
applyFilters();
|
||
} catch (error) {
|
||
console.error('Error loading initial data:', error);
|
||
NetworkManager.handleNetworkError(error);
|
||
ui.displayErrorMessage('Error loading data. Retrying in background...');
|
||
}
|
||
}
|
||
|
||
function loadSavedSettings() {
|
||
const storageKey = isSentOffers ? 'sentOffersTableSettings' : 'networkOffersTableSettings';
|
||
const saved = localStorage.getItem(storageKey);
|
||
|
||
if (saved) {
|
||
const settings = JSON.parse(saved);
|
||
|
||
['coin_to', 'coin_from'].forEach(filterType => {
|
||
const savedCoins = settings[filterType];
|
||
if (savedCoins && Array.isArray(savedCoins)) {
|
||
|
||
document.querySelectorAll(`.${filterType}-checkbox`).forEach(cb => cb.checked = false);
|
||
|
||
savedCoins.forEach(coinValue => {
|
||
const checkbox = document.querySelector(`.${filterType}-checkbox[value="${coinValue}"]`);
|
||
if (checkbox) checkbox.checked = true;
|
||
});
|
||
|
||
updateFilterButtonText(filterType);
|
||
updateCoinBadges(filterType);
|
||
}
|
||
});
|
||
|
||
['status', 'sent_from', 'auto_accept_type'].forEach(id => {
|
||
const element = document.getElementById(id);
|
||
if (element && settings[id]) element.value = settings[id];
|
||
});
|
||
|
||
if (settings.sortColumn !== undefined) {
|
||
currentSortColumn = settings.sortColumn;
|
||
currentSortDirection = settings.sortDirection;
|
||
updateSortIndicators();
|
||
}
|
||
}
|
||
}
|
||
|
||
function updateSortIndicators() {
|
||
document.querySelectorAll('.sort-icon').forEach(icon => {
|
||
icon.classList.remove('text-blue-500');
|
||
icon.textContent = '↓';
|
||
});
|
||
|
||
const sortIcon = document.getElementById(`sort-icon-${currentSortColumn}`);
|
||
if (sortIcon) {
|
||
sortIcon.textContent = currentSortDirection === 'asc' ? '↑' : '↓';
|
||
sortIcon.classList.add('text-blue-500');
|
||
}
|
||
}
|
||
|
||
document.addEventListener('DOMContentLoaded', async function() {
|
||
try {
|
||
if (typeof window.PriceManager === 'undefined') {
|
||
console.error('PriceManager module not loaded');
|
||
ui.displayErrorMessage('Price data unavailable. Some features may not work correctly.');
|
||
}
|
||
|
||
if (initializeTableRateModule()) {
|
||
tableRateModule.init();
|
||
}
|
||
|
||
document.addEventListener('memoryOptimized', (e) => {
|
||
if (jsonData && jsonData.length > e.detail.maxDataSize) {
|
||
console.log(`Trimming offers data from ${jsonData.length} to ${e.detail.maxDataSize} items`);
|
||
jsonData = jsonData.slice(0, e.detail.maxDataSize);
|
||
}
|
||
|
||
if (originalJsonData && originalJsonData.length > e.detail.maxDataSize) {
|
||
console.log(`Trimming original offers data from ${originalJsonData.length} to ${e.detail.maxDataSize} items`);
|
||
originalJsonData = originalJsonData.slice(0, e.detail.maxDataSize);
|
||
}
|
||
});
|
||
|
||
await initializeTableAndData();
|
||
|
||
if (window.PriceManager) {
|
||
window.PriceManager.addEventListener('priceUpdate', function(prices) {
|
||
latestPrices = prices;
|
||
updateProfitLossDisplays();
|
||
});
|
||
}
|
||
|
||
if (window.WebSocketManager) {
|
||
WebSocketManager.addMessageHandler('message', async (data) => {
|
||
if (data.event === 'new_offer' || data.event === 'offer_revoked') {
|
||
try {
|
||
|
||
const fetchWithRetry = async (url, maxRetries = 3) => {
|
||
for (let i = 0; i < maxRetries; i++) {
|
||
try {
|
||
const response = await fetch(url);
|
||
if (!response.ok) {
|
||
throw new Error(`HTTP error! status: ${response.status}`);
|
||
}
|
||
return response;
|
||
} catch (error) {
|
||
if (i === maxRetries - 1) throw error;
|
||
|
||
await new Promise(resolve => CleanupManager.setTimeout(resolve, 100 * Math.pow(2, i)));
|
||
}
|
||
}
|
||
};
|
||
|
||
const offersResponse = await fetchWithRetry(isSentOffers ? '/json/sentoffers' : '/json/offers');
|
||
const newData = await offersResponse.json();
|
||
const processedNewData = Array.isArray(newData) ? newData : Object.values(newData);
|
||
const newFormattedData = formatInitialData(processedNewData);
|
||
|
||
const oldOfferIds = originalJsonData.map(o => o.offer_id).sort().join(',');
|
||
const newOfferIds = newFormattedData.map(o => o.offer_id).sort().join(',');
|
||
const dataChanged = oldOfferIds !== newOfferIds;
|
||
|
||
if (!dataChanged) {
|
||
return;
|
||
}
|
||
|
||
jsonData = newFormattedData;
|
||
originalJsonData = [...jsonData];
|
||
|
||
const previousPrices = latestPrices;
|
||
let priceData;
|
||
if (window.PriceManager) {
|
||
priceData = await window.PriceManager.getPrices(false);
|
||
} else {
|
||
priceData = await fetchLatestPrices();
|
||
}
|
||
|
||
if (priceData) {
|
||
latestPrices = priceData;
|
||
CacheManager.set('prices_coingecko', priceData, 'prices');
|
||
} else if (previousPrices) {
|
||
latestPrices = previousPrices;
|
||
}
|
||
|
||
applyFilters({ incremental: true, skipSkeleton: true });
|
||
updateProfitLossDisplays();
|
||
|
||
document.querySelectorAll('.usd-value').forEach(usdValue => {
|
||
const coinName = usdValue.getAttribute('data-coin');
|
||
if (coinName) {
|
||
const priceKey = getPriceKey(coinName);
|
||
const price = latestPrices[priceKey]?.usd;
|
||
if (price !== undefined && price !== null) {
|
||
const amount = parseFloat(usdValue.getAttribute('data-amount') || '0');
|
||
if (!isNaN(amount) && amount > 0) {
|
||
const calculatedUSD = amount * price;
|
||
const formattedUSD = calculatedUSD < 0.01
|
||
? calculatedUSD.toFixed(8) + ' USD'
|
||
: calculatedUSD.toFixed(2) + ' USD';
|
||
|
||
if (usdValue.textContent !== formattedUSD) {
|
||
usdValue.textContent = formattedUSD;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
});
|
||
|
||
updatePaginationInfo();
|
||
|
||
} catch (error) {
|
||
console.error('Error during WebSocket-triggered refresh:', error);
|
||
NetworkManager.handleNetworkError(error);
|
||
}
|
||
}
|
||
});
|
||
|
||
if (!WebSocketManager.isConnected()) {
|
||
WebSocketManager.connect();
|
||
}
|
||
}
|
||
|
||
document.querySelectorAll('th[data-sortable="true"]').forEach(header => {
|
||
CleanupManager.addListener(header, 'click', () => {
|
||
const columnIndex = parseInt(header.getAttribute('data-column-index'), 10);
|
||
if (columnIndex !== null && !isNaN(columnIndex)) {
|
||
handleTableSort(columnIndex, header);
|
||
}
|
||
});
|
||
});
|
||
|
||
if (window.NetworkManager) {
|
||
NetworkManager.addHandler('online', () => {
|
||
updateConnectionStatus('connected');
|
||
if (document.getElementById('error-overlay').classList.contains('hidden')) {
|
||
fetchOffers();
|
||
}
|
||
});
|
||
|
||
NetworkManager.addHandler('offline', () => {
|
||
updateConnectionStatus('disconnected');
|
||
});
|
||
}
|
||
|
||
startAutoRefresh();
|
||
|
||
const filterForm = document.getElementById('filterForm');
|
||
if (filterForm) {
|
||
filterForm.querySelectorAll('select').forEach(select => {
|
||
CleanupManager.addListener(select, 'change', () => {
|
||
applyFilters();
|
||
updateClearFiltersButton();
|
||
});
|
||
});
|
||
}
|
||
|
||
const clearFiltersBtn = document.getElementById('clearFilters');
|
||
if (clearFiltersBtn) {
|
||
CleanupManager.addListener(clearFiltersBtn, 'click', clearFilters);
|
||
}
|
||
|
||
CleanupManager.addListener(document, 'click', (e) => {
|
||
const coinToDropdown = document.getElementById('coin_to_dropdown');
|
||
const coinFromDropdown = document.getElementById('coin_from_dropdown');
|
||
|
||
if (coinToDropdown && coinToDropdown.style.display !== 'none') {
|
||
if (!e.target.closest('#coin_to_button') && !e.target.closest('#coin_to_dropdown')) {
|
||
hideDropdown('coin_to');
|
||
}
|
||
}
|
||
|
||
if (coinFromDropdown && coinFromDropdown.style.display !== 'none') {
|
||
if (!e.target.closest('#coin_from_button') && !e.target.closest('#coin_from_dropdown')) {
|
||
hideDropdown('coin_from');
|
||
}
|
||
}
|
||
});
|
||
|
||
const rowTimeInterval = CleanupManager.setInterval(updateRowTimes, 30000);
|
||
CleanupManager.registerResource('rowTimeInterval', rowTimeInterval, () => {
|
||
clearInterval(rowTimeInterval);
|
||
});
|
||
|
||
} catch (error) {
|
||
console.error('Error during initialization:', error);
|
||
ui.displayErrorMessage('Error initializing application. Please refresh the page.');
|
||
}
|
||
});
|
||
|
||
function cleanup() {
|
||
console.log('Starting offers.js cleanup process');
|
||
|
||
try {
|
||
if (window.filterTimeout) {
|
||
clearTimeout(window.filterTimeout);
|
||
window.filterTimeout = null;
|
||
}
|
||
|
||
if (window.sortTimeout) {
|
||
clearTimeout(window.sortTimeout);
|
||
window.sortTimeout = null;
|
||
}
|
||
|
||
if (window.refreshInterval) {
|
||
clearInterval(window.refreshInterval);
|
||
window.refreshInterval = null;
|
||
}
|
||
|
||
if (window.countdownInterval) {
|
||
clearInterval(window.countdownInterval);
|
||
window.countdownInterval = null;
|
||
}
|
||
|
||
stopAutoRefresh();
|
||
|
||
if (window._cleanupIntervals && Array.isArray(window._cleanupIntervals)) {
|
||
window._cleanupIntervals.forEach(interval => {
|
||
clearInterval(interval);
|
||
});
|
||
window._cleanupIntervals = [];
|
||
}
|
||
|
||
if (window.PriceManager) {
|
||
if (typeof window.PriceManager.removeEventListener === 'function') {
|
||
window.PriceManager.removeEventListener('priceUpdate');
|
||
}
|
||
}
|
||
|
||
const offersBody = document.getElementById('offers-body');
|
||
if (offersBody) {
|
||
const existingRows = Array.from(offersBody.querySelectorAll('tr'));
|
||
existingRows.forEach(row => {
|
||
const tooltipTriggers = row.querySelectorAll('[data-tooltip-trigger-id]');
|
||
tooltipTriggers.forEach(trigger => {
|
||
if (window.TooltipManager) {
|
||
window.TooltipManager.destroy(trigger);
|
||
}
|
||
});
|
||
|
||
if (window.CleanupManager) {
|
||
window.CleanupManager.removeListenersByElement(row);
|
||
}
|
||
});
|
||
offersBody.innerHTML = '';
|
||
}
|
||
|
||
jsonData = null;
|
||
originalJsonData = null;
|
||
latestPrices = null;
|
||
|
||
if (window.TooltipManager) {
|
||
window.TooltipManager.cleanup();
|
||
}
|
||
|
||
if (window.MemoryManager) {
|
||
if (window.MemoryManager.forceCleanup) {
|
||
window.MemoryManager.forceCleanup();
|
||
}
|
||
}
|
||
|
||
} catch (error) {
|
||
console.error('Error during cleanup:', error);
|
||
}
|
||
}
|
||
window.cleanup = cleanup;
|