1/** 2 * Pricing Crypto Table - ÙØ§Ø±Ûز ٠برداشت ر٠زارز 3 * Fetches data from ArzPaya APIs and populates the crypto fee table. 4 * 5 * APIs: 6 * - https://na1.arzpaya.com/pricing (fee data + NetworkName per network) 7 * - https://na.arzpaya.com/general/currencytype (currency symbol, name, icon) 8 */ 9(function () { 10 'use strict'; 11 12 const PRICING_API = 'https://na1.arzpaya.com/pricing'; 13 const CURRENCY_TYPE_API = 'https://na.arzpaya.com/general/currencytype'; 14 const ICON_PATH = '/assets/images/CurrencyTypeIcon/'; 15 const ICON_FALLBACK = 'UNKNOW.svg'; 16 17 /** 18 * Ø¢ÛÚ©Ù٠ارز از Ù¾ÙØ´Ù ÙÙکا٠ت٠(Ù Ø«Ù Ù¾ÙØ§Ú¯ÛÙ crypto-table) 19 * @param {string} symbol - Ù٠اد ارز (Ù Ø«ÙØ§Ù BTC, ETH) 20 * @returns {string} URL Ø¢ÛÚ©ÙÙ 21 */ 22 function getLocalIconUrl(symbol) { 23 const themeUri = 24 (typeof arzpayaPricingTable !== 'undefined' && arzpayaPricingTable.templateDirectoryUri) || ''; 25 if (!themeUri) { 26 return ''; 27 } 28 if (!symbol || symbol === 'â' || String(symbol).trim() === '') { 29 return themeUri + ICON_PATH + ICON_FALLBACK; 30 } 31 let clean = String(symbol).toUpperCase().trim(); 32 // Remove market suffixes like BTCUSDT / ETHIR, but keep literal symbols USDT/IR intact. 33 const suffixMatch = clean.match(/^(.*?)[-\s]*(IR|USDT)\s*$/i); 34 if (suffixMatch && suffixMatch[1] && suffixMatch[1].trim() !== '') { 35 clean = suffixMatch[1].trim(); 36 } clean = clean.trim(); 37 if (!clean) { 38 clean = 'UNKNOW'; 39 } 40 return themeUri + ICON_PATH + clean + '.svg'; 41 } 42 43 /** 44 * Fetch JSON from URL 45 * @param {string} url 46 * @returns {Promise<object|null>} 47 */ 48 async function fetchJson(url) { 49 try { 50 const res = await fetch(url, { 51 headers: { Accept: 'application/json' }, 52 }); 53 if (!res.ok) return null; 54 return await res.json(); 55 } catch (e) { 56 console.error('pricing-crypto-table: fetch error', url, e); 57 return null; 58 } 59 } 60 61 /** 62 * Format number for display (trim unnecessary trailing zeros) 63 * @param {number} n 64 * @param {number} maxDecimals 65 * @returns {string} 66 */ 67 function formatFee(n, maxDecimals) { 68 if (n == null || Number.isNaN(n)) return 'â'; 69 const num = Number(n); 70 if (num === 0) return '0'; 71 if (maxDecimals != null) { 72 const s = num.toFixed(maxDecimals); 73 return s.replace(/\.?0+$/, '') || '0'; 74 } 75 const s = String(num); 76 return s.replace(/\.?0+$/, '') || '0'; 77 } 78 79 /** 80 * Escape HTML for safe text content 81 * @param {string} s 82 * @returns {string} 83 */ 84 function escapeHtml(s) { 85 if (s == null) return ''; 86 const div = document.createElement('div'); 87 div.textContent = s; 88 return div.innerHTML; 89 } 90 91 /** 92 * Build currency id -> { symbol, nameFa, nameEn } from currencytype API 93 * @param {object} data - Full API response 94 * @returns {Object<number, {symbol: string, nameFa: string, nameEn: string}>} 95 */ 96 function buildCurrencyMap(data) { 97 const map = {}; 98 const fa = (data && data.Data && data.Data.Fa) || []; 99 fa.forEach(function (c) { 100 map[c.Id] = { 101 symbol: (c.Symbol || '').trim() || 'â', 102 nameFa: (c.PersianName || '').trim() || 'â', 103 nameEn: (c.LatinName || '').trim() || '', 104 }; 105 }); 106 return map; 107 } 108 109 /** 110 * سÙÙÙ ÙØ§Ù ارز (ÙÙØ· در اÙÙÛ٠سطر ÙØ± Ø§Ø±Ø²Ø Ø§Ú¯Ø± ÚÙØ¯ شبک٠باشد rowspan Ø¨Ø±Ø§Û ÙØ³Ø·âÚÛÙ Ø¹Ù ÙØ¯Û) 111 */ 112 function buildNameCell(currency, currencyMap, rowspan, groupId) { 113 const currencyId = currency.CurrencyType; 114 const meta = currencyMap[currencyId] || null; 115 const symbol = meta ? meta.symbol : (currency.CurrencyTypeTitle || 'â'); 116 const nameFa = meta ? meta.nameFa : (currency.CurrencyTypeTitle || 'â'); 117 const iconUrl = getLocalIconUrl(symbol); 118 const symbolEsc = escapeHtml(symbol); 119 const nameFaEsc = escapeHtml(nameFa); 120 const rowspanAttr = rowspan > 1 ? ' rowspan="' + rowspan + '"' : ''; 121 const multiClass = rowspan > 1 ? ' pricing-crypto-table-name-cell-multi' : ''; 122 return ( 123 '<td class="pricing-crypto-table-col-1 pricing-crypto-table-name-cell' + 124 multiClass + 125 '"' + 126 rowspanAttr + 127 ' data-group="' + 128 escapeHtml(groupId || '') + 129 '"' + 130 '>' + 131 '<div class="pricing-crypto-name">' + 132 '<div class="pricing-crypto-icon"><img src="' + 133 escapeHtml(iconUrl) + 134 '" alt="" width="24" height="24" loading="lazy" /></div>' + 135 '<div class="pricing-crypto-name-text">' + 136 '<div class="pricing-crypto-name-persian">' + 137 nameFaEsc + 138 '</div>' + 139 '<div class="pricing-crypto-name-symbol">' + 140 symbolEsc + 141 '</div>' + 142 '</div>' + 143 '</div>' + 144 '</td>'
145 ); 146 } 147 148 /** 149 * سÙÙ٠خاÙÛ Ø³ØªÙÙ ÙØ§Ù (Ø¨Ø±Ø§Û Ø³Ø·Ø±ÙØ§Û Ø¨Ø¹Ø¯Û Ù٠ا٠ارز ÙÙØªÛ ÙÙØ· ÛÚ© شبک٠دارÛÙ ) 150 */ 151 function buildNameCellEmpty() { 152 return '<td class="pricing-crypto-table-col-1 pricing-crypto-table-name-cell-empty"></td>'; 153 } 154 155 /** 156 * ÙÙØ· Û· سÙÙ٠داد٠(Ø´Ø¨Ú©ÙØ Ú©Ø§Ø±Ù Ø²Ø¯ÙØ§Ø ...) بدÙ٠سÙÙÙ ÙØ§Ù ارز 157 */ 158 function buildDataCells(currency, network, currencyMap) { 159 const networkTitle = (network.NetworkName || '').trim() || 'â'; 160 161 const depositFee = 'راÛگاÙ'; 162 const withdrawFee = formatFee(network.WithdrawFee, 8); 163 const withdrawMin = formatFee(network.WithdrawMin, 8); 164 const depositMin = formatFee(network.DepositMin, 8); 165 const minConfirm = network.MinConfirm != null && network.MinConfirm !== '' ? String(network.MinConfirm) : 'â'; 166 const isActive = network.DepositEnable === true; 167 const activeIcon = isActive ? 'â' : 'â'; 168 const activeClass = isActive ? 'pricing-crypto-active-yes' : 'pricing-crypto-active-no'; 169 170 const networkEsc = escapeHtml(networkTitle); 171 172 return ( 173 '<td class="pricing-crypto-table-col-2">' + 174 '<span class="pricing-crypto-network-chip">' + networkEsc + '</span>' + 175 '</td>' + 176 '<td class="pricing-crypto-table-col-3">' + 177 depositFee + 178 '</td>' + 179 '<td class="pricing-crypto-table-col-4">' + 180 withdrawFee + 181 '</td>' + 182 '<td class="pricing-crypto-table-col-5">' + 183 withdrawMin + 184 '</td>' + 185 '<td class="pricing-crypto-table-col-6">' + 186 depositMin + 187 '</td>' + 188 '<td class="pricing-crypto-table-col-7">' + 189 escapeHtml(minConfirm) + 190 '</td>' + 191 '<td class="pricing-crypto-table-col-8">' + 192 '<span class="pricing-crypto-active ' + 193 activeClass + 194 '" aria-label="' + 195 (isActive ? 'ÙØ¹Ø§Ù' : 'ØºÛØ±ÙعاÙ') + 196 '">' + 197 escapeHtml(activeIcon) + 198 '</span>' + 199 '</td>' 200 ); 201 } 202 203 /** 204 * Ø¨Ø±Ø§Û ÙØ± ارز: ÙØ§Ù ÙÙØ· در اÙÙÛÙ Ø³Ø·Ø±Ø Ø§Ú¯Ø± Û²+ شبک٠باشد با rowspan Ù Ø³Ø·Ø±ÙØ§Û Ø¨Ø¹Ø¯Û ÙÙØ· Û· سÙÙÙ 205 */ 206 function buildRowsForCurrency(currency, currencyMap, groupId) { 207 const networks = currency.Networks || []; 208 const rows = []; 209 if (networks.length === 0) return rows; 210 211 const nameFa = (currencyMap[currency.CurrencyType] && currencyMap[currency.CurrencyType].nameFa) || currency.CurrencyTypeTitle || 'â'; 212 const symbol = (currencyMap[currency.CurrencyType] && currencyMap[currency.CurrencyType].symbol) || 'â'; 213 const rowspan = networks.length; 214 const isMultiNetwork = rowspan > 1; 215 const multiClass = isMultiNetwork ? ' pricing-crypto-table-row-multi' : ''; 216 217 for (let j = 0; j < networks.length; j++) { 218 const networkTitle = (networks[j].NetworkName || '').trim() || 'â'; 219 const searchText = [nameFa, symbol, networkTitle].join(' '); 220 if (j === 0) { 221 rows.push( 222 '<tr class="pricing-crypto-table-row pricing-crypto-table-row-first' + 223 multiClass + 224 '" data-search="' + 225 escapeHtml(searchText) + 226 '" data-group="' + 227 escapeHtml(groupId || '') + 228 '">' + 229 buildNameCell(currency, currencyMap, rowspan, groupId) + 230 buildDataCells(currency, networks[j], currencyMap) + 231 '</tr>' 232 ); 233 } else { 234 const continuationClass = 235 j === networks.length - 1 236 ? ' pricing-crypto-table-row-continuation pricing-crypto-table-row-multi-last' 237 : ' pricing-crypto-table-row-continuation'; 238 rows.push( 239 '<tr class="pricing-crypto-table-row' + 240 continuationClass + 241 multiClass + 242 '" data-search="' + 243 escapeHtml(searchText) + 244 '" data-group="' + 245 escapeHtml(groupId || '') + 246 '">' + 247 buildDataCells(currency, networks[j], currencyMap) + 248 '</tr>' 249 ); 250 } 251 } 252 return rows; 253 } 254 255 /** 256 * Clear loading/error overlay state from table scroll container. 257 * @param {Element} tbody 258 */ 259 function clearTableState(tbody) { 260 const scroll = tbody && tbody.closest('.pricing-crypto-table-scroll'); 261 if (!scroll) return; 262 scroll.classList.remove('is-state');
263 const overlay = scroll.querySelector('.pricing-crypto-table-state-overlay'); 264 if (overlay) { 265 overlay.remove(); 266 } 267 } 268 269 /** 270 * Render loading/error state centered in scroll box viewport. 271 * @param {Element} tbody 272 * @param {'loading'|'error'|'empty'} type 273 * @param {string} [message] 274 * @param {{keepRows?: boolean}} [options] 275 */ 276 function setTableState(tbody, type, message, options) { 277 const scroll = tbody && tbody.closest('.pricing-crypto-table-scroll'); 278 if (!scroll) return; 279 const keepRows = !!(options && options.keepRows); 280 281 clearTableState(tbody); 282 if (!keepRows) { 283 tbody.innerHTML = ''; 284 } 285 286 const overlay = document.createElement('div'); 287 overlay.className = 'pricing-crypto-table-state-overlay'; 288 289 const state = document.createElement('div'); 290 const stateClass = 291 type === 'error' 292 ? 'pricing-crypto-table-state-error' 293 : type === 'empty' 294 ? 'pricing-crypto-table-state-empty' 295 : 'pricing-crypto-table-state-loading'; 296 state.className = 'pricing-crypto-table-state ' + stateClass; 297 298 if (type === 'loading') { 299 state.innerHTML = 300 '<span class="pricing-crypto-table-spinner" aria-hidden="true"></span>' + 301 '<span class="pricing-crypto-table-state-text">در ØØ§Ù بارگذارÛ...</span>'; 302 } else if (type === 'empty') { 303 state.innerHTML = 304 '<span class="pricing-crypto-table-state-text">' + 305 escapeHtml(message || 'ÙØªÛجÙâØ§Û Ø¨Ø±Ø§Û Ø¬Ø³ØªØ¬Ù Ù¾ÛØ¯Ø§ ÙØ´Ø¯.') + 306 '</span>'; 307 } else { 308 state.innerHTML = 309 '<span class="pricing-crypto-table-state-text">' + 310 escapeHtml(message || 'خطا در Ø¨Ø§Ø±Ú¯Ø°Ø§Ø±Û Ø¯Ø§Ø¯ÙâÙØ§.') + 311 '</span>'; 312 } 313 314 overlay.appendChild(state); 315 scroll.appendChild(overlay); 316 scroll.classList.add('is-state'); 317 } 318 319 /** 320 * Show loading state in tbody 321 * @param {Element} tbody 322 */ 323 function showLoading(tbody) { 324 setTableState(tbody, 'loading'); 325 } 326 327 /** 328 * Show error state in tbody 329 * @param {Element} tbody 330 * @param {string} message 331 */ 332 function showError(tbody, message) { 333 setTableState(tbody, 'error', message); 334 } 335 336 /** 337 * Populate tbody with pricing data 338 * @param {Element} tbody - #crypto-table-body 339 */ 340 async function populateTable(tbody) { 341 showLoading(tbody); 342 343 const [pricingRes, currencyRes] = await Promise.all([ 344 fetchJson(PRICING_API), 345 fetchJson(CURRENCY_TYPE_API), 346 ]); 347 348 if (!pricingRes || !Array.isArray(pricingRes.Data)) { 349 showError(tbody, 'Ø¯Ø±ÛØ§Ùت ÙÛØ³Øª Ú©Ø§Ø±Ù Ø²Ø¯ÙØ§ Ù Ù Ú©Ù ÙØ´Ø¯.'); 350 return; 351 } 352 353 const currencyMap = buildCurrencyMap(currencyRes || {}); 354 355 const rows = []; 356 const data = pricingRes.Data; 357 358 for (let i = 0; i < data.length; i++) { 359 const groupId = 'currency-group-' + i; 360 rows.push.apply(rows, buildRowsForCurrency(data[i], currencyMap, groupId)); 361 } 362 363 clearTableState(tbody); 364 tbody.innerHTML = rows.join(''); 365 attachSearch(tbody); 366 attachGroupHover(tbody); 367 } 368 369 /** 370 * Attach search filter to #crypto-search and table rows 371 * @param {Element} tbody 372 */ 373 function attachSearch(tbody) { 374 const input = document.getElementById('crypto-search'); 375 const table = tbody.closest('.pricing-crypto-table'); 376 if (!input || !table) return; 377 378 function filter() { 379 const q = (input.value || '').trim().toLowerCase(); 380 const rows = tbody.querySelectorAll('.pricing-crypto-table-row[data-search]'); 381 let visibleCount = 0; 382 rows.forEach(function (tr) { 383 const text = (tr.getAttribute('data-search') || '').toLowerCase(); 384 const isVisible = q === '' || text.indexOf(q) !== -1; 385 tr.style.display = isVisible ? '' : 'none'; 386 if (isVisible) { 387 visibleCount++; 388 } 389 }); 390 391 if (q !== '' && visibleCount === 0) { 392 setTableState(tbody, 'empty', 'ÙØªÛجÙâØ§Û Ø¨Ø±Ø§Û Ø¬Ø³ØªØ¬Ù Ù¾ÛØ¯Ø§ ÙØ´Ø¯.', { keepRows: true }); 393 } else { 394 clearTableState(tbody); 395 } 396 } 397 398 input.addEventListener('input', filter); 399 input.addEventListener('change', filter); 400 } 401 402 /** 403 * Hovering the currency name cell highlights all rows of that currency group. 404 * Hovering any network row highlights only the hovered row (handled by CSS). 405 * @param {Element} tbody 406 */ 407 function attachGroupHover(tbody) { 408 const nameCells = tbody.querySelectorAll('.pricing-crypto-table-name-cell-multi[data-group]'); 409 if (!nameCells.length) return; 410 const rows = tbody.querySelectorAll('.pricing-crypto-table-row[data-group]'); 411 412 function getNameCell(groupId) { 413 return tbody.querySelector('.pricing-crypto-table-name-cell-multi[data-group="' + groupId + '"]'); 414 } 415 416 function setGroupHover(groupId, active) { 417 if (!groupId) return; 418 const rows = tbody.querySelectorAll('.pricing-crypto-table-row[data-group="' + groupId + '"]'); 419 rows.forEach(function (row) { 420 row.classList.toggle('pricing-crypto-group-hover', active); 421 }); 422 const nameCell = getNameCell(groupId); 423 if (nameCell) { 424 nameCell.classList.toggle('pricing-crypto-group-hover-cell', active); 425 } 426 } 427
428 nameCells.forEach(function (cell) { 429 const groupId = cell.getAttribute('data-group') || ''; 430 if (!groupId) return; 431 432 cell.addEventListener('mouseenter', function () { 433 setGroupHover(groupId, true); 434 }); 435 436 cell.addEventListener('mouseleave', function () { 437 setGroupHover(groupId, false); 438 }); 439 }); 440 441 // Hovering any network row of a multi-network currency should also 442 // highlight its currency name cell (but not other rows). 443 rows.forEach(function (row) { 444 const groupId = row.getAttribute('data-group') || ''; 445 const nameCell = getNameCell(groupId); 446 if (!groupId || !nameCell) return; // skip single-network rows 447 448 row.addEventListener('mouseenter', function () { 449 nameCell.classList.add('pricing-crypto-row-hover-cell'); 450 }); 451 452 row.addEventListener('mouseleave', function () { 453 nameCell.classList.remove('pricing-crypto-row-hover-cell'); 454 }); 455 }); 456 } 457 458 /** 459 * Init when DOM is ready 460 */ 461 function init() { 462 const tbody = document.getElementById('crypto-table-body'); 463 if (tbody) { 464 populateTable(tbody); 465 } 466 } 467 468 if (document.readyState === 'loading') { 469 document.addEventListener('DOMContentLoaded', init); 470 } else { 471 init(); 472 } 473})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.