Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Crypto Price Tracker with Charts</title> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> | |
| <script src="https://cdn.jsdelivr.net/npm/chart.js"></script> | |
| <style> | |
| @keyframes pulse { | |
| 0% { transform: scale(1); } | |
| 50% { transform: scale(1.05); } | |
| 100% { transform: scale(1); } | |
| } | |
| .pulse { | |
| animation: pulse 2s infinite; | |
| } | |
| .gradient-bg { | |
| background: linear-gradient(135deg, #1e3a8a 0%, #0ea5e9 100%); | |
| } | |
| .card-hover:hover { | |
| transform: translateY(-5px); | |
| box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04); | |
| } | |
| .price-up { | |
| color: #10b981; | |
| } | |
| .price-down { | |
| color: #ef4444; | |
| } | |
| .loading-spinner { | |
| border-top-color: #3b82f6; | |
| animation: spin 1s linear infinite; | |
| } | |
| @keyframes spin { | |
| 0% { transform: rotate(0deg); } | |
| 100% { transform: rotate(360deg); } | |
| } | |
| .modal { | |
| transition: opacity 0.3s ease, transform 0.3s ease; | |
| } | |
| .modal-active { | |
| opacity: 1; | |
| visibility: visible; | |
| transform: translate(-50%, -50%) scale(1); | |
| } | |
| .modal-inactive { | |
| opacity: 0; | |
| visibility: hidden; | |
| transform: translate(-50%, -50%) scale(0.9); | |
| } | |
| .chart-tab.active { | |
| background-color: rgba(255, 255, 255, 0.2); | |
| font-weight: 600; | |
| } | |
| </style> | |
| </head> | |
| <body class="min-h-screen gradient-bg text-white"> | |
| <div class="container mx-auto px-4 py-12"> | |
| <header class="text-center mb-12"> | |
| <h1 class="text-4xl md:text-5xl font-bold mb-4">Crypto Price Tracker</h1> | |
| <p class="text-xl opacity-90">Real-time cryptocurrency prices with interactive charts</p> | |
| <div class="mt-6 flex justify-center"> | |
| <div class="relative w-full max-w-md"> | |
| <input | |
| type="text" | |
| id="searchInput" | |
| placeholder="Search cryptocurrencies..." | |
| class="w-full px-4 py-3 rounded-full bg-white/10 border border-white/20 focus:outline-none focus:ring-2 focus:ring-blue-400 focus:border-transparent text-white placeholder-white/70" | |
| > | |
| <button id="searchBtn" class="absolute right-3 top-3 text-white/70 hover:text-white"> | |
| <i class="fas fa-search"></i> | |
| </button> | |
| </div> | |
| </div> | |
| </header> | |
| <div class="flex justify-between items-center mb-6"> | |
| <h2 class="text-2xl font-semibold">Top Cryptocurrencies</h2> | |
| <div class="flex items-center space-x-2"> | |
| <span class="text-sm">Auto-refresh:</span> | |
| <label class="relative inline-flex items-center cursor-pointer"> | |
| <input type="checkbox" id="autoRefresh" class="sr-only peer" checked> | |
| <div class="w-11 h-6 bg-gray-700 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-blue-500"></div> | |
| </label> | |
| </div> | |
| </div> | |
| <div id="loading" class="flex justify-center items-center py-12"> | |
| <div class="loading-spinner h-12 w-12 border-4 border-white/20 rounded-full"></div> | |
| </div> | |
| <div id="cryptoContainer" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 hidden"> | |
| <!-- Crypto cards will be inserted here --> | |
| </div> | |
| <div id="errorMessage" class="hidden text-center py-12"> | |
| <i class="fas fa-exclamation-triangle text-4xl mb-4 text-yellow-400"></i> | |
| <h3 class="text-2xl font-semibold mb-2">Failed to load data</h3> | |
| <p class="mb-4">We couldn't fetch cryptocurrency prices. Please check your connection and try again.</p> | |
| <button id="retryBtn" class="px-6 py-2 bg-white/10 hover:bg-white/20 rounded-full border border-white/20 transition-all"> | |
| <i class="fas fa-sync-alt mr-2"></i> Retry | |
| </button> | |
| </div> | |
| <div class="mt-12 text-center text-sm opacity-80"> | |
| <p>Data provided by CoinGecko API. Prices update every 60 seconds.</p> | |
| <p class="mt-2">Last updated: <span id="lastUpdated">-</span></p> | |
| </div> | |
| </div> | |
| <!-- Modal --> | |
| <div id="coinModal" class="modal modal-inactive fixed inset-0 z-50"> | |
| <div class="absolute inset-0 bg-black/70 backdrop-blur-sm" id="modalBackdrop"></div> | |
| <div class="absolute top-1/2 left-1/2 w-11/12 max-w-4xl -translate-x-1/2 -translate-y-1/2"> | |
| <div class="bg-gray-900 rounded-xl border border-white/10 overflow-hidden shadow-2xl"> | |
| <div class="flex justify-between items-center p-4 border-b border-white/10"> | |
| <div class="flex items-center"> | |
| <img id="modalCoinImage" src="" alt="" class="w-8 h-8 mr-3"> | |
| <h3 id="modalCoinName" class="text-xl font-bold"></h3> | |
| <span id="modalCoinSymbol" class="ml-2 text-sm opacity-80"></span> | |
| </div> | |
| <button id="closeModal" class="text-white/50 hover:text-white"> | |
| <i class="fas fa-times text-xl"></i> | |
| </button> | |
| </div> | |
| <div class="p-6"> | |
| <div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-6"> | |
| <div class="bg-white/5 p-4 rounded-lg border border-white/10"> | |
| <div class="text-sm opacity-80 mb-1">Current Price</div> | |
| <div id="modalCurrentPrice" class="text-2xl font-bold"></div> | |
| </div> | |
| <div class="bg-white/5 p-4 rounded-lg border border-white/10"> | |
| <div class="text-sm opacity-80 mb-1">Market Cap</div> | |
| <div id="modalMarketCap" class="text-2xl font-bold"></div> | |
| </div> | |
| <div class="bg-white/5 p-4 rounded-lg border border-white/10"> | |
| <div class="text-sm opacity-80 mb-1">24h Change</div> | |
| <div id="modal24hChange" class="text-2xl font-bold"></div> | |
| </div> | |
| </div> | |
| <div class="mb-4"> | |
| <div class="flex border-b border-white/10"> | |
| <button class="chart-tab px-4 py-2 active" data-period="24h">24 Hours</button> | |
| <button class="chart-tab px-4 py-2" data-period="7d">7 Days</button> | |
| <button class="chart-tab px-4 py-2" data-period="30d">30 Days</button> | |
| </div> | |
| </div> | |
| <div class="h-80 mb-6"> | |
| <canvas id="detailedChart"></canvas> | |
| </div> | |
| <div class="grid grid-cols-2 md:grid-cols-4 gap-3 text-sm"> | |
| <div class="bg-white/5 p-3 rounded border border-white/10"> | |
| <div class="opacity-70">24h High</div> | |
| <div id="24hHigh" class="text-lg mt-1"></div> | |
| </div> | |
| <div class="bg-white/5 p-3 rounded border border-white/10"> | |
| <div class="opacity-70">24h Low</div> | |
| <div id="24hLow" class="text-lg mt-1"></div> | |
| </div> | |
| <div class="bg-white/5 p-3 rounded border border-white/10"> | |
| <div class="opacity-70">24h Volume</div> | |
| <div id="24hVolume" class="text-lg mt-1"></div> | |
| </div> | |
| <div class="bg-white/5 p-3 rounded border border-white/10"> | |
| <div class="opacity-70">Circ. Supply</div> | |
| <div id="circSupply" class="text-lg mt-1"></div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| // Configuration | |
| const config = { | |
| coins: ['bitcoin', 'ethereum', 'binancecoin', 'ripple', 'cardano', 'solana', 'dogecoin', 'polkadot', 'litecoin', 'chainlink'], | |
| vsCurrency: 'usd', | |
| autoRefreshInterval: 60000 // 60 seconds | |
| }; | |
| // DOM Elements | |
| const cryptoContainer = document.getElementById('cryptoContainer'); | |
| const loadingElement = document.getElementById('loading'); | |
| const errorMessage = document.getElementById('errorMessage'); | |
| const retryBtn = document.getElementById('retryBtn'); | |
| const searchInput = document.getElementById('searchInput'); | |
| const searchBtn = document.getElementById('searchBtn'); | |
| const autoRefreshToggle = document.getElementById('autoRefresh'); | |
| const lastUpdatedElement = document.getElementById('lastUpdated'); | |
| const coinModal = document.getElementById('coinModal'); | |
| const modalBackdrop = document.getElementById('modalBackdrop'); | |
| const closeModal = document.getElementById('closeModal'); | |
| const detailedChart = document.getElementById('detailedChart'); | |
| const chartTabs = document.querySelectorAll('.chart-tab'); | |
| // Chart instance | |
| let chartInstance = null; | |
| let currentCoinId = null; | |
| // State | |
| let refreshInterval; | |
| let allCoinsData = []; | |
| // Initialize | |
| document.addEventListener('DOMContentLoaded', () => { | |
| fetchCryptoData(); | |
| setupAutoRefresh(); | |
| setupEventListeners(); | |
| }); | |
| // Event Listeners | |
| function setupEventListeners() { | |
| retryBtn.addEventListener('click', fetchCryptoData); | |
| searchBtn.addEventListener('click', filterCoins); | |
| searchInput.addEventListener('keyup', (e) => { | |
| if (e.key === 'Enter') filterCoins(); | |
| }); | |
| autoRefreshToggle.addEventListener('change', setupAutoRefresh); | |
| modalBackdrop.addEventListener('click', closeCoinModal); | |
| closeModal.addEventListener('click', closeCoinModal); | |
| // Add event listeners to chart tabs | |
| chartTabs.forEach(tab => { | |
| tab.addEventListener('click', () => { | |
| changeChartPeriod(tab.dataset.period); | |
| }); | |
| }); | |
| } | |
| // Auto Refresh | |
| function setupAutoRefresh() { | |
| clearInterval(refreshInterval); | |
| if (autoRefreshToggle.checked) { | |
| refreshInterval = setInterval(fetchCryptoData, config.autoRefreshInterval); | |
| } | |
| } | |
| // Fetch Data | |
| async function fetchCryptoData() { | |
| try { | |
| showLoading(); | |
| hideError(); | |
| const ids = config.coins.join(','); | |
| const url = `https://api.coingecko.com/api/v3/coins/markets?vs_currency=${config.vsCurrency}&ids=${ids}&order=market_cap_desc&per_page=100&page=1&sparkline=true&price_change_percentage=1h,24h,7d`; | |
| const response = await fetch(url); | |
| if (!response.ok) { | |
| throw new Error('Network response was not ok'); | |
| } | |
| allCoinsData = await response.json(); | |
| renderCryptoCards(allCoinsData); | |
| updateLastUpdated(); | |
| } catch (error) { | |
| console.error('Error fetching crypto data:', error); | |
| showError(); | |
| } finally { | |
| hideLoading(); | |
| } | |
| } | |
| // Render Crypto Cards | |
| function renderCryptoCards(coinsData) { | |
| cryptoContainer.innerHTML = ''; | |
| coinsData.forEach(coin => { | |
| const priceChange24h = coin.price_change_percentage_24h; | |
| const priceChangeClass = priceChange24h >= 0 ? 'price-up' : 'price-down'; | |
| const priceChangeIcon = priceChange24h >= 0 ? 'fa-arrow-up' : 'fa-arrow-down'; | |
| const card = document.createElement('div'); | |
| card.className = 'bg-white/5 rounded-xl p-6 border border-white/10 transition-all duration-300 card-hover cursor-pointer'; | |
| card.dataset.coinId = coin.id; | |
| card.innerHTML = ` | |
| <div class="flex justify-between items-start mb-4"> | |
| <div class="flex items-center"> | |
| <img src="${coin.image}" alt="${coin.name}" class="w-10 h-10 mr-3 rounded-full"> | |
| <div> | |
| <h3 class="font-bold text-lg">${coin.name}</h3> | |
| <span class="text-sm opacity-80">${coin.symbol.toUpperCase()}</span> | |
| </div> | |
| </div> | |
| <span class="text-xs px-2 py-1 rounded-full bg-white/10">#${coin.market_cap_rank}</span> | |
| </div> | |
| <div class="mb-4"> | |
| <div class="flex items-baseline mb-1"> | |
| <span class="text-2xl font-bold mr-2">$${coin.current_price.toLocaleString()}</span> | |
| <span class="text-sm ${priceChangeClass}"> | |
| <i class="fas ${priceChangeIcon} mr-1"></i>${Math.abs(priceChange24h).toFixed(2)}% | |
| </span> | |
| </div> | |
| <div class="text-sm opacity-80">Market Cap: $${coin.market_cap.toLocaleString()}</div> | |
| </div> | |
| <div class="h-16 mb-4"> | |
| <canvas class="w-full h-full" id="sparkline-${coin.id}"></canvas> | |
| </div> | |
| <div class="grid grid-cols-3 gap-2 text-center text-sm"> | |
| <div class="bg-white/5 p-2 rounded"> | |
| <div>1h</div> | |
| <div class="${coin.price_change_percentage_1h_in_currency >= 0 ? 'price-up' : 'price-down'}"> | |
| ${coin.price_change_percentage_1h_in_currency ? coin.price_change_percentage_1h_in_currency.toFixed(2) + '%' : '-'} | |
| </div> | |
| </div> | |
| <div class="bg-white/5 p-2 rounded"> | |
| <div>24h</div> | |
| <div class="${priceChangeClass}"> | |
| ${priceChange24h.toFixed(2)}% | |
| </div> | |
| </div> | |
| <div class="bg-white/5 p-2 rounded"> | |
| <div>7d</div> | |
| <div class="${coin.price_change_percentage_7d_in_currency >= 0 ? 'price-up' : 'price-down'}"> | |
| ${coin.price_change_percentage_7d_in_currency.toFixed(2)}% | |
| </div> | |
| </div> | |
| </div> | |
| `; | |
| // Add click event to show modal with detailed info | |
| card.addEventListener('click', () => showCoinModal(coin.id)); | |
| cryptoContainer.appendChild(card); | |
| // Render sparkline after the card is added to DOM | |
| setTimeout(() => renderSparkline(coin), 100); | |
| }); | |
| showCryptoContainer(); | |
| } | |
| // Render Sparkline Chart | |
| function renderSparkline(coin) { | |
| const canvas = document.getElementById(`sparkline-${coin.id}`); | |
| if (!canvas) return; | |
| const ctx = canvas.getContext('2d'); | |
| const sparklineData = coin.sparkline_in_7d.price; | |
| const width = canvas.width; | |
| const height = canvas.height; | |
| // Calculate scale factors | |
| const minPrice = Math.min(...sparklineData); | |
| const maxPrice = Math.max(...sparklineData); | |
| const scaleY = height / (maxPrice - minPrice); | |
| const scaleX = width / (sparklineData.length - 1); | |
| // Draw sparkline | |
| ctx.beginPath(); | |
| ctx.moveTo(0, height - (sparklineData[0] - minPrice) * scaleY); | |
| for (let i = 1; i < sparklineData.length; i++) { | |
| ctx.lineTo(i * scaleX, height - (sparklineData[i] - minPrice) * scaleY); | |
| } | |
| ctx.strokeStyle = coin.price_change_percentage_7d_in_currency >= 0 ? '#10b981' : '#ef4444'; | |
| ctx.lineWidth = 2; | |
| ctx.stroke(); | |
| } | |
| // Show Coin Modal | |
| async function showCoinModal(coinId) { | |
| try { | |
| currentCoinId = coinId; | |
| // Get the basic coin data from our existing data | |
| const coinData = allCoinsData.find(c => c.id === coinId); | |
| if (!coinData) return; | |
| // Get more detailed data from API | |
| const response = await fetch(`https://api.coingecko.com/api/v3/coins/${coinId}/market_chart?vs_currency=${config.vsCurrency}&days=30`); | |
| if (!response.ok) throw new Error('Could not fetch detailed data'); | |
| const detailedData = await response.json(); | |
| // Update modal with basic info | |
| document.getElementById('modalCoinImage').src = coinData.image; | |
| document.getElementById('modalCoinName').textContent = coinData.name; | |
| document.getElementById('modalCoinSymbol').textContent = coinData.symbol.toUpperCase(); | |
| document.getElementById('modalCurrentPrice').textContent = `$${coinData.current_price.toLocaleString()}`; | |
| document.getElementById('modalMarketCap').textContent = `$${coinData.market_cap.toLocaleString()}`; | |
| const priceChange24h = coinData.price_change_percentage_24h; | |
| const priceChangeClass = priceChange24h >= 0 ? 'price-up' : 'price-down'; | |
| const priceChangeIcon = priceChange24h >= 0 ? 'fa-arrow-up' : 'fa-arrow-down'; | |
| document.getElementById('modal24hChange').innerHTML = ` | |
| <span class="${priceChangeClass}"> | |
| <i class="fas ${priceChangeIcon} mr-1"></i>${Math.abs(priceChange24h).toFixed(2)}% | |
| </span> | |
| `; | |
| // Update additional info | |
| document.getElementById('24hHigh').textContent = `$${coinData.high_24h.toLocaleString()}`; | |
| document.getElementById('24hLow').textContent = `$${coinData.low_24h.toLocaleString()}`; | |
| document.getElementById('24hVolume').textContent = `$${coinData.total_volume.toLocaleString()}`; | |
| document.getElementById('circSupply').textContent = `${coinData.circulating_supply.toLocaleString()} ${coinData.symbol.toUpperCase()}`; | |
| // Create the initial chart (24h view) | |
| createDetailedChart(detailedData.prices, '24h'); | |
| // Show the modal | |
| coinModal.classList.remove('modal-inactive'); | |
| coinModal.classList.add('modal-active'); | |
| // Set the first tab as active | |
| chartTabs.forEach(tab => tab.classList.remove('active')); | |
| document.querySelector('[data-period="24h"]').classList.add('active'); | |
| } catch (error) { | |
| console.error('Error showing coin modal:', error); | |
| alert('Failed to load detailed coin data. Please try again.'); | |
| } | |
| } | |
| // Close Coin Modal | |
| function closeCoinModal() { | |
| coinModal.classList.remove('modal-active'); | |
| coinModal.classList.add('modal-inactive'); | |
| // Destroy the chart if it exists | |
| if (chartInstance) { | |
| chartInstance.destroy(); | |
| chartInstance = null; | |
| } | |
| currentCoinId = null; | |
| } | |
| // Create Detailed Chart | |
| function createDetailedChart(prices, period) { | |
| // Filter data based on period | |
| let filteredPrices = []; | |
| const now = new Date().getTime(); | |
| if (period === '24h') { | |
| const twentyFourHoursAgo = now - 24 * 60 * 60 * 1000; | |
| filteredPrices = prices.filter(price => price[0] >= twentyFourHoursAgo); | |
| } else if (period === '7d') { | |
| const sevenDaysAgo = now - 7 * 24 * 60 * 60 * 1000; | |
| filteredPrices = prices.filter(price => price[0] >= sevenDaysAgo); | |
| } else { // 30d | |
| filteredPrices = prices; | |
| } | |
| // Prepare data for chart | |
| const labels = filteredPrices.map(price => { | |
| const date = new Date(price[0]); | |
| if (period === '24h') { | |
| return date.toLocaleTimeString(); | |
| } else { | |
| return date.toLocaleDateString(); | |
| } | |
| }); | |
| const data = filteredPrices.map(price => price[1]); | |
| // Get color based on price trend | |
| const firstPrice = filteredPrices[0][1]; | |
| const lastPrice = filteredPrices[filteredPrices.length - 1][1]; | |
| const isPositive = lastPrice >= firstPrice; | |
| const lineColor = isPositive ? '#10b981' : '#ef4444'; | |
| const bgColor = isPositive ? 'rgba(16, 185, 129, 0.1)' : 'rgba(239, 68, 68, 0.1)'; | |
| // Destroy previous chart if it exists | |
| if (chartInstance) { | |
| chartInstance.destroy(); | |
| } | |
| // Create new chart | |
| const ctx = detailedChart.getContext('2d'); | |
| chartInstance = new Chart(ctx, { | |
| type: 'line', | |
| data: { | |
| labels: labels, | |
| datasets: [{ | |
| label: 'Price', | |
| data: data, | |
| borderColor: lineColor, | |
| backgroundColor: bgColor, | |
| borderWidth: 2, | |
| fill: true, | |
| tension: 0.4, | |
| pointRadius: 0 | |
| }] | |
| }, | |
| options: { | |
| responsive: true, | |
| maintainAspectRatio: false, | |
| plugins: { | |
| legend: { | |
| display: false | |
| }, | |
| tooltip: { | |
| mode: 'index', | |
| intersect: false, | |
| callbacks: { | |
| label: function(context) { | |
| return `${context.dataset.label}: $${context.parsed.y.toFixed(2)}`; | |
| } | |
| } | |
| } | |
| }, | |
| scales: { | |
| x: { | |
| grid: { | |
| color: 'rgba(255, 255, 255, 0.1)', | |
| display: false | |
| }, | |
| ticks: { | |
| color: 'rgba(255, 255, 255, 0.7)', | |
| maxRotation: 0, | |
| autoSkip: true, | |
| maxTicksLimit: 8 | |
| } | |
| }, | |
| y: { | |
| grid: { | |
| color: 'rgba(255, 255, 255, 0.1)' | |
| }, | |
| ticks: { | |
| color: 'rgba(255, 255, 255, 0.7)', | |
| callback: function(value) { | |
| return '$' + value.toLocaleString(); | |
| } | |
| } | |
| } | |
| } | |
| } | |
| }); | |
| } | |
| // Change Chart Period | |
| async function changeChartPeriod(period) { | |
| if (!currentCoinId) return; | |
| try { | |
| // Set active tab | |
| chartTabs.forEach(tab => tab.classList.remove('active')); | |
| document.querySelector(`[data-period="${period}"]`).classList.add('active'); | |
| // Get the updated data for the selected period | |
| let days; | |
| if (period === '24h') days = 1; | |
| else if (period === '7d') days = 7; | |
| else days = 30; | |
| const response = await fetch(`https://api.coingecko.com/api/v3/coins/${currentCoinId}/market_chart?vs_currency=${config.vsCurrency}&days=${days}`); | |
| if (!response.ok) throw new Error('Could not fetch chart data'); | |
| const detailedData = await response.json(); | |
| createDetailedChart(detailedData.prices, period); | |
| } catch (error) { | |
| console.error('Error changing chart period:', error); | |
| alert('Failed to load chart data. Please try again.'); | |
| } | |
| } | |
| // Filter Coins | |
| function filterCoins() { | |
| const searchTerm = searchInput.value.toLowerCase(); | |
| if (!searchTerm) { | |
| renderCryptoCards(allCoinsData); | |
| return; | |
| } | |
| const filteredCoins = allCoinsData.filter(coin => | |
| coin.name.toLowerCase().includes(searchTerm) || | |
| coin.symbol.toLowerCase().includes(searchTerm) | |
| ); | |
| if (filteredCoins.length === 0) { | |
| cryptoContainer.innerHTML = ` | |
| <div class="col-span-full text-center py-12"> | |
| <i class="fas fa-search text-4xl mb-4 opacity-50"></i> | |
| <h3 class="text-xl font-semibold">No cryptocurrencies found</h3> | |
| <p class="opacity-80">Try a different search term</p> | |
| </div> | |
| `; | |
| } else { | |
| renderCryptoCards(filteredCoins); | |
| } | |
| } | |
| // UI Helpers | |
| function showLoading() { | |
| loadingElement.classList.remove('hidden'); | |
| cryptoContainer.classList.add('hidden'); | |
| } | |
| function hideLoading() { | |
| loadingElement.classList.add('hidden'); | |
| } | |
| function showCryptoContainer() { | |
| cryptoContainer.classList.remove('hidden'); | |
| } | |
| function showError() { | |
| errorMessage.classList.remove('hidden'); | |
| cryptoContainer.classList.add('hidden'); | |
| } | |
| function hideError() { | |
| errorMessage.classList.add('hidden'); | |
| } | |
| function updateLastUpdated() { | |
| const now = new Date(); | |
| lastUpdatedElement.textContent = now.toLocaleTimeString(); | |
| } | |
| </script> | |
| </body> | |
| </html> |