mirror of
https://github.com/basicswap/basicswap.git
synced 2025-01-22 10:34:34 +00:00
JS/UI: Fix scrolling lag / tooltips + Various fixes and cleanup. (#236)
* JS/UI: Fix scrolling lag + Various fixes and cleanup. * Fix clear button * JS: Fix when page is hidden, reconnect and proper pause/resume logic. * JS: Fix tooltips bugs. * JS: Various fixes. * JS: Fix fetch system. * JS: Cleanup
This commit is contained in:
parent
443bd6917f
commit
f084c6f538
9 changed files with 938 additions and 309 deletions
190
basicswap/static/js/dropdown.js
Normal file
190
basicswap/static/js/dropdown.js
Normal file
|
@ -0,0 +1,190 @@
|
|||
(function(window) {
|
||||
'use strict';
|
||||
|
||||
function positionElement(targetEl, triggerEl, placement = 'bottom', offsetDistance = 8) {
|
||||
targetEl.style.visibility = 'hidden';
|
||||
targetEl.style.display = 'block';
|
||||
|
||||
const triggerRect = triggerEl.getBoundingClientRect();
|
||||
const targetRect = targetEl.getBoundingClientRect();
|
||||
const scrollLeft = window.pageXOffset || document.documentElement.scrollLeft;
|
||||
const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
|
||||
|
||||
let top, left;
|
||||
|
||||
top = triggerRect.bottom + offsetDistance;
|
||||
left = triggerRect.left + (triggerRect.width - targetRect.width) / 2;
|
||||
|
||||
switch (placement) {
|
||||
case 'bottom-start':
|
||||
left = triggerRect.left;
|
||||
break;
|
||||
case 'bottom-end':
|
||||
left = triggerRect.right - targetRect.width;
|
||||
break;
|
||||
}
|
||||
|
||||
const viewport = {
|
||||
width: window.innerWidth,
|
||||
height: window.innerHeight
|
||||
};
|
||||
|
||||
if (left < 10) left = 10;
|
||||
if (left + targetRect.width > viewport.width - 10) {
|
||||
left = viewport.width - targetRect.width - 10;
|
||||
}
|
||||
|
||||
targetEl.style.position = 'fixed';
|
||||
targetEl.style.top = `${Math.round(top)}px`;
|
||||
targetEl.style.left = `${Math.round(left)}px`;
|
||||
targetEl.style.margin = '0';
|
||||
targetEl.style.maxHeight = `${viewport.height - top - 10}px`;
|
||||
targetEl.style.overflow = 'auto';
|
||||
targetEl.style.visibility = 'visible';
|
||||
}
|
||||
|
||||
class Dropdown {
|
||||
constructor(targetEl, triggerEl, options = {}) {
|
||||
this._targetEl = targetEl;
|
||||
this._triggerEl = triggerEl;
|
||||
this._options = {
|
||||
placement: options.placement || 'bottom',
|
||||
offset: options.offset || 5,
|
||||
onShow: options.onShow || function() {},
|
||||
onHide: options.onHide || function() {}
|
||||
};
|
||||
this._visible = false;
|
||||
this._initialized = false;
|
||||
this._handleScroll = this._handleScroll.bind(this);
|
||||
this._handleResize = this._handleResize.bind(this);
|
||||
this._handleOutsideClick = this._handleOutsideClick.bind(this);
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
if (!this._initialized) {
|
||||
this._targetEl.style.margin = '0';
|
||||
this._targetEl.style.display = 'none';
|
||||
this._targetEl.style.position = 'fixed';
|
||||
this._targetEl.style.zIndex = '50';
|
||||
|
||||
this._setupEventListeners();
|
||||
this._initialized = true;
|
||||
}
|
||||
}
|
||||
|
||||
_setupEventListeners() {
|
||||
this._triggerEl.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
this.toggle();
|
||||
});
|
||||
|
||||
document.addEventListener('click', this._handleOutsideClick);
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape') this.hide();
|
||||
});
|
||||
window.addEventListener('scroll', this._handleScroll, true);
|
||||
window.addEventListener('resize', this._handleResize);
|
||||
}
|
||||
|
||||
_handleScroll() {
|
||||
if (this._visible) {
|
||||
requestAnimationFrame(() => {
|
||||
positionElement(
|
||||
this._targetEl,
|
||||
this._triggerEl,
|
||||
this._options.placement,
|
||||
this._options.offset
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
_handleResize() {
|
||||
if (this._visible) {
|
||||
requestAnimationFrame(() => {
|
||||
positionElement(
|
||||
this._targetEl,
|
||||
this._triggerEl,
|
||||
this._options.placement,
|
||||
this._options.offset
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
_handleOutsideClick(e) {
|
||||
if (this._visible &&
|
||||
!this._targetEl.contains(e.target) &&
|
||||
!this._triggerEl.contains(e.target)) {
|
||||
this.hide();
|
||||
}
|
||||
}
|
||||
|
||||
show() {
|
||||
if (!this._visible) {
|
||||
this._targetEl.style.display = 'block';
|
||||
this._targetEl.style.visibility = 'hidden';
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
positionElement(
|
||||
this._targetEl,
|
||||
this._triggerEl,
|
||||
this._options.placement,
|
||||
this._options.offset
|
||||
);
|
||||
|
||||
this._visible = true;
|
||||
this._options.onShow();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
hide() {
|
||||
if (this._visible) {
|
||||
this._targetEl.style.display = 'none';
|
||||
this._visible = false;
|
||||
this._options.onHide();
|
||||
}
|
||||
}
|
||||
|
||||
toggle() {
|
||||
if (this._visible) {
|
||||
this.hide();
|
||||
} else {
|
||||
this.show();
|
||||
}
|
||||
}
|
||||
|
||||
destroy() {
|
||||
document.removeEventListener('click', this._handleOutsideClick);
|
||||
window.removeEventListener('scroll', this._handleScroll, true);
|
||||
window.removeEventListener('resize', this._handleResize);
|
||||
this._initialized = false;
|
||||
}
|
||||
}
|
||||
|
||||
function initDropdowns() {
|
||||
document.querySelectorAll('[data-dropdown-toggle]').forEach(triggerEl => {
|
||||
const targetId = triggerEl.getAttribute('data-dropdown-toggle');
|
||||
const targetEl = document.getElementById(targetId);
|
||||
|
||||
if (targetEl) {
|
||||
const placement = triggerEl.getAttribute('data-dropdown-placement');
|
||||
new Dropdown(targetEl, triggerEl, {
|
||||
placement: placement || 'bottom-start'
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', initDropdowns);
|
||||
} else {
|
||||
initDropdowns();
|
||||
}
|
||||
|
||||
window.Dropdown = Dropdown;
|
||||
window.initDropdowns = initDropdowns;
|
||||
|
||||
})(window);
|
File diff suppressed because one or more lines are too long
|
@ -155,6 +155,9 @@ const WebSocketManager = {
|
|||
maxQueueSize: 1000,
|
||||
isIntentionallyClosed: false,
|
||||
handlers: {},
|
||||
isPageHidden: document.hidden,
|
||||
priceUpdatePaused: false,
|
||||
lastVisibilityChange: Date.now(),
|
||||
|
||||
connectionState: {
|
||||
isConnecting: false,
|
||||
|
@ -165,7 +168,6 @@ const WebSocketManager = {
|
|||
},
|
||||
|
||||
initialize() {
|
||||
console.log('Initializing WebSocket Manager');
|
||||
this.setupPageVisibilityHandler();
|
||||
this.connect();
|
||||
this.startHealthCheck();
|
||||
|
@ -179,12 +181,12 @@ const WebSocketManager = {
|
|||
this.handlePageVisible();
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('visibilitychange', this.handlers.visibilityChange);
|
||||
},
|
||||
|
||||
handlePageHidden() {
|
||||
console.log('📱 Page hidden, suspending operations');
|
||||
this.isPageHidden = true;
|
||||
this.priceUpdatePaused = true;
|
||||
this.stopHealthCheck();
|
||||
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
||||
this.isIntentionallyClosed = true;
|
||||
|
@ -193,12 +195,17 @@ const WebSocketManager = {
|
|||
},
|
||||
|
||||
handlePageVisible() {
|
||||
console.log('📱 Page visible, resuming operations');
|
||||
this.isPageHidden = false;
|
||||
this.lastVisibilityChange = Date.now();
|
||||
this.isIntentionallyClosed = false;
|
||||
|
||||
setTimeout(() => {
|
||||
this.priceUpdatePaused = false;
|
||||
if (!this.isConnected()) {
|
||||
this.connect();
|
||||
}
|
||||
this.startHealthCheck();
|
||||
}, 1000);
|
||||
},
|
||||
|
||||
startHealthCheck() {
|
||||
|
@ -217,7 +224,6 @@ const WebSocketManager = {
|
|||
|
||||
performHealthCheck() {
|
||||
if (!this.isConnected()) {
|
||||
console.warn('Health check: Connection lost, attempting reconnect');
|
||||
this.handleReconnect();
|
||||
return;
|
||||
}
|
||||
|
@ -225,13 +231,11 @@ const WebSocketManager = {
|
|||
const now = Date.now();
|
||||
const lastCheck = this.connectionState.lastHealthCheck;
|
||||
if (lastCheck && (now - lastCheck) > 60000) {
|
||||
console.warn('Health check: Connection stale, refreshing');
|
||||
this.handleReconnect();
|
||||
return;
|
||||
}
|
||||
|
||||
this.connectionState.lastHealthCheck = now;
|
||||
console.log('Health check passed');
|
||||
},
|
||||
|
||||
connect() {
|
||||
|
@ -248,7 +252,6 @@ const WebSocketManager = {
|
|||
const wsPort = config.port || window.ws_port || '11700';
|
||||
|
||||
if (!wsPort) {
|
||||
console.error('WebSocket port not configured');
|
||||
this.connectionState.isConnecting = false;
|
||||
return false;
|
||||
}
|
||||
|
@ -258,7 +261,6 @@ const WebSocketManager = {
|
|||
|
||||
this.connectionState.connectTimeout = setTimeout(() => {
|
||||
if (this.connectionState.isConnecting) {
|
||||
console.log('⏳ Connection attempt timed out');
|
||||
this.cleanup();
|
||||
this.handleReconnect();
|
||||
}
|
||||
|
@ -266,7 +268,6 @@ const WebSocketManager = {
|
|||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Error creating WebSocket:', error);
|
||||
this.connectionState.isConnecting = false;
|
||||
this.handleReconnect();
|
||||
return false;
|
||||
|
@ -277,7 +278,6 @@ const WebSocketManager = {
|
|||
if (!this.ws) return;
|
||||
|
||||
this.handlers.open = () => {
|
||||
console.log('🟢 WebSocket connected successfully');
|
||||
this.connectionState.isConnecting = false;
|
||||
this.reconnectAttempts = 0;
|
||||
clearTimeout(this.connectionState.connectTimeout);
|
||||
|
@ -291,18 +291,15 @@ const WebSocketManager = {
|
|||
const message = JSON.parse(event.data);
|
||||
this.handleMessage(message);
|
||||
} catch (error) {
|
||||
console.error('Error processing WebSocket message:', error);
|
||||
updateConnectionStatus('error');
|
||||
}
|
||||
};
|
||||
|
||||
this.handlers.error = (error) => {
|
||||
console.error('WebSocket error:', error);
|
||||
updateConnectionStatus('error');
|
||||
};
|
||||
|
||||
this.handlers.close = (event) => {
|
||||
console.log('🔴 WebSocket closed:', event.code, event.reason);
|
||||
this.connectionState.isConnecting = false;
|
||||
window.ws = null;
|
||||
updateConnectionStatus('disconnected');
|
||||
|
@ -320,7 +317,6 @@ const WebSocketManager = {
|
|||
|
||||
handleMessage(message) {
|
||||
if (this.messageQueue.length >= this.maxQueueSize) {
|
||||
console.warn('Message queue full, dropping oldest message');
|
||||
this.messageQueue.shift();
|
||||
}
|
||||
|
||||
|
@ -359,7 +355,6 @@ const WebSocketManager = {
|
|||
|
||||
this.messageQueue = [];
|
||||
} catch (error) {
|
||||
console.error('Error processing message queue:', error);
|
||||
} finally {
|
||||
this.processingQueue = false;
|
||||
}
|
||||
|
@ -372,8 +367,6 @@ const WebSocketManager = {
|
|||
|
||||
this.reconnectAttempts++;
|
||||
if (this.reconnectAttempts <= this.maxReconnectAttempts) {
|
||||
console.log(`Attempting to reconnect (${this.reconnectAttempts}/${this.maxReconnectAttempts})`);
|
||||
|
||||
const delay = Math.min(
|
||||
this.reconnectDelay * Math.pow(1.5, this.reconnectAttempts - 1),
|
||||
30000
|
||||
|
@ -385,9 +378,7 @@ const WebSocketManager = {
|
|||
}
|
||||
}, delay);
|
||||
} else {
|
||||
console.error('Max reconnection attempts reached');
|
||||
updateConnectionStatus('error');
|
||||
|
||||
setTimeout(() => {
|
||||
this.reconnectAttempts = 0;
|
||||
this.connect();
|
||||
|
@ -396,8 +387,6 @@ const WebSocketManager = {
|
|||
},
|
||||
|
||||
cleanup() {
|
||||
console.log('Cleaning up WebSocket resources');
|
||||
|
||||
clearTimeout(this.debounceTimeout);
|
||||
clearTimeout(this.reconnectTimeout);
|
||||
clearTimeout(this.connectionState.connectTimeout);
|
||||
|
@ -862,7 +851,7 @@ function continueInitialization() {
|
|||
//console.log('Initialization completed');
|
||||
}
|
||||
|
||||
function initializeFlowbiteTooltips() {
|
||||
function initializeTooltips() {
|
||||
if (typeof Tooltip === 'undefined') {
|
||||
console.warn('Tooltip is not defined. Make sure the required library is loaded.');
|
||||
return;
|
||||
|
@ -1074,6 +1063,11 @@ function getEmptyPriceData() {
|
|||
}
|
||||
|
||||
async function fetchLatestPrices() {
|
||||
if (WebSocketManager.isPageHidden || WebSocketManager.priceUpdatePaused) {
|
||||
const cachedData = CacheManager.get('prices_coingecko');
|
||||
return cachedData?.value || getEmptyPriceData();
|
||||
}
|
||||
|
||||
const PRICES_CACHE_KEY = 'prices_coingecko';
|
||||
const minRequestInterval = 60000;
|
||||
const currentTime = Date.now();
|
||||
|
@ -1081,7 +1075,6 @@ async function fetchLatestPrices() {
|
|||
if (!window.isManualRefresh) {
|
||||
const lastRequestTime = window.lastPriceRequest || 0;
|
||||
if (currentTime - lastRequestTime < minRequestInterval) {
|
||||
console.log('Request too soon, using cache');
|
||||
const cachedData = CacheManager.get(PRICES_CACHE_KEY);
|
||||
if (cachedData) {
|
||||
return cachedData.value;
|
||||
|
@ -1089,10 +1082,10 @@ async function fetchLatestPrices() {
|
|||
}
|
||||
}
|
||||
window.lastPriceRequest = currentTime;
|
||||
|
||||
if (!window.isManualRefresh) {
|
||||
const cachedData = CacheManager.get(PRICES_CACHE_KEY);
|
||||
if (cachedData && cachedData.remainingTime > 60000) {
|
||||
console.log('Using cached price data');
|
||||
latestPrices = cachedData.value;
|
||||
Object.entries(cachedData.value).forEach(([coin, prices]) => {
|
||||
if (prices.usd) {
|
||||
|
@ -1102,11 +1095,12 @@ async function fetchLatestPrices() {
|
|||
return cachedData.value;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
console.log('Initiating fresh price data fetch...');
|
||||
const existingCache = CacheManager.get(PRICES_CACHE_KEY, true);
|
||||
const fallbackData = existingCache ? existingCache.value : null;
|
||||
const url = `${offersConfig.apiEndpoints.coinGecko}/simple/price?ids=bitcoin,bitcoin-cash,dash,dogecoin,decred,litecoin,particl,pivx,monero,zano,wownero,zcoin&vs_currencies=USD,BTC&api_key=${offersConfig.apiKeys.coinGecko}`;
|
||||
|
||||
const response = await fetch('/json/readurl', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
|
@ -1121,19 +1115,21 @@ async function fetchLatestPrices() {
|
|||
}
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP Error: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.Error) {
|
||||
if (fallbackData) {
|
||||
console.log('Using fallback data due to API error');
|
||||
return fallbackData;
|
||||
}
|
||||
throw new Error(data.Error);
|
||||
}
|
||||
|
||||
if (data && Object.keys(data).length > 0) {
|
||||
console.log('Processing fresh price data');
|
||||
latestPrices = data;
|
||||
CacheManager.set(PRICES_CACHE_KEY, data, CACHE_DURATION);
|
||||
|
||||
|
@ -1144,10 +1140,11 @@ async function fetchLatestPrices() {
|
|||
});
|
||||
return data;
|
||||
}
|
||||
|
||||
if (fallbackData) {
|
||||
console.log('Using fallback data due to empty response');
|
||||
return fallbackData;
|
||||
}
|
||||
|
||||
const fallbackPrices = {};
|
||||
Object.keys(getEmptyPriceData()).forEach(coin => {
|
||||
const fallbackValue = tableRateModule.getFallbackValue(coin);
|
||||
|
@ -1155,14 +1152,13 @@ async function fetchLatestPrices() {
|
|||
fallbackPrices[coin] = { usd: fallbackValue, btc: null };
|
||||
}
|
||||
});
|
||||
|
||||
if (Object.keys(fallbackPrices).length > 0) {
|
||||
return fallbackPrices;
|
||||
}
|
||||
console.warn('No price data received');
|
||||
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.error('Price Fetch Error:', error);
|
||||
|
||||
const fallbackPrices = {};
|
||||
Object.keys(getEmptyPriceData()).forEach(coin => {
|
||||
const fallbackValue = tableRateModule.getFallbackValue(coin);
|
||||
|
@ -1556,7 +1552,7 @@ async function updateOffersTable() {
|
|||
offersBody.appendChild(fragment);
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
initializeFlowbiteTooltips();
|
||||
initializeTooltips();
|
||||
updateRowTimes();
|
||||
updatePaginationControls(totalPages);
|
||||
|
||||
|
@ -2014,14 +2010,12 @@ function createTooltips(offer, treatAsSentOffer, coinFrom, coinTo, fromAmount, t
|
|||
Grey: Less than 5 minutes left or expired
|
||||
</p>
|
||||
</div>
|
||||
<div class="tooltip-arrow" data-popper-arrow></div>
|
||||
</div>
|
||||
|
||||
<div id="tooltip-wallet-${uniqueId}" role="tooltip" class="inline-block absolute invisible z-50 py-2 px-3 text-sm font-medium text-white bg-gray-400 rounded-lg shadow-sm opacity-0 transition-opacity duration-300 tooltip">
|
||||
<div class="active-revoked-expired">
|
||||
<span class="bold">${treatAsSentOffer ? 'My' : ''} ${coinTo} Wallet</span>
|
||||
</div>
|
||||
<div class="tooltip-arrow pl-1" data-popper-arrow></div>
|
||||
</div>
|
||||
|
||||
<div id="tooltip-offer-${uniqueId}" role="tooltip" class="inline-block absolute z-50 py-2 px-3 text-sm font-medium text-white ${isRevoked ? 'bg-red-500' : (offer.is_own_offer ? 'bg-gray-300' : 'bg-green-700')} rounded-lg shadow-sm opacity-0 transition-opacity duration-300 tooltip">
|
||||
|
@ -2030,28 +2024,24 @@ function createTooltips(offer, treatAsSentOffer, coinFrom, coinTo, fromAmount, t
|
|||
${isRevoked ? 'Offer Revoked' : (offer.is_own_offer ? 'Edit Offer' : `Buy ${coinFrom}`)}
|
||||
</span>
|
||||
</div>
|
||||
<div class="tooltip-arrow pr-6" data-popper-arrow></div>
|
||||
</div>
|
||||
|
||||
<div id="tooltip-wallet-maker-${uniqueId}" role="tooltip" class="inline-block absolute invisible z-50 py-2 px-3 text-sm font-medium text-white bg-gray-400 rounded-lg shadow-sm opacity-0 transition-opacity duration-300 tooltip">
|
||||
<div class="active-revoked-expired">
|
||||
<span class="bold">${treatAsSentOffer ? 'My' : ''} ${coinFrom} Wallet</span>
|
||||
</div>
|
||||
<div class="tooltip-arrow pl-1" data-popper-arrow></div>
|
||||
</div>
|
||||
|
||||
<div id="tooltip-rate-${uniqueId}" role="tooltip" class="inline-block absolute invisible z-50 py-2 px-3 text-sm font-medium text-white bg-gray-400 rounded-lg shadow-sm opacity-0 transition-opacity duration-300 tooltip">
|
||||
<div class="tooltip-content">
|
||||
${combinedRateTooltip}
|
||||
</div>
|
||||
<div class="tooltip-arrow" data-popper-arrow></div>
|
||||
</div>
|
||||
|
||||
<div id="percentage-tooltip-${uniqueId}" role="tooltip" class="inline-block absolute invisible z-50 py-2 px-3 text-sm font-medium text-white bg-gray-400 rounded-lg shadow-sm opacity-0 transition-opacity duration-300 tooltip">
|
||||
<div class="tooltip-content">
|
||||
${percentageTooltipContent}
|
||||
</div>
|
||||
<div class="tooltip-arrow" data-popper-arrow></div>
|
||||
</div>
|
||||
|
||||
${createRecipientTooltip(uniqueId, identityInfo, identity, successRate, totalBids)}
|
||||
|
|
|
@ -35,7 +35,19 @@ const config = {
|
|||
sixMonths: { days: 180, interval: 'daily' },
|
||||
day: { days: 1, interval: 'hourly' }
|
||||
},
|
||||
currentResolution: 'year'
|
||||
currentResolution: 'year',
|
||||
requestTimeout: 60000, // 60 sec
|
||||
retryDelays: [5000, 15000, 30000],
|
||||
rateLimits: {
|
||||
coingecko: {
|
||||
requestsPerMinute: 50,
|
||||
minInterval: 1200 // 1.2 sec
|
||||
},
|
||||
cryptocompare: {
|
||||
requestsPerMinute: 30,
|
||||
minInterval: 2000 // 2 sec
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// UTILS
|
||||
|
@ -82,8 +94,13 @@ const api = {
|
|||
const xhr = new XMLHttpRequest();
|
||||
xhr.open('POST', '/json/readurl');
|
||||
xhr.setRequestHeader('Content-Type', 'application/json');
|
||||
xhr.timeout = 30000;
|
||||
xhr.ontimeout = () => reject(new AppError('Request timed out'));
|
||||
xhr.timeout = config.requestTimeout;
|
||||
|
||||
xhr.ontimeout = () => {
|
||||
logger.warn(`Request timed out for ${url}`);
|
||||
reject(new AppError('Request timed out'));
|
||||
};
|
||||
|
||||
xhr.onload = () => {
|
||||
logger.log(`Response for ${url}:`, xhr.responseText);
|
||||
if (xhr.status === 200) {
|
||||
|
@ -104,7 +121,12 @@ const api = {
|
|||
reject(new AppError(`HTTP Error: ${xhr.status} ${xhr.statusText}`, 'HTTPError'));
|
||||
}
|
||||
};
|
||||
xhr.onerror = () => reject(new AppError('Network error occurred', 'NetworkError'));
|
||||
|
||||
xhr.onerror = () => {
|
||||
logger.error(`Network error occurred for ${url}`);
|
||||
reject(new AppError('Network error occurred', 'NetworkError'));
|
||||
};
|
||||
|
||||
xhr.send(JSON.stringify({
|
||||
url: url,
|
||||
headers: headers
|
||||
|
@ -123,6 +145,12 @@ const api = {
|
|||
try {
|
||||
return await api.makePostRequest(url, headers);
|
||||
} catch (error) {
|
||||
logger.error(`CryptoCompare request failed for ${coin}:`, error);
|
||||
const cachedData = cache.get(`coinData_${coin}`);
|
||||
if (cachedData) {
|
||||
logger.info(`Using cached data for ${coin}`);
|
||||
return cachedData.value;
|
||||
}
|
||||
return { error: error.message };
|
||||
}
|
||||
});
|
||||
|
@ -282,11 +310,11 @@ const api = {
|
|||
const rateLimiter = {
|
||||
lastRequestTime: {},
|
||||
minRequestInterval: {
|
||||
coingecko: 30000,
|
||||
cryptocompare: 2000
|
||||
coingecko: config.rateLimits.coingecko.minInterval,
|
||||
cryptocompare: config.rateLimits.cryptocompare.minInterval
|
||||
},
|
||||
requestQueue: {},
|
||||
retryDelays: [2000, 5000, 10000],
|
||||
retryDelays: config.retryDelays,
|
||||
|
||||
canMakeRequest: function(apiName) {
|
||||
const now = Date.now();
|
||||
|
@ -328,6 +356,19 @@ const rateLimiter = {
|
|||
await new Promise(resolve => setTimeout(resolve, delay));
|
||||
return this.queueRequest(apiName, requestFn, retryCount + 1);
|
||||
}
|
||||
|
||||
if ((error.message.includes('timeout') || error.name === 'NetworkError') &&
|
||||
retryCount < this.retryDelays.length) {
|
||||
const delay = this.retryDelays[retryCount];
|
||||
logger.warn(`Request failed, retrying in ${delay/1000} seconds...`, {
|
||||
apiName,
|
||||
retryCount,
|
||||
error: error.message
|
||||
});
|
||||
await new Promise(resolve => setTimeout(resolve, delay));
|
||||
return this.queueRequest(apiName, requestFn, retryCount + 1);
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
@ -336,10 +377,12 @@ const rateLimiter = {
|
|||
return await this.requestQueue[apiName];
|
||||
|
||||
} catch (error) {
|
||||
if (error.message.includes('429')) {
|
||||
if (error.message.includes('429') ||
|
||||
error.message.includes('timeout') ||
|
||||
error.name === 'NetworkError') {
|
||||
const cachedData = cache.get(`coinData_${apiName}`);
|
||||
if (cachedData) {
|
||||
console.log('Rate limit reached, using cached data');
|
||||
console.log('Using cached data due to request failure');
|
||||
return cachedData.value;
|
||||
}
|
||||
}
|
||||
|
@ -439,8 +482,8 @@ displayCoinData: (coin, data) => {
|
|||
}
|
||||
updateUI(false);
|
||||
} catch (error) {
|
||||
logger.error('Failed to parse cache item:', error.message);
|
||||
localStorage.removeItem(key);
|
||||
logger.error(`Failed to display data for ${coin}:`, error.message);
|
||||
updateUI(true); // Show error state in UI
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -664,6 +707,8 @@ const chartModule = {
|
|||
family: "'Inter', sans-serif"
|
||||
},
|
||||
color: 'rgba(156, 163, 175, 1)',
|
||||
maxRotation: 0,
|
||||
minRotation: 0,
|
||||
callback: function(value) {
|
||||
const date = new Date(value);
|
||||
if (config.currentResolution === 'day') {
|
||||
|
@ -857,68 +902,67 @@ const chartModule = {
|
|||
|
||||
updateChart: async (coinSymbol, forceRefresh = false) => {
|
||||
try {
|
||||
const currentChartData = chartModule.chart?.data.datasets[0].data || [];
|
||||
if (currentChartData.length === 0) {
|
||||
chartModule.showChartLoader();
|
||||
}
|
||||
chartModule.loadStartTime = Date.now();
|
||||
const cacheKey = `chartData_${coinSymbol}_${config.currentResolution}`;
|
||||
let cachedData = !forceRefresh ? cache.get(cacheKey) : null;
|
||||
let data;
|
||||
if (cachedData && Object.keys(cachedData.value).length > 0) {
|
||||
data = cachedData.value;
|
||||
//console.log(`Using cached data for ${coinSymbol} (${config.currentResolution})`);
|
||||
console.log(`Using cached data for ${coinSymbol}`);
|
||||
} else {
|
||||
//console.log(`Fetching fresh data for ${coinSymbol} (${config.currentResolution})`);
|
||||
try {
|
||||
const allData = await api.fetchHistoricalDataXHR([coinSymbol]);
|
||||
data = allData[coinSymbol];
|
||||
if (!data || Object.keys(data).length === 0) {
|
||||
throw new Error(`No data returned for ${coinSymbol}`);
|
||||
}
|
||||
//console.log(`Caching new data for ${cacheKey}`);
|
||||
cache.set(cacheKey, data, config.cacheTTL);
|
||||
cachedData = null;
|
||||
} catch (error) {
|
||||
if (error.message.includes('429') && currentChartData.length > 0) {
|
||||
console.warn(`Rate limit hit for ${coinSymbol}, maintaining current chart`);
|
||||
return;
|
||||
}
|
||||
const expiredCache = localStorage.getItem(cacheKey);
|
||||
if (expiredCache) {
|
||||
try {
|
||||
const parsedCache = JSON.parse(expiredCache);
|
||||
data = parsedCache.value;
|
||||
console.log(`Using expired cache data for ${coinSymbol}`);
|
||||
} catch (cacheError) {
|
||||
throw error;
|
||||
}
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const chartData = chartModule.prepareChartData(coinSymbol, data);
|
||||
//console.log(`Prepared chart data for ${coinSymbol}:`, chartData.slice(0, 5));
|
||||
|
||||
if (chartData.length === 0) {
|
||||
throw new Error(`No valid chart data for ${coinSymbol}`);
|
||||
}
|
||||
|
||||
if (chartModule.chart) {
|
||||
if (chartData.length > 0 && chartModule.chart) {
|
||||
chartModule.chart.data.datasets[0].data = chartData;
|
||||
chartModule.chart.data.datasets[0].label = `${coinSymbol} Price (USD)`;
|
||||
|
||||
if (coinSymbol === 'WOW') {
|
||||
chartModule.chart.options.scales.x.time.unit = 'hour';
|
||||
chartModule.chart.options.scales.x.ticks.maxTicksLimit = 24;
|
||||
} else {
|
||||
const resolution = config.resolutions[config.currentResolution];
|
||||
chartModule.chart.options.scales.x.time.unit = resolution.interval === 'hourly' ? 'hour' : 'day';
|
||||
if (config.currentResolution === 'year' || config.currentResolution === 'sixMonths') {
|
||||
chartModule.chart.options.scales.x.time.unit = 'month';
|
||||
chartModule.chart.options.scales.x.time.unit =
|
||||
resolution.interval === 'hourly' ? 'hour' :
|
||||
config.currentResolution === 'year' ? 'month' : 'day';
|
||||
}
|
||||
if (config.currentResolution === 'year') {
|
||||
chartModule.chart.options.scales.x.ticks.maxTicksLimit = 12;
|
||||
} else if (config.currentResolution === 'sixMonths') {
|
||||
chartModule.chart.options.scales.x.ticks.maxTicksLimit = 6;
|
||||
} else if (config.currentResolution === 'day') {
|
||||
chartModule.chart.options.scales.x.ticks.maxTicksLimit = 24;
|
||||
}
|
||||
}
|
||||
|
||||
chartModule.chart.update('active');
|
||||
} else {
|
||||
//console.error('Chart object not initialized');
|
||||
throw new Error('Chart object not initialized');
|
||||
}
|
||||
|
||||
chartModule.currentCoin = coinSymbol;
|
||||
const loadTime = Date.now() - chartModule.loadStartTime;
|
||||
ui.updateLoadTimeAndCache(loadTime, cachedData);
|
||||
|
||||
}
|
||||
} catch (error) {
|
||||
//console.error(`Error updating chart for ${coinSymbol}:`, error);
|
||||
ui.displayErrorMessage(`Failed to update chart for ${coinSymbol}: ${error.message}`);
|
||||
console.error(`Error updating chart for ${coinSymbol}:`, error);
|
||||
if (!(chartModule.chart?.data.datasets[0].data.length > 0)) {
|
||||
chartModule.chart.data.datasets[0].data = [];
|
||||
chartModule.chart.update('active');
|
||||
}
|
||||
} finally {
|
||||
chartModule.hideChartLoader();
|
||||
}
|
||||
|
@ -994,8 +1038,8 @@ const app = {
|
|||
disabled: 'Auto-refresh: disabled',
|
||||
justRefreshed: 'Just refreshed',
|
||||
},
|
||||
cacheTTL: 5 * 60 * 1000, // 5 minutes
|
||||
minimumRefreshInterval: 60 * 1000, // 1 minute
|
||||
cacheTTL: 5 * 60 * 1000, // 5 min
|
||||
minimumRefreshInterval: 60 * 1000, // 1 min
|
||||
|
||||
init: () => {
|
||||
console.log('Initializing app...');
|
||||
|
@ -1208,7 +1252,6 @@ setupEventListeners: () => {
|
|||
return;
|
||||
}
|
||||
|
||||
|
||||
const lastGeckoRequest = rateLimiter.lastRequestTime['coingecko'] || 0;
|
||||
const timeSinceLastRequest = Date.now() - lastGeckoRequest;
|
||||
const waitTime = Math.max(0, rateLimiter.minRequestInterval.coingecko - timeSinceLastRequest);
|
||||
|
@ -1217,7 +1260,6 @@ setupEventListeners: () => {
|
|||
const seconds = Math.ceil(waitTime / 1000);
|
||||
ui.displayErrorMessage(`Rate limit: Please wait ${seconds} seconds before refreshing`);
|
||||
|
||||
|
||||
let remainingTime = seconds;
|
||||
const countdownInterval = setInterval(() => {
|
||||
remainingTime--;
|
||||
|
@ -1283,7 +1325,6 @@ setupEventListeners: () => {
|
|||
await chartModule.updateChart(chartModule.currentCoin, true);
|
||||
} catch (chartError) {
|
||||
console.error('Chart update failed:', chartError);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1315,7 +1356,6 @@ setupEventListeners: () => {
|
|||
} catch (error) {
|
||||
console.error('Critical error during refresh:', error);
|
||||
|
||||
|
||||
let countdown = 10;
|
||||
ui.displayErrorMessage(`Refresh failed: ${error.message}. Please try again later. (${countdown}s)`);
|
||||
|
||||
|
@ -1338,7 +1378,7 @@ setupEventListeners: () => {
|
|||
app.scheduleNextRefresh();
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
updateNextRefreshTime: () => {
|
||||
console.log('Updating next refresh time display');
|
||||
|
|
115
basicswap/static/js/tabs.js
Normal file
115
basicswap/static/js/tabs.js
Normal file
|
@ -0,0 +1,115 @@
|
|||
(function(window) {
|
||||
'use strict';
|
||||
|
||||
class Tabs {
|
||||
constructor(tabsEl, items = [], options = {}) {
|
||||
this._tabsEl = tabsEl;
|
||||
this._items = items;
|
||||
this._activeTab = options.defaultTabId ? this.getTab(options.defaultTabId) : null;
|
||||
this._options = {
|
||||
defaultTabId: options.defaultTabId || null,
|
||||
activeClasses: options.activeClasses || 'text-blue-600 hover:text-blue-600 dark:text-blue-500 dark:hover:text-blue-500 border-blue-600 dark:border-blue-500',
|
||||
inactiveClasses: options.inactiveClasses || 'dark:border-transparent text-gray-500 hover:text-gray-600 dark:text-gray-400 border-gray-100 hover:border-gray-300 dark:border-gray-700 dark:hover:text-gray-300',
|
||||
onShow: options.onShow || function() {}
|
||||
};
|
||||
this._initialized = false;
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
if (this._items.length && !this._initialized) {
|
||||
if (!this._activeTab) {
|
||||
this.setActiveTab(this._items[0]);
|
||||
}
|
||||
|
||||
this.show(this._activeTab.id, true);
|
||||
|
||||
this._items.forEach(tab => {
|
||||
tab.triggerEl.addEventListener('click', () => {
|
||||
this.show(tab.id);
|
||||
});
|
||||
});
|
||||
|
||||
this._initialized = true;
|
||||
}
|
||||
}
|
||||
|
||||
show(tabId, force = false) {
|
||||
const tab = this.getTab(tabId);
|
||||
|
||||
if ((tab !== this._activeTab) || force) {
|
||||
this._items.forEach(t => {
|
||||
if (t !== tab) {
|
||||
t.triggerEl.classList.remove(...this._options.activeClasses.split(' '));
|
||||
t.triggerEl.classList.add(...this._options.inactiveClasses.split(' '));
|
||||
t.targetEl.classList.add('hidden');
|
||||
t.triggerEl.setAttribute('aria-selected', false);
|
||||
}
|
||||
});
|
||||
|
||||
tab.triggerEl.classList.add(...this._options.activeClasses.split(' '));
|
||||
tab.triggerEl.classList.remove(...this._options.inactiveClasses.split(' '));
|
||||
tab.triggerEl.setAttribute('aria-selected', true);
|
||||
tab.targetEl.classList.remove('hidden');
|
||||
|
||||
this.setActiveTab(tab);
|
||||
this._options.onShow(this, tab);
|
||||
}
|
||||
}
|
||||
|
||||
getTab(id) {
|
||||
return this._items.find(t => t.id === id);
|
||||
}
|
||||
|
||||
getActiveTab() {
|
||||
return this._activeTab;
|
||||
}
|
||||
|
||||
setActiveTab(tab) {
|
||||
this._activeTab = tab;
|
||||
}
|
||||
}
|
||||
|
||||
function initTabs() {
|
||||
document.querySelectorAll('[data-tabs-toggle]').forEach(tabsEl => {
|
||||
const items = [];
|
||||
let defaultTabId = null;
|
||||
|
||||
tabsEl.querySelectorAll('[role="tab"]').forEach(triggerEl => {
|
||||
const isActive = triggerEl.getAttribute('aria-selected') === 'true';
|
||||
const tab = {
|
||||
id: triggerEl.getAttribute('data-tabs-target'),
|
||||
triggerEl: triggerEl,
|
||||
targetEl: document.querySelector(triggerEl.getAttribute('data-tabs-target'))
|
||||
};
|
||||
items.push(tab);
|
||||
|
||||
if (isActive) {
|
||||
defaultTabId = tab.id;
|
||||
}
|
||||
});
|
||||
|
||||
new Tabs(tabsEl, items, {
|
||||
defaultTabId: defaultTabId
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const style = document.createElement('style');
|
||||
style.textContent = `
|
||||
[data-tabs-toggle] [role="tab"] {
|
||||
cursor: pointer;
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', initTabs);
|
||||
} else {
|
||||
initTabs();
|
||||
}
|
||||
|
||||
window.Tabs = Tabs;
|
||||
window.initTabs = initTabs;
|
||||
|
||||
})(window);
|
309
basicswap/static/js/tooltips.js
Normal file
309
basicswap/static/js/tooltips.js
Normal file
|
@ -0,0 +1,309 @@
|
|||
(function(window) {
|
||||
'use strict';
|
||||
|
||||
const tooltipContainer = document.createElement('div');
|
||||
tooltipContainer.className = 'tooltip-container';
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
.tooltip-container {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 0;
|
||||
overflow: visible;
|
||||
pointer-events: none;
|
||||
z-index: 9999;
|
||||
}
|
||||
`;
|
||||
|
||||
function ensureContainerExists() {
|
||||
if (!document.body.contains(tooltipContainer)) {
|
||||
document.body.appendChild(tooltipContainer);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
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));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function positionElement(targetEl, triggerEl, placement = 'top', offsetDistance = 8) {
|
||||
const triggerRect = triggerEl.getBoundingClientRect();
|
||||
const targetRect = targetEl.getBoundingClientRect();
|
||||
let top, left;
|
||||
|
||||
switch (placement) {
|
||||
case 'top':
|
||||
top = triggerRect.top - targetRect.height - offsetDistance;
|
||||
left = triggerRect.left + (triggerRect.width - targetRect.width) / 2;
|
||||
break;
|
||||
case 'bottom':
|
||||
top = triggerRect.bottom + offsetDistance;
|
||||
left = triggerRect.left + (triggerRect.width - targetRect.width) / 2;
|
||||
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() {
|
||||
this._triggerEl.addEventListener('mouseenter', this._showHandler);
|
||||
this._triggerEl.addEventListener('mouseleave', this._hideHandler);
|
||||
this._triggerEl.addEventListener('focus', this._showHandler);
|
||||
this._triggerEl.addEventListener('blur', this._hideHandler);
|
||||
|
||||
this._targetEl.addEventListener('mouseenter', () => {
|
||||
clearTimeout(this._hideTimeout);
|
||||
clearTimeout(this._showTimeout);
|
||||
this._visible = true;
|
||||
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);
|
||||
|
||||
function initTooltips() {
|
||||
ensureContainerExists();
|
||||
|
||||
document.querySelectorAll('[data-tooltip-target]').forEach(triggerEl => {
|
||||
if (tooltips.has(triggerEl)) return;
|
||||
|
||||
const targetId = triggerEl.getAttribute('data-tooltip-target');
|
||||
const targetEl = document.getElementById(targetId);
|
||||
|
||||
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 (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', initTooltips);
|
||||
} else {
|
||||
initTooltips();
|
||||
}
|
||||
|
||||
window.Tooltip = Tooltip;
|
||||
window.initTooltips = initTooltips;
|
||||
|
||||
})(window);
|
|
@ -8,13 +8,18 @@
|
|||
{% endif %}
|
||||
<script src="/static/js/libs/chart.js"></script>
|
||||
<script src="/static/js/libs/chartjs-adapter-date-fns.bundle.min.js"></script>
|
||||
|
||||
<link type="text/css" media="all" href="/static/css/libs/flowbite.min.css" rel="stylesheet" />
|
||||
<link type="text/css" media="all" href="/static/css/libs/tailwind.min.css" rel="stylesheet">
|
||||
<link type="text/css" media="all" href="/static/css/style.css" rel="stylesheet">
|
||||
<script src="/static/js/main.js"></script>
|
||||
<script src="/static/js/libs/flowbite.js"></script>
|
||||
|
||||
<script>
|
||||
<script src="/static/js/main.js"></script>
|
||||
|
||||
<script src="/static/js/tabs.js"></script>
|
||||
<script src="/static/js/dropdown.js"></script>
|
||||
<script src="/static/js/tooltips.js"></script>
|
||||
|
||||
<script>
|
||||
const isDarkMode =
|
||||
localStorage.getItem('color-theme') === 'dark' ||
|
||||
(!localStorage.getItem('color-theme') &&
|
||||
|
@ -451,7 +456,6 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||
|
||||
<div id="tooltip-your-offers" role="tooltip" class="inline-block absolute invisible z-10 py-2 px-3 text-sm font-medium text-white bg-blue-500 rounded-lg shadow-sm opacity-0 transition-opacity duration-300 tooltip">
|
||||
<p><b>Total:</b> {{ summary.num_sent_offers }}</p>
|
||||
<div class="tooltip-arrow" data-popper-arrow></div>
|
||||
</div>
|
||||
|
||||
</li>
|
||||
|
@ -468,7 +472,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||
|
||||
<div id="tooltip-bids-received" role="tooltip" class="inline-block absolute invisible z-10 py-2 px-3 text-sm font-medium text-white bg-blue-500 rounded-lg shadow-sm opacity-0 transition-opacity duration-300 tooltip">
|
||||
<p><b>Total:</b> {{ summary.num_recv_bids }}</p>
|
||||
<div class="tooltip-arrow" data-popper-arrow></div>
|
||||
|
||||
</div>
|
||||
<li>
|
||||
<a data-tooltip-target="tooltip-bids-sent" class="flex mr-10 items-center text-sm text-gray-400 hover:text-gray-600 dark:text-gray-100 dark:hover:text-gray-100" href="/sentbids">
|
||||
|
@ -476,7 +480,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||
<span>Bids Sent</span><span class="inline-flex justify-center items-center text-xs font-semibold ml-3 mr-2 px-2.5 py-1 font-small text-white bg-blue-500 rounded-full">{{ summary.num_sent_active_bids }}</span></a>
|
||||
<div id="tooltip-bids-sent" role="tooltip" class="inline-block absolute invisible z-10 py-2 px-3 text-sm font-medium text-white bg-blue-500 rounded-lg shadow-sm opacity-0 transition-opacity duration-300 tooltip">
|
||||
<p><b>Total:</b> {{ summary.num_sent_bids }}</p>
|
||||
<div class="tooltip-arrow" data-popper-arrow></div>
|
||||
|
||||
</div>
|
||||
|
||||
</li>
|
||||
|
|
|
@ -747,40 +747,24 @@ function resetForm() {
|
|||
const bidAmountInput = document.getElementById('bid_amount');
|
||||
const bidRateInput = document.getElementById('bid_rate');
|
||||
const validMinsInput = document.querySelector('input[name="validmins"]');
|
||||
const addrFromSelect = document.querySelector('select[name="addr_from"]');
|
||||
|
||||
const amtVar = document.getElementById('amt_var')?.value === 'True';
|
||||
if (bidAmountSendInput) {
|
||||
const defaultSendAmount = bidAmountSendInput.getAttribute('max');
|
||||
bidAmountSendInput.value = defaultSendAmount;
|
||||
bidAmountSendInput.value = amtVar ? '' : bidAmountSendInput.getAttribute('max');
|
||||
}
|
||||
|
||||
if (bidAmountInput) {
|
||||
const defaultReceiveAmount = bidAmountInput.getAttribute('max');
|
||||
bidAmountInput.value = defaultReceiveAmount;
|
||||
bidAmountInput.value = amtVar ? '' : bidAmountInput.getAttribute('max');
|
||||
}
|
||||
|
||||
if (bidRateInput && !bidRateInput.disabled) {
|
||||
const defaultRate = document.getElementById('offer_rate')?.value || '';
|
||||
bidRateInput.value = defaultRate;
|
||||
}
|
||||
|
||||
if (validMinsInput) {
|
||||
validMinsInput.value = "60";
|
||||
}
|
||||
|
||||
if (addrFromSelect) {
|
||||
if (addrFromSelect.options.length > 1) {
|
||||
addrFromSelect.selectedIndex = 1;
|
||||
} else {
|
||||
addrFromSelect.selectedIndex = 0;
|
||||
}
|
||||
const selectedOption = addrFromSelect.options[addrFromSelect.selectedIndex];
|
||||
saveAddress(selectedOption.value, selectedOption.text);
|
||||
}
|
||||
|
||||
if (!amtVar) {
|
||||
updateBidParams('rate');
|
||||
}
|
||||
updateModalValues();
|
||||
|
||||
const errorMessages = document.querySelectorAll('.error-message');
|
||||
errorMessages.forEach(msg => msg.remove());
|
||||
|
||||
|
|
|
@ -26,10 +26,10 @@ function getWebSocketConfig() {
|
|||
|
||||
{% if sent_offers %}
|
||||
<div class="lg:container mx-auto">
|
||||
<section class="p-5 mt-5">
|
||||
<div class="flex flex-wrap items-center">
|
||||
<div class="w-full md:w-1/2 p-2">
|
||||
<ul class="flex flex-wrap items-center gap-x-3 mb-2">
|
||||
<section class="p-3 mt-2">
|
||||
<div class="flex items-center">
|
||||
<div class="w-full">
|
||||
<ul class="flex items-center gap-x-2 mb-1">
|
||||
<li><a class="flex font-medium text-xs text-coolGray-500 dark:text-gray-300 hover:text-coolGray-700" href="/">Home</a></li>
|
||||
<li>{{ breadcrumb_line_svg | safe }}</li>
|
||||
<li>
|
||||
|
@ -46,26 +46,26 @@ function getWebSocketConfig() {
|
|||
{% endif %}
|
||||
|
||||
{% if sent_offers %}
|
||||
<section class="py-5">
|
||||
<section class="py-3">
|
||||
{% else %}
|
||||
<section class="py-5 px-6 mt-5">
|
||||
<section class="py-3 px-4 mt-3">
|
||||
{% endif %}
|
||||
<div class="lg:container mx-auto">
|
||||
<div class="relative py-11 px-16 bg-coolGray-900 dark:bg-gray-500 rounded-md overflow-hidden">
|
||||
<img class="absolute z-10 left-4 top-4 right-4 bottom-4" src="/static/images/elements/dots-red.svg" alt="dots-red">
|
||||
<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="wave">
|
||||
<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">{{ page_type }}</h2>
|
||||
<div class="relative py-6 px-8 bg-coolGray-900 dark:bg-gray-500 rounded-md overflow-hidden">
|
||||
|
||||
<img class="absolute h-48 left-1/2 top-1/2 transform -translate-x-1/2 -translate-y-1/2 object-cover" src="/static/images/elements/wave.svg" alt="wave">
|
||||
<div class="relative z-20 flex flex-wrap items-center -m-2">
|
||||
<div class="w-full md:w-1/2 p-2">
|
||||
<h2 class="mb-3 text-3xl font-bold text-white tracking-tighter">{{ page_type }}</h2>
|
||||
<p class="font-normal text-coolGray-200 dark:text-white">{{ page_type_description }}</p>
|
||||
</div>
|
||||
<div class="rounded-full{{ page_button }} w-full md:w-1/2 p-3 p-6 lg:container flex flex-wrap items-center justify-end items-center mx-auto">
|
||||
<a id="refresh" href="/newoffer" class="rounded-full flex flex-wrap justify-center px-5 py-3 bg-blue-500 hover:bg-green-600 hover:border-green-600 font-medium text-sm text-white border border-blue-500 rounded-md focus:ring-0 focus:outline-none">{{ place_new_offer_svg | safe }}<span>Place new Offer</span></a>
|
||||
<div class="rounded-full{{ page_button }} w-full md:w-1/2 p-2 lg:container flex flex-wrap items-center justify-end mx-auto">
|
||||
<a id="refresh" href="/newoffer" class="rounded-full flex flex-wrap justify-center px-4 py-2 bg-blue-500 hover:bg-green-600 hover:border-green-600 font-medium text-sm text-white border border-blue-500 rounded-md focus:ring-0 focus:outline-none">{{ place_new_offer_svg | safe }}<span>Place new Offer</span></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
{% include 'inc_messages.html' %}
|
||||
|
||||
|
@ -90,36 +90,28 @@ function getWebSocketConfig() {
|
|||
<span id="load-time hidden" class="mr-2 text-sm text-gray-600 dark:text-gray-300"></span>
|
||||
<span id="last-refreshed-time hidden" class="mr-2 text-sm text-gray-600 dark:text-gray-300"></span>
|
||||
<span id="cache-status hidden" class="mr-4 text-sm text-gray-600 dark:text-gray-300"></span>
|
||||
<span id="tor-status" class="mr-4 text-sm hidden {% if tor_established %}text-green-500{% else %}text-red-500{% endif %}"> Tor {% if tor_established %}ON{% else %}OFF{% endif %}
|
||||
<span id="tor-status" class="mr-4 text-sm hidden {% if tor_established %}text-green-500{% else %}text-red-500{% endif %}">
|
||||
Tor {% if tor_established %}ON{% else %}OFF{% endif %}
|
||||
<a href="https://academy.particl.io/en/latest/basicswap-guides/basicswapguides_tor.html" target="_blank" rel="noopener noreferrer" class="underline">(?)</a>
|
||||
</span>
|
||||
<span id="next-refresh-time" class="mr-4 text-sm text-gray-600 dark:text-gray-300">
|
||||
<span id="next-refresh-label"></span>
|
||||
<span id="next-refresh-value"></span>
|
||||
</span>
|
||||
<div id="tooltip-volume" role="tooltip" class="inline-block absolute invisible z-10 py-2 px-3 text-sm font-medium text-white bg-blue-500 rounded-lg shadow-sm opacity-0 transition-opacity duration-300 tooltip">
|
||||
Toggle Coin Volume
|
||||
<div class="tooltip-arrow" data-popper-arrow></div>
|
||||
</div>
|
||||
<div id="tooltip-volume" role="tooltip" class="inline-block absolute invisible z-10 py-2 px-3 text-sm font-medium text-white bg-blue-500 rounded-lg shadow-sm opacity-0 transition-opacity duration-300 tooltip">Toggle Coin Volume</div>
|
||||
<button id="toggle-volume" data-tooltip-target="tooltip-volume" class="text-white font-bold py-2 px-4 rounded mr-2 focus:outline-none focus:ring-0 transition-colors duration-200" title="Toggle Volume">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path d="M2 11a1 1 0 011-1h2a1 1 0 011 1v5a1 1 0 01-1 1H3a1 1 0 01-1-1v-5zM8 7a1 1 0 011-1h2a1 1 0 011 1v9a1 1 0 01-1 1H9a1 1 0 01-1-1V7zM14 4a1 1 0 011-1h2a1 1 0 011 1v12a1 1 0 01-1 1h-2a1 1 0 01-1-1V4z" />
|
||||
</svg>
|
||||
</button>
|
||||
<div id="tooltip-refresh" role="tooltip" class="inline-block absolute invisible z-10 py-2 px-3 text-sm font-medium text-white bg-blue-500 rounded-lg shadow-sm opacity-0 transition-opacity duration-300 tooltip">
|
||||
Refresh Charts/Prices & Clear Cache
|
||||
<div class="tooltip-arrow" data-popper-arrow></div>
|
||||
</div>
|
||||
<div id="tooltip-refresh" role="tooltip" class="inline-block absolute invisible z-10 py-2 px-3 text-sm font-medium text-white bg-blue-500 rounded-lg shadow-sm opacity-0 transition-opacity duration-300 tooltip">Refresh Charts/Prices & Clear Cache</div>
|
||||
<button id="refresh-all" data-tooltip-target="tooltip-refresh" class="text-gray-600 dark:text-gray-400 font-bold py-2 px-4 rounded mr-2 focus:outline-none focus:ring-0 transition-colors duration-200" title="Refresh Data & Clear Cache">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
||||
<circle cx="12" cy="12" r="3" />
|
||||
</svg>
|
||||
</button>
|
||||
<div id="tooltip-auto" role="tooltip" class="inline-block absolute invisible z-10 py-2 px-3 text-sm font-medium text-white bg-blue-500 rounded-lg shadow-sm opacity-0 transition-opacity duration-300 tooltip">
|
||||
Auto Refresh Enable/Disable
|
||||
<div class="tooltip-arrow" data-popper-arrow></div>
|
||||
</div>
|
||||
<div id="tooltip-auto" role="tooltip" class="inline-block absolute invisible z-10 py-2 px-3 text-sm font-medium text-white bg-blue-500 rounded-lg shadow-sm opacity-0 transition-opacity duration-300 tooltip">Auto Refresh Enable/Disable</div>
|
||||
<button id="toggle-auto-refresh" data-enabled="false" data-tooltip-target="tooltip-auto" class="text-white font-bold py-2 px-4 rounded mr-2 focus:outline-none focus:ring-0 transition-colors duration-200" title="Enable Auto-Refresh">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M4 2a1 1 0 011 1v2.101a7.002 7.002 0 0111.601 2.566 1 1 0 11-1.885.666A5.002 5.002 0 005.999 7H9a1 1 0 010 2H4a1 1 0 01-1-1V3a1 1 0 011-1zm.008 9.057a1 1 0 011.276.61A5.002 5.002 0 0014.001 13H11a1 1 0 110-2h5a1 1 0 011 1v5a1 1 0 11-2 0v-2.101a7.002 7.002 0 01-11.601-2.566 1 1 0 01.61-1.276z" clip-rule="evenodd" />
|
||||
|
@ -137,18 +129,25 @@ function getWebSocketConfig() {
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="error-overlay" class="error-overlay hidden absolute inset-0 bg-black bg-opacity-50 flex items-center justify-center">
|
||||
<div id="error-content" class="error-content bg-white dark:bg-gray-800 rounded-lg p-6 w-full max-w-2xl mx-4 relative">
|
||||
<button id="close-error" class="absolute top-3 right-3 bg-red-500 text-white rounded-full p-2 hover:bg-red-600 focus:outline-none">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<div id="error-overlay" class="notice-overlay hidden absolute inset-0 bg-opacity-30 backdrop-blur-sm flex items-center justify-center">
|
||||
<div id="error-content" class="notice-content bg-white dark:bg-gray-800 rounded-xl p-6 w-full max-w-2xl mx-4 relative shadow-lg">
|
||||
<div class="absolute top-0 left-0 right-0 h-1 bg-gradient-to-r from-blue-400 via-blue-500 to-blue-600"></div>
|
||||
<button id="close-error" class="absolute top-3 right-3 text-gray-400 hover:text-gray-500 dark:text-gray-500 dark:hover:text-white rounded-full p-2 hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors focus:outline-none">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
<p class="text-red-600 font-semibold text-xl mb-4">Error</p>
|
||||
<p id="error-message" class="text-gray-700 dark:text-gray-300 text-lg mb-6"></p>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">To review or update your Chart API Key(s), navigate to <a href="/settings" class="text-blue-500 hover:underline">Settings & Tools > Settings > General (TAB)</a>.
|
||||
</p>
|
||||
<div class="flex items-center gap-3 mb-4">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-blue-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" />
|
||||
</svg>
|
||||
<p class="text-lg font-medium text-gray-900 dark:text-gray-100">Notice</p>
|
||||
</div>
|
||||
<p id="error-message" class="text-gray-600 dark:text-gray-300 text-base mb-6"></p>
|
||||
<div class="text-sm text-gray-500 dark:text-gray-400">
|
||||
Need to update your (API) settings?
|
||||
<a href="/settings" class="text-blue-500 hover:text-blue-600 dark:hover:text-blue-400 font-medium ml-1 hover:underline">Go to Settings -> General</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
Loading…
Reference in a new issue