GUI: Multi-select coin filtering / Various fixes. (#327)

* GUI: Multi-select coin filtering / Various fixes.

* Use coin-manager / clean-up.

* Fix BCH in filters + fix UX with bid pages modals when amount is empty.

* Fix amount not empty.

* Abandon Bid under debug_ui
This commit is contained in:
Gerlof van Ek
2025-06-23 22:12:34 +02:00
committed by GitHub
parent 1797ab055b
commit 6e5b8fb0ad
15 changed files with 803 additions and 256 deletions

View File

@@ -365,3 +365,147 @@ select.disabled-select-enabled {
#toggle-auto-refresh[data-enabled="true"] {
@apply bg-green-500 hover:bg-green-600 focus:ring-green-300;
}
/* Multi-select dropdown styles */
.multi-select-dropdown::-webkit-scrollbar {
width: 12px;
}
.multi-select-dropdown::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 6px;
}
.multi-select-dropdown::-webkit-scrollbar-thumb {
background: #888;
border-radius: 6px;
}
.multi-select-dropdown::-webkit-scrollbar-thumb:hover {
background: #555;
}
.dark .multi-select-dropdown::-webkit-scrollbar-track {
background: #374151;
}
.dark .multi-select-dropdown::-webkit-scrollbar-thumb {
background: #6b7280;
}
.dark .multi-select-dropdown::-webkit-scrollbar-thumb:hover {
background: #9ca3af;
}
.multi-select-dropdown input[type="checkbox"]:focus {
outline: none !important;
box-shadow: none !important;
border-color: inherit !important;
}
.multi-select-dropdown label:focus-within {
outline: none !important;
box-shadow: none !important;
}
#coin_to_button:focus,
#coin_from_button:focus {
outline: none !important;
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.3) !important;
border-color: #3b82f6 !important;
}
.coin-badge {
background: #3b82f6;
color: white;
padding: 2px 8px;
border-radius: 12px;
font-size: 12px;
display: inline-flex;
align-items: center;
gap: 4px;
margin: 2px;
}
.coin-badge .remove {
cursor: pointer;
font-weight: bold;
opacity: 0.7;
margin-left: 4px;
}
.coin-badge .remove:hover {
opacity: 1;
}
.multi-select-dropdown {
max-height: 300px;
overflow-y: auto;
z-index: 9999 !important;
position: fixed !important;
min-width: 200px;
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.3);
}
.multi-select-dropdown::-webkit-scrollbar {
width: 6px;
}
.multi-select-dropdown::-webkit-scrollbar-track {
background: #f1f1f1;
}
.multi-select-dropdown::-webkit-scrollbar-thumb {
background: #888;
border-radius: 3px;
}
.multi-select-dropdown::-webkit-scrollbar-thumb:hover {
background: #555;
}
.dark .multi-select-dropdown::-webkit-scrollbar-track {
background: #374151;
}
.dark .multi-select-dropdown::-webkit-scrollbar-thumb {
background: #6b7280;
}
.dropdown-container {
position: relative;
z-index: 1;
}
.dropdown-container.open {
z-index: 9999;
}
.filter-button-text {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.multi-select-dropdown input[type="checkbox"] {
outline: none !important;
box-shadow: none !important;
border: 1px solid #d1d5db;
border-radius: 3px;
}
.multi-select-dropdown input[type="checkbox"]:focus {
outline: none !important;
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.3) !important;
border-color: #3b82f6 !important;
}
.multi-select-dropdown input[type="checkbox"]:checked {
background-color: #3b82f6 !important;
border-color: #3b82f6 !important;
}
.dark .multi-select-dropdown input[type="checkbox"] {
border-color: #6b7280;
background-color: #374151;
}
.dark .multi-select-dropdown input[type="checkbox"]:focus {
border-color: #3b82f6 !important;
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.3) !important;
}
.dark .multi-select-dropdown input[type="checkbox"]:checked {
background-color: #3b82f6 !important;
border-color: #3b82f6 !important;
}
.multi-select-dropdown label {
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}

View File

