PageSourceSearch

https://arzpaya.com/wp-content/themes/arzpaya02/assets/js/pricing-crypto-table.js?ver=1.0.2

js arzpaya.com collected 2026-09-24 12:59:55 UTC 15,575 bytes, 473 lines download raw bytes

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.