485 lines
18 KiB
JavaScript
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();
|
|
});
|
|
})();
|