From e7b47486f5e17f84e99867dbe1bd448a278d4b6d Mon Sep 17 00:00:00 2001 From: gerlofvanek Date: Fri, 9 May 2025 20:43:31 +0200 Subject: [PATCH] ALL tab/table on bids page. + Fix bids export. --- basicswap/static/js/bids_sentreceived.js | 744 ++++++++++-------- .../static/js/bids_sentreceived_export.js | 27 +- basicswap/static/js/ui/bids-tab-navigation.js | 231 ++++-- basicswap/templates/bids.html | 115 ++- 4 files changed, 720 insertions(+), 397 deletions(-) diff --git a/basicswap/static/js/bids_sentreceived.js b/basicswap/static/js/bids_sentreceived.js index 3ad6af4..ae4effd 100644 --- a/basicswap/static/js/bids_sentreceived.js +++ b/basicswap/static/js/bids_sentreceived.js @@ -1,15 +1,19 @@ const PAGE_SIZE = 50; +let activeFetchController = null; + const state = { currentPage: { + all: 1, sent: 1, received: 1 }, isLoading: false, isRefreshing: false, - currentTab: 'sent', + currentTab: 'all', wsConnected: false, refreshPromise: null, data: { + all: [], sent: [], received: [] }, @@ -24,6 +28,16 @@ const state = { } }; +document.addEventListener('tabactivated', function(event) { + if (event.detail && event.detail.tabId) { + const tabType = event.detail.type || (event.detail.tabId === '#all' ? 'all' : + (event.detail.tabId === '#sent' ? 'sent' : 'received')); + //console.log('Tab activation event received for:', tabType); + state.currentTab = tabType; + updateBidsTable(); + } +}); + const STATE_MAP = { 1: ['Sent'], 2: ['Receiving'], @@ -61,33 +75,43 @@ const STATE_MAP = { }; const elements = { - sentBidsBody: document.querySelector('#sent tbody'), - receivedBidsBody: document.querySelector('#received tbody'), + allBidsBody: document.querySelector('#all-tbody'), + 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'), + allContent: document.getElementById('all'), sentContent: document.getElementById('sent'), receivedContent: document.getElementById('received'), + allPaginationControls: document.getElementById('pagination-controls-all'), sentPaginationControls: document.getElementById('pagination-controls-sent'), receivedPaginationControls: document.getElementById('pagination-controls-received'), + prevPageAll: document.getElementById('prevPageAll'), prevPageSent: document.getElementById('prevPageSent'), - nextPageSent: document.getElementById('nextPageSent'), prevPageReceived: document.getElementById('prevPageReceived'), + nextPageAll: document.getElementById('nextPageAll'), + nextPageSent: document.getElementById('nextPageSent'), nextPageReceived: document.getElementById('nextPageReceived'), + currentPageAll: document.getElementById('currentPageAll'), currentPageSent: document.getElementById('currentPageSent'), currentPageReceived: document.getElementById('currentPageReceived'), + allBidsCount: document.getElementById('allBidsCount'), sentBidsCount: document.getElementById('sentBidsCount'), receivedBidsCount: document.getElementById('receivedBidsCount'), + statusDotAll: document.getElementById('status-dot-all'), + statusTextAll: document.getElementById('status-text-all'), statusDotSent: document.getElementById('status-dot-sent'), statusTextSent: document.getElementById('status-text-sent'), statusDotReceived: document.getElementById('status-dot-received'), statusTextReceived: document.getElementById('status-text-received'), + refreshAllBids: document.getElementById('refreshAllBids'), refreshSentBids: document.getElementById('refreshSentBids'), refreshReceivedBids: document.getElementById('refreshReceivedBids') }; @@ -213,6 +237,7 @@ function cleanup() { } }; + cleanupTableBody('all-tbody'); cleanupTableBody('sent-tbody'); cleanupTableBody('received-tbody'); @@ -234,11 +259,13 @@ function cleanup() { clearAllAnimationFrames(); state.data = { + all: [], sent: [], received: [] }; state.currentPage = { + all: 1, sent: 1, received: 1 }; @@ -283,7 +310,7 @@ function cleanup() { if (window.CleanupManager) CleanupManager.clearAll(); if (window.WebSocketManager) WebSocketManager.disconnect(); - state.data = { sent: [], received: [] }; + state.data = { all: [], sent: [], received: [] }; state.isLoading = false; Object.keys(elements).forEach(key => { @@ -366,7 +393,7 @@ function cleanupRow(row) { function optimizeMemoryUsage() { const MAX_BIDS_IN_MEMORY = 500; - ['sent', 'received'].forEach(type => { + ['all', '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); @@ -528,7 +555,9 @@ function filterAndSortData(bids) { const coinName = selectedOption?.textContent.trim(); if (coinName) { - const coinToMatch = state.currentTab === 'sent' ? bid.coin_to : bid.coin_from; + const coinToMatch = state.currentTab === 'all' + ? (bid.source === 'sent' ? bid.coin_to : bid.coin_from) + : (state.currentTab === 'sent' ? bid.coin_to : bid.coin_from); if (!coinMatches(coinToMatch, coinName)) { return false; } @@ -541,7 +570,10 @@ function filterAndSortData(bids) { const coinName = selectedOption?.textContent.trim(); if (coinName) { - const coinToMatch = state.currentTab === 'sent' ? bid.coin_from : bid.coin_to; + const coinToMatch = state.currentTab === 'all' + ? (bid.source === 'sent' ? bid.coin_from : bid.coin_to) + : (state.currentTab === 'sent' ? bid.coin_from : bid.coin_to); + if (!coinMatches(coinToMatch, coinName)) { return false; } @@ -582,7 +614,8 @@ function filterAndSortData(bids) { let matchesDisplayedLabel = false; if (!matchesLabel && document) { try { - const tableId = state.currentTab === 'sent' ? 'sent' : 'received'; + const tableId = state.currentTab === 'sent' ? 'sent' : + (state.currentTab === 'received' ? 'received' : 'all'); const cells = document.querySelectorAll(`#${tableId} a[href^="/identity/"]`); for (const cell of cells) { @@ -680,7 +713,7 @@ function updateCoinFilterImages() { const updateLoadingState = (isLoading) => { state.isLoading = isLoading; - ['Sent', 'Received'].forEach(type => { + ['All', 'Sent', 'Received'].forEach(type => { const refreshButton = elements[`refresh${type}Bids`]; const refreshText = refreshButton?.querySelector(`#refresh${type}Text`); const refreshIcon = refreshButton?.querySelector('svg'); @@ -731,7 +764,7 @@ const updateConnectionStatus = (status) => { const config = statusConfig[status] || statusConfig.connected; - ['sent', 'received'].forEach(type => { + ['all', '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)}`]; @@ -769,16 +802,69 @@ const processIdentityStats = (identity) => { const createIdentityTooltipContent = (identity) => { if (!identity) return ''; + const address = identity.address || ''; + let statsSection = ''; - const stats = processIdentityStats(identity); - if (!stats) return ''; + try { + const stats = processIdentityStats(identity); + if (stats) { + 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'; + }; - 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'; - }; + statsSection = ` +
+
Swap History:
+
+
+
+ ${stats.successRate}% +
+
Success Rate
+
+
+
${stats.totalBids}
+
Total Trades
+
+
+
+
+
+ ${stats.totalSuccessful} +
+
Successful
+
+
+
+ ${stats.totalRejected} +
+
Rejected
+
+
+
+ ${stats.totalFailed} +
+
Failed
+
+
+
+ `; + } + } catch (e) { + console.warn('Error processing identity stats:', e); + } + + const addressSection = ` +
+
Bid From Address:
+
+ ${address || identity.address || ''} +
+
+ `; return `
@@ -789,12 +875,7 @@ const createIdentityTooltipContent = (identity) => {
` : ''} -
-
Bid From Address:
-
- ${identity.address || ''} -
-
+ ${addressSection} ${identity.note ? `
@@ -803,41 +884,7 @@ const createIdentityTooltipContent = (identity) => {
` : ''} -
-
Swap History:
-
-
-
- ${stats.successRate}% -
-
Success Rate
-
-
-
${stats.totalBids}
-
Total Trades
-
-
-
-
-
- ${stats.totalSuccessful} -
-
Successful
-
-
-
- ${stats.totalRejected} -
-
Rejected
-
-
-
- ${stats.totalFailed} -
-
Failed
-
-
-
+ ${statsSection} `; }; @@ -954,12 +1001,117 @@ const forceTooltipDOMCleanup = () => { } } +async function fetchAllBids() { + try { + const sentController = new AbortController(); + const receivedController = new AbortController(); + + const sentPromise = fetch('/json/sentbids', { + 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: sentController.signal + }); + + const receivedPromise = fetch('/json/bids', { + 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: receivedController.signal + }); + + const timeoutId = setTimeout(() => { + sentController.abort(); + receivedController.abort(); + }, 30000); + + const [sentResponse, receivedResponse] = await Promise.all([sentPromise, receivedPromise]); + + clearTimeout(timeoutId); + + if (!sentResponse.ok || !receivedResponse.ok) { + throw new Error(`HTTP error! Sent status: ${sentResponse.status}, Received status: ${receivedResponse.status}`); + } + + const [sentData, receivedData] = await Promise.all([ + sentResponse.json(), + receivedResponse.json() + ]); + + const filteredSentData = filterAndSortData(sentData).map(bid => ({ ...bid, source: 'sent' })); + const filteredReceivedData = filterAndSortData(receivedData).map(bid => ({ ...bid, source: 'received' })); + + const combined = [...filteredSentData, ...filteredReceivedData].sort((a, b) => { + const direction = state.filters.sort_dir === 'asc' ? 1 : -1; + return direction * (a.created_at - b.created_at); + }); + + return combined; + } catch (error) { + console.error('Error fetching all bids:', error); + throw error; + } +} + const createTableRow = async (bid) => { - const identity = await IdentityManager.getIdentityData(bid.addr_from); + const rawAddress = bid.addr_from || ''; + let identity = null; + try { + identity = await IdentityManager.getIdentityData(rawAddress); + } catch (e) { + console.warn('Error loading identity for bid:', bid.bid_id, e); + } + if (!identity) { + identity = { address: rawAddress, label: null }; + } else if (!identity.address) { + identity.address = rawAddress; + } const uniqueId = `${bid.bid_id}_${Date.now()}`; tooltipIdsToCleanup.add(`tooltip-identity-${uniqueId}`); tooltipIdsToCleanup.add(`tooltip-status-${uniqueId}`); const timeColor = getTimeStrokeColor(bid.expire_at); + const currentTabIsAll = state.currentTab === 'all'; + const isSent = currentTabIsAll ? (bid.source === 'sent') : (state.currentTab === 'sent'); + const sourceIndicator = currentTabIsAll ? + ` + ${isSent ? 'Sent' : 'Received'} + ` : ''; + let tooltipContent = ''; + try { + tooltipContent = createIdentityTooltipContent(identity); + } catch (e) { + console.warn('Error creating tooltip content:', e); + } + if (!tooltipContent) { + tooltipContent = ` +
+
+
Bid From Address:
+
+ ${rawAddress} +
+
+
+ `; + } return ` @@ -972,7 +1124,10 @@ const createTableRow = async (bid) => { -
${formatTime(bid.created_at)}
+
+ ${formatTime(bid.created_at)} + ${sourceIndicator} +
@@ -981,11 +1136,11 @@ const createTableRow = async (bid) => {
@@ -1001,12 +1156,12 @@ const createTableRow = async (bid) => {
${state.currentTab === 'sent' ? bid.coin_to : bid.coin_from}
-
${state.currentTab === 'sent' ? bid.amount_to : bid.amount_from}
-
${state.currentTab === 'sent' ? bid.coin_to : bid.coin_from}
+
${isSent ? bid.amount_to : bid.amount_from}
+
${isSent ? bid.coin_to : bid.coin_from}
@@ -1015,12 +1170,12 @@ const createTableRow = async (bid) => {
${state.currentTab === 'sent' ? bid.coin_from : bid.coin_to}
-
${state.currentTab === 'sent' ? bid.amount_from : bid.amount_to}
-
${state.currentTab === 'sent' ? bid.coin_from : bid.coin_to}
+
${isSent ? bid.amount_from : bid.amount_to}
+
${isSent ? bid.coin_from : bid.coin_to}
@@ -1045,10 +1200,9 @@ const createTableRow = async (bid) => { - @@ -1071,6 +1225,131 @@ const createTableRow = async (bid) => { `; }; +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); + } + }); + } + }); +} + +async function fetchBids(type = state.currentTab) { + if (type === 'all') { + return fetchAllBids(); + } + + try { + if (activeFetchController) { + activeFetchController.abort(); + } + activeFetchController = new AbortController(); + const endpoint = type === 'sent' ? '/json/sentbids' : '/json/bids'; + const withExpiredSelect = document.getElementById('with_expired'); + const includeExpired = withExpiredSelect ? withExpiredSelect.value === 'true' : true; + + //console.log(`Fetching ${type} 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 ${type} 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 fetch${type.charAt(0).toUpperCase() + type.slice(1)}Bids:`, error); + } + throw error; + } finally { + activeFetchController = null; + } +} + const updateTableContent = async (type) => { const tbody = elements[`${type}BidsBody`]; if (!tbody) return; @@ -1220,146 +1499,6 @@ const createTooltipForTrigger = (trigger) => { } }; -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; @@ -1369,7 +1508,13 @@ const updateBidsTable = async () => { state.isLoading = true; updateLoadingState(true); - const bids = await fetchBids(); + let bids; + + if (state.currentTab === 'all') { + bids = await fetchAllBids(); + } else { + bids = await fetchBids(); + } // Add identity preloading if we're searching if (state.filters.searchQuery && state.filters.searchQuery.length > 0) { @@ -1416,7 +1561,11 @@ const updatePaginationControls = (type) => { } if (currentPageSpan) { - currentPageSpan.textContent = totalPages > 0 ? state.currentPage[type] : 0; + if (totalPages > 0) { + currentPageSpan.innerHTML = `${state.currentPage[type]} of ${totalPages}`; + } else { + currentPageSpan.textContent = "0"; + } } if (prevButton) { @@ -1581,7 +1730,7 @@ function setupFilterEventListeners() { } const setupRefreshButtons = () => { - ['Sent', 'Received'].forEach(type => { + ['All', 'Sent', 'Received'].forEach(type => { const refreshButton = elements[`refresh${type}Bids`]; if (refreshButton) { EventManager.add(refreshButton, 'click', async () => { @@ -1597,30 +1746,35 @@ const setupRefreshButtons = () => { 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 (lowerType === 'all') { + state.data.all = await fetchAllBids(); + } else { + const response = await fetch(lowerType === '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}`); + 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; } - - const data = await response.json(); - if (!Array.isArray(data)) { - throw new Error('Invalid response format'); - } - - state.data[lowerType] = data; + await updateTableContent(lowerType); updatePaginationControls(lowerType); @@ -1648,8 +1802,10 @@ const switchTab = (tabId) => { tooltipIdsToCleanup.clear(); - state.currentTab = tabId === '#sent' ? 'sent' : 'received'; + state.currentTab = tabId === '#all' ? 'all' : + (tabId === '#sent' ? 'sent' : 'received'); + elements.allContent.classList.add('hidden'); elements.sentContent.classList.add('hidden'); elements.receivedContent.classList.add('hidden'); @@ -1669,11 +1825,31 @@ const switchTab = (tabId) => { tab.classList.add('hover:text-gray-600', 'hover:bg-gray-50', 'dark:hover:bg-gray-500'); } }); + setTimeout(() => { updateBidsTable(); }, 10); }; +window.switchTab = switchTab; + +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 }); + } +} + const setupEventListeners = () => { const filterControls = document.querySelector('.flex.flex-wrap.justify-center'); if (filterControls) { @@ -1708,10 +1884,12 @@ const setupEventListeners = () => { } }); + elements.allContent.classList.toggle('hidden', targetId !== '#all'); elements.sentContent.classList.toggle('hidden', targetId !== '#sent'); elements.receivedContent.classList.toggle('hidden', targetId !== '#received'); - state.currentTab = targetId === '#sent' ? 'sent' : 'received'; + state.currentTab = targetId === '#all' ? 'all' : + (targetId === '#sent' ? 'sent' : 'received'); state.currentPage[state.currentTab] = 1; if (window.TooltipManager) { @@ -1724,7 +1902,7 @@ const setupEventListeners = () => { }); } - ['Sent', 'Received'].forEach(type => { + ['All', 'Sent', 'Received'].forEach(type => { const lowerType = type.toLowerCase(); if (elements[`prevPage${type}`]) { @@ -1863,6 +2041,11 @@ function setupMemoryMonitoring() { window.TooltipManager.cleanup(); } + if (state.data.all.length > 1000) { + console.log('Trimming all bids data'); + state.data.all = state.data.all.slice(0, 1000); + } + if (state.data.sent.length > 1000) { console.log('Trimming sent bids data'); state.data.sent = state.data.sent.slice(0, 1000); @@ -1912,11 +2095,13 @@ function initialize() { setTimeout(() => { updateClearFiltersButton(); - state.currentTab = 'sent'; + state.currentTab = 'all'; state.filters.state = -1; updateBidsTable(); }, 100); + setupMemoryMonitoring(); + window.cleanupBidsTable = cleanup; } @@ -1925,74 +2110,3 @@ if (document.readyState === 'loading') { } else { initialize(); } - -(function() { - function handleBidsTabFromHash() { - if (window.location.pathname !== '/bids') { - return; - } - - const hash = window.location.hash; - - if (hash) { - const tabName = hash.substring(1); - let tabId; - switch (tabName.toLowerCase()) { - case 'sent': - tabId = '#sent'; - break; - case 'received': - tabId = '#received'; - break; - default: - tabId = '#sent'; - } - switchTab(tabId); - } else { - switchTab('#sent'); - } - } - - function switchTab(tabId) { - const targetTabBtn = document.querySelector(`[data-tabs-target="${tabId}"]`); - if (targetTabBtn) { - targetTabBtn.click(); - } - } - - function setupBidsTabNavigation() { - handleBidsTabFromHash(); - window.addEventListener('hashchange', handleBidsTabFromHash); - const originalSwitchTab = window.switchTab || null; - - window.switchTab = function(tabId) { - const newTabName = tabId.replace('#', ''); - if (window.location.hash !== `#${newTabName}`) { - history.replaceState(null, null, `#${newTabName}`); - } - if (originalSwitchTab && typeof originalSwitchTab === 'function') { - originalSwitchTab(tabId); - } else { - const targetTabBtn = document.querySelector(`[data-tabs-target="${tabId}"]`); - if (targetTabBtn) { - targetTabBtn.click(); - } - } - }; - - const tabButtons = document.querySelectorAll('[data-tabs-target]'); - tabButtons.forEach(btn => { - btn.addEventListener('click', function() { - const tabId = this.getAttribute('data-tabs-target'); - const tabName = tabId.replace('#', ''); - history.replaceState(null, null, `#${tabName}`); - }); - }); - } - - if (document.readyState === 'loading') { - document.addEventListener('DOMContentLoaded', setupBidsTabNavigation); - } else { - setupBidsTabNavigation(); - } -})(); diff --git a/basicswap/static/js/bids_sentreceived_export.js b/basicswap/static/js/bids_sentreceived_export.js index acee851..3021267 100644 --- a/basicswap/static/js/bids_sentreceived_export.js +++ b/basicswap/static/js/bids_sentreceived_export.js @@ -4,17 +4,18 @@ const BidExporter = { return 'No data to export'; } - const isSent = type === 'sent'; + const isAllTab = type === 'all'; const headers = [ 'Date/Time', 'Bid ID', 'Offer ID', 'From Address', - isSent ? 'You Send Amount' : 'You Receive Amount', - isSent ? 'You Send Coin' : 'You Receive Coin', - isSent ? 'You Receive Amount' : 'You Send Amount', - isSent ? 'You Receive Coin' : 'You Send Coin', + ...(isAllTab ? ['Type'] : []), + 'You Send Amount', + 'You Send Coin', + 'You Receive Amount', + 'You Receive Coin', 'Status', 'Created At', 'Expires At' @@ -23,11 +24,13 @@ const BidExporter = { let csvContent = headers.join(',') + '\n'; bids.forEach(bid => { + const isSent = isAllTab ? (bid.source === 'sent') : (type === 'sent'); const row = [ `"${formatTime(bid.created_at)}"`, `"${bid.bid_id}"`, `"${bid.offer_id}"`, `"${bid.addr_from}"`, + ...(isAllTab ? [`"${bid.source}"`] : []), isSent ? bid.amount_from : bid.amount_to, `"${isSent ? bid.coin_from : bid.coin_to}"`, isSent ? bid.amount_to : bid.amount_from, @@ -103,6 +106,15 @@ const BidExporter = { document.addEventListener('DOMContentLoaded', function() { setTimeout(function() { if (typeof state !== 'undefined' && typeof EventManager !== 'undefined') { + const exportAllButton = document.getElementById('exportAllBids'); + if (exportAllButton) { + EventManager.add(exportAllButton, 'click', (e) => { + e.preventDefault(); + state.currentTab = 'all'; + BidExporter.exportCurrentView(); + }); + } + const exportSentButton = document.getElementById('exportSentBids'); if (exportSentButton) { EventManager.add(exportSentButton, 'click', (e) => { @@ -128,9 +140,14 @@ const originalCleanup = window.cleanup || function(){}; window.cleanup = function() { originalCleanup(); + const exportAllButton = document.getElementById('exportAllBids'); const exportSentButton = document.getElementById('exportSentBids'); const exportReceivedButton = document.getElementById('exportReceivedBids'); + if (exportAllButton && typeof EventManager !== 'undefined') { + EventManager.remove(exportAllButton, 'click'); + } + if (exportSentButton && typeof EventManager !== 'undefined') { EventManager.remove(exportSentButton, 'click'); } diff --git a/basicswap/static/js/ui/bids-tab-navigation.js b/basicswap/static/js/ui/bids-tab-navigation.js index d1acb93..c88d732 100644 --- a/basicswap/static/js/ui/bids-tab-navigation.js +++ b/basicswap/static/js/ui/bids-tab-navigation.js @@ -1,36 +1,39 @@ (function() { 'use strict'; + const originalOnload = window.onload; - document.addEventListener('DOMContentLoaded', initBidsTabNavigation); - window.addEventListener('load', handleHashChange); - window.addEventListener('hashchange', preventScrollOnHashChange); + window.onload = function() { + if (typeof originalOnload === 'function') { + originalOnload(); + } + + setTimeout(function() { + initBidsTabNavigation(); + handleInitialNavigation(); + }, 100); + }; + + document.addEventListener('DOMContentLoaded', function() { + initBidsTabNavigation(); + }); + + window.addEventListener('hashchange', handleHashChange); + + window.bidsTabNavigationInitialized = false; function initBidsTabNavigation() { - const sentTabButton = document.getElementById('sent-tab'); - const receivedTabButton = document.getElementById('received-tab'); - - if (!sentTabButton || !receivedTabButton) { + if (window.bidsTabNavigationInitialized) { return; } - + document.querySelectorAll('.bids-tab-link').forEach(link => { link.addEventListener('click', function(e) { e.preventDefault(); e.stopPropagation(); - const targetTabId = this.getAttribute('data-tab-target'); - if (targetTabId) { if (window.location.pathname === '/bids') { - const oldScrollPosition = window.scrollY; - - activateTab(targetTabId); - - setTimeout(function() { - window.scrollTo(0, oldScrollPosition); - - history.replaceState(null, null, '#' + targetTabId.replace('#', '')); - }, 0); + navigateToTabDirectly(targetTabId); } else { localStorage.setItem('bidsTabToActivate', targetTabId.replace('#', '')); window.location.href = '/bids'; @@ -39,35 +42,28 @@ }); }); - const tabToActivate = localStorage.getItem('bidsTabToActivate'); - if (tabToActivate) { - localStorage.removeItem('bidsTabToActivate'); - activateTab('#' + tabToActivate); - } else if (window.location.pathname === '/bids' && !window.location.hash) { - activateTab('#sent'); - } + window.bidsTabNavigationInitialized = true; + console.log('Bids tab navigation initialized'); } - function preventScrollOnHashChange(e) { + function handleInitialNavigation() { if (window.location.pathname !== '/bids') { return; } + + const tabToActivate = localStorage.getItem('bidsTabToActivate'); - e.preventDefault(); - - const oldScrollPosition = window.scrollY; - const hash = window.location.hash; - - if (hash) { - const tabId = `#${hash.replace('#', '')}`; - activateTab(tabId); + if (tabToActivate) { + //console.log('Activating tab from localStorage:', tabToActivate); + localStorage.removeItem('bidsTabToActivate'); + activateTabWithRetry('#' + tabToActivate); + } else if (window.location.hash) { + //console.log('Activating tab from hash:', window.location.hash); + activateTabWithRetry(window.location.hash); } else { - activateTab('#sent'); + //console.log('Activating default tab: #all'); + activateTabWithRetry('#all'); } - - setTimeout(function() { - window.scrollTo(0, oldScrollPosition); - }, 0); } function handleHashChange() { @@ -75,50 +71,141 @@ return; } - const oldScrollPosition = window.scrollY; const hash = window.location.hash; - if (hash) { - const tabId = `#${hash.replace('#', '')}`; - activateTab(tabId); + //console.log('Hash changed, activating tab:', hash); + activateTabWithRetry(hash); } else { - activateTab('#sent'); + //console.log('Hash cleared, activating default tab: #all'); + activateTabWithRetry('#all'); } + } + + function activateTabWithRetry(tabId, retryCount = 0) { + const normalizedTabId = tabId.startsWith('#') ? tabId : '#' + tabId; + + if (normalizedTabId !== '#all' && normalizedTabId !== '#sent' && normalizedTabId !== '#received') { + //console.log('Invalid tab ID, defaulting to #all'); + activateTabWithRetry('#all'); + return; + } + + const tabButtonId = normalizedTabId === '#all' ? 'all-tab' : + (normalizedTabId === '#sent' ? 'sent-tab' : 'received-tab'); + const tabButton = document.getElementById(tabButtonId); + + if (!tabButton) { + if (retryCount < 5) { + //console.log('Tab button not found, retrying...', retryCount + 1); + setTimeout(() => { + activateTabWithRetry(normalizedTabId, retryCount + 1); + }, 100); + } else { + //console.error('Failed to find tab button after retries'); + } + return; + } + + //console.log('Activating tab:', normalizedTabId); + + tabButton.click(); + + if (window.Tabs) { + const tabsEl = document.querySelector('[data-tabs-toggle="#bidstab"]'); + if (tabsEl) { + const allTabs = Array.from(tabsEl.querySelectorAll('[role="tab"]')); + const targetTab = allTabs.find(tab => tab.getAttribute('data-tabs-target') === normalizedTabId); + + if (targetTab) { + + allTabs.forEach(tab => { + tab.setAttribute('aria-selected', tab === targetTab ? 'true' : 'false'); + + if (tab === targetTab) { + 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'); + } + }); + + const allContent = document.getElementById('all'); + const sentContent = document.getElementById('sent'); + const receivedContent = document.getElementById('received'); + + if (allContent && sentContent && receivedContent) { + allContent.classList.toggle('hidden', normalizedTabId !== '#all'); + sentContent.classList.toggle('hidden', normalizedTabId !== '#sent'); + receivedContent.classList.toggle('hidden', normalizedTabId !== '#received'); + } + } + } + } + + const allPanel = document.getElementById('all'); + const sentPanel = document.getElementById('sent'); + const receivedPanel = document.getElementById('received'); + + if (allPanel && sentPanel && receivedPanel) { + allPanel.classList.toggle('hidden', normalizedTabId !== '#all'); + sentPanel.classList.toggle('hidden', normalizedTabId !== '#sent'); + receivedPanel.classList.toggle('hidden', normalizedTabId !== '#received'); + } + + const newHash = normalizedTabId.replace('#', ''); + if (window.location.hash !== '#' + newHash) { + history.replaceState(null, null, '#' + newHash); + } + + triggerDataLoad(normalizedTabId); + } + + function triggerDataLoad(tabId) { + setTimeout(() => { + if (window.state) { + window.state.currentTab = tabId === '#all' ? 'all' : + (tabId === '#sent' ? 'sent' : 'received'); + + if (typeof window.updateBidsTable === 'function') { + //console.log('Triggering data load for', tabId); + window.updateBidsTable(); + } + } + + const event = new CustomEvent('tabactivated', { + detail: { + tabId: tabId, + type: tabId === '#all' ? 'all' : + (tabId === '#sent' ? 'sent' : 'received') + } + }); + document.dispatchEvent(event); + + if (window.TooltipManager && typeof window.TooltipManager.cleanup === 'function') { + setTimeout(() => { + window.TooltipManager.cleanup(); + if (typeof window.initializeTooltips === 'function') { + window.initializeTooltips(); + } + }, 200); + } + }, 100); + } + + function navigateToTabDirectly(tabId) { + const oldScrollPosition = window.scrollY; + + activateTabWithRetry(tabId); setTimeout(function() { window.scrollTo(0, oldScrollPosition); }, 0); } - function activateTab(tabId) { - if (tabId !== '#sent' && tabId !== '#received') { - tabId = '#sent'; - } - - const tabButtonId = tabId === '#sent' ? 'sent-tab' : 'received-tab'; - const tabButton = document.getElementById(tabButtonId); - - if (tabButton) { - const oldScrollPosition = window.scrollY; - - tabButton.click(); - - setTimeout(function() { - window.scrollTo(0, oldScrollPosition); - }, 0); - } - } - window.navigateToBidsTab = function(tabId) { if (window.location.pathname === '/bids') { - const oldScrollPosition = window.scrollY; - - activateTab('#' + (tabId === 'sent' || tabId === 'received' ? tabId : 'sent')); - - setTimeout(function() { - window.scrollTo(0, oldScrollPosition); - history.replaceState(null, null, '#' + tabId); - }, 0); + navigateToTabDirectly('#' + tabId); } else { localStorage.setItem('bidsTabToActivate', tabId); window.location.href = '/bids'; diff --git a/basicswap/templates/bids.html b/basicswap/templates/bids.html index 3cd6ebc..2bd172a 100644 --- a/basicswap/templates/bids.html +++ b/basicswap/templates/bids.html @@ -10,7 +10,7 @@
-

Sent Bids / Received Bids

+

All Bids / Sent Bids / Received Bids

View, and manage bids.

@@ -28,7 +28,12 @@
  • - +
  • +
  • +
  • @@ -167,7 +172,106 @@
    -
    + +
    +
    +
    +
    +
    +
    + + + + + + + + + + + + + +
    +
    + Date/Time +
    +
    +
    + You Send +
    +
    +
    + You Receive +
    +
    +
    + Status +
    +
    +
    + Actions +
    +
    +
    +
    +
    +
    +
    +
    + + Connecting... +
    +

    + All Bids: 0 +

    + {% if debug_ui_mode == true %} + + {% endif %} + + + +
    + +
    +
    +
    +
    +
    +
    +
    +
    + + +