| |
| |
| |
|
|
|
|
|
|
| const AppState = {
|
| coins: [],
|
| selectedCoin: null,
|
| selectedTimeframe: 7,
|
| selectedColorScheme: 'blue',
|
| charts: {},
|
| lastUpdate: null
|
| };
|
|
|
|
|
| const ColorSchemes = {
|
| blue: {
|
| primary: '#3B82F6',
|
| secondary: '#06B6D4',
|
| gradient: ['#3B82F6', '#06B6D4']
|
| },
|
| purple: {
|
| primary: '#8B5CF6',
|
| secondary: '#EC4899',
|
| gradient: ['#8B5CF6', '#EC4899']
|
| },
|
| green: {
|
| primary: '#10B981',
|
| secondary: '#34D399',
|
| gradient: ['#10B981', '#34D399']
|
| },
|
| orange: {
|
| primary: '#F97316',
|
| secondary: '#FBBF24',
|
| gradient: ['#F97316', '#FBBF24']
|
| },
|
| rainbow: {
|
| primary: '#3B82F6',
|
| secondary: '#EC4899',
|
| gradient: ['#3B82F6', '#8B5CF6', '#EC4899', '#F97316']
|
| }
|
| };
|
|
|
|
|
| Chart.defaults.color = '#E2E8F0';
|
| Chart.defaults.borderColor = 'rgba(255, 255, 255, 0.1)';
|
| Chart.defaults.font.family = "'Manrope', 'Inter', sans-serif";
|
| Chart.defaults.font.size = 13;
|
| Chart.defaults.font.weight = 500;
|
|
|
|
|
| document.addEventListener('DOMContentLoaded', () => {
|
| initNavigation();
|
| initCombobox();
|
| initChartControls();
|
| initColorSchemeSelector();
|
| loadInitialData();
|
| startAutoRefresh();
|
| });
|
|
|
|
|
| function initNavigation() {
|
| const navButtons = document.querySelectorAll('.nav-button');
|
| const pages = document.querySelectorAll('.page');
|
|
|
| navButtons.forEach(button => {
|
| button.addEventListener('click', () => {
|
| const targetPage = button.dataset.nav;
|
|
|
|
|
| navButtons.forEach(btn => btn.classList.remove('active'));
|
| button.classList.add('active');
|
|
|
|
|
| pages.forEach(page => {
|
| page.classList.toggle('active', page.id === targetPage);
|
| });
|
| });
|
| });
|
| }
|
|
|
|
|
| function initCombobox() {
|
| const input = document.getElementById('coinSelector');
|
| const dropdown = document.getElementById('coinDropdown');
|
|
|
| if (!input || !dropdown) return;
|
|
|
| input.addEventListener('focus', () => {
|
| dropdown.classList.add('active');
|
| if (AppState.coins.length === 0) {
|
| loadCoinsForCombobox();
|
| }
|
| });
|
|
|
| input.addEventListener('input', (e) => {
|
| const searchTerm = e.target.value.toLowerCase();
|
| filterComboboxOptions(searchTerm);
|
| });
|
|
|
| document.addEventListener('click', (e) => {
|
| if (!input.contains(e.target) && !dropdown.contains(e.target)) {
|
| dropdown.classList.remove('active');
|
| }
|
| });
|
| }
|
|
|
| async function loadCoinsForCombobox() {
|
| try {
|
| const response = await fetch('https://api.coingecko.com/api/v3/coins/markets?vs_currency=usd&order=market_cap_desc&per_page=100&page=1');
|
| const coins = await response.json();
|
| AppState.coins = coins;
|
| renderComboboxOptions(coins);
|
| } catch (error) {
|
| console.error('Error loading coins:', error);
|
| }
|
| }
|
|
|
| function renderComboboxOptions(coins) {
|
| const dropdown = document.getElementById('coinDropdown');
|
| if (!dropdown) return;
|
|
|
| dropdown.innerHTML = coins.map(coin => `
|
| <div class="combobox-option" data-coin-id="${coin.id}">
|
| <img src="${coin.image}" alt="${coin.name}" class="combobox-option-icon">
|
| <div class="combobox-option-text">
|
| <div class="combobox-option-name">${coin.name}</div>
|
| <div class="combobox-option-symbol">${coin.symbol}</div>
|
| </div>
|
| <div class="combobox-option-price">$${formatNumber(coin.current_price)}</div>
|
| </div>
|
| `).join('');
|
|
|
|
|
| dropdown.querySelectorAll('.combobox-option').forEach(option => {
|
| option.addEventListener('click', () => {
|
| const coinId = option.dataset.coinId;
|
| selectCoin(coinId);
|
| dropdown.classList.remove('active');
|
| });
|
| });
|
| }
|
|
|
| function filterComboboxOptions(searchTerm) {
|
| const options = document.querySelectorAll('.combobox-option');
|
| options.forEach(option => {
|
| const name = option.querySelector('.combobox-option-name').textContent.toLowerCase();
|
| const symbol = option.querySelector('.combobox-option-symbol').textContent.toLowerCase();
|
| const matches = name.includes(searchTerm) || symbol.includes(searchTerm);
|
| option.style.display = matches ? 'flex' : 'none';
|
| });
|
| }
|
|
|
| function selectCoin(coinId) {
|
| const coin = AppState.coins.find(c => c.id === coinId);
|
| if (!coin) return;
|
|
|
| AppState.selectedCoin = coin;
|
| document.getElementById('coinSelector').value = `${coin.name} (${coin.symbol.toUpperCase()})`;
|
|
|
|
|
| loadCoinChart(coinId, AppState.selectedTimeframe);
|
| }
|
|
|
|
|
| function initChartControls() {
|
|
|
| const timeframeButtons = document.querySelectorAll('[data-timeframe]');
|
| timeframeButtons.forEach(button => {
|
| button.addEventListener('click', () => {
|
| timeframeButtons.forEach(btn => btn.classList.remove('active'));
|
| button.classList.add('active');
|
|
|
| AppState.selectedTimeframe = parseInt(button.dataset.timeframe);
|
|
|
| if (AppState.selectedCoin) {
|
| loadCoinChart(AppState.selectedCoin.id, AppState.selectedTimeframe);
|
| }
|
| });
|
| });
|
| }
|
|
|
|
|
| function initColorSchemeSelector() {
|
| const schemeOptions = document.querySelectorAll('.color-scheme-option');
|
| schemeOptions.forEach(option => {
|
| option.addEventListener('click', () => {
|
| schemeOptions.forEach(opt => opt.classList.remove('active'));
|
| option.classList.add('active');
|
|
|
| AppState.selectedColorScheme = option.dataset.scheme;
|
|
|
| if (AppState.selectedCoin) {
|
| loadCoinChart(AppState.selectedCoin.id, AppState.selectedTimeframe);
|
| }
|
| });
|
| });
|
| }
|
|
|
|
|
| async function loadInitialData() {
|
| try {
|
| await Promise.all([
|
| loadMarketStats(),
|
| loadTopCoins(),
|
| loadMainChart()
|
| ]);
|
|
|
| AppState.lastUpdate = new Date();
|
| updateLastUpdateTime();
|
| } catch (error) {
|
| console.error('Error loading initial data:', error);
|
| }
|
| }
|
|
|
|
|
| async function loadMarketStats() {
|
| try {
|
| const response = await fetch('https://api.coingecko.com/api/v3/coins/markets?vs_currency=usd&order=market_cap_desc&per_page=10&page=1');
|
| const coins = await response.json();
|
|
|
|
|
| const totalMarketCap = coins.reduce((sum, coin) => sum + coin.market_cap, 0);
|
| const totalVolume = coins.reduce((sum, coin) => sum + coin.total_volume, 0);
|
| const btc = coins.find(c => c.id === 'bitcoin');
|
| const eth = coins.find(c => c.id === 'ethereum');
|
|
|
|
|
| const statsGrid = document.getElementById('statsGrid');
|
| if (statsGrid) {
|
| statsGrid.innerHTML = `
|
| ${createStatCard('Total Market Cap', formatCurrency(totalMarketCap), '+2.5%', 'positive', '#3B82F6')}
|
| ${createStatCard('24h Volume', formatCurrency(totalVolume), '+5.2%', 'positive', '#06B6D4')}
|
| ${createStatCard('Bitcoin', formatCurrency(btc?.current_price || 0), `${btc?.price_change_percentage_24h?.toFixed(2) || 0}%`, btc?.price_change_percentage_24h >= 0 ? 'positive' : 'negative', '#F7931A')}
|
| ${createStatCard('Ethereum', formatCurrency(eth?.current_price || 0), `${eth?.price_change_percentage_24h?.toFixed(2) || 0}%`, eth?.price_change_percentage_24h >= 0 ? 'positive' : 'negative', '#627EEA')}
|
| `;
|
| }
|
|
|
|
|
| document.getElementById('sidebarMarketCap').textContent = formatCurrency(totalMarketCap);
|
| document.getElementById('sidebarVolume').textContent = formatCurrency(totalVolume);
|
| document.getElementById('sidebarBTC').textContent = formatCurrency(btc?.current_price || 0);
|
| document.getElementById('sidebarETH').textContent = formatCurrency(eth?.current_price || 0);
|
|
|
|
|
| const btcElement = document.getElementById('sidebarBTC');
|
| const ethElement = document.getElementById('sidebarETH');
|
|
|
| if (btc?.price_change_percentage_24h >= 0) {
|
| btcElement.classList.add('positive');
|
| btcElement.classList.remove('negative');
|
| } else {
|
| btcElement.classList.add('negative');
|
| btcElement.classList.remove('positive');
|
| }
|
|
|
| if (eth?.price_change_percentage_24h >= 0) {
|
| ethElement.classList.add('positive');
|
| ethElement.classList.remove('negative');
|
| } else {
|
| ethElement.classList.add('negative');
|
| ethElement.classList.remove('positive');
|
| }
|
|
|
| } catch (error) {
|
| console.error('Error loading market stats:', error);
|
| }
|
| }
|
|
|
| function createStatCard(label, value, change, changeType, color) {
|
| const changeIcon = changeType === 'positive'
|
| ? '<path d="M12 19V5M5 12l7-7 7 7" stroke="currentColor" stroke-width="2"/>'
|
| : '<path d="M12 5v14M19 12l-7 7-7-7" stroke="currentColor" stroke-width="2"/>';
|
|
|
| return `
|
| <div class="glass-card stat-card">
|
| <div class="stat-header">
|
| <div class="stat-icon" style="background: linear-gradient(135deg, ${color}, ${color}CC); box-shadow: 0 0 20px ${color}66;">
|
| <svg width="24" height="24" viewBox="0 0 24 24" fill="none">
|
| <path d="M12 2L2 7L12 12L22 7L12 2Z" stroke="white" stroke-width="2"/>
|
| </svg>
|
| </div>
|
| <h3>${label}</h3>
|
| </div>
|
| <div class="stat-value-wrapper">
|
| <div class="stat-value">${value}</div>
|
| <div class="stat-change ${changeType}">
|
| <div class="change-icon-wrapper ${changeType}">
|
| <svg width="12" height="12" viewBox="0 0 24 24" fill="none">
|
| ${changeIcon}
|
| </svg>
|
| </div>
|
| <span class="change-value">${change}</span>
|
| </div>
|
| </div>
|
| </div>
|
| `;
|
| }
|
|
|
|
|
| async function loadTopCoins() {
|
| try {
|
| const response = await fetch('https://api.coingecko.com/api/v3/coins/markets?vs_currency=usd&order=market_cap_desc&per_page=20&page=1&sparkline=true');
|
| const coins = await response.json();
|
|
|
| const table = document.getElementById('topCoinsTable');
|
| if (!table) return;
|
|
|
| table.innerHTML = coins.map((coin, index) => {
|
| const change24h = coin.price_change_percentage_24h || 0;
|
| const change7d = coin.price_change_percentage_7d_in_currency || 0;
|
|
|
| return `
|
| <tr>
|
| <td>${index + 1}</td>
|
| <td>
|
| <div style="display: flex; align-items: center; gap: 12px;">
|
| <img src="${coin.image}" alt="${coin.name}" style="width: 32px; height: 32px; border-radius: 50%;">
|
| <div>
|
| <div style="font-weight: 600;">${coin.name}</div>
|
| <div style="font-size: 12px; color: var(--text-muted);">${coin.symbol.toUpperCase()}</div>
|
| </div>
|
| </div>
|
| </td>
|
| <td style="font-weight: 600;">$${formatNumber(coin.current_price)}</td>
|
| <td>
|
| <span class="badge ${change24h >= 0 ? 'badge-success' : 'badge-danger'}">
|
| ${change24h >= 0 ? '↑' : '↓'} ${Math.abs(change24h).toFixed(2)}%
|
| </span>
|
| </td>
|
| <td>
|
| <span class="badge ${change7d >= 0 ? 'badge-success' : 'badge-danger'}">
|
| ${change7d >= 0 ? '↑' : '↓'} ${Math.abs(change7d).toFixed(2)}%
|
| </span>
|
| </td>
|
| <td>$${formatNumber(coin.market_cap)}</td>
|
| <td>$${formatNumber(coin.total_volume)}</td>
|
| <td>
|
| <canvas id="spark-${coin.id}" width="100" height="30"></canvas>
|
| </td>
|
| </tr>
|
| `;
|
| }).join('');
|
|
|
|
|
| setTimeout(() => {
|
| coins.forEach(coin => {
|
| if (coin.sparkline_in_7d && coin.sparkline_in_7d.price) {
|
| createSparkline(`spark-${coin.id}`, coin.sparkline_in_7d.price, coin.price_change_percentage_24h >= 0);
|
| }
|
| });
|
| }, 100);
|
|
|
| } catch (error) {
|
| console.error('Error loading top coins:', error);
|
| }
|
| }
|
|
|
|
|
| function createSparkline(canvasId, data, isPositive) {
|
| const canvas = document.getElementById(canvasId);
|
| if (!canvas) return;
|
|
|
| const color = isPositive ? '#10B981' : '#EF4444';
|
|
|
| new Chart(canvas, {
|
| type: 'line',
|
| data: {
|
| labels: data.map((_, i) => i),
|
| datasets: [{
|
| data: data,
|
| borderColor: color,
|
| backgroundColor: color + '20',
|
| borderWidth: 2,
|
| fill: true,
|
| tension: 0.4,
|
| pointRadius: 0
|
| }]
|
| },
|
| options: {
|
| responsive: false,
|
| maintainAspectRatio: false,
|
| plugins: { legend: { display: false }, tooltip: { enabled: false } },
|
| scales: { x: { display: false }, y: { display: false } }
|
| }
|
| });
|
| }
|
|
|
|
|
| async function loadMainChart() {
|
| try {
|
| const response = await fetch('https://api.coingecko.com/api/v3/coins/markets?vs_currency=usd&order=market_cap_desc&per_page=10&page=1&sparkline=true');
|
| const coins = await response.json();
|
|
|
| const canvas = document.getElementById('mainChart');
|
| if (!canvas) return;
|
|
|
| const ctx = canvas.getContext('2d');
|
|
|
| if (AppState.charts.main) {
|
| AppState.charts.main.destroy();
|
| }
|
|
|
| const colors = ['#3B82F6', '#06B6D4', '#10B981', '#F59E0B', '#EF4444', '#8B5CF6', '#EC4899', '#F97316', '#14B8A6', '#6366F1'];
|
|
|
| const datasets = coins.slice(0, 10).map((coin, index) => ({
|
| label: coin.name,
|
| data: coin.sparkline_in_7d.price,
|
| borderColor: colors[index],
|
| backgroundColor: colors[index] + '20',
|
| borderWidth: 3,
|
| fill: false,
|
| tension: 0.4,
|
| pointRadius: 0,
|
| pointHoverRadius: 6,
|
| pointHoverBackgroundColor: colors[index],
|
| pointHoverBorderColor: '#fff',
|
| pointHoverBorderWidth: 2
|
| }));
|
|
|
| AppState.charts.main = new Chart(ctx, {
|
| type: 'line',
|
| data: {
|
| labels: Array.from({length: 168}, (_, i) => i),
|
| datasets: datasets
|
| },
|
| options: {
|
| responsive: true,
|
| maintainAspectRatio: false,
|
| interaction: {
|
| mode: 'index',
|
| intersect: false,
|
| },
|
| plugins: {
|
| legend: {
|
| display: true,
|
| position: 'top',
|
| align: 'end',
|
| labels: {
|
| usePointStyle: true,
|
| pointStyle: 'circle',
|
| padding: 15,
|
| font: { size: 12, weight: 600 }
|
| }
|
| },
|
| tooltip: {
|
| backgroundColor: 'rgba(15, 23, 42, 0.95)',
|
| titleColor: '#fff',
|
| bodyColor: '#E2E8F0',
|
| borderColor: 'rgba(6, 182, 212, 0.5)',
|
| borderWidth: 1,
|
| padding: 16,
|
| displayColors: true,
|
| boxPadding: 8,
|
| usePointStyle: true
|
| }
|
| },
|
| scales: {
|
| x: {
|
| grid: { display: false },
|
| ticks: { display: false }
|
| },
|
| y: {
|
| grid: {
|
| color: 'rgba(255, 255, 255, 0.05)',
|
| drawBorder: false
|
| },
|
| ticks: {
|
| color: '#94A3B8',
|
| callback: function(value) {
|
| return '$' + formatNumber(value);
|
| }
|
| }
|
| }
|
| }
|
| }
|
| });
|
|
|
| } catch (error) {
|
| console.error('Error loading main chart:', error);
|
| }
|
| }
|
|
|
|
|
| async function loadCoinChart(coinId, days) {
|
| try {
|
| const response = await fetch(`https://api.coingecko.com/api/v3/coins/${coinId}/market_chart?vs_currency=usd&days=${days}`);
|
| const data = await response.json();
|
|
|
| const scheme = ColorSchemes[AppState.selectedColorScheme];
|
|
|
|
|
| const coin = AppState.selectedCoin;
|
| document.getElementById('chartTitle').textContent = `${coin.name} (${coin.symbol.toUpperCase()}) Price Chart`;
|
| document.getElementById('chartPrice').textContent = `$${formatNumber(coin.current_price)}`;
|
|
|
| const change = coin.price_change_percentage_24h;
|
| const changeElement = document.getElementById('chartChange');
|
| changeElement.textContent = `${change >= 0 ? '+' : ''}${change.toFixed(2)}%`;
|
| changeElement.className = `badge ${change >= 0 ? 'badge-success' : 'badge-danger'}`;
|
|
|
|
|
| const priceCanvas = document.getElementById('priceChart');
|
| if (priceCanvas) {
|
| const ctx = priceCanvas.getContext('2d');
|
|
|
| if (AppState.charts.price) {
|
| AppState.charts.price.destroy();
|
| }
|
|
|
| const labels = data.prices.map(p => new Date(p[0]));
|
| const prices = data.prices.map(p => p[1]);
|
|
|
| AppState.charts.price = new Chart(ctx, {
|
| type: 'line',
|
| data: {
|
| labels: labels,
|
| datasets: [{
|
| label: 'Price (USD)',
|
| data: prices,
|
| borderColor: scheme.primary,
|
| backgroundColor: scheme.primary + '20',
|
| borderWidth: 3,
|
| fill: true,
|
| tension: 0.4,
|
| pointRadius: 0,
|
| pointHoverRadius: 8,
|
| pointHoverBackgroundColor: scheme.primary,
|
| pointHoverBorderColor: '#fff',
|
| pointHoverBorderWidth: 3
|
| }]
|
| },
|
| options: {
|
| responsive: true,
|
| maintainAspectRatio: false,
|
| plugins: {
|
| legend: { display: false },
|
| tooltip: {
|
| backgroundColor: 'rgba(15, 23, 42, 0.95)',
|
| padding: 16,
|
| displayColors: false,
|
| callbacks: {
|
| label: function(context) {
|
| return 'Price: $' + formatNumber(context.parsed.y);
|
| }
|
| }
|
| }
|
| },
|
| scales: {
|
| x: {
|
| type: 'time',
|
| time: {
|
| unit: days <= 1 ? 'hour' : days <= 7 ? 'day' : days <= 30 ? 'day' : 'week'
|
| },
|
| grid: { display: false },
|
| ticks: { color: '#94A3B8', maxRotation: 0, autoSkip: true, maxTicksLimit: 8 }
|
| },
|
| y: {
|
| grid: { color: 'rgba(255, 255, 255, 0.05)', drawBorder: false },
|
| ticks: {
|
| color: '#94A3B8',
|
| callback: function(value) {
|
| return '$' + formatNumber(value);
|
| }
|
| }
|
| }
|
| }
|
| }
|
| });
|
| }
|
|
|
|
|
| const volumeCanvas = document.getElementById('volumeChart');
|
| if (volumeCanvas) {
|
| const ctx = volumeCanvas.getContext('2d');
|
|
|
| if (AppState.charts.volume) {
|
| AppState.charts.volume.destroy();
|
| }
|
|
|
| const volumeLabels = data.total_volumes.map(v => new Date(v[0]));
|
| const volumes = data.total_volumes.map(v => v[1]);
|
|
|
| AppState.charts.volume = new Chart(ctx, {
|
| type: 'bar',
|
| data: {
|
| labels: volumeLabels,
|
| datasets: [{
|
| label: 'Volume',
|
| data: volumes,
|
| backgroundColor: scheme.secondary + '80',
|
| borderColor: scheme.secondary,
|
| borderWidth: 2,
|
| borderRadius: 6,
|
| borderSkipped: false
|
| }]
|
| },
|
| options: {
|
| responsive: true,
|
| maintainAspectRatio: false,
|
| plugins: {
|
| legend: { display: false },
|
| tooltip: {
|
| backgroundColor: 'rgba(15, 23, 42, 0.95)',
|
| padding: 16,
|
| callbacks: {
|
| label: function(context) {
|
| return 'Volume: $' + formatNumber(context.parsed.y);
|
| }
|
| }
|
| }
|
| },
|
| scales: {
|
| x: {
|
| type: 'time',
|
| time: {
|
| unit: days <= 1 ? 'hour' : days <= 7 ? 'day' : days <= 30 ? 'day' : 'week'
|
| },
|
| grid: { display: false },
|
| ticks: { color: '#94A3B8', maxRotation: 0, autoSkip: true, maxTicksLimit: 8 }
|
| },
|
| y: {
|
| grid: { color: 'rgba(255, 255, 255, 0.05)', drawBorder: false },
|
| ticks: {
|
| color: '#94A3B8',
|
| callback: function(value) {
|
| return '$' + formatNumber(value);
|
| }
|
| }
|
| }
|
| }
|
| }
|
| });
|
| }
|
|
|
| } catch (error) {
|
| console.error('Error loading coin chart:', error);
|
| }
|
| }
|
|
|
|
|
| function startAutoRefresh() {
|
| setInterval(() => {
|
| loadMarketStats();
|
| AppState.lastUpdate = new Date();
|
| updateLastUpdateTime();
|
| }, 60000);
|
| }
|
|
|
| function updateLastUpdateTime() {
|
| const element = document.getElementById('lastUpdate');
|
| if (!element) return;
|
|
|
| const now = new Date();
|
| const diff = Math.floor((now - AppState.lastUpdate) / 1000);
|
|
|
| if (diff < 60) {
|
| element.textContent = 'Just now';
|
| } else if (diff < 3600) {
|
| element.textContent = `${Math.floor(diff / 60)}m ago`;
|
| } else {
|
| element.textContent = `${Math.floor(diff / 3600)}h ago`;
|
| }
|
| }
|
|
|
|
|
| window.refreshData = function() {
|
| loadInitialData();
|
| };
|
|
|
|
|
| function formatNumber(num) {
|
| if (num === null || num === undefined || isNaN(num)) {
|
| return '0.00';
|
| }
|
| num = Number(num);
|
| if (num >= 1e12) return (num / 1e12).toFixed(2) + 'T';
|
| if (num >= 1e9) return (num / 1e9).toFixed(2) + 'B';
|
| if (num >= 1e6) return (num / 1e6).toFixed(2) + 'M';
|
| if (num >= 1e3) return (num / 1e3).toFixed(2) + 'K';
|
| return num.toFixed(2);
|
| }
|
|
|
| function formatCurrency(num) {
|
| return '$' + formatNumber(num);
|
| }
|
|
|
|
|
| window.AppState = AppState;
|
| window.selectCoin = selectCoin;
|
|
|