4monedas / index.html
JairoDanielMT's picture
Update index.html
f5ca7d3 verified
<!doctype html>
<html lang="es">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Dashboard: 7d (XRP, PAXG, WLD, USDC) + Earn Morpho (USDC)</title>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<style>
body { font-family: Inter, system-ui, Arial; margin: 18px; background:#f7f8fb; color:#111; }
header { display:flex; gap:12px; align-items:center; margin-bottom:12px; flex-wrap:wrap; }
.card { background:white; border-radius:10px; padding:12px; box-shadow:0 6px 18px rgba(20,20,40,0.06); }
.small { font-size:0.9rem; color:#555; }
#grid { display:grid; grid-template-columns: 1fr 320px; gap:12px; align-items:start; }
.charts { display:grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap:12px; }
.chartWrap { height: 260px; position:relative; }
.banner {
display:none; margin-bottom:10px; padding:10px 12px;
background:#fff7ed; border:1px solid #fed7aa; color:#9a3412;
border-radius:10px; font-size:0.95rem;
}
input[type="number"]{ width:120px; padding:6px; border-radius:8px; border:1px solid #ddd; }
select { padding:6px; border-radius:8px; border:1px solid #ddd; background:white; }
button { padding:8px 10px; border-radius:8px; border:none; background:#2563eb; color:white; cursor:pointer; }
button.secondary { background:#e5e7eb; color:#111; }
.row { display:flex; gap:8px; flex-wrap:wrap; align-items:center; }
.muted { color:#6b7280; font-size:0.85rem; }
@media (max-width: 980px){
#grid { grid-template-columns: 1fr; }
.charts { grid-template-columns: 1fr; }
.chartWrap { height: 240px; }
}
</style>
</head>
<body>
<header>
<h2 style="margin:0;">Crypto 7d — 4 gráficas (XRP, PAXG, WLD, USDC)</h2>
<div class="small">Precios: CoinGecko · Realtime por polling · Morpho estimator (ejemplo).</div>
</header>
<div id="grid">
<div class="charts" id="charts"></div>
<div class="card">
<h3 style="margin-top:0;">Realtime (polling)</h3>
<div class="small" style="margin-bottom:8px;">
Última actualización: <strong id="lastUpdate">-</strong>
</div>
<div class="row" style="margin-bottom:10px;">
<label class="small">Intervalo:</label>
<select id="pollSelect">
<option value="10">10s</option>
<option value="20" selected>20s</option>
<option value="30">30s</option>
<option value="60">60s</option>
</select>
<button id="toggleLiveBtn">Iniciar</button>
<button id="refreshNowBtn" class="secondary">Actualizar ahora</button>
</div>
<div class="muted">Recomendado: 30–60s para máxima estabilidad (menos 429).</div>
<hr style="margin:12px 0;" />
<h3 style="margin:0 0 6px;">Earn Morpho — Estimador (USDC)</h3>
<div class="small" style="margin-bottom:8px;">
Tasa usada (APY referencia): <span id="morphoRateDisplay">4.00%</span>
</div>
<label class="small">Monto en USDC:
<input id="amount" type="number" min="0" step="0.01" value="1000">
</label>
<div class="row" style="margin-top:8px;">
<button id="calcBtn">Calcular (7 días)</button>
<button id="setRateBtn" class="secondary">Usar otra tasa</button>
</div>
<div id="results" style="margin-top:12px;">
<div class="small">Ganancia estimada (7 días): <strong id="gain7">-</strong></div>
<div class="small">Total aproximado: <strong id="total7">-</strong></div>
<div class="muted" style="margin-top:6px;">(Interés compuesto diario aplicado sobre APY)</div>
</div>
<div class="muted" style="margin-top:12px;">
Nota: CoinGecko API pública se usa para datos históricos. Ajusta la tasa Morpho según tu fuente.
</div>
</div>
</div>
<script>
/** IDs correctos CoinGecko */
const ASSETS = [
{ id: 'ripple', label: 'XRP', color: '#1e90ff' },
{ id: 'pax-gold', label: 'PAXG', color: '#bfa34a' },
{ id: 'worldcoin-wld', label: 'WLD', color: '#7c3aed' },
{ id: 'usd-coin', label: 'USDC', color: '#16a34a' },
];
const chartsById = new Map(); // assetId -> { chart, labels, data, hasLivePoint }
let liveTimer = null;
function tsToDateLabel(ts) {
const d = new Date(ts);
return d.toLocaleDateString();
}
function timeLabelNow() {
const d = new Date();
return 'Ahora ' + d.toLocaleTimeString([], {hour:'2-digit', minute:'2-digit', second:'2-digit'});
}
async function fetchMarketChart7d(assetId) {
const url = `https://api.coingecko.com/api/v3/coins/${assetId}/market_chart?vs_currency=usd&days=7&interval=daily`;
const res = await fetch(url);
if (!res.ok) throw new Error(`${assetId} -> ${res.status}`);
const j = await res.json();
return j.prices.map(p => ({ t: p[0], price: p[1] })); // [timestamp, price]
}
async function fetchSimplePrices(ids) {
const qs = encodeURIComponent(ids.join(','));
const url = `https://api.coingecko.com/api/v3/simple/price?ids=${qs}&vs_currencies=usd&include_last_updated_at=true`;
const res = await fetch(url);
if (!res.ok) throw new Error(`simple/price -> ${res.status}`);
return res.json();
}
function makeCard(asset) {
const el = document.createElement('div');
el.className = 'card';
el.innerHTML = `
<div style="display:flex; align-items:baseline; justify-content:space-between; gap:10px; flex-wrap:wrap;">
<div>
<div style="font-size:1.05rem; font-weight:700;">${asset.label}</div>
<div class="muted">${asset.id}</div>
</div>
<div class="small">USD</div>
</div>
<div class="banner" id="err_${asset.id}"></div>
<div class="chartWrap"><canvas id="c_${asset.id}"></canvas></div>
`;
return el;
}
function showAssetError(assetId, msg) {
const box = document.getElementById(`err_${assetId}`);
if (!box) return;
box.style.display = 'block';
box.textContent = msg;
}
function hideAssetError(assetId) {
const box = document.getElementById(`err_${assetId}`);
if (!box) return;
box.style.display = 'none';
box.textContent = '';
}
function createChart(asset, labels, data) {
const ctx = document.getElementById(`c_${asset.id}`).getContext('2d');
const chart = new Chart(ctx, {
type: 'line',
data: {
labels,
datasets: [{
label: asset.label,
data,
borderColor: asset.color,
backgroundColor: asset.color,
tension: 0.2,
fill: false,
pointRadius: 2.5
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
interaction: { mode: 'index', intersect: false },
plugins: { legend: { display: false } },
scales: {
x: { ticks: { maxRotation: 0 } },
y: { ticks: { callback: (v) => v } }
}
}
});
chartsById.set(asset.id, { chart, labels, data, hasLivePoint: false });
}
function upsertLivePoint(assetId, price) {
const entry = chartsById.get(assetId);
if (!entry) return;
if (!entry.hasLivePoint) {
entry.labels.push(timeLabelNow());
entry.data.push(price);
entry.hasLivePoint = true;
} else {
entry.labels[entry.labels.length - 1] = timeLabelNow();
entry.data[entry.data.length - 1] = price;
}
// Mantener puntaje razonable (histórico ~8 + live)
while (entry.labels.length > 9) {
entry.labels.shift();
entry.data.shift();
}
entry.chart.update();
}
function setLastUpdate(ts) {
const el = document.getElementById('lastUpdate');
if (!ts) { el.textContent = '-'; return; }
const d = new Date(ts * 1000);
el.textContent = d.toLocaleString();
}
async function refreshLiveOnce() {
const ids = ASSETS.map(a => a.id);
const j = await fetchSimplePrices(ids);
let newest = 0;
ASSETS.forEach(a => {
const row = j[a.id];
if (!row || typeof row.usd !== 'number') return;
hideAssetError(a.id);
upsertLivePoint(a.id, row.usd);
if (row.last_updated_at && row.last_updated_at > newest) newest = row.last_updated_at;
});
if (newest) setLastUpdate(newest);
}
function getSafeIntervalSeconds() {
const allowed = new Set([10, 20, 30, 60]);
const raw = parseInt(document.getElementById('pollSelect').value, 10);
const chosen = allowed.has(raw) ? raw : 20;
// “Más estable”: mínimo recomendado 20s
return Math.max(20, chosen);
}
function stopLive() {
if (liveTimer) clearInterval(liveTimer);
liveTimer = null;
document.getElementById('toggleLiveBtn').textContent = 'Iniciar';
}
async function startLive() {
stopLive();
try { await refreshLiveOnce(); } catch (e) { console.warn(e); }
const s = getSafeIntervalSeconds();
liveTimer = setInterval(async () => {
try { await refreshLiveOnce(); } catch (e) { console.warn(e); }
}, s * 1000);
document.getElementById('toggleLiveBtn').textContent = 'Detener';
}
// Re-inicia automáticamente si cambias intervalo mientras está corriendo
document.getElementById('pollSelect').addEventListener('change', () => {
if (liveTimer) startLive();
});
document.getElementById('refreshNowBtn').addEventListener('click', async () => {
try { await refreshLiveOnce(); } catch (e) { console.warn(e); }
});
document.getElementById('toggleLiveBtn').addEventListener('click', () => {
if (liveTimer) stopLive();
else startLive();
});
// --- Morpho estimator ---
let morphoAPY = 0.04;
document.getElementById('morphoRateDisplay').innerText = (morphoAPY * 100).toFixed(2) + '%';
function estimateCompound(amount, apy, days) {
const daily = apy / 365;
const total = amount * Math.pow(1 + daily, days);
return { total, gain: total - amount };
}
document.getElementById('calcBtn').addEventListener('click', () => {
const amt = parseFloat(document.getElementById('amount').value) || 0;
const res = estimateCompound(amt, morphoAPY, 7);
document.getElementById('gain7').innerText = res.gain.toFixed(6) + ' USDC';
document.getElementById('total7').innerText = res.total.toFixed(6) + ' USDC';
});
document.getElementById('setRateBtn').addEventListener('click', () => {
const v = prompt('Nueva tasa APY (%) — ejemplo 4 para 4%:', (morphoAPY * 100).toString());
if (v === null) return;
const n = parseFloat(v) / 100;
if (!isNaN(n) && n >= 0) {
morphoAPY = n;
document.getElementById('morphoRateDisplay').innerText = (morphoAPY * 100).toFixed(2) + '%';
document.getElementById('calcBtn').click();
} else alert('Valor inválido');
});
(async function init() {
const chartsDiv = document.getElementById('charts');
ASSETS.forEach(a => chartsDiv.appendChild(makeCard(a)));
// Cargar histórico por activo (independiente)
await Promise.allSettled(ASSETS.map(async (a) => {
try {
const series = await fetchMarketChart7d(a.id);
const labels = series.map(p => tsToDateLabel(p.t));
const data = series.map(p => Number(p.price.toFixed(6)));
createChart(a, labels, data);
} catch (e) {
showAssetError(a.id, `Error cargando histórico: ${String(e.message || e)}`);
}
}));
// Primer cálculo Morpho
document.getElementById('calcBtn').click();
// Primer refresh realtime (sin iniciar timer)
try { await refreshLiveOnce(); } catch (e) { console.warn(e); }
})();
</script>
</body>
</html>