/* Dashboard Pools Page JS - CSP compliant (no inline handlers) */ (function () { 'use strict'; // Toast helpers function showSuccessToast(message) { const body = document.getElementById('successToastBody'); if (body) body.textContent = message; const toastEl = document.getElementById('successToast'); if (toastEl && window.bootstrap) { const toast = bootstrap.Toast.getOrCreateInstance(toastEl); toast.show(); } } function showErrorToast(message) { const body = document.getElementById('errorToastBody'); if (body) body.textContent = message; const toastEl = document.getElementById('errorToast'); if (toastEl && window.bootstrap) { const toast = bootstrap.Toast.getOrCreateInstance(toastEl); toast.show(); } } // Hydration function readHydration() { try { const el = document.getElementById('pools-hydration'); if (!el) return {}; return JSON.parse(el.textContent || '{}'); } catch (e) { console.warn('Failed to parse pools hydration JSON', e); return {}; } } // Charts let charts = {}; function initCharts() { if (!window.Chart) return; // Chart.js not loaded // Global defaults Chart.defaults.font.family = "'Poppins', 'Helvetica Neue', 'Helvetica', 'Arial', sans-serif"; Chart.defaults.font.size = 12; Chart.defaults.responsive = true; Chart.defaults.maintainAspectRatio = false; const priceHistoryCtx = document.getElementById('creditsPriceHistoryChart'); const liquidityCtx = document.getElementById('liquidityPoolDistributionChart'); const volumeCtx = document.getElementById('exchangeVolumeChart'); const stakingCtx = document.getElementById('stakingDistributionChart'); if (priceHistoryCtx) { charts.priceHistory = new Chart(priceHistoryCtx.getContext('2d'), { type: 'line', data: { labels: ['Dec', 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun'], datasets: [ { label: 'Credits-EUR Rate', data: [0.82, 0.84, 0.83, 0.85, 0.85, 0.86, 0.85], borderColor: '#007bff', backgroundColor: 'rgba(0, 123, 255, 0.1)', borderWidth: 2, tension: 0.3, fill: true, }, { label: 'Credits-TFT Rate', data: [4.8, 4.9, 5.0, 5.1, 5.0, 4.9, 5.0], borderColor: '#28a745', backgroundColor: 'rgba(40, 167, 69, 0.1)', borderWidth: 2, tension: 0.3, fill: true, }, ], }, options: { plugins: { legend: { position: 'top' }, }, scales: { y: { beginAtZero: false, title: { display: true, text: 'Exchange Rate' } }, }, }, }); } if (liquidityCtx) { charts.liquidity = new Chart(liquidityCtx.getContext('2d'), { type: 'doughnut', data: { labels: ['Credits-Fiat Pool', 'Credits-TFT Pool', 'Credits-PEAQ Pool'], datasets: [ { data: [1250000, 250000, 100000], backgroundColor: ['#007bff', '#28a745', '#17a2b8'], borderWidth: 1, }, ], }, options: { plugins: { legend: { position: 'right', labels: { boxWidth: 12 } }, }, }, }); } if (volumeCtx) { charts.volume = new Chart(volumeCtx.getContext('2d'), { type: 'bar', data: { labels: ['Week 1', 'Week 2', 'Week 3', 'Week 4'], datasets: [ { label: 'Credits-Fiat', data: [2500, 3200, 2800, 3500], backgroundColor: '#007bff', borderWidth: 1 }, { label: 'Credits-TFT', data: [1500, 1800, 2200, 2000], backgroundColor: '#28a745', borderWidth: 1 }, { label: 'Credits-PEAQ', data: [800, 1000, 1200, 900], backgroundColor: '#17a2b8', borderWidth: 1 }, ], }, options: { plugins: { legend: { position: 'top' } }, scales: { y: { beginAtZero: true, title: { display: true, text: 'Volume (USD)' }, stacked: true }, x: { stacked: true }, }, }, }); } if (stakingCtx) { charts.staking = new Chart(stakingCtx.getContext('2d'), { type: 'pie', data: { labels: ['$10-50', '$51-100', '$101-500', '$501+'], datasets: [ { data: [450, 280, 150, 75], backgroundColor: ['#007bff', '#28a745', '#ffc107', '#dc3545'], borderWidth: 1 }, ], }, options: { plugins: { legend: { position: 'right', labels: { boxWidth: 12 } } }, }, }); } } async function loadPoolData() { try { const pools = await window.apiJson('/api/pools', { cache: 'no-store' }); (Array.isArray(pools) ? pools : []).forEach(updatePoolCard); } catch (e) { console.warn('Failed to load /api/pools', e); } } function updatePoolCard(pool) { const card = document.querySelector(`[data-pool-id="${pool.id}"]`); if (!card) return; const rateEl = card.querySelector('.exchange-rate'); const liqEl = card.querySelector('.liquidity'); if (rateEl) rateEl.textContent = `1 ${pool.token_a} = ${pool.exchange_rate} ${pool.token_b}`; if (liqEl) liqEl.textContent = `${(pool.liquidity || 0).toLocaleString()} ${pool.token_a}`; } async function loadAnalyticsData() { try { const analytics = await window.apiJson('/api/pools/analytics', { cache: 'no-store' }); if (analytics) updateChartsWithRealData(analytics); } catch (e) { console.warn('Failed to load /api/pools/analytics', e); } } function updateChartsWithRealData(analytics) { if (analytics.price_history && charts.priceHistory) { const labels = analytics.price_history.map((p) => new Date(p.timestamp).toLocaleDateString()); const prices = analytics.price_history.map((p) => p.price); charts.priceHistory.data.labels = labels; charts.priceHistory.data.datasets[0].data = prices; charts.priceHistory.update(); } if (analytics.liquidity_distribution && charts.liquidity) { const labels = Object.keys(analytics.liquidity_distribution); const data = Object.values(analytics.liquidity_distribution); charts.liquidity.data.labels = labels; charts.liquidity.data.datasets[0].data = data; charts.liquidity.update(); } if (analytics.staking_distribution && charts.staking) { const labels = Object.keys(analytics.staking_distribution); const data = Object.values(analytics.staking_distribution); charts.staking.data.labels = labels; charts.staking.data.datasets[0].data = data; charts.staking.update(); } } // Calculators function setupCalculators() { // Buy with TFT const tfpAmountTFT = document.getElementById('tfpAmountTFT'); if (tfpAmountTFT) { tfpAmountTFT.addEventListener('input', () => { const amount = parseFloat(tfpAmountTFT.value) || 0; const tftCost = amount * 0.5; // 1 MC = 0.5 TFT const modal = document.getElementById('buyMCWithTFTModal'); if (modal) { const rows = modal.querySelectorAll('.alert .d-flex.justify-content-between .text-end'); // rows[0] -> Amount, rows[1] -> Cost if (rows[0]) rows[0].textContent = `${amount} MC`; if (rows[1]) rows[1].textContent = `${tftCost.toFixed(1)} TFT`; } }); } // Sell for TFT const sellTfpAmountTFT = document.getElementById('sellTfpAmountTFT'); if (sellTfpAmountTFT) { sellTfpAmountTFT.addEventListener('input', () => { const amount = parseFloat(sellTfpAmountTFT.value) || 0; const tftReceive = amount * 0.5; // 1 MC = 0.5 TFT const modal = document.getElementById('sellMCForTFTModal'); if (modal) { const rows = modal.querySelectorAll('.alert .d-flex.justify-content-between .text-end'); if (rows[0]) rows[0].textContent = `${amount} MC`; if (rows[1]) rows[1].textContent = `${tftReceive.toFixed(1)} TFT`; } }); } // Buy with PEAQ const tfpAmountPEAQ = document.getElementById('tfpAmountPEAQ'); if (tfpAmountPEAQ) { tfpAmountPEAQ.addEventListener('input', () => { const amount = parseFloat(tfpAmountPEAQ.value) || 0; const peaqCost = amount * 2.0; // 1 MC = 2 PEAQ const modal = document.getElementById('buyMCWithPEAQModal'); if (modal) { const rows = modal.querySelectorAll('.alert .d-flex.justify-content-between .text-end'); if (rows[0]) rows[0].textContent = `${amount} MC`; if (rows[1]) rows[1].textContent = `${peaqCost.toFixed(0)} PEAQ`; } }); } // Sell for PEAQ const sellTfpAmountPEAQ = document.getElementById('sellTfpAmountPEAQ'); if (sellTfpAmountPEAQ) { sellTfpAmountPEAQ.addEventListener('input', () => { const amount = parseFloat(sellTfpAmountPEAQ.value) || 0; const peaqReceive = amount * 2.0; // 1 MC = 2 PEAQ const modal = document.getElementById('sellMCForPEAQModal'); if (modal) { const rows = modal.querySelectorAll('.alert .d-flex.justify-content-between .text-end'); if (rows[0]) rows[0].textContent = `${amount} MC`; if (rows[1]) rows[1].textContent = `${peaqReceive.toFixed(0)} PEAQ`; } }); } // Optional: Sell Credits for fiat quick calculator using simple FX placeholders const sellCreditsAmount = document.getElementById('sellCreditsAmount'); const receiveCurrency = document.getElementById('receiveCurrency'); if (sellCreditsAmount && receiveCurrency) { const updateReceive = () => { const amount = parseFloat(sellCreditsAmount.value) || 0; const ccy = receiveCurrency.value; let rate = 1.0; // 1 Credit = 1 USD base if (ccy === 'EUR') rate = 0.9; // placeholder if (ccy === 'GBP') rate = 0.8; // placeholder const receive = amount * rate; const modal = document.getElementById('sellCreditsModal'); if (modal) { // Find the last text-end in alert -> corresponds to You receive const rows = modal.querySelectorAll('.alert .d-flex.justify-content-between .text-end'); if (rows[1]) rows[1].textContent = `${receive.toFixed(2)} ${ccy}`; } }; sellCreditsAmount.addEventListener('input', updateReceive); receiveCurrency.addEventListener('change', updateReceive); } } // Event delegation for actions async function handleAction(action) { switch (action) { case 'confirm-buy-credits-fiat': { const amount = parseFloat(document.getElementById('creditsAmount')?.value); const paymentMethod = document.getElementById('paymentMethod')?.value; if (!amount || amount <= 0) return showErrorToast('Enter a valid amount'); try { await window.apiJson('/api/wallet/buy-credits', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ amount, payment_method: paymentMethod }), }); showSuccessToast('Credits purchase successful'); const modal = document.getElementById('buyCreditsModal'); if (modal && window.bootstrap) bootstrap.Modal.getOrCreateInstance(modal).hide(); if (document.getElementById('creditsAmount')) document.getElementById('creditsAmount').value = ''; } catch (e) { if (e && e.status === 402) { // Insufficient funds handled globally via 402 interceptor (opens credit modal) } else { showErrorToast(e?.message || 'Purchase failed'); } } break; } case 'confirm-sell-credits-fiat': { const amount = parseFloat(document.getElementById('sellCreditsAmount')?.value); const currency = document.getElementById('receiveCurrency')?.value; const payout_method = document.getElementById('payoutMethod')?.value; if (!amount || amount <= 0) return showErrorToast('Enter a valid amount'); try { await window.apiJson('/api/wallet/sell-credits', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ amount, currency, payout_method }), }); showSuccessToast('Credits sale successful'); const modal = document.getElementById('sellCreditsModal'); if (modal && window.bootstrap) bootstrap.Modal.getOrCreateInstance(modal).hide(); if (document.getElementById('sellCreditsAmount')) document.getElementById('sellCreditsAmount').value = ''; } catch (e) { if (e && e.status === 402) { // Handled globally via credit modal } else { showErrorToast(e?.message || 'Sale failed'); } } break; } case 'confirm-stake-credits': { const amount = parseFloat(document.getElementById('stakeAmount')?.value); const duration_months = parseInt(document.getElementById('stakeDuration')?.value, 10); if (!amount || amount <= 0) return showErrorToast('Enter a valid amount'); try { await window.apiJson('/api/pools/stake', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ amount, duration_months }), }); showSuccessToast(`Successfully staked $${amount} for ${duration_months} months`); const modal = document.getElementById('stakeCreditsModal'); if (modal && window.bootstrap) bootstrap.Modal.getOrCreateInstance(modal).hide(); if (document.getElementById('stakeAmount')) document.getElementById('stakeAmount').value = ''; } catch (e) { if (e && e.status === 402) { // Handled globally } else { showErrorToast(e?.message || 'Staking failed'); } } break; } case 'confirm-buy-tfc-tft': { const amount = parseFloat(document.getElementById('tfpAmountTFT')?.value); if (!amount || amount <= 0) return showErrorToast('Enter a valid amount'); try { await window.apiJson('/api/wallet/buy-credits', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ amount, payment_method: 'TFT' }), }); showSuccessToast(`Purchased ${amount} MC with TFT`); const modal = document.getElementById('buyMCWithTFTModal'); if (modal && window.bootstrap) bootstrap.Modal.getOrCreateInstance(modal).hide(); } catch (e) { if (e && e.status === 402) { // Handled globally } else { showErrorToast(e?.message || 'Purchase failed'); } } break; } case 'confirm-sell-tfc-tft': { const amount = parseFloat(document.getElementById('sellTfpAmountTFT')?.value); if (!amount || amount <= 0) return showErrorToast('Enter a valid amount'); try { await window.apiJson('/api/wallet/sell-credits', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ amount, currency: 'TFT', payout_method: 'blockchain' }), }); showSuccessToast(`Sold ${amount} MC for TFT`); const modal = document.getElementById('sellMCForTFTModal'); if (modal && window.bootstrap) bootstrap.Modal.getOrCreateInstance(modal).hide(); } catch (e) { if (e && e.status === 402) { // Handled globally } else { showErrorToast(e?.message || 'Sale failed'); } } break; } case 'confirm-buy-tfc-peaq': { const amount = parseFloat(document.getElementById('tfpAmountPEAQ')?.value); if (!amount || amount <= 0) return showErrorToast('Enter a valid amount'); try { await window.apiJson('/api/wallet/buy-credits', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ amount, payment_method: 'PEAQ' }), }); showSuccessToast(`Purchased ${amount} MC with PEAQ`); const modal = document.getElementById('buyMCWithPEAQModal'); if (modal && window.bootstrap) bootstrap.Modal.getOrCreateInstance(modal).hide(); } catch (e) { if (e && e.status === 402) { // Handled globally } else { showErrorToast(e?.message || 'Purchase failed'); } } break; } case 'confirm-sell-tfc-peaq': { const amount = parseFloat(document.getElementById('sellTfpAmountPEAQ')?.value); if (!amount || amount <= 0) return showErrorToast('Enter a valid amount'); try { await window.apiJson('/api/wallet/sell-credits', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ amount, currency: 'PEAQ', payout_method: 'blockchain' }), }); showSuccessToast(`Sold ${amount} MC for PEAQ`); const modal = document.getElementById('sellMCForPEAQModal'); if (modal && window.bootstrap) bootstrap.Modal.getOrCreateInstance(modal).hide(); } catch (e) { if (e && e.status === 402) { // Handled globally } else { showErrorToast(e?.message || 'Sale failed'); } } break; } default: break; } } function setupEventDelegation() { document.addEventListener('click', (e) => { const btn = e.target.closest('[data-action]'); if (!btn) return; const action = btn.getAttribute('data-action'); if (!action) return; handleAction(action); }); } // Init document.addEventListener('DOMContentLoaded', () => { const hydration = readHydration(); if (hydration.charts) initCharts(); // data loads (best-effort) loadPoolData(); loadAnalyticsData(); setupCalculators(); setupEventDelegation(); }); })();