mirror of
https://github.com/basicswap/basicswap.git
synced 2025-11-05 18:38:09 +01:00
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:
@@ -11349,6 +11349,11 @@ class BasicSwap(BaseApp, UIApp):
|
||||
if filter_include_sent is not None and filter_include_sent is not True:
|
||||
query_suffix += " AND was_sent = 0"
|
||||
|
||||
filter_auto_accept_type = filters.get("auto_accept_type", None)
|
||||
if filter_auto_accept_type and filter_auto_accept_type != "any":
|
||||
query_suffix += " AND auto_accept_type = :filter_auto_accept_type"
|
||||
query_data["filter_auto_accept_type"] = int(filter_auto_accept_type)
|
||||
|
||||
query_suffix += getOrderByStr(filters)
|
||||
|
||||
limit = filters.get("limit", None)
|
||||
|
||||
@@ -278,6 +278,7 @@ def js_offers(self, url_split, post_string, is_json, sent=False) -> bytes:
|
||||
"is_public": o.addr_to == swap_client.network_addr
|
||||
or o.addr_to.strip() == "",
|
||||
}
|
||||
offer_data["auto_accept_type"] = getattr(o, "auto_accept_type", 0)
|
||||
if with_extra_info:
|
||||
offer_data["amount_negotiable"] = o.amount_negotiable
|
||||
offer_data["rate_negotiable"] = o.rate_negotiable
|
||||
@@ -293,6 +294,7 @@ def js_offers(self, url_split, post_string, is_json, sent=False) -> bytes:
|
||||
offer_data["feerate_to"] = o.to_feerate
|
||||
|
||||
offer_data["automation_strat_id"] = getattr(o, "auto_accept_type", 0)
|
||||
offer_data["auto_accept_type"] = getattr(o, "auto_accept_type", 0)
|
||||
|
||||
if o.was_sent:
|
||||
try:
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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`;
|
||||
}
|
||||
};
|
||||
})();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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, () => {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -532,12 +532,12 @@
|
||||
<div class="w-full md:w-auto p-1.5">
|
||||
<button name="edit_bid" type="submit" value="Edit Bid" class="flex flex-wrap justify-center w-full px-4 py-2.5 font-medium text-sm text-coolGray-500 hover:text-coolGray-600 border border-coolGray-200 hover:border-coolGray-300 bg-white rounded-md shadow-button focus:ring-0 focus:outline-none dark:text-white dark:hover:text-white dark:bg-gray-600 dark:hover:bg-gray-700 dark:border-gray-600 dark:hover:border-gray-600">Edit Bid</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if data.can_abandon == true and not edit_bid %}
|
||||
<div class="w-full md:w-auto p-1.5">
|
||||
<button name="abandon_bid" type="submit" value="Abandon Bid" onclick="return confirmPopup();" class="flex flex-wrap justify-center w-full px-4 py-2.5 font-medium text-sm text-white hover:text-red border border-red-500 hover:border-red-500 hover:bg-red-600 bg-red-500 rounded-md shadow-button focus:ring-0 focus:outline-none">Abandon Bid</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if data.was_received and not edit_bid and data.can_accept_bid %}
|
||||
<div class="w-full md:w-auto p-1.5">
|
||||
|
||||
@@ -808,13 +808,13 @@
|
||||
<div class="w-full md:w-auto p-1.5">
|
||||
<button name="edit_bid" type="submit" value="Edit Bid" class="flex flex-wrap justify-center w-full px-4 py-2.5 font-medium text-sm text-coolGray-500 hover:text-coolGray-600 border border-coolGray-200 hover:border-coolGray-300 bg-white rounded-md focus:ring-0 focus:outline-none dark:text-white dark:hover:text-white dark:bg-gray-600 dark:hover:bg-gray-700 dark:border-gray-600 dark:hover:border-gray-600">Edit Bid</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if data.can_abandon == true %}
|
||||
<div class="w-full md:w-auto p-1.5">
|
||||
<button name="abandon_bid" type="submit" value="Abandon Bid" onclick="return confirmPopup();" class="flex flex-wrap justify-center w-full px-4 py-2.5 font-medium text-sm text-white hover:text-red border border-red-500 hover:border-red-500 hover:bg-red-600 bg-red-500 rounded-md focus:ring-0 focus:outline-none">Abandon Bid</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if data.was_received and not edit_bid and data.can_accept_bid %}
|
||||
<div class="w-full md:w-auto p-1.5">
|
||||
<button name="accept_bid" value="Accept Bid" type="submit" onclick='return confirmPopup("Accept");' class="flex flex-wrap justify-center w-full px-4 py-2.5 bg-blue-500 hover:bg-blue-600 font-medium text-sm text-white border border-blue-500 rounded-md focus:ring-0 focus:outline-none">Accept Bid</button>
|
||||
|
||||
@@ -846,6 +846,27 @@ function validateMaxAmount(input, maxAmount) {
|
||||
}
|
||||
|
||||
function showConfirmModal() {
|
||||
const bidAmountSendInput = document.getElementById('bid_amount_send');
|
||||
const bidAmountInput = document.getElementById('bid_amount');
|
||||
|
||||
let sendAmount = 0;
|
||||
let receiveAmount = 0;
|
||||
|
||||
if (bidAmountSendInput && bidAmountSendInput.value) {
|
||||
sendAmount = parseFloat(bidAmountSendInput.value) || 0;
|
||||
}
|
||||
if (bidAmountInput && bidAmountInput.value) {
|
||||
receiveAmount = parseFloat(bidAmountInput.value) || 0;
|
||||
}
|
||||
|
||||
if (sendAmount <= 0 && bidAmountSendInput && !bidAmountSendInput.disabled) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (receiveAmount <= 0 && bidAmountInput && !bidAmountInput.disabled) {
|
||||
return false;
|
||||
}
|
||||
|
||||
updateModalValues();
|
||||
const modal = document.getElementById('confirmModal');
|
||||
if (modal) {
|
||||
|
||||
@@ -199,97 +199,128 @@
|
||||
|
||||
|
||||
<section>
|
||||
<div class="px-6 py-0 mt-5 h-full overflow-hidden">
|
||||
<div class="px-6 py-0 h-full overflow-hidden">
|
||||
<div class="border-coolGray-100">
|
||||
<div class="flex flex-wrap items-center justify-between">
|
||||
<div class="w-full mx-auto pt-2">
|
||||
<form method="post" id="filterForm">
|
||||
<div class="flex items-center justify-center pb-4 dark:text-white">
|
||||
<div class="rounded-b-md">
|
||||
<div class="w-full md:w-0/12">
|
||||
<div class="lg:container flex flex-wrap justify-center">
|
||||
<div class="md:w-auto hover-container">
|
||||
<div class="flex flex-wrap">
|
||||
<div class="pt-3 px-3 md:w-auto hover-container">
|
||||
<div class="flex">
|
||||
<button id="coin_to_button" class="bg-gray-50 text-gray-900 appearance-none w-10 dark:bg-gray-500 dark:text-white border-l border-t border-b border-gray-300 dark:border-gray-400 dark:text-gray-50 dark:placeholder-gray-50 text-sm rounded-l-lg flex items-center" disabled></button>
|
||||
<div class="relative">
|
||||
{{ input_arrow_down_svg | safe }}
|
||||
<select name="coin_to" id="coin_to" class="bg-gray-50 text-gray-900 appearance-none pr-10 dark:bg-gray-500 dark:text-white border border-gray-300 dark:border-gray-400 dark:text-gray-50 dark:placeholder-gray-50 text-sm rounded-none rounded-r-lg outline-none block w-full p-2.5 focus:ring-0">
|
||||
<option value="any" {% if filters.coin_to==-1 %} selected{% endif %}>Filter {% if sent_offers %}Receiving{% else %}Bids{% endif %}</option>
|
||||
{% for c in coins %}
|
||||
<option class="text-sm" value="{{ c[0] }}" {% if filters.coin_to==c[0] %} selected{% endif %} data-image="/static/images/coins/{{ c[1]|replace(" ", "-") }}.png">{{ c[1] }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<div class="w-full md:w-auto p-1.5">
|
||||
<p class="text-sm font-heading">{{ arrow_right_svg | safe }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<button id="coin_from_button" class="bg-gray-50 text-gray-900 appearance-none w-10 dark:bg-gray-500 dark:text-white border-l border-t border-b border-gray-300 dark:border-gray-400 dark:text-gray-50 dark:placeholder-gray-50 text-sm rounded-l-lg flex items-center" disabled></button>
|
||||
<div class="relative">
|
||||
{{ input_arrow_down_svg | safe }}
|
||||
<select name="coin_from" id="coin_from" class="bg-gray-50 text-gray-900 appearance-none pr-10 dark:bg-gray-500 dark:text-white border border-gray-300 dark:border-gray-400 dark:text-gray-50 dark:placeholder-gray-50 text-sm rounded-none rounded-r-lg outline-none block w-full p-2.5 focus:ring-0">
|
||||
<option value="any" {% if filters.coin_from==-1 %} selected{% endif %}>Filter {% if sent_offers %}Sending{% else %}Offers{% endif %}</option>
|
||||
{% for c in coins_from %}
|
||||
<option class="text-sm" value="{{ c[0] }}" {% if filters.coin_from==c[0] %} selected{% endif %} data-image="/static/images/coins/{{ c[1]|replace(" ", "-") }}.png">{{ c[1] }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% if sent_offers %}
|
||||
<div class="pt-3 px-3 md:w-auto hover-container">
|
||||
<div class="flex">
|
||||
<div class="relative">
|
||||
{{ input_arrow_down_svg | safe }}
|
||||
<select name="status" id="status" class="bg-gray-50 text-gray-900 appearance-none pr-10 pl-5 dark:bg-gray-500 dark:text-white border border-gray-300 dark:border-gray-400 dark:text-gray-50 dark:placeholder-gray-50 text-sm rounded-lg outline-none block w-full p-2.5 focus:ring-0">
|
||||
<option value="any" {% if not filters.status %} selected{% endif %}>Filter Status</option>
|
||||
<option value="active" {% if filters.status == 'active' %} selected{% endif %}>Active</option>
|
||||
<option value="expired" {% if filters.status == 'expired' %} selected{% endif %}>Expired</option>
|
||||
<option value="revoked" {% if filters.status == 'revoked' %} selected{% endif %}>Revoked</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="pt-3 px-3 md:w-auto hover-container">
|
||||
<div class="flex">
|
||||
<div class="relative">
|
||||
{{ input_arrow_down_svg | safe }}
|
||||
<select name="sent_from" id="sent_from" class="bg-gray-50 text-gray-900 appearance-none pr-10 pl-5 dark:bg-gray-500 dark:text-white border border-gray-300 dark:border-gray-400 dark:text-gray-50 dark:placeholder-gray-50 text-sm rounded-lg outline-none block w-full p-2.5 focus:ring-0">
|
||||
<option value="any" {% if not filters.sent_from %} selected{% endif %}>All Offers</option>
|
||||
<option value="public" {% if filters.sent_from == 'public' %} selected{% endif %}>Public</option>
|
||||
<option value="private" {% if filters.sent_from == 'private' %} selected{% endif %}>Private</option>
|
||||
</select>
|
||||
<div class="pb-6 mt-6 border-coolGray-100">
|
||||
<div class="flex flex-wrap justify-center -m-1.5">
|
||||
<div class="w-full md:w-auto p-1.5 hover-container">
|
||||
<div class="flex justify-center md:justify-start">
|
||||
<div class="relative">
|
||||
{{ input_arrow_down_svg | safe }}
|
||||
<button type="button" id="coin_to_button" class="bg-gray-50 text-gray-900 appearance-none pr-10 pl-5 dark:bg-gray-500 dark:text-white border border-gray-300 dark:border-gray-400 dark:text-gray-50 dark:placeholder-gray-50 text-sm rounded-lg outline-none block w-full p-2.5 text-left focus:ring-0 whitespace-nowrap">
|
||||
<span id="coin_to_text" class="filter-button-text">Filter {% if sent_offers %}Receiving{% else %}Bids{% endif %}</span>
|
||||
</button>
|
||||
<div id="coin_to_dropdown" class="multi-select-dropdown bg-gray-50 dark:bg-gray-500 border border-gray-300 dark:border-gray-400 rounded-lg shadow-lg max-h-64 overflow-y-auto" style="display: none; position: absolute; z-index: 1000; min-width: 200px; top: 100%; left: 0;">
|
||||
<div class="p-2">
|
||||
<label class="flex items-center p-2 hover:bg-gray-200 dark:hover:bg-gray-600 rounded cursor-pointer">
|
||||
<input type="checkbox" name="coin_to" value="any" class="mr-3 coin-to-checkbox" {% if filters.coin_to==-1 %}checked{% endif %}>
|
||||
<span class="text-sm text-gray-900 dark:text-white">Any (clear all)</span>
|
||||
</label>
|
||||
{% for c in coins %}
|
||||
<label class="flex items-center p-2 hover:bg-gray-200 dark:hover:bg-gray-600 rounded cursor-pointer">
|
||||
<input type="checkbox" name="coin_to" value="{{ c[0] }}" class="mr-3 coin-to-checkbox" {% if filters.coin_to==c[0] %}checked{% endif %}>
|
||||
<img src="/static/images/coins/{{ c[1]|replace(" ", "%20") }}.png" class="w-4 h-4 mr-2" alt="{{ c[1] }}">
|
||||
<span class="text-sm text-gray-900 dark:text-white">{{ c[1] }}</span>
|
||||
</label>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-full lg:w-auto pt-3 px-3">
|
||||
<div class="relative">
|
||||
<button type="button" id="clearFilters" class="transition-opacity duration-200 flex flex-wrap justify-center w-full px-4 py-2.5 font-medium text-sm hover:text-white dark:text-white dark:bg-gray-500 bg-coolGray-200 hover:bg-green-600 hover:border-green-600 rounded-lg transition duration-200 border border-coolGray-200 dark:border-gray-400 rounded-md shadow-button focus:ring-0 focus:outline-none" disabled>
|
||||
<span>Clear Filters</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<div class="w-full md:w-auto p-1.5">
|
||||
<p class="text-sm font-heading text-gray-500 dark:text-white">{{ arrow_right_svg | safe }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-full lg:w-auto pt-3 px-3">
|
||||
<div class="relative">
|
||||
<button type="button" id="refreshOffers" class="flex flex-wrap justify-center w-full px-4 py-2.5 font-medium text-sm text-white bg-blue-600 hover:bg-green-600 hover:border-green-600 rounded-lg transition duration-200 border border-blue-500 rounded-md shadow-button focus:ring-0 focus:outline-none">
|
||||
<svg id="refreshIcon" class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" 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"></path>
|
||||
</svg>
|
||||
<span id="refreshText">Refresh</span>
|
||||
</button>
|
||||
<div class="relative">
|
||||
{{ input_arrow_down_svg | safe }}
|
||||
<button type="button" id="coin_from_button" class="bg-gray-50 text-gray-900 appearance-none pr-10 pl-5 dark:bg-gray-500 dark:text-white border border-gray-300 dark:border-gray-400 dark:text-gray-50 dark:placeholder-gray-50 text-sm rounded-lg outline-none block w-full p-2.5 text-left focus:ring-0 whitespace-nowrap">
|
||||
<span id="coin_from_text" class="filter-button-text">Filter {% if sent_offers %}Sending{% else %}Offers{% endif %}</span>
|
||||
</button>
|
||||
<div id="coin_from_dropdown" class="multi-select-dropdown bg-gray-50 dark:bg-gray-500 border border-gray-300 dark:border-gray-400 rounded-lg shadow-lg max-h-64 overflow-y-auto" style="display: none; position: absolute; z-index: 1000; min-width: 200px; top: 100%; left: 0;">
|
||||
<div class="p-2">
|
||||
<label class="flex items-center p-2 hover:bg-gray-200 dark:hover:bg-gray-600 rounded cursor-pointer">
|
||||
<input type="checkbox" name="coin_from" value="any" class="mr-3 coin-from-checkbox" {% if filters.coin_from==-1 %}checked{% endif %}>
|
||||
<span class="text-sm text-gray-900 dark:text-white">Any (clear all)</span>
|
||||
</label>
|
||||
{% for c in coins_from %}
|
||||
<label class="flex items-center p-2 hover:bg-gray-200 dark:hover:bg-gray-600 rounded cursor-pointer">
|
||||
<input type="checkbox" name="coin_from" value="{{ c[0] }}" class="mr-3 coin-from-checkbox" {% if filters.coin_from==c[0] %}checked{% endif %}>
|
||||
<img src="/static/images/coins/{{ c[1]|replace(" ", "%20") }}.png" class="w-4 h-4 mr-2" alt="{{ c[1] }}">
|
||||
<span class="text-sm text-gray-900 dark:text-white">{{ c[1] }}</span>
|
||||
</label>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if sent_offers %}
|
||||
<div class="w-full md:w-auto p-1.5">
|
||||
<div class="relative">
|
||||
{{ input_arrow_down_svg | safe }}
|
||||
<select name="status" id="status" class="bg-gray-50 text-gray-900 appearance-none pr-10 pl-5 dark:bg-gray-500 dark:text-white border border-gray-300 dark:border-gray-400 dark:text-gray-50 dark:placeholder-gray-50 text-sm rounded-lg outline-none block w-full p-2.5 focus:ring-0">
|
||||
<option value="any" {% if not filters.status %} selected{% endif %}>Filter Status</option>
|
||||
<option value="active" {% if filters.status == 'active' %} selected{% endif %}>Active</option>
|
||||
<option value="expired" {% if filters.status == 'expired' %} selected{% endif %}>Expired</option>
|
||||
<option value="revoked" {% if filters.status == 'revoked' %} selected{% endif %}>Revoked</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="w-full md:w-auto p-1.5">
|
||||
<div class="relative">
|
||||
{{ input_arrow_down_svg | safe }}
|
||||
<select name="auto_accept_type" id="auto_accept_type" class="bg-gray-50 text-gray-900 appearance-none pr-10 pl-5 dark:bg-gray-500 dark:text-white border border-gray-300 dark:border-gray-400 dark:text-gray-50 dark:placeholder-gray-50 text-sm rounded-lg outline-none block w-full p-2.5 focus:ring-0">
|
||||
<option value="any" {% if filters.auto_accept_type == 'any' %} selected{% endif %}>Auto Accept Type</option>
|
||||
<option value="0" {% if filters.auto_accept_type == 0 %} selected{% endif %}>Manual</option>
|
||||
<option value="1" {% if filters.auto_accept_type == 1 %} selected{% endif %}>Automatic</option>
|
||||
<option value="2" {% if filters.auto_accept_type == 2 %} selected{% endif %}>Known Identities</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="w-full md:w-auto p-1.5">
|
||||
<div class="relative">
|
||||
{{ input_arrow_down_svg | safe }}
|
||||
<select name="sent_from" id="sent_from" class="bg-gray-50 text-gray-900 appearance-none pr-10 pl-5 dark:bg-gray-500 dark:text-white border border-gray-300 dark:border-gray-400 dark:text-gray-50 dark:placeholder-gray-50 text-sm rounded-lg outline-none block w-full p-2.5 focus:ring-0">
|
||||
<option value="any" {% if not filters.sent_from %} selected{% endif %}>All Offers</option>
|
||||
<option value="public" {% if filters.sent_from == 'public' %} selected{% endif %}>Public</option>
|
||||
<option value="private" {% if filters.sent_from == 'private' %} selected{% endif %}>Private</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="w-full md:w-auto p-1.5">
|
||||
<div class="relative">
|
||||
<button type="button" id="clearFilters" class="transition-opacity duration-200 flex justify-center w-full px-4 py-2.5 font-medium text-sm hover:text-white dark:text-white dark:bg-gray-500 bg-coolGray-200 hover:bg-green-600 hover:border-green-600 rounded-lg transition duration-200 border border-coolGray-200 dark:border-gray-400 rounded-md shadow-button focus:ring-0 focus:outline-none" disabled>
|
||||
<span>Clear Filters</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-full md:w-auto p-1.5">
|
||||
<div class="relative">
|
||||
<button type="button" id="refreshOffers" class="flex justify-center w-full px-4 py-2.5 font-medium text-sm text-white bg-blue-600 hover:bg-green-600 hover:border-green-600 rounded-lg transition duration-200 border border-blue-500 rounded-md shadow-button focus:ring-0 focus:outline-none">
|
||||
<svg id="refreshIcon" class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" 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"></path>
|
||||
</svg>
|
||||
<span id="refreshText">Refresh</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<div id="selected_coins_container" class="w-full py-3" style="display: none;">
|
||||
<div class="flex flex-wrap justify-center gap-2">
|
||||
<div id="coin_to_badges" class="flex flex-wrap gap-1"></div>
|
||||
<div id="coin_from_badges" class="flex flex-wrap gap-1"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -297,7 +328,7 @@
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<div class="mt-5 lg:container mx-auto lg:px-0 px-6">
|
||||
<div class="lg:container mx-auto lg:px-0 px-6">
|
||||
<div class="pt-0 pb-6 bg-coolGray-100 dark:bg-gray-500 rounded-xl">
|
||||
<div class="px-0">
|
||||
<div class="w-auto mt-6 overflow-auto lg:overflow-hidden">
|
||||
|
||||
@@ -884,6 +884,7 @@ def page_offers(self, url_split, post_string, sent=False):
|
||||
"sort_dir": "desc",
|
||||
"sent_from": "any" if sent is False else "only",
|
||||
"active": "any",
|
||||
"auto_accept_type": "any",
|
||||
}
|
||||
|
||||
filter_prefix = "page_offers_sent" if sent else "page_offers"
|
||||
@@ -908,6 +909,16 @@ def page_offers(self, url_split, post_string, sent=False):
|
||||
sent_from = get_data_entry(form_data, "sent_from")
|
||||
ensure(sent_from in ["any", "only"], "Invalid sent filter")
|
||||
filters["sent_from"] = sent_from
|
||||
if have_data_entry(form_data, "auto_accept_type"):
|
||||
auto_accept_type = get_data_entry(form_data, "auto_accept_type")
|
||||
ensure(
|
||||
auto_accept_type in ["any", "0", "1", "2"],
|
||||
"Invalid auto accept type filter",
|
||||
)
|
||||
if auto_accept_type == "any":
|
||||
filters["auto_accept_type"] = "any"
|
||||
else:
|
||||
filters["auto_accept_type"] = int(auto_accept_type)
|
||||
if have_data_entry(form_data, "active"):
|
||||
active_filter = get_data_entry(form_data, "active")
|
||||
ensure(
|
||||
|
||||
Reference in New Issue
Block a user