JS/UI: Tooltips + Sorting table + Memory fix and new header. ()

* JS/UI: Tooltips + Sorting table + Memory fix and new header.

* LINT

* Light theme fix

* JS: Global / standalone Tooltips.

* Unminimized versions of tippy and popper js libs

* Formatting / Cleanup
This commit is contained in:
Gerlof van Ek 2025-01-30 13:16:41 +01:00 committed by GitHub
parent 4ae97790aa
commit 713577d868
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 6516 additions and 1543 deletions

View file

@ -578,10 +578,8 @@ class HttpHandler(BaseHTTPRequestHandler):
return page_offers(self, url_split, post_string, sent=True) return page_offers(self, url_split, post_string, sent=True)
if page == "bid": if page == "bid":
return page_bid(self, url_split, post_string) return page_bid(self, url_split, post_string)
if page == "receivedbids": if page == "bids":
return page_bids(self, url_split, post_string, received=True) return page_bids(self, url_split, post_string)
if page == "sentbids":
return page_bids(self, url_split, post_string, sent=True)
if page == "availablebids": if page == "availablebids":
return page_bids(self, url_split, post_string, available=True) return page_bids(self, url_split, post_string, available=True)
if page == "watched": if page == "watched":

View file

@ -1,153 +1,157 @@
/* General Styles */ /* General Styles */
.bold { .bold {
font-weight: bold; font-weight: bold;
} }
.monospace { .monospace {
font-family: monospace; font-family: monospace;
} }
.floatright { .floatright {
position: fixed; position: fixed;
top: 1.25rem; top: 1.25rem;
right: 1.25rem; right: 1.25rem;
z-index: 9999; z-index: 9999;
} }
/* Table Styles */ /* Table Styles */
.padded_row td { .padded_row td {
padding-top: 1.5em; padding-top: 1.5em;
} }
/* Modal Styles */ /* Modal Styles */
.modal-highest { .modal-highest {
z-index: 9999; z-index: 9999;
} }
/* Animation */ /* Animation */
#hide { #hide {
-moz-animation: cssAnimation 0s ease-in 15s forwards; -moz-animation: cssAnimation 0s ease-in 15s forwards;
-webkit-animation: cssAnimation 0s ease-in 15s forwards; -webkit-animation: cssAnimation 0s ease-in 15s forwards;
-o-animation: cssAnimation 0s ease-in 15s forwards; -o-animation: cssAnimation 0s ease-in 15s forwards;
animation: cssAnimation 0s ease-in 15s forwards; animation: cssAnimation 0s ease-in 15s forwards;
-webkit-animation-fill-mode: forwards; -webkit-animation-fill-mode: forwards;
animation-fill-mode: forwards; animation-fill-mode: forwards;
} }
@keyframes cssAnimation { @keyframes cssAnimation {
to { to {
width: 0; width: 0;
height: 0; height: 0;
overflow: hidden; overflow: hidden;
} }
} }
@-webkit-keyframes cssAnimation { @-webkit-keyframes cssAnimation {
to { to {
width: 0; width: 0;
height: 0; height: 0;
visibility: hidden; visibility: hidden;
} }
} }
/* Custom Select Styles */ /* Custom Select Styles */
.custom-select .select { .custom-select .select {
appearance: none; appearance: none;
background-image: url('/static/images/other/coin.png'); background-image: url('/static/images/other/coin.png');
background-position: 10px center; background-position: 10px center;
background-repeat: no-repeat; background-repeat: no-repeat;
position: relative; position: relative;
} }
.custom-select select::-webkit-scrollbar { .custom-select select::-webkit-scrollbar {
width: 0; width: 0;
} }
.custom-select .select option { .custom-select .select option {
padding-left: 0; padding-left: 0;
text-indent: 0; text-indent: 0;
background-repeat: no-repeat; background-repeat: no-repeat;
background-position: 0 50%; background-position: 0 50%;
} }
.custom-select .select option.no-space { .custom-select .select option.no-space {
padding-left: 0; padding-left: 0;
} }
.custom-select .select option[data-image] { .custom-select .select option[data-image] {
background-image: url(''); background-image: url('');
} }
.custom-select .select-icon { .custom-select .select-icon {
position: absolute; position: absolute;
top: 50%; top: 50%;
left: 10px; left: 10px;
transform: translateY(-50%); transform: translateY(-50%);
} }
.custom-select .select-image { .custom-select .select-image {
display: none; display: none;
margin-top: 10px; margin-top: 10px;
} }
.custom-select .select:focus + .select-dropdown .select-image { .custom-select .select:focus + .select-dropdown .select-image {
display: block; display: block;
} }
/* Blur and Overlay Styles */ /* Blur and Overlay Styles */
.blurred { .blurred {
filter: blur(3px); filter: blur(3px);
pointer-events: none; pointer-events: none;
user-select: none; user-select: none;
} }
.error-overlay.non-blurred { .error-overlay.non-blurred {
filter: none; filter: none;
pointer-events: auto; pointer-events: auto;
user-select: auto; user-select: auto;
} }
/* Form Element Styles */ /* Form Element Styles */
@media screen and (-webkit-min-device-pixel-ratio:0) { @media screen and (-webkit-min-device-pixel-ratio: 0) {
select:disabled, select:disabled,
input:disabled, input:disabled,
textarea:disabled { textarea:disabled {
opacity: 1 !important; opacity: 1 !important;
} }
} }
.error { .error {
border: 1px solid red !important; border: 1px solid red !important;
} }
/* Active Container Styles */ /* Active Container Styles */
.active-container { .active-container {
position: relative; position: relative;
border-radius: 10px; border-radius: 10px;
} }
.active-container::before { .active-container::before {
content: ""; content: "";
position: absolute; position: absolute;
top: 0; top: 0;
left: 0; left: 0;
right: 0; right: 0;
bottom: 0; bottom: 0;
border: 1px solid rgb(77, 132, 240); border: 1px solid rgb(77, 132, 240);
border-radius: inherit; border-radius: inherit;
pointer-events: none; pointer-events: none;
} }
/* Center Spin Animation */ /* Center Spin Animation */
.center-spin { .center-spin {
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
} }
@keyframes spin { @keyframes spin {
0% { transform: rotate(0deg); } 0% {
100% { transform: rotate(360deg); } transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
} }
/* Hover Container Styles */ /* Hover Container Styles */
@ -155,215 +159,209 @@
.hover-container:hover #coin_to, .hover-container:hover #coin_to,
.hover-container:hover #coin_from_button, .hover-container:hover #coin_from_button,
.hover-container:hover #coin_from { .hover-container:hover #coin_from {
border-color: #3b82f6; border-color: #3b82f6;
} }
#coin_to_button, #coin_from_button { #coin_to_button,
background-repeat: no-repeat; #coin_from_button {
background-position: center; background-repeat: no-repeat;
background-size: 20px 20px; background-position: center;
background-size: 20px 20px;
} }
/* Input-like Container Styles */ /* Input-like Container Styles */
.input-like-container { .input-like-container {
max-width: 100%; max-width: 100%;
background-color: #ffffff; background-color: #ffffff;
width: 360px; width: 360px;
padding: 1rem; padding: 1rem;
color: #374151; color: #374151;
border-radius: 0.375rem; border-radius: 0.375rem;
font-size: 0.875rem; font-size: 0.875rem;
line-height: 1.25rem; line-height: 1.25rem;
outline: none; outline: none;
word-wrap: break-word; word-wrap: break-word;
overflow-wrap: break-word; overflow-wrap: break-word;
word-break: break-all; word-break: break-all;
height: auto; height: auto;
min-height: 90px; min-height: 90px;
max-height: 150px; max-height: 150px;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
position: relative; position: relative;
overflow-y: auto; overflow-y: auto;
} }
.input-like-container.dark { .input-like-container.dark {
background-color: #374151; background-color: #374151;
color: #ffffff; color: #ffffff;
} }
.input-like-container.copying { .input-like-container.copying {
width: inherit; width: inherit;
} }
/* QR Code Styles */ /* QR Code Styles */
.qrcode { .qrcode {
position: relative; position: relative;
display: inline-block; display: inline-block;
padding: 10px; padding: 10px;
overflow: hidden; overflow: hidden;
} }
.qrcode-border { .qrcode-border {
border: 2px solid; border: 2px solid;
background-color: #ffffff; background-color: #ffffff;
border-radius: 0px; border-radius: 0px;
} }
.qrcode img { .qrcode img {
width: 100%; width: 100%;
height: auto; height: auto;
border-radius: 0px; border-radius: 0px;
} }
#showQR { #showQR {
position: absolute; position: absolute;
top: 50%; top: 50%;
left: 50%; left: 50%;
transform: translate(-50%, -50%); transform: translate(-50%, -50%);
height: 25px; height: 25px;
} }
.qrcode-container { .qrcode-container {
margin-top: 25px; margin-top: 25px;
} }
/* Disabled Element Styles */ /* Disabled Element Styles */
select.select-disabled, select.select-disabled,
.disabled-input-enabled, .disabled-input-enabled,
select.disabled-select-enabled { select.disabled-select-enabled {
opacity: 0.40 !important; opacity: 0.40 !important;
} }
/* Shutdown Modal Styles */ /* Shutdown Modal Styles */
#shutdownModal { #shutdownModal {
z-index: 50; z-index: 50;
} }
#shutdownModal > div:first-child { #shutdownModal > div:first-child {
z-index: 40; z-index: 40;
} }
#shutdownModal > div:last-child { #shutdownModal > div:last-child {
z-index: 50; z-index: 50;
} }
#shutdownModal > div { #shutdownModal > div {
transition: opacity 0.3s ease-out; transition: opacity 0.3s ease-out;
} }
#shutdownModal.hidden > div { #shutdownModal.hidden > div {
opacity: 0; opacity: 0;
} }
#shutdownModal:not(.hidden) > div { #shutdownModal:not(.hidden) > div {
opacity: 1; opacity: 1;
} }
.shutdown-button { .shutdown-button {
transition: all 0.3s ease; transition: all 0.3s ease;
} }
.shutdown-button.shutdown-disabled { .shutdown-button.shutdown-disabled {
opacity: 0.6; opacity: 0.6;
cursor: not-allowed; cursor: not-allowed;
color: #a0aec0; color: #a0aec0;
} }
.shutdown-button.shutdown-disabled:hover { .shutdown-button.shutdown-disabled:hover {
background-color: #4a5568; background-color: #4a5568;
} }
.shutdown-button.shutdown-disabled svg { .shutdown-button.shutdown-disabled svg {
opacity: 0.5; opacity: 0.5;
} }
/* Loading Line Animation */
/* Loading line animation */
.loading-line { .loading-line {
width: 100%; width: 100%;
height: 2px; height: 2px;
background-color: #ccc; background-color: #ccc;
overflow: hidden; overflow: hidden;
position: relative; position: relative;
} }
.loading-line::before { .loading-line::before {
content: ''; content: '';
display: block; display: block;
width: 100%; width: 100%;
height: 100%; height: 100%;
background: linear-gradient(to right, transparent, #007bff, transparent); background: linear-gradient(to right, transparent, #007bff, transparent);
animation: loading 1.5s infinite; animation: loading 1.5s infinite;
} }
@keyframes loading { @keyframes loading {
0% { 0% {
transform: translateX(-100%); transform: translateX(-100%);
} }
100% { 100% {
transform: translateX(100%); transform: translateX(100%);
} }
} }
/* Hide the loading line once data is loaded */
.usd-value:not(.loading) .loading-line, .usd-value:not(.loading) .loading-line,
.profit-loss:not(.loading) .loading-line { .profit-loss:not(.loading) .loading-line {
display: none; display: none;
} }
.resolution-button { /* Resolution Button Styles */
.resolution-button {
background: none; background: none;
border: none; border: none;
color: #4B5563; /* gray-600 */ color: #4B5563;
font-size: 0.875rem; /* text-sm */ font-size: 0.875rem;
font-weight: 500; /* font-medium */ font-weight: 500;
padding: 0.25rem 0.5rem; padding: 0.25rem 0.5rem;
border-radius: 0.25rem; border-radius: 0.25rem;
transition: all 0.2s; transition: all 0.2s;
outline: 2px solid transparent; outline: 2px solid transparent;
outline-offset: 2px; outline-offset: 2px;
} }
.resolution-button:hover { .resolution-button:hover {
color: #1F2937; /* gray-800 */ color: #1F2937;
} }
.resolution-button:focus { .resolution-button:focus {
outline: 2px solid #3B82F6; /* blue-500 */ outline: 2px solid #3B82F6;
} }
.resolution-button.active { .resolution-button.active {
color: #3B82F6; /* blue-500 */ color: #3B82F6;
outline: 2px solid #3B82F6; /* blue-500 */ outline: 2px solid #3B82F6;
} }
.dark .resolution-button { .dark .resolution-button {
color: #9CA3AF; /* gray-400 */ color: #9CA3AF;
} }
.dark .resolution-button:hover { .dark .resolution-button:hover {
color: #F3F4F6; /* gray-100 */ color: #F3F4F6;
} }
.dark .resolution-button.active { .dark .resolution-button.active {
color: #60A5FA; /* blue-400 */ color: #60A5FA;
outline-color: #60A5FA; /* blue-400 */ outline-color: #60A5FA;
color: #fff; color: #fff;
}
#toggle-volume.active {
@apply bg-green-500 hover:bg-green-600 focus:ring-green-300;
}
#toggle-auto-refresh[data-enabled="true"] {
@apply bg-green-500 hover:bg-green-600 focus:ring-green-300;
}
[data-popper-placement] {
will-change: transform;
transform: translateZ(0);
} }
.tooltip { /* Toggle Button Styles */
backface-visibility: hidden; #toggle-volume.active {
-webkit-backface-visibility: hidden; @apply bg-green-500 hover:bg-green-600 focus:ring-green-300;
} }
#toggle-auto-refresh[data-enabled="true"] {
@apply bg-green-500 hover:bg-green-600 focus:ring-green-300;
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -1,3 +1,4 @@
// EVENT MANAGER
const EventManager = { const EventManager = {
listeners: new Map(), listeners: new Map(),
@ -72,12 +73,11 @@ let currentSortDirection = 'desc';
let filterTimeout = null; let filterTimeout = null;
// CONFIGURATION CONSTANTS // CONFIGURATION CONSTANTS
// TIME CONSTANTS
// Time Constants
const CACHE_DURATION = 10 * 60 * 1000; const CACHE_DURATION = 10 * 60 * 1000;
// Application Constants // APP CONSTANTS
const itemsPerPage = 100; const itemsPerPage = 50;
const isSentOffers = window.offersTableConfig.isSentOffers; const isSentOffers = window.offersTableConfig.isSentOffers;
const offersConfig = { const offersConfig = {
@ -142,6 +142,7 @@ const totalPagesSpan = document.getElementById('totalPages');
const lastRefreshTimeSpan = document.getElementById('lastRefreshTime'); const lastRefreshTimeSpan = document.getElementById('lastRefreshTime');
const newEntriesCountSpan = document.getElementById('newEntriesCount'); const newEntriesCountSpan = document.getElementById('newEntriesCount');
// MANAGER OBJECTS // MANAGER OBJECTS
const WebSocketManager = { const WebSocketManager = {
ws: null, ws: null,
@ -248,7 +249,6 @@ const WebSocketManager = {
this.connectionState.lastConnectAttempt = Date.now(); this.connectionState.lastConnectAttempt = Date.now();
try { try {
const config = getWebSocketConfig();
const wsPort = config.port || window.ws_port || '11700'; const wsPort = config.port || window.ws_port || '11700';
if (!wsPort) { if (!wsPort) {
@ -349,7 +349,6 @@ const WebSocketManager = {
requestAnimationFrame(() => { requestAnimationFrame(() => {
updateOffersTable(); updateOffersTable();
updateJsonView();
updatePaginationInfo(); updatePaginationInfo();
}); });
@ -641,14 +640,13 @@ const CacheManager = {
window.CacheManager = CacheManager; window.CacheManager = CacheManager;
// Identity cache management // IDENTITY CACHE MANAGEMENT
const IdentityManager = { const IdentityManager = {
cache: new Map(), cache: new Map(),
pendingRequests: new Map(), pendingRequests: new Map(),
retryDelay: 2000, retryDelay: 2000,
maxRetries: 3, maxRetries: 3,
cacheTimeout: 5 * 60 * 1000, // 5 minutes cacheTimeout: 5 * 60 * 1000,
async getIdentityData(address) { async getIdentityData(address) {
const cachedData = this.getCachedIdentity(address); const cachedData = this.getCachedIdentity(address);
@ -849,22 +847,13 @@ function continueInitialization() {
listingLabel.textContent = isSentOffers ? 'Total Listings: ' : 'Network Listings: '; listingLabel.textContent = isSentOffers ? 'Total Listings: ' : 'Network Listings: ';
} }
//console.log('Initialization completed'); //console.log('Initialization completed');
} }
function initializeTooltips() { function initializeTooltips() {
if (typeof Tooltip === 'undefined') { if (window.TooltipManager) {
console.warn('Tooltip is not defined. Make sure the required library is loaded.'); window.TooltipManager.initializeTooltips();
return;
} }
const tooltipElements = document.querySelectorAll('[data-tooltip-target]');
tooltipElements.forEach((el) => {
const tooltipId = el.getAttribute('data-tooltip-target');
const tooltipElement = document.getElementById(tooltipId);
if (tooltipElement) {
new Tooltip(tooltipElement, el);
}
});
} }
// DATA PROCESSING FUNCTIONS // DATA PROCESSING FUNCTIONS
@ -880,114 +869,105 @@ function getValidOffers() {
} }
function filterAndSortData() { function filterAndSortData() {
//console.log('[Debug] Starting filter with data length:', originalJsonData.length); const formData = new FormData(filterForm);
const filters = Object.fromEntries(formData);
const formData = new FormData(filterForm); localStorage.setItem('offersTableSettings', JSON.stringify({
const filters = Object.fromEntries(formData); coin_to: filters.coin_to,
//console.log('[Debug] Active filters:', filters); coin_from: filters.coin_from,
status: filters.status,
sent_from: filters.sent_from,
sortColumn: currentSortColumn,
sortDirection: currentSortDirection
}));
if (filters.coin_to !== 'any') { if (filters.coin_to !== 'any') {
filters.coin_to = coinIdToName[filters.coin_to] || filters.coin_to; filters.coin_to = coinIdToName[filters.coin_to] || filters.coin_to;
} }
if (filters.coin_from !== 'any') { if (filters.coin_from !== 'any') {
filters.coin_from = coinIdToName[filters.coin_from] || filters.coin_from; filters.coin_from = coinIdToName[filters.coin_from] || filters.coin_from;
} }
let filteredData = [...originalJsonData]; let filteredData = [...originalJsonData];
const sentFromFilter = filters.sent_from || 'any'; const sentFromFilter = filters.sent_from || 'any';
filteredData = filteredData.filter(offer => {
if (sentFromFilter === 'public') return offer.is_public;
if (sentFromFilter === 'private') return !offer.is_public;
return true;
});
filteredData = filteredData.filter(offer => { filteredData = filteredData.filter(offer => {
if (sentFromFilter === 'public') { if (!isSentOffers && isOfferExpired(offer)) return false;
return offer.is_public;
} else if (sentFromFilter === 'private') {
return !offer.is_public;
}
return true;
});
filteredData = filteredData.filter(offer => { const coinFrom = (offer.coin_from || '').toLowerCase();
if (!isSentOffers && isOfferExpired(offer)) { const coinTo = (offer.coin_to || '').toLowerCase();
return false;
}
const coinFrom = (offer.coin_from || '').toLowerCase(); if (filters.coin_to !== 'any' && !coinMatches(coinTo, filters.coin_to)) return false;
const coinTo = (offer.coin_to || '').toLowerCase(); if (filters.coin_from !== 'any' && !coinMatches(coinFrom, filters.coin_from)) return false;
if (filters.coin_to !== 'any') { if (isSentOffers && filters.status && filters.status !== 'any') {
if (!coinMatches(coinTo, filters.coin_to)) { const isExpired = offer.expire_at <= Math.floor(Date.now() / 1000);
return false; const isRevoked = Boolean(offer.is_revoked);
}
}
if (filters.coin_from !== 'any') { switch (filters.status) {
if (!coinMatches(coinFrom, filters.coin_from)) { case 'active': return !isExpired && !isRevoked;
return false; case 'expired': return isExpired && !isRevoked;
} case 'revoked': return isRevoked;
} }
}
return true;
});
if (isSentOffers && filters.status && filters.status !== 'any') { if (currentSortColumn !== null) {
const currentTime = Math.floor(Date.now() / 1000); const priceCache = new Map();
const isExpired = offer.expire_at <= currentTime; const getPrice = coin => {
const isRevoked = Boolean(offer.is_revoked); if (priceCache.has(coin)) return priceCache.get(coin);
const symbol = coin === 'Firo' || coin === 'Zcoin' ? 'zcoin' :
coin === 'Bitcoin Cash' ? 'bitcoin-cash' :
coin.includes('Particl') ? 'particl' :
coin.toLowerCase();
const price = latestPrices[symbol]?.usd || 0;
priceCache.set(coin, price);
return price;
};
switch (filters.status) { const calculateValue = offer => {
case 'active': const fromUSD = parseFloat(offer.amount_from) * getPrice(offer.coin_from);
return !isExpired && !isRevoked; const toUSD = parseFloat(offer.amount_to) * getPrice(offer.coin_to);
case 'expired': return (isSentOffers || offer.is_own_offer) ?
return isExpired && !isRevoked; ((toUSD / fromUSD) - 1) * 100 :
case 'revoked': ((fromUSD / toUSD) - 1) * 100;
return isRevoked; };
default:
return true;
}
}
return true; const sortValues = new Map();
}); if (currentSortColumn === 5 || currentSortColumn === 6) {
filteredData.forEach(offer => {
sortValues.set(offer.offer_id, calculateValue(offer));
});
}
if (currentSortColumn !== null) { filteredData.sort((a, b) => {
filteredData.sort((a, b) => { let comparison;
let comparison = 0; switch(currentSortColumn) {
case 0: // Time
switch(currentSortColumn) { comparison = a.created_at - b.created_at;
case 0: // Time break;
comparison = a.created_at - b.created_at; case 5: // Rate
break; case 6: // Market +/-
case 5: // Rate comparison = sortValues.get(a.offer_id) - sortValues.get(b.offer_id);
comparison = parseFloat(a.rate) - parseFloat(b.rate); break;
break; case 7: // Trade
case 6: // Market +/- comparison = a.offer_id.localeCompare(b.offer_id);
const aFromSymbol = getCoinSymbolLowercase(a.coin_from); break;
const aToSymbol = getCoinSymbolLowercase(a.coin_to); default:
const bFromSymbol = getCoinSymbolLowercase(b.coin_from); comparison = 0;
const bToSymbol = getCoinSymbolLowercase(b.coin_to); }
return currentSortDirection === 'desc' ? -comparison : comparison;
const aFromPrice = latestPrices[aFromSymbol]?.usd || 0; });
const aToPrice = latestPrices[aToSymbol]?.usd || 0; }
const bFromPrice = latestPrices[bFromSymbol]?.usd || 0;
const bToPrice = latestPrices[bToSymbol]?.usd || 0; return filteredData;
const aMarketRate = aToPrice / aFromPrice;
const bMarketRate = bToPrice / bFromPrice;
const aOfferedRate = parseFloat(a.rate);
const bOfferedRate = parseFloat(b.rate);
const aPercentDiff = ((aOfferedRate - aMarketRate) / aMarketRate) * 100;
const bPercentDiff = ((bOfferedRate - bMarketRate) / bMarketRate) * 100;
comparison = aPercentDiff - bPercentDiff;
break;
case 7: // Trade
comparison = a.offer_id.localeCompare(b.offer_id);
break;
}
return currentSortDirection === 'desc' ? -comparison : comparison;
});
}
//console.log(`[Debug] Filtered data length: ${filteredData.length}`);
return filteredData;
} }
async function calculateProfitLoss(fromCoin, toCoin, fromAmount, toAmount, isOwnOffer) { async function calculateProfitLoss(fromCoin, toCoin, fromAmount, toAmount, isOwnOffer) {
@ -1191,7 +1171,6 @@ async function fetchOffers() {
originalJsonData = [...jsonData]; originalJsonData = [...jsonData];
await updateOffersTable(); await updateOffersTable();
updateJsonView();
updatePaginationInfo(); updatePaginationInfo();
} catch (error) { } catch (error) {
@ -1280,10 +1259,6 @@ function updateRowTimes() {
}); });
} }
function updateJsonView() {
jsonContent.textContent = JSON.stringify(jsonData, null, 2);
}
function updateLastRefreshTime() { function updateLastRefreshTime() {
if (lastRefreshTimeSpan) { if (lastRefreshTimeSpan) {
lastRefreshTimeSpan.textContent = lastRefreshTime ? new Date(lastRefreshTime).toLocaleTimeString() : 'Never'; lastRefreshTimeSpan.textContent = lastRefreshTime ? new Date(lastRefreshTime).toLocaleTimeString() : 'Never';
@ -1428,28 +1403,26 @@ function updateClearFiltersButton() {
} }
function cleanupRow(row) { function cleanupRow(row) {
EventManager.removeAll(row); const tooltipTriggers = row.querySelectorAll('[data-tooltip-trigger-id]');
tooltipTriggers.forEach(trigger => {
const tooltips = row.querySelectorAll('[data-tooltip-target]'); if (window.TooltipManager) {
tooltips.forEach(tooltip => { window.TooltipManager.destroy(trigger);
const tooltipId = tooltip.getAttribute('data-tooltip-target');
const tooltipElement = document.getElementById(tooltipId);
if (tooltipElement) {
tooltipElement.remove();
} }
}); });
EventManager.removeAll(row);
} }
function cleanupTable() { function cleanupTable() {
EventManager.clearAll(); EventManager.clearAll();
if (offersBody) { if (offersBody) {
const existingRows = offersBody.querySelectorAll('tr'); const existingRows = offersBody.querySelectorAll('tr');
existingRows.forEach(row => { existingRows.forEach(row => cleanupRow(row));
cleanupRow(row);
});
offersBody.innerHTML = ''; offersBody.innerHTML = '';
} }
if (window.TooltipManager) {
window.TooltipManager.cleanup();
}
} }
function handleNoOffersScenario() { function handleNoOffersScenario() {
@ -1490,6 +1463,10 @@ function handleNoOffersScenario() {
async function updateOffersTable() { async function updateOffersTable() {
try { try {
if (window.TooltipManager) {
window.TooltipManager.cleanup();
}
const PRICES_CACHE_KEY = 'prices_coingecko'; const PRICES_CACHE_KEY = 'prices_coingecko';
const cachedPrices = CacheManager.get(PRICES_CACHE_KEY); const cachedPrices = CacheManager.get(PRICES_CACHE_KEY);
latestPrices = cachedPrices?.value || getEmptyPriceData(); latestPrices = cachedPrices?.value || getEmptyPriceData();
@ -1510,16 +1487,13 @@ async function updateOffersTable() {
const BATCH_SIZE = 5; const BATCH_SIZE = 5;
const identities = []; const identities = [];
for (let i = 0; i < itemsToDisplay.length; i += BATCH_SIZE) { for (let i = 0; i < itemsToDisplay.length; i += BATCH_SIZE) {
const batch = itemsToDisplay.slice(i, i + BATCH_SIZE); const batch = itemsToDisplay.slice(i, i + BATCH_SIZE);
const batchPromises = batch.map(offer => const batchPromises = batch.map(offer =>
offer.addr_from ? IdentityManager.getIdentityData(offer.addr_from) : Promise.resolve(null) offer.addr_from ? IdentityManager.getIdentityData(offer.addr_from) : Promise.resolve(null)
); );
const batchResults = await Promise.all(batchPromises); const batchResults = await Promise.all(batchPromises);
identities.push(...batchResults); identities.push(...batchResults);
if (i + BATCH_SIZE < itemsToDisplay.length) { if (i + BATCH_SIZE < itemsToDisplay.length) {
await new Promise(resolve => setTimeout(resolve, 100)); await new Promise(resolve => setTimeout(resolve, 100));
} }
@ -1533,13 +1507,12 @@ async function updateOffersTable() {
const totalPages = Math.max(1, Math.ceil(validOffers.length / itemsPerPage)); const totalPages = Math.max(1, Math.ceil(validOffers.length / itemsPerPage));
currentPage = Math.min(currentPage, totalPages); currentPage = Math.min(currentPage, totalPages);
const fragment = document.createDocumentFragment();
const existingRows = offersBody.querySelectorAll('tr'); const existingRows = offersBody.querySelectorAll('tr');
existingRows.forEach(row => { existingRows.forEach(row => {
cleanupRow(row); cleanupRow(row);
}); });
const fragment = document.createDocumentFragment();
itemsToDisplay.forEach((offer, index) => { itemsToDisplay.forEach((offer, index) => {
const identity = identities[index]; const identity = identities[index];
const row = createTableRow(offer, identity); const row = createTableRow(offer, identity);
@ -1551,11 +1524,21 @@ async function updateOffersTable() {
offersBody.textContent = ''; offersBody.textContent = '';
offersBody.appendChild(fragment); offersBody.appendChild(fragment);
const tooltipTriggers = offersBody.querySelectorAll('[data-tooltip-target]');
tooltipTriggers.forEach(trigger => {
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'
});
}
});
requestAnimationFrame(() => { requestAnimationFrame(() => {
initializeTooltips();
updateRowTimes(); updateRowTimes();
updatePaginationControls(totalPages); updatePaginationControls(totalPages);
if (tableRateModule?.initializeTable) { if (tableRateModule?.initializeTable) {
tableRateModule.initializeTable(); tableRateModule.initializeTable();
} }
@ -1571,7 +1554,6 @@ async function updateOffersTable() {
try { try {
const cachedOffers = CacheManager.get('offers_cached'); const cachedOffers = CacheManager.get('offers_cached');
if (cachedOffers?.value) { if (cachedOffers?.value) {
console.log('Recovering with cached offers data');
jsonData = cachedOffers.value; jsonData = cachedOffers.value;
updateOffersTable(); updateOffersTable();
} }
@ -1650,65 +1632,65 @@ function getIdentityInfo(address, identity) {
} }
function createTableRow(offer, identity = null) { function createTableRow(offer, identity = null) {
const row = document.createElement('tr'); const row = document.createElement('tr');
const uniqueId = `${offer.offer_id}_${offer.created_at}`; const uniqueId = `${offer.offer_id}_${offer.created_at}`;
row.className = 'relative opacity-100 text-gray-500 dark:text-gray-100 hover:bg-coolGray-200 dark:hover:bg-gray-600'; row.className = 'relative opacity-100 text-gray-500 dark:text-gray-100 hover:bg-coolGray-200 dark:hover:bg-gray-600';
row.setAttribute('data-offer-id', uniqueId); row.setAttribute('data-offer-id', uniqueId);
const { const {
coin_from: coinFrom, coin_from: coinFrom,
coin_to: coinTo, coin_to: coinTo,
created_at: createdAt, created_at: createdAt,
expire_at: expireAt, expire_at: expireAt,
amount_from: amountFrom, amount_from: amountFrom,
amount_to: amountTo, amount_to: amountTo,
is_own_offer: isOwnOffer, is_own_offer: isOwnOffer,
is_revoked: isRevoked, is_revoked: isRevoked,
is_public: isPublic is_public: isPublic
} = offer; } = offer;
const coinFromSymbol = coinNameToSymbol[coinFrom] || coinFrom.toLowerCase(); const coinFromSymbol = coinNameToSymbol[coinFrom] || coinFrom.toLowerCase();
const coinToSymbol = coinNameToSymbol[coinTo] || coinTo.toLowerCase(); const coinToSymbol = coinNameToSymbol[coinTo] || coinTo.toLowerCase();
const coinFromDisplay = getDisplayName(coinFrom); const coinFromDisplay = getDisplayName(coinFrom);
const coinToDisplay = getDisplayName(coinTo); const coinToDisplay = getDisplayName(coinTo);
const postedTime = formatTime(createdAt, true); const postedTime = formatTime(createdAt, true);
const expiresIn = formatTime(expireAt); const expiresIn = formatTime(expireAt);
const currentTime = Math.floor(Date.now() / 1000); const currentTime = Math.floor(Date.now() / 1000);
const isActuallyExpired = currentTime > expireAt; const isActuallyExpired = currentTime > expireAt;
const fromAmount = parseFloat(amountFrom) || 0; const fromAmount = parseFloat(amountFrom) || 0;
const toAmount = parseFloat(amountTo) || 0; const toAmount = parseFloat(amountTo) || 0;
row.innerHTML = ` row.innerHTML = `
${!isPublic ? createPrivateIndicator() : '<td class="w-0 p-0 m-0"></td>'} ${!isPublic ? createPrivateIndicator() : '<td class="w-0 p-0 m-0"></td>'}
${createTimeColumn(offer, postedTime, expiresIn)} ${createTimeColumn(offer, postedTime, expiresIn)}
${createDetailsColumn(offer, identity)} ${createDetailsColumn(offer, identity)}
${createTakerAmountColumn(offer, coinTo, coinFrom)} ${createTakerAmountColumn(offer, coinTo, coinFrom)}
${createSwapColumn(offer, coinFromDisplay, coinToDisplay, coinFromSymbol, coinToSymbol)} ${createSwapColumn(offer, coinFromDisplay, coinToDisplay, coinFromSymbol, coinToSymbol)}
${createOrderbookColumn(offer, coinFrom, coinTo)} ${createOrderbookColumn(offer, coinFrom, coinTo)}
${createRateColumn(offer, coinFrom, coinTo)} ${createRateColumn(offer, coinFrom, coinTo)}
${createPercentageColumn(offer)} ${createPercentageColumn(offer)}
${createActionColumn(offer, isActuallyExpired)} ${createActionColumn(offer, isActuallyExpired)}
${createTooltips( ${createTooltips(
offer, offer,
isOwnOffer, isOwnOffer,
coinFrom, coinFrom,
coinTo, coinTo,
fromAmount, fromAmount,
toAmount, toAmount,
postedTime, postedTime,
expiresIn, expiresIn,
isActuallyExpired, isActuallyExpired,
Boolean(isRevoked), Boolean(isRevoked),
identity identity
)} )}
`; `;
updateTooltipTargets(row, uniqueId); updateTooltipTargets(row, uniqueId);
updateProfitLoss(row, coinFrom, coinTo, fromAmount, toAmount, isOwnOffer); updateProfitLoss(row, coinFrom, coinTo, fromAmount, toAmount, isOwnOffer);
return row; return row;
} }
function createPrivateIndicator() { function createPrivateIndicator() {
@ -2090,7 +2072,7 @@ function createRecipientTooltip(uniqueId, identityInfo, identity, successRate, t
` : ''} ` : ''}
${identity ? ` ${identity ? `
<div class="border-t border-gray-400 pt-2 mt-2"> <div class= pt-2 mt-2">
<div class="text-white text-xs tracking-wide font-semibold mb-2">Swap History:</div> <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="grid grid-cols-2 gap-2">
<div class="text-center p-2 bg-gray-500 rounded-md"> <div class="text-center p-2 bg-gray-500 rounded-md">
@ -2299,7 +2281,6 @@ function applyFilters() {
filterTimeout = setTimeout(() => { filterTimeout = setTimeout(() => {
jsonData = filterAndSortData(); jsonData = filterAndSortData();
updateOffersTable(); updateOffersTable();
updateJsonView();
updatePaginationInfo(); updatePaginationInfo();
updateClearFiltersButton(); updateClearFiltersButton();
filterTimeout = null; filterTimeout = null;
@ -2317,7 +2298,6 @@ function clearFilters() {
const selectElements = filterForm.querySelectorAll('select'); const selectElements = filterForm.querySelectorAll('select');
selectElements.forEach(select => { selectElements.forEach(select => {
select.value = 'any'; select.value = 'any';
// Trigger change event
const event = new Event('change', { bubbles: true }); const event = new Event('change', { bubbles: true });
select.dispatchEvent(event); select.dispatchEvent(event);
}); });
@ -2331,7 +2311,6 @@ function clearFilters() {
currentPage = 1; currentPage = 1;
updateOffersTable(); updateOffersTable();
updateJsonView();
updateCoinFilterImages(); updateCoinFilterImages();
updateClearFiltersButton(); updateClearFiltersButton();
} }
@ -2421,7 +2400,7 @@ function isOfferExpired(offer) {
const currentTime = Math.floor(Date.now() / 1000); const currentTime = Math.floor(Date.now() / 1000);
const isExpired = offer.expire_at <= currentTime; const isExpired = offer.expire_at <= currentTime;
if (isExpired) { if (isExpired) {
console.log(`Offer ${offer.offer_id} is expired. Expire time: ${offer.expire_at}, Current time: ${currentTime}`); // console.log(`Offer ${offer.offer_id} is expired. Expire time: ${offer.expire_at}, Current time: ${currentTime}`);
} }
return isExpired; return isExpired;
} }
@ -2582,7 +2561,6 @@ if (refreshButton) {
} else { } else {
throw new Error('Unable to fetch price data'); throw new Error('Unable to fetch price data');
} }
updateJsonView();
updatePaginationInfo(); updatePaginationInfo();
lastRefreshTime = now; lastRefreshTime = now;
updateLastRefreshTime(); updateLastRefreshTime();
@ -2653,29 +2631,50 @@ function handleTableSort(columnIndex, header) {
currentSortDirection = 'desc'; currentSortDirection = 'desc';
} }
document.querySelectorAll('.sort-icon').forEach(icon => { localStorage.setItem('offersTableSettings', JSON.stringify({
icon.classList.remove('text-blue-500'); coin_to: document.getElementById('coin_to').value,
icon.textContent = '↓'; coin_from: document.getElementById('coin_from').value,
}); status: document.getElementById('status')?.value || 'any',
sent_from: document.getElementById('sent_from').value,
const sortIcon = document.getElementById(`sort-icon-${columnIndex}`); sortColumn: currentSortColumn,
if (sortIcon) { sortDirection: currentSortDirection
sortIcon.textContent = currentSortDirection === 'asc' ? '↑' : '↓'; }));
sortIcon.classList.add('text-blue-500');
}
document.querySelectorAll('th[data-sortable="true"]').forEach(th => { document.querySelectorAll('th[data-sortable="true"]').forEach(th => {
if (th === header) { const columnSpan = th.querySelector('span:not(.sort-icon)');
th.classList.add('text-blue-500'); const icon = th.querySelector('.sort-icon');
const thisColumnIndex = parseInt(th.getAttribute('data-column-index'));
if (thisColumnIndex === columnIndex) {
if (columnSpan) {
columnSpan.classList.remove('text-gray-600', 'dark:text-gray-300');
columnSpan.classList.add('text-blue-500', 'dark:text-blue-500');
}
if (icon) {
icon.classList.remove('text-gray-600', 'dark:text-gray-400');
icon.classList.add('text-blue-500', 'dark:text-blue-500');
icon.textContent = currentSortDirection === 'asc' ? '↑' : '↓';
}
} else { } else {
th.classList.remove('text-blue-500'); if (columnSpan) {
columnSpan.classList.remove('text-blue-500', 'dark:text-blue-500');
columnSpan.classList.add('text-gray-600', 'dark:text-gray-300');
}
if (icon) {
icon.classList.remove('text-blue-500', 'dark:text-blue-500');
icon.classList.add('text-gray-600', 'dark:text-gray-400');
icon.textContent = '↓';
}
} }
}); });
localStorage.setItem('tableSortColumn', currentSortColumn); if (window.sortTimeout) {
localStorage.setItem('tableSortDirection', currentSortDirection); clearTimeout(window.sortTimeout);
}
applyFilters();
window.sortTimeout = setTimeout(() => {
applyFilters();
}, 100);
} }
// TIMER MANAGEMENT // TIMER MANAGEMENT
@ -2713,72 +2712,93 @@ const timerManager = {
// INITIALIZATION AND EVENT BINDING // INITIALIZATION AND EVENT BINDING
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
console.log('DOM content loaded, initializing...'); const saved = localStorage.getItem('offersTableSettings');
console.log('View type:', isSentOffers ? 'sent offers' : 'received offers'); if (saved) {
const settings = JSON.parse(saved);
updateClearFiltersButton(); ['coin_to', 'coin_from', 'status', 'sent_from'].forEach(id => {
initializeTableEvents(); const element = document.getElementById(id);
if (element && settings[id]) element.value = settings[id];
});
setTimeout(() => {
console.log('Starting WebSocket initialization...');
WebSocketManager.initialize();
}, 1000);
if (initializeTableRateModule()) { if (settings.sortColumn !== undefined) {
continueInitialization(); currentSortColumn = settings.sortColumn;
} else { currentSortDirection = settings.sortDirection;
let retryCount = 0;
const maxRetries = 5;
const retryInterval = setInterval(() => {
retryCount++;
if (initializeTableRateModule()) {
clearInterval(retryInterval);
continueInitialization();
} else if (retryCount >= maxRetries) {
clearInterval(retryInterval);
continueInitialization();
}
}, 1000);
}
timerManager.addInterval(() => {
if (WebSocketManager.isConnected()) {
console.log('WebSocket Status: Connected');
}
}, 30000);
timerManager.addInterval(() => { document.querySelectorAll('.sort-icon').forEach(icon => {
CacheManager.cleanup(); icon.classList.remove('text-blue-500');
}, 300000); icon.textContent = '↓';
});
updateCoinFilterImages(); const sortIcon = document.getElementById(`sort-icon-${currentSortColumn}`);
fetchOffers().then(() => { if (sortIcon) {
applyFilters(); sortIcon.textContent = currentSortDirection === 'asc' ? '↑' : '↓';
}).catch(error => { sortIcon.classList.add('text-blue-500');
console.error('Error fetching initial offers:', error); }
}); }
}
const listingLabel = document.querySelector('span[data-listing-label]'); updateClearFiltersButton();
if (listingLabel) { initializeTableEvents();
listingLabel.textContent = isSentOffers ? 'Total Listings: ' : 'Network Listings: '; initializeTooltips();
} updateCoinFilterImages();
timerManager.addInterval(updateRowTimes, 900000); setTimeout(() => {
WebSocketManager.initialize();
}, 1000);
EventManager.add(document, 'visibilitychange', () => { if (initializeTableRateModule()) {
if (!document.hidden) { continueInitialization();
console.log('Page became visible, checking WebSocket connection'); } else {
if (!WebSocketManager.isConnected()) { let retryCount = 0;
WebSocketManager.connect(); const maxRetries = 5;
} const retryInterval = setInterval(() => {
} retryCount++;
}); if (initializeTableRateModule()) {
clearInterval(retryInterval);
continueInitialization();
} else if (retryCount >= maxRetries) {
clearInterval(retryInterval);
continueInitialization();
}
}, 1000);
}
EventManager.add(window, 'beforeunload', () => { timerManager.addInterval(() => {
cleanup(); if (WebSocketManager.isConnected()) {
}); console.log('🟢 WebSocket connection one established');
}
}, 30000);
console.log('Initialization completed'); timerManager.addInterval(() => {
CacheManager.cleanup();
}, 300000);
timerManager.addInterval(updateRowTimes, 900000);
EventManager.add(document, 'visibilitychange', () => {
if (!document.hidden) {
if (!WebSocketManager.isConnected()) {
WebSocketManager.connect();
}
}
});
EventManager.add(window, 'beforeunload', () => {
cleanup();
});
updateCoinFilterImages();
fetchOffers().then(() => {
applyFilters();
if (!isSentOffers) {
return;
}
}).catch(error => {
console.error('Error fetching initial offers:', error);
});
}); });
async function cleanup() { async function cleanup() {
@ -2796,35 +2816,23 @@ async function cleanup() {
console.error(`[Cleanup Error ${timeFromStart}ms] ${step}:`, error); console.error(`[Cleanup Error ${timeFromStart}ms] ${step}:`, error);
this.errors.push({ step, error, time: timeFromStart }); this.errors.push({ step, error, time: timeFromStart });
}, },
summarize: function() { summarizeLogs: function() {
const totalTime = Date.now() - this.startTime; console.log('Cleanup Summary:');
console.group('Cleanup Summary'); console.log(`Total cleanup time: ${Date.now() - this.startTime}ms`);
console.log(`Total cleanup time: ${totalTime}ms`); console.log(`Steps completed: ${this.steps.length}`);
console.log('Steps completed:', this.steps.length); console.log(`Errors encountered: ${this.errors.length}`);
console.log('Errors encountered:', this.errors.length);
if (this.steps.length > 0) {
console.group('Steps Timeline');
this.steps.forEach(({step, time}) => {
console.log(`${time}ms - ${step}`);
});
console.groupEnd();
}
if (this.errors.length > 0) {
console.group('Errors');
this.errors.forEach(({step, error, time}) => {
console.log(`${time}ms - ${step}:`, error);
});
console.groupEnd();
}
console.groupEnd();
} }
}; };
try { try {
debug.addStep('Starting cleanup process'); debug.addStep('Starting cleanup process');
debug.addStep('Starting tooltip cleanup');
if (window.TooltipManager) {
window.TooltipManager.cleanup();
}
debug.addStep('Tooltip cleanup completed');
debug.addStep('Clearing timers'); debug.addStep('Clearing timers');
const timerCount = timerManager.intervals.length + timerManager.timeouts.length; const timerCount = timerManager.intervals.length + timerManager.timeouts.length;
timerManager.clearAll(); timerManager.clearAll();
@ -2863,16 +2871,9 @@ async function cleanup() {
debug.addStep('Global state reset', globals); debug.addStep('Global state reset', globals);
debug.addStep('Clearing global references'); debug.addStep('Clearing global references');
if (window.WebSocketManager) { [
window.WebSocketManager = null; 'WebSocketManager',
} 'tableRateModule',
if (window.tableRateModule) {
window.tableRateModule = null;
}
debug.addStep('Global references cleared');
debug.addStep('Clearing DOM references');
const elementRefs = [
'offersBody', 'offersBody',
'filterForm', 'filterForm',
'prevPageButton', 'prevPageButton',
@ -2881,25 +2882,24 @@ async function cleanup() {
'totalPagesSpan', 'totalPagesSpan',
'lastRefreshTimeSpan', 'lastRefreshTimeSpan',
'newEntriesCountSpan' 'newEntriesCountSpan'
]; ].forEach(ref => {
let clearedRefs = 0;
elementRefs.forEach(ref => {
if (window[ref]) { if (window[ref]) {
window[ref] = null; window[ref] = null;
clearedRefs++;
} }
}); });
debug.addStep('DOM references cleared', `Cleared ${clearedRefs} references`); debug.addStep('Global references cleared');
debug.addStep('Clearing tooltips'); debug.addStep('Cleaning up tooltip containers');
const tooltips = document.querySelectorAll('[role="tooltip"]'); const tooltipContainers = document.querySelectorAll('.tooltip-container');
const tooltipCount = tooltips.length; tooltipContainers.forEach(container => {
tooltips.forEach(tooltip => tooltip.remove()); if (container && container.parentNode) {
debug.addStep('Tooltips cleared', `Removed ${tooltipCount} tooltips`); container.parentNode.removeChild(container);
}
});
debug.addStep('Tooltip containers cleaned up');
debug.addStep('Clearing document/window events'); debug.addStep('Clearing document/window events');
const events = ['visibilitychange', 'beforeunload', 'scroll']; ['visibilitychange', 'beforeunload', 'scroll'].forEach(event => {
events.forEach(event => {
document.removeEventListener(event, null); document.removeEventListener(event, null);
window.removeEventListener(event, null); window.removeEventListener(event, null);
}); });
@ -2919,6 +2919,9 @@ async function cleanup() {
debug.addStep('Starting failsafe cleanup'); debug.addStep('Starting failsafe cleanup');
try { try {
if (window.TooltipManager) {
window.TooltipManager.cleanup();
}
WebSocketManager.cleanup(); WebSocketManager.cleanup();
EventManager.clearAll(); EventManager.clearAll();
timerManager.clearAll(); timerManager.clearAll();
@ -2931,21 +2934,10 @@ async function cleanup() {
debug.addError('Critical failsafe cleanup', criticalError); debug.addError('Critical failsafe cleanup', criticalError);
} }
} finally { } finally {
debug.addStep('Running final cleanups'); debug.summarizeLogs();
if (document.body.classList.contains('optimize-scroll')) {
document.body.classList.remove('optimize-scroll');
}
if (document.body.classList.contains('is-scrolling')) {
document.body.classList.remove('is-scrolling');
}
const inlineStyles = document.querySelectorAll('style[data-dynamic]');
inlineStyles.forEach(style => style.remove());
debug.addStep('Final cleanups completed');
debug.summarize();
} }
} }
window.cleanup = cleanup; window.cleanup = cleanup;
console.log('Offers Table Module fully initialized'); //console.log('Offers Table Module fully initialized');

View file

@ -161,7 +161,7 @@ const api = {
const cachedData = cache.get(cacheKey); const cachedData = cache.get(cacheKey);
if (cachedData) { if (cachedData) {
console.log('Using cached CoinGecko data'); //console.log('Using cached CoinGecko data');
return cachedData.value; return cachedData.value;
} }
@ -194,7 +194,7 @@ const api = {
if (typeof response !== 'object' || response === null) { if (typeof response !== 'object' || response === null) {
if (fallbackData) { if (fallbackData) {
console.log('Using fallback data due to invalid response'); //console.log('Using fallback data due to invalid response');
return fallbackData; return fallbackData;
} }
throw new AppError('Invalid data structure received from CoinGecko'); throw new AppError('Invalid data structure received from CoinGecko');
@ -202,7 +202,7 @@ const api = {
if (response.error || response.Error) { if (response.error || response.Error) {
if (fallbackData) { if (fallbackData) {
console.log('Using fallback data due to API error'); //console.log('Using fallback data due to API error');
return fallbackData; return fallbackData;
} }
throw new AppError(response.error || response.Error); throw new AppError(response.error || response.Error);
@ -229,7 +229,7 @@ const api = {
const cachedData = cache.get(cacheKey); const cachedData = cache.get(cacheKey);
if (cachedData) { if (cachedData) {
console.log('Using expired cache data due to error'); //console.log('Using expired cache data due to error');
return cachedData.value; return cachedData.value;
} }
@ -382,7 +382,7 @@ const rateLimiter = {
error.name === 'NetworkError') { error.name === 'NetworkError') {
const cachedData = cache.get(`coinData_${apiName}`); const cachedData = cache.get(`coinData_${apiName}`);
if (cachedData) { if (cachedData) {
console.log('Using cached data due to request failure'); //console.log('Using cached data due to request failure');
return cachedData.value; return cachedData.value;
} }
} }
@ -421,7 +421,7 @@ const cache = {
localStorage.removeItem(key); localStorage.removeItem(key);
} }
} catch (error) { } catch (error) {
console.error('Error parsing cache item:', error.message); //console.error('Error parsing cache item:', error.message);
localStorage.removeItem(key); localStorage.removeItem(key);
} }
return null; return null;
@ -912,7 +912,7 @@ const chartModule = {
let data; let data;
if (cachedData && Object.keys(cachedData.value).length > 0) { if (cachedData && Object.keys(cachedData.value).length > 0) {
data = cachedData.value; data = cachedData.value;
console.log(`Using cached data for ${coinSymbol}`); //console.log(`Using cached data for ${coinSymbol}`);
} else { } else {
try { try {
const allData = await api.fetchHistoricalDataXHR([coinSymbol]); const allData = await api.fetchHistoricalDataXHR([coinSymbol]);
@ -923,7 +923,7 @@ const chartModule = {
cache.set(cacheKey, data, config.cacheTTL); cache.set(cacheKey, data, config.cacheTTL);
} catch (error) { } catch (error) {
if (error.message.includes('429') && currentChartData.length > 0) { if (error.message.includes('429') && currentChartData.length > 0) {
console.warn(`Rate limit hit for ${coinSymbol}, maintaining current chart`); //console.warn(`Rate limit hit for ${coinSymbol}, maintaining current chart`);
return; return;
} }
const expiredCache = localStorage.getItem(cacheKey); const expiredCache = localStorage.getItem(cacheKey);
@ -931,7 +931,7 @@ const chartModule = {
try { try {
const parsedCache = JSON.parse(expiredCache); const parsedCache = JSON.parse(expiredCache);
data = parsedCache.value; data = parsedCache.value;
console.log(`Using expired cache data for ${coinSymbol}`); //console.log(`Using expired cache data for ${coinSymbol}`);
} catch (cacheError) { } catch (cacheError) {
throw error; throw error;
} }
@ -1042,15 +1042,15 @@ const app = {
minimumRefreshInterval: 60 * 1000, // 1 min minimumRefreshInterval: 60 * 1000, // 1 min
init: () => { init: () => {
console.log('Initializing app...'); //console.log('Init');
window.addEventListener('load', app.onLoad); window.addEventListener('load', app.onLoad);
app.loadLastRefreshedTime(); app.loadLastRefreshedTime();
app.updateAutoRefreshButton(); app.updateAutoRefreshButton();
console.log('App initialized'); //console.log('App initialized');
}, },
onLoad: async () => { onLoad: async () => {
console.log('App onLoad event triggered'); //console.log('App onLoad event triggered');
ui.showLoader(); ui.showLoader();
try { try {
volumeToggle.init(); volumeToggle.init();
@ -1061,7 +1061,7 @@ const app = {
chartModule.showChartLoader(); chartModule.showChartLoader();
} }
console.log('Loading all coin data...'); //console.log('Loading all coin data...');
await app.loadAllCoinData(); await app.loadAllCoinData();
if (chartModule.chart) { if (chartModule.chart) {
@ -1087,7 +1087,7 @@ const app = {
if (chartModule.chart) { if (chartModule.chart) {
chartModule.hideChartLoader(); chartModule.hideChartLoader();
} }
console.log('App onLoad completed'); //console.log('App onLoad completed');
} }
}, },
loadAllCoinData: async () => { loadAllCoinData: async () => {
@ -1192,7 +1192,7 @@ setupEventListeners: () => {
}, },
initAutoRefresh: () => { initAutoRefresh: () => {
console.log('Initializing auto-refresh...'); //console.log('Initializing auto-refresh...');
const toggleAutoRefreshButton = document.getElementById('toggle-auto-refresh'); const toggleAutoRefreshButton = document.getElementById('toggle-auto-refresh');
if (toggleAutoRefreshButton) { if (toggleAutoRefreshButton) {
toggleAutoRefreshButton.addEventListener('click', app.toggleAutoRefresh); toggleAutoRefreshButton.addEventListener('click', app.toggleAutoRefresh);
@ -1208,7 +1208,7 @@ setupEventListeners: () => {
}, },
scheduleNextRefresh: () => { scheduleNextRefresh: () => {
console.log('Scheduling next refresh...'); //console.log('Scheduling next refresh...');
if (app.autoRefreshInterval) { if (app.autoRefreshInterval) {
clearTimeout(app.autoRefreshInterval); clearTimeout(app.autoRefreshInterval);
} }
@ -1274,7 +1274,7 @@ setupEventListeners: () => {
return; return;
} }
console.log('Starting refresh of all data...'); //console.log('Starting refresh of all data...');
app.isRefreshing = true; app.isRefreshing = true;
ui.showLoader(); ui.showLoader();
chartModule.showChartLoader(); chartModule.showChartLoader();
@ -1381,7 +1381,7 @@ setupEventListeners: () => {
}, },
updateNextRefreshTime: () => { updateNextRefreshTime: () => {
console.log('Updating next refresh time display'); //console.log('Updating next refresh time display');
const nextRefreshSpan = document.getElementById('next-refresh-time'); const nextRefreshSpan = document.getElementById('next-refresh-time');
const labelElement = document.getElementById('next-refresh-label'); const labelElement = document.getElementById('next-refresh-label');
const valueElement = document.getElementById('next-refresh-value'); const valueElement = document.getElementById('next-refresh-value');
@ -1417,7 +1417,7 @@ setupEventListeners: () => {
}, },
updateAutoRefreshButton: () => { updateAutoRefreshButton: () => {
console.log('Updating auto-refresh button state'); //console.log('Updating auto-refresh button state');
const button = document.getElementById('toggle-auto-refresh'); const button = document.getElementById('toggle-auto-refresh');
if (button) { if (button) {
if (app.isAutoRefreshEnabled) { if (app.isAutoRefreshEnabled) {
@ -1462,7 +1462,7 @@ setupEventListeners: () => {
}, },
loadLastRefreshedTime: () => { loadLastRefreshedTime: () => {
console.log('Loading last refreshed time from storage'); //console.log('Loading last refreshed time from storage');
const storedTime = localStorage.getItem('lastRefreshedTime'); const storedTime = localStorage.getItem('lastRefreshedTime');
if (storedTime) { if (storedTime) {
app.lastRefreshedTime = new Date(parseInt(storedTime)); app.lastRefreshedTime = new Date(parseInt(storedTime));
@ -1471,7 +1471,7 @@ setupEventListeners: () => {
}, },
updateBTCPrice: async () => { updateBTCPrice: async () => {
console.log('Updating BTC price...'); //console.log('Updating BTC price...');
try { try {
const priceData = await api.fetchCoinGeckoDataXHR(); const priceData = await api.fetchCoinGeckoDataXHR();

View file

@ -1,309 +1,306 @@
(function(window) { class TooltipManager {
'use strict'; constructor() {
this.activeTooltips = new Map();
this.sizeCheckIntervals = new Map();
this.setupStyles();
this.setupCleanupEvents();
}
const tooltipContainer = document.createElement('div'); static initialize() {
tooltipContainer.className = 'tooltip-container'; if (!window.TooltipManager) {
window.TooltipManager = new TooltipManager();
const style = document.createElement('style');
style.textContent = `
[role="tooltip"] {
position: absolute;
z-index: 9999;
transition: opacity 0.2s ease-in-out;
pointer-events: auto;
opacity: 0;
visibility: hidden;
} }
return window.TooltipManager;
}
create(element, content, options = {}) {
if (!element) return null;
.tooltip-container { this.destroy(element);
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 0;
overflow: visible;
pointer-events: none;
z-index: 9999;
}
`;
function ensureContainerExists() { const checkSize = () => {
if (!document.body.contains(tooltipContainer)) { const rect = element.getBoundingClientRect();
document.body.appendChild(tooltipContainer); if (rect.width && rect.height) {
} clearInterval(this.sizeCheckIntervals.get(element));
} this.sizeCheckIntervals.delete(element);
this.createTooltip(element, content, options, rect);
function rafThrottle(callback) {
let requestId = null;
let lastArgs = null;
const later = (context) => {
requestId = null;
callback.apply(context, lastArgs);
};
return function(...args) {
lastArgs = args;
if (requestId === null) {
requestId = requestAnimationFrame(() => later(this));
} }
}; };
this.sizeCheckIntervals.set(element, setInterval(checkSize, 50));
checkSize();
return null;
} }
function positionElement(targetEl, triggerEl, placement = 'top', offsetDistance = 8) { createTooltip(element, content, options, rect) {
const triggerRect = triggerEl.getBoundingClientRect(); const targetId = element.getAttribute('data-tooltip-target');
const targetRect = targetEl.getBoundingClientRect(); let bgClass = 'bg-gray-400';
let top, left; let arrowColor = 'rgb(156 163 175)';
switch (placement) { if (targetId?.includes('tooltip-offer-')) {
case 'top': const offerId = targetId.split('tooltip-offer-')[1];
top = triggerRect.top - targetRect.height - offsetDistance; const [actualOfferId] = offerId.split('_');
left = triggerRect.left + (triggerRect.width - targetRect.width) / 2;
break; if (window.jsonData) {
case 'bottom': const offer = window.jsonData.find(o =>
top = triggerRect.bottom + offsetDistance; o.unique_id === offerId ||
left = triggerRect.left + (triggerRect.width - targetRect.width) / 2; o.offer_id === actualOfferId
break;
case 'left':
top = triggerRect.top + (triggerRect.height - targetRect.height) / 2;
left = triggerRect.left - targetRect.width - offsetDistance;
break;
case 'right':
top = triggerRect.top + (triggerRect.height - targetRect.height) / 2;
left = triggerRect.right + offsetDistance;
break;
}
const viewport = {
width: window.innerWidth,
height: window.innerHeight
};
if (left < 0) left = 0;
if (top < 0) top = 0;
if (left + targetRect.width > viewport.width)
left = viewport.width - targetRect.width;
if (top + targetRect.height > viewport.height)
top = viewport.height - targetRect.height;
targetEl.style.transform = `translate(${Math.round(left)}px, ${Math.round(top)}px)`;
}
const tooltips = new WeakMap();
class Tooltip {
constructor(targetEl, triggerEl, options = {}) {
ensureContainerExists();
this._targetEl = targetEl;
this._triggerEl = triggerEl;
this._options = {
placement: options.placement || 'top',
triggerType: options.triggerType || 'hover',
offset: options.offset || 8,
onShow: options.onShow || function() {},
onHide: options.onHide || function() {}
};
this._visible = false;
this._initialized = false;
this._hideTimeout = null;
this._showTimeout = null;
if (this._targetEl.parentNode !== tooltipContainer) {
tooltipContainer.appendChild(this._targetEl);
}
this._targetEl.style.visibility = 'hidden';
this._targetEl.style.opacity = '0';
this._showHandler = this.show.bind(this);
this._hideHandler = this._handleHide.bind(this);
this._updatePosition = rafThrottle(() => {
if (this._visible) {
positionElement(
this._targetEl,
this._triggerEl,
this._options.placement,
this._options.offset
);
}
});
this.init();
}
init() {
if (!this._initialized) {
this._setupEventListeners();
this._initialized = true;
positionElement(
this._targetEl,
this._triggerEl,
this._options.placement,
this._options.offset
); );
}
}
_setupEventListeners() { if (offer) {
this._triggerEl.addEventListener('mouseenter', this._showHandler); if (offer.is_revoked) {
this._triggerEl.addEventListener('mouseleave', this._hideHandler); bgClass = 'bg-red-500';
this._triggerEl.addEventListener('focus', this._showHandler); arrowColor = 'rgb(239 68 68)';
this._triggerEl.addEventListener('blur', this._hideHandler); } else if (offer.is_own_offer) {
bgClass = 'bg-gray-300';
this._targetEl.addEventListener('mouseenter', () => { arrowColor = 'rgb(209 213 219)';
clearTimeout(this._hideTimeout); } else {
clearTimeout(this._showTimeout); bgClass = 'bg-green-700';
this._visible = true; arrowColor = 'rgb(21 128 61)';
this._targetEl.style.visibility = 'visible'; }
this._targetEl.style.opacity = '1';
});
this._targetEl.addEventListener('mouseleave', this._hideHandler);
if (this._options.triggerType === 'click') {
this._triggerEl.addEventListener('click', this._showHandler);
}
window.addEventListener('scroll', this._updatePosition, { passive: true });
document.addEventListener('scroll', this._updatePosition, { passive: true, capture: true });
window.addEventListener('resize', this._updatePosition, { passive: true });
let rafId;
const smoothUpdate = () => {
if (this._visible) {
this._updatePosition();
rafId = requestAnimationFrame(smoothUpdate);
} }
};
this._startSmoothUpdate = () => {
if (!rafId) rafId = requestAnimationFrame(smoothUpdate);
};
this._stopSmoothUpdate = () => {
if (rafId) {
cancelAnimationFrame(rafId);
rafId = null;
}
};
}
_handleHide() {
clearTimeout(this._hideTimeout);
clearTimeout(this._showTimeout);
this._hideTimeout = setTimeout(() => {
if (this._visible) {
this.hide();
}
}, 100);
}
show() {
clearTimeout(this._hideTimeout);
clearTimeout(this._showTimeout);
this._showTimeout = setTimeout(() => {
if (!this._visible) {
positionElement(
this._targetEl,
this._triggerEl,
this._options.placement,
this._options.offset
);
this._targetEl.style.visibility = 'visible';
this._targetEl.style.opacity = '1';
this._visible = true;
this._startSmoothUpdate();
this._options.onShow();
}
}, 20);
}
hide() {
this._targetEl.style.opacity = '0';
this._targetEl.style.visibility = 'hidden';
this._visible = false;
this._stopSmoothUpdate();
this._options.onHide();
}
destroy() {
clearTimeout(this._hideTimeout);
clearTimeout(this._showTimeout);
this._stopSmoothUpdate();
this._triggerEl.removeEventListener('mouseenter', this._showHandler);
this._triggerEl.removeEventListener('mouseleave', this._hideHandler);
this._triggerEl.removeEventListener('focus', this._showHandler);
this._triggerEl.removeEventListener('blur', this._hideHandler);
this._targetEl.removeEventListener('mouseenter', this._showHandler);
this._targetEl.removeEventListener('mouseleave', this._hideHandler);
if (this._options.triggerType === 'click') {
this._triggerEl.removeEventListener('click', this._showHandler);
}
window.removeEventListener('scroll', this._updatePosition);
document.removeEventListener('scroll', this._updatePosition, true);
window.removeEventListener('resize', this._updatePosition);
this._targetEl.style.visibility = '';
this._targetEl.style.opacity = '';
this._targetEl.style.transform = '';
if (this._targetEl.parentNode === tooltipContainer) {
document.body.appendChild(this._targetEl);
}
this._initialized = false;
}
toggle() {
if (this._visible) {
this.hide();
} else {
this.show();
} }
} }
}
document.head.appendChild(style); const instance = tippy(element, {
content,
allowHTML: true,
placement: options.placement || 'top',
appendTo: document.body,
animation: false,
duration: 0,
delay: 0,
interactive: true,
arrow: false,
theme: '',
moveTransition: 'none',
offset: [0, 10],
popperOptions: {
strategy: 'fixed',
modifiers: [
{
name: 'preventOverflow',
options: {
boundary: 'viewport',
padding: 10
}
},
{
name: 'flip',
options: {
padding: 10,
fallbackPlacements: ['top', 'bottom', 'right', 'left']
}
}
]
},
onCreate(instance) {
instance._originalPlacement = instance.props.placement;
},
onShow(instance) {
if (!document.body.contains(element)) {
return false;
}
function initTooltips() { const rect = element.getBoundingClientRect();
ensureContainerExists(); if (!rect.width || !rect.height) {
return false;
document.querySelectorAll('[data-tooltip-target]').forEach(triggerEl => { }
if (tooltips.has(triggerEl)) return;
const targetId = triggerEl.getAttribute('data-tooltip-target'); instance.setProps({
const targetEl = document.getElementById(targetId); placement: instance._originalPlacement
if (targetEl) {
const placement = triggerEl.getAttribute('data-tooltip-placement');
const triggerType = triggerEl.getAttribute('data-tooltip-trigger');
const tooltip = new Tooltip(targetEl, triggerEl, {
placement: placement || 'top',
triggerType: triggerType || 'hover',
offset: 8
}); });
tooltips.set(triggerEl, tooltip); if (instance.popper.firstElementChild) {
instance.popper.firstElementChild.classList.add(bgClass);
}
return true;
},
onMount(instance) {
if (instance.popper.firstElementChild) {
instance.popper.firstElementChild.classList.add(bgClass);
}
const arrow = instance.popper.querySelector('.tippy-arrow');
if (arrow) {
arrow.style.setProperty('color', arrowColor, 'important');
}
}
});
const id = element.getAttribute('data-tooltip-trigger-id') ||
`tooltip-${Math.random().toString(36).substring(7)}`;
element.setAttribute('data-tooltip-trigger-id', id);
this.activeTooltips.set(id, instance);
return instance;
}
destroy(element) {
if (!element) return;
if (this.sizeCheckIntervals.has(element)) {
clearInterval(this.sizeCheckIntervals.get(element));
this.sizeCheckIntervals.delete(element);
}
const id = element.getAttribute('data-tooltip-trigger-id');
if (!id) return;
const instance = this.activeTooltips.get(id);
if (instance?.[0]) {
try {
instance[0].destroy();
} catch (e) {
console.warn('Error destroying tooltip:', e);
}
}
this.activeTooltips.delete(id);
element.removeAttribute('data-tooltip-trigger-id');
}
cleanup() {
this.sizeCheckIntervals.forEach((interval) => clearInterval(interval));
this.sizeCheckIntervals.clear();
this.activeTooltips.forEach((instance, id) => {
if (instance?.[0]) {
try {
instance[0].destroy();
} catch (e) {
console.warn('Error cleaning up tooltip:', e);
}
}
});
this.activeTooltips.clear();
document.querySelectorAll('[data-tippy-root]').forEach(element => {
if (element.parentNode) {
element.parentNode.removeChild(element);
} }
}); });
} }
if (document.readyState === 'loading') { setupStyles() {
document.addEventListener('DOMContentLoaded', initTooltips); if (document.getElementById('tooltip-styles')) return;
} else {
initTooltips(); document.head.insertAdjacentHTML('beforeend', `
<style id="tooltip-styles">
[data-tippy-root] {
position: fixed !important;
z-index: 9999 !important;
pointer-events: none !important;
}
.tippy-box {
font-size: 0.875rem;
line-height: 1.25rem;
font-weight: 500;
border-radius: 0.5rem;
color: white;
position: relative !important;
pointer-events: auto !important;
}
.tippy-content {
padding: 0.5rem 0.75rem !important;
}
.tippy-box .bg-gray-400 {
background-color: rgb(156 163 175);
padding: 0.5rem 0.75rem;
}
.tippy-box:has(.bg-gray-400) .tippy-arrow {
color: rgb(156 163 175);
}
.tippy-box .bg-red-500 {
background-color: rgb(239 68 68);
padding: 0.5rem 0.75rem;
}
.tippy-box:has(.bg-red-500) .tippy-arrow {
color: rgb(239 68 68);
}
.tippy-box .bg-gray-300 {
background-color: rgb(209 213 219);
padding: 0.5rem 0.75rem;
}
.tippy-box:has(.bg-gray-300) .tippy-arrow {
color: rgb(209 213 219);
}
.tippy-box .bg-green-700 {
background-color: rgb(21 128 61);
padding: 0.5rem 0.75rem;
}
.tippy-box:has(.bg-green-700) .tippy-arrow {
color: rgb(21 128 61);
}
.tippy-box[data-placement^='top'] > .tippy-arrow::before {
border-top-color: currentColor;
}
.tippy-box[data-placement^='bottom'] > .tippy-arrow::before {
border-bottom-color: currentColor;
}
.tippy-box[data-placement^='left'] > .tippy-arrow::before {
border-left-color: currentColor;
}
.tippy-box[data-placement^='right'] > .tippy-arrow::before {
border-right-color: currentColor;
}
.tippy-box[data-placement^='top'] > .tippy-arrow {
bottom: 0;
}
.tippy-box[data-placement^='bottom'] > .tippy-arrow {
top: 0;
}
.tippy-box[data-placement^='left'] > .tippy-arrow {
right: 0;
}
.tippy-box[data-placement^='right'] > .tippy-arrow {
left: 0;
}
</style>
`);
} }
window.Tooltip = Tooltip; setupCleanupEvents() {
window.initTooltips = initTooltips; window.addEventListener('beforeunload', () => this.cleanup());
window.addEventListener('unload', () => this.cleanup());
document.addEventListener('visibilitychange', () => {
if (document.hidden) {
this.cleanup();
}
});
}
})(window); initializeTooltips(selector = '[data-tooltip-target]') {
document.querySelectorAll(selector).forEach(element => {
const targetId = element.getAttribute('data-tooltip-target');
const tooltipContent = document.getElementById(targetId);
if (tooltipContent) {
this.create(element, tooltipContent.innerHTML, {
placement: element.getAttribute('data-tooltip-placement') || 'top'
});
}
});
}
}
if (typeof module !== 'undefined' && module.exports) {
module.exports = TooltipManager;
}
document.addEventListener('DOMContentLoaded', () => {
TooltipManager.initialize();
});

View file

@ -14,7 +14,6 @@
<li> <li>
<a class="flex font-medium text-xs text-coolGray-500 dark:text-gray-300 hover:text-coolGray-700" href="/active">Swaps In Progress</a> <a class="flex font-medium text-xs text-coolGray-500 dark:text-gray-300 hover:text-coolGray-700" href="/active">Swaps In Progress</a>
</li> </li>
<li>{{ breadcrumb_line_svg | safe }}</li>
</ul> </ul>
</div> </div>
</div> </div>

View file

@ -1,43 +1,60 @@
{% include 'header.html' %} {% include 'header.html' %}
{% from 'style.html' import breadcrumb_line_svg, page_back_svg, page_forwards_svg, filter_clear_svg, filter_apply_svg, circular_arrows_svg, input_arrow_down_svg %} {% from 'style.html' import breadcrumb_line_svg, page_back_svg, page_forwards_svg, filter_clear_svg, filter_apply_svg, circular_arrows_svg, input_arrow_down_svg %}
<div class="container mx-auto"> <div class="container mx-auto">
<section class="p-5 mt-5"> <section class="p-5 mt-5">
<div class="flex flex-wrap items-center -m-2"> <div class="flex flex-wrap items-center -m-2">
<div class="w-full md:w-1/2 p-2"> <div class="w-full md:w-1/2 p-2">
<ul class="flex flex-wrap items-center gap-x-3 mb-2"> <ul class="flex flex-wrap items-center gap-x-3 mb-2">
<li> <li><a class="flex font-medium text-xs text-coolGray-500 dark:text-gray-300 hover:text-coolGray-700" href="/"><p>Home</p></a></li>
<a class="flex font-medium text-xs text-coolGray-500 dark:text-gray-300 hover:text-coolGray-700" href="/"><p>Home</p></a> <li>{{ breadcrumb_line_svg | safe }}</li>
</li> <li><a class="flex font-medium text-xs text-coolGray-500 dark:text-gray-300 hover:text-coolGray-700" href="#">Sent Bids / Received Bids</a></li>
<li> {{ breadcrumb_line_svg | safe }} </li>
<li> <a class="flex font-medium text-xs text-coolGray-500 dark:text-gray-300 hover:text-coolGray-700" href="#">{{ page_type_available }} {{ page_type_received }} {{ page_type_sent }}</a> </li>
<li> {{ breadcrumb_line_svg | safe }} </li>
</ul> </ul>
</div> </div>
</div> </div>
</section> </section>
<section class="py-4"> <section class="py-4">
<div class="container px-4 mx-auto"> <div class="container px-4 mx-auto">
<div class="relative py-11 px-16 bg-coolGray-900 dark:bg-blue-500 rounded-md overflow-hidden"> <div class="relative py-11 px-16 bg-coolGray-900 dark:bg-blue-500 rounded-md overflow-hidden">
<img class="absolute z-10 left-4 top-4" src="/static/images/elements/dots-red.svg" alt=""> <img class="absolute z-10 right-4 bottom-4" src="/static/images/elements/dots-red.svg" alt=""> <img class="absolute z-10 left-4 top-4" src="/static/images/elements/dots-red.svg" alt="">
<img class="absolute z-10 right-4 bottom-4" src="/static/images/elements/dots-red.svg" alt="">
<img class="absolute h-64 left-1/2 top-1/2 transform -translate-x-1/2 -translate-y-1/2 object-cover" src="/static/images/elements/wave.svg" alt=""> <img class="absolute h-64 left-1/2 top-1/2 transform -translate-x-1/2 -translate-y-1/2 object-cover" src="/static/images/elements/wave.svg" alt="">
<div class="relative z-20 flex flex-wrap items-center -m-3"> <div class="relative z-20 flex flex-wrap items-center -m-3">
<div class="w-full md:w-1/2 p-3"> <div class="w-full md:w-1/2 p-3">
<h2 class="mb-6 text-4xl font-bold text-white tracking-tighter">{{ page_type_available }} {{ page_type_received }} {{ page_type_sent }}</h2> <h2 class="mb-6 text-4xl font-bold text-white tracking-tighter">Sent Bids / Received Bids</h2>
<p class="font-normal text-coolGray-200 dark:text-white">{{ page_type_available_description }} {{ page_type_received_description }} {{ page_type_sent_description }}</p> <p class="font-normal text-coolGray-200 dark:text-white">View, and manage bids</p>
</div> </div>
<div class="w-full md:w-1/2 p-3 p-6 container flex flex-wrap items-center justify-end items-center mx-auto">
{% if refresh %}
<a id="refresh" href="/bid/{{ bid_id }}" class="rounded-full mr-5 flex flex-wrap justify-center px-5 py-3 bg-blue-500 hover:bg-blue-600 font-medium text-sm text-white border dark:bg-gray-500 dark:hover:bg-gray-700 border-blue-500 rounded-md shadow-button focus:ring-0 focus:outline-none">
{{ circular_arrows_svg | safe }}
<span>Refresh {{ refresh }} seconds</span>
</a>
{% endif %}
</div>
</div> </div>
</div> </div>
</div> </div>
</section> </section>
{% include 'inc_messages.html' %} {% include 'inc_messages.html' %}
<section>
<div class="pl-6 pr-6 pt-0 mt-5 h-full overflow-hidden">
<div class="flex flex-wrap items-center justify-between -m-2">
<div class="w-full pt-2">
<div class="mb-4 border-b pb-5 border-gray-200 dark:border-gray-500">
<ul class="flex flex-wrap text-sm font-medium text-center text-gray-500 dark:text-gray-400" id="myTab" data-tabs-toggle="#bidstab" role="tablist">
<li class="mr-2">
<button class="inline-block px-4 py-3 rounded-lg hover:text-gray-900 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white focus:outline-none focus:ring-0" id="sent-tab" data-tabs-target="#sent" type="button" role="tab" aria-controls="sent" aria-selected="true">
Sent Bids ({{ sent_bids_count }})
</button>
</li>
<li class="mr-2">
<button class="inline-block px-4 py-3 rounded-lg hover:text-gray-900 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white focus:outline-none focus:ring-0" id="received-tab" data-tabs-target="#received" type="button" role="tab" aria-controls="received" aria-selected="false">
Received Bids ({{ received_bids_count }})
</button>
</li>
</ul>
</div>
</div>
</div>
</div>
</section>
<div class="pl-6 pr-6 pt-0 pb-0 mt-5 h-full overflow-hidden"> <div class="pl-6 pr-6 pt-0 pb-0 mt-5 h-full overflow-hidden">
<div class="pb-6 border-coolGray-100"> <div class="pb-6 border-coolGray-100">
<div class="flex flex-wrap items-center justify-between -m-2"> <div class="flex flex-wrap items-center justify-between -m-2">
@ -91,7 +108,8 @@
<select name="with_expired" class="hover:border-blue-500 bg-gray-50 text-gray-900 appearance-none pr-10 dark:bg-gray-500 dark:text-white border border-gray-300 dark:border-gray-400 dark:text-gray-50 dark:placeholder-gray-50 text-sm rounded-lg outline-none focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 focus:ring-0"> <select name="with_expired" class="hover:border-blue-500 bg-gray-50 text-gray-900 appearance-none pr-10 dark:bg-gray-500 dark:text-white border border-gray-300 dark:border-gray-400 dark:text-gray-50 dark:placeholder-gray-50 text-sm rounded-lg outline-none focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 focus:ring-0">
<option value="true" {% if filters.with_expired==true %} selected{% endif %}>Include</option> <option value="true" {% if filters.with_expired==true %} selected{% endif %}>Include</option>
<option value="false" {% if filters.with_expired==false %} selected{% endif %}>Exclude</option> <option value="false" {% if filters.with_expired==false %} selected{% endif %}>Exclude</option>
</select> </div> </select>
</div>
</div> </div>
<div class="w-full md:w-auto p-1.5"> <div class="w-full md:w-auto p-1.5">
<div class="relative"> <div class="relative">
@ -101,120 +119,294 @@
</div> </div>
</div> </div>
<div class="w-full md:w-auto p-1.5"> <div class="w-full md:w-auto p-1.5">
<div class="relative"> <button type="submit" name='applyfilters' value="Apply Filters" class="flex flex-wrap justify-center w-full px-4 py-2.5 font-medium text-sm text-white bg-blue-600 hover:bg-green-600 hover:border-green-600 rounded-lg transition duration-200 border border-blue-500 rounded-md shadow-button focus:ring-0 focus:outline-none"> <div class="relative">
{{ filter_apply_svg | safe }} <button type="submit" name='applyfilters' value="Apply Filters" class="flex flex-wrap justify-center w-full px-4 py-2.5 font-medium text-sm text-white bg-blue-600 hover:bg-green-600 hover:border-green-600 rounded-lg transition duration-200 border border-blue-500 rounded-md shadow-button focus:ring-0 focus:outline-none">
<span>Apply Filters</span> {{ filter_apply_svg | safe }}
</button> <span>Apply Filters</span>
</div> </button>
</div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div class="container mt-5 mx-auto">
<div class="pt-6 pb-6 bg-coolGray-100 dark:bg-gray-500 rounded-xl"> <div id="bidstab">
<div class="px-6"> <div class="rounded-lg" id="sent" role="tabpanel" aria-labelledby="sent-tab">
<div class="w-full mt-6 pb-6 overflow-x-auto"> <div id="sent-content">
<table class="w-full min-w-max"> <div class="container mt-5 mx-auto">
<thead class="uppercase"> <div class="pt-6 pb-6 bg-coolGray-100 dark:bg-gray-500 rounded-xl">
<tr class="text-left"> <div class="px-6">
<th class="p-0"> <div class="w-full mt-6 pb-6 overflow-x-auto">
<div class="py-3 px-6 rounded-tl-xl bg-coolGray-200 dark:bg-gray-600"> <span class="text-xs text-gray-600 dark:text-gray-300 font-semibold">Date/Time at</span> <table class="w-full min-w-max">
<thead class="uppercase">
<tr class="text-left">
<th class="p-0">
<div class="py-3 px-6 rounded-tl-xl bg-coolGray-200 dark:bg-gray-600">
<span class="text-xs text-gray-600 dark:text-gray-300 font-semibold">Date/Time at</span>
</div>
</th>
<th class="p-0">
<div class="py-3 px-6 bg-coolGray-200 dark:bg-gray-600">
<span class="text-xs text-gray-600 dark:text-gray-300 font-semibold">Bid ID</span>
</div>
</th>
<th class="p-0">
<div class="py-3 px-6 bg-coolGray-200 dark:bg-gray-600">
<span class="text-xs text-gray-600 dark:text-gray-300 font-semibold">Offer ID</span>
</div>
</th>
<th class="p-0">
<div class="py-3 px-6 bg-coolGray-200 dark:bg-gray-600">
<span class="text-xs text-gray-600 dark:text-gray-300 font-semibold">Bid From</span>
</div>
</th>
<th class="p-0">
<div class="py-3 px-6 bg-coolGray-200 dark:bg-gray-600">
<span class="text-xs text-gray-600 dark:text-gray-300 font-semibold">Bid Status</span>
</div>
</th>
<th class="p-0">
<div class="py-3 px-6 bg-coolGray-200 dark:bg-gray-600">
<span class="text-xs text-gray-600 dark:text-gray-300 font-semibold">ITX Status</span>
</div>
</th>
<th class="p-0">
<div class="py-3 px-6 bg-coolGray-200 dark:bg-gray-600">
<span class="text-xs text-gray-600 dark:text-gray-300 font-semibold">PTX Status</span>
</div>
</th>
<th class="p-0">
<div class="py-3 px-6 rounded-tr-xl bg-coolGray-200 dark:bg-gray-600">
<span class="text-xs text-gray-600 dark:text-gray-300 font-semibold">Details</span>
</div>
</th>
</tr>
</thead>
<tbody>
{% for b in sent_bids %}
<tr class="opacity-100 text-gray-500 dark:text-gray-100 hover:bg-coolGray-200 dark:hover:bg-gray-600">
<th scope="row" class="flex items-center py-7 px-46 text-gray-900 whitespace-nowrap">
<svg class="w-5 h-5 rounded-full ml-5" xmlns="http://www.w3.org/2000/svg" height="20" width="20" viewBox="0 0 24 24">
<g stroke-linecap="round" stroke-width="2" fill="none" stroke="#6b7280" stroke-linejoin="round">
<circle cx="12" cy="12" r="11"></circle>
<polyline points=" 12,6 12,12 18,12 " stroke="#6b7280"></polyline>
</g>
</svg>
<div class="pl-3">
<div class="font-semibold text-xs dark:text-white">{{ b[0] }}</div>
</div>
</th>
<td class="py-3 px-6 text-xs monospace"><a href=/bid/{{ b[1] }}>{{ b[1]|truncate(20, True) }}</a></td>
<td class="py-3 px-6 text-xs monospace"><a href=/offer/{{ b[2] }}>{{ b[2]|truncate(20, True) }}</a></td>
<td class="py-3 px-6 text-xs monospace"><a href=/identity/{{ b[6] }}>{{ b[6] }}</a></td>
<td class="py-3 px-6 text-xs">{{ b[3] }}</td>
<td class="py-3 px-6 text-xs">{{ b[4] }}</td>
<td class="py-3 px-6 text-xs">{{ b[5] }}</td>
<td class="py-3 px-6 text-xs">
<a 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" href="/bid/{{ b[1] }}">Details</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<div class="rounded-b-md">
<div class="w-full md:w-0/12">
<div class="flex flex-wrap justify-end pt-6 pr-6 border-t border-gray-100 dark:border-gray-400">
{% if filters.page_no > 1 %}
<div class="w-full md:w-auto p-1.5">
<input type="hidden" name="filter_key" value="{{ filter_key }}">
<input type="hidden" name="page_no" value="{{ filters.page_no - 1 }}">
<button type="submit" name='pageback' value="Previous" class="inline-flex items-center h-9 py-1 px-4 text-xs text-blue-50 font-semibold bg-blue-500 hover:bg-blue-600 rounded-lg transition duration-200 focus:ring-0 focus:outline-none">
{{ page_back_svg | safe }}
<span>Previous</span>
</button>
</div>
{% endif %}
<div class="flex items-center">
<div class="w-full md:w-auto p-1.5">
<p class="text-sm font-heading dark:text-white">Page: {{ filters.page_no }}</p>
</div>
</div>
{% if sent_bids_count >= filters.limit %}
<div class="w-full md:w-auto p-1.5">
<input type="hidden" name="filter_key" value="{{ filter_key }}">
<input type="hidden" name="page_no" value="{{ filters.page_no + 1 }}">
<button type="submit" name='pageforwards' value="Next" class="inline-flex items-center h-9 py-1 px-4 text-xs text-blue-50 font-semibold bg-blue-500 hover:bg-blue-600 rounded-lg transition duration-200 focus:ring-0 focus:outline-none">
<span>Next</span>
{{ page_forwards_svg | safe }}
</button>
</div>
{% endif %}
</div> </div>
</th> </div>
<th class="p-0">
<div class="py-3 px-6 bg-coolGray-200 dark:bg-gray-600"> <span class="text-xs text-gray-600 dark:text-gray-300 font-semibold">Bid ID</span>
</div>
</th>
<th class="p-0">
<div class="py-3 px-6 bg-coolGray-200 dark:bg-gray-600"> <span class="text-xs text-gray-600 dark:text-gray-300 font-semibold">Offer ID</span>
</div>
</th>
<th class="p-0">
<div class="py-3 px-6 bg-coolGray-200 dark:bg-gray-600"> <span class="text-xs text-gray-600 dark:text-gray-300 font-semibold">Bid From</span>
</div>
</th>
<th class="p-0">
<div class="py-3 px-6 bg-coolGray-200 dark:bg-gray-600"> <span class="text-xs text-gray-600 dark:text-gray-300 font-semibold">Bid Status</span>
</div>
</th>
<th class="p-0">
<div class="py-3 px-6 bg-coolGray-200 dark:bg-gray-600"> <span class="text-xs text-gray-600 dark:text-gray-300 font-semibold">ITX Status</span>
</div>
</th>
<th class="p-0">
<div class="py-3 px-6 bg-coolGray-200 dark:bg-gray-600"> <span class="text-xs text-gray-600 dark:text-gray-300 font-semibold">PTX Status</span>
</div>
</th>
<th class="p-0">
<div class="py-3 px-6 rounded-tr-xl bg-coolGray-200 dark:bg-gray-600"> <span class="text-xs text-gray-600 dark:text-gray-300 font-semibold">Details</span>
</div>
</th>
</tr>
</thead>
<tbody>
{% for b in bids %}
<tr class="opacity-100 text-gray-500 dark:text-gray-100 hover:bg-coolGray-200 dark:hover:bg-gray-600">
<th scope="row" class="flex items-center py-7 px-46 text-gray-900 whitespace-nowrap"> <svg class="w-5 h-5 rounded-full ml-5" xmlns="http://www.w3.org/2000/svg" height="20" width="20" viewBox="0 0 24 24">
<g stroke-linecap="round" stroke-width="2" fill="none" stroke="#6b7280" stroke-linejoin="round">
<circle cx="12" cy="12" r="11"></circle>
<polyline points=" 12,6 12,12 18,12 " stroke="#6b7280"></polyline>
</g>
</svg>
<div class="pl-3">
<div class="font-semibold text-xs dark:text-white">{{ b[0] }}</div>
</div>
</th>
<td class="py-3 px-6 text-xs monospace"> <a href=/bid/{{ b[1] }}>{{ b[1]|truncate(20, True) }}</a> </td>
<td class="py-3 px-6 text-xs monospace"> <a href=/offer/{{ b[2] }}>{{ b[2]|truncate(20, True) }}</a> </td>
<td class="py-3 px-6 text-xs monospace"> <a href=/identity/{{ b[6] }}>{{ b[6] }}</a> </td>
<td class="py-3 px-6 text-xs">{{ b[3] }}</td>
<td class="py-3 px-6 text-xs">{{ b[4] }}</td>
<td class="py-3 px-6 text-xs">{{ b[5] }}</td>
{% if page_type_received or page_type_sent %}
<td class="py-3 px-6 text-xs"> <a 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 bg-blue-500 text-white hover:bg-blue-600 transition duration-200" href="/bid/{{ b[1] }}">Details</a>
</td> {% elif page_type_available %}
<td class="py-3 px-6 text-xs"> <a 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 bg-blue-500 text-white hover:bg-blue-600 transition duration-200" href="/bid/{{ b[1] }}">Accept</a>
</td>
{% endif %}
</tr>
</tbody>
{% endfor %}
</table> <input type="hidden" name="formid" value="{{ form_id }}"> <input type="hidden" name="pageno" value="{{ filters.page_no }}">
</div>
</div>
<div class="rounded-b-md">
<div class="w-full md:w-0/12">
<div class="flex flex-wrap justify-end pt-6 pr-6 border-t border-gray-100 dark:border-gray-400">
{% if filters.page_no > 1 %} <div class="w-full md:w-auto p-1.5">
<button type="submit" name='pageback' value="Previous" class="inline-flex items-center h-9 py-1 px-4 text-xs text-blue-50 font-semibold bg-blue-500 hover:bg-blue-600 rounded-lg transition duration-200 focus:ring-0 focus:outline-none">
{{ page_back_svg | safe }}
<span>Previous</span>
</button>
</div>
{% endif %}
<div class="flex items-center">
<div class="w-full md:w-auto p-1.5">
<p class="text-sm font-heading dark:text-white">Page: {{ filters.page_no }}</p>
</div> </div>
</div> </div>
{% if bids_count > 20 %} </div>
<div class="w-full md:w-auto p-1.5"> <button type="submit" name='pageforwards' value="Next" class="inline-flex items-center h-9 py-1 px-4 text-xs text-blue-50 font-semibold bg-blue-500 hover:bg-blue-600 rounded-lg transition duration-200 focus:ring-0 focus:outline-none">
<span>Next</span>
{{ page_forwards_svg | safe }}
</button>
</div>
{% endif %}
</div>
</div> </div>
</div> </div>
</div>
<div class="hidden rounded-lg" id="received" role="tabpanel" aria-labelledby="received-tab">
<div id="received-content">
<div class="container mt-5 mx-auto">
<div class="pt-6 pb-6 bg-coolGray-100 dark:bg-gray-500 rounded-xl">
<div class="px-6">
<div class="w-full mt-6 pb-6 overflow-x-auto">
<table class="w-full min-w-max">
<thead class="uppercase">
<tr class="text-left">
<th class="p-0">
<div class="py-3 px-6 rounded-tl-xl bg-coolGray-200 dark:bg-gray-600">
<span class="text-xs text-gray-600 dark:text-gray-300 font-semibold">Date/Time at</span>
</div>
</th>
<th class="p-0">
<div class="py-3 px-6 bg-coolGray-200 dark:bg-gray-600">
<span class="text-xs text-gray-600 dark:text-gray-300 font-semibold">Bid ID</span>
</div>
</th>
<th class="p-0">
<div class="py-3 px-6 bg-coolGray-200 dark:bg-gray-600">
<span class="text-xs text-gray-600 dark:text-gray-300 font-semibold">Offer ID</span>
</div>
</th>
<th class="p-0">
<div class="py-3 px-6 bg-coolGray-200 dark:bg-gray-600">
<span class="text-xs text-gray-600 dark:text-gray-300 font-semibold">Bid From</span>
</div>
</th>
<th class="p-0">
<div class="py-3 px-6 bg-coolGray-200 dark:bg-gray-600">
<span class="text-xs text-gray-600 dark:text-gray-300 font-semibold">Bid Status</span>
</div>
</th>
<th class="p-0">
<div class="py-3 px-6 bg-coolGray-200 dark:bg-gray-600">
<span class="text-xs text-gray-600 dark:text-gray-300 font-semibold">ITX Status</span>
</div>
</th>
<th class="p-0">
<div class="py-3 px-6 bg-coolGray-200 dark:bg-gray-600">
<span class="text-xs text-gray-600 dark:text-gray-300 font-semibold">PTX Status</span>
</div>
</th>
<th class="p-0">
<div class="py-3 px-6 rounded-tr-xl bg-coolGray-200 dark:bg-gray-600">
<span class="text-xs text-gray-600 dark:text-gray-300 font-semibold">Details</span>
</div>
</th>
</tr>
</thead>
<tbody>
{% for b in received_bids %}
<tr class="opacity-100 text-gray-500 dark:text-gray-100 hover:bg-coolGray-200 dark:hover:bg-gray-600">
<th scope="row" class="flex items-center py-7 px-46 text-gray-900 whitespace-nowrap">
<svg class="w-5 h-5 rounded-full ml-5" xmlns="http://www.w3.org/2000/svg" height="20" width="20" viewBox="0 0 24 24">
<g stroke-linecap="round" stroke-width="2" fill="none" stroke="#6b7280" stroke-linejoin="round">
<circle cx="12" cy="12" r="11"></circle>
<polyline points=" 12,6 12,12 18,12 " stroke="#6b7280"></polyline>
</g>
</svg>
<div class="pl-3">
<div class="font-semibold text-xs dark:text-white">{{ b[0] }}</div>
</div>
</th>
<td class="py-3 px-6 text-xs monospace"><a href=/bid/{{ b[1] }}>{{ b[1]|truncate(20, True) }}</a></td>
<td class="py-3 px-6 text-xs monospace"><a href=/offer/{{ b[2] }}>{{ b[2]|truncate(20, True) }}</a></td>
<td class="py-3 px-6 text-xs monospace"><a href=/identity/{{ b[6] }}>{{ b[6] }}</a></td>
<td class="py-3 px-6 text-xs">{{ b[3] }}</td>
<td class="py-3 px-6 text-xs">{{ b[4] }}</td>
<td class="py-3 px-6 text-xs">{{ b[5] }}</td>
<td class="py-3 px-6 text-xs">
<a 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" href="/bid/{{ b[1] }}">Details</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<div class="rounded-b-md">
<div class="w-full md:w-0/12">
<div class="flex flex-wrap justify-end pt-6 pr-6 border-t border-gray-100 dark:border-gray-400">
{% if filters.page_no > 1 %}
<div class="w-full md:w-auto p-1.5">
<input type="hidden" name="filter_key" value="{{ filter_key }}">
<input type="hidden" name="page_no" value="{{ filters.page_no - 1 }}">
<button type="submit" name='pageback' value="Previous" class="inline-flex items-center h-9 py-1 px-4 text-xs text-blue-50 font-semibold bg-blue-500 hover:bg-blue-600 rounded-lg transition duration-200 focus:ring-0 focus:outline-none">
{{ page_back_svg | safe }}
<span>Previous</span>
</button>
</div>
{% endif %}
<div class="flex items-center">
<div class="w-full md:w-auto p-1.5">
<p class="text-sm font-heading dark:text-white">Page: {{ filters.page_no }}</p>
</div>
</div>
{% if received_bids_count >= filters.limit %}
<div class="w-full md:w-auto p-1.5">
<input type="hidden" name="filter_key" value="{{ filter_key }}">
<input type="hidden" name="page_no" value="{{ filters.page_no + 1 }}">
<button type="submit" name='pageforwards' value="Next" class="inline-flex items-center h-9 py-1 px-4 text-xs text-blue-50 font-semibold bg-blue-500 hover:bg-blue-600 rounded-lg transition duration-200 focus:ring-0 focus:outline-none">
<span>Next</span>
{{ page_forwards_svg | safe }}
</button>
</div>
{% endif %}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<input type="hidden" name="formid" value="{{ form_id }}">
<input type="hidden" name="pageno" value="{{ filters.page_no }}">
<input type="hidden" name="filter_key" value="page_bids">
</form> </form>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</section>
</div> </div>
<script>
document.addEventListener('DOMContentLoaded', function() {
const tabs = document.querySelectorAll('#myTab button');
const contents = document.querySelectorAll('[role="tabpanel"]');
function switchTab(targetId) {
contents.forEach(content => {
content.classList.add('hidden');
});
const targetContent = document.querySelector(targetId);
if (targetContent) {
targetContent.classList.remove('hidden');
}
tabs.forEach(tab => {
const selected = tab.dataset.tabsTarget === targetId;
tab.setAttribute('aria-selected', selected);
if (selected) {
tab.classList.add('bg-gray-100', 'dark:bg-gray-600', 'text-gray-900', 'dark:text-white');
} else {
tab.classList.remove('bg-gray-100', 'dark:bg-gray-600', 'text-gray-900', 'dark:text-white');
}
});
}
tabs.forEach(tab => {
tab.addEventListener('click', () => {
switchTab(tab.dataset.tabsTarget);
});
});
switchTab('#sent');
});
</script>
{% include 'footer.html' %} {% include 'footer.html' %}
</body>
</html>

View file

@ -0,0 +1,200 @@
{% include 'header.html' %}
{% from 'style.html' import breadcrumb_line_svg, page_back_svg, page_forwards_svg, filter_clear_svg, filter_apply_svg, input_arrow_down_svg %}
<div class="container mx-auto">
<section class="p-5 mt-5">
<div class="flex flex-wrap items-center -m-2">
<div class="w-full md:w-1/2 p-2">
<ul class="flex flex-wrap items-center gap-x-3 mb-2">
<li><a class="flex font-medium text-xs text-coolGray-500 dark:text-gray-300 hover:text-coolGray-700" href="/"><p>Home</p></a></li>
<li>{{ breadcrumb_line_svg | safe }}</li>
<li><a class="flex font-medium text-xs text-coolGray-500 dark:text-gray-300 hover:text-coolGray-700" href="#">Bids Requests</a></li>
</ul>
</div>
</div>
</section>
<section class="py-4">
<div class="container px-4 mx-auto">
<div class="relative py-11 px-16 bg-coolGray-900 dark:bg-blue-500 rounded-md overflow-hidden">
<img class="absolute z-10 left-4 top-4" src="/static/images/elements/dots-red.svg" alt="">
<img class="absolute z-10 right-4 bottom-4" src="/static/images/elements/dots-red.svg" alt="">
<img class="absolute h-64 left-1/2 top-1/2 transform -translate-x-1/2 -translate-y-1/2 object-cover" src="/static/images/elements/wave.svg" alt="">
<div class="relative z-20 flex flex-wrap items-center -m-3">
<div class="w-full md:w-1/2 p-3">
<h2 class="mb-6 text-4xl font-bold text-white tracking-tighter">Bids Requests</h2>
<p class="font-normal text-coolGray-200 dark:text-white">Review and accept bids from other users</p>
</div>
</div>
</div>
</div>
</section>
{% include 'inc_messages.html' %}
<div class="pl-6 pr-6 pt-0 pb-0 mt-5 h-full overflow-hidden">
<div class="pb-6 border-coolGray-100">
<div class="flex flex-wrap items-center justify-between -m-2">
<div class="w-full mx-auto pt-2">
<form method="post">
<div class="flex items-center justify-center pb-4 dark:text-white">
<div class="rounded-b-md">
<div class="w-full md:w-0/12">
<div class="flex flex-wrap justify-center -m-1.5">
<div class="w-full md:w-auto p-1.5">
<div class="relative">
{{ input_arrow_down_svg | safe }}
<select name="sort_by" class="hover:border-blue-500 bg-gray-50 text-gray-900 appearance-none pr-10 dark:bg-gray-500 dark:text-white border border-gray-300 dark:border-gray-400 dark:text-gray-50 dark:placeholder-gray-50 text-sm rounded-lg outline-none focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 focus:ring-0">
<option value="created_at" {% if filters.sort_by=='created_at' %} selected{% endif %}>Time At</option>
</select>
</div>
</div>
<div class="w-full md:w-auto p-1.5">
<div class="relative">
{{ input_arrow_down_svg | safe }}
<select name="sort_dir" class="hover:border-blue-500 bg-gray-50 text-gray-900 appearance-none pr-10 dark:bg-gray-500 dark:text-white border border-gray-300 dark:border-gray-400 dark:text-gray-50 dark:placeholder-gray-50 text-sm rounded-lg outline-none focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 focus:ring-0">
<option value="asc" {% if filters.sort_dir=='asc' %} selected{% endif %}>Ascending</option>
<option value="desc" {% if filters.sort_dir=='desc' %} selected{% endif %}>Descending</option>
</select>
</div>
</div>
<div class="w-full md:w-auto p-1.5">
<div class="relative">
<button type="submit" name='clearfilters' value="Clear Filters" class="flex flex-wrap justify-center w-full px-4 py-2.5 font-medium text-sm hover:text-white dark:text-white dark:bg-gray-500 bg-coolGray-200 hover:bg-green-600 hover:border-green-600 rounded-lg transition duration-200 border border-coolGray-200 dark:border-gray-400 rounded-md shadow-button focus:ring-0 focus:outline-none">
<span>Clear Filters</span>
</button>
</div>
</div>
<div class="w-full md:w-auto p-1.5">
<div class="relative">
<button type="submit" name='applyfilters' value="Apply Filters" class="flex flex-wrap justify-center w-full px-4 py-2.5 font-medium text-sm text-white bg-blue-600 hover:bg-green-600 hover:border-green-600 rounded-lg transition duration-200 border border-blue-500 rounded-md shadow-button focus:ring-0 focus:outline-none">
{{ filter_apply_svg | safe }}
<span>Apply Filters</span>
</button>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="container mt-5 mx-auto">
<div class="pt-6 pb-6 bg-coolGray-100 dark:bg-gray-500 rounded-xl">
<div class="px-6">
<div class="w-full mt-6 pb-6 overflow-x-auto">
<table class="w-full min-w-max">
<thead class="uppercase">
<tr class="text-left">
<th class="p-0">
<div class="py-3 px-6 rounded-tl-xl bg-coolGray-200 dark:bg-gray-600">
<span class="text-xs text-gray-600 dark:text-gray-300 font-semibold">Date/Time at</span>
</div>
</th>
<th class="p-0">
<div class="py-3 px-6 bg-coolGray-200 dark:bg-gray-600">
<span class="text-xs text-gray-600 dark:text-gray-300 font-semibold">Bid ID</span>
</div>
</th>
<th class="p-0">
<div class="py-3 px-6 bg-coolGray-200 dark:bg-gray-600">
<span class="text-xs text-gray-600 dark:text-gray-300 font-semibold">Offer ID</span>
</div>
</th>
<th class="p-0">
<div class="py-3 px-6 bg-coolGray-200 dark:bg-gray-600">
<span class="text-xs text-gray-600 dark:text-gray-300 font-semibold">From</span>
</div>
</th>
<th class="p-0">
<div class="py-3 px-6 bg-coolGray-200 dark:bg-gray-600">
<span class="text-xs text-gray-600 dark:text-gray-300 font-semibold">Status</span>
</div>
</th>
<th class="p-0">
<div class="py-3 px-6 rounded-tr-xl bg-coolGray-200 dark:bg-gray-600">
<span class="text-xs text-gray-600 dark:text-gray-300 font-semibold">Actions</span>
</div>
</th>
</tr>
</thead>
<tbody>
{% for b in bids %}
<tr class="opacity-100 text-gray-500 dark:text-gray-100 hover:bg-coolGray-200 dark:hover:bg-gray-600">
<th scope="row" class="flex items-center py-7 px-46 text-gray-900 whitespace-nowrap">
<svg class="w-5 h-5 rounded-full ml-5" xmlns="http://www.w3.org/2000/svg" height="20" width="20" viewBox="0 0 24 24">
<g stroke-linecap="round" stroke-width="2" fill="none" stroke="#6b7280" stroke-linejoin="round">
<circle cx="12" cy="12" r="11"></circle>
<polyline points=" 12,6 12,12 18,12 " stroke="#6b7280"></polyline>
</g>
</svg>
<div class="pl-3">
<div class="font-semibold text-xs dark:text-white">{{ b[0] }}</div>
</div>
</th>
<td class="py-3 px-6 text-xs monospace"><a href=/bid/{{ b[1] }}>{{ b[1]|truncate(20, True) }}</a></td>
<td class="py-3 px-6 text-xs monospace"><a href=/offer/{{ b[2] }}>{{ b[2]|truncate(20, True) }}</a></td>
<td class="py-3 px-6 text-xs monospace"><a href=/identity/{{ b[6] }}>{{ b[6] }}</a></td>
<td class="py-3 px-6 text-xs">{{ b[3] }}</td>
<td class="py-3 px-6">
<div class="flex space-x-2">
<a 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" href="/bid/{{ b[1] }}">View</a>
{% if b[3] == "Received" %}
<form method="post" action="/bid/{{ b[1] }}" class="inline">
<input type="hidden" name="formid" value="{{ form_id }}">
<button type="submit" name="accept_bid" class="inline-block w-20 py-1 px-2 font-medium text-center text-sm rounded-md bg-green-500 text-white border border-green-500 hover:bg-green-600 transition duration-200">Accept</button>
</form>
{% endif %}
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
<div class="rounded-b-md">
<div class="w-full md:w-0/12">
<div class="flex flex-wrap justify-end pt-6 pr-6 border-t border-gray-100 dark:border-gray-400">
{% if filters.page_no > 1 %}
<div class="w-full md:w-auto p-1.5">
<input type="hidden" name="filter_key" value="{{ filter_key }}">
<input type="hidden" name="page_no" value="{{ filters.page_no - 1 }}">
<button type="submit" name='pageback' value="Previous" class="inline-flex items-center h-9 py-1 px-4 text-xs text-blue-50 font-semibold bg-blue-500 hover:bg-blue-600 rounded-lg transition duration-200 focus:ring-0 focus:outline-none">
{{ page_back_svg | safe }}
<span>Previous</span>
</button>
</div>
{% endif %}
<div class="flex items-center">
<div class="w-full md:w-auto p-1.5">
<p class="text-sm font-heading dark:text-white">Page: {{ filters.page_no }}</p>
</div>
</div>
{% if bids_count >= filters.limit %}
<div class="w-full md:w-auto p-1.5">
<input type="hidden" name="filter_key" value="{{ filter_key }}">
<input type="hidden" name="page_no" value="{{ filters.page_no + 1 }}">
<button type="submit" name='pageforwards' value="Next" class="inline-flex items-center h-9 py-1 px-4 text-xs text-blue-50 font-semibold bg-blue-500 hover:bg-blue-600 rounded-lg transition duration-200 focus:ring-0 focus:outline-none">
<span>Next</span>
{{ page_forwards_svg | safe }}
</button>
</div>
{% endif %}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<input type="hidden" name="formid" value="{{ form_id }}">
<input type="hidden" name="pageno" value="{{ filters.page_no }}">
<input type="hidden" name="filter_key" value="page_available_bids">
</form>
</div>
</div>
</div>
</div>
</div>
{% include 'footer.html' %}

File diff suppressed because it is too large Load diff

View file

@ -331,9 +331,6 @@ function getWebSocketConfig() {
</section> </section>
<section> <section>
<div id="jsonView" class="hidden mb-4">
<pre id="jsonContent" class="bg-gray-100 p-4 rounded overflow-auto" style="max-height: 300px;"></pre>
</div>
<div class="mt-5 lg:container mx-auto lg:px-0 px-6"> <div class="mt-5 lg:container mx-auto lg:px-0 px-6">
<div class="pt-0 pb-6 bg-coolGray-100 dark:bg-gray-500 rounded-xl"> <div class="pt-0 pb-6 bg-coolGray-100 dark:bg-gray-500 rounded-xl">
<div class="px-0"> <div class="px-0">
@ -399,11 +396,9 @@ function getWebSocketConfig() {
<span class="text-sm text-gray-600 dark:text-gray-300 font-semibold">Market +/-</span> <span class="text-sm text-gray-600 dark:text-gray-300 font-semibold">Market +/-</span>
<span class="sort-icon ml-1 text-gray-600 dark:text-gray-400" id="sort-icon-6"></span> <span class="sort-icon ml-1 text-gray-600 dark:text-gray-400" id="sort-icon-6"></span>
</div> </div>
</th> <th class="p-0">
<th class="p-0" data-sortable="true" data-column-index="7">
<div class="py-3 px-4 bg-coolGray-200 dark:bg-gray-600 rounded-tr-xl"> <div class="py-3 px-4 bg-coolGray-200 dark:bg-gray-600 rounded-tr-xl">
<span class="text-sm text-gray-600 dark:text-gray-300 font-semibold">Trade</span> <span class="text-sm text-gray-600 dark:text-gray-300 font-semibold">Trade</span>
<span class="sort-icon ml-1 text-gray-600 dark:text-gray-400" id="sort-icon-7"></span>
</div> </div>
</th> </th>
</tr> </tr>
@ -447,6 +442,5 @@ function getWebSocketConfig() {
</section> </section>
<input type="hidden" name="formid" value="{{ form_id }}"> <input type="hidden" name="formid" value="{{ form_id }}">
<script src="/static/js/offerstable.js"></script> <script src="/static/js/offerstable.js"></script>
{% include 'footer.html' %} {% include 'footer.html' %}

View file

@ -149,13 +149,12 @@ def page_bid(self, url_split, post_string):
) )
def page_bids( def page_bids(self, url_split, post_string, sent=False, available=False, received=False):
self, url_split, post_string, sent=False, available=False, received=False
):
server = self.server server = self.server
swap_client = server.swap_client swap_client = server.swap_client
swap_client.checkSystemStatus() swap_client.checkSystemStatus()
summary = swap_client.getSummary() summary = swap_client.getSummary()
filter_key = "page_available_bids" if available else "page_bids"
filters = { filters = {
"page_no": 1, "page_no": 1,
@ -169,26 +168,15 @@ def page_bids(
filters["bid_state_ind"] = BidStates.BID_RECEIVED filters["bid_state_ind"] = BidStates.BID_RECEIVED
filters["with_expired"] = False filters["with_expired"] = False
filter_prefix = (
"page_bids_sent"
if sent
else "page_bids_available" if available else "page_bids_received"
)
messages = [] messages = []
form_data = self.checkForm(post_string, "bids", messages) form_data = self.checkForm(post_string, "bids", messages)
if form_data: if form_data:
if have_data_entry(form_data, "clearfilters"): if have_data_entry(form_data, "clearfilters"):
swap_client.clearFilters(filter_prefix) swap_client.clearFilters(filter_key)
else: else:
if have_data_entry(form_data, "sort_by"): if have_data_entry(form_data, "sort_by"):
sort_by = get_data_entry(form_data, "sort_by") sort_by = get_data_entry(form_data, "sort_by")
ensure( ensure(sort_by in ["created_at"], "Invalid sort by")
sort_by
in [
"created_at",
],
"Invalid sort by",
)
filters["sort_by"] = sort_by filters["sort_by"] = sort_by
if have_data_entry(form_data, "sort_dir"): if have_data_entry(form_data, "sort_dir"):
sort_dir = get_data_entry(form_data, "sort_dir") sort_dir = get_data_entry(form_data, "sort_dir")
@ -199,7 +187,7 @@ def page_bids(
if state_ind != -1: if state_ind != -1:
try: try:
_ = BidStates(state_ind) _ = BidStates(state_ind)
except Exception as e: # noqa: F841 except Exception:
raise ValueError("Invalid state") raise ValueError("Invalid state")
filters["bid_state_ind"] = state_ind filters["bid_state_ind"] = state_ind
if have_data_entry(form_data, "with_expired"): if have_data_entry(form_data, "with_expired"):
@ -208,38 +196,52 @@ def page_bids(
set_pagination_filters(form_data, filters) set_pagination_filters(form_data, filters)
if have_data_entry(form_data, "applyfilters"): if have_data_entry(form_data, "applyfilters"):
swap_client.setFilters(filter_prefix, filters) swap_client.setFilters(filter_key, filters)
else: else:
saved_filters = swap_client.getFilters(filter_prefix) saved_filters = swap_client.getFilters(filter_key)
if saved_filters: if saved_filters:
filters.update(saved_filters) filters.update(saved_filters)
bids = swap_client.listBids(sent=sent, filters=filters)
page_data = { page_data = {
"bid_states": listBidStates(), "bid_states": listBidStates(),
} }
if available:
bids = swap_client.listBids(sent=False, filters=filters)
template = server.env.get_template("bids_available.html")
return self.render_template(
template,
{
"page_type_available": "Bids Available",
"page_type_available_description": "Bids available for you to accept.",
"messages": messages,
"filters": filters,
"data": page_data,
"summary": summary,
"filter_key": filter_key,
"bids": [
(format_timestamp(b[0]), b[2].hex(), b[3].hex(),
strBidState(b[5]), strTxState(b[7]),
strTxState(b[8]), b[11])
for b in bids
],
"bids_count": len(bids),
}
)
sent_bids = swap_client.listBids(sent=True, filters=filters)
received_bids = swap_client.listBids(sent=False, filters=filters)
template = server.env.get_template("bids.html") template = server.env.get_template("bids.html")
return self.render_template( return self.render_template(
template, template,
{ {
"page_type_sent": "Bids Sent" if sent else "",
"page_type_available": "Bids Available" if available else "",
"page_type_received": "Received Bids" if received else "",
"page_type_sent_description": (
"All the bids you have placed on offers." if sent else ""
),
"page_type_available_description": (
"Bids available for you to accept." if available else ""
),
"page_type_received_description": (
"All the bids placed on your offers." if received else ""
),
"messages": messages, "messages": messages,
"filters": filters, "filters": filters,
"data": page_data, "data": page_data,
"summary": summary, "summary": summary,
"bids": [ "filter_key": filter_key,
"sent_bids": [
( (
format_timestamp(b[0]), format_timestamp(b[0]),
b[2].hex(), b[2].hex(),
@ -249,8 +251,22 @@ def page_bids(
strTxState(b[8]), strTxState(b[8]),
b[11], b[11],
) )
for b in bids for b in sent_bids
], ],
"bids_count": len(bids), "received_bids": [
(
format_timestamp(b[0]),
b[2].hex(),
b[3].hex(),
strBidState(b[5]),
strTxState(b[7]),
strTxState(b[8]),
b[11],
)
for b in received_bids
],
"sent_bids_count": len(sent_bids),
"received_bids_count": len(received_bids),
"bids_count": len(sent_bids) + len(received_bids),
}, },
) )