Chotto.app
Login
Sign Up
ES
App Name
kuyawa / crypto
App Icon in SVG
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="100%" height="100%"> <circle cx="256" cy="256" r="240" fill="#F7931A"/> <path fill="#FFFFFF" d="M361.9 220.1c4.8-32.2-19.7-49.5-53.2-61.1l10.9-43.6-26.5-6.6-10.6 42.5c-7-.1-14.1-.1-21.3.1l10.7-42.8-26.5-6.6-10.9 43.6c-5.8-.1-11.5-.2-17.1-.1l-.1-.5-36.6-9.1-7.1 28.4s19.7 4.5 19.3 4.8c10.8 2.7 12.7 9.8 12.4 15.5l-12.4 49.8c.7.2 1.7.5 2.8.9-.9-.2-1.9-.5-2.8-.8l-17.4 69.8c-1.3 3.3-4.7 8.3-12.3 6.4.3.4-19.3-4.8-19.3-4.8l-13.2 30.5 34.5 8.6c6.4 1.6 12.8 3.3 19.1 4.9l-11 44.2 26.5 6.6 10.9-43.7c7.2 2 14.2 3.8 21.2 5.5l-10.8 43.3 26.5 6.6 11-44.1c45.2 8.6 79.2 5.1 93.5-35.8 11.5-32.9-0.6-51.9-24.4-64.3 17.3-4 30.3-15.4 33.8-38.9zm-60.6 85c-8.2 33-63.7 15.2-81.7 10.7l14.6-58.4c18 4.5 75.5 13.4 67.1 47.7zm8.2-85.5c-7.5 29.9-53.7 14.7-68.7 11l13.2-53c15 3.7 63.1 10.7 55.5 42z"/> </svg>
HTML Content
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Crypto</title> <style> /* ============================================================ CSS VARIABLES & THEMING ============================================================ */ :root { --bg-primary: #0d1117; --bg-secondary: #161b22; --bg-card: #1c2333; --bg-hover: #252d3f; --border-color: #30363d; --text-primary: #f0f6fc; --text-secondary: #8b949e; --text-muted: #6e7681; --accent-green: #3fb950; --accent-red: #f85149; --accent-blue: #58a6ff; --shadow: 0 8px 30px rgba(0, 0, 0, 0.6); --radius: 12px; --transition: 0.3s ease; } [data-theme="light"] { --bg-primary: #f6f8fa; --bg-secondary: #ffffff; --bg-card: #f0f2f5; --bg-hover: #e1e4e8; --border-color: #d0d7de; --text-primary: #1f2328; --text-secondary: #656d76; --text-muted: #8b949e; --shadow: 0 8px 30px rgba(0, 0, 0, 0.1); } /* ============================================================ RESET & BASE STYLES ============================================================ */ * { margin: 0; padding: 0; box-sizing: border-box; } body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif; background: var(--bg-primary); color: var(--text-primary); transition: background var(--transition), color var(--transition); height: 100vh; overflow: hidden; } /* ============================================================ APP LAYOUT ============================================================ */ .app { display: flex; height: 100vh; width: 100vw; overflow: hidden; } /* ============================================================ SIDEBAR ============================================================ */ .sidebar { width: 340px; min-width: 340px; background: var(--bg-secondary); border-right: 1px solid var(--border-color); display: flex; flex-direction: column; transition: background var(--transition), border-color var(--transition); overflow: hidden; } .sidebar-header { padding: 16px 20px 12px; border-bottom: 1px solid var(--border-color); display: flex; justify-content: space-between; align-items: center; flex-shrink: 0; } .sidebar-header h2 { font-size: 14px; font-weight: 600; color: var(--text-primary); display: flex; align-items: center; gap: 8px; } .sidebar-header h2 svg { width: 18px; height: 18px; fill: var(--accent-blue); } .sidebar-header .coin-count { font-size: 11px; color: var(--text-muted); background: var(--bg-card); padding: 2px 10px; border-radius: 12px; } .theme-toggle { background: var(--bg-card); border: 1px solid var(--border-color); border-radius: 8px; padding: 5px 8px; cursor: pointer; display: flex; align-items: center; justify-content: center; transition: all var(--transition); color: var(--text-secondary); } .theme-toggle:hover { background: var(--bg-hover); border-color: var(--text-secondary); } .theme-toggle svg { width: 16px; height: 16px; fill: currentColor; } /* Search */ .search-box { padding: 10px 16px; flex-shrink: 0; } .search-box input { width: 100%; padding: 6px 12px; background: var(--bg-primary); border: 1px solid var(--border-color); border-radius: 6px; color: var(--text-primary); font-size: 12px; transition: all var(--transition); outline: none; } .search-box input:focus { border-color: var(--accent-blue); box-shadow: 0 0 0 3px rgba(88, 166, 255, 0.15); } .search-box input::placeholder { color: var(--text-muted); } /* Coin List */ .coin-list { flex: 1; overflow-y: auto; padding: 6px 10px 10px; } .coin-list::-webkit-scrollbar { width: 4px; } .coin-list::-webkit-scrollbar-track { background: transparent; } .coin-list::-webkit-scrollbar-thumb { background: var(--border-color); border-radius: 4px; } .coin-item { display: flex; align-items: center; padding: 8px 10px; border-radius: 6px; cursor: pointer; transition: all var(--transition); gap: 10px; position: relative; border: 1px solid transparent; } .coin-item:hover { background: var(--bg-hover); } .coin-item.active { background: var(--bg-hover); border-color: var(--accent-blue); } .coin-item .rank { font-size: 10px; font-weight: 600; color: var(--text-muted); min-width: 22px; text-align: center; } .coin-item .icon { width: 28px; height: 28px; flex-shrink: 0; border-radius: 50%; display: flex; align-items: center; justify-content: center; background: var(--bg-card); font-weight: 600; font-size: 12px; color: var(--text-primary); } .coin-item .icon svg { width: 20px; height: 20px; fill: currentColor; } .coin-item .info { flex: 1; min-width: 0; } .coin-item .info .symbol { font-weight: 600; font-size: 13px; color: var(--text-primary); } .coin-item .info .name { font-size: 11px; color: var(--text-secondary); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .coin-item .price-info { text-align: right; flex-shrink: 0; } .coin-item .price-info .price { font-size: 13px; font-weight: 500; color: var(--text-primary); } .coin-item .price-info .change { font-size: 11px; font-weight: 500; } .coin-item .price-info .change.positive { color: var(--accent-green); } .coin-item .price-info .change.negative { color: var(--accent-red); } .coin-item .market-cap { font-size: 9px; color: var(--text-muted); text-align: right; } .loading-spinner { text-align: center; padding: 40px 20px; color: var(--text-secondary); font-size: 13px; } .loading-spinner svg { animation: spin 1s linear infinite; width: 22px; height: 22px; fill: var(--text-secondary); margin-bottom: 8px; } @keyframes spin { 100% { transform: rotate(360deg); } } /* ============================================================ CHART AREA ============================================================ */ .chart-area { flex: 1; display: flex; flex-direction: column; background: var(--bg-primary); transition: background var(--transition); min-width: 0; overflow: hidden; } .chart-header { padding: 16px 20px 10px; border-bottom: 1px solid var(--border-color); display: flex; justify-content: space-between; align-items: center; flex-shrink: 0; flex-wrap: wrap; gap: 10px; } .chart-header .left { display: flex; align-items: center; gap: 14px; flex-wrap: wrap; } .chart-header .left .coin-title { font-size: 18px; font-weight: 600; } .chart-header .left .coin-title small { font-weight: 400; font-size: 13px; color: var(--text-secondary); margin-left: 6px; } .chart-header .left .current-price { font-size: 16px; font-weight: 500; } .chart-header .left .current-change { font-size: 13px; font-weight: 500; padding: 1px 10px; border-radius: 12px; } .chart-header .left .current-change.positive { color: var(--accent-green); background: rgba(63, 185, 80, 0.12); } .chart-header .left .current-change.negative { color: var(--accent-red); background: rgba(248, 81, 73, 0.12); } .chart-header .left .market-cap-display { font-size: 12px; color: var(--text-muted); } .chart-controls { display: flex; gap: 4px; align-items: center; flex-wrap: wrap; } .chart-controls button { background: var(--bg-card); border: 1px solid var(--border-color); border-radius: 5px; padding: 3px 10px; color: var(--text-secondary); font-size: 11px; cursor: pointer; transition: all var(--transition); font-family: inherit; } .chart-controls button:hover { background: var(--bg-hover); color: var(--text-primary); } .chart-controls button.active { background: var(--accent-blue); color: white; border-color: var(--accent-blue); } .chart-wrapper { flex: 1; position: relative; padding: 12px 16px 16px; min-height: 0; } .chart-wrapper canvas { display: block; width: 100%; height: 100%; border-radius: 8px; background: var(--bg-secondary); border: 1px solid var(--border-color); cursor: crosshair; transition: background var(--transition), border-color var(--transition); } #tooltip { position: absolute; display: none; background: var(--bg-secondary); border: 1px solid var(--border-color); border-radius: 8px; padding: 8px 12px; color: var(--text-primary); font-size: 12px; pointer-events: none; min-width: 130px; box-shadow: var(--shadow); line-height: 1.5; z-index: 10; transition: background var(--transition), border-color var(--transition); } #tooltip .price-up { color: var(--accent-green); } #tooltip .price-down { color: var(--accent-red); } #tooltip .label { color: var(--text-secondary); font-size: 10px; text-transform: uppercase; letter-spacing: 0.4px; } .chart-placeholder { display: flex; align-items: center; justify-content: center; height: 100%; color: var(--text-secondary); font-size: 14px; flex-direction: column; gap: 10px; } .chart-placeholder svg { width: 40px; height: 40px; fill: var(--text-muted); opacity: 0.5; } /* ============================================================ RESPONSIVE ============================================================ */ @media (max-width: 1024px) { .sidebar { width: 280px; min-width: 280px; } } @media (max-width: 800px) { .app { flex-direction: column; height: 100vh; overflow: hidden; } .sidebar { width: 100%; min-width: unset; max-height: 40vh; border-right: none; border-bottom: 1px solid var(--border-color); } .chart-area { height: 60vh; } .chart-header .left .coin-title { font-size: 15px; } .chart-header .left .current-price { font-size: 14px; } } @media (max-width: 600px) { .sidebar-header h2 { font-size: 12px; } .sidebar-header .coin-count { font-size: 10px; padding: 1px 8px; } .coin-item { padding: 6px 8px; } .coin-item .info .symbol { font-size: 12px; } .coin-item .price-info .price { font-size: 12px; } .chart-header { padding: 10px 12px 6px; } .chart-wrapper { padding: 6px 10px 10px; } .chart-controls button { padding: 2px 7px; font-size: 10px; } .chart-header .left { gap: 8px; } .chart-header .left .market-cap-display { font-size: 10px; } } @media (max-width: 480px) { .sidebar { max-height: 35vh; } .chart-area { height: 65vh; } .coin-item .icon { width: 22px; height: 22px; font-size: 10px; } .coin-item .icon svg { width: 16px; height: 16px; } } </style> </head> <body> <div class="app"> <!-- ========================================================== SIDEBAR ========================================================== --> <aside class="sidebar" id="sidebar"> <div class="sidebar-header"> <h2> <svg viewBox="0 0 24 24"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-1 14H9V8h2v8zm4 0h-2V8h2v8z"/></svg> Crypto <span class="coin-count" id="coinCount">100</span> </h2> <button class="theme-toggle" id="themeToggle" title="Toggle theme"> <svg viewBox="0 0 24 24"><path d="M12 7c-2.76 0-5 2.24-5 5s2.24 5 5 5 5-2.24 5-5-2.24-5-5-5zM2 13h2c.55 0 1-.45 1-1s-.45-1-1-1H2c-.55 0-1 .45-1 1s.45 1 1 1zm18 0h2c.55 0 1-.45 1-1s-.45-1-1-1h-2c-.55 0-1 .45-1 1s.45 1 1 1zM11 2v2c0 .55.45 1 1 1s1-.45 1-1V2c0-.55-.45-1-1-1s-1 .45-1 1zm0 18v2c0 .55.45 1 1 1s1-.45 1-1v-2c0-.55-.45-1-1-1s-1 .45-1 1zM5.99 4.58c-.39-.39-1.03-.39-1.41 0-.39.39-.39 1.03 0 1.41l1.06 1.06c.39.39 1.03.39 1.41 0s.39-1.03 0-1.41L5.99 4.58zm12.37 12.37c-.39-.39-1.03-.39-1.41 0-.39.39-.39 1.03 0 1.41l1.06 1.06c.39.39 1.03.39 1.41 0 .39-.39.39-1.03 0-1.41l-1.06-1.06zm1.06-10.96c.39-.39.39-1.03 0-1.41-.39-.39-1.03-.39-1.41 0l-1.06 1.06c-.39.39-.39 1.03 0 1.41s1.03.39 1.41 0l1.06-1.06zM7.05 18.36c.39-.39.39-1.03 0-1.41-.39-.39-1.03-.39-1.41 0l-1.06 1.06c-.39.39-.39 1.03 0 1.41s1.03.39 1.41 0l1.06-1.06z"/></svg> </button> </div> <div class="search-box"> <input type="text" id="searchInput" placeholder="Search coins..." /> </div> <div class="coin-list" id="coinList"> <div class="loading-spinner"> <svg viewBox="0 0 24 24"><path d="M12 4V1L8 5l4 4V6c3.31 0 6 2.69 6 6s-2.69 6-6 6-6-2.69-6-6H4c0 4.42 3.58 8 8 8s8-3.58 8-8-3.58-8-8-8z"/></svg> <div>Loading top 100 cryptos...</div> </div> </div> </aside> <!-- ========================================================== CHART AREA ========================================================== --> <main class="chart-area"> <div class="chart-header"> <div class="left"> <div class="coin-title"> <span id="chartSymbol">BTC</span> <small id="chartName">Bitcoin</small> </div> <div class="current-price" id="currentPrice">—</div> <div class="current-change" id="currentChange">—</div> <div class="market-cap-display" id="marketCapDisplay">MC: —</div> </div> <div class="chart-controls"> <button data-interval="15m" class="active">15m</button> <button data-interval="1h">1h</button> <button data-interval="4h">4h</button> <button data-interval="1d">1d</button> <button id="resetViewBtn" title="Reset Zoom">⟲</button> </div> </div> <div class="chart-wrapper" id="chartWrapper"> <canvas id="chartCanvas"></canvas> <div id="tooltip"></div> <div class="chart-placeholder" id="placeholder"> <svg viewBox="0 0 24 24"><path d="M3.5 18.49l6-6.01 4 4L22 6.92l-1.41-1.41-7.09 7.97-4-4L2 16.99z"/></svg> <span>Select a cryptocurrency to view chart</span> </div> </div> </main> </div> <script> // ================================================================ // 1. COIN ICONS (SVG) - Expanded // ================================================================ const COIN_ICONS = { 'BTC': `<svg viewBox="0 0 24 24"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 13h-2V9h2v6zm-4-6v6H7V9h2zm6 0v6h-2V9h2z" fill="#f7931a"/></svg>`, 'ETH': `<svg viewBox="0 0 24 24"><path d="M12 2L4 12l8 10 8-10L12 2zm0 4.5l4.5 7.5H7.5L12 6.5z" fill="#627eea"/></svg>`, 'USDT': `<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="10" fill="#26a17b"/><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 14.5v-2.2c2.5-.4 4-1.6 4-3.2 0-1.8-1.8-2.8-4-3.1V6h-2v2.1c-2.2.3-4 1.3-4 3.1 0 1.6 1.5 2.8 4 3.2V16.5H9v2h6v-2h-2z" fill="white"/></svg>`, 'BNB': `<svg viewBox="0 0 24 24"><path d="M12 2L4 6l8 4 8-4-8-4zM4 14l8 4 8-4-8-4-8 4zM4 10l8 4 8-4-8-4-8 4z" fill="#f3ba2f"/></svg>`, 'SOL': `<svg viewBox="0 0 24 24"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 4.5L7.5 11.5 12 16.5l4.5-5L12 6.5z" fill="#9945ff"/></svg>`, 'XRP': `<svg viewBox="0 0 24 24"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 4.5L7.5 11.5 12 16.5l4.5-5L12 6.5z" fill="#00aae4"/></svg>`, 'ADA': `<svg viewBox="0 0 24 24"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 4.5L7.5 11.5 12 16.5l4.5-5L12 6.5z" fill="#0033ad"/></svg>`, 'DOGE': `<svg viewBox="0 0 24 24"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 4.5L7.5 11.5 12 16.5l4.5-5L12 6.5z" fill="#c2a633"/></svg>`, 'DOT': `<svg viewBox="0 0 24 24"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 4.5L7.5 11.5 12 16.5l4.5-5L12 6.5z" fill="#e6007a"/></svg>`, 'LINK': `<svg viewBox="0 0 24 24"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 4.5L7.5 11.5 12 16.5l4.5-5L12 6.5z" fill="#2a5ada"/></svg>`, 'UNI': `<svg viewBox="0 0 24 24"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 4.5L7.5 11.5 12 16.5l4.5-5L12 6.5z" fill="#ff007a"/></svg>`, 'MATIC': `<svg viewBox="0 0 24 24"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 4.5L7.5 11.5 12 16.5l4.5-5L12 6.5z" fill="#8247e5"/></svg>`, 'LTC': `<svg viewBox="0 0 24 24"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 4.5L7.5 11.5 12 16.5l4.5-5L12 6.5z" fill="#bfbbbb"/></svg>`, 'BCH': `<svg viewBox="0 0 24 24"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 4.5L7.5 11.5 12 16.5l4.5-5L12 6.5z" fill="#8dc351"/></svg>`, 'AVAX': `<svg viewBox="0 0 24 24"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 4.5L7.5 11.5 12 16.5l4.5-5L12 6.5z" fill="#e84142"/></svg>`, 'TRX': `<svg viewBox="0 0 24 24"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-1 5h2v10h-2V7z" fill="#ef0027"/></svg>`, 'XLM': `<svg viewBox="0 0 24 24"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-1 5h2v10h-2V7z" fill="#14b4e7"/></svg>`, 'ATOM': `<svg viewBox="0 0 24 24"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 4.5L7.5 11.5 12 16.5l4.5-5L12 6.5z" fill="#2e3148"/></svg>`, 'ETC': `<svg viewBox="0 0 24 24"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 4.5L7.5 11.5 12 16.5l4.5-5L12 6.5z" fill="#328a6d"/></svg>`, 'FIL': `<svg viewBox="0 0 24 24"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-1 5h2v10h-2V7z" fill="#0090ff"/></svg>`, 'VET': `<svg viewBox="0 0 24 24"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-1 5h2v10h-2V7z" fill="#15bdff"/></svg>`, 'ICP': `<svg viewBox="0 0 24 24"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 4.5L7.5 11.5 12 16.5l4.5-5L12 6.5z" fill="#3b00b9"/></svg>`, 'FTM': `<svg viewBox="0 0 24 24"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 4.5L7.5 11.5 12 16.5l4.5-5L12 6.5z" fill="#1969ff"/></svg>`, 'ARB': `<svg viewBox="0 0 24 24"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 4.5L7.5 11.5 12 16.5l4.5-5L12 6.5z" fill="#2d374b"/></svg>`, 'NEAR': `<svg viewBox="0 0 24 24"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 4.5L7.5 11.5 12 16.5l4.5-5L12 6.5z" fill="#000000"/></svg>`, 'OP': `<svg viewBox="0 0 24 24"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 4.5L7.5 11.5 12 16.5l4.5-5L12 6.5z" fill="#ff0420"/></svg>`, 'SUI': `<svg viewBox="0 0 24 24"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 4.5L7.5 11.5 12 16.5l4.5-5L12 6.5z" fill="#4da2ff"/></svg>`, 'INJ': `<svg viewBox="0 0 24 24"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 4.5L7.5 11.5 12 16.5l4.5-5L12 6.5z" fill="#00afe0"/></svg>`, }; function getCoinIcon(symbol) { const upper = symbol.toUpperCase(); if (COIN_ICONS[upper]) return COIN_ICONS[upper]; // Fallback: first 2 letters const letters = upper.slice(0, 2); return `<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="10" fill="var(--text-muted)" opacity="0.3"/><text x="12" y="16" text-anchor="middle" fill="var(--text-primary)" font-size="9" font-weight="600">${letters}</text></svg>`; } // ================================================================ // 2. STATE // ================================================================ const state = { coins: [], selectedSymbol: null, chartData: [], interval: '15m', limit: 100, theme: 'dark', searchQuery: '', chartInstance: null, }; // ================================================================ // 3. API HELPERS // ================================================================ async function fetchTopCoins() { const res = await fetch('https://www.binance.com/bapi/asset/v2/public/asset-service/product/get-products'); if (!res.ok) throw new Error(`HTTP ${res.status}`); const json = await res.json(); if (json.code !== '000000') throw new Error(json.message || 'API error'); // Filter for USDT pairs and coins with positive circulating supply const usdtPairs = json.data.filter(item => item.q === 'USDT' && item.cs > 0 && parseFloat(item.c) > 0 && item.s !== 'USDT' && // Exclude USDT itself !item.tags.includes('bStocks') && !item.tags.includes('stablecoin') ); // Calculate market cap: cs * c (circulating supply * close price) const withMarketCap = usdtPairs.map(item => ({ symbol: item.b, name: item.an || item.b, price: parseFloat(item.c), change24h: parseFloat(item.p || 0), volume: parseFloat(item.v || 0), high: parseFloat(item.h || 0), low: parseFloat(item.l || 0), circulatingSupply: item.cs, marketCap: item.cs * parseFloat(item.c), raw: item, })); // Sort by market cap descending withMarketCap.sort((a, b) => b.marketCap - a.marketCap); // Take top 100 return withMarketCap.slice(0, 100); } async function fetchKlines(symbol, interval, limit = 100) { const url = `https://api.binance.com/api/v3/klines?symbol=${symbol}USDT&interval=${interval}&limit=${limit}`; const res = await fetch(url); if (!res.ok) throw new Error(`HTTP ${res.status}`); const raw = await res.json(); if (!Array.isArray(raw) || raw.length === 0) { throw new Error('No kline data received'); } return raw.map(k => ({ time: new Date(k[0]), open: parseFloat(k[1]), high: parseFloat(k[2]), low: parseFloat(k[3]), close: parseFloat(k[4]), volume: parseFloat(k[5]) })); } // ================================================================ // 4. CHART RENDERER (Canvas-based) // ================================================================ class CandlestickChart { constructor(canvasId, tooltipId, placeholderId) { this.canvas = document.getElementById(canvasId); this.ctx = this.canvas.getContext('2d'); this.tooltip = document.getElementById(tooltipId); this.placeholder = document.getElementById(placeholderId); this.data = []; this.visibleRange = { startIdx: 0, endIdx: 0 }; this.viewOffset = 0; this.zoomLevel = 1; this.isDragging = false; this.dragStartX = 0; this.dragStartOffset = 0; this.setupEvents(); this.resizeObserver = new ResizeObserver(() => this.render()); this.resizeObserver.observe(this.canvas); } setData(data) { this.data = data || []; this.resetView(); this.render(); } resetView() { this.zoomLevel = 1; this.viewOffset = 0; this.updateVisibleRange(); this.render(); } updateVisibleRange() { const total = this.data.length; if (total === 0) { this.visibleRange = { startIdx: 0, endIdx: 0 }; return; } const visible = Math.max(1, Math.floor(total / this.zoomLevel)); const start = Math.max(0, Math.min( Math.floor(this.viewOffset), total - visible )); const end = Math.min(total, start + visible); this.visibleRange = { startIdx: start, endIdx: end }; return this.visibleRange; } getVisibleData() { const { startIdx, endIdx } = this.visibleRange; return this.data.slice(startIdx, endIdx); } getPriceRange(data) { if (!data || data.length === 0) return { min: 0, max: 1 }; let min = Infinity, max = -Infinity; for (const d of data) { if (d.low < min) min = d.low; if (d.high > max) max = d.high; } const padding = (max - min) * 0.08 || 1; return { min: min - padding, max: max + padding }; } render() { const rect = this.canvas.getBoundingClientRect(); const dpr = window.devicePixelRatio || 1; if (rect.width === 0 || rect.height === 0) { this.placeholder.style.display = 'flex'; return; } this.canvas.width = rect.width * dpr; this.canvas.height = rect.height * dpr; this.canvas.style.width = rect.width + 'px'; this.canvas.style.height = rect.height + 'px'; this.ctx.scale(dpr, dpr); const W = rect.width; const H = rect.height; const ctx = this.ctx; ctx.clearRect(0, 0, W, H); this.placeholder.style.display = 'none'; if (!this.data || this.data.length === 0) { this.placeholder.style.display = 'flex'; return; } const visibleData = this.getVisibleData(); if (visibleData.length === 0) { ctx.fillStyle = 'var(--text-secondary)'; ctx.font = '13px sans-serif'; ctx.textAlign = 'center'; ctx.fillText('Zoomed out of range', W / 2, H / 2); return; } const isDark = document.documentElement.getAttribute('data-theme') !== 'light'; const bgColor = isDark ? '#161b22' : '#ffffff'; const gridColor = isDark ? '#21262d' : '#e1e4e8'; const textColor = isDark ? '#8b949e' : '#656d76'; const green = '#3fb950'; const red = '#f85149'; const priceRange = this.getPriceRange(visibleData); const padding = { top: 18, bottom: 26, left: 56, right: 14 }; const chartW = W - padding.left - padding.right; const chartH = H - padding.top - padding.bottom; const candleWidth = Math.min( chartW / visibleData.length * 0.65, 28 ); const candleSpacing = chartW / visibleData.length; ctx.fillStyle = bgColor; ctx.fillRect(0, 0, W, H); // Grid ctx.strokeStyle = gridColor; ctx.lineWidth = 0.5; const gridLines = 5; for (let i = 0; i <= gridLines; i++) { const y = padding.top + (chartH / gridLines) * i; ctx.beginPath(); ctx.moveTo(padding.left, y); ctx.lineTo(W - padding.right, y); ctx.stroke(); const price = priceRange.max - (priceRange.max - priceRange.min) * (i / gridLines); ctx.fillStyle = textColor; ctx.font = '9px sans-serif'; ctx.textAlign = 'right'; ctx.textBaseline = 'middle'; const priceStr = price >= 100 ? price.toFixed(0) : price >= 1 ? price.toFixed(2) : price.toFixed(4); ctx.fillText('$' + priceStr, padding.left - 5, y); } // Candles for (let i = 0; i < visibleData.length; i++) { const d = visibleData[i]; const x = padding.left + i * candleSpacing + candleSpacing / 2; const yHigh = padding.top + chartH - ((d.high - priceRange.min) / (priceRange.max - priceRange .min)) * chartH; const yLow = padding.top + chartH - ((d.low - priceRange.min) / (priceRange.max - priceRange .min)) * chartH; const yOpen = padding.top + chartH - ((d.open - priceRange.min) / (priceRange.max - priceRange .min)) * chartH; const yClose = padding.top + chartH - ((d.close - priceRange.min) / (priceRange.max - priceRange .min)) * chartH; const isBullish = d.close >= d.open; const color = isBullish ? green : red; ctx.strokeStyle = color; ctx.lineWidth = 1; ctx.beginPath(); ctx.moveTo(x, yHigh); ctx.lineTo(x, yLow); ctx.stroke(); const bodyTop = Math.min(yOpen, yClose); const bodyBottom = Math.max(yOpen, yClose); const bodyHeight = Math.max(bodyBottom - bodyTop, 1); ctx.fillStyle = color; ctx.fillRect( x - candleWidth / 2, bodyTop, candleWidth, bodyHeight ); if (!isBullish) { ctx.strokeStyle = color; ctx.lineWidth = 0.5; ctx.strokeRect( x - candleWidth / 2, bodyTop, candleWidth, bodyHeight ); } } // X-axis labels ctx.fillStyle = textColor; ctx.font = '8px sans-serif'; ctx.textAlign = 'center'; ctx.textBaseline = 'top'; const labelStep = Math.max(1, Math.floor(visibleData.length / 8)); for (let i = 0; i < visibleData.length; i += labelStep) { const d = visibleData[i]; const x = padding.left + i * candleSpacing + candleSpacing / 2; let label; if (state.interval === '1d') { label = d.time.toLocaleDateString([], { month: 'short', day: 'numeric' }); } else { label = d.time.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); } ctx.fillText(label, x, H - padding.bottom + 3); } } setupEvents() { const canvas = this.canvas; canvas.addEventListener('wheel', (e) => { e.preventDefault(); const delta = e.deltaY > 0 ? 1.1 : 1 / 1.1; this.zoomLevel = Math.max(1, Math.min(50, this.zoomLevel * delta)); this.updateVisibleRange(); this.render(); }); canvas.addEventListener('mousedown', (e) => { this.isDragging = true; this.dragStartX = e.clientX; this.dragStartOffset = this.viewOffset; canvas.style.cursor = 'grabbing'; }); canvas.addEventListener('mousemove', (e) => { if (this.isDragging) { const rect = this.canvas.getBoundingClientRect(); const dx = (e.clientX - this.dragStartX) / rect.width; const total = this.data.length; const visible = Math.max(1, Math.floor(total / this.zoomLevel)); const deltaCandles = -dx * visible; this.viewOffset = Math.max(0, Math.min( total - visible, this.dragStartOffset + deltaCandles )); this.updateVisibleRange(); this.render(); this.tooltip.style.display = 'none'; return; } this.showTooltip(e); }); canvas.addEventListener('mouseup', () => { if (this.isDragging) { this.isDragging = false; canvas.style.cursor = 'crosshair'; } }); canvas.addEventListener('mouseleave', () => { this.isDragging = false; canvas.style.cursor = 'crosshair'; this.tooltip.style.display = 'none'; }); // Touch let touchStartX = 0; let touchStartOffset = 0; let touchDist = 0; canvas.addEventListener('touchstart', (e) => { e.preventDefault(); if (e.touches.length === 1) { touchStartX = e.touches[0].clientX; touchStartOffset = this.viewOffset; this.isDragging = true; } else if (e.touches.length === 2) { touchDist = Math.hypot( e.touches[0].clientX - e.touches[1].clientX, e.touches[0].clientY - e.touches[1].clientY ); } }, { passive: false }); canvas.addEventListener('touchmove', (e) => { e.preventDefault(); if (e.touches.length === 1 && this.isDragging) { const rect = this.canvas.getBoundingClientRect(); const dx = (e.touches[0].clientX - touchStartX) / rect.width; const total = this.data.length; const visible = Math.max(1, Math.floor(total / this.zoomLevel)); const deltaCandles = -dx * visible; this.viewOffset = Math.max(0, Math.min( total - visible, touchStartOffset + deltaCandles )); this.updateVisibleRange(); this.render(); this.tooltip.style.display = 'none'; } else if (e.touches.length === 2) { const newDist = Math.hypot( e.touches[0].clientX - e.touches[1].clientX, e.touches[0].clientY - e.touches[1].clientY ); const delta = newDist / touchDist; this.zoomLevel = Math.max(1, Math.min(50, this.zoomLevel * delta)); touchDist = newDist; this.updateVisibleRange(); this.render(); } }, { passive: false }); canvas.addEventListener('touchend', (e) => { this.isDragging = false; this.tooltip.style.display = 'none'; }); } showTooltip(e) { const rect = this.canvas.getBoundingClientRect(); const W = rect.width; const padding = { top: 18, bottom: 26, left: 56, right: 14 }; const chartW = W - padding.left - padding.right; const visibleData = this.getVisibleData(); if (visibleData.length === 0) { this.tooltip.style.display = 'none'; return; } const candleSpacing = chartW / visibleData.length; const mouseX = e.clientX - rect.left; let idx = Math.floor((mouseX - padding.left) / candleSpacing); idx = Math.max(0, Math.min(visibleData.length - 1, idx)); const d = visibleData[idx]; if (!d) { this.tooltip.style.display = 'none'; return; } const isBullish = d.close >= d.open; const priceClass = isBullish ? 'price-up' : 'price-down'; this.tooltip.innerHTML = ` <div class="label">${d.time.toLocaleString()}</div> <div>O: <span class="${priceClass}">$${d.open.toFixed(2)}</span></div> <div>H: <span class="${priceClass}">$${d.high.toFixed(2)}</span></div> <div>L: <span class="${priceClass}">$${d.low.toFixed(2)}</span></div> <div>C: <span class="${priceClass}">$${d.close.toFixed(2)}</span></div> <div style="color:var(--text-secondary);font-size:10px;">Vol: ${(d.volume/1000).toFixed(1)}K</div> `; this.tooltip.style.display = 'block'; const xPos = padding.left + idx * candleSpacing + candleSpacing / 2; let left = xPos + padding.left + 10; let top = e.clientY - rect.top - 20; const tipRect = this.tooltip.getBoundingClientRect(); if (left + tipRect.width > rect.width) { left = xPos + padding.left - tipRect.width - 10; } if (top + tipRect.height > rect.height) { top = rect.height - tipRect.height - 10; } if (top < 10) top = 10; this.tooltip.style.left = left + 'px'; this.tooltip.style.top = top + 'px'; } } // ================================================================ // 5. APP CONTROLLER // ================================================================ let chartInstance = null; async function initApp() { chartInstance = new CandlestickChart('chartCanvas', 'tooltip', 'placeholder'); const savedTheme = localStorage.getItem('crypto-dashboard-theme') || 'dark'; setTheme(savedTheme); await loadCoins(); document.getElementById('themeToggle').addEventListener('click', toggleTheme); document.getElementById('searchInput').addEventListener('input', filterCoins); document.getElementById('resetViewBtn').addEventListener('click', () => { if (chartInstance) chartInstance.resetView(); }); document.querySelectorAll('[data-interval]').forEach(btn => { btn.addEventListener('click', () => { document.querySelectorAll('[data-interval]').forEach(b => b.classList.remove('active')); btn.classList.add('active'); state.interval = btn.dataset.interval; if (state.selectedSymbol) { loadChart(state.selectedSymbol); } }); }); let resizeTimeout; window.addEventListener('resize', () => { clearTimeout(resizeTimeout); resizeTimeout = setTimeout(() => { if (chartInstance) chartInstance.render(); }, 200); }); } async function loadCoins() { const list = document.getElementById('coinList'); try { const coins = await fetchTopCoins(); state.coins = coins; document.getElementById('coinCount').textContent = coins.length; renderCoinList(coins); if (coins.length > 0) { selectCoin(coins[0].symbol); } } catch (err) { console.error('Failed to load coins:', err); list.innerHTML = ` <div style="color:var(--accent-red);padding:20px;text-align:center;font-size:13px;"> ❌ Failed to load top cryptos<br> <small style="color:var(--text-secondary);">${err.message}</small> <br><br> <button onclick="loadCoins()" style="background:var(--bg-card);border:1px solid var(--border-color);color:var(--text-primary);padding:5px 14px;border-radius:6px;cursor:pointer;font-size:12px;">Retry</button> </div> `; } } function renderCoinList(coins, filter = '') { const list = document.getElementById('coinList'); const search = filter.toLowerCase().trim(); let filtered = coins; if (search) { filtered = coins.filter(c => c.symbol.toLowerCase().includes(search) || c.name.toLowerCase().includes(search) ); } if (filtered.length === 0) { list.innerHTML = `<div style="color:var(--text-secondary);padding:20px;text-align:center;font-size:13px;">No coins found</div>`; return; } list.innerHTML = filtered.map((coin, index) => { const isActive = state.selectedSymbol === coin.symbol; const changeClass = coin.change24h >= 0 ? 'positive' : 'negative'; const changeSign = coin.change24h >= 0 ? '+' : ''; const icon = getCoinIcon(coin.symbol); const priceStr = coin.price >= 100 ? coin.price.toFixed(0) : coin.price >= 1 ? coin.price.toFixed(2) : coin.price.toFixed(4); const mcStr = coin.marketCap >= 1e9 ? (coin.marketCap / 1e9).toFixed(1) + 'B' : (coin.marketCap / 1e6).toFixed(1) + 'M'; return ` <div class="coin-item ${isActive ? 'active' : ''}" data-symbol="${coin.symbol}" onclick="selectCoin('${coin.symbol}')"> <span class="rank">#${index + 1}</span> <div class="icon">${icon}</div> <div class="info"> <div class="symbol">${coin.symbol}</div> <div class="name">${coin.name}</div> </div> <div class="price-info"> <div class="price">$${priceStr}</div> <div class="change ${changeClass}">${changeSign}${coin.change24h.toFixed(2)}%</div> <div class="market-cap">$${mcStr}</div> </div> </div> `; }).join(''); } function filterCoins(e) { state.searchQuery = e.target.value; renderCoinList(state.coins, state.searchQuery); } async function selectCoin(symbol) { state.selectedSymbol = symbol; renderCoinList(state.coins, state.searchQuery); await loadChart(symbol); } async function loadChart(symbol) { const placeholder = document.getElementById('placeholder'); try { const coin = state.coins.find(c => c.symbol === symbol); if (coin) { document.getElementById('chartSymbol').textContent = symbol; document.getElementById('chartName').textContent = coin.name; const priceStr = coin.price >= 100 ? coin.price.toFixed(0) : coin.price >= 1 ? coin.price.toFixed(2) : coin.price.toFixed(4); document.getElementById('currentPrice').textContent = '$' + priceStr; const change = document.getElementById('currentChange'); change.textContent = (coin.change24h >= 0 ? '+' : '') + coin.change24h.toFixed(2) + '%'; change.className = 'current-change ' + (coin.change24h >= 0 ? 'positive' : 'negative'); const mcStr = coin.marketCap >= 1e9 ? (coin.marketCap / 1e9).toFixed(2) + 'B' : (coin.marketCap / 1e6).toFixed(2) + 'M'; document.getElementById('marketCapDisplay').textContent = 'MC: $' + mcStr; } const data = await fetchKlines(symbol, state.interval, state.limit); state.chartData = data; if (chartInstance) { chartInstance.setData(data); } placeholder.style.display = 'none'; } catch (err) { console.error('Failed to load chart:', err); placeholder.style.display = 'flex'; placeholder.innerHTML = ` <div style="text-align:center;color:var(--accent-red);font-size:13px;"> ❌ Failed to load chart data<br> <small style="color:var(--text-secondary);">${err.message}</small> <br><br> <button onclick="loadChart('${symbol}')" style="background:var(--bg-card);border:1px solid var(--border-color);color:var(--text-primary);padding:5px 14px;border-radius:6px;cursor:pointer;font-size:12px;">Retry</button> </div> `; } } // ================================================================ // 6. THEME // ================================================================ function setTheme(theme) { state.theme = theme; document.documentElement.setAttribute('data-theme', theme); localStorage.setItem('crypto-dashboard-theme', theme); const btn = document.getElementById('themeToggle'); if (theme === 'dark') { btn.innerHTML = `<svg viewBox="0 0 24 24"><path d="M12 7c-2.76 0-5 2.24-5 5s2.24 5 5 5 5-2.24 5-5-2.24-5-5-5zM2 13h2c.55 0 1-.45 1-1s-.45-1-1-1H2c-.55 0-1 .45-1 1s.45 1 1 1zm18 0h2c.55 0 1-.45 1-1s-.45-1-1-1h-2c-.55 0-1 .45-1 1s.45 1 1 1zM11 2v2c0 .55.45 1 1 1s1-.45 1-1V2c0-.55-.45-1-1-1s-1 .45-1 1zm0 18v2c0 .55.45 1 1 1s1-.45 1-1v-2c0-.55-.45-1-1-1s-1 .45-1 1zM5.99 4.58c-.39-.39-1.03-.39-1.41 0-.39.39-.39 1.03 0 1.41l1.06 1.06c.39.39 1.03.39 1.41 0s.39-1.03 0-1.41L5.99 4.58zm12.37 12.37c-.39-.39-1.03-.39-1.41 0-.39.39-.39 1.03 0 1.41l1.06 1.06c.39.39 1.03.39 1.41 0 .39-.39.39-1.03 0-1.41l-1.06-1.06zm1.06-10.96c.39-.39.39-1.03 0-1.41-.39-.39-1.03-.39-1.41 0l-1.06 1.06c-.39.39-.39 1.03 0 1.41s1.03.39 1.41 0l1.06-1.06zM7.05 18.36c.39-.39.39-1.03 0-1.41-.39-.39-1.03-.39-1.41 0l-1.06 1.06c-.39.39-.39 1.03 0 1.41s1.03.39 1.41 0l1.06-1.06z"/></svg>`; } else { btn.innerHTML = `<svg viewBox="0 0 24 24"><path d="M9 2c-1.05 0-2.05.16-3 .46 4.06 1.27 7 5.06 7 9.54 0 4.48-2.94 8.27-7 9.54.95.3 1.95.46 3 .46 5.52 0 10-4.48 10-10S14.52 2 9 2z"/></svg>`; } if (chartInstance) { setTimeout(() => chartInstance.render(), 50); } } function toggleTheme() { setTheme(state.theme === 'dark' ? 'light' : 'dark'); } // ================================================================ // 7. START // ================================================================ document.addEventListener('DOMContentLoaded', initApp); window.selectCoin = selectCoin; window.loadCoins = loadCoins; window.loadChart = loadChart; </script> </body> </html>
Cancel