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> |