mirror of
https://github.com/basicswap/basicswap.git
synced 2025-11-05 18:38:09 +01:00
1942 lines
66 KiB
JavaScript
1942 lines
66 KiB
JavaScript
const PAGE_SIZE = 50;
|
|
const state = {
|
|
currentPage: {
|
|
sent: 1,
|
|
received: 1
|
|
},
|
|
isLoading: false,
|
|
isRefreshing: false,
|
|
currentTab: 'sent',
|
|
wsConnected: false,
|
|
refreshPromise: null,
|
|
data: {
|
|
sent: [],
|
|
received: []
|
|
},
|
|
filters: {
|
|
state: -1,
|
|
sort_by: 'created_at',
|
|
sort_dir: 'desc',
|
|
with_expired: true,
|
|
searchQuery: '',
|
|
coin_from: 'any',
|
|
coin_to: 'any'
|
|
}
|
|
};
|
|
|
|
const STATE_MAP = {
|
|
1: ['Sent'],
|
|
2: ['Receiving'],
|
|
3: ['Received'],
|
|
4: ['Receiving accept'],
|
|
5: ['Accepted'],
|
|
6: ['Initiated'],
|
|
7: ['Participating'],
|
|
8: ['Completed'],
|
|
9: ['Script coin locked'],
|
|
10: ['Script coin spend tx valid'],
|
|
11: ['Scriptless coin locked'],
|
|
12: ['Script coin lock released'],
|
|
13: ['Script tx redeemed'],
|
|
14: ['Script pre-refund tx in chain'],
|
|
15: ['Scriptless tx redeemed'],
|
|
16: ['Scriptless tx recovered'],
|
|
17: ['Failed, refunded'],
|
|
18: ['Failed, swiped'],
|
|
19: ['Failed'],
|
|
20: ['Delaying'],
|
|
21: ['Timed-out', 'Expired'],
|
|
22: ['Abandoned'],
|
|
23: ['Error'],
|
|
24: ['Stalled (debug)'],
|
|
25: ['Rejected'],
|
|
26: ['Unknown bid state'],
|
|
27: ['Exchanged script lock tx sigs msg'],
|
|
28: ['Exchanged script lock spend tx msg'],
|
|
29: ['Request sent'],
|
|
30: ['Request accepted'],
|
|
31: ['Expired'],
|
|
32: ['Auto accept delay'],
|
|
33: ['Auto accept failed']
|
|
};
|
|
|
|
const elements = {
|
|
sentBidsBody: document.querySelector('#sent tbody'),
|
|
receivedBidsBody: document.querySelector('#received tbody'),
|
|
filterForm: document.querySelector('form'),
|
|
stateSelect: document.querySelector('select[name="state"]'),
|
|
sortBySelect: document.querySelector('select[name="sort_by"]'),
|
|
sortDirSelect: document.querySelector('select[name="sort_dir"]'),
|
|
withExpiredSelect: document.querySelector('select[name="with_expired"]'),
|
|
tabButtons: document.querySelectorAll('#myTab button'),
|
|
sentContent: document.getElementById('sent'),
|
|
receivedContent: document.getElementById('received'),
|
|
|
|
sentPaginationControls: document.getElementById('pagination-controls-sent'),
|
|
receivedPaginationControls: document.getElementById('pagination-controls-received'),
|
|
prevPageSent: document.getElementById('prevPageSent'),
|
|
nextPageSent: document.getElementById('nextPageSent'),
|
|
prevPageReceived: document.getElementById('prevPageReceived'),
|
|
nextPageReceived: document.getElementById('nextPageReceived'),
|
|
currentPageSent: document.getElementById('currentPageSent'),
|
|
currentPageReceived: document.getElementById('currentPageReceived'),
|
|
sentBidsCount: document.getElementById('sentBidsCount'),
|
|
receivedBidsCount: document.getElementById('receivedBidsCount'),
|
|
|
|
statusDotSent: document.getElementById('status-dot-sent'),
|
|
statusTextSent: document.getElementById('status-text-sent'),
|
|
statusDotReceived: document.getElementById('status-dot-received'),
|
|
statusTextReceived: document.getElementById('status-text-received'),
|
|
|
|
refreshSentBids: document.getElementById('refreshSentBids'),
|
|
refreshReceivedBids: document.getElementById('refreshReceivedBids')
|
|
};
|
|
|
|
const EventManager = {
|
|
listeners: new Map(),
|
|
|
|
add(element, type, handler, options = false) {
|
|
if (!element) return null;
|
|
|
|
if (!this.listeners.has(element)) {
|
|
this.listeners.set(element, new Map());
|
|
}
|
|
|
|
const elementListeners = this.listeners.get(element);
|
|
if (!elementListeners.has(type)) {
|
|
elementListeners.set(type, new Set());
|
|
}
|
|
|
|
const handlerInfo = { handler, options };
|
|
elementListeners.get(type).add(handlerInfo);
|
|
element.addEventListener(type, handler, options);
|
|
|
|
return handlerInfo;
|
|
},
|
|
|
|
remove(element, type, handler, options = false) {
|
|
if (!element) return;
|
|
|
|
const elementListeners = this.listeners.get(element);
|
|
if (!elementListeners) return;
|
|
|
|
const typeListeners = elementListeners.get(type);
|
|
if (!typeListeners) return;
|
|
|
|
typeListeners.forEach(info => {
|
|
if (info.handler === handler) {
|
|
element.removeEventListener(type, handler, options);
|
|
typeListeners.delete(info);
|
|
}
|
|
});
|
|
|
|
if (typeListeners.size === 0) {
|
|
elementListeners.delete(type);
|
|
}
|
|
if (elementListeners.size === 0) {
|
|
this.listeners.delete(element);
|
|
}
|
|
},
|
|
|
|
removeAll(element) {
|
|
if (!element) return;
|
|
|
|
const elementListeners = this.listeners.get(element);
|
|
if (!elementListeners) return;
|
|
|
|
elementListeners.forEach((typeListeners, type) => {
|
|
typeListeners.forEach(info => {
|
|
try {
|
|
element.removeEventListener(type, info.handler, info.options);
|
|
} catch (e) {
|
|
console.warn('Error removing event listener:', e);
|
|
}
|
|
});
|
|
});
|
|
|
|
this.listeners.delete(element);
|
|
},
|
|
|
|
clearAll() {
|
|
this.listeners.forEach((elementListeners, element) => {
|
|
this.removeAll(element);
|
|
});
|
|
this.listeners.clear();
|
|
}
|
|
};
|
|
|
|
function cleanup() {
|
|
//console.log('Starting comprehensive cleanup process for bids table');
|
|
|
|
try {
|
|
if (searchTimeout) {
|
|
clearTimeout(searchTimeout);
|
|
searchTimeout = null;
|
|
}
|
|
|
|
if (state.refreshPromise) {
|
|
state.isRefreshing = false;
|
|
}
|
|
|
|
if (window.WebSocketManager) {
|
|
WebSocketManager.disconnect();
|
|
}
|
|
|
|
cleanupTooltips();
|
|
forceTooltipDOMCleanup();
|
|
|
|
if (window.TooltipManager) {
|
|
window.TooltipManager.cleanup();
|
|
}
|
|
|
|
tooltipIdsToCleanup.clear();
|
|
|
|
const cleanupTableBody = (tableId) => {
|
|
const tbody = document.getElementById(tableId);
|
|
if (!tbody) return;
|
|
|
|
const rows = tbody.querySelectorAll('tr');
|
|
rows.forEach(row => {
|
|
if (window.CleanupManager) {
|
|
CleanupManager.removeListenersByElement(row);
|
|
} else {
|
|
EventManager.removeAll(row);
|
|
}
|
|
Array.from(row.attributes).forEach(attr => {
|
|
if (attr.name.startsWith('data-')) {
|
|
row.removeAttribute(attr.name);
|
|
}
|
|
});
|
|
});
|
|
while (tbody.firstChild) {
|
|
tbody.removeChild(tbody.firstChild);
|
|
}
|
|
};
|
|
|
|
cleanupTableBody('sent-tbody');
|
|
cleanupTableBody('received-tbody');
|
|
|
|
if (window.CleanupManager) {
|
|
CleanupManager.clearAll();
|
|
} else {
|
|
EventManager.clearAll();
|
|
}
|
|
|
|
const clearAllAnimationFrames = () => {
|
|
const rafList = window.requestAnimationFrameList;
|
|
if (Array.isArray(rafList)) {
|
|
rafList.forEach(id => {
|
|
cancelAnimationFrame(id);
|
|
});
|
|
window.requestAnimationFrameList = [];
|
|
}
|
|
};
|
|
clearAllAnimationFrames();
|
|
|
|
state.data = {
|
|
sent: [],
|
|
received: []
|
|
};
|
|
|
|
state.currentPage = {
|
|
sent: 1,
|
|
received: 1
|
|
};
|
|
|
|
state.isLoading = false;
|
|
state.isRefreshing = false;
|
|
state.wsConnected = false;
|
|
state.refreshPromise = null;
|
|
|
|
state.filters = {
|
|
state: -1,
|
|
sort_by: 'created_at',
|
|
sort_dir: 'desc',
|
|
with_expired: true,
|
|
searchQuery: '',
|
|
coin_from: 'any',
|
|
coin_to: 'any'
|
|
};
|
|
|
|
if (window.IdentityManager) {
|
|
IdentityManager.clearCache();
|
|
}
|
|
|
|
if (window.CacheManager) {
|
|
CacheManager.cleanup(true);
|
|
}
|
|
|
|
if (window.MemoryManager) {
|
|
MemoryManager.forceCleanup();
|
|
}
|
|
|
|
Object.keys(elements).forEach(key => {
|
|
elements[key] = null;
|
|
});
|
|
|
|
console.log('Comprehensive cleanup completed');
|
|
} catch (error) {
|
|
console.error('Error during cleanup process:', error);
|
|
|
|
try {
|
|
if (window.EventManager) EventManager.clearAll();
|
|
if (window.CleanupManager) CleanupManager.clearAll();
|
|
if (window.WebSocketManager) WebSocketManager.disconnect();
|
|
|
|
state.data = { sent: [], received: [] };
|
|
state.isLoading = false;
|
|
|
|
Object.keys(elements).forEach(key => {
|
|
elements[key] = null;
|
|
});
|
|
} catch (e) {
|
|
console.error('Failsafe cleanup also failed:', e);
|
|
}
|
|
}
|
|
}
|
|
|
|
window.cleanupBidsTable = cleanup;
|
|
|
|
CleanupManager.addListener(document, 'visibilitychange', () => {
|
|
if (document.hidden) {
|
|
//console.log('Page hidden - pausing WebSocket and optimizing memory');
|
|
|
|
if (WebSocketManager && typeof WebSocketManager.pause === 'function') {
|
|
WebSocketManager.pause();
|
|
} else if (WebSocketManager && typeof WebSocketManager.disconnect === 'function') {
|
|
WebSocketManager.disconnect();
|
|
}
|
|
|
|
if (window.TooltipManager && typeof window.TooltipManager.cleanup === 'function') {
|
|
window.TooltipManager.cleanup();
|
|
}
|
|
|
|
// Run memory optimization
|
|
if (window.MemoryManager) {
|
|
MemoryManager.forceCleanup();
|
|
}
|
|
} else {
|
|
|
|
if (WebSocketManager && typeof WebSocketManager.resume === 'function') {
|
|
WebSocketManager.resume();
|
|
} else if (WebSocketManager && typeof WebSocketManager.connect === 'function') {
|
|
WebSocketManager.connect();
|
|
}
|
|
|
|
const lastUpdateTime = state.lastRefresh || 0;
|
|
const now = Date.now();
|
|
const refreshInterval = 5 * 60 * 1000; // 5 minutes
|
|
|
|
if (now - lastUpdateTime > refreshInterval) {
|
|
setTimeout(() => {
|
|
updateBidsTable();
|
|
}, 500);
|
|
}
|
|
}
|
|
});
|
|
|
|
CleanupManager.addListener(window, 'beforeunload', () => {
|
|
cleanup();
|
|
});
|
|
|
|
function cleanupRow(row) {
|
|
if (!row) return;
|
|
|
|
const tooltipTriggers = row.querySelectorAll('[data-tooltip-target]');
|
|
tooltipTriggers.forEach(trigger => {
|
|
if (window.TooltipManager) {
|
|
window.TooltipManager.destroy(trigger);
|
|
}
|
|
});
|
|
|
|
if (window.CleanupManager) {
|
|
CleanupManager.removeListenersByElement(row);
|
|
} else {
|
|
EventManager.removeAll(row);
|
|
}
|
|
|
|
row.removeAttribute('data-offer-id');
|
|
row.removeAttribute('data-bid-id');
|
|
|
|
while (row.firstChild) {
|
|
const child = row.firstChild;
|
|
row.removeChild(child);
|
|
}
|
|
}
|
|
|
|
function optimizeMemoryUsage() {
|
|
const MAX_BIDS_IN_MEMORY = 500;
|
|
|
|
['sent', 'received'].forEach(type => {
|
|
if (state.data[type] && state.data[type].length > MAX_BIDS_IN_MEMORY) {
|
|
console.log(`Trimming ${type} bids data from ${state.data[type].length} to ${MAX_BIDS_IN_MEMORY}`);
|
|
state.data[type] = state.data[type].slice(0, MAX_BIDS_IN_MEMORY);
|
|
}
|
|
});
|
|
|
|
cleanupOffscreenTooltips();
|
|
|
|
if (window.IdentityManager && typeof IdentityManager.limitCacheSize === 'function') {
|
|
IdentityManager.limitCacheSize(100);
|
|
}
|
|
|
|
if (window.MemoryManager) {
|
|
MemoryManager.forceCleanup();
|
|
}
|
|
}
|
|
|
|
const safeParseInt = (value) => {
|
|
const parsed = parseInt(value);
|
|
return isNaN(parsed) ? 0 : parsed;
|
|
};
|
|
|
|
const formatAddress = (address, displayLength = 20) => {
|
|
if (!address) return '';
|
|
if (address.length <= displayLength) return address;
|
|
return `${address.slice(8, displayLength)}...`;
|
|
};
|
|
|
|
const formatAddressSMSG = (address, displayLength = 14) => {
|
|
if (!address) return '';
|
|
if (address.length <= displayLength) return address;
|
|
return `${address.slice(0, displayLength)}...`;
|
|
};
|
|
|
|
const formatTime = (timestamp) => {
|
|
if (!timestamp) return '';
|
|
const date = new Date(timestamp * 1000);
|
|
return date.toLocaleString('en-US', {
|
|
year: 'numeric',
|
|
month: 'short',
|
|
day: 'numeric',
|
|
hour: '2-digit',
|
|
minute: '2-digit'
|
|
});
|
|
};
|
|
|
|
const getTimeStrokeColor = (expireTime) => {
|
|
const now = Math.floor(Date.now() / 1000);
|
|
return expireTime > now ? '#10B981' : '#9CA3AF';
|
|
};
|
|
|
|
const getStatusClass = (status) => {
|
|
switch (status) {
|
|
case 'Completed':
|
|
return 'bg-green-300 text-black dark:bg-green-600 dark:text-white';
|
|
case 'Expired':
|
|
case 'Timed-out':
|
|
return 'bg-gray-200 text-black dark:bg-gray-400 dark:text-white';
|
|
case 'Error':
|
|
case 'Failed':
|
|
return 'bg-red-300 text-black dark:bg-red-600 dark:text-white';
|
|
case 'Failed, swiped':
|
|
case 'Failed, refunded':
|
|
return 'bg-gray-200 text-black dark:bg-gray-400 dark:text-red-500';
|
|
case 'InProgress':
|
|
case 'Script coin locked':
|
|
case 'Scriptless coin locked':
|
|
case 'Script coin lock released':
|
|
case 'SendingInitialTx':
|
|
case 'SendingPaymentTx':
|
|
return 'bg-blue-300 text-black dark:bg-blue-500 dark:text-white';
|
|
case 'Received':
|
|
case 'Exchanged script lock tx sigs msg':
|
|
case 'Exchanged script lock spend tx msg':
|
|
case 'Script tx redeemed':
|
|
case 'Scriptless tx redeemed':
|
|
case 'Scriptless tx recovered':
|
|
return 'bg-blue-300 text-black dark:bg-blue-500 dark:text-white';
|
|
case 'Accepted':
|
|
case 'Request accepted':
|
|
return 'bg-green-300 text-black dark:bg-green-600 dark:text-white';
|
|
case 'Delaying':
|
|
case 'Auto accept delay':
|
|
return 'bg-blue-300 text-black dark:bg-blue-500 dark:text-white';
|
|
case 'Abandoned':
|
|
case 'Rejected':
|
|
return 'bg-red-300 text-black dark:bg-red-600 dark:text-white';
|
|
default:
|
|
return 'bg-blue-300 text-black dark:bg-blue-500 dark:text-white';
|
|
}
|
|
};
|
|
|
|
function coinMatches(offerCoin, filterCoin) {
|
|
if (!offerCoin || !filterCoin || filterCoin === 'any') return true;
|
|
|
|
offerCoin = offerCoin.toLowerCase();
|
|
filterCoin = filterCoin.toLowerCase();
|
|
|
|
if (offerCoin === filterCoin) return true;
|
|
|
|
if ((offerCoin === 'firo' || offerCoin === 'zcoin') &&
|
|
(filterCoin === 'firo' || filterCoin === 'zcoin')) {
|
|
return true;
|
|
}
|
|
|
|
if ((offerCoin === 'bitcoincash' && filterCoin === 'bitcoin cash') ||
|
|
(offerCoin === 'bitcoin cash' && filterCoin === 'bitcoincash')) {
|
|
return true;
|
|
}
|
|
|
|
const particlVariants = ['particl', 'particl anon', 'particl blind'];
|
|
if (filterCoin === 'particl' && particlVariants.includes(offerCoin)) {
|
|
return true;
|
|
}
|
|
|
|
if (particlVariants.includes(filterCoin)) {
|
|
return offerCoin === filterCoin;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
function hasActiveFilters() {
|
|
const coinFromSelect = document.getElementById('coin_from');
|
|
const coinToSelect = document.getElementById('coin_to');
|
|
const withExpiredSelect = document.getElementById('with_expired');
|
|
const stateSelect = document.getElementById('state');
|
|
const hasNonDefaultState = stateSelect && stateSelect.value !== '-1';
|
|
const hasSearchQuery = state.filters.searchQuery.trim() !== '';
|
|
const hasNonDefaultCoinFrom = coinFromSelect && coinFromSelect.value !== 'any';
|
|
const hasNonDefaultCoinTo = coinToSelect && coinToSelect.value !== 'any';
|
|
const hasNonDefaultExpired = withExpiredSelect && withExpiredSelect.value !== 'true';
|
|
|
|
return hasNonDefaultState ||
|
|
hasSearchQuery ||
|
|
hasNonDefaultCoinFrom ||
|
|
hasNonDefaultCoinTo ||
|
|
hasNonDefaultExpired;
|
|
}
|
|
|
|
function filterAndSortData(bids) {
|
|
if (!Array.isArray(bids)) {
|
|
return [];
|
|
}
|
|
|
|
const expiredStates = ['Expired', 'Timed-out'];
|
|
|
|
return bids.filter(bid => {
|
|
if (state.filters.state !== -1) {
|
|
const allowedStates = STATE_MAP[state.filters.state] || [];
|
|
if (allowedStates.length > 0 && !allowedStates.includes(bid.bid_state)) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if (!state.filters.with_expired && expiredStates.includes(bid.bid_state)) {
|
|
return false;
|
|
}
|
|
|
|
if (state.filters.coin_from !== 'any') {
|
|
const coinFromSelect = document.getElementById('coin_from');
|
|
const selectedOption = coinFromSelect?.querySelector(`option[value="${state.filters.coin_from}"]`);
|
|
const coinName = selectedOption?.textContent.trim();
|
|
|
|
if (coinName) {
|
|
const coinToMatch = state.currentTab === 'sent' ? bid.coin_to : bid.coin_from;
|
|
if (!coinMatches(coinToMatch, coinName)) {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (state.filters.coin_to !== 'any') {
|
|
const coinToSelect = document.getElementById('coin_to');
|
|
const selectedOption = coinToSelect?.querySelector(`option[value="${state.filters.coin_to}"]`);
|
|
const coinName = selectedOption?.textContent.trim();
|
|
|
|
if (coinName) {
|
|
const coinToMatch = state.currentTab === 'sent' ? bid.coin_from : bid.coin_to;
|
|
if (!coinMatches(coinToMatch, coinName)) {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (state.filters.searchQuery) {
|
|
const searchStr = state.filters.searchQuery.toLowerCase();
|
|
const matchesBidId = bid.bid_id.toLowerCase().includes(searchStr);
|
|
const matchesIdentity = bid.addr_from?.toLowerCase().includes(searchStr);
|
|
|
|
let label = '';
|
|
try {
|
|
if (window.IdentityManager) {
|
|
|
|
let identity = null;
|
|
|
|
if (IdentityManager.cache && typeof IdentityManager.cache.get === 'function') {
|
|
identity = IdentityManager.cache.get(bid.addr_from);
|
|
}
|
|
|
|
if (identity && identity.label) {
|
|
label = identity.label;
|
|
} else if (identity && identity.data && identity.data.label) {
|
|
label = identity.data.label;
|
|
}
|
|
|
|
if (!label && bid.identity) {
|
|
label = bid.identity.label || '';
|
|
}
|
|
}
|
|
} catch (e) {
|
|
console.warn('Error accessing identity for search:', e);
|
|
}
|
|
|
|
const matchesLabel = label.toLowerCase().includes(searchStr);
|
|
|
|
let matchesDisplayedLabel = false;
|
|
if (!matchesLabel && document) {
|
|
try {
|
|
const tableId = state.currentTab === 'sent' ? 'sent' : 'received';
|
|
const cells = document.querySelectorAll(`#${tableId} a[href^="/identity/"]`);
|
|
|
|
for (const cell of cells) {
|
|
|
|
const href = cell.getAttribute('href');
|
|
const cellAddress = href ? href.split('/').pop() : '';
|
|
|
|
if (cellAddress === bid.addr_from) {
|
|
const cellText = cell.textContent.trim().toLowerCase();
|
|
if (cellText.includes(searchStr)) {
|
|
matchesDisplayedLabel = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
} catch (e) {
|
|
console.warn('Error checking displayed labels:', e);
|
|
}
|
|
}
|
|
|
|
if (!(matchesBidId || matchesIdentity || matchesLabel || matchesDisplayedLabel)) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}).sort((a, b) => {
|
|
if (state.filters.sort_by === 'created_at') {
|
|
const direction = state.filters.sort_dir === 'asc' ? 1 : -1;
|
|
return direction * (a.created_at - b.created_at);
|
|
}
|
|
return 0;
|
|
});
|
|
}
|
|
|
|
async function preloadIdentitiesForSearch(bids) {
|
|
if (!window.IdentityManager || typeof IdentityManager.getIdentityData !== 'function') {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const addresses = new Set();
|
|
bids.forEach(bid => {
|
|
if (bid.addr_from) {
|
|
addresses.add(bid.addr_from);
|
|
}
|
|
});
|
|
|
|
const BATCH_SIZE = 20;
|
|
const addressArray = Array.from(addresses);
|
|
|
|
for (let i = 0; i < addressArray.length; i += BATCH_SIZE) {
|
|
const batch = addressArray.slice(i, i + BATCH_SIZE);
|
|
await Promise.all(batch.map(addr => IdentityManager.getIdentityData(addr)));
|
|
|
|
if (i + BATCH_SIZE < addressArray.length) {
|
|
await new Promise(resolve => setTimeout(resolve, 10));
|
|
}
|
|
}
|
|
|
|
console.log(`Preloaded ${addressArray.length} identities for search`);
|
|
} catch (error) {
|
|
console.error('Error preloading identities:', error);
|
|
}
|
|
}
|
|
|
|
function updateCoinFilterImages() {
|
|
const coinToSelect = document.getElementById('coin_to');
|
|
const coinFromSelect = document.getElementById('coin_from');
|
|
const coinToButton = document.getElementById('coin_to_button');
|
|
const coinFromButton = document.getElementById('coin_from_button');
|
|
|
|
function updateButtonImage(select, button) {
|
|
if (!select || !button) return;
|
|
|
|
const selectedOption = select.options[select.selectedIndex];
|
|
const imagePath = selectedOption.getAttribute('data-image');
|
|
|
|
if (imagePath && select.value !== 'any') {
|
|
button.style.backgroundImage = `url(${imagePath})`;
|
|
button.style.backgroundSize = '25px';
|
|
button.style.backgroundRepeat = 'no-repeat';
|
|
button.style.backgroundPosition = 'center';
|
|
|
|
} else {
|
|
button.style.backgroundImage = 'none';
|
|
button.style.opacity = '1';
|
|
}
|
|
}
|
|
|
|
updateButtonImage(coinToSelect, coinToButton);
|
|
updateButtonImage(coinFromSelect, coinFromButton);
|
|
}
|
|
|
|
const updateLoadingState = (isLoading) => {
|
|
state.isLoading = isLoading;
|
|
|
|
['Sent', 'Received'].forEach(type => {
|
|
const refreshButton = elements[`refresh${type}Bids`];
|
|
const refreshText = refreshButton?.querySelector(`#refresh${type}Text`);
|
|
const refreshIcon = refreshButton?.querySelector('svg');
|
|
|
|
if (refreshButton) {
|
|
refreshButton.disabled = isLoading;
|
|
if (isLoading) {
|
|
refreshButton.classList.add('opacity-75', 'cursor-wait');
|
|
} else {
|
|
refreshButton.classList.remove('opacity-75', 'cursor-wait');
|
|
}
|
|
}
|
|
|
|
if (refreshIcon) {
|
|
if (isLoading) {
|
|
refreshIcon.classList.add('animate-spin');
|
|
refreshIcon.style.transform = 'rotate(0deg)';
|
|
} else {
|
|
refreshIcon.classList.remove('animate-spin');
|
|
refreshIcon.style.transform = '';
|
|
}
|
|
}
|
|
|
|
if (refreshText) {
|
|
refreshText.textContent = isLoading ? 'Refreshing...' : 'Refresh';
|
|
}
|
|
});
|
|
};
|
|
|
|
const updateConnectionStatus = (status) => {
|
|
const statusConfig = {
|
|
connected: {
|
|
dotClass: 'w-2.5 h-2.5 rounded-full bg-green-500 mr-2',
|
|
textClass: 'text-sm text-green-500',
|
|
message: 'Connected'
|
|
},
|
|
disconnected: {
|
|
dotClass: 'w-2.5 h-2.5 rounded-full bg-red-500 mr-2',
|
|
textClass: 'text-sm text-red-500',
|
|
message: 'Disconnected - Reconnecting...'
|
|
},
|
|
error: {
|
|
dotClass: 'w-2.5 h-2.5 rounded-full bg-yellow-500 mr-2',
|
|
textClass: 'text-sm text-yellow-500',
|
|
message: 'Connection Error'
|
|
}
|
|
};
|
|
|
|
const config = statusConfig[status] || statusConfig.connected;
|
|
|
|
['sent', 'received'].forEach(type => {
|
|
const dot = elements[`statusDot${type.charAt(0).toUpperCase() + type.slice(1)}`];
|
|
const text = elements[`statusText${type.charAt(0).toUpperCase() + type.slice(1)}`];
|
|
|
|
if (dot && text) {
|
|
dot.className = config.dotClass;
|
|
text.className = config.textClass;
|
|
text.textContent = config.message;
|
|
}
|
|
});
|
|
};
|
|
|
|
const processIdentityStats = (identity) => {
|
|
if (!identity) return null;
|
|
|
|
const stats = {
|
|
sentSuccessful: safeParseInt(identity.num_sent_bids_successful),
|
|
recvSuccessful: safeParseInt(identity.num_recv_bids_successful),
|
|
sentFailed: safeParseInt(identity.num_sent_bids_failed),
|
|
recvFailed: safeParseInt(identity.num_recv_bids_failed),
|
|
sentRejected: safeParseInt(identity.num_sent_bids_rejected),
|
|
recvRejected: safeParseInt(identity.num_recv_bids_rejected)
|
|
};
|
|
|
|
stats.totalSuccessful = stats.sentSuccessful + stats.recvSuccessful;
|
|
stats.totalFailed = stats.sentFailed + stats.recvFailed;
|
|
stats.totalRejected = stats.sentRejected + stats.recvRejected;
|
|
stats.totalBids = stats.totalSuccessful + stats.totalFailed + stats.totalRejected;
|
|
|
|
stats.successRate = stats.totalBids > 0
|
|
? ((stats.totalSuccessful / stats.totalBids) * 100).toFixed(1)
|
|
: '0.0';
|
|
|
|
return stats;
|
|
};
|
|
|
|
const createIdentityTooltipContent = (identity) => {
|
|
if (!identity) return '';
|
|
|
|
const stats = processIdentityStats(identity);
|
|
if (!stats) return '';
|
|
|
|
const getSuccessRateColor = (rate) => {
|
|
const numRate = parseFloat(rate);
|
|
if (numRate >= 80) return 'text-green-600';
|
|
if (numRate >= 60) return 'text-yellow-600';
|
|
return 'text-red-600';
|
|
};
|
|
|
|
return `
|
|
<div class="identity-info space-y-2">
|
|
${identity.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">${identity.label}</div>
|
|
</div>
|
|
` : ''}
|
|
|
|
<div class="space-y-1">
|
|
<div class="text-white text-xs tracking-wide font-semibold">Bid From Address:</div>
|
|
<div class="monospace text-xs break-all bg-gray-500 p-2 rounded-md text-white">
|
|
${identity.address || ''}
|
|
</div>
|
|
</div>
|
|
|
|
${identity.note ? `
|
|
<div class="space-y-1">
|
|
<div class="text-white text-xs tracking-wide font-semibold">Note:</div>
|
|
<div class="text-white text-sm italic">${identity.note}</div>
|
|
</div>
|
|
` : ''}
|
|
|
|
<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(stats.successRate)}">
|
|
${stats.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">${stats.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">
|
|
${stats.totalSuccessful}
|
|
</div>
|
|
<div class="text-white">Successful</div>
|
|
</div>
|
|
<div>
|
|
<div class="text-yellow-600 font-semibold">
|
|
${stats.totalRejected}
|
|
</div>
|
|
<div class="text-white">Rejected</div>
|
|
</div>
|
|
<div>
|
|
<div class="text-red-600 font-semibold">
|
|
${stats.totalFailed}
|
|
</div>
|
|
<div class="text-white">Failed</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
};
|
|
|
|
const tooltipIdsToCleanup = new Set();
|
|
|
|
const cleanupTooltips = () => {
|
|
if (window.TooltipManager) {
|
|
Array.from(tooltipIdsToCleanup).forEach(id => {
|
|
const element = document.getElementById(id);
|
|
if (element) {
|
|
element.remove();
|
|
}
|
|
});
|
|
tooltipIdsToCleanup.clear();
|
|
}
|
|
forceTooltipDOMCleanup();
|
|
};
|
|
|
|
const forceTooltipDOMCleanup = () => {
|
|
let foundCount = 0;
|
|
let removedCount = 0;
|
|
const allTooltipElements = document.querySelectorAll('[role="tooltip"], [id^="tooltip-"], .tippy-box, [data-tippy-root]');
|
|
foundCount += allTooltipElements.length;
|
|
|
|
allTooltipElements.forEach(element => {
|
|
const isDetached = !document.body.contains(element) ||
|
|
element.classList.contains('hidden') ||
|
|
element.style.display === 'none';
|
|
|
|
if (element.id && element.id.startsWith('tooltip-')) {
|
|
const triggerId = element.id;
|
|
const triggerElement = document.querySelector(`[data-tooltip-target="${triggerId}"]`);
|
|
|
|
if (!triggerElement ||
|
|
!document.body.contains(triggerElement) ||
|
|
triggerElement.classList.contains('hidden')) {
|
|
element.remove();
|
|
removedCount++;
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (isDetached) {
|
|
try {
|
|
element.remove();
|
|
removedCount++;
|
|
} catch (e) {
|
|
console.warn('Error removing detached tooltip:', e);
|
|
}
|
|
}
|
|
});
|
|
|
|
const tippyRoots = document.querySelectorAll('[data-tippy-root]');
|
|
foundCount += tippyRoots.length;
|
|
tippyRoots.forEach(element => {
|
|
const isOrphan = !element.children.length ||
|
|
element.children[0].classList.contains('hidden') ||
|
|
!document.body.contains(element);
|
|
|
|
if (isOrphan) {
|
|
try {
|
|
element.remove();
|
|
removedCount++;
|
|
} catch (e) {
|
|
console.warn('Error removing tippy root:', e);
|
|
}
|
|
}
|
|
});
|
|
|
|
const tippyBoxes = document.querySelectorAll('.tippy-box');
|
|
foundCount += tippyBoxes.length;
|
|
tippyBoxes.forEach(element => {
|
|
if (!element.parentElement || !document.body.contains(element.parentElement)) {
|
|
try {
|
|
element.remove();
|
|
removedCount++;
|
|
} catch (e) {
|
|
console.warn('Error removing tippy box:', e);
|
|
}
|
|
}
|
|
});
|
|
document.querySelectorAll('.tooltip').forEach(element => {
|
|
const isTrulyDetached = !element.parentElement ||
|
|
!document.body.contains(element.parentElement) ||
|
|
element.classList.contains('hidden');
|
|
if (isTrulyDetached) {
|
|
try {
|
|
element.remove();
|
|
removedCount++;
|
|
} catch (e) {
|
|
console.warn('Error removing legacy tooltip:', e);
|
|
}
|
|
}
|
|
});
|
|
|
|
if (window.TooltipManager && typeof window.TooltipManager.getActiveTooltipInstances === 'function') {
|
|
const activeTooltips = window.TooltipManager.getActiveTooltipInstances();
|
|
activeTooltips.forEach(([element, instance]) => {
|
|
const tooltipId = element.getAttribute('data-tooltip-trigger-id');
|
|
if (!document.body.contains(element)) {
|
|
if (instance?.[0]) {
|
|
try {
|
|
instance[0].destroy();
|
|
} catch (e) {
|
|
console.warn('Error destroying tooltip instance:', e);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
if (removedCount > 0) {
|
|
// console.log(`Tooltip cleanup: found ${foundCount}, removed ${removedCount} detached tooltips`);
|
|
}
|
|
}
|
|
|
|
const createTableRow = async (bid) => {
|
|
const identity = await IdentityManager.getIdentityData(bid.addr_from);
|
|
const uniqueId = `${bid.bid_id}_${Date.now()}`;
|
|
tooltipIdsToCleanup.add(`tooltip-identity-${uniqueId}`);
|
|
tooltipIdsToCleanup.add(`tooltip-status-${uniqueId}`);
|
|
const timeColor = getTimeStrokeColor(bid.expire_at);
|
|
|
|
return `
|
|
<tr class="opacity-100 text-gray-500 dark:text-gray-100 hover:bg-coolGray-200 dark:hover:bg-gray-600">
|
|
<!-- Time Column -->
|
|
<td class="py-3 pl-6 pr-3">
|
|
<div class="flex items-center min-w-max">
|
|
<svg class="w-5 h-5 mr-5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
|
<g stroke-linecap="round" stroke-width="2" fill="none" stroke="${timeColor}" stroke-linejoin="round">
|
|
<circle cx="12" cy="12" r="11"></circle>
|
|
<polyline points="12,6 12,12 18,12"></polyline>
|
|
</g>
|
|
</svg>
|
|
<div class="text-xs">${formatTime(bid.created_at)}</div>
|
|
</div>
|
|
</td>
|
|
|
|
<!-- Details Column -->
|
|
<td class="p-3 hidden lg:flex">
|
|
<div class="flex flex-col">
|
|
<div class="flex items-center min-w-max">
|
|
<div class="relative" data-tooltip-target="tooltip-identity-${uniqueId}">
|
|
<a href="/identity/${bid.addr_from}" class="text-xs font-mono">
|
|
<span>
|
|
${state.currentTab === 'sent' ? 'Out:' : 'In:'}
|
|
</span>
|
|
${identity?.label || formatAddressSMSG(bid.addr_from)}
|
|
</a>
|
|
</div>
|
|
</div>
|
|
<div class="font-mono text-xs opacity-75">
|
|
<a href="/offer/${bid.offer_id}">
|
|
Offer: ${formatAddress(bid.offer_id)}
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</td>
|
|
|
|
<!-- Send Coin Column -->
|
|
<td class="p-3">
|
|
<div class="flex items-center min-w-max">
|
|
<img class="w-8 h-8 mr-2"
|
|
src="/static/images/coins/${state.currentTab === 'sent' ? bid.coin_to.replace(' ', '-') : bid.coin_from.replace(' ', '-')}.png"
|
|
alt="${state.currentTab === 'sent' ? bid.coin_to : bid.coin_from}"
|
|
onerror="this.src='/static/images/coins/default.png'">
|
|
<div>
|
|
<div class="text-sm font-medium monospace">${state.currentTab === 'sent' ? bid.amount_to : bid.amount_from}</div>
|
|
<div class="text-xs opacity-75 monospace">${state.currentTab === 'sent' ? bid.coin_to : bid.coin_from}</div>
|
|
</div>
|
|
</div>
|
|
</td>
|
|
|
|
<!-- Receive Coin Column -->
|
|
<td class="p-3">
|
|
<div class="flex items-center min-w-max">
|
|
<img class="w-8 h-8 mr-2"
|
|
src="/static/images/coins/${state.currentTab === 'sent' ? bid.coin_from.replace(' ', '-') : bid.coin_to.replace(' ', '-')}.png"
|
|
alt="${state.currentTab === 'sent' ? bid.coin_from : bid.coin_to}"
|
|
onerror="this.src='/static/images/coins/default.png'">
|
|
<div>
|
|
<div class="text-sm font-medium monospace">${state.currentTab === 'sent' ? bid.amount_from : bid.amount_to}</div>
|
|
<div class="text-xs opacity-75 monospace">${state.currentTab === 'sent' ? bid.coin_from : bid.coin_to}</div>
|
|
</div>
|
|
</div>
|
|
</td>
|
|
|
|
<!-- Status Column -->
|
|
<td class="py-3 px-6">
|
|
<div class="relative flex justify-center" data-tooltip-target="tooltip-status-${uniqueId}">
|
|
<span class="w-full lg:w-7/8 xl:w-2/3 px-2.5 py-1 inline-flex items-center justify-center text-center rounded-full text-xs font-medium bold ${getStatusClass(bid.bid_state)}">
|
|
${bid.bid_state}
|
|
</span>
|
|
</div>
|
|
</td>
|
|
|
|
<!-- Actions Column -->
|
|
<td class="py-3 pr-4">
|
|
<div class="flex justify-center">
|
|
<a href="/bid/${bid.bid_id}"
|
|
class="inline-block w-20 py-1 px-2 font-medium text-center text-sm rounded-md bg-blue-500 text-white border border-blue-500 hover:bg-blue-600 transition duration-200">
|
|
View Bid
|
|
</a>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
|
|
|
|
<!-- Tooltips -->
|
|
<div id="tooltip-identity-${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 dark:bg-gray-600 max-w-sm pointer-events-none">
|
|
${createIdentityTooltipContent(identity)}
|
|
<div class="tooltip-arrow" data-popper-arrow></div>
|
|
</div>
|
|
|
|
<div id="tooltip-status-${uniqueId}" role="tooltip" class="inline-block absolute 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 dark:bg-gray-600">
|
|
<div class="text-white">
|
|
<p class="font-bold mb-2">Transaction Status</p>
|
|
<div class="grid grid-cols-2 gap-2">
|
|
<div class="bg-gray-500 p-2 rounded">
|
|
<p class="text-xs font-bold">ITX:</p>
|
|
<p>${bid.tx_state_a || 'N/A'}</p>
|
|
</div>
|
|
<div class="bg-gray-500 p-2 rounded">
|
|
<p class="text-xs font-bold">PTX:</p>
|
|
<p>${bid.tx_state_b || 'N/A'}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="tooltip-arrow" data-popper-arrow></div>
|
|
</div>
|
|
`;
|
|
};
|
|
|
|
const updateTableContent = async (type) => {
|
|
const tbody = elements[`${type}BidsBody`];
|
|
if (!tbody) return;
|
|
|
|
if (window.TooltipManager) {
|
|
window.TooltipManager.cleanup();
|
|
}
|
|
|
|
cleanupTooltips();
|
|
forceTooltipDOMCleanup();
|
|
|
|
tooltipIdsToCleanup.clear();
|
|
|
|
const filteredData = state.data[type];
|
|
|
|
const startIndex = (state.currentPage[type] - 1) * PAGE_SIZE;
|
|
const endIndex = startIndex + PAGE_SIZE;
|
|
|
|
const currentPageData = filteredData.slice(startIndex, endIndex);
|
|
|
|
//console.log('Updating table content:', {
|
|
// type: type,
|
|
// totalFilteredBids: filteredData.length,
|
|
// currentPageBids: currentPageData.length,
|
|
// startIndex: startIndex,
|
|
// endIndex: endIndex
|
|
//});
|
|
|
|
try {
|
|
if (currentPageData.length > 0) {
|
|
const BATCH_SIZE = 10;
|
|
let allRows = [];
|
|
|
|
for (let i = 0; i < currentPageData.length; i += BATCH_SIZE) {
|
|
const batch = currentPageData.slice(i, i + BATCH_SIZE);
|
|
const rowPromises = batch.map(bid => createTableRow(bid));
|
|
const rows = await Promise.all(rowPromises);
|
|
allRows = allRows.concat(rows);
|
|
|
|
if (i + BATCH_SIZE < currentPageData.length) {
|
|
await new Promise(resolve => setTimeout(resolve, 5));
|
|
}
|
|
}
|
|
|
|
const scrollPosition = tbody.parentElement?.scrollTop || 0;
|
|
|
|
tbody.innerHTML = allRows.join('');
|
|
|
|
if (tbody.parentElement && scrollPosition > 0) {
|
|
tbody.parentElement.scrollTop = scrollPosition;
|
|
}
|
|
|
|
if (document.visibilityState === 'visible') {
|
|
|
|
setTimeout(() => {
|
|
initializeTooltips();
|
|
|
|
setTimeout(() => {
|
|
forceTooltipDOMCleanup();
|
|
}, 100);
|
|
}, 10);
|
|
}
|
|
} else {
|
|
tbody.innerHTML = `
|
|
<tr>
|
|
<td colspan="8" class="text-center py-4 text-gray-500 dark:text-white">
|
|
No ${type} bids found
|
|
</td>
|
|
</tr>`;
|
|
}
|
|
} catch (error) {
|
|
console.error('Error updating table content:', error);
|
|
tbody.innerHTML = `
|
|
<tr>
|
|
<td colspan="8" class="text-center py-4 text-red-500">
|
|
Error loading data. Please try refreshing.
|
|
</td>
|
|
</tr>`;
|
|
}
|
|
|
|
updatePaginationControls(type);
|
|
};
|
|
|
|
const initializeTooltips = () => {
|
|
if (!window.TooltipManager || document.hidden) {
|
|
return;
|
|
}
|
|
|
|
window.TooltipManager.cleanup();
|
|
|
|
const selector = '#' + state.currentTab + ' [data-tooltip-target]';
|
|
const tooltipTriggers = document.querySelectorAll(selector);
|
|
const tooltipCount = tooltipTriggers.length;
|
|
if (tooltipCount > 50) {
|
|
//console.log(`Optimizing ${tooltipCount} tooltips`);
|
|
const viewportMargin = 200;
|
|
const viewportTooltips = Array.from(tooltipTriggers).filter(trigger => {
|
|
const rect = trigger.getBoundingClientRect();
|
|
return (
|
|
rect.bottom >= -viewportMargin &&
|
|
rect.top <= (window.innerHeight + viewportMargin) &&
|
|
rect.right >= 0 &&
|
|
rect.left <= window.innerWidth
|
|
);
|
|
});
|
|
|
|
viewportTooltips.forEach(trigger => {
|
|
createTooltipForTrigger(trigger);
|
|
});
|
|
|
|
const offscreenTooltips = Array.from(tooltipTriggers).filter(t => !viewportTooltips.includes(t));
|
|
|
|
offscreenTooltips.forEach(trigger => {
|
|
const createTooltipOnHover = () => {
|
|
createTooltipForTrigger(trigger);
|
|
trigger.removeEventListener('mouseenter', createTooltipOnHover);
|
|
};
|
|
|
|
trigger.addEventListener('mouseenter', createTooltipOnHover);
|
|
});
|
|
} else {
|
|
|
|
tooltipTriggers.forEach(trigger => {
|
|
createTooltipForTrigger(trigger);
|
|
});
|
|
}
|
|
};
|
|
|
|
const createTooltipForTrigger = (trigger) => {
|
|
if (!trigger || !window.TooltipManager) return;
|
|
|
|
const targetId = trigger.getAttribute('data-tooltip-target');
|
|
const tooltipContent = document.getElementById(targetId);
|
|
|
|
if (tooltipContent) {
|
|
window.TooltipManager.create(trigger, tooltipContent.innerHTML, {
|
|
placement: trigger.getAttribute('data-tooltip-placement') || 'top',
|
|
interactive: true,
|
|
animation: false,
|
|
maxWidth: 400,
|
|
allowHTML: true,
|
|
offset: [0, 8],
|
|
zIndex: 50,
|
|
delay: [200, 0],
|
|
appendTo: () => document.body
|
|
});
|
|
}
|
|
};
|
|
|
|
function optimizeForLargeDatasets() {
|
|
if (state.data[state.currentTab]?.length > 50) {
|
|
|
|
const simplifyTooltips = tooltipIdsToCleanup.size > 50;
|
|
|
|
implementVirtualizedRows();
|
|
|
|
let scrollTimeout;
|
|
window.addEventListener('scroll', () => {
|
|
clearTimeout(scrollTimeout);
|
|
scrollTimeout = setTimeout(() => {
|
|
cleanupOffscreenTooltips();
|
|
}, 150);
|
|
}, { passive: true });
|
|
}
|
|
}
|
|
|
|
function cleanupOffscreenTooltips() {
|
|
if (!window.TooltipManager) return;
|
|
|
|
const selector = '#' + state.currentTab + ' [data-tooltip-target]';
|
|
const tooltipTriggers = document.querySelectorAll(selector);
|
|
|
|
const farOffscreenTriggers = Array.from(tooltipTriggers).filter(trigger => {
|
|
const rect = trigger.getBoundingClientRect();
|
|
return (rect.bottom < -window.innerHeight * 2 ||
|
|
rect.top > window.innerHeight * 3);
|
|
});
|
|
|
|
farOffscreenTriggers.forEach(trigger => {
|
|
const targetId = trigger.getAttribute('data-tooltip-target');
|
|
if (targetId) {
|
|
const tooltipElement = document.getElementById(targetId);
|
|
if (tooltipElement) {
|
|
window.TooltipManager.destroy(trigger);
|
|
trigger.addEventListener('mouseenter', () => {
|
|
createTooltipForTrigger(trigger);
|
|
}, { once: true });
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
function implementVirtualizedRows() {
|
|
const tbody = elements[`${state.currentTab}BidsBody`];
|
|
if (!tbody) return;
|
|
|
|
const tableRows = tbody.querySelectorAll('tr');
|
|
if (tableRows.length < 30) return;
|
|
|
|
Array.from(tableRows).forEach(row => {
|
|
const rect = row.getBoundingClientRect();
|
|
const isVisible = (
|
|
rect.bottom >= 0 &&
|
|
rect.top <= window.innerHeight
|
|
);
|
|
|
|
if (!isVisible && (rect.bottom < -window.innerHeight || rect.top > window.innerHeight * 2)) {
|
|
const tooltipTriggers = row.querySelectorAll('[data-tooltip-target]');
|
|
tooltipTriggers.forEach(trigger => {
|
|
if (window.TooltipManager) {
|
|
window.TooltipManager.destroy(trigger);
|
|
}
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
let activeFetchController = null;
|
|
|
|
const fetchBids = async () => {
|
|
try {
|
|
if (activeFetchController) {
|
|
activeFetchController.abort();
|
|
}
|
|
activeFetchController = new AbortController();
|
|
const endpoint = state.currentTab === 'sent' ? '/json/sentbids' : '/json/bids';
|
|
const withExpiredSelect = document.getElementById('with_expired');
|
|
const includeExpired = withExpiredSelect ? withExpiredSelect.value === 'true' : true;
|
|
|
|
//console.log('Fetching bids, include expired:', includeExpired);
|
|
|
|
const timeoutId = setTimeout(() => {
|
|
if (activeFetchController) {
|
|
activeFetchController.abort();
|
|
}
|
|
}, 30000);
|
|
|
|
const response = await fetch(endpoint, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Accept': 'application/json'
|
|
},
|
|
body: JSON.stringify({
|
|
sort_by: state.filters.sort_by || 'created_at',
|
|
sort_dir: state.filters.sort_dir || 'desc',
|
|
with_expired: true,
|
|
state: state.filters.state ?? -1,
|
|
with_extra_info: true
|
|
}),
|
|
signal: activeFetchController.signal
|
|
});
|
|
|
|
clearTimeout(timeoutId);
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`HTTP error! status: ${response.status}`);
|
|
}
|
|
|
|
const data = await response.json();
|
|
//console.log('Received raw data:', data.length, 'bids');
|
|
|
|
state.filters.with_expired = includeExpired;
|
|
|
|
let processedData;
|
|
if (data.length > 500) {
|
|
processedData = await new Promise(resolve => {
|
|
setTimeout(() => {
|
|
const filtered = filterAndSortData(data);
|
|
resolve(filtered);
|
|
}, 10);
|
|
});
|
|
} else {
|
|
processedData = filterAndSortData(data);
|
|
}
|
|
|
|
return processedData;
|
|
} catch (error) {
|
|
if (error.name === 'AbortError') {
|
|
console.log('Fetch request was aborted');
|
|
} else {
|
|
console.error('Error in fetchBids:', error);
|
|
}
|
|
throw error;
|
|
} finally {
|
|
activeFetchController = null;
|
|
}
|
|
};
|
|
|
|
const updateBidsTable = async () => {
|
|
if (state.isLoading) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
state.isLoading = true;
|
|
updateLoadingState(true);
|
|
|
|
const bids = await fetchBids();
|
|
|
|
// Add identity preloading if we're searching
|
|
if (state.filters.searchQuery && state.filters.searchQuery.length > 0) {
|
|
await preloadIdentitiesForSearch(bids);
|
|
}
|
|
|
|
state.data[state.currentTab] = bids;
|
|
state.currentPage[state.currentTab] = 1;
|
|
|
|
await updateTableContent(state.currentTab);
|
|
updatePaginationControls(state.currentTab);
|
|
|
|
} catch (error) {
|
|
console.error('Error in updateBidsTable:', error);
|
|
updateConnectionStatus('error');
|
|
} finally {
|
|
state.isLoading = false;
|
|
updateLoadingState(false);
|
|
}
|
|
};
|
|
|
|
const updatePaginationControls = (type) => {
|
|
const data = state.data[type] || [];
|
|
const totalPages = Math.ceil(data.length / PAGE_SIZE);
|
|
const controls = elements[`${type}PaginationControls`];
|
|
const prevButton = elements[`prevPage${type.charAt(0).toUpperCase() + type.slice(1)}`];
|
|
const nextButton = elements[`nextPage${type.charAt(0).toUpperCase() + type.slice(1)}`];
|
|
const currentPageSpan = elements[`currentPage${type.charAt(0).toUpperCase() + type.slice(1)}`];
|
|
const bidsCount = elements[`${type}BidsCount`];
|
|
|
|
//console.log('Pagination controls update:', {
|
|
// type: type,
|
|
// totalBids: data.length,
|
|
// totalPages: totalPages,
|
|
// currentPage: state.currentPage[type]
|
|
//});
|
|
|
|
if (state.currentPage[type] > totalPages) {
|
|
state.currentPage[type] = totalPages > 0 ? totalPages : 1;
|
|
}
|
|
|
|
if (controls) {
|
|
controls.style.display = totalPages > 1 ? 'flex' : 'none';
|
|
}
|
|
|
|
if (currentPageSpan) {
|
|
currentPageSpan.textContent = totalPages > 0 ? state.currentPage[type] : 0;
|
|
}
|
|
|
|
if (prevButton) {
|
|
prevButton.style.display = state.currentPage[type] > 1 ? 'inline-flex' : 'none';
|
|
}
|
|
|
|
if (nextButton) {
|
|
nextButton.style.display = state.currentPage[type] < totalPages ? 'inline-flex' : 'none';
|
|
}
|
|
|
|
if (bidsCount) {
|
|
bidsCount.textContent = data.length;
|
|
}
|
|
};
|
|
|
|
let searchTimeout;
|
|
function handleSearch(event) {
|
|
if (searchTimeout) {
|
|
clearTimeout(searchTimeout);
|
|
}
|
|
|
|
searchTimeout = setTimeout(() => {
|
|
state.filters.searchQuery = event.target.value.toLowerCase();
|
|
updateBidsTable();
|
|
updateClearFiltersButton();
|
|
}, 300);
|
|
}
|
|
|
|
function clearFilters() {
|
|
if (!hasActiveFilters()) return;
|
|
|
|
const filterElements = {
|
|
stateSelect: document.getElementById('state'),
|
|
withExpiredSelect: document.getElementById('with_expired'),
|
|
coinFrom: document.getElementById('coin_from'),
|
|
coinTo: document.getElementById('coin_to'),
|
|
searchInput: document.getElementById('searchInput')
|
|
};
|
|
|
|
if (filterElements.stateSelect) filterElements.stateSelect.value = '-1';
|
|
if (filterElements.withExpiredSelect) filterElements.withExpiredSelect.value = 'true';
|
|
if (filterElements.coinFrom) filterElements.coinFrom.value = 'any';
|
|
if (filterElements.coinTo) filterElements.coinTo.value = 'any';
|
|
if (filterElements.searchInput) filterElements.searchInput.value = '';
|
|
|
|
state.filters = {
|
|
state: -1,
|
|
sort_by: 'created_at',
|
|
sort_dir: 'desc',
|
|
with_expired: true,
|
|
searchQuery: '',
|
|
coin_from: 'any',
|
|
coin_to: 'any'
|
|
};
|
|
|
|
localStorage.removeItem('bidsTableSettings');
|
|
updateCoinFilterImages();
|
|
updateBidsTable();
|
|
updateClearFiltersButton();
|
|
}
|
|
|
|
function applyFilters() {
|
|
const stateSelect = document.getElementById('state');
|
|
const sortBySelect = document.getElementById('sort_by');
|
|
const sortDirSelect = document.getElementById('sort_dir');
|
|
const withExpiredSelect = document.getElementById('with_expired');
|
|
const coinFromSelect = document.getElementById('coin_from');
|
|
const coinToSelect = document.getElementById('coin_to');
|
|
const searchInput = document.getElementById('searchInput');
|
|
|
|
state.filters = {
|
|
state: stateSelect ? parseInt(stateSelect.value) : -1,
|
|
sort_by: sortBySelect ? sortBySelect.value : 'created_at',
|
|
sort_dir: sortDirSelect ? sortDirSelect.value : 'desc',
|
|
with_expired: withExpiredSelect ? withExpiredSelect.value === 'true' : true,
|
|
searchQuery: searchInput ? searchInput.value.toLowerCase() : '',
|
|
coin_from: coinFromSelect ? coinFromSelect.value : 'any',
|
|
coin_to: coinToSelect ? coinToSelect.value : 'any'
|
|
};
|
|
|
|
updateBidsTable();
|
|
updateClearFiltersButton();
|
|
}
|
|
|
|
function updateClearFiltersButton() {
|
|
const clearButton = document.getElementById('clearFilters');
|
|
if (clearButton) {
|
|
const hasFilters = hasActiveFilters();
|
|
|
|
clearButton.disabled = !hasFilters;
|
|
|
|
if (hasFilters) {
|
|
clearButton.classList.remove('opacity-50', 'cursor-not-allowed', 'bg-gray-300');
|
|
clearButton.classList.add('hover:bg-green-600', 'hover:text-white', 'bg-coolGray-200');
|
|
} else {
|
|
clearButton.classList.add('opacity-50', 'cursor-not-allowed', 'bg-gray-300');
|
|
clearButton.classList.remove('hover:bg-green-600', 'hover:text-white', 'bg-coolGray-200');
|
|
}
|
|
}
|
|
}
|
|
|
|
const handleFilterChange = (e) => {
|
|
if (e) e.preventDefault();
|
|
|
|
state.filters = {
|
|
state: parseInt(elements.stateSelect.value),
|
|
sort_by: elements.sortBySelect.value,
|
|
sort_dir: elements.sortDirSelect.value,
|
|
with_expired: elements.withExpiredSelect.value === 'true'
|
|
};
|
|
|
|
state.currentPage[state.currentTab] = 1;
|
|
|
|
updateBidsTable();
|
|
};
|
|
|
|
function setupFilterEventListeners() {
|
|
const coinToSelect = document.getElementById('coin_to');
|
|
const coinFromSelect = document.getElementById('coin_from');
|
|
const withExpiredSelect = document.getElementById('with_expired');
|
|
|
|
if (coinToSelect) {
|
|
EventManager.add(coinToSelect, 'change', () => {
|
|
state.filters.coin_to = coinToSelect.value;
|
|
updateBidsTable();
|
|
updateCoinFilterImages();
|
|
updateClearFiltersButton();
|
|
});
|
|
}
|
|
|
|
if (coinFromSelect) {
|
|
EventManager.add(coinFromSelect, 'change', () => {
|
|
state.filters.coin_from = coinFromSelect.value;
|
|
updateBidsTable();
|
|
updateCoinFilterImages();
|
|
updateClearFiltersButton();
|
|
});
|
|
}
|
|
|
|
if (withExpiredSelect) {
|
|
EventManager.add(withExpiredSelect, 'change', () => {
|
|
state.filters.with_expired = withExpiredSelect.value === 'true';
|
|
updateBidsTable();
|
|
updateClearFiltersButton();
|
|
});
|
|
}
|
|
|
|
const searchInput = document.getElementById('searchInput');
|
|
if (searchInput) {
|
|
EventManager.add(searchInput, 'input', (event) => {
|
|
if (searchTimeout) {
|
|
clearTimeout(searchTimeout);
|
|
}
|
|
|
|
searchTimeout = setTimeout(() => {
|
|
state.filters.searchQuery = event.target.value.toLowerCase();
|
|
updateBidsTable();
|
|
updateClearFiltersButton();
|
|
}, 300);
|
|
});
|
|
}
|
|
}
|
|
|
|
const setupRefreshButtons = () => {
|
|
['Sent', 'Received'].forEach(type => {
|
|
const refreshButton = elements[`refresh${type}Bids`];
|
|
if (refreshButton) {
|
|
EventManager.add(refreshButton, 'click', async () => {
|
|
const lowerType = type.toLowerCase();
|
|
|
|
if (state.isRefreshing) {
|
|
console.log('Already refreshing, skipping');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
state.isRefreshing = true;
|
|
state.isLoading = true;
|
|
updateLoadingState(true);
|
|
|
|
const response = await fetch(state.currentTab === 'sent' ? '/json/sentbids' : '/json/bids', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify({
|
|
sort_by: state.filters.sort_by,
|
|
sort_dir: state.filters.sort_dir,
|
|
with_expired: state.filters.with_expired,
|
|
state: state.filters.state,
|
|
with_extra_info: true
|
|
})
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`HTTP error! status: ${response.status}`);
|
|
}
|
|
|
|
const data = await response.json();
|
|
if (!Array.isArray(data)) {
|
|
throw new Error('Invalid response format');
|
|
}
|
|
|
|
state.data[lowerType] = data;
|
|
await updateTableContent(lowerType);
|
|
updatePaginationControls(lowerType);
|
|
|
|
} catch (error) {
|
|
console.error(`Error refreshing ${type} bids:`, error);
|
|
} finally {
|
|
state.isRefreshing = false;
|
|
state.isLoading = false;
|
|
updateLoadingState(false);
|
|
}
|
|
});
|
|
}
|
|
});
|
|
};
|
|
|
|
const switchTab = (tabId) => {
|
|
if (state.isLoading) return;
|
|
|
|
if (window.TooltipManager) {
|
|
window.TooltipManager.cleanup();
|
|
}
|
|
|
|
cleanupTooltips();
|
|
forceTooltipDOMCleanup();
|
|
|
|
tooltipIdsToCleanup.clear();
|
|
|
|
state.currentTab = tabId === '#sent' ? 'sent' : 'received';
|
|
|
|
elements.sentContent.classList.add('hidden');
|
|
elements.receivedContent.classList.add('hidden');
|
|
|
|
const targetPanel = document.querySelector(tabId);
|
|
if (targetPanel) {
|
|
targetPanel.classList.remove('hidden');
|
|
}
|
|
|
|
elements.tabButtons.forEach(tab => {
|
|
const selected = tab.dataset.tabsTarget === tabId;
|
|
tab.setAttribute('aria-selected', selected);
|
|
if (selected) {
|
|
tab.classList.add('bg-gray-100', 'dark:bg-gray-600', 'text-gray-900', 'dark:text-white');
|
|
tab.classList.remove('hover:text-gray-600', 'hover:bg-gray-50', 'dark:hover:bg-gray-500');
|
|
} else {
|
|
tab.classList.remove('bg-gray-100', 'dark:bg-gray-600', 'text-gray-900', 'dark:text-white');
|
|
tab.classList.add('hover:text-gray-600', 'hover:bg-gray-50', 'dark:hover:bg-gray-500');
|
|
}
|
|
});
|
|
setTimeout(() => {
|
|
updateBidsTable();
|
|
}, 10);
|
|
};
|
|
|
|
const setupEventListeners = () => {
|
|
const filterControls = document.querySelector('.flex.flex-wrap.justify-center');
|
|
if (filterControls) {
|
|
EventManager.add(filterControls, 'submit', (e) => {
|
|
e.preventDefault();
|
|
});
|
|
}
|
|
|
|
const applyFiltersBtn = document.getElementById('applyFilters');
|
|
if (applyFiltersBtn) {
|
|
applyFiltersBtn.remove();
|
|
}
|
|
|
|
if (elements.tabButtons) {
|
|
elements.tabButtons.forEach(button => {
|
|
EventManager.add(button, 'click', () => {
|
|
if (state.isLoading) return;
|
|
|
|
const targetId = button.getAttribute('data-tabs-target');
|
|
if (!targetId) return;
|
|
|
|
elements.tabButtons.forEach(tab => {
|
|
const isSelected = tab.getAttribute('data-tabs-target') === targetId;
|
|
tab.setAttribute('aria-selected', isSelected);
|
|
|
|
if (isSelected) {
|
|
tab.classList.add('bg-gray-100', 'dark:bg-gray-600', 'text-gray-900', 'dark:text-white');
|
|
tab.classList.remove('hover:text-gray-600', 'hover:bg-gray-50', 'dark:hover:bg-gray-500');
|
|
} else {
|
|
tab.classList.remove('bg-gray-100', 'dark:bg-gray-600', 'text-gray-900', 'dark:text-white');
|
|
tab.classList.add('hover:text-gray-600', 'hover:bg-gray-50', 'dark:hover:bg-gray-500');
|
|
}
|
|
});
|
|
|
|
elements.sentContent.classList.toggle('hidden', targetId !== '#sent');
|
|
elements.receivedContent.classList.toggle('hidden', targetId !== '#received');
|
|
|
|
state.currentTab = targetId === '#sent' ? 'sent' : 'received';
|
|
state.currentPage[state.currentTab] = 1;
|
|
|
|
if (window.TooltipManager) {
|
|
window.TooltipManager.cleanup();
|
|
}
|
|
cleanupTooltips();
|
|
|
|
updateBidsTable();
|
|
});
|
|
});
|
|
}
|
|
|
|
['Sent', 'Received'].forEach(type => {
|
|
const lowerType = type.toLowerCase();
|
|
|
|
if (elements[`prevPage${type}`]) {
|
|
EventManager.add(elements[`prevPage${type}`], 'click', () => {
|
|
if (state.isLoading) return;
|
|
if (state.currentPage[lowerType] > 1) {
|
|
state.currentPage[lowerType]--;
|
|
updateTableContent(lowerType);
|
|
}
|
|
});
|
|
}
|
|
|
|
if (elements[`nextPage${type}`]) {
|
|
EventManager.add(elements[`nextPage${type}`], 'click', () => {
|
|
if (state.isLoading) return;
|
|
const totalPages = Math.ceil(state.data[lowerType].length / PAGE_SIZE);
|
|
if (state.currentPage[lowerType] < totalPages) {
|
|
state.currentPage[lowerType]++;
|
|
updateTableContent(lowerType);
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
const searchInput = document.getElementById('searchInput');
|
|
if (searchInput) {
|
|
EventManager.add(searchInput, 'input', handleSearch);
|
|
}
|
|
|
|
const coinToSelect = document.getElementById('coin_to');
|
|
const coinFromSelect = document.getElementById('coin_from');
|
|
|
|
if (coinToSelect) {
|
|
EventManager.add(coinToSelect, 'change', () => {
|
|
state.filters.coin_to = coinToSelect.value;
|
|
updateBidsTable();
|
|
updateCoinFilterImages();
|
|
});
|
|
}
|
|
|
|
if (coinFromSelect) {
|
|
EventManager.add(coinFromSelect, 'change', () => {
|
|
state.filters.coin_from = coinFromSelect.value;
|
|
updateBidsTable();
|
|
updateCoinFilterImages();
|
|
});
|
|
}
|
|
|
|
const filterElements = {
|
|
stateSelect: document.getElementById('state'),
|
|
sortBySelect: document.getElementById('sort_by'),
|
|
sortDirSelect: document.getElementById('sort_dir'),
|
|
withExpiredSelect: document.getElementById('with_expired'),
|
|
clearFiltersBtn: document.getElementById('clearFilters')
|
|
};
|
|
|
|
if (filterElements.stateSelect) {
|
|
EventManager.add(filterElements.stateSelect, 'change', () => {
|
|
const stateValue = parseInt(filterElements.stateSelect.value);
|
|
|
|
state.filters.state = isNaN(stateValue) ? -1 : stateValue;
|
|
|
|
console.log('State filter changed:', {
|
|
selectedValue: filterElements.stateSelect.value,
|
|
parsedState: state.filters.state
|
|
});
|
|
|
|
updateBidsTable();
|
|
updateClearFiltersButton();
|
|
});
|
|
}
|
|
|
|
[
|
|
filterElements.sortBySelect,
|
|
filterElements.sortDirSelect,
|
|
filterElements.withExpiredSelect
|
|
].forEach(element => {
|
|
if (element) {
|
|
EventManager.add(element, 'change', () => {
|
|
updateBidsTable();
|
|
updateClearFiltersButton();
|
|
});
|
|
}
|
|
});
|
|
|
|
if (filterElements.clearFiltersBtn) {
|
|
EventManager.add(filterElements.clearFiltersBtn, 'click', () => {
|
|
if (filterElements.clearFiltersBtn.disabled) return;
|
|
clearFilters();
|
|
});
|
|
}
|
|
|
|
EventManager.add(document, 'change', (event) => {
|
|
const target = event.target;
|
|
const filterForm = document.querySelector('.flex.flex-wrap.justify-center');
|
|
|
|
if (filterForm && filterForm.contains(target)) {
|
|
const formData = {
|
|
state: filterElements.stateSelect?.value,
|
|
sort_by: filterElements.sortBySelect?.value,
|
|
sort_dir: filterElements.sortDirSelect?.value,
|
|
with_expired: filterElements.withExpiredSelect?.value,
|
|
coin_from: coinFromSelect?.value,
|
|
coin_to: coinToSelect?.value,
|
|
searchQuery: searchInput?.value
|
|
};
|
|
|
|
localStorage.setItem('bidsTableSettings', JSON.stringify(formData));
|
|
}
|
|
});
|
|
|
|
EventManager.add(window, 'scroll', () => {
|
|
if (!document.hidden && !state.isLoading) {
|
|
setTimeout(initializeTooltips, 100);
|
|
}
|
|
}, { passive: true });
|
|
initializeTooltips();
|
|
updateCoinFilterImages();
|
|
updateClearFiltersButton();
|
|
};
|
|
|
|
function setupMemoryMonitoring() {
|
|
const MEMORY_CHECK_INTERVAL = 2 * 60 * 1000;
|
|
|
|
const intervalId = setInterval(() => {
|
|
if (document.hidden) {
|
|
console.log('Tab hidden - running memory optimization');
|
|
|
|
if (window.IdentityManager) {
|
|
if (typeof IdentityManager.limitCacheSize === 'function') {
|
|
IdentityManager.limitCacheSize(100);
|
|
}
|
|
}
|
|
|
|
if (window.TooltipManager && typeof window.TooltipManager.cleanup === 'function') {
|
|
window.TooltipManager.cleanup();
|
|
}
|
|
|
|
if (state.data.sent.length > 1000) {
|
|
console.log('Trimming sent bids data');
|
|
state.data.sent = state.data.sent.slice(0, 1000);
|
|
}
|
|
|
|
if (state.data.received.length > 1000) {
|
|
console.log('Trimming received bids data');
|
|
state.data.received = state.data.received.slice(0, 1000);
|
|
}
|
|
} else {
|
|
cleanupTooltips();
|
|
}
|
|
}, MEMORY_CHECK_INTERVAL);
|
|
|
|
document.addEventListener('beforeunload', () => {
|
|
clearInterval(intervalId);
|
|
}, { once: true });
|
|
}
|
|
|
|
// Init
|
|
function initialize() {
|
|
const filterElements = {
|
|
stateSelect: document.getElementById('state'),
|
|
sortBySelect: document.getElementById('sort_by'),
|
|
sortDirSelect: document.getElementById('sort_dir'),
|
|
withExpiredSelect: document.getElementById('with_expired'),
|
|
coinFrom: document.getElementById('coin_from'),
|
|
coinTo: document.getElementById('coin_to')
|
|
};
|
|
|
|
if (filterElements.stateSelect) filterElements.stateSelect.value = '-1';
|
|
if (filterElements.sortBySelect) filterElements.sortBySelect.value = 'created_at';
|
|
if (filterElements.sortDirSelect) filterElements.sortDirSelect.value = 'desc';
|
|
if (filterElements.withExpiredSelect) filterElements.withExpiredSelect.value = 'true';
|
|
if (filterElements.coinFrom) filterElements.coinFrom.value = 'any';
|
|
if (filterElements.coinTo) filterElements.coinTo.value = 'any';
|
|
|
|
setupMemoryMonitoring();
|
|
|
|
setTimeout(() => {
|
|
WebSocketManager.initialize();
|
|
setupEventListeners();
|
|
}, 10);
|
|
|
|
setTimeout(() => {
|
|
setupRefreshButtons();
|
|
setupFilterEventListeners();
|
|
updateCoinFilterImages();
|
|
}, 50);
|
|
|
|
setTimeout(() => {
|
|
updateClearFiltersButton();
|
|
state.currentTab = 'sent';
|
|
state.filters.state = -1;
|
|
updateBidsTable();
|
|
}, 100);
|
|
|
|
setInterval(() => {
|
|
if ((state.data.sent.length + state.data.received.length) > 1000) {
|
|
optimizeMemoryUsage();
|
|
}
|
|
}, 5 * 60 * 1000); // Check every 5 minutes
|
|
|
|
window.cleanupBidsTable = cleanup;
|
|
}
|
|
|
|
if (document.readyState === 'loading') {
|
|
document.addEventListener('DOMContentLoaded', initialize);
|
|
} else {
|
|
initialize();
|
|
}
|