Spaces:
Running
Running
<html lang="en"> | |
<head> | |
<meta charset="UTF-8" /> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> | |
<title>Crypto Stake Platform</title> | |
<!-- Tailwind CSS --> | |
<script src="https://cdn.tailwindcss.com"></script> | |
<!-- Chart.js for charts --> | |
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script> | |
<!-- Axios for API calls --> | |
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script> | |
<style> | |
.gradient-bg { background: linear-gradient(135deg, #0b1220 0%, #101c3a 100%); } | |
.card-glass { background: rgba(255,255,255,0.06); backdrop-filter: blur(12px); border: 1px solid rgba(255,255,255,0.08); } | |
.coin-card { transition: transform .25s, box-shadow .25s, background-color .25s; } | |
.coin-card:hover { transform: translateY(-6px); box-shadow: 0 20px 40px rgba(0,0,0,.35); background-color: #2c3347; } | |
.tab-content { display: none; } | |
.tab-content.active { display: block; animation: fadeIn .45s ease; } | |
.tab-button.active { background-color: #3b82f6; color: white; } | |
.btn { padding: 0.5rem 1rem; border-radius: 0.5rem; font-weight: 600; transition: all .2s ease; } | |
.btn[disabled] { opacity: .6; cursor: not-allowed; } | |
.btn-primary { background:#2563eb; color:#fff; } | |
.btn-primary:hover { background:#1d4ed8; } | |
.btn-outline { background: transparent; color:#e5e7eb; border:1px solid #4b5563; } | |
.btn-outline:hover { border-color:#9ca3af; } | |
.badge { font-size: .75rem; padding: .25rem .5rem; border-radius: 9999px; } | |
.dot { width: 8px; height: 8px; border-radius: 9999px; display:inline-block; margin-right:6px; } | |
@keyframes pulse { 0%{opacity:.4} 50%{opacity:1} 100%{opacity:.4} } | |
.dot-live { background:#22c55e; animation: pulse 2s infinite; } | |
.dot-off { background:#ef4444; } | |
@keyframes fadeIn { from {opacity:0; transform: translateY(8px);} to {opacity:1; transform: translateY(0);} } | |
</style> | |
</head> | |
<body class="gradient-bg min-h-screen text-white font-sans"> | |
<!-- Top Bar --> | |
<header class="border-b border-white/10"> | |
<div class="container mx-auto px-4 py-4 flex items-center justify-between"> | |
<div class="flex items-center gap-3"> | |
<div class="h-9 w-9 rounded-xl bg-blue-600 grid place-items-center font-black">S</div> | |
<div class="font-bold tracking-tight">StakeX</div> | |
</div> | |
<div class="flex items-center gap-5 text-sm"> | |
<div id="price-status" class="flex items-center gap-2 text-gray-300"> | |
<span id="live-dot" class="dot dot-off"></span> | |
<span id="live-text">Prices: offline</span> | |
</div> | |
<div id="user-badge" class="hidden sm:flex items-center gap-2 text-sm text-gray-300"></div> | |
<button id="logout-btn" class="hidden btn btn-outline" title="Log out">Logout</button> | |
</div> | |
</div> | |
</header> | |
<main class="container mx-auto px-4 py-8"> | |
<!-- Tabs --> | |
<div class="flex justify-center mb-10"> | |
<div class="inline-flex rounded-lg bg-gray-800/70 p-1"> | |
<button id="register-tab" class="px-6 py-2 rounded-md font-medium focus:outline-none tab-button">Register</button> | |
<button id="login-tab" class="px-6 py-2 rounded-md font-medium focus:outline-none tab-button">Login</button> | |
<button id="stake-tab" class="px-6 py-2 rounded-md font-medium focus:outline-none tab-button">Stake</button> | |
<button id="portfolio-tab" class="px-6 py-2 rounded-md font-medium focus:outline-none tab-button">Portfolio</button> | |
</div> | |
</div> | |
<!-- Sections --> | |
<section id="register-section" class="tab-content"> | |
<div class="max-w-md mx-auto card-glass rounded-2xl p-8 shadow-2xl"> | |
<h2 class="text-2xl font-bold mb-6 text-center">Create your account</h2> | |
<form id="registration-form" class="space-y-4"> | |
<div> | |
<label for="reg-email" class="block mb-2 text-sm text-gray-300">Email</label> | |
<input id="reg-email" type="email" required class="w-full px-4 py-2 rounded-lg bg-gray-800 border border-gray-700 focus:border-blue-500 focus:outline-none" /> | |
</div> | |
<div> | |
<label for="reg-password" class="block mb-2 text-sm text-gray-300">Password</label> | |
<input id="reg-password" type="password" minlength="6" required class="w-full px-4 py-2 rounded-lg bg-gray-800 border border-gray-700 focus:border-blue-500 focus:outline-none" /> | |
</div> | |
<div> | |
<label for="reg-password2" class="block mb-2 text-sm text-gray-300">Confirm Password</label> | |
<input id="reg-password2" type="password" minlength="6" required class="w-full px-4 py-2 rounded-lg bg-gray-800 border border-gray-700 focus:border-blue-500 focus:outline-none" /> | |
</div> | |
<button class="w-full btn btn-primary" type="submit">Register</button> | |
</form> | |
<div id="registration-success" class="hidden mt-4 p-3 bg-green-700/70 rounded-lg text-center" role="status" aria-live="polite">Account created! Redirecting…</div> | |
<p class="text-xs text-gray-400 mt-4">Note: This demo stores data in your browser <em>(localStorage)</em>. Do not use real passwords.</p> | |
</div> | |
</section> | |
<section id="login-section" class="tab-content"> | |
<div class="max-w-md mx-auto card-glass rounded-2xl p-8 shadow-2xl"> | |
<h2 class="text-2xl font-bold mb-6 text-center">Welcome back</h2> | |
<form id="login-form" class="space-y-4"> | |
<div> | |
<label for="login-email" class="block mb-2 text-sm text-gray-300">Email</label> | |
<input id="login-email" type="email" required class="w-full px-4 py-2 rounded-lg bg-gray-800 border border-gray-700 focus:border-blue-500 focus:outline-none" /> | |
</div> | |
<div> | |
<label for="login-password" class="block mb-2 text-sm text-gray-300">Password</label> | |
<input id="login-password" type="password" required class="w-full px-4 py-2 rounded-lg bg-gray-800 border border-gray-700 focus:border-blue-500 focus:outline-none" /> | |
</div> | |
<button class="w-full btn btn-primary" type="submit">Login</button> | |
</form> | |
<div id="login-error" class="hidden mt-4 p-3 bg-red-700/70 rounded-lg text-center" role="alert">Invalid email or password.</div> | |
</div> | |
</section> | |
<section id="stake-section" class="tab-content"> | |
<div class="max-w-5xl mx-auto"> | |
<h2 class="text-3xl font-bold mb-8 text-center">Stake your assets</h2> | |
<!-- Coin selector --> | |
<div class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-5 gap-4 mb-8"> | |
<div class="coin-card bg-gray-800 rounded-xl p-4 cursor-pointer text-center" data-coin="BTC"> | |
<img src="https://placehold.co/80x80/f7931a/ffffff?text=BTC" alt="BTC" class="mx-auto mb-2 h-12 w-12 rounded-full" /> | |
<h3 class="font-bold">Bitcoin</h3><div class="text-sm text-gray-400">BTC</div> | |
</div> | |
<div class="coin-card bg-gray-800 rounded-xl p-4 cursor-pointer text-center" data-coin="ETH"> | |
<img src="https://placehold.co/80x80/627eea/ffffff?text=ETH" alt="ETH" class="mx-auto mb-2 h-12 w-12 rounded-full" /> | |
<h3 class="font-bold">Ethereum</h3><div class="text-sm text-gray-400">ETH</div> | |
</div> | |
<div class="coin-card bg-gray-800 rounded-xl p-4 cursor-pointer text-center" data-coin="SOL"> | |
<img src="https://placehold.co/80x80/9945ff/ffffff?text=SOL" alt="SOL" class="mx-auto mb-2 h-12 w-12 rounded-full" /> | |
<h3 class="font-bold">Solana</h3><div class="text-sm text-gray-400">SOL</div> | |
</div> | |
<div class="coin-card bg-gray-800 rounded-xl p-4 cursor-pointer text-center" data-coin="USDT"> | |
<img src="https://placehold.co/80x80/26a17b/ffffff?text=USDT" alt="USDT" class="mx-auto mb-2 h-12 w-12 rounded-full" /> | |
<h3 class="font-bold">Tether</h3><div class="text-sm text-gray-400">USDT</div> | |
</div> | |
<div class="coin-card bg-gray-800 rounded-xl p-4 cursor-pointer text-center" data-coin="USDC"> | |
<img src="https://placehold.co/80x80/2775ca/ffffff?text=USDC" alt="USDC" class="mx-auto mb-2 h-12 w-12 rounded-full" /> | |
<h3 class="font-bold">USD Coin</h3><div class="text-sm text-gray-400">USDC</div> | |
</div> | |
</div> | |
<!-- Selected coin details --> | |
<div id="coin-details" class="card-glass rounded-2xl p-6 mb-8 hidden"> | |
<div class="flex flex-col md:flex-row justify-between md:items-center gap-4 mb-6"> | |
<div class="flex items-center gap-3"> | |
<img id="selected-coin-icon" src="" alt="Selected coin icon" class="h-10 w-10 rounded-full" /> | |
<div> | |
<h3 id="selected-coin-name" class="font-bold text-xl"></h3> | |
<div id="selected-coin-symbol" class="text-sm text-gray-400"></div> | |
</div> | |
</div> | |
<div class="text-left md:text-right"> | |
<div class="text-sm text-gray-400">Current APY</div> | |
<div id="selected-coin-apy" class="font-bold text-xl text-green-400"></div> | |
</div> | |
</div> | |
<div class="grid grid-cols-1 md:grid-cols-2 gap-8"> | |
<div class="h-64"> | |
<div class="flex items-center justify-between mb-2 text-sm text-gray-300"> | |
<div>Price (USD): <span id="selected-coin-price" class="font-semibold">—</span></div> | |
<div id="selected-coin-price-change" class="text-xs"></div> | |
</div> | |
<canvas id="coin-chart"></canvas> | |
</div> | |
<div> | |
<form id="stake-form" class="space-y-4"> | |
<div> | |
<label for="deposit-address" class="block mb-2 text-sm text-gray-300">Deposit Address</label> | |
<div class="flex gap-2"> | |
<input id="deposit-address" type="text" readonly class="flex-1 px-4 py-2 rounded-lg bg-gray-900 border border-gray-700 focus:outline-none cursor-text" /> | |
<button id="copy-address" class="btn btn-outline" type="button" aria-live="polite">Copy</button> | |
</div> | |
<p id="clipboard-notice" class="hidden text-xs text-amber-300 mt-2">Clipboard may be blocked in this environment. If copying fails, the address will be selected—press <strong>Ctrl+C</strong> (or <strong>Cmd+C</strong> on Mac).</p> | |
</div> | |
<div> | |
<label for="stake-amount" class="block mb-2 text-sm text-gray-300">Amount to Stake</label> | |
<input id="stake-amount" type="number" min="0.0001" step="any" required class="w-full px-4 py-2 rounded-lg bg-gray-800 border border-gray-700 focus:border-blue-500 focus:outline-none" /> | |
</div> | |
<button class="w-full btn btn-primary" type="submit">Confirm Stake</button> | |
</form> | |
</div> | |
</div> | |
</div> | |
</div> | |
</section> | |
<section id="portfolio-section" class="tab-content"> | |
<div class="max-w-6xl mx-auto"> | |
<h2 class="text-3xl font-bold mb-8 text-center">Your portfolio</h2> | |
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6 mb-8"> | |
<div class="card-glass rounded-2xl p-6"><h3 class="font-semibold text-gray-300">Total Value</h3><div id="total-value" class="text-3xl font-extrabold mt-2">$0.00</div></div> | |
<div class="card-glass rounded-2xl p-6"><h3 class="font-semibold text-gray-300">Active Stakes</h3><div id="active-stakes" class="text-3xl font-extrabold mt-2">0</div></div> | |
<div class="card-glass rounded-2xl p-6"><h3 class="font-semibold text-gray-300">Rewards Balance</h3> | |
<div class="flex items-end justify-between mt-2"> | |
<div id="total-rewards" class="text-3xl font-extrabold">$0.00</div> | |
<button id="withdraw-rewards" class="btn btn-primary" disabled>Withdraw</button> | |
</div> | |
<p class="text-xs text-gray-400 mt-2">Rewards become claimable after 7 days per stake.</p> | |
</div> | |
</div> | |
<div class="card-glass rounded-2xl p-6 mb-8"> | |
<h3 class="font-bold text-xl mb-4">Asset allocation</h3> | |
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8 items-center"> | |
<div class="h-64"><canvas id="portfolio-chart"></canvas></div> | |
<div id="assets-list" class="space-y-3"></div> | |
</div> | |
</div> | |
<div class="card-glass rounded-2xl p-6"> | |
<div class="flex items-center justify-between mb-4"> | |
<h3 class="font-bold text-xl">Staking history</h3> | |
<span class="text-xs text-gray-400">Claim button appears after 7 days</span> | |
</div> | |
<div class="overflow-x-auto"> | |
<table class="min-w-full divide-y divide-gray-700"> | |
<thead class="bg-gray-800/70"> | |
<tr> | |
<th class="px-6 py-3 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">Coin</th> | |
<th class="px-6 py-3 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">Amount</th> | |
<th class="px-6 py-3 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">Start</th> | |
<th class="px-6 py-3 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">APY</th> | |
<th class="px-6 py-3 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">Status</th> | |
<th class="px-6 py-3 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">Action</th> | |
</tr> | |
</thead> | |
<tbody id="staking-history" class="bg-gray-900 divide-y divide-gray-800"></tbody> | |
</table> | |
</div> | |
</div> | |
<!-- Developer test harness --> | |
<div class="mt-8 card-glass rounded-2xl p-6"> | |
<div class="flex items-center justify-between"> | |
<h4 class="font-semibold">Developer Tools</h4> | |
<div class="flex gap-2"> | |
<button id="run-clipboard-tests" class="btn btn-outline text-sm">Clipboard Tests</button> | |
<button id="run-price-tests" class="btn btn-outline text-sm">Price Tests</button> | |
</div> | |
</div> | |
<pre id="dev-tests" class="mt-3 text-xs text-gray-300 whitespace-pre-wrap"></pre> | |
</div> | |
</div> | |
</section> | |
</main> | |
<script> | |
// ---------------------- MOCK DATA & HELPERS ---------------------- | |
const LS_KEYS = { users: 'STAKEX_USERS', wallets: 'STAKEX_WALLETS', session: 'STAKEX_SESSION' }; | |
// Coin metadata + default fallback prices (will be overwritten by live API) | |
const coinData = { | |
BTC: { id:'bitcoin', name: 'Bitcoin', price: 37500, apy: 5.2, chart: [], color: '#f7931a' }, | |
ETH: { id:'ethereum', name: 'Ethereum', price: 2050, apy: 4.8, chart: [], color: '#627eea' }, | |
SOL: { id:'solana', name: 'Solana', price: 62, apy: 7.5, chart: [], color: '#9945ff' }, | |
USDT:{ id:'tether', name: 'Tether', price: 1, apy: 3.1, chart: [], color: '#26a17b' }, | |
USDC:{ id:'usd-coin', name: 'USD Coin', price: 1, apy: 3.2, chart: [], color: '#2775ca' } | |
}; | |
const COINGECKO_BASE = 'https://api.coingecko.com/api/v3'; | |
let coinChart, portfolioChart; | |
const $ = (sel) => document.querySelector(sel); | |
const $$ = (sel) => document.querySelectorAll(sel); | |
const load = (k, fallback) => { try { return JSON.parse(localStorage.getItem(k)) ?? fallback; } catch { return fallback; } }; | |
const save = (k, v) => localStorage.setItem(k, JSON.stringify(v)); | |
function hash(pw){ try{ return btoa(unescape(encodeURIComponent(pw))).split('').reverse().join(''); }catch{ return pw; } } | |
function fmtUSD(n){ return '$' + (Number(n)||0).toFixed(2); } | |
function daysBetween(isoStart){ const a = new Date(isoStart); const b = new Date(); return Math.floor((b - a) / (1000*60*60*24)); } | |
// ---------------------- STATE ---------------------- | |
let usersDB = load(LS_KEYS.users, {}); | |
let walletsDB = load(LS_KEYS.wallets, {}); | |
let currentUser = load(LS_KEYS.session, null); | |
let selectedCoin = null; | |
// ---------------------- INIT ---------------------- | |
document.addEventListener('DOMContentLoaded', () => { | |
setupTabs(); | |
setupAuth(); | |
setupCoins(); | |
setupStakeForm(); | |
setupPortfolio(); | |
refreshAuthUI(); | |
updateClipboardUI(); | |
// Start live price polling | |
startPriceStream(); | |
// Default visible tab | |
if(currentUser){ activateTab('stake-section'); } else { activateTab('login-section'); } | |
// Dev tests | |
$('#run-clipboard-tests').addEventListener('click', runClipboardTests); | |
$('#run-price-tests').addEventListener('click', runPriceTests); | |
}); | |
// ---------------------- LIVE PRICES ---------------------- | |
function setLiveStatus(ok){ | |
const dot = $('#live-dot'); | |
const text = $('#live-text'); | |
if(ok){ dot.classList.remove('dot-off'); dot.classList.add('dot-live'); text.textContent = `Prices: live • ${new Date().toLocaleTimeString()}`; } | |
else { dot.classList.remove('dot-live'); dot.classList.add('dot-off'); text.textContent = 'Prices: offline'; } | |
} | |
async function fetchSimplePrices(){ | |
const ids = Object.values(coinData).map(c=>c.id).join(','); | |
const url = `${COINGECKO_BASE}/simple/price?ids=${ids}&vs_currencies=usd`; | |
const { data } = await axios.get(url, { timeout: 10000 }); | |
let changed = false; | |
for(const [sym, meta] of Object.entries(coinData)){ | |
const newP = data[meta.id]?.usd; | |
if(typeof newP === 'number' && newP > 0){ | |
if(meta.price !== newP){ changed = true; } | |
meta.price = newP; | |
} | |
} | |
if(changed){ | |
// Update selected coin price label if open | |
if(selectedCoin){ $('#selected-coin-price').textContent = fmtUSD(coinData[selectedCoin].price); } | |
// Re-render portfolio (valuations) | |
renderPortfolio(); | |
} | |
setLiveStatus(true); | |
} | |
async function fetchCoinChart(sym){ | |
const meta = coinData[sym]; if(!meta) return; | |
const url = `${COINGECKO_BASE}/coins/${meta.id}/market_chart?vs_currency=usd&days=7&interval=hourly`; | |
const { data } = await axios.get(url, { timeout: 15000 }); | |
const prices = data.prices?.map(p=>p[1]) || []; | |
meta.chart = prices.slice(-100); // keep it light | |
return meta.chart; | |
} | |
function startPriceStream(){ | |
// Initial fetch soon after load, then poll every 20s | |
const tick = async ()=>{ | |
try{ await fetchSimplePrices(); } | |
catch(e){ console.warn('Price fetch failed', e); setLiveStatus(false); } | |
}; | |
tick(); | |
setInterval(tick, 20000); | |
} | |
// ---------------------- TABS ---------------------- | |
function setupTabs(){ | |
$$('#register-tab, #login-tab, #stake-tab, #portfolio-tab').forEach(btn => { | |
btn.addEventListener('click', () => { | |
const section = btn.id.replace('-tab','-section'); | |
if(!currentUser && !['register-section','login-section'].includes(section)){ | |
alert('Please login or register first.'); | |
activateTab('login-section'); | |
return; | |
} | |
activateTab(section); | |
}); | |
}); | |
} | |
function activateTab(id){ | |
$$('.tab-button').forEach(b => b.classList.remove('active')); | |
const btn = document.getElementById(id.replace('-section','-tab')); | |
if(btn) btn.classList.add('active'); | |
$$('.tab-content').forEach(s => s.classList.toggle('active', s.id === id)); | |
if(id==='portfolio-section') renderPortfolio(); | |
} | |
// ---------------------- AUTH ---------------------- | |
function setupAuth(){ | |
const regForm = $('#registration-form'); | |
regForm.addEventListener('submit', (e)=>{ | |
e.preventDefault(); | |
const email = $('#reg-email').value.trim().toLowerCase(); | |
const pw = $('#reg-password').value; | |
const pw2 = $('#reg-password2').value; | |
if(pw !== pw2){ alert('Passwords do not match'); return; } | |
if(usersDB[email]){ alert('Email already registered'); return; } | |
usersDB[email] = { email, passHash: hash(pw) }; | |
walletsDB[email] = makeFreshWallets(); | |
save(LS_KEYS.users, usersDB); | |
save(LS_KEYS.wallets, walletsDB); | |
currentUser = email; save(LS_KEYS.session, currentUser); | |
userToast('#registration-success'); | |
refreshAuthUI(); | |
setTimeout(()=> activateTab('stake-section'), 1200); | |
regForm.reset(); | |
}); | |
const loginForm = $('#login-form'); | |
loginForm.addEventListener('submit', (e)=>{ | |
e.preventDefault(); | |
const email = $('#login-email').value.trim().toLowerCase(); | |
const pw = $('#login-password').value; | |
const u = usersDB[email]; | |
if(!u || u.passHash !== hash(pw)){ | |
$('#login-error').classList.remove('hidden'); | |
setTimeout(()=> $('#login-error').classList.add('hidden'), 1500); | |
return; | |
} | |
currentUser = email; save(LS_KEYS.session, currentUser); | |
refreshAuthUI(); | |
activateTab('stake-section'); | |
loginForm.reset(); | |
}); | |
$('#logout-btn').addEventListener('click', ()=>{ | |
currentUser = null; save(LS_KEYS.session, currentUser); | |
refreshAuthUI(); | |
activateTab('login-section'); | |
}); | |
} | |
function refreshAuthUI(){ | |
const badge = $('#user-badge'); | |
const logout = $('#logout-btn'); | |
if(currentUser){ | |
badge.innerHTML = `<span class="hidden sm:inline">Signed in as</span> <span class="font-semibold">${currentUser}</span>`; | |
badge.classList.remove('hidden'); | |
logout.classList.remove('hidden'); | |
}else{ | |
badge.classList.add('hidden'); | |
logout.classList.add('hidden'); | |
} | |
} | |
function userToast(sel){ const el = document.querySelector(sel); el.classList.remove('hidden'); setTimeout(()=> el.classList.add('hidden'), 1200); } | |
function makeFreshWallets(){ | |
const mk = (prefix) => prefix + Math.random().toString(36).slice(2,10) + Math.random().toString(36).slice(2,12); | |
return { | |
BTC:{ address:'bc1q'+mk(''), balance:0, rewardsBalance:0, stakes:[] }, | |
ETH:{ address:'0x'+mk(''), balance:0, rewardsBalance:0, stakes:[] }, | |
SOL:{ address:'SoL'+mk(''), balance:0, rewardsBalance:0, stakes:[] }, | |
USDT:{ address:'0x'+mk(''), balance:0, rewardsBalance:0, stakes:[] }, | |
USDC:{ address:'0x'+mk(''), balance:0, rewardsBalance:0, stakes:[] }, | |
}; | |
} | |
// ---------------------- COIN SELECT + STAKE ---------------------- | |
function setupCoins(){ | |
$$('.coin-card').forEach(card => { | |
card.addEventListener('click', async ()=>{ | |
if(!currentUser){ alert('Login first'); return; } | |
const coin = card.dataset.coin; selectedCoin = coin; | |
await openCoin(coin, card.querySelector('img').src); | |
}); | |
}); | |
// Copy button with robust fallbacks | |
$('#copy-address').addEventListener('click', async ()=>{ | |
const addr = $('#deposit-address').value; | |
const copied = await safeCopyText(addr); | |
if(copied){ const btn = $('#copy-address'); const original = btn.textContent; btn.textContent='Copied!'; setTimeout(()=> btn.textContent = original, 1200); } | |
else { const input = $('#deposit-address'); input.focus(); input.select(); alert('Copy is blocked by this page\'s permissions. The address is selected — press Ctrl+C (Cmd+C on Mac) to copy.'); } | |
}); | |
$('#deposit-address').addEventListener('click', (e)=>{ e.target.select(); }); | |
} | |
async function openCoin(coin, iconSrc){ | |
const d = coinData[coin]; | |
const w = walletsDB[currentUser][coin]; | |
$('#selected-coin-icon').src = iconSrc; | |
$('#selected-coin-name').textContent = d.name; | |
$('#selected-coin-symbol').textContent = coin; | |
$('#selected-coin-apy').textContent = `${d.apy}%`; | |
$('#deposit-address').value = w.address; | |
// Load fresh 7d chart & set current price label | |
try{ | |
const chart = await fetchCoinChart(coin); | |
$('#selected-coin-price').textContent = fmtUSD(d.price); | |
const change = calcChangePct(chart); | |
const el = $('#selected-coin-price-change'); | |
if(change !== null){ el.textContent = `${change>0?'+':''}${change.toFixed(2)}% (7d)`; el.style.color = change>=0 ? '#34d399' : '#f87171'; } | |
if(coinChart) coinChart.destroy(); | |
coinChart = new Chart($('#coin-chart').getContext('2d'), lineCfg(chart, d.color, `${coin} Price`)); | |
}catch(e){ console.warn('Chart fetch failed', e); if(coinChart) coinChart.destroy(); coinChart = new Chart($('#coin-chart').getContext('2d'), lineCfg([d.price], d.color, `${coin} Price`)); } | |
$('#coin-details').classList.remove('hidden'); | |
updateClipboardUI(); | |
} | |
function calcChangePct(series){ if(!series || series.length < 2) return null; const first = series[0]; const last = series[series.length-1]; if(first<=0) return null; return ((last-first)/first)*100; } | |
function setupStakeForm(){ | |
const form = $('#stake-form'); | |
form.addEventListener('submit', (e)=>{ | |
e.preventDefault(); | |
const coin = $('#selected-coin-symbol').textContent.trim(); | |
const amt = parseFloat($('#stake-amount').value); | |
if(!coin || isNaN(amt) || amt<=0){ alert('Select a coin and enter a valid amount'); return; } | |
const w = walletsDB[currentUser][coin]; | |
w.balance += amt; | |
w.stakes.push({ amount: amt, start: new Date().toISOString(), apy: coinData[coin].apy, status: 'Active', claimed:false }); | |
save(LS_KEYS.wallets, walletsDB); | |
alert(`Staked ${amt} ${coin}`); | |
form.reset(); | |
renderPortfolio(); | |
}); | |
} | |
// ---------------------- PORTFOLIO ---------------------- | |
function setupPortfolio(){ | |
$('#withdraw-rewards').addEventListener('click', ()=>{ | |
const userW = walletsDB[currentUser]; | |
let total = 0; | |
Object.keys(userW).forEach(c=>{ total += userW[c].rewardsBalance; userW[c].rewardsBalance = 0; }); | |
save(LS_KEYS.wallets, walletsDB); | |
alert(`Withdrew rewards: ${fmtUSD(total)}`); | |
renderPortfolio(); | |
}); | |
} | |
function renderPortfolio(){ | |
if(!currentUser) return; | |
const userW = walletsDB[currentUser]; | |
let totalValue = 0, activeStakes = 0, rewardsUSD = 0; | |
const allocationUSD = {}; | |
const assetsEl = $('#assets-list'); | |
const histEl = $('#staking-history'); | |
assetsEl.innerHTML = ''; histEl.innerHTML = ''; | |
Object.keys(userW).forEach(coin => { | |
const w = userW[coin]; | |
const value = w.balance * (coinData[coin].price || 0); | |
totalValue += value; | |
if(w.balance>0) allocationUSD[coin] = value; | |
w.stakes.forEach(st => { | |
activeStakes++; | |
const days = daysBetween(st.start); | |
const monthly = (st.amount * (coinData[coin].price||0)) * (st.apy/100) / 12; | |
const canClaim = days >= 7 && !st.claimed; | |
const tr = document.createElement('tr'); | |
tr.innerHTML = ` | |
<td class="px-6 py-4 whitespace-nowrap"><div class="flex items-center"><img src="${document.querySelector(`.coin-card[data-coin="${coin}"] img`).src}" class="h-5 w-5 mr-2 rounded-full"/>${coin}</div></td> | |
<td class="px-6 py-4 whitespace-nowrap">${st.amount}</td> | |
<td class="px-6 py-4 whitespace-nowrap">${new Date(st.start).toISOString().slice(0,10)}</td> | |
<td class="px-6 py-4 whitespace-nowrap text-green-400">${st.apy}%</td> | |
<td class="px-6 py-4 whitespace-nowrap">${canClaim?'<span class="badge bg-yellow-500/30 border border-yellow-500/50">Matured</span>': st.claimed?'<span class="badge bg-emerald-500/30 border border-emerald-500/50">Claimed</span>':'<span class="badge bg-blue-600/30 border border-blue-600/50">Active</span>'}</td> | |
<td class="px-6 py-4 whitespace-nowrap"> | |
<button class="btn btn-outline text-xs claim-btn" ${canClaim?'':'disabled'} data-coin="${coin}">Claim ${fmtUSD(monthly)}</button> | |
</td>`; | |
histEl.appendChild(tr); | |
}); | |
if(w.balance>0){ | |
assetsEl.insertAdjacentHTML('beforeend', ` | |
<div class="p-3 rounded-lg bg-gray-800/80"> | |
<div class="flex justify-between items-center"> | |
<div class="flex items-center"> | |
<img src="${document.querySelector(`.coin-card[data-coin="${coin}"] img`).src}" class="h-8 w-8 mr-3 rounded-full"/> | |
<div class="font-bold">${coinData[coin].name}</div> | |
</div> | |
<div class="text-right"> | |
<div class="font-bold">${w.balance.toFixed(4)} ${coin}</div> | |
<div class="text-xs text-gray-400">${fmtUSD(value)}</div> | |
</div> | |
</div> | |
</div>`); | |
} | |
rewardsUSD += w.rewardsBalance || 0; | |
}); | |
$('#total-value').textContent = fmtUSD(totalValue); | |
$('#active-stakes').textContent = activeStakes; | |
$('#total-rewards').textContent = fmtUSD(rewardsUSD); | |
const withdrawBtn = $('#withdraw-rewards'); | |
withdrawBtn.disabled = rewardsUSD <= 0; | |
$$('.claim-btn').forEach(btn => btn.addEventListener('click', (e)=>{ | |
const coin = btn.getAttribute('data-coin'); | |
const w = userW[coin]; | |
const st = w.stakes.find(s => !s.claimed && daysBetween(s.start) >= 7); | |
if(!st){ alert('No matured stake to claim'); return; } | |
const monthly = (st.amount * (coinData[coin].price||0)) * (st.apy/100) / 12; | |
w.rewardsBalance = (w.rewardsBalance || 0) + monthly; | |
st.claimed = true; st.status = 'Claimed'; | |
save(LS_KEYS.wallets, walletsDB); | |
renderPortfolio(); | |
})); | |
if(portfolioChart) portfolioChart.destroy(); | |
portfolioChart = new Chart($('#portfolio-chart').getContext('2d'), donutCfg(allocationUSD)); | |
} | |
// ---------------------- CLIPBOARD ---------------------- | |
async function safeCopyText(text){ | |
try { if (window.isSecureContext && navigator.clipboard && navigator.clipboard.writeText) { await navigator.clipboard.writeText(text); return true; } } | |
catch (err) { console.warn('Clipboard API failed, falling back:', err); } | |
try { return legacyCopyUsingExecCommand(text); } catch (e) { console.warn('execCommand fallback failed:', e); return false; } | |
} | |
function legacyCopyUsingExecCommand(text){ | |
const ta = document.createElement('textarea'); ta.value = text; ta.setAttribute('readonly',''); ta.style.position='absolute'; ta.style.left='-9999px'; document.body.appendChild(ta); ta.select(); let ok=false; try { ok = document.execCommand && document.execCommand('copy'); } catch {} document.body.removeChild(ta); return !!ok; | |
} | |
function updateClipboardUI(){ const notice = document.getElementById('clipboard-notice'); if(!window.isSecureContext) notice.classList.remove('hidden'); else notice.classList.add('hidden'); } | |
// ---------------------- CHART CONFIGS ---------------------- | |
function lineCfg(data, color, label){ | |
return { type:'line', data:{ labels:Array.from({length:data.length}, (_,i)=> i===data.length-1 ? 'Now' : `${data.length-1-i}d`).reverse(), datasets:[{ label, data, borderColor: color, backgroundColor: toRGBA(color, .12), tension:.4, fill:true }] }, options:{ responsive:true, maintainAspectRatio:false, plugins:{ legend:{ display:false } }, scales:{ x:{ ticks:{ color:'#9ca3af' }, grid:{ display:false } }, y:{ ticks:{ color:'#9ca3af' }, grid:{ color:'rgba(255,255,255,.08)' } } } } }; | |
} | |
function donutCfg(alloc){ const labels = Object.keys(alloc); const data = Object.values(alloc); const colors = labels.map(l => coinData[l].color); return { type:'doughnut', data:{ labels, datasets:[{ data, backgroundColor: colors, borderColor:'#0b1220', borderWidth:2 }] }, options:{ responsive:true, maintainAspectRatio:false, plugins:{ legend:{ position:'right', labels:{ color:'#d1d5db' } } } } }; } | |
function toRGBA(hex, alpha){ let h = hex.replace('#',''); if(h.length===3){ h = h.split('').map(c=>c+c).join(''); } const r = parseInt(h.substring(0,2),16), g = parseInt(h.substring(2,4),16), b = parseInt(h.substring(4,6),16); return `rgba(${r}, ${g}, ${b}, ${alpha})`; } | |
// ---------------------- DEV TESTS ---------------------- | |
function runClipboardTests(){ | |
const out = []; | |
try { out.push(`isSecureContext: ${window.isSecureContext}`); } catch { out.push('isSecureContext: n/a'); } | |
try { const ok = legacyCopyUsingExecCommand('TEST'); out.push(`legacy execCommand callable: ${ok}`); } catch (e) { out.push(`legacy execCommand threw: ${e && e.message}`); } | |
try { const input = document.createElement('input'); input.value = 'X'; document.body.appendChild(input); input.select(); const span = (input.selectionEnd - input.selectionStart); document.body.removeChild(input); out.push(`input selectable length: ${span}`); } catch (e) { out.push(`input selectable error: ${e && e.message}`); } | |
document.getElementById('dev-tests').textContent = out.map(x=>`• ${x}`).join('\n'); | |
} | |
async function runPriceTests(){ | |
const out = []; | |
try { await fetchSimplePrices(); out.push('simple/price OK'); } catch(e){ out.push('simple/price FAILED: '+ (e && e.message)); } | |
try { await fetchCoinChart('BTC'); out.push('market_chart BTC OK'); } catch(e){ out.push('market_chart FAILED: '+ (e && e.message)); } | |
document.getElementById('dev-tests').textContent = out.map(x=>`• ${x}`).join('\n'); | |
} | |
</script> | |
<p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 🧬 <a href="https://enzostvs-deepsite.hf.space?remix=barudakponk/stakex" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body> | |
</html> |