@@ -61,64 +61,9 @@ const AmmTablesManager = (function() {
function getImageFilename(coinSymbol) {
if (!coinSymbol) return 'Unknown.png';
const coinNameToSymbol = {
'bitcoin': 'BTC',
'monero': 'XMR',
'particl': 'PART',
'particl anon': 'PART_ANON',
'particl blind': 'PART_BLIND',
'litecoin': 'LTC',
'bitcoincash': 'BCH',
'bitcoin cash': 'BCH',
'firo': 'FIRO',
'zcoin': 'FIRO',
'pivx': 'PIVX',
'dash': 'DASH',
'ethereum': 'ETH',
'dogecoin': 'DOGE',
'decred': 'DCR',
'namecoin': 'NMC',
'zano': 'ZANO',
'wownero': 'WOW'
};
let normalizedInput = coinSymbol.toLowerCase();
if (coinNameToSymbol[normalizedInput]) {
normalizedInput = coinNameToSymbol[normalizedInput];
}
const normalizedSymbol = normalizedInput.toUpperCase();
if (normalizedSymbol === 'FIRO' || normalizedSymbol === 'ZCOIN') return 'Firo.png';
if (normalizedSymbol === 'BCH' || normalizedSymbol === 'BITCOINCASH') return 'Bitcoin-Cash.png';
if (normalizedSymbol === 'PART_ANON' || normalizedSymbol === 'PARTICL_ANON') return 'Particl.png';
if (normalizedSymbol === 'PART_BLIND' || normalizedSymbol === 'PARTICL_BLIND') return 'Particl.png';
if (window.CoinManager && window.CoinManager.getCoinBySymbol) {
const coin = window.CoinManager.getCoinBySymbol(normalizedSymbol);
if (coin && coin.image) return coin.image;
}
const coinImages = {
'BTC': 'Bitcoin.png',
'XMR': 'Monero.png',
'PART': 'Particl.png',
'LTC': 'Litecoin.png',
'FIRO': 'Firo.png',
'PIVX': 'PIVX.png',
'DASH': 'Dash.png',
'ETH': 'Ethereum.png',
'DOGE': 'Dogecoin.png',
'DCR': 'Decred.png',
'NMC': 'Namecoin.png',
'ZANO': 'Zano.png',
'WOW': 'Wownero.png'
};
const result = coinImages[normalizedSymbol] || 'Unknown.png';
debugLog(`Coin symbol: ${coinSymbol}, normalized: ${normalizedSymbol}, image: ${result}`);
return result;
const icon = window.CoinManager.getCoinIcon(coinSymbol);
debugLog(`CoinManager returned icon: ${icon} for ${coinSymbol}`);
return icon || 'Unknown.png';
}
function getCoinDisplayName(coinId) {

View File

@@ -352,7 +352,7 @@ const createBidTableRow = async (bid) => {
<div class="flex items-center justify-center">
<span class="inline-flex mr-3 align-middle items-center justify-center w-18 h-20 rounded">
<img class="h-12"
src="/static/images/coins/${bid.coin_from.replace(' ', '-')}.png"
src="/static/images/coins/${window.CoinManager.getCoinIcon(bid.coin_from)}"
alt="${bid.coin_from}"
onerror="this.src='/static/images/coins/default.png'">
</span>
@@ -361,7 +361,7 @@ const createBidTableRow = async (bid) => {
</svg>
<span class="inline-flex ml-3 align-middle items-center justify-center w-18 h-20 rounded">
<img class="h-12"
src="/static/images/coins/${bid.coin_to.replace(' ', '-')}.png"
src="/static/images/coins/${window.CoinManager.getCoinIcon(bid.coin_to)}"
alt="${bid.coin_to}"
onerror="this.src='/static/images/coins/default.png'">
</span>

View File

@@ -509,6 +509,21 @@ function coinMatches(offerCoin, filterCoin) {
return offerCoin === filterCoin;
}
if (filterCoin.includes(' ') || offerCoin.includes(' ')) {
const filterFirstWord = filterCoin.split(' ')[0];
const offerFirstWord = offerCoin.split(' ')[0];
if (filterFirstWord === 'bitcoin' && offerFirstWord === 'bitcoin') {
const filterHasCash = filterCoin.includes('cash');
const offerHasCash = offerCoin.includes('cash');
return filterHasCash === offerHasCash;
}
if (filterFirstWord === offerFirstWord && filterFirstWord.length > 4) {
return true;
}
}
return false;
}

View File

@@ -47,7 +47,7 @@ const CoinManager = (function() {
usesCryptoCompare: true,
usesCoinGecko: true,
historicalDays: 30,
icon: 'Bitcoin-Cash.png'
icon: 'Bitcoin%20Cash.png'
},
{
symbol: 'PIVX',
@@ -235,6 +235,31 @@ const CoinManager = (function() {
const coin = getCoinByAnyIdentifier(coinIdentifier);
if (!coin) return coinIdentifier.toLowerCase();
return coin.coingeckoId;
},
getCoinIcon: function(identifier) {
if (!identifier) return null;
const normalizedId = identifier.toString().toLowerCase().trim();
if (normalizedId === 'particl anon' || normalizedId === 'part_anon' || normalizedId === 'particl_anon') {
return 'Particl.png';
}
if (normalizedId === 'particl blind' || normalizedId === 'part_blind' || normalizedId === 'particl_blind') {
return 'Particl.png';
}
if (normalizedId === 'litecoin mweb' || normalizedId === 'ltc_mweb' || normalizedId === 'litecoin_mweb') {
return 'Litecoin.png';
}
const coin = getCoinByAnyIdentifier(identifier);
if (coin && coin.icon) {
return coin.icon;
}
const capitalizedName = identifier.toString().split(' ')
.map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
.join('%20');
return `${capitalizedName}.png`;
}
};
})();

View File

@@ -152,6 +152,21 @@ const ConfigManager = (function() {
if (filterCoin === 'particl' && particlVariants.includes(offerCoin)) {
return true;
}
if (filterCoin.includes(' ') || offerCoin.includes(' ')) {
const filterFirstWord = filterCoin.split(' ')[0];
const offerFirstWord = offerCoin.split(' ')[0];
if (filterFirstWord === 'bitcoin' && offerFirstWord === 'bitcoin') {
const filterHasCash = filterCoin.includes('cash');
const offerHasCash = offerCoin.includes('cash');
return filterHasCash === offerHasCash;
}
if (filterFirstWord === offerFirstWord && filterFirstWord.length > 4) {
return true;
}
}
if (particlVariants.includes(filterCoin)) {
return offerCoin === filterCoin;
}

View File

@@ -132,7 +132,6 @@ function initializeTableRateModule() {
}
function continueInitialization() {
updateCoinFilterImages();
fetchOffers().then(() => {
applyFilters();
if (!isSentOffers) {
@@ -157,65 +156,340 @@ function getValidOffers() {
return [];
}
const filteredData = filterAndSortData();
return filteredData;
return jsonData;
}
function saveFilterSettings() {
const formData = new FormData(filterForm);
const filters = Object.fromEntries(formData);
const storageKey = isSentOffers ? 'sentOffersTableSettings' : 'networkOffersTableSettings';
const selectedCoinTo = getSelectedCoins('coin_to');
const selectedCoinFrom = getSelectedCoins('coin_from');
const statusSelect = document.getElementById('status');
const sentFromSelect = document.getElementById('sent_from');
const autoAcceptTypeSelect = document.getElementById('auto_accept_type');
localStorage.setItem(storageKey, JSON.stringify({
coin_to: filters.coin_to,
coin_from: filters.coin_from,
status: filters.status,
sent_from: filters.sent_from,
coin_to: selectedCoinTo,
coin_from: selectedCoinFrom,
status: statusSelect ? statusSelect.value : 'any',
sent_from: sentFromSelect ? sentFromSelect.value : 'any',
auto_accept_type: autoAcceptTypeSelect ? autoAcceptTypeSelect.value : 'any',
sortColumn: currentSortColumn,
sortDirection: currentSortDirection
}));
}
function coinMatches(offerCoin, filterCoin) {
if (!offerCoin || !filterCoin || filterCoin === 'any') return true;
offerCoin = offerCoin.toLowerCase();
filterCoin = filterCoin.toLowerCase();
function getSelectedCoins(filterType) {
if (offerCoin === filterCoin) return true;
if ((offerCoin === 'firo' || offerCoin === 'zcoin') &&
(filterCoin === 'firo' || filterCoin === 'zcoin')) {
return true;
const dropdown = document.getElementById(`${filterType}_dropdown`);
if (!dropdown) {
return ['any'];
}
if ((offerCoin === 'bitcoincash' && filterCoin === 'bitcoin cash') ||
(offerCoin === 'bitcoin cash' && filterCoin === 'bitcoincash')) {
return true;
}
const particlVariants = ['particl', 'particl anon', 'particl blind'];
if (filterCoin === 'particl' && particlVariants.includes(offerCoin)) {
return true;
}
const allCheckboxes = dropdown.querySelectorAll('input[type="checkbox"]');
if (particlVariants.includes(filterCoin)) {
return offerCoin === filterCoin;
}
const selected = [];
allCheckboxes.forEach((cb) => {
if (cb.checked && cb.value !== 'any') {
selected.push(cb.value);
}
});
return false;
return selected.length > 0 ? selected : ['any'];
}
function filterAndSortData() {
const formData = new FormData(filterForm);
const filters = Object.fromEntries(formData);
function getCoinNameFromValue(value, filterType) {
if (value === 'any') {
return 'any';
}
const dropdown = document.getElementById(`${filterType}_dropdown`);
if (!dropdown) {
return value;
}
const checkbox = dropdown.querySelector(`input[value="${value}"]`);
if (checkbox) {
const label = checkbox.closest('label');
const spans = label.querySelectorAll('span');
const coinSpan = spans[spans.length - 1];
if (coinSpan) {
const text = coinSpan.textContent.trim();
const cleanText = text.replace(/\s*\(clear all\)\s*/, '');
return cleanText;
}
}
return value;
}
function getCoinImage(coinName) {
return window.CoinManager.getCoinIcon(coinName);
}
function updateFilterButtonText(filterType) {
const selected = getSelectedCoins(filterType);
const button = document.getElementById(`${filterType}_button`);
const textSpan = document.getElementById(`${filterType}_text`);
if (!button || !textSpan) return;
if (selected.length === 0 || (selected.length === 1 && selected[0] === 'any')) {
const defaultText = filterType === 'coin_to' ?
(isSentOffers ? 'Filter Receiving' : 'Filter Bids') :
(isSentOffers ? 'Filter Sending' : 'Filter Offers');
textSpan.textContent = defaultText;
} else {
const filterLabel = filterType === 'coin_to' ?
(isSentOffers ? 'Receiving' : 'Bids') :
(isSentOffers ? 'Sending' : 'Offers');
textSpan.textContent = `Filter ${filterLabel} (${selected.length} selected)`;
}
button.style.width = '210px';
}
function updateCoinBadges(filterType) {
const selected = getSelectedCoins(filterType);
const badgesContainer = document.getElementById(`${filterType}_badges`);
const mainContainer = document.getElementById('selected_coins_container');
if (!badgesContainer) return;
badgesContainer.innerHTML = '';
if (selected.length > 0 && !(selected.length === 1 && selected[0] === 'any')) {
selected.forEach(coinValue => {
const coinName = getCoinNameFromValue(coinValue, filterType);
const badge = document.createElement('span');
const isBidsFilter = filterType === 'coin_to' && !isSentOffers;
const isOffersFilter = filterType === 'coin_from' && !isSentOffers;
const isReceivingFilter = filterType === 'coin_to' && isSentOffers;
const isSendingFilter = filterType === 'coin_from' && isSentOffers;
let badgeClass = 'inline-flex items-center px-3 py-2 rounded-full text-sm font-medium mr-2 mb-1';
if (isBidsFilter || isReceivingFilter) {
badgeClass += ' bg-blue-500 text-white dark:bg-blue-600 dark:text-white';
} else {
badgeClass += ' bg-green-400 text-white dark:bg-green-500 dark:text-white';
}
badge.className = badgeClass + ' cursor-pointer hover:opacity-80';
const coinImage = getCoinImage(coinName);
badge.innerHTML = `
<img src="/static/images/coins/${coinImage}" class="w-4 h-4 mr-1" alt="${coinName}" onerror="this.style.display='none'">
<span>${coinName}</span>
<button type="button" class="ml-1 text-current hover:text-red-500 focus:outline-none remove-coin-btn">
<span class="sr-only">Remove ${coinName}</span>
×
</button>
`;
CleanupManager.addListener(badge, 'click', (e) => {
e.preventDefault();
e.stopPropagation();
removeCoinFilter(filterType, coinValue);
});
const closeBtn = badge.querySelector('.remove-coin-btn');
if (closeBtn) {
CleanupManager.addListener(closeBtn, 'click', (e) => {
e.preventDefault();
e.stopPropagation();
removeCoinFilter(filterType, coinValue);
});
}
badgesContainer.appendChild(badge);
});
if (mainContainer) {
mainContainer.style.display = 'block';
}
} else {
const otherType = filterType === 'coin_to' ? 'coin_from' : 'coin_to';
const otherSelected = getSelectedCoins(otherType);
if (otherSelected.length === 0 || (otherSelected.length === 1 && otherSelected[0] === 'any')) {
if (mainContainer) {
mainContainer.style.display = 'none';
}
}
}
}
function coinMatches(offerCoin, filterCoins) {
if (!offerCoin || !filterCoins) return true;
if (typeof filterCoins === 'string') {
filterCoins = [filterCoins];
}
if (filterCoins.includes('any') || filterCoins.length === 0) return true;
const normalizedOfferCoin = offerCoin.toLowerCase().trim();
return filterCoins.some(filterCoin => {
if (!filterCoin) return false;
const normalizedFilterCoin = filterCoin.toLowerCase().trim();
if (normalizedOfferCoin === normalizedFilterCoin) {
return true;
}
if ((normalizedOfferCoin === 'firo' || normalizedOfferCoin === 'zcoin') &&
(normalizedFilterCoin === 'firo' || normalizedFilterCoin === 'zcoin')) {
return true;
}
if ((normalizedOfferCoin === 'bitcoincash' && normalizedFilterCoin === 'bitcoin cash') ||
(normalizedOfferCoin === 'bitcoin cash' && normalizedFilterCoin === 'bitcoincash')) {
return true;
}
const particlVariants = ['particl', 'particl anon', 'particl blind'];
if (normalizedFilterCoin === 'particl' && particlVariants.includes(normalizedOfferCoin)) {
return true;
}
if (particlVariants.includes(normalizedFilterCoin)) {
return normalizedOfferCoin === normalizedFilterCoin;
}
if (normalizedFilterCoin.includes(' ') || normalizedOfferCoin.includes(' ')) {
const filterFirstWord = normalizedFilterCoin.split(' ')[0];
const offerFirstWord = normalizedOfferCoin.split(' ')[0];
if (filterFirstWord === 'bitcoin' && offerFirstWord === 'bitcoin') {
const filterHasCash = normalizedFilterCoin.includes('cash');
const offerHasCash = normalizedOfferCoin.includes('cash');
return filterHasCash === offerHasCash;
}
if (filterFirstWord === offerFirstWord && filterFirstWord.length > 4) {
return true;
}
}
return false;
});
}
function toggleDropdown(filterType) {
const dropdown = document.getElementById(`${filterType}_dropdown`);
const button = document.getElementById(`${filterType}_button`);
const container = dropdown?.closest('.dropdown-container');
if (dropdown && button) {
const isVisible = dropdown.style.display !== 'none';
hideAllDropdowns();
if (!isVisible) {
const buttonRect = button.getBoundingClientRect();
dropdown.style.position = 'fixed';
dropdown.style.top = (buttonRect.bottom + 4) + 'px';
dropdown.style.left = buttonRect.left + 'px';
dropdown.style.width = buttonRect.width + 'px';
dropdown.style.display = 'block';
if (container) {
container.classList.add('open');
}
}
}
}
function hideDropdown(filterType) {
const dropdown = document.getElementById(`${filterType}_dropdown`);
const container = dropdown?.closest('.dropdown-container');
if (dropdown) {
dropdown.style.display = 'none';
if (container) {
container.classList.remove('open');
}
}
}
function hideAllDropdowns() {
hideDropdown('coin_to');
hideDropdown('coin_from');
}
function handleCoinCheckboxChange(filterType, checkbox) {
if (checkbox.value === 'any') {
if (checkbox.checked) {
const dropdown = document.getElementById(`${filterType}_dropdown`);
if (dropdown) {
const otherCheckboxes = dropdown.querySelectorAll('input[type="checkbox"]:not([value="any"])');
otherCheckboxes.forEach(cb => cb.checked = false);
}
}
} else {
if (checkbox.checked) {
const dropdown = document.getElementById(`${filterType}_dropdown`);
if (dropdown) {
const anyCheckbox = dropdown.querySelector('input[value="any"]');
if (anyCheckbox) anyCheckbox.checked = false;
}
}
}
updateFilterButtonText(filterType);
updateCoinBadges(filterType);
applyFilters();
updateClearFiltersButton();
}
function removeCoinFilter(filterType, coinValue) {
const dropdown = document.getElementById(`${filterType}_dropdown`);
if (!dropdown) {
return;
}
const checkbox = dropdown.querySelector(`input[value="${coinValue}"]`);
if (checkbox) {
checkbox.checked = false;
updateFilterButtonText(filterType);
updateCoinBadges(filterType);
applyFilters();
updateClearFiltersButton();
}
}
window.removeCoinFilter = removeCoinFilter;
function filterAndSortData() {
saveFilterSettings();
let filteredData = [...originalJsonData];
const sentFromFilter = filters.sent_from || 'any';
const statusSelect = document.getElementById('status');
const sentFromSelect = document.getElementById('sent_from');
const autoAcceptTypeSelect = document.getElementById('auto_accept_type');
const selectedCoinTo = getSelectedCoins('coin_to');
const selectedCoinFrom = getSelectedCoins('coin_from');
const sentFromFilter = sentFromSelect ? sentFromSelect.value : 'any';
filteredData = filteredData.filter(offer => {
const isMatch = sentFromFilter === 'public' ? offer.is_public :
sentFromFilter === 'private' ? !offer.is_public :
@@ -223,37 +497,43 @@ function filterAndSortData() {
return isMatch;
});
const autoAcceptTypeFilter = autoAcceptTypeSelect ? autoAcceptTypeSelect.value : 'any';
if (autoAcceptTypeFilter !== 'any') {
filteredData = filteredData.filter(offer => {
const offerAutoAcceptType = offer.auto_accept_type !== undefined ? offer.auto_accept_type : 0;
return offerAutoAcceptType === parseInt(autoAcceptTypeFilter);
});
}
filteredData = filteredData.filter(offer => {
if (!isSentOffers && isOfferExpired(offer)) {
return false;
}
if (filters.coin_to && filters.coin_to !== 'any') {
const coinToSelect = document.getElementById('coin_to');
const selectedOption = coinToSelect?.querySelector(`option[value="${filters.coin_to}"]`);
const coinName = selectedOption?.textContent.trim();
if (coinName && !coinMatches(offer.coin_to, coinName)) {
if (selectedCoinTo.length > 0 && !(selectedCoinTo.length === 1 && selectedCoinTo[0] === 'any')) {
const coinNames = selectedCoinTo.map(value => getCoinNameFromValue(value, 'coin_to'));
const matches = coinMatches(offer.coin_to, coinNames);
if (!matches) {
return false;
}
}
if (filters.coin_from && filters.coin_from !== 'any') {
const coinFromSelect = document.getElementById('coin_from');
const selectedOption = coinFromSelect?.querySelector(`option[value="${filters.coin_from}"]`);
const coinName = selectedOption?.textContent.trim();
if (coinName && !coinMatches(offer.coin_from, coinName)) {
if (selectedCoinFrom.length > 0 && !(selectedCoinFrom.length === 1 && selectedCoinFrom[0] === 'any')) {
const coinNames = selectedCoinFrom.map(value => getCoinNameFromValue(value, 'coin_from'));
const matches = coinMatches(offer.coin_from, coinNames);
if (!matches) {
return false;
}
}
if (isSentOffers && filters.status && filters.status !== 'any') {
if (isSentOffers && statusSelect && statusSelect.value !== 'any') {
const isExpired = offer.expire_at <= Math.floor(Date.now() / 1000);
const isRevoked = Boolean(offer.is_revoked);
let statusMatch = false;
switch (filters.status) {
switch (statusSelect.value) {
case 'active':
statusMatch = !isExpired && !isRevoked;
break;
@@ -554,6 +834,7 @@ function formatInitialData(data) {
amount_negotiable: Boolean(offer.amount_negotiable),
is_revoked: Boolean(offer.is_revoked),
is_public: offer.is_public !== undefined ? Boolean(offer.is_public) : false,
auto_accept_type: offer.auto_accept_type !== undefined ? Number(offer.auto_accept_type) : 0,
unique_id: `${offer.offer_id}_${offer.created_at}_${offer.coin_from}_${offer.coin_to}`
}));
}
@@ -721,28 +1002,7 @@ function updateProfitLoss(row, fromCoin, toCoin, fromAmount, toAmount, isOwnOffe
}
function updateCoinFilterImages() {
const coinToSelect = document.getElementById('coin_to');
const coinFromSelect = document.getElementById('coin_from');
const coinToButton = document.getElementById('coin_to_button');
const coinFromButton = document.getElementById('coin_from_button');
function updateButtonImage(select, button) {
const selectedOption = select.options[select.selectedIndex];
const imagePath = selectedOption.getAttribute('data-image');
if (imagePath && select.value !== 'any') {
button.style.backgroundImage = `url(${imagePath})`;
button.style.backgroundSize = '25px 25px';
button.style.backgroundPosition = 'center';
button.style.backgroundRepeat = 'no-repeat';
} else {
button.style.backgroundImage = 'none';
}
button.style.minWidth = '25px';
button.style.minHeight = '25px';
}
updateButtonImage(coinToSelect, coinToButton);
updateButtonImage(coinFromSelect, coinFromButton);
return;
}
function updateClearFiltersButton() {
@@ -799,11 +1059,7 @@ function cleanupTable() {
}
function handleNoOffersScenario() {
const formData = new FormData(filterForm);
const filters = Object.fromEntries(formData);
const hasActiveFilters = filters.coin_to !== 'any' ||
filters.coin_from !== 'any' ||
(filters.status && filters.status !== 'any');
const hasFilters = hasActiveFilters();
stopRefreshAnimation();
@@ -812,7 +1068,7 @@ function handleNoOffersScenario() {
cleanupRow(row);
});
if (hasActiveFilters) {
if (hasFilters) {
offersBody.innerHTML = `
<tr>
<td colspan="9" class="text-center py-8">
@@ -1726,13 +1982,8 @@ function applyFilters() {
}
function clearFilters() {
filterForm.reset();
const selectElements = filterForm.querySelectorAll('select');
selectElements.forEach(select => {
select.value = 'any';
const event = new Event('change', { bubbles: true });
select.dispatchEvent(event);
document.querySelectorAll('.coin-to-checkbox, .coin-from-checkbox').forEach(checkbox => {
checkbox.checked = checkbox.value === 'any';
});
const statusSelect = document.getElementById('status');
@@ -1740,6 +1991,22 @@ function clearFilters() {
statusSelect.value = 'any';
}
const sentFromSelect = document.getElementById('sent_from');
if (sentFromSelect) {
sentFromSelect.value = 'any';
}
const autoAcceptTypeSelect = document.getElementById('auto_accept_type');
if (autoAcceptTypeSelect) {
autoAcceptTypeSelect.value = 'any';
}
updateFilterButtonText('coin_to');
updateFilterButtonText('coin_from');
updateCoinBadges('coin_to');
updateCoinBadges('coin_from');
hideAllDropdowns();
jsonData = [...originalJsonData];
currentPage = 1;
@@ -1747,21 +2014,25 @@ function clearFilters() {
localStorage.removeItem(storageKey);
updateOffersTable();
updateCoinFilterImages();
updateClearFiltersButton();
}
function hasActiveFilters() {
const selectElements = filterForm.querySelectorAll('select');
let hasChangedFilters = false;
const selectedCoinTo = getSelectedCoins('coin_to');
const selectedCoinFrom = getSelectedCoins('coin_from');
selectElements.forEach(select => {
if (select.value !== 'any') {
hasChangedFilters = true;
}
});
const hasCoinToFilter = selectedCoinTo.length > 0 && !(selectedCoinTo.length === 1 && selectedCoinTo[0] === 'any');
const hasCoinFromFilter = selectedCoinFrom.length > 0 && !(selectedCoinFrom.length === 1 && selectedCoinFrom[0] === 'any');
return hasChangedFilters;
const statusSelect = document.getElementById('status');
const sentFromSelect = document.getElementById('sent_from');
const autoAcceptTypeSelect = document.getElementById('auto_accept_type');
const hasStatusFilter = statusSelect && statusSelect.value !== 'any';
const hasSentFromFilter = sentFromSelect && sentFromSelect.value !== 'any';
const hasAutoAcceptTypeFilter = autoAcceptTypeSelect && autoAcceptTypeSelect.value !== 'any';
return hasCoinToFilter || hasCoinFromFilter || hasStatusFilter || hasSentFromFilter || hasAutoAcceptTypeFilter;
}
function formatTimeLeft(timestamp) {
@@ -1791,13 +2062,6 @@ function getCoinSymbolLowercase(coin) {
}
}
function coinMatches(offerCoin, filterCoin) {
if (window.CoinManager) {
return window.CoinManager.coinMatches(offerCoin, filterCoin);
}
return window.config.coinMatches(offerCoin, filterCoin);
}
function getProfitColorClass(percentage) {
const numericPercentage = parseFloat(percentage);
if (numericPercentage > 0) return 'text-green-500';
@@ -1872,25 +2136,51 @@ function initializeTableEvents() {
});
}
const coinToSelect = document.getElementById('coin_to');
const coinFromSelect = document.getElementById('coin_from');
const coinToButton = document.getElementById('coin_to_button');
const coinFromButton = document.getElementById('coin_from_button');
const coinToDropdown = document.getElementById('coin_to_dropdown');
const coinFromDropdown = document.getElementById('coin_from_dropdown');
const statusSelect = document.getElementById('status');
const sentFromSelect = document.getElementById('sent_from');
if (coinToSelect) {
CleanupManager.addListener(coinToSelect, 'change', () => {
applyFilters();
updateCoinFilterImages();
if (coinToButton && coinToDropdown) {
CleanupManager.addListener(coinToButton, 'click', (e) => {
e.stopPropagation();
toggleDropdown('coin_to');
});
const coinToCheckboxes = coinToDropdown.querySelectorAll('.coin-to-checkbox');
coinToCheckboxes.forEach(checkbox => {
CleanupManager.addListener(checkbox, 'change', (e) => {
handleCoinCheckboxChange('coin_to', e.target);
});
});
}
if (coinFromSelect) {
CleanupManager.addListener(coinFromSelect, 'change', () => {
applyFilters();
updateCoinFilterImages();
if (coinFromButton && coinFromDropdown) {
CleanupManager.addListener(coinFromButton, 'click', (e) => {
e.stopPropagation();
toggleDropdown('coin_from');
});
const coinFromCheckboxes = coinFromDropdown.querySelectorAll('.coin-from-checkbox');
coinFromCheckboxes.forEach(checkbox => {
CleanupManager.addListener(checkbox, 'change', (e) => {
handleCoinCheckboxChange('coin_from', e.target);
});
});
}
CleanupManager.addListener(window, 'resize', () => {
hideAllDropdowns();
});
CleanupManager.addListener(window, 'scroll', () => {
hideAllDropdowns();
});
if (statusSelect) {
CleanupManager.addListener(statusSelect, 'change', () => {
applyFilters();
@@ -1903,11 +2193,17 @@ function initializeTableEvents() {
});
}
const autoAcceptTypeSelect = document.getElementById('auto_accept_type');
if (autoAcceptTypeSelect) {
CleanupManager.addListener(autoAcceptTypeSelect, 'change', () => {
applyFilters();
});
}
const clearFiltersBtn = document.getElementById('clearFilters');
if (clearFiltersBtn) {
CleanupManager.addListener(clearFiltersBtn, 'click', () => {
clearFilters();
updateCoinFilterImages();
});
}
@@ -2133,7 +2429,12 @@ async function initializeTableAndData() {
updateClearFiltersButton();
initializeTableEvents();
initializeTooltips();
updateCoinFilterImages();
updateFilterButtonText('coin_to');
updateFilterButtonText('coin_from');
updateCoinBadges('coin_to');
updateCoinBadges('coin_from');
try {
await fetchOffers();
@@ -2152,7 +2453,23 @@ function loadSavedSettings() {
if (saved) {
const settings = JSON.parse(saved);
['coin_to', 'coin_from', 'status', 'sent_from'].forEach(id => {
['coin_to', 'coin_from'].forEach(filterType => {
const savedCoins = settings[filterType];
if (savedCoins && Array.isArray(savedCoins)) {
document.querySelectorAll(`.${filterType}-checkbox`).forEach(cb => cb.checked = false);
savedCoins.forEach(coinValue => {
const checkbox = document.querySelector(`.${filterType}-checkbox[value="${coinValue}"]`);
if (checkbox) checkbox.checked = true;
});
updateFilterButtonText(filterType);
updateCoinBadges(filterType);
}
});
['status', 'sent_from', 'auto_accept_type'].forEach(id => {
const element = document.getElementById(id);
if (element && settings[id]) element.value = settings[id];
});
@@ -2308,7 +2625,6 @@ document.addEventListener('DOMContentLoaded', async function() {
filterForm.querySelectorAll('select').forEach(select => {
CleanupManager.addListener(select, 'change', () => {
applyFilters();
updateCoinFilterImages();
updateClearFiltersButton();
});
});
@@ -2319,6 +2635,23 @@ document.addEventListener('DOMContentLoaded', async function() {
CleanupManager.addListener(clearFiltersBtn, 'click', clearFilters);
}
CleanupManager.addListener(document, 'click', (e) => {
const coinToDropdown = document.getElementById('coin_to_dropdown');
const coinFromDropdown = document.getElementById('coin_from_dropdown');
if (coinToDropdown && coinToDropdown.style.display !== 'none') {
if (!e.target.closest('#coin_to_button') && !e.target.closest('#coin_to_dropdown')) {
hideDropdown('coin_to');
}
}
if (coinFromDropdown && coinFromDropdown.style.display !== 'none') {
if (!e.target.closest('#coin_from_button') && !e.target.closest('#coin_from_dropdown')) {
hideDropdown('coin_from');
}
}
});
const rowTimeInterval = setInterval(updateRowTimes, 30000);
if (CleanupManager.registerResource) {
CleanupManager.registerResource('rowTimeInterval', rowTimeInterval, () => {

View File

@@ -389,7 +389,7 @@ const createSwapTableRow = async (swap) => {
<div class="flex items-center justify-center">
<span class="inline-flex mr-3 align-middle items-center justify-center w-18 h-20 rounded">
<img class="h-12"
src="/static/images/coins/${swap.coin_from.replace(' ', '-')}.png"
src="/static/images/coins/${window.CoinManager.getCoinIcon(swap.coin_from)}"
alt="${swap.coin_from}"
onerror="this.src='/static/images/coins/default.png'">
</span>
@@ -398,7 +398,7 @@ const createSwapTableRow = async (swap) => {
</svg>
<span class="inline-flex ml-3 align-middle items-center justify-center w-18 h-20 rounded">
<img class="h-12"
src="/static/images/coins/${swap.coin_to.replace(' ', '-')}.png"
src="/static/images/coins/${window.CoinManager.getCoinIcon(swap.coin_to)}"
alt="${swap.coin_to}"
onerror="this.src='/static/images/coins/default.png'">
</span>