mirror of
https://github.com/basicswap/basicswap.git
synced 2024-12-22 19:49:20 +00:00
Merge pull request #163 from gerlofvanek/offers-3
ui: Offers: Fixed cache, better filtering on tables, visually on refresh.
This commit is contained in:
commit
d417a46e67
1 changed files with 339 additions and 127 deletions
|
@ -7,8 +7,11 @@ const itemsPerPage = 100;
|
||||||
let lastAppliedFilters = {};
|
let lastAppliedFilters = {};
|
||||||
|
|
||||||
const CACHE_KEY = 'latestPricesCache';
|
const CACHE_KEY = 'latestPricesCache';
|
||||||
const CACHE_DURATION = 10 * 60 * 1000; // 10 min
|
|
||||||
const MIN_REFRESH_INTERVAL = 30; // 30 sec
|
const MIN_REFRESH_INTERVAL = 60; // 60 sec
|
||||||
|
|
||||||
|
const CACHE_DURATION = 5 * 60 * 1000; // 5 minutes
|
||||||
|
const FALLBACK_CACHE_DURATION = 24 * 60 * 60 * 1000; // 24 hours
|
||||||
|
|
||||||
let jsonData = [];
|
let jsonData = [];
|
||||||
let originalJsonData = [];
|
let originalJsonData = [];
|
||||||
|
@ -16,6 +19,93 @@ let isInitialLoad = true;
|
||||||
let tableRateModule;
|
let tableRateModule;
|
||||||
const isSentOffers = window.offersTableConfig.isSentOffers;
|
const isSentOffers = window.offersTableConfig.isSentOffers;
|
||||||
|
|
||||||
|
let currentSortColumn = 0;
|
||||||
|
let currentSortDirection = 'desc';
|
||||||
|
|
||||||
|
const offerCache = {
|
||||||
|
set: (key, value, customTtl = null) => {
|
||||||
|
const item = {
|
||||||
|
value: value,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
expiresAt: Date.now() + (customTtl || CACHE_DURATION)
|
||||||
|
};
|
||||||
|
localStorage.setItem(key, JSON.stringify(item));
|
||||||
|
console.log(`🟢 Cache set for ${key}, expires in ${(customTtl || CACHE_DURATION) / 1000} seconds`);
|
||||||
|
console.log('Cached data:', {
|
||||||
|
key: key,
|
||||||
|
expiresIn: (customTtl || CACHE_DURATION) / 1000,
|
||||||
|
dataSize: typeof value === 'object' ? Object.keys(value).length : 'not an object'
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
get: (key) => {
|
||||||
|
const itemStr = localStorage.getItem(key);
|
||||||
|
if (!itemStr) {
|
||||||
|
console.log(`🔴 No cache found for ${key}`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const item = JSON.parse(itemStr);
|
||||||
|
const now = Date.now();
|
||||||
|
if (now < item.expiresAt) {
|
||||||
|
const remainingTime = (item.expiresAt - now) / 1000;
|
||||||
|
console.log(`🟢 Cache hit for ${key}, ${remainingTime.toFixed(1)} seconds remaining`);
|
||||||
|
return {
|
||||||
|
value: item.value,
|
||||||
|
remainingTime: item.expiresAt - now
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
console.log(`🟡 Cache expired for ${key}`);
|
||||||
|
localStorage.removeItem(key);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('❌ Error parsing cache item:', e);
|
||||||
|
localStorage.removeItem(key);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
|
||||||
|
isValid: (key) => {
|
||||||
|
const result = offerCache.get(key) !== null;
|
||||||
|
console.log(`🔍 Cache validity check for ${key}: ${result ? 'valid' : 'invalid'}`);
|
||||||
|
return result;
|
||||||
|
},
|
||||||
|
|
||||||
|
clear: () => {
|
||||||
|
let clearedItems = [];
|
||||||
|
Object.keys(localStorage).forEach(key => {
|
||||||
|
if (key.startsWith('offers_') || key.startsWith('prices_')) {
|
||||||
|
clearedItems.push(key);
|
||||||
|
localStorage.removeItem(key);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
console.log(`🧹 Cache cleared: ${clearedItems.length} items removed`);
|
||||||
|
if (clearedItems.length > 0) {
|
||||||
|
console.log('Cleared items:', clearedItems);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
debug: () => {
|
||||||
|
const cacheItems = {};
|
||||||
|
Object.keys(localStorage).forEach(key => {
|
||||||
|
if (key.startsWith('offers_') || key.startsWith('prices_')) {
|
||||||
|
try {
|
||||||
|
const item = JSON.parse(localStorage.getItem(key));
|
||||||
|
cacheItems[key] = {
|
||||||
|
expiresIn: ((item.expiresAt - Date.now()) / 1000).toFixed(1) + ' seconds',
|
||||||
|
dataSize: typeof item.value === 'object' ? Object.keys(item.value).length : 'not an object'
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
cacheItems[key] = 'invalid cache item';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
console.log('📊 Current cache status:', cacheItems);
|
||||||
|
return cacheItems;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
const coinNameToSymbol = {
|
const coinNameToSymbol = {
|
||||||
'Bitcoin': 'bitcoin',
|
'Bitcoin': 'bitcoin',
|
||||||
'Particl': 'particl',
|
'Particl': 'particl',
|
||||||
|
@ -189,6 +279,47 @@ window.tableRateModule = {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
document.querySelectorAll('th[data-sortable="true"]').forEach(header => {
|
||||||
|
header.addEventListener('click', () => {
|
||||||
|
const columnIndex = parseInt(header.getAttribute('data-column-index'));
|
||||||
|
|
||||||
|
if (currentSortColumn === columnIndex) {
|
||||||
|
currentSortDirection = currentSortDirection === 'asc' ? 'desc' : 'asc';
|
||||||
|
} else {
|
||||||
|
|
||||||
|
currentSortColumn = columnIndex;
|
||||||
|
currentSortDirection = 'desc';
|
||||||
|
}
|
||||||
|
|
||||||
|
document.querySelectorAll('.sort-icon').forEach(icon => {
|
||||||
|
icon.classList.remove('text-blue-500');
|
||||||
|
icon.textContent = '↓';
|
||||||
|
});
|
||||||
|
|
||||||
|
const sortIcon = document.getElementById(`sort-icon-${columnIndex}`);
|
||||||
|
if (sortIcon) {
|
||||||
|
sortIcon.textContent = currentSortDirection === 'asc' ? '↑' : '↓';
|
||||||
|
sortIcon.classList.add('text-blue-500');
|
||||||
|
}
|
||||||
|
|
||||||
|
document.querySelectorAll('th[data-sortable="true"]').forEach(th => {
|
||||||
|
const thColumnIndex = parseInt(th.getAttribute('data-column-index'));
|
||||||
|
if (thColumnIndex === columnIndex) {
|
||||||
|
th.classList.add('text-blue-500');
|
||||||
|
} else {
|
||||||
|
th.classList.remove('text-blue-500');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
localStorage.setItem('tableSortColumn', currentSortColumn);
|
||||||
|
localStorage.setItem('tableSortDirection', currentSortDirection);
|
||||||
|
|
||||||
|
applyFilters();
|
||||||
|
});
|
||||||
|
|
||||||
|
header.classList.add('cursor-pointer', 'hover:bg-gray-100', 'dark:hover:bg-gray-700');
|
||||||
|
});
|
||||||
|
|
||||||
function makePostRequest(url, headers = {}) {
|
function makePostRequest(url, headers = {}) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const xhr = new XMLHttpRequest();
|
const xhr = new XMLHttpRequest();
|
||||||
|
@ -224,6 +355,16 @@ function makePostRequest(url, headers = {}) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function loadSortPreferences() {
|
||||||
|
const savedColumn = localStorage.getItem('tableSortColumn');
|
||||||
|
const savedDirection = localStorage.getItem('tableSortDirection');
|
||||||
|
|
||||||
|
if (savedColumn !== null) {
|
||||||
|
currentSortColumn = parseInt(savedColumn);
|
||||||
|
currentSortDirection = savedDirection || 'desc';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function escapeHtml(unsafe) {
|
function escapeHtml(unsafe) {
|
||||||
if (typeof unsafe !== 'string') {
|
if (typeof unsafe !== 'string') {
|
||||||
console.warn('escapeHtml received a non-string value:', unsafe);
|
console.warn('escapeHtml received a non-string value:', unsafe);
|
||||||
|
@ -441,8 +582,18 @@ function setRefreshButtonLoading(isLoading) {
|
||||||
|
|
||||||
refreshButton.disabled = isLoading;
|
refreshButton.disabled = isLoading;
|
||||||
refreshIcon.classList.toggle('animate-spin', isLoading);
|
refreshIcon.classList.toggle('animate-spin', isLoading);
|
||||||
refreshText.textContent = isLoading ? 'Refresh' : 'Refresh';
|
refreshText.textContent = isLoading ? 'Refreshing...' : 'Refresh';
|
||||||
|
|
||||||
|
// Add visual feedback
|
||||||
|
if (isLoading) {
|
||||||
|
refreshButton.classList.add('opacity-75');
|
||||||
|
refreshButton.classList.add('cursor-wait');
|
||||||
|
} else {
|
||||||
|
refreshButton.classList.remove('opacity-75');
|
||||||
|
refreshButton.classList.remove('cursor-wait');
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
function initializeFlowbiteTooltips() {
|
function initializeFlowbiteTooltips() {
|
||||||
if (typeof Tooltip === 'undefined') {
|
if (typeof Tooltip === 'undefined') {
|
||||||
|
@ -596,14 +747,15 @@ function updatePaginationControls(totalPages) {
|
||||||
totalPagesSpan.textContent = totalPages;
|
totalPagesSpan.textContent = totalPages;
|
||||||
}
|
}
|
||||||
|
|
||||||
function checkExpiredAndFetchNew() {
|
async function checkExpiredAndFetchNew() {
|
||||||
if (isSentOffers) return Promise.resolve();
|
if (isSentOffers) return Promise.resolve();
|
||||||
|
|
||||||
console.log('Starting checkExpiredAndFetchNew');
|
console.log('Starting checkExpiredAndFetchNew');
|
||||||
|
const OFFERS_CACHE_KEY = 'offers_received';
|
||||||
|
|
||||||
return fetch('/json/offers')
|
try {
|
||||||
.then(response => response.json())
|
const response = await fetch('/json/offers');
|
||||||
.then(data => {
|
const data = await response.json();
|
||||||
let newListings = Array.isArray(data) ? data : Object.values(data);
|
let newListings = Array.isArray(data) ? data : Object.values(data);
|
||||||
|
|
||||||
newListings = newListings.map(offer => ({
|
newListings = newListings.map(offer => ({
|
||||||
|
@ -626,6 +778,8 @@ function checkExpiredAndFetchNew() {
|
||||||
newListings = newListings.filter(offer => !isOfferExpired(offer));
|
newListings = newListings.filter(offer => !isOfferExpired(offer));
|
||||||
originalJsonData = newListings;
|
originalJsonData = newListings;
|
||||||
|
|
||||||
|
cache.set(OFFERS_CACHE_KEY, newListings, CACHE_DURATION);
|
||||||
|
|
||||||
const currentFilters = new FormData(filterForm);
|
const currentFilters = new FormData(filterForm);
|
||||||
const hasActiveFilters = currentFilters.get('coin_to') !== 'any' ||
|
const hasActiveFilters = currentFilters.get('coin_to') !== 'any' ||
|
||||||
currentFilters.get('coin_from') !== 'any';
|
currentFilters.get('coin_from') !== 'any';
|
||||||
|
@ -648,12 +802,11 @@ function checkExpiredAndFetchNew() {
|
||||||
console.log(`Next refresh in ${nextRefreshCountdown} seconds`);
|
console.log(`Next refresh in ${nextRefreshCountdown} seconds`);
|
||||||
|
|
||||||
return jsonData.length;
|
return jsonData.length;
|
||||||
})
|
} catch (error) {
|
||||||
.catch(error => {
|
|
||||||
console.error('Error fetching new listings:', error);
|
console.error('Error fetching new listings:', error);
|
||||||
nextRefreshCountdown = 60;
|
nextRefreshCountdown = 60;
|
||||||
return Promise.reject(error);
|
return Promise.reject(error);
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function createTimeColumn(offer, postedTime, expiresIn) {
|
function createTimeColumn(offer, postedTime, expiresIn) {
|
||||||
|
@ -1119,33 +1272,30 @@ function clearFilters() {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchLatestPrices() {
|
async function fetchLatestPrices() {
|
||||||
const MAX_RETRIES = 3;
|
const PRICES_CACHE_KEY = 'prices_coingecko';
|
||||||
const BASE_DELAY = 1000;
|
const cachedData = offerCache.get(PRICES_CACHE_KEY);
|
||||||
|
|
||||||
const cachedData = getCachedPrices();
|
|
||||||
if (cachedData) {
|
if (cachedData) {
|
||||||
latestPrices = cachedData;
|
latestPrices = cachedData.value;
|
||||||
return cachedData;
|
return cachedData.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
const url = 'https://api.coingecko.com/api/v3/simple/price?ids=bitcoin,bitcoin-cash,dash,dogecoin,decred,litecoin,particl,pivx,monero,zano,wownero,zcoin&vs_currencies=USD,BTC';
|
const url = 'https://api.coingecko.com/api/v3/simple/price?ids=bitcoin,bitcoin-cash,dash,dogecoin,decred,litecoin,particl,pivx,monero,zano,wownero,zcoin&vs_currencies=USD,BTC';
|
||||||
|
|
||||||
for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
|
|
||||||
try {
|
try {
|
||||||
const data = await makePostRequest(url);
|
const data = await makePostRequest(url);
|
||||||
if (data && Object.keys(data).length > 0) {
|
if (data && Object.keys(data).length > 0) {
|
||||||
latestPrices = data;
|
latestPrices = data;
|
||||||
setCachedPrices(data);
|
offerCache.set(PRICES_CACHE_KEY, data, CACHE_DURATION);
|
||||||
// Store as fallback values with 24h expiry
|
|
||||||
Object.entries(data).forEach(([coin, prices]) => {
|
Object.entries(data).forEach(([coin, prices]) => {
|
||||||
tableRateModule.setFallbackValue(coin, prices.usd);
|
tableRateModule.setFallbackValue(coin, prices.usd);
|
||||||
});
|
});
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const delay = Math.min(BASE_DELAY * Math.pow(2, attempt), 10000);
|
console.error('Error fetching prices:', error);
|
||||||
await new Promise(resolve => setTimeout(resolve, delay));
|
return getFallbackPrices();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return getFallbackPrices();
|
return getFallbackPrices();
|
||||||
|
@ -1167,11 +1317,28 @@ function getFallbackPrices() {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchOffers(manualRefresh = false) {
|
async function fetchOffers(manualRefresh = false) {
|
||||||
return new Promise((resolve, reject) => {
|
setRefreshButtonLoading(true);
|
||||||
|
const OFFERS_CACHE_KEY = `offers_${isSentOffers ? 'sent' : 'received'}`;
|
||||||
|
|
||||||
|
if (!manualRefresh) {
|
||||||
|
const cachedData = offerCache.get(OFFERS_CACHE_KEY);
|
||||||
|
if (cachedData) {
|
||||||
|
console.log('Using cached offers data');
|
||||||
|
jsonData = cachedData.value;
|
||||||
|
originalJsonData = [...cachedData.value];
|
||||||
|
updateOffersTable();
|
||||||
|
updateJsonView();
|
||||||
|
updatePaginationInfo();
|
||||||
|
setRefreshButtonLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
const endpoint = isSentOffers ? '/json/sentoffers' : '/json/offers';
|
const endpoint = isSentOffers ? '/json/sentoffers' : '/json/offers';
|
||||||
console.log(`[Debug] Fetching from endpoint: ${endpoint}`);
|
console.log(`[Debug] Fetching from endpoint: ${endpoint}`);
|
||||||
|
|
||||||
fetch(endpoint, {
|
const response = await fetch(endpoint, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
|
@ -1180,11 +1347,9 @@ async function fetchOffers(manualRefresh = false) {
|
||||||
with_extra_info: true,
|
with_extra_info: true,
|
||||||
limit: 1000
|
limit: 1000
|
||||||
})
|
})
|
||||||
})
|
});
|
||||||
.then(response => response.json())
|
|
||||||
.then(data => {
|
|
||||||
console.log('[Debug] Raw data received:', data);
|
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
let newData = Array.isArray(data) ? data : Object.values(data);
|
let newData = Array.isArray(data) ? data : Object.values(data);
|
||||||
|
|
||||||
newData = newData.map(offer => ({
|
newData = newData.map(offer => ({
|
||||||
|
@ -1214,6 +1379,9 @@ async function fetchOffers(manualRefresh = false) {
|
||||||
originalJsonData = [...mergedData];
|
originalJsonData = [...mergedData];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Cache the new data
|
||||||
|
offerCache.set(OFFERS_CACHE_KEY, jsonData, CACHE_DURATION);
|
||||||
|
|
||||||
if (newEntriesCountSpan) {
|
if (newEntriesCountSpan) {
|
||||||
newEntriesCountSpan.textContent = jsonData.length;
|
newEntriesCountSpan.textContent = jsonData.length;
|
||||||
}
|
}
|
||||||
|
@ -1222,13 +1390,11 @@ async function fetchOffers(manualRefresh = false) {
|
||||||
updateJsonView();
|
updateJsonView();
|
||||||
updatePaginationInfo();
|
updatePaginationInfo();
|
||||||
|
|
||||||
resolve();
|
} catch (error) {
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
console.error('[Debug] Error fetching offers:', error);
|
console.error('[Debug] Error fetching offers:', error);
|
||||||
reject(error);
|
} finally {
|
||||||
});
|
setRefreshButtonLoading(false);
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function mergeSentOffers(existingOffers, newOffers) {
|
function mergeSentOffers(existingOffers, newOffers) {
|
||||||
|
@ -1319,6 +1485,50 @@ function filterAndSortData() {
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (currentSortColumn !== null) {
|
||||||
|
filteredData.sort((a, b) => {
|
||||||
|
let comparison = 0;
|
||||||
|
|
||||||
|
switch(currentSortColumn) {
|
||||||
|
case 0: // Time
|
||||||
|
comparison = a.created_at - b.created_at;
|
||||||
|
break;
|
||||||
|
case 5: // Rate
|
||||||
|
comparison = parseFloat(a.rate) - parseFloat(b.rate);
|
||||||
|
break;
|
||||||
|
case 6: // Market +/-
|
||||||
|
// Calculate market differences for comparison
|
||||||
|
const aFromSymbol = getCoinSymbolLowercase(a.coin_from);
|
||||||
|
const aToSymbol = getCoinSymbolLowercase(a.coin_to);
|
||||||
|
const bFromSymbol = getCoinSymbolLowercase(b.coin_from);
|
||||||
|
const bToSymbol = getCoinSymbolLowercase(b.coin_to);
|
||||||
|
|
||||||
|
const aFromPrice = latestPrices[aFromSymbol]?.usd || 0;
|
||||||
|
const aToPrice = latestPrices[aToSymbol]?.usd || 0;
|
||||||
|
const bFromPrice = latestPrices[bFromSymbol]?.usd || 0;
|
||||||
|
const bToPrice = latestPrices[bToSymbol]?.usd || 0;
|
||||||
|
|
||||||
|
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;
|
return filteredData;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1676,10 +1886,12 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
document.getElementById('refreshOffers').addEventListener('click', () => {
|
document.getElementById('refreshOffers').addEventListener('click', () => {
|
||||||
console.log('Refresh button clicked');
|
console.log('🔄 Refresh button clicked');
|
||||||
|
console.log('Clearing cache before refresh...');
|
||||||
|
offerCache.clear();
|
||||||
|
console.log('Fetching fresh data...');
|
||||||
fetchOffers(true);
|
fetchOffers(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
toggleButton.addEventListener('click', () => {
|
toggleButton.addEventListener('click', () => {
|
||||||
tableView.classList.toggle('hidden');
|
tableView.classList.toggle('hidden');
|
||||||
jsonView.classList.toggle('hidden');
|
jsonView.classList.toggle('hidden');
|
||||||
|
|
Loading…
Reference in a new issue