Components
A sleek, real-time cryptocurrency dashboard with interactive price charts, asset tracking, and market news. Features dynamic price updates, portfolio overview, and detailed asset information with customizable time frames. Perfect for fintech applications and crypto trading platforms.
Loading preview...
import { useState, useEffect, useRef } from "react";
// import { cn } from "@/lib/utils"; // cn is imported but not used, can be removed if not planned for use.
import {
TrendingUp,
TrendingDown,
// RefreshCw, // Imported but not used
Clock,
// ChevronDown, // Imported but not used
// ChevronUp, // Imported but not used
Eye,
EyeOff,
BarChart2,
Zap,
Clock3,
ArrowUpRight,
ArrowDownRight,
Menu,
Layers
} from "lucide-react";
// Mock crypto data
const cryptoData = [
{
id: "bitcoin",
name: "Bitcoin",
symbol: "BTC",
price: 48235.67,
change24h: 2.34,
volume24h: 28453921567,
marketCap: 924573622591,
sparkline: [46782, 47102, 47385, 46920, 47210, 47850, 48100, 47950, 48235],
logo: "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAzMiAzMiI+PHBhdGggZmlsbD0iI2Y3OTMxYSIgZD0iTTE1Ljk2OSAzMS4xMDJjLTE0LjA5IDAtMTcuNTctMTMuMjQ1LTYuNDc5LTIwLjY3M2wsLjAwMi0xMS44OTJoMTIuOTUzbC4wMDIgMTEuODkxYzExLjA5MSA3LjQyOCA3LjYxIDIwLjY3NC02LjQ3OCAyMC42NzR6Ii8+PHBhdGggZmlsbD0iI2ZmZmJlYiIgZD0iTTIwLjgxOSAxNS44MzZ2LS41aDIuOTQ4di01LjAwMmgtMi44OTlsLjAwMi0zLjU1NWgtMy41NTVsLS4wMDEgMy41NTVoLTIuOTAxdi01aDIuODUzbC0uMDAyLTMuNTUzaC0zLjU1M2wtLjAwMiAzLjU1M2gtMi44NTZ2NWgyLjg1NnYuNWMtNC43NTcgMS44NzMtNC41NTcgOC42ODQuMzk5IDEwLjI3My00Ljk2MSAxLjU5NC01LjE1NiA4LjM5Ni0uMzg5IDEwLjI3MWgyLjg1MnYuNjJoMy41NTF2LS42MmgyLjkwMXY0Ljk4aC0yLjg1M3YuNjJoMy41NTJ2LS42MmgyLjg5OXYtNC45OGgtMi45MzdjNC43NjUtMS44OTEgNC41NjUtOC43ODQtLjQxLTEwLjI5MSA0Ljk3LTEuNTEyIDUuMTc1LTguNDI0LjM5OC0xMC4yNzN6Ii8+PHBhdGggZmlsbD0iI2Y3OTMxYSIgZD0iTTE4Ljc5MSAxOS4yNXYzLjVoLTUuNTgydi0zLjVoNS41ODJ6bTAtNy41djMuNWgtNS41ODJ2LTMuNWg1LjU4MnoiLz48L3N2Zz4="
},
{
id: "ethereum",
name: "Ethereum",
symbol: "ETH",
price: 3267.42,
change24h: -1.27,
volume24h: 15274362187,
marketCap: 392578623410,
sparkline: [3318, 3301, 3275, 3290, 3310, 3280, 3260, 3250, 3267],
logo: "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAzMiAzMiI+PGcgZmlsbD0ibm9uZSIgZmlsbC1ydWxlPSJldmVub2RkIj48Y2lyY2xlIGN4PSIxNiIgY3k9IjE2IiByPSIxNiIgZmlsbD0iIzYyN0VFQSIvPjxnIGZpbGw9IiNGRkYiIGZpbGwtcnVsZT0ibm9uemVybyI+PHBhdGggZmlsbC1vcGFjaXR5PSIuNjAyIiBkPSJNMTYuNDk4IDR2OC44N2w3LjQ5NyAzLjM1eiIvPjxwYXRoIGQ9Ik0xNi40OTggNHY4Ljg3bC03LjQ5NyAzLjM1eiIvPjxwYXRoIGZpbGwtb3BhY2l0eT0iLjYwMiIgZD0iTTE2LjQ5OCAyMC45Njh2Ni45OTdsMy41MDUuMDA1IDcuNTM4LTEwLjU2NXoiLz48cGF0aCBkPSJNMTYuNDk4IDI3Ljk2NXYtNi45OTdsLTcuNDk3LTMuNTYzeiIvPjxwYXRoIGZpbGwtb3BhY2l0eT0iLjIiIGQ9Ik0xNi40OTggMTkuNTdsNy40OTctMy41NjQtNy40OTctMy40MTZ6Ii8+PHBhdGggZmlsbC1vcGFjaXR5PSIuNjAyIiBkPSJNOS4wMDEgMTYuMDA2bDcuNDk3IDMuNTY0di02Ljk4eiIvPjwvZz48L2c+PC9zdmc+"
},
{
id: "solana",
name: "Solana",
symbol: "SOL",
price: 102.73,
change24h: 5.86,
volume24h: 2845391287,
marketCap: 44751928374,
sparkline: [97.21, 98.45, 99.12, 98.75, 99.45, 100.12, 101.34, 102.15, 102.73],
logo: "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA0MDAgNDAwIj48ZGVmcz48c3R5bGU+LmNscy0xe2ZpbGw6IzAwZmZiZDt9LmNscy0ye2ZpbGw6I2ZmZjt9LmNscy0ze2ZpbGw6IzkyZjBkZDt9PC9zdHlsZT48L2RlZnM+PGNpcmNsZSBjbGFzcz0iY2xzLTEiIGN4PSIyMDAiIGN5PSIyMDAiIHI9IjIwMCIvPjxwYXRoIGNsYXNzPSJjbHMtMiIgZD0iTTI2OS4zLDk4LjAzaC0xMzlhNy4zLDcuMywwLDAsMC02LjE5LDMuNDFsLTMwLDUyYTcuMjgsNy4yOCwwLDAsMCwwLDcuMjlsMTIuMjQsMjEuMjRhNy4yOSw3LjI5LDAsMCwwLDEyLjgxLTEuODVsLjM2LTFhNy4zLDcuMywwLDAsMSwxMy43NS4wNmwuNDIsMXN2MGE3LjMsNy4zLDAsMCwwLDEyLjgxLjgzbDEuODUtNC00Mi0yMy40MUwxMzgsMTEzSDI2OS4zYTcuMyw3LjMsMCwwLDEsNy4zLDcuM3YuMDhhNy4zLDcuMywwLDAsMS03LjMsNy4zSDIwMy4xMWE3LjI4LDcuMjgsMCwwLDAtNi4xOSwzLjQxbC02Ljc2LDExLjc1YTcuMyw3LjMsMCwwLDAsMCw3LjMzbDE0LjYzLDI0Ljk1YTcuMjksNy4yOSwwLDAsMCw2LjI1LDMuNTJIMjY5LjNhNy4zLDcuMywwLDAsMSw3LjMsNy4zdjBhNy4zLDcuMywwLDAsMS03LjMsNy4zSDE3My42NWE3LjI5LDcuMjksMCwwLDAtNi4yNCwzLjUybC02LjgsNTIuMzNhNy4zLDcuMywwLDAsMCw3LjI2LDguMjhIMjY5LjNhNy4zLDcuMywwLDAsMSw3LjMsNy4zdjBhNy4zLDcuMywwLDAsMS03LjMsNy4zSDEzMS4wNmE3LjMsNy4zLDAsMCwxLTcuMy03LjN2LS4wOGE3LjMsNy4zLDAsMCwxLDIuNjctNS42N2w2LjQxLTUuMzdBNy4zLDcuMywwLDAsMCwxMzUuMjgsMjcwbC0uMzYtMWE3LjMsNy4zLDAsMCwwLTEzLjc1LS4wNmwtLjQyLDFhNy4zLDcuMywwLDAsMS0xMi44OCwyLjczTDExLjI2LDEyMC40MWE3LjI2LDcuMjYsMCwwLDEtMS0zLjc0di0xMGE3LjMsNy4zLDAsMCwxLDcuMy03LjNIMTMxYTcuMyw3LjMsMCwwLDEsNy4zLDcuM3YuMDhhNy4zLDcuMywwLDAsMS03LjMsNy4zSDgwLjA5YTcuMyw3LjMsMCwwLDAtNi4zNiwzLjcybC0xLjkzLDMuNTZhNy4yNiw3LjI2LDAsMCwwLC4wNyw3LjIxbDguMjgsMTQuOSw0MC40NSwyMi40OEwxNDAsMTYwSDIxMy45YTcuMjksNy4yOSwwLDAsMCw2LjI1LTMuNTJsMTQuNjMtMjUuMDFhNy4yOSs3LjI5LDAsMCwwLDAtNy4yOUwyMDQuNDgsMTA2LjVMMTc4LjMsMTEzSDEzOWw5Mi0xNWgyMS43MnYtLjA4YTcuMyw3LjMsMCwwLDEsMTQuNiwwdi4wOEEyLjc1LDIuNzUsMCwwLDEsMjY5LjMsOTguMDNaIi8+PHBhdGggY2xhc3M9ImNscy0zIiBkPSJNMjU0LjE2LDI1Nkg2Ni41NWE1LDUsMCwwLDEtNS01VjYyLjY1YTUsNSwwLDAsMSw1LTVIMjU0LjE2YTUsNSwwLDAsMSw1LDV2MTguN0gyMzguODFWNjMuMzJINjcuMjFWMjUwLjM1SDIzOC44MXYtMTguN2gyMC4zNXYxOS4zNkE1LDUsMCwwLDEsMjU0LjE2LDI1NloiLz48L3N2Zz4="
},
{
id: "cardano",
name: "Cardano",
symbol: "ADA",
price: 0.5832,
change24h: 0.78,
volume24h: 925671498,
marketCap: 20565781234,
sparkline: [0.5775, 0.5790, 0.5810, 0.5795, 0.5820, 0.5830, 0.5825, 0.5820, 0.5832],
logo: "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAzMiAzMiI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJhIiB4MT0iLjUiIHkxPSIuOTg0IiB4Mj0iLjUiIHkyPSIuMDE2IiBncmFkaWVudFVuaXRzPSJvYmplY3RCb3VuZGluZ0JveCI+PHN0b3Agb2Zmc2V0PSIwIiBzdG9wLWNvbG9yPSIjMDEzM0FFIi8+PHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjMDM3NkQ4Ii8+PC9saW5lYXJHcmFkaWVudD48L2RlZnM+PGcgZmlsbD0ibm9uZSI+PGNpcmNsZSBmaWxsPSJ1cmwoI2EpIiBjeD0iMTYiIGN5PSIxNiIgcj0iMTYiLz48cGF0aCBkPSJNMjEuMzA3IDE2LjMxNWMuMDA4LS4xMDguMDMtLjE0My4wNzQtLjE1MmEuNDczLjQ3MyAwIDAwLjE3My0uMDY3Yy4yMDctLjExNi4zMS0uMzIuMzEtLjYxMyAwLS4yOTgtLjEwNC0uNTA4LS4zMS0uNjNhLjQ3My4Q3MyAwIDAwLS4xNzMtLjA2N2MtLjA0My0uMDA4LS4wNjUtLjA0Ni0uMDc0LS4xNTNhMS45MTcgMS45MTcgMCAwMC0uMDg2LS4zNzhjLS4wNjctLjIxLS4xNi0uNC0uMjgtLjU3MmEzLjMyNyAzLjMyNyAwIDAwLS40MTMtLjUxNWMuMTEtLjE2NC4xNzItLjM1LjE4Ny0uNTU1LjAwNy0uMTIzLjAzLS4xNi4wNzItLjE3NWwuMDkzLS4wMzhjLjIzNC0uMTE5LjM1MS0uMzQzLjM1MS0uNjczIDAtLjI5OC0uMTA0LS41MDctLjMxLS42MjlhLjQ5Ny40OTcgMCAwMC0uMTc0LS4wNjdjLS4wNDItLjAwOC0uMDY1LS4wNDUtLjA3NC0uMTUzYTQuOTg1IDQuOTg1IDAgMDAtLjE5LS44MzggNC45OCA0Ljk4IDAgMDAtLjMyOC0uNzU3Yy0uMTI5LS4yNTctLjI4LS41MDYtLjQ1LS43NDRhMTAuMTg2IDEwLjE4NiAwIDAwLS4yODYtLjM5NWMtLjEzNy0uMTgtLjI4Ni0uMzU0LS40NDMtLjUyNGE5LjMwOCAxMC4zMDggMCAwMC0uNTE2LS41MzEgNi42MDQgNi42MDQgMCAwMC0uNTM0LS40NjdjLS4xMzgtLjEwOC0uMjgtLjIxLS40MjYtLjMxMWFNuMjYzIDYuMjYzIDAgMDAtLjMxOC0uMThMMTYuNjkgNmwtLjIzOS0uMTE3YTguOTQ2IDguOTQ2IDAgMDAtLjczNi0uMzFjLS4yMDQtLjA3NC0uNDA4LS4xNDMtLjYxOC0uMjA2YTkuMTgzIDkuMTgzIDAgMDAtLjczNS0uMTYyIDQuOTk4IDQuOTk4IDAgMDAtLjU1Mi0uMDY3Yy0uMDc1LS4wMDYtLjExNi0uMDQyLS4xMjQtLjEwNmEuNDE0LjQxNCAwIDAwLS4wNjYtLjE3NkM5LjMyOSA3LjAyIDcuMTA0IDkuMjE4IDcuMTA0IDEyYzAgMi41MzYgMS45MzMgNC41NzcgNC4zMzEgNC41NzcgMi40MDMgMCA0LjMzNi0yLjA0IDQuMzM2LTQuNTc3YzAtLjExOC0uMDEtLjIzNi0uMDItLjM1NS0uMDExLS4wNzMuMDI2LS4wOTUuMDk1LS4wNjMuMDc4LjAzNC4xNTYuMDcuMjMuMTE0LjA3NC4wNDMuMTUuMDg3LjIyMi4xMzVsLjIwMy4xM2MuMDg2LjA1NS4xNjguMTE1LjI0Ny4xNzhhNi4xNCA2LjE0IDAgMDEuNTkuNTE1IDcuNTYgNy41NiAwIDAxMS4wMzYgMS4yNDZjLjExNi4xNy4yMjUuMzQ3LjMyMi41MjguMDk4LjE4Mi4xODcuMzY3LjI1My41NGEzLjU2OCAzLjU2OCAwIDAxLjE5Ny41MDQgMS40NiAxLjQ2IDAgMDEuMDcuMjg3LjQ3OS40NzkgMCAwMS4wNDMuMTM4Yy4wMDguMDQtLjAxOS4wNTYtLjA3Mi4wNDEtLjA0OS0uMDEyLS4wOTktLjAyLS4xNS0uMDMtLjA0OC0uMDEtLjA5OS0uMDIzLS4xNS0uMDM0YTYuOTI4IDYuOTI4IDAgMDEtLjM1Mi0uMDk4Yy0uMTEtLjAzNC0uMjIxLS4wNy0uMzI4LS4xMTJhNC43NTEgNC43NTEgMCAwMS0uMzIyLS4xMzYgMy4zMDEgMy4zMDEgMCAwMS0uMjM4LS4xMmMtLjA2OC0uMDQtLjA3Mi0uMDg3LS4wMDYtLjEzYy4wNjctLjQ1LjA2My0uOTAxLS4wMS0xLjM1ek0yNC4zMDIgMTAuODgyYy0uMDgtLjE4OC0uMTc3LS4zNjctLjI4LS41NDQtLjIzOC0uNDEzLS41Mi0uODA0LS44MzgtMS4xNzRhMy40MDQgMy40MDQgMCAwMC0uNDktLjQ2NCAyLjM0MyAyLjM0MyAwIDAwLS41Mi0uMzAzYy0uMDg0LS4wMzYtLjE3LS4wNy0uMjU2LS4xMDJhMy44MzMgMy44MzMgMCAwMC0uMjU5LS4wNzkgMS44NTggMS44NTggMCAwMC0uMjU1LS4wMzZDNS4zMyAyNC43MTIgNC4zMDYgMjQuNDg2IDQuMzA2IDI0YzAgLjM3Mi4xNy42ODUuNDI5Ljc5M2EuMzk4LjM5OCAwIDAwLjE3Mi4wNDZDNy43NzggMTQuNzA2IDExLjQ3NCAxNC40NyAxMS40NzQgMTRjMCAuMDQ1LjAxMi4wODcuMDM1LjEyMy4wMDYuMDA3LjAyMS4wMTQuMDQyLjAxOWwuMDY2LjAxMmMuMTE1LjAxNy4yMjkuMDE3LjM0NC0uMDExLjExMS0uMDI1LjIxLS4wNy4yOTMtLjEzNGE2Ljc1OCA2Ljc1OCAwIDAxLjQ4NC0uMjkgMy41NiAzLjU2IDAgMDEuNTk4LS4yNTljLjE2LS4wNTQuMzEtLjEwNi40NTItLjE1N2EyLjk5MyAyLjk5MyAwIDAxLjYwMi0uMTczbC4xMzQtLjAyNmMuMDQ0LS4wMS4wODgtLjAxMy4xMzItLjAxM2ExLjMzIDEuMzMgMCAwMC4xMzItLjAxMiAxLjY3MSAxLjY3MSAwIDAwLjM5My0uMDEyIDQuMzQ4IDQuMzQ4IDAgMDAuMzY0LS4wMzJsLjA2NS0uMDA2Yy4wMi0uMDAyLjAzMy0uMDA3LjA0MS0uMDE0LjAxLS4wMDcuMDE0LS4wMi4wMTQtLjAzM2wtLjAwMy0uMDIxYy4xNjgtLjg3MS4xMzQtMS43MzQtLjEtMi41ODN6bS0xLjQ5OSAxNS4xOTVjLS4wMjQuMDA1LS4wNDguMDExLS4wNzMuMDExLS4wMjIgMC0uMDQ0LS4wMDUtLjA2Ni0uMDEtLjA1My0uMDEyLS4xMDMtLjAzMy0uMTQ4LS4wNjFhMS4xMDggMS4xMDggMCAwMS0uMTQ3LS4wNzNjLS4wNTQtLjAzMi0uMTA3LS4wNy0uMTU4LS4xMTMtLjA0Mi0uMDM0LS4wODQtLjA3MS0uMTI0LS4xMThhMS4zMyAxLjMzIDAgMDEtLjA5LS4xMzJsLS4wMjEtLjA0MWMtLjAxNC0uMDMxLS4wMjYtLjA2Mi0uMDM5LS4wOTMtLjAxMi0uMDMyLS4wMTYtLjA2NC0uMDEyLS4wOTdsLjAwOS0uMDQ1Yy4wMzgtLjEzNC4xMDctLjI0My4yMDUtLjMyOS4wODYtLjA3NC4xOC0uMTI3LjI4LS4xNTlhMS4zMzIgMS4zMzIgMCAwMS4yODYtLjExNGwxLjIyMS0uMjk2Yy4xMTItLjAyNy4yMjItLjA0My4zMjgtLjA1MWExLjgzNiAxLjgzNiAwIDAxLjMyLS4wMTMgNC40MzIgNC40MzIgMCAwMTAuMDA0IDguNzk5IDEuODM2IDEuODM2IDAgMDEtLjMyLjAxMyAyLjIxNCAyLjIxNCAwIDAxLS4zMjkuMDUybC0xLjIyMS4yOTZhMS4zMzIgMS4zMzIgMCAwMS0uMjg2LjExNCAuOTYxLjk2MSAwIDAxLS4yOC4xNTljLS4wOTcuMDg2LS4xNjcuMTk1LS4yMDUuMzI5bC0uMDA5LjA0NGMtLjAwNC4wMzQuMDAxLjA2NS4wMTMuMDk4LjAxMy4wMy4wMjUuMDYyLjAzOS4wOTRsLjAyMS4wNDFjLjAyNC4wNDYuMDYxLjA5LjA5LjEzMi4wNS4wNDcuMTAyLjA4NC4xMjQuMTE4LjA1MS4wNDQuMTAyLjA4NC4xNTguMTE0LjA1LjAyNC4xMDQuMDQ5LjE0Ny4wNzNjLjA0Ni4wMjguMDk1LjA0OS4xNDkuMDYxLjAyMS4wMDYuMDQ0LjAxLjA2Ni4wMTEuMDI0IDAgLjA0OC0uMDA1LjA3My0uMDEyek0yNiA4LjE3Yy41MTYgMCAuOTM0LjQxOS45MzQuOTM0cy0uNDE4LjkzNC0uOTM0LjkzNC0uOTM0LS40MTktLjkzNC0uOTM0UzI1LjQ4NCA4LjE3IDI2IDguMTd6IiBmaWxsPSIjRkZGIi8+PC9nPjwvc3ZnPg=="
},
];
const trendingNews = [
{
title: "Bitcoin Surges Past $48K as Institutional Interest Grows",
source: "CryptoDaily",
time: "2 hours ago",
url: "#"
},
{
title: "Ethereum Layer 2 Solution Sees Record Transaction Volume",
source: "BlockchainNews",
time: "4 hours ago",
url: "#"
},
{
title: "Major Bank Announces Crypto Custody Services for Institutions",
source: "FinanceWeek",
time: "6 hours ago",
url: "#"
}
];
// Sparkline chart component
const SparklineChart = ({ data, color, width = 100, height = 30 }: { data: number[], color: string, width?: number, height?: number }) => {
// Basic sparkline logic
const minVal = Math.min(...data); // Renamed 'min' to 'minVal' to avoid conflict with potential built-ins
const maxVal = Math.max(...data); // Renamed 'max' to 'maxVal'
const range = maxVal - minVal;
const points = data.map((value, index) => {
const x = (data.length > 1 ? (index / (data.length - 1)) : 0.5) * width; // Handle single data point
const y = height - (range !== 0 ? ((value - minVal) / range) * height : height / 2); // Handle flat line (range is 0)
return `${x},${y}`;
}).join(' ');
return (
<svg width={width} height={height} className="overflow-visible">
<defs>
<linearGradient id={`gradient-${color.replace('#', '')}`} x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" stopColor={color} stopOpacity="0.2" />
<stop offset="100%" stopColor={color} stopOpacity="0" />
</linearGradient>
</defs>
<polyline
points={points}
fill="none"
stroke={color}
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
<polygon
points={`0,${height} ${points} ${width},${height}`}
fill={`url(#gradient-${color.replace('#', '')})`}
/>
</svg>
);
};
export const Component = () => {
const [showBalance, setShowBalance] = useState(true);
const [selectedCrypto, setSelectedCrypto] = useState(cryptoData[0]);
const [timeFrame, setTimeFrame] = useState<'1h' | '24h' | '7d' | '30d'>('24h');
const [currentTime, setCurrentTime] = useState(new Date());
const priceRef = useRef<HTMLDivElement>(null);
// Format currency
const formatCurrency = (value: number, maximumFractionDigits = 2) => {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
maximumFractionDigits
}).format(value);
};
// Format large numbers with abbreviations
const formatNumber = (value: number) => {
if (value >= 1e9) {
return `$${(value / 1e9).toFixed(2)}B`;
} else if (value >= 1e6) {
return `$${(value / 1e6).toFixed(2)}M`;
} else if (value >= 1e3) {
return `$${(value / 1e3).toFixed(2)}K`;
}
return `$${value.toFixed(2)}`;
};
// Simulated real-time price updates
useEffect(() => {
const interval = setInterval(() => {
// Update crypto data with small random changes
// Note: This direct mutation of `cryptoData` is generally not recommended in React.
// For a real application, this data would likely come from state or a store.
cryptoData.forEach(crypto => {
const change = crypto.price * (Math.random() * 0.006 - 0.003);
crypto.price += change;
crypto.change24h += (change / crypto.price) * 100 * (Math.random() > 0.5 ? 1 : -1) * 0.1;
// Update sparkline
crypto.sparkline.shift();
crypto.sparkline.push(crypto.price);
});
// Update selected crypto if it's in the list
const updated = cryptoData.find(c => c.id === selectedCrypto.id);
if (updated) {
setSelectedCrypto({...updated}); // Create a new object to trigger re-render
// Add price change animation
if (priceRef.current) {
priceRef.current.classList.add('price-changed');
setTimeout(() => {
if (priceRef.current) {
priceRef.current.classList.remove('price-changed');
}
}, 1000);
}
}
// Update current time
setCurrentTime(new Date());
}, 5000);
return () => clearInterval(interval);
}, [selectedCrypto, selectedCrypto.id]); // Added selectedCrypto.id to dependencies for robustness
// Calculate portfolio value
const portfolioValue = 32567.42;
const portfolioChange = 783.21;
return (
<div className="w-full max-w-5xl mx-auto bg-gray-900 text-white rounded-xl overflow-hidden shadow-2xl border border-gray-800">
{/* Header with balance */}
<div className="p-4 md:p-6 bg-gradient-to-r from-gray-900 to-gray-800 border-b border-gray-800">
<div className="flex justify-between items-center mb-4">
<div className="flex items-center gap-2">
<div className="h-8 w-8 bg-gradient-to-br from-blue-500 to-purple-600 rounded-lg flex items-center justify-center">
<Layers className="h-5 w-5 text-white" />
</div>
<h1 className="text-xl font-bold tracking-tight">CryptoVault</h1>
</div>
<div className="flex items-center gap-2">
<div className="text-xs text-gray-400 flex items-center gap-1">
<Clock size={12} />
<span>{currentTime.toLocaleTimeString()}</span>
</div>
<button className="bg-gray-800 hover:bg-gray-700 p-2 rounded-full transition-colors">
<Menu size={18} />
</button>
</div>
</div>
{/* Portfolio value */}
<div className="mb-6">
<div className="flex items-center gap-2 mb-1">
<h2 className="text-sm font-medium text-gray-400">Total Balance</h2>
<button
className="text-gray-500 hover:text-gray-400"
onClick={() => setShowBalance(!showBalance)}
>
{showBalance ? <Eye size={16} /> : <EyeOff size={16} />}
</button>
</div>
<div className="flex items-end gap-3">
<div className="text-3xl font-bold">
{showBalance
? formatCurrency(portfolioValue)
: '••••••••'}
</div>
<div className={`text-sm font-medium flex items-center ${portfolioChange >= 0 ? 'text-green-400' : 'text-red-400'}`}>
{portfolioChange >= 0
? <ArrowUpRight size={16} className="inline mr-0.5" />
: <ArrowDownRight size={16} className="inline mr-0.5" />}
{showBalance
? `${portfolioChange >= 0 ? '+' : ''}${formatCurrency(portfolioChange)} (${(portfolioChange / (portfolioValue - portfolioChange) * 100).toFixed(2)}%)`
: '•••••'}
</div>
</div>
</div>
{/* Time frame filters */}
<div className="flex gap-2">
{(['1h', '24h', '7d', '30d'] as const).map((time) => (
<button
key={time}
onClick={() => setTimeFrame(time)}
className={`px-3 py-1.5 text-sm rounded-lg transition-colors ${
timeFrame === time
? 'bg-blue-600 text-white'
: 'bg-gray-800 text-gray-400 hover:bg-gray-700'
}`}
>
{time}
</button>
))}
</div>
</div>
{/* Main content */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 p-4 md:p-6">
{/* Left panel - Selected crypto */}
<div className="md:col-span-2 bg-gray-800 rounded-xl p-4 border border-gray-700 relative overflow-hidden">
<div className="absolute inset-0 overflow-hidden pointer-events-none">
<div className="absolute -right-10 -top-10 w-40 h-40 bg-blue-600 opacity-10 blur-3xl rounded-full"></div>
<div className="absolute -left-10 -bottom-10 w-40 h-40 bg-purple-600 opacity-10 blur-3xl rounded-full"></div>
</div>
<div className="flex justify-between items-start mb-4 relative z-10">
<div className="flex items-center gap-3">
<div className="h-10 w-10 rounded-full overflow-hidden">
<img src={selectedCrypto.logo} alt={selectedCrypto.name} className="h-full w-full" />
</div>
<div>
<h3 className="text-xl font-bold flex items-center gap-2">
{selectedCrypto.name}
<span className="text-sm font-medium text-gray-400">{selectedCrypto.symbol}</span>
</h3>
<div
ref={priceRef}
className="text-2xl font-bold transition-colors"
>
{formatCurrency(selectedCrypto.price, selectedCrypto.price < 1 ? 4 : 2)}
</div>
</div>
</div>
<div className={`flex items-center gap-1 px-3 py-1 rounded-lg ${
selectedCrypto.change24h >= 0
? 'bg-green-500/10 text-green-400'
: 'bg-red-500/10 text-red-400'
}`}>
{selectedCrypto.change24h >= 0
? <TrendingUp size={16} />
: <TrendingDown size={16} />}
<span className="font-medium">
{selectedCrypto.change24h >= 0 ? '+' : ''}
{selectedCrypto.change24h.toFixed(2)}%
</span>
</div>
</div>
{/* Chart */}
<div className="h-52 w-full bg-gray-800/50 rounded-lg border border-gray-700/50 mb-4 overflow-hidden">
<div className="h-full w-full p-4">
<SparklineChart
data={selectedCrypto.sparkline}
color={selectedCrypto.change24h >= 0 ? '#10B981' : '#EF4444'}
width={typeof window !== 'undefined' && priceRef.current ? priceRef.current.closest('.md\\:col-span-2')?.clientWidth - 32 - 32 : 600} // Example for dynamic width
height={160}
/>
</div>
</div>
{/* Stats */}
<div className="grid grid-cols-3 gap-4">
<div className="bg-gray-900/50 rounded-lg p-3 border border-gray-700/50">
<div className="text-xs text-gray-400 mb-1">Market Cap</div>
<div className="text-lg font-medium">{formatNumber(selectedCrypto.marketCap)}</div>
</div>
<div className="bg-gray-900/50 rounded-lg p-3 border border-gray-700/50">
<div className="text-xs text-gray-400 mb-1">24h Volume</div>
<div className="text-lg font-medium">{formatNumber(selectedCrypto.volume24h)}</div>
</div>
<div className="bg-gray-900/50 rounded-lg p-3 border border-gray-700/50">
<div className="text-xs text-gray-400 mb-1">All Time High</div>
<div className="text-lg font-medium">{formatCurrency(selectedCrypto.price * 1.5, 0)}</div> {/* Placeholder for ATH */}
</div>
</div>
{/* Action buttons */}
<div className="mt-6 flex gap-4">
<button className="flex-1 py-2 rounded-lg bg-gradient-to-r from-blue-600 to-blue-500 text-white font-medium text-sm hover:from-blue-700 hover:to-blue-600 transition-colors focus:ring-2 focus:ring-blue-500 focus:ring-opacity-50">
Buy {selectedCrypto.symbol}
</button>
<button className="flex-1 py-2 rounded-lg bg-gray-700 text-white font-medium text-sm hover:bg-gray-600 transition-colors focus:ring-2 focus:ring-gray-500 focus:ring-opacity-50">
Sell {selectedCrypto.symbol}
</button>
</div>
</div>
{/* Right panel - Assets & News */}
<div className="flex flex-col gap-4">
{/* Assets list */}
<div className="bg-gray-800 rounded-xl p-4 border border-gray-700">
<h3 className="text-sm font-medium text-gray-400 mb-3">Top Assets</h3>
<div className="space-y-3">
{cryptoData.map((crypto) => (
<button
key={crypto.id}
className={`w-full flex items-center p-2 rounded-lg transition-colors ${
selectedCrypto.id === crypto.id
? 'bg-blue-600/10 border border-blue-600/30'
: 'hover:bg-gray-700/50 border border-transparent'
}`}
onClick={() => setSelectedCrypto(crypto)}
>
<div className="h-8 w-8 rounded-full overflow-hidden mr-3">
<img src={crypto.logo} alt={crypto.name} className="h-full w-full" />
</div>
<div className="flex-1 flex flex-col items-start min-w-0">
<div className="flex items-center w-full">
<span className="text-sm font-medium truncate">{crypto.name}</span>
<span className="text-xs text-gray-400 ml-1">{crypto.symbol}</span>
</div>
<div className="flex items-center w-full">
<span className="text-sm truncate">{formatCurrency(crypto.price, crypto.price < 1 ? 4 : 2)}</span>
<span className={`text-xs ml-2 ${
crypto.change24h >= 0 ? 'text-green-400' : 'text-red-400'
}`}>
{crypto.change24h >= 0 ? '+' : ''}
{crypto.change24h.toFixed(2)}%
</span>
</div>
</div>
<div className="w-16 h-12 flex items-center justify-center"> {/* Added flex for centering */}
<SparklineChart
data={crypto.sparkline}
color={crypto.change24h >= 0 ? '#10B981' : '#EF4444'}
width={64}
height={32}
/>
</div>
</button>
))}
</div>
</div>
{/* News */}
<div className="bg-gray-800 rounded-xl p-4 border border-gray-700">
<h3 className="text-sm font-medium text-gray-400 mb-3">Trending News</h3>
<div className="space-y-3">
{trendingNews.map((news, index) => (
<a // <-- FIX: Added opening <a> tag here
key={index}
href={news.url}
target="_blank" // Good practice for external links
rel="noopener noreferrer" // Good practice for security
className="block p-2 rounded-lg hover:bg-gray-700/30 transition-colors"
>
<h4 className="text-sm font-medium mb-1">{news.title}</h4>
<div className="flex items-center text-xs text-gray-400">
<span>{news.source}</span>
<span className="mx-1">•</span>
<span className="flex items-center">
<Clock3 size={10} className="mr-1" />
{news.time}
</span>
</div>
</a>
))}
</div>
</div>
{/* Quick actions */}
<div className="bg-gray-800 rounded-xl p-4 border border-gray-700">
<h3 className="text-sm font-medium text-gray-400 mb-3">Quick Actions</h3>
<div className="grid grid-cols-2 gap-2">
<button className="p-3 rounded-lg bg-blue-600/20 hover:bg-blue-600/30 transition-colors flex flex-col items-center">
<Zap size={18} className="text-blue-400 mb-1" />
<span className="text-xs">Swap</span>
</button>
<button className="p-3 rounded-lg bg-purple-600/20 hover:bg-purple-600/30 transition-colors flex flex-col items-center">
<BarChart2 size={18} className="text-purple-400 mb-1" />
<span className="text-xs">Trade</span>
</button>
</div>
</div>
</div>
</div>
{/* CSS for animations */}
<style jsx>{`
.price-changed {
animation: pulse 1s ease-in-out;
}
@keyframes pulse {
0% { color: white; }
50% { color: rgb(59, 130, 246); } /* Tailwind's blue-500 */
100% { color: white; }
}
`}</style>
</div>
);
};