Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Live Crypto Tracker</title> | |
| <style> | |
| * { | |
| margin: 0; | |
| padding: 0; | |
| box-sizing: border-box; | |
| } | |
| body { | |
| font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| min-height: 100vh; | |
| color: white; | |
| overflow-x: hidden; | |
| } | |
| .container { | |
| max-width: 1400px; | |
| margin: 0 auto; | |
| padding: 20px; | |
| } | |
| .header { | |
| text-align: center; | |
| margin-bottom: 40px; | |
| position: relative; | |
| } | |
| .header h1 { | |
| font-size: 3.5rem; | |
| font-weight: 800; | |
| background: linear-gradient(45deg, #f093fb 0%, #f5576c 50%, #4facfe 100%); | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| background-clip: text; | |
| margin-bottom: 10px; | |
| animation: glow 2s ease-in-out infinite alternate; | |
| } | |
| @keyframes glow { | |
| from { filter: drop-shadow(0 0 10px rgba(240, 147, 251, 0.3)); } | |
| to { filter: drop-shadow(0 0 20px rgba(240, 147, 251, 0.6)); } | |
| } | |
| .subtitle { | |
| font-size: 1.2rem; | |
| opacity: 0.9; | |
| font-weight: 300; | |
| } | |
| .status { | |
| display: inline-flex; | |
| align-items: center; | |
| gap: 8px; | |
| background: rgba(255, 255, 255, 0.1); | |
| padding: 8px 16px; | |
| border-radius: 50px; | |
| margin-top: 20px; | |
| backdrop-filter: blur(10px); | |
| border: 1px solid rgba(255, 255, 255, 0.2); | |
| font-size: 0.9rem; | |
| } | |
| .status-dot { | |
| width: 8px; | |
| height: 8px; | |
| border-radius: 50%; | |
| background: #4ade80; | |
| animation: pulse 2s infinite; | |
| } | |
| .status-dot.error { | |
| background: #f87171; | |
| } | |
| @keyframes pulse { | |
| 0%, 100% { opacity: 1; } | |
| 50% { opacity: 0.5; } | |
| } | |
| .controls { | |
| display: flex; | |
| justify-content: center; | |
| gap: 20px; | |
| margin-bottom: 30px; | |
| flex-wrap: wrap; | |
| } | |
| .control-btn { | |
| background: rgba(255, 255, 255, 0.15); | |
| border: none; | |
| color: white; | |
| padding: 12px 24px; | |
| border-radius: 25px; | |
| cursor: pointer; | |
| font-weight: 600; | |
| transition: all 0.3s ease; | |
| backdrop-filter: blur(10px); | |
| border: 1px solid rgba(255, 255, 255, 0.2); | |
| } | |
| .control-btn:hover { | |
| background: rgba(255, 255, 255, 0.25); | |
| transform: translateY(-2px); | |
| box-shadow: 0 10px 25px rgba(0, 0, 0, 0.2); | |
| } | |
| .control-btn.active { | |
| background: linear-gradient(45deg, #f093fb, #f5576c); | |
| box-shadow: 0 5px 15px rgba(240, 147, 251, 0.4); | |
| } | |
| #toggleAutoUpdateBtn.off { | |
| background: rgba(255, 255, 255, 0.15); | |
| box-shadow: none; | |
| } | |
| .crypto-grid { | |
| display: grid; | |
| grid-template-columns: repeat(auto-fit, minmax(350px, 1fr)); | |
| gap: 25px; | |
| margin-bottom: 40px; | |
| } | |
| .crypto-card { | |
| background: rgba(255, 255, 255, 0.1); | |
| border-radius: 20px; | |
| padding: 25px; | |
| backdrop-filter: blur(15px); | |
| border: 1px solid rgba(255, 255, 255, 0.2); | |
| transition: all 0.4s ease; | |
| position: relative; | |
| overflow: hidden; | |
| } | |
| .crypto-card::before { | |
| content: ''; | |
| position: absolute; | |
| top: 0; | |
| left: -100%; | |
| width: 100%; | |
| height: 100%; | |
| background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.1), transparent); | |
| transition: left 0.5s; | |
| } | |
| .crypto-card:hover::before { | |
| left: 100%; | |
| } | |
| .crypto-card:hover { | |
| transform: translateY(-5px); | |
| box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3); | |
| border-color: rgba(255, 255, 255, 0.3); | |
| } | |
| .crypto-header { | |
| display: flex; | |
| align-items: center; | |
| gap: 15px; | |
| margin-bottom: 20px; | |
| } | |
| .crypto-icon { | |
| width: 50px; | |
| height: 50px; | |
| } | |
| .crypto-info h3 { | |
| font-size: 1.3rem; | |
| font-weight: 700; | |
| margin-bottom: 5px; | |
| } | |
| .crypto-symbol { | |
| color: rgba(255, 255, 255, 0.7); | |
| font-size: 0.9rem; | |
| font-weight: 500; | |
| text-transform: uppercase; | |
| } | |
| .price-display { | |
| text-align: center; | |
| margin-bottom: 20px; | |
| } | |
| .current-price { | |
| font-size: 2.2rem; | |
| font-weight: 800; | |
| margin-bottom: 10px; | |
| background: linear-gradient(45deg, #4facfe, #00f2fe); | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| background-clip: text; | |
| } | |
| .price-change { | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| gap: 8px; | |
| font-weight: 600; | |
| padding: 8px 16px; | |
| border-radius: 15px; | |
| font-size: 0.95rem; | |
| } | |
| .price-change.positive { | |
| background: rgba(74, 222, 128, 0.2); | |
| color: #4ade80; | |
| border: 1px solid rgba(74, 222, 128, 0.3); | |
| } | |
| .price-change.negative { | |
| background: rgba(248, 113, 113, 0.2); | |
| color: #f87171; | |
| border: 1px solid rgba(248, 113, 113, 0.3); | |
| } | |
| .crypto-stats { | |
| display: grid; | |
| grid-template-columns: 1fr 1fr; | |
| gap: 15px; | |
| margin-top: 20px; | |
| } | |
| .stat-item { | |
| text-align: center; | |
| padding: 12px; | |
| background: rgba(255, 255, 255, 0.05); | |
| border-radius: 12px; | |
| border: 1px solid rgba(255, 255, 255, 0.1); | |
| } | |
| .stat-label { | |
| font-size: 0.8rem; | |
| opacity: 0.7; | |
| margin-bottom: 5px; | |
| text-transform: uppercase; | |
| letter-spacing: 0.5px; | |
| } | |
| .stat-value { | |
| font-weight: 700; | |
| font-size: 1rem; | |
| } | |
| .loading { | |
| text-align: center; | |
| padding: 50px; | |
| font-size: 1.2rem; | |
| } | |
| .spinner { | |
| display: inline-block; | |
| width: 40px; | |
| height: 40px; | |
| border: 4px solid rgba(255, 255, 255, 0.3); | |
| border-radius: 50%; | |
| border-top-color: #f093fb; | |
| animation: spin 1s ease-in-out infinite; | |
| margin-bottom: 20px; | |
| } | |
| @keyframes spin { | |
| to { transform: rotate(360deg); } | |
| } | |
| .error { | |
| text-align: center; | |
| padding: 30px; | |
| background: rgba(248, 113, 113, 0.1); | |
| border-radius: 15px; | |
| border: 1px solid rgba(248, 113, 113, 0.3); | |
| margin: 20px 0; | |
| } | |
| .refresh-btn { | |
| background: linear-gradient(45deg, #4facfe, #00f2fe); | |
| border: none; | |
| color: white; | |
| padding: 12px 30px; | |
| border-radius: 25px; | |
| cursor: pointer; | |
| font-weight: 600; | |
| margin-top: 15px; | |
| transition: all 0.3s ease; | |
| box-shadow: 0 5px 15px rgba(79, 172, 254, 0.4); | |
| } | |
| .refresh-btn:hover { | |
| transform: translateY(-2px); | |
| box-shadow: 0 8px 25px rgba(79, 172, 254, 0.6); | |
| } | |
| @media (max-width: 768px) { | |
| .header h1 { | |
| font-size: 2.5rem; | |
| } | |
| .crypto-grid { | |
| grid-template-columns: 1fr; | |
| gap: 20px; | |
| } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| <div class="header"> | |
| <h1>CryptoTracker Pro</h1> | |
| <p class="subtitle">Real-time cryptocurrency price monitoring</p> | |
| <div class="status"> | |
| <div id="statusDot" class="status-dot"></div> | |
| <span id="statusText">Connecting...</span> | |
| </div> | |
| </div> | |
| <div class="controls"> | |
| <button class="control-btn active" onclick="setUpdateInterval(5000, this)">5s</button> | |
| <button class="control-btn" onclick="setUpdateInterval(10000, this)">10s</button> | |
| <button class="control-btn" onclick="setUpdateInterval(30000, this)">30s</button> | |
| <button id="toggleAutoUpdateBtn" class="control-btn active" onclick="toggleAutoUpdate(this)">Auto: ON</button> | |
| </div> | |
| <div id="cryptoContainer"> | |
| <div class="loading"> | |
| <div class="spinner"></div> | |
| <p>Loading cryptocurrency data...</p> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| // --- CONFIGURATION --- | |
| // Primary API: CoinGecko (Free, No Key Required, High Rate Limits) | |
| const COINGECKO_BASE_URL = 'https://api.coingecko.com/api/v3'; | |
| const COINGECKO_IDS = ['bitcoin', 'ethereum', 'binancecoin', 'ripple', 'cardano', 'solana', 'dogecoin', 'polkadot']; | |
| // Fallback API: CoinMarketCap (Requires free API key, limited credits) | |
| const CMC_BASE_URL = 'https://pro-api.coinmarketcap.com/v1/cryptocurrency/quotes/latest'; | |
| const CMC_SYMBOLS = ['BTC', 'ETH', 'BNB', 'XRP', 'ADA', 'SOL', 'DOGE', 'DOT']; | |
| const CMC_API_KEYS = [ | |
| '51a08ddd-bb80-4f8e-8acf-2d7d7e54ce1d', // <-- YOUR API KEY IS HERE | |
| 'YOUR_SECOND_API_KEY_HERE', // Add more keys here to cycle through them | |
| 'YOUR_THIRD_API_KEY_HERE', | |
| ]; | |
| const PROXY_URL = 'https://api.allorigins.win/get?url='; // CORS Proxy | |
| // --- APPLICATION STATE --- | |
| let updateInterval = 5000; | |
| let autoUpdate = true; | |
| let updateTimer; | |
| let currentCmcKeyIndex = 0; | |
| // --- DOM ELEMENTS --- | |
| const statusText = document.getElementById('statusText'); | |
| const statusDot = document.getElementById('statusDot'); | |
| const cryptoContainer = document.getElementById('cryptoContainer'); | |
| // --- CORE LOGIC --- | |
| async function fetchCryptoData() { | |
| statusText.textContent = 'Fetching data...'; | |
| statusDot.classList.remove('error'); | |
| try { | |
| // 1. Try fetching from CoinGecko first | |
| const data = await fetchFromCoinGecko(); | |
| displayCryptoData(data); | |
| statusText.textContent = `Live (Updated: ${new Date().toLocaleTimeString()}) - CoinGecko`; | |
| return; | |
| } catch (error) { | |
| console.warn('CoinGecko API failed:', error.message, 'Falling back to CoinMarketCap.'); | |
| // 2. If CoinGecko fails, try CoinMarketCap | |
| try { | |
| const data = await fetchFromCoinMarketCap(); | |
| if (data) { | |
| // The status text is updated within the CMC function itself | |
| const transformedData = transformCmcData(data); | |
| displayCryptoData(transformedData); | |
| } else { | |
| throw new Error("All CMC keys failed."); | |
| } | |
| } catch (cmcError) { | |
| console.error('All API sources have failed:', cmcError.message); | |
| displayError("Could not fetch data from any source. APIs might be down or you may have exceeded rate limits."); | |
| statusText.textContent = 'Connection Error'; | |
| statusDot.classList.add('error'); | |
| } | |
| } | |
| } | |
| async function fetchFromCoinGecko() { | |
| const ids = COINGECKO_IDS.join(','); | |
| const url = `${COINGECKO_BASE_URL}/coins/markets?vs_currency=usd&ids=${ids}`; | |
| const response = await fetch(url); | |
| if (!response.ok) { | |
| throw new Error(`CoinGecko API error: ${response.status} ${response.statusText}`); | |
| } | |
| const data = await response.json(); | |
| // Transform CoinGecko data to our standard format | |
| return transformGeckoData(data); | |
| } | |
| async function fetchFromCoinMarketCap() { | |
| const symbols = CMC_SYMBOLS.join(','); | |
| const url = `${CMC_BASE_URL}?symbol=${symbols}&convert=USD`; | |
| for (let i = 0; i < CMC_API_KEYS.length; i++) { | |
| const keyIndex = (currentCmcKeyIndex + i) % CMC_API_KEYS.length; | |
| const apiKey = CMC_API_KEYS[keyIndex]; | |
| if (apiKey.startsWith('YOUR_')) continue; // Skip placeholder keys | |
| try { | |
| console.log(`Trying CMC API Key #${keyIndex + 1}`); | |
| const proxyUrl = `${PROXY_URL}${encodeURIComponent(url)}`; | |
| const response = await fetch(proxyUrl, { | |
| headers: { 'X-CMC_PRO_API_KEY': apiKey } | |
| }); | |
| if (!response.ok) throw new Error(`Proxy error with status: ${response.status}`); | |
| const proxyData = await response.json(); | |
| if (!proxyData.contents) throw new Error('Proxy returned empty contents.'); | |
| const data = JSON.parse(proxyData.contents); | |
| if (data.status.error_code !== 0) { | |
| throw new Error(`CMC API Error: ${data.status.error_message}`); | |
| } | |
| console.log(`Success with CMC API Key #${keyIndex + 1}`); | |
| currentCmcKeyIndex = keyIndex; // Remember the working key for next time | |
| statusText.textContent = `Live (Updated: ${new Date().toLocaleTimeString()}) - CoinMarketCap`; | |
| return data.data; // Success! | |
| } catch (error) { | |
| console.warn(`CMC API Key #${keyIndex + 1} failed:`, error.message); | |
| // Continue to the next key | |
| } | |
| } | |
| return null; // All keys failed | |
| } | |
| // --- DATA TRANSFORMATION --- | |
| function transformGeckoData(geckoData) { | |
| const transformed = {}; | |
| geckoData.forEach(coin => { | |
| const symbol = coin.symbol.toUpperCase(); | |
| transformed[symbol] = { | |
| name: coin.name, | |
| symbol: symbol, | |
| image: coin.image, | |
| quote: { | |
| USD: { | |
| price: coin.current_price, | |
| percent_change_24h: coin.price_change_percentage_24h, | |
| market_cap: coin.market_cap, | |
| volume_24h: coin.total_volume, | |
| high_24h: coin.high_24h, | |
| low_24h: coin.low_24h, | |
| } | |
| } | |
| }; | |
| }); | |
| return transformed; | |
| } | |
| function transformCmcData(cmcData) { | |
| const transformed = {}; | |
| for (const symbol in cmcData) { | |
| const coin = cmcData[symbol]; | |
| transformed[symbol] = { | |
| name: coin.name, | |
| symbol: coin.symbol, | |
| image: null, // CMC basic API doesn't provide image URLs | |
| quote: { | |
| USD: { | |
| price: coin.quote.USD.price, | |
| percent_change_24h: coin.quote.USD.percent_change_24h, | |
| market_cap: coin.quote.USD.market_cap, | |
| volume_24h: coin.quote.USD.volume_24h, | |
| high_24h: null, // Not available in CMC free tier | |
| low_24h: null, // Not available in CMC free tier | |
| } | |
| } | |
| }; | |
| } | |
| return transformed; | |
| } | |
| // --- UI RENDERING --- | |
| function displayCryptoData(data) { | |
| const cryptoGrid = document.createElement('div'); | |
| cryptoGrid.className = 'crypto-grid'; | |
| const symbolsToDisplay = Object.keys(data); | |
| symbolsToDisplay.forEach(symbol => { | |
| if (data[symbol]) { | |
| const crypto = data[symbol]; | |
| const quote = crypto.quote.USD; | |
| const card = createCryptoCard(crypto, quote); | |
| cryptoGrid.appendChild(card); | |
| } | |
| }); | |
| cryptoContainer.innerHTML = ''; | |
| cryptoContainer.appendChild(cryptoGrid); | |
| } | |
| function createCryptoCard(crypto, quote) { | |
| const card = document.createElement('div'); | |
| card.className = 'crypto-card'; | |
| const priceChange = quote.percent_change_24h || 0; | |
| const isPositive = priceChange >= 0; | |
| const changeClass = isPositive ? 'positive' : 'negative'; | |
| const changeSymbol = isPositive ? '↗' : '↘'; | |
| // Use image from API if available, otherwise default to symbol initial | |
| const iconHtml = crypto.image | |
| ? `<img src="${crypto.image}" alt="${crypto.symbol}" class="crypto-icon">` | |
| : `<div class="crypto-icon" style="display:flex;align-items:center;justify-content:center;font-size:1.5rem;background:linear-gradient(45deg, #667eea, #764ba2);border-radius:50%;">${crypto.symbol.charAt(0)}</div>`; | |
| card.innerHTML = ` | |
| <div class="crypto-header"> | |
| ${iconHtml} | |
| <div class="crypto-info"> | |
| <h3>${crypto.name}</h3> | |
| <div class="crypto-symbol">${crypto.symbol}</div> | |
| </div> | |
| </div> | |
| <div class="price-display"> | |
| <div class="current-price">$${formatPrice(quote.price)}</div> | |
| <div class="price-change ${changeClass}"> | |
| <span>${changeSymbol}</span> | |
| <span>${Math.abs(priceChange).toFixed(2)}% (24h)</span> | |
| </div> | |
| </div> | |
| <div class="crypto-stats"> | |
| <div class="stat-item"> | |
| <div class="stat-label">24h High</div> | |
| <div class="stat-value">$${formatPrice(quote.high_24h)}</div> | |
| </div> | |
| <div class="stat-item"> | |
| <div class="stat-label">24h Low</div> | |
| <div class="stat-value">$${formatPrice(quote.low_24h)}</div> | |
| </div> | |
| <div class="stat-item"> | |
| <div class="stat-label">Market Cap</div> | |
| <div class="stat-value">$${formatMarketCap(quote.market_cap)}</div> | |
| </div> | |
| <div class="stat-item"> | |
| <div class="stat-label">Volume (24h)</div> | |
| <div class="stat-value">$${formatMarketCap(quote.volume_24h)}</div> | |
| </div> | |
| </div> | |
| `; | |
| return card; | |
| } | |
| function displayError(message) { | |
| cryptoContainer.innerHTML = ` | |
| <div class="error"> | |
| <h3>Oops! Something went wrong.</h3> | |
| <p>${message}</p> | |
| <button class="refresh-btn" onclick="location.reload()">Refresh Page</button> | |
| </div> | |
| `; | |
| } | |
| // --- UTILITY & CONTROL FUNCTIONS --- | |
| function formatPrice(price) { | |
| if (price == null) return 'N/A'; | |
| if (price >= 1) { | |
| return price.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 }); | |
| } | |
| return price.toPrecision(4); | |
| } | |
| function formatMarketCap(value) { | |
| if (value == null) return 'N/A'; | |
| 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); | |
| } | |
| function setUpdateInterval(interval, clickedButton) { | |
| updateInterval = interval; | |
| document.querySelectorAll('.controls .control-btn').forEach(btn => { | |
| if (btn.id !== 'toggleAutoUpdateBtn') btn.classList.remove('active'); | |
| }); | |
| clickedButton.classList.add('active'); | |
| if (autoUpdate) { | |
| clearInterval(updateTimer); | |
| startAutoUpdate(); | |
| } | |
| } | |
| function toggleAutoUpdate(button) { | |
| autoUpdate = !autoUpdate; | |
| if (autoUpdate) { | |
| button.textContent = 'Auto: ON'; | |
| button.classList.add('active'); | |
| button.classList.remove('off'); | |
| fetchCryptoData(); // Fetch immediately | |
| startAutoUpdate(); | |
| } else { | |
| button.textContent = 'Auto: OFF'; | |
| button.classList.remove('active'); | |
| button.classList.add('off'); | |
| clearInterval(updateTimer); | |
| statusText.textContent = "Auto-update paused"; | |
| } | |
| } | |
| function startAutoUpdate() { | |
| if (updateTimer) clearInterval(updateTimer); | |
| updateTimer = setInterval(fetchCryptoData, updateInterval); | |
| } | |
| // --- INITIALIZATION --- | |
| // Initial fetch | |
| document.addEventListener('DOMContentLoaded', () => { | |
| fetchCryptoData(); | |
| if (autoUpdate) { | |
| startAutoUpdate(); | |
| } | |
| }); | |
| </script> | |
| </body> | |
| </html> |