|
|
<!DOCTYPE html> |
|
|
<html lang="en"> |
|
|
<head> |
|
|
<meta charset="UTF-8" /> |
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> |
|
|
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" /> |
|
|
<meta http-equiv="Pragma" content="no-cache" /> |
|
|
<meta http-equiv="Expires" content="0" /> |
|
|
<title>🚀 Crypto Intelligence Hub - Advanced Dashboard</title> |
|
|
|
|
|
|
|
|
<link rel="preconnect" href="https://fonts.googleapis.com"> |
|
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&family=Manrope:wght@400;500;600;700;800&family=DM+Sans:wght@400;500;700&display=swap" rel="stylesheet"> |
|
|
|
|
|
<link rel="stylesheet" href="static/css/design-tokens.css" /> |
|
|
<link rel="stylesheet" href="static/css/glassmorphism.css" /> |
|
|
<link rel="stylesheet" href="static/css/design-system.css" /> |
|
|
<link rel="stylesheet" href="static/css/dashboard.css" /> |
|
|
<link rel="stylesheet" href="static/css/pro-dashboard.css" /> |
|
|
<link rel="stylesheet" href="static/css/sentiment-modern.css" /> |
|
|
|
|
|
|
|
|
<link rel="stylesheet" href="static/css/mobile-responsive.css" media="screen" /> |
|
|
<link rel="stylesheet" href="static/css/toast.css" /> |
|
|
<link rel="stylesheet" href="static/css/accessibility.css" /> |
|
|
<link rel="stylesheet" href="static/css/navigation.css" /> |
|
|
<link rel="stylesheet" href="static/css/connection-status.css" /> |
|
|
|
|
|
|
|
|
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/chart.umd.min.js" defer></script> |
|
|
|
|
|
|
|
|
<style> |
|
|
.status-pill .status-icon { |
|
|
margin-inline: 0.35rem; |
|
|
flex-shrink: 0; |
|
|
} |
|
|
.status-pill .status-label { |
|
|
white-space: nowrap; |
|
|
} |
|
|
</style> |
|
|
</head> |
|
|
<body data-theme="dark" id="main-body"> |
|
|
|
|
|
<script> |
|
|
|
|
|
if (window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1') { |
|
|
window.BACKEND_URL = `http://${window.location.hostname}:7860`; |
|
|
} else { |
|
|
|
|
|
window.BACKEND_URL = 'https://really-amin-datasourceforcryptocurrency.hf.space'; |
|
|
} |
|
|
</script> |
|
|
|
|
|
<div class="app-shell"> |
|
|
|
|
|
<aside class="sidebar"> |
|
|
<div class="brand"> |
|
|
<div class="brand-icon"> |
|
|
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> |
|
|
<circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="2" fill="rgba(96, 165, 250, 0.15)"/> |
|
|
<path d="M12 2v4M12 18v4M4.93 4.93l2.83 2.83m8.48 8.48l2.83 2.83M2 12h4m12 0h4M4.93 19.07l2.83-2.83m8.48-8.48l2.83-2.83" stroke="currentColor" stroke-width="2" stroke-linecap="round"/> |
|
|
<circle cx="12" cy="12" r="3" fill="currentColor" opacity="0.8"/> |
|
|
<path d="M8 12h8M12 8v8" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" opacity="0.6"/> |
|
|
</svg> |
|
|
</div> |
|
|
<div class="brand-text"> |
|
|
<strong>Crypto Intelligence Hub</strong> |
|
|
<span class="env-pill"> |
|
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> |
|
|
<path d="M12 2L2 7L12 12L22 7L12 2Z" stroke="currentColor" stroke-width="2" fill="rgba(129, 140, 248, 0.2)"/> |
|
|
<path d="M2 17L12 22L22 17" stroke="currentColor" stroke-width="2"/> |
|
|
</svg> |
|
|
AI Powered |
|
|
</span> |
|
|
</div> |
|
|
</div> |
|
|
<nav class="nav"> |
|
|
<button class="nav-button active" data-nav="page-overview"> |
|
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"> |
|
|
<rect x="3" y="3" width="7" height="7" rx="1.5" fill="currentColor" opacity="0.2"/><rect x="14" y="3" width="7" height="7" rx="1.5" fill="currentColor" opacity="0.2"/> |
|
|
<rect x="3" y="14" width="7" height="7" rx="1.5" fill="currentColor" opacity="0.2"/><rect x="14" y="14" width="7" height="7" rx="1.5" fill="currentColor" opacity="0.2"/> |
|
|
</svg> |
|
|
Overview |
|
|
</button> |
|
|
<button class="nav-button" data-nav="page-market"> |
|
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"> |
|
|
<line x1="12" y1="2" x2="12" y2="22" stroke-width="3"/> |
|
|
<path d="M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6" stroke-width="2.5"/> |
|
|
</svg> |
|
|
Market |
|
|
</button> |
|
|
<button class="nav-button" data-nav="page-chart"> |
|
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"> |
|
|
<path d="M3 3v18h18" stroke-width="3"/> |
|
|
<path d="M7 12l4-4 4 4 6-6v8H7z" stroke-width="2.5"/> |
|
|
</svg> |
|
|
Chart Lab |
|
|
</button> |
|
|
<button class="nav-button" data-nav="page-ai"> |
|
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"> |
|
|
<path d="M12 2v4M12 18v4M4.93 4.93l2.83 2.83m8.48 8.48l2.83 2.83M2 12h4m12 0h4M4.93 19.07l2.83-2.83m8.48-8.48l2.83-2.83" stroke-width="2.5"/> |
|
|
<circle cx="12" cy="12" r="3.5" fill="currentColor" opacity="0.3"/> |
|
|
</svg> |
|
|
AI Advisor |
|
|
</button> |
|
|
<button class="nav-button" data-nav="page-news"> |
|
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"> |
|
|
<path d="M4 22h16a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2H8a2 2 0 0 0-2 2v16a2 2 0 0 1-2 2Zm0 0a2 2 0 0 1-2-2v-9c0-1.1.9-2 2-2h2" stroke-width="2.5"/> |
|
|
<path d="M18 14h-8M15 10h-5M19 18h-3" stroke-width="2.5"/> |
|
|
</svg> |
|
|
News |
|
|
</button> |
|
|
<button class="nav-button" data-nav="page-providers"> |
|
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"> |
|
|
<path d="M12 2L2 7l10 5 10-5-10-5z" fill="currentColor" opacity="0.2"/><path d="M2 17l10 5 10-5" fill="currentColor" opacity="0.2"/><path d="M2 12l10 5 10-5" stroke-width="2.5"/> |
|
|
</svg> |
|
|
Providers |
|
|
</button> |
|
|
<button class="nav-button" data-nav="page-datasets"> |
|
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"> |
|
|
<rect x="3" y="3" width="18" height="18" rx="2" ry="2" fill="currentColor" opacity="0.1"/> |
|
|
<line x1="9" y1="3" x2="9" y2="21" stroke-width="3"/> |
|
|
<line x1="3" y1="9" x2="21" y2="9" stroke-width="3"/> |
|
|
</svg> |
|
|
Datasets & Models |
|
|
</button> |
|
|
<button class="nav-button" data-nav="page-api"> |
|
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"> |
|
|
<path d="M4 9h16M4 15h16" stroke-width="3"/> |
|
|
<path d="M12 3v18" stroke-width="3"/> |
|
|
<circle cx="12" cy="12" r="2.5" fill="currentColor" opacity="0.3"/> |
|
|
</svg> |
|
|
API Explorer |
|
|
</button> |
|
|
<button class="nav-button" data-nav="page-debug"> |
|
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"> |
|
|
<circle cx="12" cy="12" r="10" fill="currentColor" opacity="0.1"/> |
|
|
<line x1="12" y1="8" x2="12" y2="12" stroke-width="3"/> |
|
|
<line x1="12" y1="16" x2="12.01" y2="16" stroke-width="3"/> |
|
|
</svg> |
|
|
Diagnostics |
|
|
</button> |
|
|
<button class="nav-button" data-nav="page-settings"> |
|
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"> |
|
|
<circle cx="12" cy="12" r="3.5" fill="currentColor" opacity="0.2"/> |
|
|
<path d="M12 1v6m0 6v6M5.64 5.64l4.24 4.24m4.24 4.24l4.24 4.24M1 12h6m6 0h6M5.64 18.36l4.24-4.24m4.24-4.24l4.24-4.24" stroke-width="2.5"/> |
|
|
</svg> |
|
|
Settings |
|
|
</button> |
|
|
</nav> |
|
|
<div class="sidebar-footer"> |
|
|
<div class="footer-badge"> |
|
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> |
|
|
<path d="M12 2L2 7L12 12L22 7L12 2Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> |
|
|
<path d="M2 17L12 22L22 17" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> |
|
|
</svg> |
|
|
<span>AI Powered</span> |
|
|
</div> |
|
|
</div> |
|
|
</aside> |
|
|
|
|
|
|
|
|
<main class="main-area"> |
|
|
|
|
|
<header class="topbar"> |
|
|
<div class="topbar-content"> |
|
|
<div class="topbar-icon"> |
|
|
<svg width="40" height="40" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> |
|
|
<circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="2" fill="rgba(96, 165, 250, 0.1)"/> |
|
|
<path d="M12 2v4M12 18v4M4.93 4.93l2.83 2.83m8.48 8.48l2.83 2.83M2 12h4m12 0h4M4.93 19.07l2.83-2.83m8.48-8.48l2.83-2.83" stroke="currentColor" stroke-width="2" stroke-linecap="round"/> |
|
|
<circle cx="12" cy="12" r="3" fill="currentColor" opacity="0.8"/> |
|
|
<path d="M8 12h8M12 8v8" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" opacity="0.6"/> |
|
|
</svg> |
|
|
</div> |
|
|
<div class="topbar-text"> |
|
|
<h1> |
|
|
<span class="title-gradient">Crypto Intelligence</span> |
|
|
<span class="title-accent">Dashboard</span> |
|
|
</h1> |
|
|
<p class="text-muted"> |
|
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" style="display: inline-block; vertical-align: middle; margin-right: 6px;"> |
|
|
<circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="2"/> |
|
|
<path d="M12 8v4l3 3" stroke="currentColor" stroke-width="2" stroke-linecap="round"/> |
|
|
</svg> |
|
|
Live market data, AI-powered sentiment analysis, and comprehensive crypto intelligence |
|
|
</p> |
|
|
</div> |
|
|
</div> |
|
|
<div class="status-group"> |
|
|
|
|
|
<div class="status-pill" data-api-health data-state="warn"> |
|
|
<span class="status-dot"></span> |
|
|
<svg class="status-icon" width="14" height="14" viewBox="0 0 24 24" aria-hidden="true"> |
|
|
<path d="M4 7h16M4 12h10M4 17h7" |
|
|
stroke="currentColor" |
|
|
stroke-width="2" |
|
|
stroke-linecap="round" |
|
|
stroke-linejoin="round" /> |
|
|
</svg> |
|
|
<span class="status-label">checking</span> |
|
|
</div> |
|
|
|
|
|
<div class="status-pill" data-ws-status data-state="warn"> |
|
|
<span class="status-dot"></span> |
|
|
<svg class="status-icon" width="14" height="14" viewBox="0 0 24 24" aria-hidden="true"> |
|
|
<path d="M4 5h16v6H4z" |
|
|
stroke="currentColor" |
|
|
stroke-width="2" |
|
|
fill="none" |
|
|
stroke-linejoin="round" /> |
|
|
<path d="M8 15h8M10 19h4" |
|
|
stroke="currentColor" |
|
|
stroke-width="2" |
|
|
stroke-linecap="round" /> |
|
|
</svg> |
|
|
<span class="status-label">connecting</span> |
|
|
</div> |
|
|
</div> |
|
|
</header> |
|
|
|
|
|
<div class="page-container"> |
|
|
|
|
|
<section id="page-overview" class="page active"> |
|
|
<div class="section-header"> |
|
|
<h2 class="section-title">📊 Market Overview</h2> |
|
|
<div style="display: flex; gap: 12px; align-items: center;"> |
|
|
<span class="chip" style="background: var(--success-glow); color: var(--success-light);">Live Data</span> |
|
|
<span class="chip" style="background: var(--primary-glow); color: var(--primary-light);">Real-time</span> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div class="grid-four" data-overview-stats></div> |
|
|
|
|
|
<div class="glass-card" style="margin-top:1.5rem;"> |
|
|
<div class="card-header"> |
|
|
<h4>Market Overview - 24H</h4> |
|
|
<button class="btn-secondary btn-sm" id="refresh-market-btn" onclick="refreshAllData()"> |
|
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none"> |
|
|
<path d="M1 4v6h6M23 20v-6h-6" stroke="currentColor" stroke-width="2"/> |
|
|
<path d="M20.49 9A9 9 0 005.64 5.64L1 10m22 4l-4.64 4.36A9 9 0 013.51 15" stroke="currentColor" stroke-width="2"/> |
|
|
</svg> |
|
|
<span id="refresh-text">Refresh</span> |
|
|
</button> |
|
|
</div> |
|
|
<div style="height: 400px; padding: 20px;"> |
|
|
<canvas id="market-overview-chart"></canvas> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div class="glass-card" style="margin-top:1.5rem;"> |
|
|
<div class="card-header"> |
|
|
<h4>Top Cryptocurrencies</h4> |
|
|
</div> |
|
|
<div class="table-container"> |
|
|
<table class="table"> |
|
|
<thead> |
|
|
<tr> |
|
|
<th>#</th> |
|
|
<th>Coin</th> |
|
|
<th>Price</th> |
|
|
<th>24h %</th> |
|
|
<th>7d %</th> |
|
|
<th>Market Cap</th> |
|
|
<th>Volume</th> |
|
|
<th>Chart</th> |
|
|
</tr> |
|
|
</thead> |
|
|
<tbody data-top-coins-body></tbody> |
|
|
</table> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div class="glass-card" style="margin-top:1.5rem;"> |
|
|
<h4>Global Sentiment</h4> |
|
|
<canvas id="sentiment-chart" height="200"></canvas> |
|
|
</div> |
|
|
</section> |
|
|
|
|
|
|
|
|
<section id="page-market" class="page"> |
|
|
<div class="section-header"> |
|
|
<h2 class="section-title">💹 Market Explorer</h2> |
|
|
<div style="display: flex; gap: 12px; align-items: center;"> |
|
|
<span class="chip" style="background: var(--primary-glow); color: var(--primary-light);">50+ Coins</span> |
|
|
<span class="chip" style="background: var(--secondary-glow); color: var(--secondary-light);">24/7 Updates</span> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div class="search-bar"> |
|
|
<input type="text" placeholder="Search coins..." data-market-search id="market-search" name="market-search" /> |
|
|
<div class="button-group"> |
|
|
<button class="secondary active" data-timeframe="24h">24h</button> |
|
|
<button class="secondary" data-timeframe="7d">7d</button> |
|
|
<button class="secondary" data-timeframe="30d">30d</button> |
|
|
</div> |
|
|
<label class="input-chip">Live Updates |
|
|
<div class="toggle"> |
|
|
<input type="checkbox" data-live-toggle id="live-toggle" name="live-toggle" /> |
|
|
<span></span> |
|
|
</div> |
|
|
</label> |
|
|
</div> |
|
|
|
|
|
<div class="table-container"> |
|
|
<table> |
|
|
<thead> |
|
|
<tr> |
|
|
<th>#</th> |
|
|
<th>Symbol</th> |
|
|
<th>Name</th> |
|
|
<th>Price</th> |
|
|
<th>24h %</th> |
|
|
<th>Volume</th> |
|
|
<th>Market Cap</th> |
|
|
</tr> |
|
|
</thead> |
|
|
<tbody data-market-body></tbody> |
|
|
</table> |
|
|
</div> |
|
|
|
|
|
<aside class="drawer" data-market-drawer> |
|
|
<header> |
|
|
<h3 data-drawer-symbol></h3> |
|
|
<button data-close-drawer>×</button> |
|
|
</header> |
|
|
<div class="drawer-body"> |
|
|
<div data-drawer-stats></div> |
|
|
<div data-chart-wrapper style="margin:1rem 0;"> |
|
|
<canvas id="market-detail-chart" height="180"></canvas> |
|
|
</div> |
|
|
<h4>Related Headlines</h4> |
|
|
<div data-drawer-news></div> |
|
|
</div> |
|
|
</aside> |
|
|
</section> |
|
|
|
|
|
|
|
|
<section id="page-chart" class="page"> |
|
|
<div class="section-header"> |
|
|
<h2 class="section-title">📈 Chart Lab</h2> |
|
|
<div style="display: flex; gap: 12px; align-items: center;"> |
|
|
<span class="chip" style="background: var(--primary-glow); color: var(--primary-light);">TradingView Style</span> |
|
|
<span class="chip" style="background: var(--secondary-glow); color: var(--secondary-light);">Professional</span> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div class="glass-card"> |
|
|
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 20px; margin-bottom: 20px;"> |
|
|
<div> |
|
|
<label style="display: block; margin-bottom: 8px; font-weight: 600; color: var(--text-normal);">Select Cryptocurrency</label> |
|
|
<div style="position: relative;"> |
|
|
<input |
|
|
type="text" |
|
|
id="chartCoinSearch" |
|
|
placeholder="Search Bitcoin, Ethereum..." |
|
|
style="width: 100%; padding: 12px 40px 12px 16px; background: rgba(15, 23, 42, 0.6); border: 1px solid rgba(255, 255, 255, 0.2); border-radius: 10px; color: white; font-size: 14px;" |
|
|
autocomplete="off" |
|
|
/> |
|
|
<svg style="position: absolute; right: 12px; top: 50%; transform: translateY(-50%); pointer-events: none; color: #94A3B8;" width="16" height="16" viewBox="0 0 24 24" fill="none"> |
|
|
<path d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" stroke="currentColor" stroke-width="2"/> |
|
|
</svg> |
|
|
<div id="chartCoinDropdown" style="display: none; position: absolute; top: calc(100% + 8px); left: 0; right: 0; max-height: 300px; overflow-y: auto; background: rgba(17, 24, 39, 0.95); border: 1px solid rgba(255, 255, 255, 0.2); border-radius: 12px; backdrop-filter: blur(20px); box-shadow: 0 20px 60px rgba(0, 0, 0, 0.6); z-index: 1000;"></div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div> |
|
|
<label style="display: block; margin-bottom: 8px; font-weight: 600; color: var(--text-normal);">Timeframe</label> |
|
|
<div style="display: flex; gap: 8px;"> |
|
|
<button class="secondary" data-chart-timeframe="1">1D</button> |
|
|
<button class="secondary active" data-chart-timeframe="7">7D</button> |
|
|
<button class="secondary" data-chart-timeframe="30">30D</button> |
|
|
<button class="secondary" data-chart-timeframe="90">90D</button> |
|
|
<button class="secondary" data-chart-timeframe="365">1Y</button> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div> |
|
|
<label style="display: block; margin-bottom: 8px; font-weight: 600; color: var(--text-normal);">Chart Type</label> |
|
|
<select id="chartType" style="width: 100%; padding: 12px 16px; background: rgba(15, 23, 42, 0.6); border: 1px solid rgba(255, 255, 255, 0.2); border-radius: 10px; color: white; font-size: 14px;"> |
|
|
<option value="line">Line Chart</option> |
|
|
<option value="area">Area Chart</option> |
|
|
<option value="bar">Bar Chart</option> |
|
|
</select> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<button class="primary" onclick="loadSelectedChart()" style="width: 100%;"> |
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none"> |
|
|
<path d="M3 3v18h18" stroke="currentColor" stroke-width="2"/> |
|
|
<path d="M7 10l4-4 4 4 6-6" stroke="currentColor" stroke-width="2"/> |
|
|
</svg> |
|
|
Load Chart |
|
|
</button> |
|
|
</div> |
|
|
|
|
|
<div class="glass-card" style="margin-top: 20px;"> |
|
|
<div class="card-header"> |
|
|
<h4 id="selectedCoinTitle">Select a coin to view chart</h4> |
|
|
<div style="display: flex; gap: 8px;"> |
|
|
<span class="badge badge-cyan" id="selectedCoinPrice">$0</span> |
|
|
<span class="badge badge-success" id="selectedCoinChange">0%</span> |
|
|
</div> |
|
|
</div> |
|
|
<div style="height: 500px; padding: 20px;"> |
|
|
<canvas id="price-chart"></canvas> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div class="glass-card" style="margin-top: 20px;"> |
|
|
<div class="card-header"> |
|
|
<h4>Volume Analysis</h4> |
|
|
</div> |
|
|
<div style="height: 300px; padding: 20px;"> |
|
|
<canvas id="volume-chart"></canvas> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div class="grid-two" style="margin-top: 20px;"> |
|
|
<div class="glass-card"> |
|
|
<div class="card-header"> |
|
|
<h4>RSI Indicator</h4> |
|
|
</div> |
|
|
<div style="height: 250px; padding: 20px;"> |
|
|
<canvas id="rsi-chart"></canvas> |
|
|
</div> |
|
|
</div> |
|
|
<div class="glass-card"> |
|
|
<div class="card-header"> |
|
|
<h4>Moving Averages</h4> |
|
|
</div> |
|
|
<div style="height: 250px; padding: 20px;"> |
|
|
<canvas id="ma-chart"></canvas> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</section> |
|
|
|
|
|
|
|
|
<section id="page-ai" class="page"> |
|
|
<div class="section-header"> |
|
|
<h2 class="section-title">🤖 AI Advisor & Sentiment Analysis</h2> |
|
|
<div style="display: flex; gap: 12px; align-items: center;"> |
|
|
<span class="chip" style="background: var(--primary-glow); color: var(--primary-light);">HF Models</span> |
|
|
<span class="chip" style="background: var(--success-glow); color: var(--success-light);">Ensemble AI</span> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div class="grid-two" style="margin-bottom: 24px;"> |
|
|
<div class="glass-card"> |
|
|
<div class="card-header"> |
|
|
<h4>💬 Natural Language Query</h4> |
|
|
</div> |
|
|
<form data-query-form style="display: flex; flex-direction: column; gap: 16px;"> |
|
|
<label style="display: flex; flex-direction: column; gap: 8px;"> |
|
|
<span style="font-weight: 600; color: var(--text-secondary);">Ask anything about crypto markets</span> |
|
|
<input type="text" placeholder="e.g., What is the current Bitcoin price? What are the top 5 coins by market cap?" name="query" style="padding: 14px 18px; border-radius: 12px; background: var(--glass-bg-light); border: 1px solid var(--glass-border); color: var(--text-primary); font-size: 0.9375rem;" /> |
|
|
</label> |
|
|
<button class="btn-secondary" type="submit" style="padding: 14px 24px; font-weight: 600; display: flex; align-items: center; justify-content: center; gap: 8px;"> |
|
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"> |
|
|
<path d="M22 2L11 13M22 2l-7 20-4-9-9-4 20-7z"/> |
|
|
</svg> |
|
|
Ask AI |
|
|
</button> |
|
|
</form> |
|
|
<div data-query-output style="margin-top: 20px; padding: 20px; background: var(--glass-bg-light); border-radius: 12px; border: 1px solid var(--glass-border); min-height: 60px; color: var(--text-secondary);"></div> |
|
|
</div> |
|
|
|
|
|
<div class="glass-card"> |
|
|
<div class="card-header"> |
|
|
<h4>📊 Sentiment Analyzer</h4> |
|
|
<span class="chip" style="font-size: 0.75rem;">Ensemble AI Models</span> |
|
|
</div> |
|
|
<form data-sentiment-form style="display: flex; flex-direction: column; gap: 16px;"> |
|
|
<label style="display: flex; flex-direction: column; gap: 8px;"> |
|
|
<span style="font-weight: 600; color: var(--text-secondary);">Enter text for sentiment analysis</span> |
|
|
<textarea name="text" rows="5" placeholder="e.g., Bitcoin is showing strong bullish momentum with increasing adoption..." style="padding: 14px 18px; border-radius: 12px; background: var(--glass-bg-light); border: 1px solid var(--glass-border); color: var(--text-primary); font-size: 0.9375rem; resize: vertical; font-family: inherit;"></textarea> |
|
|
</label> |
|
|
<button class="btn-secondary" type="submit" style="padding: 14px 24px; font-weight: 600; display: flex; align-items: center; justify-content: center; gap: 8px;"> |
|
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"> |
|
|
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/> |
|
|
</svg> |
|
|
Analyze Sentiment |
|
|
</button> |
|
|
</form> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
|
|
|
<div id="sentiment-results" class="glass-card" style="display: none; margin-top: 24px;"> |
|
|
<div class="card-header"> |
|
|
<h4>📈 Analysis Results</h4> |
|
|
</div> |
|
|
<div data-sentiment-output style="padding: 24px;"></div> |
|
|
</div> |
|
|
</section> |
|
|
|
|
|
|
|
|
<section id="page-news" class="page"> |
|
|
<div class="section-header"> |
|
|
<h2 class="section-title">📰 News Feed</h2> |
|
|
<div style="display: flex; gap: 12px; align-items: center;"> |
|
|
<span class="chip" id="news-count">Loading...</span> |
|
|
<button class="btn-secondary" id="refresh-news" style="padding: 8px 16px; font-size: 0.875rem;"> |
|
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" style="margin-right: 6px;"> |
|
|
<path d="M1 4v6h6M23 20v-6h-6" stroke="currentColor" stroke-width="2" stroke-linecap="round"/> |
|
|
<path d="M3.51 9a9 9 0 0114.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0020.49 15" stroke="currentColor" stroke-width="2" stroke-linecap="round"/> |
|
|
</svg> |
|
|
Refresh |
|
|
</button> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div class="glass-card" style="margin-bottom: 24px;"> |
|
|
<div class="search-bar" style="display: flex; gap: 12px; flex-wrap: wrap;"> |
|
|
<input type="text" placeholder="🔍 Search headlines..." data-news-search id="news-search" name="news-search" style="flex: 1; min-width: 200px;" /> |
|
|
<select data-news-range style="padding: 10px 14px; border-radius: 10px; background: var(--glass-bg); border: 1px solid var(--glass-border); color: var(--text-secondary);"> |
|
|
<option value="24h">Last 24h</option> |
|
|
<option value="7d">Last 7d</option> |
|
|
<option value="30d">Last 30d</option> |
|
|
<option value="all">All Time</option> |
|
|
</select> |
|
|
<input type="text" placeholder="Symbol (BTC, ETH...)" data-news-symbol id="news-symbol" name="news-symbol" style="width: 150px;" /> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div id="news-loading" style="text-align: center; padding: 60px; color: var(--text-muted); display: none;"> |
|
|
<div style="font-size: 3rem; margin-bottom: 16px;">📰</div> |
|
|
<div>Loading news...</div> |
|
|
</div> |
|
|
|
|
|
<div id="news-error" style="display: none; padding: 40px; text-align: center; color: var(--danger);"></div> |
|
|
|
|
|
<div id="news-grid" data-news-container style="display: grid; grid-template-columns: repeat(auto-fill, minmax(380px, 1fr)); gap: 20px;"></div> |
|
|
|
|
|
<div class="modal-backdrop" data-news-modal style="display: none;"> |
|
|
<div class="modal glass-card" data-news-modal-content style="max-width: 800px; max-height: 90vh; overflow-y: auto;"></div> |
|
|
<button data-close-news-modal style="position: absolute; top: 20px; right: 20px; background: var(--glass-bg-strong); border: 1px solid var(--glass-border); color: var(--text-primary); width: 40px; height: 40px; border-radius: 50%; font-size: 24px; cursor: pointer; display: flex; align-items: center; justify-content: center;">×</button> |
|
|
</div> |
|
|
</section> |
|
|
|
|
|
|
|
|
<section id="page-providers" class="page"> |
|
|
<div class="section-header"> |
|
|
<h2 class="section-title">API Providers</h2> |
|
|
<span class="chip">Multi-source</span> |
|
|
</div> |
|
|
|
|
|
<div data-providers-grid class="grid-three"></div> |
|
|
</section> |
|
|
|
|
|
|
|
|
<section id="page-datasets" class="page"> |
|
|
<div class="section-header"> |
|
|
<h2 class="section-title">Datasets & Models</h2> |
|
|
<span class="chip">14+ datasets</span> |
|
|
</div> |
|
|
|
|
|
<div class="glass-card"> |
|
|
<h4>Datasets</h4> |
|
|
<div class="table-container"> |
|
|
<table> |
|
|
<thead> |
|
|
<tr> |
|
|
<th>Name</th> |
|
|
<th>Type</th> |
|
|
<th>Updated</th> |
|
|
<th>Actions</th> |
|
|
</tr> |
|
|
</thead> |
|
|
<tbody data-datasets-body></tbody> |
|
|
</table> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div class="glass-card" style="margin-top:1.5rem;"> |
|
|
<h4>HF Models</h4> |
|
|
<div class="table-container"> |
|
|
<table> |
|
|
<thead> |
|
|
<tr> |
|
|
<th>Name</th> |
|
|
<th>Task</th> |
|
|
<th>Status</th> |
|
|
<th>Description</th> |
|
|
</tr> |
|
|
</thead> |
|
|
<tbody data-models-body></tbody> |
|
|
</table> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div class="glass-card" style="margin-top:1.5rem;"> |
|
|
<h4>Test Model</h4> |
|
|
<form data-model-test-form> |
|
|
<div class="grid-two"> |
|
|
<label>Model |
|
|
<select name="model" data-model-select></select> |
|
|
</label> |
|
|
<label>Input Text |
|
|
<textarea name="input" rows="3" placeholder="Enter text to test the model..."></textarea> |
|
|
</label> |
|
|
</div> |
|
|
<button class="primary" type="submit">Run Test</button> |
|
|
</form> |
|
|
<div data-model-test-output style="margin-top:1rem;"></div> |
|
|
</div> |
|
|
|
|
|
<div class="modal-backdrop" data-dataset-modal> |
|
|
<div class="modal" data-dataset-modal-content></div> |
|
|
<button data-close-dataset-modal>×</button> |
|
|
</div> |
|
|
</section> |
|
|
|
|
|
|
|
|
<section id="page-api" class="page"> |
|
|
<div class="section-header"> |
|
|
<h2 class="section-title">API Explorer</h2> |
|
|
<span class="chip">15+ endpoints</span> |
|
|
</div> |
|
|
|
|
|
<div class="glass-card"> |
|
|
<h4>Test Endpoint</h4> |
|
|
<form data-api-form> |
|
|
<div class="grid-two"> |
|
|
<label>Endpoint |
|
|
<select data-endpoint-select> |
|
|
<option value="0">/api/health</option> |
|
|
</select> |
|
|
</label> |
|
|
<label>Method |
|
|
<select data-method-select> |
|
|
<option value="GET">GET</option> |
|
|
<option value="POST">POST</option> |
|
|
</select> |
|
|
</label> |
|
|
</div> |
|
|
<div data-api-description style="margin:0.5rem 0;font-size:0.875rem;color:var(--text-secondary);"></div> |
|
|
<div data-api-path style="margin:0.5rem 0;font-family:monospace;font-size:0.875rem;"></div> |
|
|
<label>Body (JSON) |
|
|
<textarea data-body-input rows="4"></textarea> |
|
|
</label> |
|
|
<button class="primary" type="submit">Send Request</button> |
|
|
</form> |
|
|
<div data-api-response style="margin-top:1rem;"></div> |
|
|
</div> |
|
|
</section> |
|
|
|
|
|
|
|
|
<section id="page-debug" class="page"> |
|
|
<div class="section-header"> |
|
|
<h2 class="section-title">System Diagnostics</h2> |
|
|
</div> |
|
|
|
|
|
<div class="grid-two"> |
|
|
<div class="glass-card"> |
|
|
<h4>Health Status</h4> |
|
|
<div data-health-info>Checking...</div> |
|
|
</div> |
|
|
|
|
|
<div class="glass-card"> |
|
|
<h4>WebSocket Status</h4> |
|
|
<div data-ws-info>Checking...</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div class="grid-two" style="margin-top:1.5rem;"> |
|
|
<div class="glass-card"> |
|
|
<h4>Request Logs</h4> |
|
|
<div class="table-container"> |
|
|
<table> |
|
|
<thead> |
|
|
<tr> |
|
|
<th>Time</th> |
|
|
<th>Method</th> |
|
|
<th>Endpoint</th> |
|
|
<th>Status</th> |
|
|
<th>Duration</th> |
|
|
</tr> |
|
|
</thead> |
|
|
<tbody data-request-log></tbody> |
|
|
</table> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div class="glass-card"> |
|
|
<h4>Error Logs</h4> |
|
|
<div class="table-container"> |
|
|
<table> |
|
|
<thead> |
|
|
<tr> |
|
|
<th>Time</th> |
|
|
<th>Endpoint</th> |
|
|
<th>Message</th> |
|
|
</tr> |
|
|
</thead> |
|
|
<tbody data-error-log></tbody> |
|
|
</table> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div class="glass-card" style="margin-top:1.5rem;"> |
|
|
<h4>WebSocket Events</h4> |
|
|
<div class="table-container"> |
|
|
<table> |
|
|
<thead> |
|
|
<tr> |
|
|
<th>Time</th> |
|
|
<th>Type</th> |
|
|
<th>Details</th> |
|
|
</tr> |
|
|
</thead> |
|
|
<tbody data-ws-log></tbody> |
|
|
</table> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<button class="secondary" data-refresh-health style="margin-top:1.5rem;">Refresh</button> |
|
|
</section> |
|
|
|
|
|
|
|
|
<section id="page-settings" class="page"> |
|
|
<div class="section-header"> |
|
|
<h2 class="section-title">Settings</h2> |
|
|
</div> |
|
|
|
|
|
<div class="glass-card"> |
|
|
<h4>Display Settings</h4> |
|
|
<div class="grid-two"> |
|
|
<label class="input-chip">Theme |
|
|
<div class="toggle"> |
|
|
<input type="checkbox" data-theme-toggle id="theme-toggle" name="theme-toggle" /> |
|
|
<span></span> |
|
|
</div> |
|
|
<span style="font-size: 0.875rem; color: var(--text-muted); margin-left: 8px;">Light / Dark</span> |
|
|
</label> |
|
|
<label class="input-chip">Compact Layout |
|
|
<div class="toggle"> |
|
|
<input type="checkbox" data-layout-toggle id="layout-toggle" name="layout-toggle" /> |
|
|
<span></span> |
|
|
</div> |
|
|
</label> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div class="glass-card" style="margin-top:1.5rem;"> |
|
|
<h4>Refresh Intervals</h4> |
|
|
<div class="grid-two"> |
|
|
<label>Market Data (seconds) |
|
|
<input type="number" min="10" step="5" value="30" data-market-interval id="market-interval" name="market-interval" /> |
|
|
</label> |
|
|
<label>News Feed (seconds) |
|
|
<input type="number" min="30" step="10" value="60" data-news-interval id="news-interval" name="news-interval" /> |
|
|
</label> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div class="inline-message inline-info" style="margin-top:1.5rem;"> |
|
|
Settings are stored locally in your browser. |
|
|
</div> |
|
|
</section> |
|
|
</div> |
|
|
</main> |
|
|
</div> |
|
|
|
|
|
|
|
|
<script> |
|
|
let chartInstances = {}; |
|
|
let allCoins = []; |
|
|
let selectedCoin = null; |
|
|
let selectedTimeframe = 7; |
|
|
|
|
|
|
|
|
function initNavigation() { |
|
|
const navButtons = document.querySelectorAll('.nav-button'); |
|
|
const pages = document.querySelectorAll('.page'); |
|
|
const topbarIcon = document.querySelector('.topbar-icon'); |
|
|
|
|
|
|
|
|
const pageIcons = { |
|
|
'page-overview': `<svg width="40" height="40" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> |
|
|
<circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="2" fill="rgba(96, 165, 250, 0.1)"/> |
|
|
<rect x="3" y="3" width="7" height="7" rx="1.5" fill="currentColor" opacity="0.3"/><rect x="14" y="3" width="7" height="7" rx="1.5" fill="currentColor" opacity="0.3"/> |
|
|
<rect x="3" y="14" width="7" height="7" rx="1.5" fill="currentColor" opacity="0.3"/><rect x="14" y="14" width="7" height="7" rx="1.5" fill="currentColor" opacity="0.3"/> |
|
|
</svg>`, |
|
|
'page-market': `<svg width="40" height="40" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> |
|
|
<circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="2" fill="rgba(167, 139, 250, 0.1)"/> |
|
|
<path d="M12 2v20M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"/> |
|
|
</svg>`, |
|
|
'page-chart': `<svg width="40" height="40" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> |
|
|
<circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="2" fill="rgba(244, 114, 182, 0.1)"/> |
|
|
<path d="M3 3v18h18" stroke="currentColor" stroke-width="2.5"/><path d="M7 12l4-4 4 4 6-6v8H7z" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/> |
|
|
</svg>`, |
|
|
'page-ai': `<svg width="40" height="40" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> |
|
|
<circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="2" fill="rgba(52, 211, 153, 0.1)"/> |
|
|
<path d="M12 2v4M12 18v4M4.93 4.93l2.83 2.83m8.48 8.48l2.83 2.83M2 12h4m12 0h4M4.93 19.07l2.83-2.83m8.48-8.48l2.83-2.83" stroke="currentColor" stroke-width="2" stroke-linecap="round"/> |
|
|
<circle cx="12" cy="12" r="3.5" fill="currentColor" opacity="0.4"/> |
|
|
</svg>`, |
|
|
'page-news': `<svg width="40" height="40" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> |
|
|
<circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="2" fill="rgba(251, 191, 36, 0.1)"/> |
|
|
<path d="M4 22h16a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2H8a2 2 0 0 0-2 2v16a2 2 0 0 1-2 2Zm0 0a2 2 0 0 1-2-2v-9c0-1.1.9-2 2-2h2" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"/> |
|
|
<path d="M18 14h-8M15 10h-5M19 18h-3" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"/> |
|
|
</svg>`, |
|
|
'page-providers': `<svg width="40" height="40" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> |
|
|
<circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="2" fill="rgba(34, 211, 238, 0.1)"/> |
|
|
<path d="M12 2L2 7l10 5 10-5-10-5z" fill="currentColor" opacity="0.3"/><path d="M2 17l10 5 10-5" fill="currentColor" opacity="0.3"/><path d="M2 12l10 5 10-5" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"/> |
|
|
</svg>`, |
|
|
'page-datasets': `<svg width="40" height="40" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> |
|
|
<circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="2" fill="rgba(192, 132, 252, 0.1)"/> |
|
|
<rect x="3" y="3" width="18" height="18" rx="2" ry="2" fill="currentColor" opacity="0.15"/> |
|
|
<line x1="9" y1="3" x2="9" y2="21" stroke="currentColor" stroke-width="2.5"/><line x1="3" y1="9" x2="21" y2="9" stroke="currentColor" stroke-width="2.5"/> |
|
|
</svg>`, |
|
|
'page-api': `<svg width="40" height="40" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> |
|
|
<circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="2" fill="rgba(129, 140, 248, 0.1)"/> |
|
|
<path d="M4 9h16M4 15h16" stroke="currentColor" stroke-width="2.5"/><path d="M12 3v18" stroke="currentColor" stroke-width="2.5"/><circle cx="12" cy="12" r="2.5" fill="currentColor" opacity="0.4"/> |
|
|
</svg>`, |
|
|
'page-debug': `<svg width="40" height="40" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> |
|
|
<circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="2" fill="rgba(248, 113, 113, 0.1)"/> |
|
|
<circle cx="12" cy="12" r="10" fill="currentColor" opacity="0.15"/> |
|
|
<line x1="12" y1="8" x2="12" y2="12" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"/><line x1="12" y1="16" x2="12.01" y2="16" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"/> |
|
|
</svg>`, |
|
|
'page-settings': `<svg width="40" height="40" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> |
|
|
<circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="2" fill="rgba(148, 163, 184, 0.1)"/> |
|
|
<circle cx="12" cy="12" r="3.5" fill="currentColor" opacity="0.25"/> |
|
|
<path d="M12 1v6m0 6v6M5.64 5.64l4.24 4.24m4.24 4.24l4.24 4.24M1 12h6m6 0h6M5.64 18.36l4.24-4.24m4.24-4.24l4.24-4.24" stroke="currentColor" stroke-width="2" stroke-linecap="round"/> |
|
|
</svg>` |
|
|
}; |
|
|
|
|
|
function updateHeaderIcon(pageId) { |
|
|
if (topbarIcon && pageIcons[pageId]) { |
|
|
topbarIcon.innerHTML = pageIcons[pageId]; |
|
|
} |
|
|
} |
|
|
|
|
|
function loadPageData(pageId) { |
|
|
|
|
|
switch(pageId) { |
|
|
case 'page-overview': |
|
|
loadMarketOverviewChart().catch(e => console.error('[Chart]', e)); |
|
|
loadTopCoinsWithSparklines().catch(e => console.error('[Top Coins]', e)); |
|
|
loadOverviewStats().catch(e => console.error('[Stats]', e)); |
|
|
break; |
|
|
case 'page-news': |
|
|
loadNewsData().catch(e => console.error('[News]', e)); |
|
|
break; |
|
|
case 'page-market': |
|
|
|
|
|
break; |
|
|
case 'page-chart': |
|
|
|
|
|
break; |
|
|
} |
|
|
} |
|
|
|
|
|
navButtons.forEach(button => { |
|
|
button.addEventListener('click', () => { |
|
|
const targetPage = button.dataset.nav; |
|
|
|
|
|
|
|
|
navButtons.forEach(btn => btn.classList.remove('active')); |
|
|
button.classList.add('active'); |
|
|
|
|
|
|
|
|
pages.forEach(page => { |
|
|
page.classList.toggle('active', page.id === targetPage); |
|
|
}); |
|
|
|
|
|
|
|
|
updateHeaderIcon(targetPage); |
|
|
|
|
|
|
|
|
loadPageData(targetPage); |
|
|
|
|
|
|
|
|
window.scrollTo({ top: 0, behavior: 'smooth' }); |
|
|
}); |
|
|
}); |
|
|
|
|
|
|
|
|
const activeButton = document.querySelector('.nav-button.active'); |
|
|
if (activeButton) { |
|
|
updateHeaderIcon(activeButton.dataset.nav); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
function initThemeToggle() { |
|
|
const themeToggle = document.querySelector('[data-theme-toggle]'); |
|
|
const body = document.getElementById('main-body'); |
|
|
|
|
|
if (!themeToggle || !body) return; |
|
|
|
|
|
|
|
|
const savedTheme = localStorage.getItem('theme') || 'dark'; |
|
|
body.setAttribute('data-theme', savedTheme); |
|
|
themeToggle.checked = savedTheme === 'light'; |
|
|
|
|
|
|
|
|
themeToggle.addEventListener('change', (e) => { |
|
|
const newTheme = e.target.checked ? 'light' : 'dark'; |
|
|
body.setAttribute('data-theme', newTheme); |
|
|
localStorage.setItem('theme', newTheme); |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
document.addEventListener('DOMContentLoaded', () => { |
|
|
initNavigation(); |
|
|
initThemeToggle(); |
|
|
|
|
|
|
|
|
|
|
|
Promise.all([ |
|
|
loadMarketOverviewChart().catch(e => { |
|
|
console.error('[Overview Chart]', e); |
|
|
const ctx = document.getElementById('market-overview-chart'); |
|
|
if (ctx && ctx.parentElement) { |
|
|
ctx.parentElement.innerHTML = `<div style="padding: 40px; text-align: center; color: var(--text-muted);">Failed to load chart: ${e.message}</div>`; |
|
|
} |
|
|
}), |
|
|
loadTopCoinsWithSparklines().catch(e => { |
|
|
console.error('[Top Coins]', e); |
|
|
const tbody = document.querySelector('[data-top-coins-body]'); |
|
|
if (tbody) { |
|
|
tbody.innerHTML = `<tr><td colspan="6" style="text-align: center; padding: 40px; color: var(--text-muted);">Failed to load: ${e.message}</td></tr>`; |
|
|
} |
|
|
}), |
|
|
loadOverviewStats().catch(e => { |
|
|
console.error('[Stats]', e); |
|
|
const statsContainer = document.querySelector('[data-overview-stats]'); |
|
|
if (statsContainer) { |
|
|
statsContainer.innerHTML = '<div style="grid-column: 1/-1; padding: 20px; text-align: center; color: var(--text-muted);">Failed to load stats. Please refresh the page.</div>'; |
|
|
} |
|
|
}), |
|
|
loadNewsData().catch(e => { |
|
|
console.error('[News]', e); |
|
|
const newsGrid = document.getElementById('news-grid'); |
|
|
if (newsGrid) { |
|
|
newsGrid.innerHTML = '<div style="grid-column: 1/-1; padding: 40px; text-align: center; color: var(--text-muted);">Failed to load news. Please refresh the page.</div>'; |
|
|
} |
|
|
}) |
|
|
]).then(() => { |
|
|
console.log('[Init] All initial data loaded'); |
|
|
}); |
|
|
|
|
|
initChartLabControls(); |
|
|
}); |
|
|
|
|
|
|
|
|
async function refreshAllData() { |
|
|
const refreshBtn = document.getElementById('refresh-market-btn'); |
|
|
const refreshText = document.getElementById('refresh-text'); |
|
|
|
|
|
if (refreshBtn) { |
|
|
refreshBtn.disabled = true; |
|
|
refreshBtn.style.opacity = '0.6'; |
|
|
refreshBtn.style.cursor = 'wait'; |
|
|
} |
|
|
if (refreshText) { |
|
|
refreshText.textContent = 'Refreshing...'; |
|
|
} |
|
|
|
|
|
try { |
|
|
|
|
|
await Promise.all([ |
|
|
loadOverviewStats().catch(e => console.error('[Stats]', e)), |
|
|
loadMarketOverviewChart().catch(e => console.error('[Chart]', e)), |
|
|
loadTopCoinsWithSparklines().catch(e => console.error('[Top Coins]', e)) |
|
|
]); |
|
|
|
|
|
console.log('[Refresh] All data refreshed successfully'); |
|
|
|
|
|
|
|
|
if (window.toast) { |
|
|
window.toast.show('Data refreshed successfully', 'success', { duration: 2000 }); |
|
|
} |
|
|
} catch (error) { |
|
|
console.error('[Refresh] Error refreshing data:', error); |
|
|
|
|
|
|
|
|
if (window.toast) { |
|
|
window.toast.show(`Refresh failed: ${error.message}`, 'error', { duration: 4000 }); |
|
|
} |
|
|
} finally { |
|
|
if (refreshBtn) { |
|
|
refreshBtn.disabled = false; |
|
|
refreshBtn.style.opacity = '1'; |
|
|
refreshBtn.style.cursor = 'pointer'; |
|
|
} |
|
|
if (refreshText) { |
|
|
refreshText.textContent = 'Refresh'; |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
async function loadOverviewStats() { |
|
|
try { |
|
|
|
|
|
let backendUrl = window.BACKEND_URL || window.location.origin; |
|
|
const statsContainer = document.querySelector('[data-overview-stats]'); |
|
|
if (!statsContainer) return; |
|
|
|
|
|
let res = await fetch(`${backendUrl}/api/market/stats`); |
|
|
|
|
|
|
|
|
if (!res.ok && res.status === 404) { |
|
|
|
|
|
const altBackend = backendUrl.includes(':7860') ? backendUrl.replace(':7860', ':7861') : `${backendUrl}:7861`; |
|
|
try { |
|
|
res = await fetch(`${altBackend}/api/market/stats`); |
|
|
if (res.ok) backendUrl = altBackend; |
|
|
} catch (e) { |
|
|
|
|
|
res = await fetch(`${window.location.origin}/api/market/stats`); |
|
|
if (res.ok) backendUrl = window.location.origin; |
|
|
} |
|
|
} |
|
|
if (!res.ok) { |
|
|
throw new Error(`HTTP ${res.status}: ${res.statusText}`); |
|
|
} |
|
|
|
|
|
const data = await res.json(); |
|
|
const stats = data.stats || data.data || data || {}; |
|
|
|
|
|
statsContainer.innerHTML = ` |
|
|
<div class="stat-card"> |
|
|
<div class="stat-icon" style="background: linear-gradient(135deg, rgba(96, 165, 250, 0.2), rgba(34, 211, 238, 0.15));"> |
|
|
<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"> |
|
|
<path d="M12 2v20M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"/> |
|
|
</svg> |
|
|
</div> |
|
|
<div class="stat-label">Total Market Cap</div> |
|
|
<div class="stat-value">$${formatNum(stats.total_market_cap || 0)}</div> |
|
|
</div> |
|
|
<div class="stat-card"> |
|
|
<div class="stat-icon" style="background: linear-gradient(135deg, rgba(52, 211, 153, 0.2), rgba(34, 211, 238, 0.15));"> |
|
|
<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"> |
|
|
<path d="M3 3v18h18"/><path d="M7 12l4-4 4 4 6-6v8H7z"/> |
|
|
</svg> |
|
|
</div> |
|
|
<div class="stat-label">24h Volume</div> |
|
|
<div class="stat-value">$${formatNum(stats.total_volume_24h || 0)}</div> |
|
|
</div> |
|
|
<div class="stat-card"> |
|
|
<div class="stat-icon" style="background: linear-gradient(135deg, rgba(251, 191, 36, 0.2), rgba(244, 114, 182, 0.15));"> |
|
|
<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"> |
|
|
<circle cx="12" cy="12" r="10"/><path d="M12 6v6l4 2"/> |
|
|
</svg> |
|
|
</div> |
|
|
<div class="stat-label">BTC Dominance</div> |
|
|
<div class="stat-value">${(stats.btc_dominance || 0).toFixed(2)}%</div> |
|
|
</div> |
|
|
<div class="stat-card"> |
|
|
<div class="stat-icon" style="background: linear-gradient(135deg, rgba(167, 139, 250, 0.2), rgba(129, 140, 248, 0.15));"> |
|
|
<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"> |
|
|
<rect x="3" y="3" width="7" height="7" rx="1"/><rect x="14" y="3" width="7" height="7" rx="1"/> |
|
|
<rect x="3" y="14" width="7" height="7" rx="1"/><rect x="14" y="14" width="7" height="7" rx="1"/> |
|
|
</svg> |
|
|
</div> |
|
|
<div class="stat-label">Active Coins</div> |
|
|
<div class="stat-value">${stats.active_cryptocurrencies || 0}</div> |
|
|
</div> |
|
|
`; |
|
|
} catch (e) { |
|
|
console.error('[Stats] Error:', e); |
|
|
const statsContainer = document.querySelector('[data-overview-stats]'); |
|
|
if (statsContainer) { |
|
|
statsContainer.innerHTML = '<div style="grid-column: 1/-1; padding: 20px; text-align: center; color: var(--text-muted);">Failed to load stats</div>'; |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
async function loadMarketOverviewChart() { |
|
|
try { |
|
|
let backendUrl = window.BACKEND_URL || window.location.origin; |
|
|
console.log('[Chart] Loading from:', backendUrl); |
|
|
|
|
|
let res = await fetch(`${backendUrl}/api/coins/top?limit=10`); |
|
|
|
|
|
|
|
|
if (!res.ok && res.status === 404) { |
|
|
const altBackend = backendUrl.includes(':7860') ? backendUrl.replace(':7860', ':7861') : `${backendUrl}:7861`; |
|
|
try { |
|
|
res = await fetch(`${altBackend}/api/coins/top?limit=10`); |
|
|
if (res.ok) backendUrl = altBackend; |
|
|
} catch (e) { |
|
|
res = await fetch(`${window.location.origin}/api/coins/top?limit=10`); |
|
|
if (res.ok) backendUrl = window.location.origin; |
|
|
} |
|
|
} |
|
|
|
|
|
if (!res.ok) { |
|
|
throw new Error(`HTTP ${res.status}: ${res.statusText}`); |
|
|
} |
|
|
|
|
|
const data = await res.json(); |
|
|
const coins = data.coins || data.data || data || []; |
|
|
|
|
|
if (!coins || coins.length === 0) { |
|
|
console.warn('[Chart] No coins data received'); |
|
|
const ctx = document.getElementById('market-overview-chart'); |
|
|
if (ctx && ctx.parentElement) { |
|
|
ctx.parentElement.innerHTML = '<div style="padding: 40px; text-align: center; color: var(--text-muted);">No data available</div>'; |
|
|
} |
|
|
return; |
|
|
} |
|
|
|
|
|
console.log('[Chart] Loaded', coins.length, 'coins'); |
|
|
|
|
|
const ctx = document.getElementById('market-overview-chart'); |
|
|
if (!ctx) return; |
|
|
|
|
|
const colors = ['#60A5FA', '#22D3EE', '#34D399', '#FBBF24', '#F87171', '#A78BFA', '#F472B6', '#FB923C', '#2DD4BF', '#818CF8']; |
|
|
|
|
|
const datasets = coins.slice(0, 10).map((coin, i) => { |
|
|
|
|
|
const priceData = coin.sparkline_in_7d?.price || |
|
|
coin.sparkline_data || |
|
|
Array.from({length: 168}, () => coin.price || coin.current_price || 0); |
|
|
|
|
|
return { |
|
|
label: coin.name || coin.symbol, |
|
|
data: priceData, |
|
|
borderColor: colors[i], |
|
|
backgroundColor: colors[i] + '15', |
|
|
borderWidth: 2.5, |
|
|
fill: false, |
|
|
tension: 0.3, |
|
|
pointRadius: 0, |
|
|
pointHoverRadius: 4, |
|
|
pointHoverBorderWidth: 2 |
|
|
}; |
|
|
}); |
|
|
|
|
|
if (chartInstances.overview) chartInstances.overview.destroy(); |
|
|
|
|
|
|
|
|
chartInstances.overview = new Chart(ctx, { |
|
|
type: 'line', |
|
|
data: { labels: Array.from({length: 168}, (_, i) => i), datasets }, |
|
|
options: { |
|
|
responsive: true, |
|
|
maintainAspectRatio: false, |
|
|
interaction: { mode: 'index', intersect: false }, |
|
|
plugins: { |
|
|
legend: { |
|
|
display: true, |
|
|
position: 'top', |
|
|
align: 'start', |
|
|
labels: { |
|
|
usePointStyle: true, |
|
|
pointStyle: 'circle', |
|
|
padding: 16, |
|
|
font: { |
|
|
size: 12, |
|
|
weight: '600', |
|
|
family: 'Manrope' |
|
|
}, |
|
|
color: '#E2E8F0', |
|
|
boxWidth: 14, |
|
|
boxHeight: 14, |
|
|
generateLabels: function(chart) { |
|
|
const original = Chart.defaults.plugins.legend.labels.generateLabels; |
|
|
const labels = original.call(this, chart); |
|
|
labels.forEach(label => { |
|
|
label.fillStyle = label.strokeStyle; |
|
|
label.strokeStyle = label.strokeStyle; |
|
|
label.lineWidth = 2; |
|
|
}); |
|
|
return labels; |
|
|
} |
|
|
}, |
|
|
title: { |
|
|
display: false |
|
|
} |
|
|
}, |
|
|
tooltip: { |
|
|
backgroundColor: 'rgba(10, 15, 30, 0.98)', |
|
|
padding: 12, |
|
|
borderColor: 'rgba(96, 165, 250, 0.3)', |
|
|
borderWidth: 1.5, |
|
|
titleColor: '#F8FAFC', |
|
|
bodyColor: '#E2E8F0', |
|
|
titleFont: { size: 12, weight: '700', family: 'Manrope' }, |
|
|
bodyFont: { size: 11, family: 'Manrope' }, |
|
|
cornerRadius: 8, |
|
|
displayColors: true, |
|
|
boxPadding: 6 |
|
|
} |
|
|
}, |
|
|
scales: { |
|
|
x: { |
|
|
grid: { |
|
|
display: true, |
|
|
color: 'rgba(255, 255, 255, 0.03)', |
|
|
lineWidth: 1 |
|
|
}, |
|
|
ticks: { |
|
|
display: false, |
|
|
color: '#64748B', |
|
|
font: { size: 10, family: 'Manrope' } |
|
|
}, |
|
|
border: { |
|
|
display: false |
|
|
} |
|
|
}, |
|
|
y: { |
|
|
position: 'right', |
|
|
grid: { |
|
|
color: 'rgba(255, 255, 255, 0.05)', |
|
|
lineWidth: 1, |
|
|
drawBorder: false, |
|
|
drawTicks: true |
|
|
}, |
|
|
ticks: { |
|
|
color: '#94A3B8', |
|
|
font: { size: 11, weight: '600', family: 'Manrope' }, |
|
|
padding: 8, |
|
|
stepSize: null, |
|
|
precision: 2, |
|
|
callback: function(value) { |
|
|
if (value >= 1e12) return '$' + (value / 1e12).toFixed(2) + 'T'; |
|
|
if (value >= 1e9) return '$' + (value / 1e9).toFixed(2) + 'B'; |
|
|
if (value >= 1e6) return '$' + (value / 1e6).toFixed(2) + 'M'; |
|
|
if (value >= 1e3) return '$' + (value / 1e3).toFixed(2) + 'K'; |
|
|
return '$' + value.toFixed(2); |
|
|
} |
|
|
}, |
|
|
border: { |
|
|
display: false |
|
|
}, |
|
|
beginAtZero: false |
|
|
} |
|
|
}, |
|
|
elements: { |
|
|
line: { |
|
|
tension: 0, |
|
|
borderWidth: 2 |
|
|
}, |
|
|
point: { |
|
|
radius: 0, |
|
|
hoverRadius: 4 |
|
|
} |
|
|
} |
|
|
} |
|
|
}); |
|
|
} catch (e) { |
|
|
console.error('[Chart] Error loading market overview:', e); |
|
|
const ctx = document.getElementById('market-overview-chart'); |
|
|
if (ctx) { |
|
|
ctx.parentElement.innerHTML = `<div style="padding: 40px; text-align: center; color: var(--text-muted);"> |
|
|
<p>Failed to load chart data</p> |
|
|
<p style="font-size: 0.875rem; margin-top: 8px;">${e.message}</p> |
|
|
</div>`; |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
async function loadTopCoinsWithSparklines() { |
|
|
try { |
|
|
let backendUrl = window.BACKEND_URL || window.location.origin; |
|
|
console.log('[Top Coins] Loading from:', backendUrl); |
|
|
|
|
|
let res = await fetch(`${backendUrl}/api/coins/top?limit=20`); |
|
|
|
|
|
|
|
|
if (!res.ok && res.status === 404) { |
|
|
const altBackend = backendUrl.includes(':7860') ? backendUrl.replace(':7860', ':7861') : `${backendUrl}:7861`; |
|
|
try { |
|
|
res = await fetch(`${altBackend}/api/coins/top?limit=20`); |
|
|
if (res.ok) backendUrl = altBackend; |
|
|
} catch (e) { |
|
|
res = await fetch(`${window.location.origin}/api/coins/top?limit=20`); |
|
|
if (res.ok) backendUrl = window.location.origin; |
|
|
} |
|
|
} |
|
|
|
|
|
if (!res.ok) { |
|
|
throw new Error(`HTTP ${res.status}: ${res.statusText}`); |
|
|
} |
|
|
|
|
|
const data = await res.json(); |
|
|
const coins = data.coins || data || []; |
|
|
|
|
|
if (!coins || coins.length === 0) { |
|
|
console.warn('No coins data received'); |
|
|
const tbody = document.querySelector('[data-top-coins-body]'); |
|
|
if (tbody) { |
|
|
tbody.innerHTML = '<tr><td colspan="7" style="text-align: center; padding: 40px; color: var(--text-muted);">No data available</td></tr>'; |
|
|
} |
|
|
return; |
|
|
} |
|
|
|
|
|
const tbody = document.querySelector('[data-top-coins-body]'); |
|
|
if (!tbody) return; |
|
|
|
|
|
tbody.innerHTML = coins.map((coin, i) => { |
|
|
const change24h = coin.price_change_percentage_24h || 0; |
|
|
const change7d = coin.price_change_percentage_7d_in_currency || 0; |
|
|
const coinName = coin.name || 'Unknown'; |
|
|
const coinSymbol = (coin.symbol || 'N/A').toUpperCase(); |
|
|
const coinImage = coin.image || 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMzIiIGhlaWdodD0iMzIiIHZpZXdCb3g9IjAgMCAzMiAzMiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48Y2lyY2xlIGN4PSIxNiIgY3k9IjE2IiByPSIxNiIgZmlsbD0iIzM0NDc1NiIvPjwvc3ZnPg=='; |
|
|
const coinId = coin.id || `coin-${i}`; |
|
|
|
|
|
return ` |
|
|
<tr> |
|
|
<td>${i + 1}</td> |
|
|
<td> |
|
|
<div style="display: flex; align-items: center; gap: 12px;"> |
|
|
<img src="${coinImage}" style="width: 32px; height: 32px; border-radius: 50%;" onerror="this.src='data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMzIiIGhlaWdodD0iMzIiIHZpZXdCb3g9IjAgMCAzMiAzMiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48Y2lyY2xlIGN4PSIxNiIgY3k9IjE2IiByPSIxNiIgZmlsbD0iIzM0NDc1NiIvPjwvc3ZnPg=='"> |
|
|
<div> |
|
|
<div style="font-weight: 600;">${coinName}</div> |
|
|
<div style="font-size: 11px; color: #94A3B8;">${coinSymbol}</div> |
|
|
</div> |
|
|
</div> |
|
|
</td> |
|
|
<td style="font-weight: 600;">$${formatNum(coin.current_price)}</td> |
|
|
<td> |
|
|
<span style="color: ${change24h >= 0 ? '#34D399' : '#F87171'}; font-weight: 600;"> |
|
|
${change24h >= 0 ? '↑' : '↓'} ${Math.abs(Number(change24h) || 0).toFixed(2)}% |
|
|
</span> |
|
|
</td> |
|
|
<td> |
|
|
<span style="color: ${change7d >= 0 ? '#34D399' : '#F87171'}; font-weight: 600;"> |
|
|
${change7d >= 0 ? '↑' : '↓'} ${Math.abs(Number(change7d) || 0).toFixed(2)}% |
|
|
</span> |
|
|
</td> |
|
|
<td>$${formatNum(coin.market_cap)}</td> |
|
|
<td>$${formatNum(coin.total_volume)}</td> |
|
|
<td><canvas id="spark-${coinId}" width="100" height="30"></canvas></td> |
|
|
</tr> |
|
|
`; |
|
|
}).join(''); |
|
|
|
|
|
setTimeout(() => { |
|
|
coins.forEach((coin, i) => { |
|
|
const coinId = coin.id || `coin-${i}`; |
|
|
if (coin.sparkline_in_7d?.price && Array.isArray(coin.sparkline_in_7d.price)) { |
|
|
const coinChange24h = coin.price_change_percentage_24h || 0; |
|
|
createSparkline(`spark-${coinId}`, coin.sparkline_in_7d.price, coinChange24h >= 0); |
|
|
} |
|
|
}); |
|
|
}, 100); |
|
|
|
|
|
} catch (e) { |
|
|
console.error('[Table] Error loading top coins:', e); |
|
|
const tbody = document.querySelector('[data-top-coins-body]'); |
|
|
if (tbody) { |
|
|
tbody.innerHTML = `<tr><td colspan="7" style="text-align: center; padding: 40px; color: var(--text-muted);"> |
|
|
<p>Failed to load data</p> |
|
|
<p style="font-size: 0.875rem; margin-top: 8px;">${e.message}</p> |
|
|
</td></tr>`; |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
function createSparkline(id, data, isPositive) { |
|
|
const canvas = document.getElementById(id); |
|
|
if (!canvas) return; |
|
|
|
|
|
const color = isPositive ? '#10B981' : '#EF4444'; |
|
|
|
|
|
new Chart(canvas, { |
|
|
type: 'line', |
|
|
data: { |
|
|
labels: data.map((_, i) => i), |
|
|
datasets: [{ |
|
|
data: data, |
|
|
borderColor: color, |
|
|
backgroundColor: color + '30', |
|
|
borderWidth: 2, |
|
|
fill: true, |
|
|
tension: 0.4, |
|
|
pointRadius: 0 |
|
|
}] |
|
|
}, |
|
|
options: { |
|
|
responsive: false, |
|
|
plugins: { legend: { display: false }, tooltip: { enabled: false } }, |
|
|
scales: { x: { display: false }, y: { display: false } } |
|
|
} |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
function initChartLabControls() { |
|
|
const input = document.getElementById('chartCoinSearch'); |
|
|
const dropdown = document.getElementById('chartCoinDropdown'); |
|
|
|
|
|
if (!input || !dropdown) return; |
|
|
|
|
|
input.addEventListener('focus', async () => { |
|
|
if (allCoins.length === 0) { |
|
|
let backendUrl = window.BACKEND_URL || window.location.origin; |
|
|
let res = await fetch(`${backendUrl}/api/coins/top?limit=100`); |
|
|
|
|
|
|
|
|
if (!res.ok && res.status === 404) { |
|
|
const altBackend = backendUrl.includes(':7860') ? backendUrl.replace(':7860', ':7861') : `${backendUrl}:7861`; |
|
|
try { |
|
|
res = await fetch(`${altBackend}/api/coins/top?limit=100`); |
|
|
if (res.ok) backendUrl = altBackend; |
|
|
} catch (e) { |
|
|
res = await fetch(`${window.location.origin}/api/coins/top?limit=100`); |
|
|
if (res.ok) backendUrl = window.location.origin; |
|
|
} |
|
|
} |
|
|
|
|
|
if (res.ok) { |
|
|
const data = await res.json(); |
|
|
allCoins = data.coins || data || []; |
|
|
} |
|
|
} |
|
|
renderCoinDropdown(allCoins); |
|
|
dropdown.style.display = 'block'; |
|
|
}); |
|
|
|
|
|
input.addEventListener('input', (e) => { |
|
|
const term = e.target.value.toLowerCase(); |
|
|
const filtered = allCoins.filter(c => c.name.toLowerCase().includes(term) || c.symbol.toLowerCase().includes(term)); |
|
|
renderCoinDropdown(filtered); |
|
|
}); |
|
|
|
|
|
document.addEventListener('click', (e) => { |
|
|
if (!input.contains(e.target) && !dropdown.contains(e.target)) { |
|
|
dropdown.style.display = 'none'; |
|
|
} |
|
|
}); |
|
|
|
|
|
|
|
|
document.querySelectorAll('[data-chart-timeframe]').forEach(btn => { |
|
|
btn.addEventListener('click', () => { |
|
|
document.querySelectorAll('[data-chart-timeframe]').forEach(b => b.classList.remove('active')); |
|
|
btn.classList.add('active'); |
|
|
selectedTimeframe = parseInt(btn.dataset.chartTimeframe); |
|
|
if (selectedCoin) loadCoinDetailChart(selectedCoin.id); |
|
|
}); |
|
|
}); |
|
|
} |
|
|
|
|
|
function renderCoinDropdown(coins) { |
|
|
const dropdown = document.getElementById('chartCoinDropdown'); |
|
|
if (!dropdown) return; |
|
|
|
|
|
dropdown.innerHTML = coins.slice(0, 50).map(coin => ` |
|
|
<div onclick="selectChartCoin('${coin.id}')" style="padding: 12px 16px; display: flex; align-items: center; gap: 12px; cursor: pointer; border-bottom: 1px solid rgba(255, 255, 255, 0.05); transition: all 0.2s;"> |
|
|
<img src="${coin.image}" style="width: 32px; height: 32px; border-radius: 50%;"> |
|
|
<div style="flex: 1;"> |
|
|
<div style="font-weight: 600;">${coin.name}</div> |
|
|
<div style="font-size: 11px; color: #94A3B8;">${coin.symbol.toUpperCase()}</div> |
|
|
</div> |
|
|
<div style="font-weight: 600;">$${formatNum(coin.current_price)}</div> |
|
|
</div> |
|
|
`).join(''); |
|
|
|
|
|
dropdown.querySelectorAll('div[onclick]').forEach(el => { |
|
|
el.addEventListener('mouseenter', () => { |
|
|
el.style.background = 'rgba(34, 211, 238, 0.15)'; |
|
|
el.style.borderLeft = '3px solid #22D3EE'; |
|
|
}); |
|
|
el.addEventListener('mouseleave', () => { |
|
|
el.style.background = 'transparent'; |
|
|
el.style.borderLeft = 'none'; |
|
|
}); |
|
|
}); |
|
|
} |
|
|
|
|
|
window.selectChartCoin = function(coinId) { |
|
|
selectedCoin = allCoins.find(c => c.id === coinId); |
|
|
if (!selectedCoin) return; |
|
|
|
|
|
document.getElementById('chartCoinSearch').value = `${selectedCoin.name} (${selectedCoin.symbol.toUpperCase()})`; |
|
|
document.getElementById('chartCoinDropdown').style.display = 'none'; |
|
|
|
|
|
loadCoinDetailChart(coinId); |
|
|
}; |
|
|
|
|
|
window.loadSelectedChart = function() { |
|
|
if (selectedCoin) { |
|
|
loadCoinDetailChart(selectedCoin.id); |
|
|
} |
|
|
}; |
|
|
|
|
|
async function loadCoinDetailChart(coinId) { |
|
|
try { |
|
|
const backendUrl = window.BACKEND_URL || window.location.origin; |
|
|
const interval = selectedTimeframe === 1 ? '1h' : selectedTimeframe === 7 ? '1d' : selectedTimeframe === 30 ? '1d' : '1d'; |
|
|
const res = await fetch(`${backendUrl}/api/charts/price/${coinId}?interval=${interval}&limit=100`); |
|
|
|
|
|
if (!res.ok) { |
|
|
throw new Error(`HTTP ${res.status}: ${res.statusText}`); |
|
|
} |
|
|
|
|
|
const chartData = await res.json(); |
|
|
|
|
|
|
|
|
let data; |
|
|
if (chartData.prices && Array.isArray(chartData.prices)) { |
|
|
data = { |
|
|
prices: chartData.prices.map((p, i) => { |
|
|
const timestamp = typeof p === 'number' ? Date.now() - (chartData.prices.length - i) * 3600000 : p[0]; |
|
|
const price = typeof p === 'number' ? p : p[1]; |
|
|
return [timestamp, price]; |
|
|
}), |
|
|
total_volumes: chartData.volumes?.map((v, i) => { |
|
|
const timestamp = typeof v === 'number' ? Date.now() - (chartData.volumes.length - i) * 3600000 : v[0]; |
|
|
const volume = typeof v === 'number' ? v : v[1]; |
|
|
return [timestamp, volume]; |
|
|
}) || [] |
|
|
}; |
|
|
} else { |
|
|
|
|
|
const ohlcvRes = await fetch(`${backendUrl}/api/ohlcv?symbol=${coinId}&interval=${interval}&limit=100`); |
|
|
if (ohlcvRes.ok) { |
|
|
const ohlcvData = await ohlcvRes.json(); |
|
|
data = { |
|
|
prices: ohlcvData.data?.map((d, i) => [Date.now() - (ohlcvData.data.length - i) * 3600000, d.close || d.price || 0]) || [], |
|
|
total_volumes: ohlcvData.data?.map((d, i) => [Date.now() - (ohlcvData.data.length - i) * 3600000, d.volume || 0]) || [] |
|
|
}; |
|
|
} else { |
|
|
throw new Error('No chart data available'); |
|
|
} |
|
|
} |
|
|
|
|
|
const coin = allCoins.find(c => c.id === coinId) || selectedCoin; |
|
|
|
|
|
document.getElementById('selectedCoinTitle').textContent = `${coin.name} (${coin.symbol.toUpperCase()})`; |
|
|
document.getElementById('selectedCoinPrice').textContent = `$${formatNum(coin.current_price)}`; |
|
|
|
|
|
const change = coin.price_change_percentage_24h || 0; |
|
|
const changeEl = document.getElementById('selectedCoinChange'); |
|
|
changeEl.textContent = `${change >= 0 ? '+' : ''}${change.toFixed(2)}%`; |
|
|
changeEl.className = `badge ${change >= 0 ? 'badge-success' : 'badge-danger'}`; |
|
|
|
|
|
|
|
|
const priceCtx = document.getElementById('price-chart'); |
|
|
if (priceCtx) { |
|
|
if (chartInstances.price) chartInstances.price.destroy(); |
|
|
|
|
|
const chartType = document.getElementById('chartType').value; |
|
|
|
|
|
|
|
|
chartInstances.price = new Chart(priceCtx, { |
|
|
type: chartType === 'bar' ? 'bar' : 'line', |
|
|
data: { |
|
|
labels: data.prices.map(p => new Date(p[0])), |
|
|
datasets: [{ |
|
|
label: 'Price', |
|
|
data: data.prices.map(p => p[1]), |
|
|
borderColor: '#60A5FA', |
|
|
backgroundColor: chartType === 'area' ? 'rgba(96, 165, 250, 0.1)' : chartType === 'bar' ? 'rgba(96, 165, 250, 0.6)' : 'transparent', |
|
|
borderWidth: chartType === 'line' ? 2 : 0, |
|
|
fill: chartType === 'area', |
|
|
tension: 0, |
|
|
pointRadius: 0, |
|
|
pointHoverRadius: 4, |
|
|
pointHoverBorderWidth: 2, |
|
|
pointHoverBackgroundColor: '#60A5FA' |
|
|
}] |
|
|
}, |
|
|
options: { |
|
|
responsive: true, |
|
|
maintainAspectRatio: false, |
|
|
interaction: { |
|
|
intersect: false, |
|
|
mode: 'index' |
|
|
}, |
|
|
plugins: { |
|
|
legend: { display: false }, |
|
|
tooltip: { |
|
|
backgroundColor: 'rgba(10, 15, 30, 0.98)', |
|
|
padding: 14, |
|
|
borderColor: 'rgba(96, 165, 250, 0.3)', |
|
|
borderWidth: 1.5, |
|
|
titleColor: '#F8FAFC', |
|
|
bodyColor: '#E2E8F0', |
|
|
titleFont: { size: 13, weight: '700', family: 'Manrope' }, |
|
|
bodyFont: { size: 12, family: 'Manrope' }, |
|
|
cornerRadius: 8, |
|
|
displayColors: false, |
|
|
callbacks: { |
|
|
label: function(context) { |
|
|
return `Price: $${formatNum(context.parsed.y)}`; |
|
|
} |
|
|
} |
|
|
} |
|
|
}, |
|
|
scales: { |
|
|
x: { |
|
|
type: 'time', |
|
|
time: { |
|
|
displayFormats: { |
|
|
hour: 'HH:mm', |
|
|
day: 'MMM dd' |
|
|
} |
|
|
}, |
|
|
grid: { |
|
|
display: true, |
|
|
color: 'rgba(255, 255, 255, 0.05)', |
|
|
lineWidth: 1 |
|
|
}, |
|
|
ticks: { |
|
|
color: '#64748B', |
|
|
font: { size: 11, family: 'Manrope' }, |
|
|
maxRotation: 0 |
|
|
}, |
|
|
border: { |
|
|
display: true, |
|
|
color: 'rgba(255, 255, 255, 0.1)' |
|
|
} |
|
|
}, |
|
|
y: { |
|
|
position: 'right', |
|
|
grid: { |
|
|
color: 'rgba(255, 255, 255, 0.05)', |
|
|
lineWidth: 1, |
|
|
drawBorder: false |
|
|
}, |
|
|
ticks: { |
|
|
color: '#64748B', |
|
|
font: { size: 11, family: 'Manrope' }, |
|
|
callback: v => '$' + formatNum(v), |
|
|
padding: 8 |
|
|
}, |
|
|
border: { |
|
|
display: true, |
|
|
color: 'rgba(255, 255, 255, 0.1)' |
|
|
} |
|
|
} |
|
|
}, |
|
|
elements: { |
|
|
line: { |
|
|
cubicInterpolationMode: 'monotone' |
|
|
} |
|
|
} |
|
|
} |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
const volumeCtx = document.getElementById('volume-chart'); |
|
|
if (volumeCtx) { |
|
|
if (chartInstances.volume) chartInstances.volume.destroy(); |
|
|
|
|
|
chartInstances.volume = new Chart(volumeCtx, { |
|
|
type: 'bar', |
|
|
data: { |
|
|
labels: data.total_volumes.map(v => new Date(v[0])), |
|
|
datasets: [{ |
|
|
label: 'Volume', |
|
|
data: data.total_volumes.map(v => v[1]), |
|
|
backgroundColor: (ctx) => { |
|
|
const idx = ctx.dataIndex; |
|
|
if (idx === 0) return 'rgba(52, 211, 153, 0.4)'; |
|
|
const prev = data.total_volumes[idx - 1]?.[1] || 0; |
|
|
const curr = data.total_volumes[idx][1]; |
|
|
return curr >= prev ? 'rgba(52, 211, 153, 0.5)' : 'rgba(248, 113, 113, 0.5)'; |
|
|
}, |
|
|
borderColor: (ctx) => { |
|
|
const idx = ctx.dataIndex; |
|
|
if (idx === 0) return '#34D399'; |
|
|
const prev = data.total_volumes[idx - 1]?.[1] || 0; |
|
|
const curr = data.total_volumes[idx][1]; |
|
|
return curr >= prev ? '#34D399' : '#F87171'; |
|
|
}, |
|
|
borderWidth: 1, |
|
|
borderRadius: 2, |
|
|
borderSkipped: false |
|
|
}] |
|
|
}, |
|
|
options: { |
|
|
responsive: true, |
|
|
maintainAspectRatio: false, |
|
|
plugins: { |
|
|
legend: { display: false }, |
|
|
tooltip: { |
|
|
backgroundColor: 'rgba(10, 15, 30, 0.98)', |
|
|
padding: 12, |
|
|
borderColor: 'rgba(52, 211, 153, 0.3)', |
|
|
borderWidth: 1.5, |
|
|
titleColor: '#F8FAFC', |
|
|
bodyColor: '#E2E8F0', |
|
|
titleFont: { size: 12, weight: '700', family: 'Manrope' }, |
|
|
bodyFont: { size: 11, family: 'Manrope' }, |
|
|
cornerRadius: 8, |
|
|
displayColors: false, |
|
|
callbacks: { |
|
|
label: function(context) { |
|
|
return `Volume: $${formatNum(context.parsed.y)}`; |
|
|
} |
|
|
} |
|
|
} |
|
|
}, |
|
|
scales: { |
|
|
x: { |
|
|
type: 'time', |
|
|
time: { |
|
|
displayFormats: { |
|
|
hour: 'HH:mm', |
|
|
day: 'MMM dd' |
|
|
} |
|
|
}, |
|
|
grid: { |
|
|
display: true, |
|
|
color: 'rgba(255, 255, 255, 0.03)', |
|
|
lineWidth: 1 |
|
|
}, |
|
|
ticks: { |
|
|
color: '#64748B', |
|
|
font: { size: 10, family: 'Manrope' }, |
|
|
maxRotation: 0 |
|
|
}, |
|
|
border: { |
|
|
display: false |
|
|
} |
|
|
}, |
|
|
y: { |
|
|
position: 'right', |
|
|
grid: { |
|
|
color: 'rgba(255, 255, 255, 0.05)', |
|
|
lineWidth: 1, |
|
|
drawBorder: false |
|
|
}, |
|
|
ticks: { |
|
|
color: '#64748B', |
|
|
font: { size: 10, family: 'Manrope' }, |
|
|
callback: v => '$' + formatNum(v), |
|
|
padding: 6 |
|
|
}, |
|
|
border: { |
|
|
display: false |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
const rsiCtx = document.getElementById('rsi-chart'); |
|
|
if (rsiCtx) { |
|
|
if (chartInstances.rsi) chartInstances.rsi.destroy(); |
|
|
|
|
|
const rsiData = calculateRSI(data.prices.map(p => p[1])); |
|
|
|
|
|
chartInstances.rsi = new Chart(rsiCtx, { |
|
|
type: 'line', |
|
|
data: { |
|
|
labels: rsiData.map((_, i) => i), |
|
|
datasets: [{ |
|
|
label: 'RSI (14)', |
|
|
data: rsiData, |
|
|
borderColor: '#A78BFA', |
|
|
backgroundColor: 'rgba(167, 139, 250, 0.12)', |
|
|
borderWidth: 2.5, |
|
|
fill: true, |
|
|
tension: 0, |
|
|
pointRadius: 0, |
|
|
pointHoverRadius: 4 |
|
|
}] |
|
|
}, |
|
|
options: { |
|
|
responsive: true, |
|
|
maintainAspectRatio: false, |
|
|
plugins: { |
|
|
legend: { |
|
|
display: true, |
|
|
position: 'top', |
|
|
align: 'start', |
|
|
labels: { |
|
|
usePointStyle: true, |
|
|
pointStyle: 'circle', |
|
|
padding: 14, |
|
|
font: { |
|
|
size: 12, |
|
|
weight: '600', |
|
|
family: 'Manrope' |
|
|
}, |
|
|
color: '#E2E8F0', |
|
|
boxWidth: 14, |
|
|
boxHeight: 14 |
|
|
} |
|
|
}, |
|
|
tooltip: { |
|
|
backgroundColor: 'rgba(10, 15, 30, 0.98)', |
|
|
padding: 12, |
|
|
borderColor: 'rgba(167, 139, 250, 0.3)', |
|
|
borderWidth: 1.5, |
|
|
titleColor: '#F8FAFC', |
|
|
bodyColor: '#E2E8F0', |
|
|
titleFont: { size: 12, weight: '700', family: 'Manrope' }, |
|
|
bodyFont: { size: 11, family: 'Manrope' }, |
|
|
cornerRadius: 8, |
|
|
displayColors: false, |
|
|
callbacks: { |
|
|
label: function(context) { |
|
|
return `RSI: ${context.parsed.y.toFixed(2)}`; |
|
|
} |
|
|
} |
|
|
} |
|
|
}, |
|
|
scales: { |
|
|
y: { |
|
|
min: 0, |
|
|
max: 100, |
|
|
grid: { |
|
|
color: 'rgba(255, 255, 255, 0.05)', |
|
|
lineWidth: 1 |
|
|
}, |
|
|
ticks: { |
|
|
color: '#64748B', |
|
|
font: { size: 11, family: 'Manrope' }, |
|
|
padding: 6 |
|
|
}, |
|
|
border: { |
|
|
display: false |
|
|
} |
|
|
}, |
|
|
x: { |
|
|
grid: { |
|
|
display: true, |
|
|
color: 'rgba(255, 255, 255, 0.03)', |
|
|
lineWidth: 1 |
|
|
}, |
|
|
ticks: { |
|
|
display: false, |
|
|
color: '#64748B', |
|
|
font: { size: 10, family: 'Manrope' } |
|
|
}, |
|
|
border: { |
|
|
display: false |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
const maCtx = document.getElementById('ma-chart'); |
|
|
if (maCtx) { |
|
|
if (chartInstances.ma) chartInstances.ma.destroy(); |
|
|
|
|
|
const prices = data.prices.map(p => p[1]); |
|
|
const ma7 = calculateMA(prices, 7); |
|
|
const ma25 = calculateMA(prices, 25); |
|
|
const ma99 = calculateMA(prices, 99); |
|
|
|
|
|
chartInstances.ma = new Chart(maCtx, { |
|
|
type: 'line', |
|
|
data: { |
|
|
labels: prices.map((_, i) => i), |
|
|
datasets: [ |
|
|
{ |
|
|
label: 'MA 7', |
|
|
data: ma7, |
|
|
borderColor: '#60A5FA', |
|
|
backgroundColor: 'rgba(96, 165, 250, 0.1)', |
|
|
borderWidth: 2.5, |
|
|
fill: false, |
|
|
tension: 0, |
|
|
pointRadius: 0, |
|
|
pointHoverRadius: 4 |
|
|
}, |
|
|
{ |
|
|
label: 'MA 25', |
|
|
data: ma25, |
|
|
borderColor: '#34D399', |
|
|
backgroundColor: 'rgba(52, 211, 153, 0.1)', |
|
|
borderWidth: 2.5, |
|
|
fill: false, |
|
|
tension: 0, |
|
|
pointRadius: 0, |
|
|
pointHoverRadius: 4 |
|
|
}, |
|
|
{ |
|
|
label: 'MA 99', |
|
|
data: ma99, |
|
|
borderColor: '#FBBF24', |
|
|
backgroundColor: 'rgba(251, 191, 36, 0.1)', |
|
|
borderWidth: 2.5, |
|
|
fill: false, |
|
|
tension: 0, |
|
|
pointRadius: 0, |
|
|
pointHoverRadius: 4 |
|
|
} |
|
|
] |
|
|
}, |
|
|
options: { |
|
|
responsive: true, |
|
|
maintainAspectRatio: false, |
|
|
plugins: { |
|
|
legend: { |
|
|
display: true, |
|
|
position: 'top', |
|
|
align: 'start', |
|
|
labels: { |
|
|
usePointStyle: true, |
|
|
pointStyle: 'circle', |
|
|
padding: 14, |
|
|
font: { |
|
|
size: 12, |
|
|
weight: '600', |
|
|
family: 'Manrope' |
|
|
}, |
|
|
color: '#E2E8F0', |
|
|
boxWidth: 14, |
|
|
boxHeight: 14 |
|
|
} |
|
|
}, |
|
|
tooltip: { |
|
|
backgroundColor: 'rgba(10, 15, 30, 0.98)', |
|
|
padding: 12, |
|
|
borderColor: 'rgba(96, 165, 250, 0.3)', |
|
|
borderWidth: 1.5, |
|
|
titleColor: '#F8FAFC', |
|
|
bodyColor: '#E2E8F0', |
|
|
titleFont: { size: 12, weight: '700', family: 'Manrope' }, |
|
|
bodyFont: { size: 11, family: 'Manrope' }, |
|
|
cornerRadius: 8, |
|
|
displayColors: true, |
|
|
boxPadding: 6 |
|
|
} |
|
|
}, |
|
|
scales: { |
|
|
y: { |
|
|
grid: { |
|
|
color: 'rgba(255, 255, 255, 0.05)', |
|
|
lineWidth: 1 |
|
|
}, |
|
|
ticks: { |
|
|
color: '#64748B', |
|
|
font: { size: 11, family: 'Manrope' }, |
|
|
callback: v => '$' + formatNum(v), |
|
|
padding: 6 |
|
|
}, |
|
|
border: { |
|
|
display: false |
|
|
} |
|
|
}, |
|
|
x: { |
|
|
grid: { |
|
|
display: true, |
|
|
color: 'rgba(255, 255, 255, 0.03)', |
|
|
lineWidth: 1 |
|
|
}, |
|
|
ticks: { |
|
|
display: false, |
|
|
color: '#64748B', |
|
|
font: { size: 10, family: 'Manrope' } |
|
|
}, |
|
|
border: { |
|
|
display: false |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
}); |
|
|
} |
|
|
|
|
|
} catch (e) { |
|
|
console.error('Detail chart error:', e); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
function calculateRSI(prices, period = 14) { |
|
|
const rsi = []; |
|
|
for (let i = period; i < prices.length; i++) { |
|
|
let gains = 0, losses = 0; |
|
|
for (let j = i - period; j < i; j++) { |
|
|
const change = prices[j + 1] - prices[j]; |
|
|
if (change > 0) gains += change; |
|
|
else losses -= change; |
|
|
} |
|
|
const avgGain = gains / period; |
|
|
const avgLoss = losses / period; |
|
|
const rs = avgGain / avgLoss; |
|
|
rsi.push(100 - (100 / (1 + rs))); |
|
|
} |
|
|
return rsi; |
|
|
} |
|
|
|
|
|
|
|
|
function calculateMA(prices, period) { |
|
|
const ma = []; |
|
|
for (let i = 0; i < prices.length; i++) { |
|
|
if (i < period - 1) { |
|
|
ma.push(null); |
|
|
} else { |
|
|
const sum = prices.slice(i - period + 1, i + 1).reduce((a, b) => a + b, 0); |
|
|
ma.push(sum / period); |
|
|
} |
|
|
} |
|
|
return ma; |
|
|
} |
|
|
|
|
|
function formatNum(num) { |
|
|
if (num === null || num === undefined || isNaN(num)) { |
|
|
return '0.00'; |
|
|
} |
|
|
num = Number(num); |
|
|
if (num >= 1e12) return (num / 1e12).toFixed(2) + 'T'; |
|
|
if (num >= 1e9) return (num / 1e9).toFixed(2) + 'B'; |
|
|
if (num >= 1e6) return (num / 1e6).toFixed(2) + 'M'; |
|
|
if (num >= 1e3) return (num / 1e3).toFixed(2) + 'K'; |
|
|
return num.toFixed(2); |
|
|
} |
|
|
|
|
|
|
|
|
let newsData = []; |
|
|
let newsFilter = { search: '', range: 'all', symbol: '' }; |
|
|
|
|
|
async function loadNewsData() { |
|
|
return loadNews(); |
|
|
} |
|
|
|
|
|
async function loadNews() { |
|
|
const loadingEl = document.getElementById('news-loading'); |
|
|
const errorEl = document.getElementById('news-error'); |
|
|
const gridEl = document.getElementById('news-grid'); |
|
|
const countEl = document.getElementById('news-count'); |
|
|
|
|
|
try { |
|
|
loadingEl.style.display = 'block'; |
|
|
errorEl.style.display = 'none'; |
|
|
gridEl.innerHTML = ''; |
|
|
|
|
|
let backendUrl = window.BACKEND_URL || window.location.origin; |
|
|
let res = await fetch(`${backendUrl}/api/news/latest?limit=40`); |
|
|
|
|
|
|
|
|
if (!res.ok && res.status === 404) { |
|
|
const altBackend = backendUrl.includes(':7860') ? backendUrl.replace(':7860', ':7861') : `${backendUrl}:7861`; |
|
|
try { |
|
|
res = await fetch(`${altBackend}/api/news/latest?limit=40`); |
|
|
if (res.ok) backendUrl = altBackend; |
|
|
} catch (e) { |
|
|
res = await fetch(`${window.location.origin}/api/news/latest?limit=40`); |
|
|
if (res.ok) backendUrl = window.location.origin; |
|
|
} |
|
|
} |
|
|
|
|
|
if (!res.ok) { |
|
|
throw new Error(`HTTP ${res.status}: ${res.statusText}`); |
|
|
} |
|
|
|
|
|
const data = await res.json(); |
|
|
newsData = data.news || data.data || data || []; |
|
|
|
|
|
if (!Array.isArray(newsData) || newsData.length === 0) { |
|
|
gridEl.innerHTML = ` |
|
|
<div style="grid-column: 1 / -1; text-align: center; padding: 60px; color: var(--text-muted);"> |
|
|
<div style="font-size: 3rem; margin-bottom: 16px;">📰</div> |
|
|
<div>No news available at the moment</div> |
|
|
</div> |
|
|
`; |
|
|
countEl.textContent = '0 articles'; |
|
|
return; |
|
|
} |
|
|
|
|
|
countEl.textContent = `${newsData.length} articles`; |
|
|
renderNews(newsData); |
|
|
|
|
|
} catch (e) { |
|
|
console.error('[News] Error loading news:', e); |
|
|
errorEl.style.display = 'block'; |
|
|
const errorMessage = e.message || 'Unknown error'; |
|
|
const is404 = errorMessage.includes('404'); |
|
|
errorEl.innerHTML = ` |
|
|
<div style="font-size: 3rem; margin-bottom: 16px;">⚠️</div> |
|
|
<div style="font-size: 1.125rem; margin-bottom: 8px;">Failed to load news</div> |
|
|
<div style="font-size: 0.875rem; color: var(--text-muted);"> |
|
|
${is404 ? 'News endpoint not available. This feature may not be configured on the backend.' : errorMessage} |
|
|
</div> |
|
|
`; |
|
|
gridEl.innerHTML = ''; |
|
|
countEl.textContent = '0 articles'; |
|
|
} finally { |
|
|
loadingEl.style.display = 'none'; |
|
|
} |
|
|
} |
|
|
|
|
|
function renderNews(news) { |
|
|
const gridEl = document.getElementById('news-grid'); |
|
|
const filtered = filterNews(news); |
|
|
|
|
|
if (filtered.length === 0) { |
|
|
gridEl.innerHTML = ` |
|
|
<div style="grid-column: 1 / -1; text-align: center; padding: 60px; color: var(--text-muted);"> |
|
|
<div style="font-size: 3rem; margin-bottom: 16px;">🔍</div> |
|
|
<div>No news found matching your filters</div> |
|
|
</div> |
|
|
`; |
|
|
return; |
|
|
} |
|
|
|
|
|
gridEl.innerHTML = filtered.map((item, idx) => { |
|
|
const date = item.published_at || item.date || item.timestamp || new Date().toISOString(); |
|
|
const timeAgo = getTimeAgo(date); |
|
|
const sentiment = item.sentiment || 'neutral'; |
|
|
const sentimentColor = sentiment === 'positive' ? 'var(--success)' : |
|
|
sentiment === 'negative' ? 'var(--danger)' : 'var(--text-muted)'; |
|
|
const sentimentIcon = sentiment === 'positive' ? '📈' : |
|
|
sentiment === 'negative' ? '📉' : '➡️'; |
|
|
const symbols = Array.isArray(item.symbols) ? item.symbols : |
|
|
Array.isArray(item.coins) ? item.coins : |
|
|
Array.isArray(item.tags) ? item.tags : []; |
|
|
const source = item.source || 'Unknown'; |
|
|
|
|
|
return ` |
|
|
<div class="glass-card news-card" style="cursor: pointer; transition: all 0.3s;" onclick="openNewsModal(${idx})"> |
|
|
<div style="display: flex; justify-content: space-between; align-items: start; margin-bottom: 12px;"> |
|
|
<div style="display: flex; align-items: center; gap: 8px; flex-wrap: wrap;"> |
|
|
<span class="chip" style="font-size: 0.75rem; padding: 4px 10px;">${source}</span> |
|
|
<span style="color: var(--text-muted); font-size: 0.75rem;">${timeAgo}</span> |
|
|
</div> |
|
|
<span style="font-size: 1.25rem;">${sentimentIcon}</span> |
|
|
</div> |
|
|
<h3 style="margin: 0 0 12px 0; font-size: 1.125rem; font-weight: 700; color: var(--text-primary); line-height: 1.4;"> |
|
|
${(item.title || 'No title').substring(0, 100)}${(item.title || '').length > 100 ? '...' : ''} |
|
|
</h3> |
|
|
${item.summary ? `<p style="margin: 0 0 12px 0; color: var(--text-secondary); font-size: 0.875rem; line-height: 1.5;">${item.summary.substring(0, 120)}...</p>` : ''} |
|
|
<div style="display: flex; gap: 8px; flex-wrap: wrap; align-items: center;"> |
|
|
${symbols.slice(0, 3).map(s => `<span class="chip" style="font-size: 0.75rem; padding: 4px 10px; background: var(--primary-glow); color: var(--primary-light);">${s}</span>`).join('')} |
|
|
${symbols.length > 3 ? `<span style="color: var(--text-muted); font-size: 0.75rem;">+${symbols.length - 3} more</span>` : ''} |
|
|
<span style="margin-left: auto; color: ${sentimentColor}; font-size: 0.75rem; font-weight: 600; text-transform: capitalize;"> |
|
|
${sentiment} |
|
|
</span> |
|
|
</div> |
|
|
</div> |
|
|
`; |
|
|
}).join(''); |
|
|
|
|
|
|
|
|
window.filteredNews = filtered; |
|
|
|
|
|
|
|
|
document.querySelectorAll('.news-card').forEach(card => { |
|
|
card.addEventListener('mouseenter', function() { |
|
|
this.style.boxShadow = '0 12px 40px rgba(0, 0, 0, 0.4), var(--shadow-glow-primary)'; |
|
|
this.style.borderColor = 'var(--primary)'; |
|
|
}); |
|
|
card.addEventListener('mouseleave', function() { |
|
|
this.style.boxShadow = ''; |
|
|
this.style.borderColor = ''; |
|
|
}); |
|
|
}); |
|
|
} |
|
|
|
|
|
function filterNews(news) { |
|
|
return news.filter(item => { |
|
|
const title = (item.title || '').toLowerCase(); |
|
|
const searchMatch = !newsFilter.search || title.includes(newsFilter.search.toLowerCase()); |
|
|
const symbols = Array.isArray(item.symbols) ? item.symbols : |
|
|
Array.isArray(item.coins) ? item.coins : []; |
|
|
const symbolMatch = !newsFilter.symbol || |
|
|
symbols.some(s => s.toLowerCase().includes(newsFilter.symbol.toLowerCase())); |
|
|
return searchMatch && symbolMatch; |
|
|
}); |
|
|
} |
|
|
|
|
|
function getTimeAgo(dateStr) { |
|
|
try { |
|
|
const date = new Date(dateStr); |
|
|
const now = new Date(); |
|
|
const diff = now - date; |
|
|
const minutes = Math.floor(diff / 60000); |
|
|
const hours = Math.floor(diff / 3600000); |
|
|
const days = Math.floor(diff / 86400000); |
|
|
|
|
|
if (minutes < 1) return 'Just now'; |
|
|
if (minutes < 60) return `${minutes}m ago`; |
|
|
if (hours < 24) return `${hours}h ago`; |
|
|
if (days < 7) return `${days}d ago`; |
|
|
return date.toLocaleDateString(); |
|
|
} catch { |
|
|
return 'Recently'; |
|
|
} |
|
|
} |
|
|
|
|
|
function openNewsModal(index) { |
|
|
const item = window.filteredNews[index]; |
|
|
if (!item) return; |
|
|
|
|
|
const modal = document.querySelector('[data-news-modal]'); |
|
|
const content = document.querySelector('[data-news-modal-content]'); |
|
|
|
|
|
const sentiment = item.sentiment || 'neutral'; |
|
|
const sentimentColor = sentiment === 'positive' ? 'var(--success)' : |
|
|
sentiment === 'negative' ? 'var(--danger)' : 'var(--text-muted)'; |
|
|
|
|
|
content.innerHTML = ` |
|
|
<div style="margin-bottom: 20px;"> |
|
|
<div style="display: flex; justify-content: space-between; align-items: start; margin-bottom: 16px;"> |
|
|
<div> |
|
|
<span class="chip" style="margin-bottom: 8px; display: inline-block;">${item.source || 'Unknown'}</span> |
|
|
<div style="color: var(--text-muted); font-size: 0.875rem;">${getTimeAgo(item.published_at || item.date)}</div> |
|
|
</div> |
|
|
<span style="color: ${sentimentColor}; font-weight: 600; text-transform: capitalize;">${sentiment}</span> |
|
|
</div> |
|
|
<h2 style="font-size: 1.75rem; font-weight: 800; margin: 0 0 16px 0; color: var(--text-primary); line-height: 1.3;"> |
|
|
${item.title || 'No title'} |
|
|
</h2> |
|
|
</div> |
|
|
<div style="margin-bottom: 20px; color: var(--text-secondary); line-height: 1.6;"> |
|
|
${item.content || item.summary || item.description || 'No content available.'} |
|
|
</div> |
|
|
${item.url ? ` |
|
|
<div style="margin-top: 24px; padding-top: 20px; border-top: 1px solid var(--glass-border);"> |
|
|
<a href="${item.url}" target="_blank" class="btn-secondary" style="display: inline-flex; align-items: center; gap: 8px;"> |
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none"> |
|
|
<path d="M18 13v6a2 2 0 01-2 2H5a2 2 0 01-2-2V8a2 2 0 012-2h6M15 3h6v6M10 14L21 3" stroke="currentColor" stroke-width="2" stroke-linecap="round"/> |
|
|
</svg> |
|
|
Read Full Article |
|
|
</a> |
|
|
</div> |
|
|
` : ''} |
|
|
`; |
|
|
|
|
|
modal.style.display = 'flex'; |
|
|
} |
|
|
|
|
|
|
|
|
document.addEventListener('DOMContentLoaded', () => { |
|
|
|
|
|
const queryForm = document.querySelector('[data-query-form]'); |
|
|
if (queryForm) { |
|
|
queryForm.addEventListener('submit', async (e) => { |
|
|
e.preventDefault(); |
|
|
const formData = new FormData(queryForm); |
|
|
const query = formData.get('query'); |
|
|
const output = document.querySelector('[data-query-output]'); |
|
|
|
|
|
if (!query || !query.trim()) { |
|
|
output.innerHTML = '<div style="color: var(--text-muted);">Please enter a query</div>'; |
|
|
return; |
|
|
} |
|
|
|
|
|
output.innerHTML = '<div style="color: var(--text-secondary); display: flex; align-items: center; gap: 8px;"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2v4M12 18v4M4.93 4.93l2.83 2.83m8.48 8.48l2.83 2.83M2 12h4m12 0h4M4.93 19.07l2.83-2.83m8.48-8.48l2.83-2.83"/></svg> Processing...</div>'; |
|
|
|
|
|
try { |
|
|
const backendUrl = window.BACKEND_URL || window.location.origin; |
|
|
const res = await fetch(`${backendUrl}/api/query`, { |
|
|
method: 'POST', |
|
|
headers: { 'Content-Type': 'application/json' }, |
|
|
body: JSON.stringify({ query }) |
|
|
}); |
|
|
|
|
|
if (!res.ok) throw new Error(`HTTP ${res.status}`); |
|
|
|
|
|
const data = await res.json(); |
|
|
output.innerHTML = ` |
|
|
<div style="color: var(--text-primary); line-height: 1.6;"> |
|
|
<div style="font-weight: 600; margin-bottom: 8px; color: var(--primary-light);">Response:</div> |
|
|
<div>${data.response || data.answer || data.message || JSON.stringify(data, null, 2)}</div> |
|
|
</div> |
|
|
`; |
|
|
} catch (e) { |
|
|
output.innerHTML = `<div style="color: var(--danger);">Error: ${e.message}</div>`; |
|
|
} |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
const sentimentForm = document.querySelector('[data-sentiment-form]'); |
|
|
if (sentimentForm) { |
|
|
sentimentForm.addEventListener('submit', async (e) => { |
|
|
e.preventDefault(); |
|
|
const formData = new FormData(sentimentForm); |
|
|
const text = formData.get('text'); |
|
|
const output = document.getElementById('sentiment-results'); |
|
|
const outputContent = document.querySelector('[data-sentiment-output]'); |
|
|
|
|
|
if (!text || !text.trim()) { |
|
|
outputContent.innerHTML = '<div style="color: var(--text-muted);">Please enter text for analysis</div>'; |
|
|
return; |
|
|
} |
|
|
|
|
|
output.style.display = 'block'; |
|
|
outputContent.innerHTML = '<div style="color: var(--text-secondary); display: flex; align-items: center; gap: 8px;"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg> Analyzing sentiment...</div>'; |
|
|
|
|
|
try { |
|
|
const backendUrl = window.BACKEND_URL || window.location.origin; |
|
|
const res = await fetch(`${backendUrl}/api/sentiment/analyze`, { |
|
|
method: 'POST', |
|
|
headers: { 'Content-Type': 'application/json' }, |
|
|
body: JSON.stringify({ text }) |
|
|
}); |
|
|
|
|
|
if (!res.ok) throw new Error(`HTTP ${res.status}`); |
|
|
|
|
|
const data = await res.json(); |
|
|
const sentiment = data.sentiment || data.label || 'neutral'; |
|
|
const confidence = data.confidence || data.score || 0; |
|
|
const isPositive = sentiment.toLowerCase().includes('positive') || sentiment.toLowerCase().includes('bullish'); |
|
|
const isNegative = sentiment.toLowerCase().includes('negative') || sentiment.toLowerCase().includes('bearish'); |
|
|
|
|
|
const sentimentColor = isPositive ? 'var(--success)' : isNegative ? 'var(--danger)' : 'var(--text-muted)'; |
|
|
const sentimentIcon = isPositive ? '📈' : isNegative ? '📉' : '➡️'; |
|
|
const sentimentBg = isPositive ? 'rgba(52, 211, 153, 0.15)' : isNegative ? 'rgba(248, 113, 113, 0.15)' : 'var(--glass-bg-light)'; |
|
|
|
|
|
outputContent.innerHTML = ` |
|
|
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 20px; margin-bottom: 24px;"> |
|
|
<div style="padding: 20px; background: ${sentimentBg}; border-radius: 16px; border: 2px solid ${sentimentColor}; text-align: center;"> |
|
|
<div style="font-size: 3rem; margin-bottom: 12px;">${sentimentIcon}</div> |
|
|
<div style="font-size: 1.5rem; font-weight: 800; color: ${sentimentColor}; margin-bottom: 8px; text-transform: capitalize;">${sentiment}</div> |
|
|
<div style="font-size: 0.875rem; color: var(--text-muted);">Confidence: ${(confidence * 100).toFixed(1)}%</div> |
|
|
</div> |
|
|
<div style="padding: 20px; background: var(--glass-bg-light); border-radius: 16px; border: 1px solid var(--glass-border);"> |
|
|
<div style="font-weight: 600; color: var(--text-secondary); margin-bottom: 12px;">Details</div> |
|
|
<div style="color: var(--text-primary); line-height: 1.6; font-size: 0.9375rem;"> |
|
|
${data.explanation || data.reasoning || 'Analysis completed using ensemble AI models.'} |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
${data.models_used ? ` |
|
|
<div style="padding: 16px; background: var(--glass-bg-light); border-radius: 12px; border: 1px solid var(--glass-border);"> |
|
|
<div style="font-weight: 600; color: var(--text-secondary); margin-bottom: 8px; font-size: 0.875rem;">Models Used:</div> |
|
|
<div style="display: flex; gap: 8px; flex-wrap: wrap;"> |
|
|
${data.models_used.map(m => `<span class="chip" style="font-size: 0.75rem;">${m}</span>`).join('')} |
|
|
</div> |
|
|
</div> |
|
|
` : ''} |
|
|
`; |
|
|
} catch (e) { |
|
|
outputContent.innerHTML = `<div style="color: var(--danger);">Error: ${e.message}</div>`; |
|
|
} |
|
|
}); |
|
|
} |
|
|
}); |
|
|
|
|
|
|
|
|
document.addEventListener('DOMContentLoaded', () => { |
|
|
const newsPage = document.getElementById('page-news'); |
|
|
if (newsPage) { |
|
|
|
|
|
const observer = new MutationObserver((mutations) => { |
|
|
if (newsPage.classList.contains('active') && newsData.length === 0) { |
|
|
loadNews(); |
|
|
} |
|
|
}); |
|
|
observer.observe(newsPage, { attributes: true, attributeFilter: ['class'] }); |
|
|
|
|
|
|
|
|
document.getElementById('refresh-news')?.addEventListener('click', loadNews); |
|
|
|
|
|
|
|
|
document.querySelector('[data-news-search]')?.addEventListener('input', (e) => { |
|
|
newsFilter.search = e.target.value; |
|
|
renderNews(newsData); |
|
|
}); |
|
|
|
|
|
|
|
|
document.querySelector('[data-news-range]')?.addEventListener('change', (e) => { |
|
|
newsFilter.range = e.target.value; |
|
|
loadNews(); |
|
|
}); |
|
|
|
|
|
|
|
|
document.querySelector('[data-news-symbol]')?.addEventListener('input', (e) => { |
|
|
newsFilter.symbol = e.target.value; |
|
|
renderNews(newsData); |
|
|
}); |
|
|
|
|
|
|
|
|
document.querySelector('[data-close-news-modal]')?.addEventListener('click', () => { |
|
|
document.querySelector('[data-news-modal]').style.display = 'none'; |
|
|
}); |
|
|
|
|
|
|
|
|
document.querySelector('[data-news-modal]')?.addEventListener('click', (e) => { |
|
|
if (e.target === e.currentTarget) { |
|
|
e.currentTarget.style.display = 'none'; |
|
|
} |
|
|
}); |
|
|
} |
|
|
}); |
|
|
</script> |
|
|
|
|
|
|
|
|
<script src="config.js?v=20250119"></script> |
|
|
|
|
|
|
|
|
<script src="static/js/icons.js" defer></script> |
|
|
<script src="static/js/toast.js" defer></script> |
|
|
<script src="static/js/theme-manager.js" defer></script> |
|
|
<script src="static/js/animations.js" defer></script> |
|
|
<script src="static/js/uiUtils.js?v=20250119" defer></script> |
|
|
<script src="static/js/accessibility.js" defer></script> |
|
|
<script src="static/js/provider-discovery.js" defer></script> |
|
|
<script src="static/js/api-resource-loader.js?v=20250120" defer></script> |
|
|
|
|
|
|
|
|
<script> |
|
|
|
|
|
if (window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1') { |
|
|
if (window.DASHBOARD_CONFIG) { |
|
|
window.DASHBOARD_CONFIG.BACKEND_URL = `http://${window.location.hostname}:7860`; |
|
|
window.DASHBOARD_CONFIG.WS_URL = `ws://${window.location.hostname}:7860/ws`; |
|
|
} |
|
|
window.BACKEND_URL = `http://${window.location.hostname}:7860`; |
|
|
} |
|
|
console.log('[Init] Hostname:', window.location.hostname); |
|
|
console.log('[Init] Backend URL:', window.BACKEND_URL || window.DASHBOARD_CONFIG?.BACKEND_URL || 'not set'); |
|
|
</script> |
|
|
|
|
|
|
|
|
<script> |
|
|
|
|
|
document.addEventListener('DOMContentLoaded', () => { |
|
|
|
|
|
if (typeof ToastManager !== 'undefined') { |
|
|
window.toast = new ToastManager(); |
|
|
console.log('[Init] Toast manager initialized'); |
|
|
} |
|
|
|
|
|
|
|
|
if (typeof ThemeManager !== 'undefined') { |
|
|
window.themeManager = new ThemeManager(); |
|
|
window.themeManager.init(); |
|
|
console.log('[Init] Theme manager initialized'); |
|
|
} |
|
|
|
|
|
|
|
|
if (typeof AnimationController !== 'undefined') { |
|
|
window.animations = new AnimationController(); |
|
|
console.log('[Init] Animation controller initialized'); |
|
|
} |
|
|
|
|
|
|
|
|
if (typeof ProviderDiscoveryEngine !== 'undefined') { |
|
|
window.providerDiscovery = new ProviderDiscoveryEngine(); |
|
|
window.providerDiscovery.init().catch(e => { |
|
|
console.warn('[Init] Provider discovery failed:', e); |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
if (typeof AccessibilityManager !== 'undefined') { |
|
|
window.accessibility = new AccessibilityManager(); |
|
|
window.accessibility.init(); |
|
|
console.log('[Init] Accessibility manager initialized'); |
|
|
} |
|
|
|
|
|
|
|
|
if (window.apiResourceLoader && !window.apiResourceLoader.initialized) { |
|
|
window.apiResourceLoader.init().then(() => { |
|
|
const stats = window.apiResourceLoader.getStats(); |
|
|
|
|
|
if (stats.unified.count > 0 || stats.ultimate.count > 0) { |
|
|
console.log('[Init] API Resource Loader ready:', stats); |
|
|
|
|
|
|
|
|
if (window.toast) { |
|
|
window.toast.show( |
|
|
`Loaded ${stats.unified.count + stats.ultimate.count} API resources`, |
|
|
'success', |
|
|
{ title: 'Resources Loaded', duration: 3000 } |
|
|
); |
|
|
} |
|
|
} |
|
|
|
|
|
}).catch(() => { |
|
|
|
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
if (window.animations) { |
|
|
|
|
|
document.querySelectorAll('.glass-card, .stat-card').forEach((card, index) => { |
|
|
card.style.opacity = '0'; |
|
|
card.style.transform = 'translateY(20px)'; |
|
|
setTimeout(() => { |
|
|
card.style.transition = 'opacity 0.5s ease, transform 0.5s ease'; |
|
|
card.style.opacity = '1'; |
|
|
card.style.transform = 'translateY(0)'; |
|
|
}, index * 50); |
|
|
}); |
|
|
} |
|
|
}); |
|
|
</script> |
|
|
|
|
|
|
|
|
<script type="module" src="static/js/app.js?v=20250119"></script> |
|
|
</body> |
|
|
</html> |
|
|
|