stakex / index.html
barudakponk's picture
undefined - Initial Deployment
c85bf47 verified
<!DOCTYPE html>
<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>