Files
projectmycelium/src/static/js/dashboard_pools.js

485 lines
18 KiB
JavaScript

/* 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();
});
})();