feat(market_chart)_: add test-api for cryptocompare chart

This commit is contained in:
Andrey Bocharnikov
2025-07-03 18:12:12 +04:00
parent bee14521ca
commit 516d2e620b
7 changed files with 852 additions and 9 deletions
+101 -1
View File
@@ -1,2 +1,102 @@
# Node.js dependencies
# Dependencies
node_modules/
# Production builds
build/
dist/
# Environment variables
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
# Logs
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Coverage directory used by tools like istanbul
coverage/
*.lcov
# nyc test coverage
.nyc_output
# Dependency directories
jspm_packages/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
.env.test
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
public
# Storybook build outputs
.out
.storybook-out
# Temporary folders
tmp/
temp/
# Editor directories and files
.vscode/
.idea/
*.swp
*.swo
*~
# OS generated files
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db
+294 -4
View File
@@ -17,6 +17,7 @@
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-scripts": "5.0.1",
"recharts": "^2.10.3",
"styled-components": "^6.1.8"
}
},
@@ -3903,6 +3904,69 @@
"@types/node": "*"
}
},
"node_modules/@types/d3-array": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.1.tgz",
"integrity": "sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==",
"license": "MIT"
},
"node_modules/@types/d3-color": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz",
"integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==",
"license": "MIT"
},
"node_modules/@types/d3-ease": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz",
"integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==",
"license": "MIT"
},
"node_modules/@types/d3-interpolate": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz",
"integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==",
"license": "MIT",
"dependencies": {
"@types/d3-color": "*"
}
},
"node_modules/@types/d3-path": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz",
"integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==",
"license": "MIT"
},
"node_modules/@types/d3-scale": {
"version": "4.0.9",
"resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz",
"integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==",
"license": "MIT",
"dependencies": {
"@types/d3-time": "*"
}
},
"node_modules/@types/d3-shape": {
"version": "3.1.7",
"resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz",
"integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==",
"license": "MIT",
"dependencies": {
"@types/d3-path": "*"
}
},
"node_modules/@types/d3-time": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz",
"integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==",
"license": "MIT"
},
"node_modules/@types/d3-timer": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz",
"integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==",
"license": "MIT"
},
"node_modules/@types/eslint": {
"version": "8.56.12",
"resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.12.tgz",
@@ -6697,6 +6761,127 @@
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
"license": "MIT"
},
"node_modules/d3-array": {
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz",
"integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==",
"license": "ISC",
"dependencies": {
"internmap": "1 - 2"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-color": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz",
"integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==",
"license": "ISC",
"engines": {
"node": ">=12"
}
},
"node_modules/d3-ease": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz",
"integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==",
"license": "BSD-3-Clause",
"engines": {
"node": ">=12"
}
},
"node_modules/d3-format": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz",
"integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==",
"license": "ISC",
"engines": {
"node": ">=12"
}
},
"node_modules/d3-interpolate": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
"integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==",
"license": "ISC",
"dependencies": {
"d3-color": "1 - 3"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-path": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz",
"integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==",
"license": "ISC",
"engines": {
"node": ">=12"
}
},
"node_modules/d3-scale": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz",
"integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==",
"license": "ISC",
"dependencies": {
"d3-array": "2.10.0 - 3",
"d3-format": "1 - 3",
"d3-interpolate": "1.2.0 - 3",
"d3-time": "2.1.1 - 3",
"d3-time-format": "2 - 4"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-shape": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz",
"integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==",
"license": "ISC",
"dependencies": {
"d3-path": "^3.1.0"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-time": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz",
"integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==",
"license": "ISC",
"dependencies": {
"d3-array": "2 - 3"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-time-format": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz",
"integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==",
"license": "ISC",
"dependencies": {
"d3-time": "1 - 3"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-timer": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz",
"integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==",
"license": "ISC",
"engines": {
"node": ">=12"
}
},
"node_modules/damerau-levenshtein": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz",
@@ -6791,6 +6976,12 @@
"integrity": "sha512-8vDa8Qxvr/+d94hSh5P3IJwI5t8/c0KsMp+g8bNw9cY2icONa5aPfvKeieW1WlG0WQYwwhJ7mjui2xtiePQSXw==",
"license": "MIT"
},
"node_modules/decimal.js-light": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz",
"integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==",
"license": "MIT"
},
"node_modules/dedent": {
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz",
@@ -8249,6 +8440,15 @@
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
"license": "MIT"
},
"node_modules/fast-equals": {
"version": "5.2.2",
"resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.2.2.tgz",
"integrity": "sha512-V7/RktU11J3I36Nwq2JnZEM7tNm17eBJz+u25qdxBZeCKiX6BkVSZQjwWIr+IobgnZy+ag73tTZgZi7tr0LrBw==",
"license": "MIT",
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/fast-glob": {
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz",
@@ -9599,6 +9799,15 @@
"node": ">= 0.4"
}
},
"node_modules/internmap": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz",
"integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==",
"license": "ISC",
"engines": {
"node": ">=12"
}
},
"node_modules/ipaddr.js": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.2.0.tgz",
@@ -12515,9 +12724,9 @@
}
},
"node_modules/postcss": {
"version": "8.5.3",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz",
"integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==",
"version": "8.5.6",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
"integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
"funding": [
{
"type": "opencollective",
@@ -12534,7 +12743,7 @@
],
"license": "MIT",
"dependencies": {
"nanoid": "^3.3.8",
"nanoid": "^3.3.11",
"picocolors": "^1.1.1",
"source-map-js": "^1.2.1"
},
@@ -14333,6 +14542,21 @@
}
}
},
"node_modules/react-smooth": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-4.0.4.tgz",
"integrity": "sha512-gnGKTpYwqL0Iii09gHobNolvX4Kiq4PKx6eWBCYYix+8cdw+cGo3do906l1NBPKkSWx1DghC1dlWG9L2uGd61Q==",
"license": "MIT",
"dependencies": {
"fast-equals": "^5.0.1",
"prop-types": "^15.8.1",
"react-transition-group": "^4.4.5"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/react-transition-group": {
"version": "4.4.5",
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz",
@@ -14384,6 +14608,44 @@
"node": ">=8.10.0"
}
},
"node_modules/recharts": {
"version": "2.15.4",
"resolved": "https://registry.npmjs.org/recharts/-/recharts-2.15.4.tgz",
"integrity": "sha512-UT/q6fwS3c1dHbXv2uFgYJ9BMFHu3fwnd7AYZaEQhXuYQ4hgsxLvsUXzGdKeZrW5xopzDCvuA2N41WJ88I7zIw==",
"license": "MIT",
"dependencies": {
"clsx": "^2.0.0",
"eventemitter3": "^4.0.1",
"lodash": "^4.17.21",
"react-is": "^18.3.1",
"react-smooth": "^4.0.4",
"recharts-scale": "^0.4.4",
"tiny-invariant": "^1.3.1",
"victory-vendor": "^36.6.8"
},
"engines": {
"node": ">=14"
},
"peerDependencies": {
"react": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/recharts-scale": {
"version": "0.4.5",
"resolved": "https://registry.npmjs.org/recharts-scale/-/recharts-scale-0.4.5.tgz",
"integrity": "sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==",
"license": "MIT",
"dependencies": {
"decimal.js-light": "^2.4.1"
}
},
"node_modules/recharts/node_modules/react-is": {
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz",
"integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==",
"license": "MIT"
},
"node_modules/recursive-readdir": {
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/recursive-readdir/-/recursive-readdir-2.2.3.tgz",
@@ -16516,6 +16778,12 @@
"integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==",
"license": "MIT"
},
"node_modules/tiny-invariant": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz",
"integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==",
"license": "MIT"
},
"node_modules/tmpl": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz",
@@ -17036,6 +17304,28 @@
"node": ">= 0.8"
}
},
"node_modules/victory-vendor": {
"version": "36.9.2",
"resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-36.9.2.tgz",
"integrity": "sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==",
"license": "MIT AND ISC",
"dependencies": {
"@types/d3-array": "^3.0.3",
"@types/d3-ease": "^3.0.0",
"@types/d3-interpolate": "^3.0.1",
"@types/d3-scale": "^4.0.2",
"@types/d3-shape": "^3.1.0",
"@types/d3-time": "^3.0.0",
"@types/d3-timer": "^3.0.0",
"d3-array": "^3.1.6",
"d3-ease": "^3.0.1",
"d3-interpolate": "^3.0.1",
"d3-scale": "^4.0.2",
"d3-shape": "^3.1.0",
"d3-time": "^3.0.0",
"d3-timer": "^3.0.1"
}
},
"node_modules/w3c-hr-time": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz",
+1
View File
@@ -12,6 +12,7 @@
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-scripts": "5.0.1",
"recharts": "^2.10.3",
"styled-components": "^6.1.8"
},
"scripts": {
+22
View File
@@ -4,6 +4,7 @@ import CryptoDataTable from './components/CryptoDataTable';
import Layout from './components/Layout';
import Tabs from './components/Tabs';
import Stats from './components/Stats';
import TokenDetails from './components/TokenDetails';
import { Loading, Error } from './components/LoadingAndErrors';
import useCoinGeckoData from './hooks/useCoinGeckoData';
import useCoinGeckoPriceData from './hooks/useCoinGeckoPriceData';
@@ -21,6 +22,9 @@ function App() {
// Endpoint state for token data
const [tokenEndpoint, setTokenEndpoint] = useState('leaderboard');
// Selected token state
const [selectedToken, setSelectedToken] = useState(null);
// CoinGecko data with token endpoint parameter
const {
coinGeckoData,
@@ -46,6 +50,15 @@ function App() {
const error = (activeMainTab === 'CoinGecko' && (coinGeckoError || coinGeckoPriceError));
// Handle token selection
const handleTokenClick = (token) => {
setSelectedToken(token);
};
const handleCloseTokenDetails = () => {
setSelectedToken(null);
};
// Get the current active data and price data based on active tab
const getCurrentData = () => {
if (activeMainTab === 'CoinGecko') {
@@ -198,8 +211,17 @@ function App() {
priceData={activePrice}
source={source}
priceEndpoint={priceEndpoint}
onTokenClick={handleTokenClick}
/>
</ErrorBoundary>
{/* Token details modal */}
{selectedToken && (
<TokenDetails
token={selectedToken}
onClose={handleCloseTokenDetails}
/>
)}
</Layout>
);
}
+13 -4
View File
@@ -62,6 +62,15 @@ const SwapButton = styled.button`
}
`;
const TableRow = styled.tr`
cursor: pointer;
transition: background-color 0.2s;
&:hover {
background-color: #f8f9fa;
}
`;
const PriceContainer = styled.div`
display: flex;
flex-direction: column;
@@ -138,7 +147,7 @@ const PageInfo = styled.div`
color: #616E85;
`;
function CryptoDataTable({ data, priceData, source, priceEndpoint }) {
function CryptoDataTable({ data, priceData, source, priceEndpoint, onTokenClick }) {
const [currentPage, setCurrentPage] = useState(1);
const [pageSize, setPageSize] = useState(10);
@@ -257,7 +266,7 @@ function CryptoDataTable({ data, priceData, source, priceEndpoint }) {
const tokenPriceData = priceData[symbol] || priceData[id];
return (
<tr key={id}>
<TableRow key={id} onClick={() => onTokenClick && onTokenClick(item)}>
<Td>{startIndex + index + 1}</Td>
<Td>
<TokenCell>
@@ -316,9 +325,9 @@ function CryptoDataTable({ data, priceData, source, priceEndpoint }) {
</PriceContainer>
</Td>
<Td>
<SwapButton>Swap</SwapButton>
<SwapButton onClick={(e) => e.stopPropagation()}>Swap</SwapButton>
</Td>
</tr>
</TableRow>
);
})}
</tbody>
+334
View File
@@ -0,0 +1,334 @@
import React, { useState, useEffect } from 'react';
import styled from 'styled-components';
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts';
import useTokenHistory, { getApiRequestCounter, subscribeToApiCounter } from '../hooks/useTokenHistory';
const Container = styled.div`
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.7);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
`;
const Modal = styled.div`
background: white;
border-radius: 12px;
width: 90%;
max-width: 1200px;
max-height: 90vh;
overflow-y: auto;
padding: 24px;
position: relative;
`;
const CloseButton = styled.button`
position: absolute;
top: 16px;
right: 16px;
background: none;
border: none;
font-size: 24px;
cursor: pointer;
color: #666;
&:hover {
color: #333;
}
`;
const TokenHeader = styled.div`
display: flex;
align-items: center;
gap: 16px;
margin-bottom: 24px;
`;
const TokenImage = styled.img`
width: 64px;
height: 64px;
border-radius: 50%;
`;
const TokenInfo = styled.div`
flex: 1;
`;
const TokenName = styled.h2`
margin: 0 0 8px 0;
font-size: 24px;
font-weight: 600;
`;
const TokenSymbol = styled.span`
color: #666;
font-size: 16px;
text-transform: uppercase;
`;
const ApiCounter = styled.div`
margin-left: auto;
background: #f0f8ff;
border: 1px solid #3861FB;
border-radius: 6px;
padding: 8px 12px;
font-size: 14px;
color: #3861FB;
font-weight: 500;
`;
const PriceInfo = styled.div`
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 16px;
margin-bottom: 24px;
`;
const PriceCard = styled.div`
background: #f8f9fa;
border-radius: 8px;
padding: 16px;
`;
const PriceLabel = styled.div`
color: #666;
font-size: 14px;
margin-bottom: 8px;
`;
const PriceValue = styled.div`
font-size: 20px;
font-weight: 600;
color: ${props => props.$isPositive ? '#16C784' : props.$isNegative ? '#EA3943' : '#333'};
`;
const ChartContainer = styled.div`
margin-bottom: 24px;
`;
const ChartTitle = styled.h3`
margin: 0 0 16px 0;
font-size: 18px;
font-weight: 600;
`;
const TimeRangeSelector = styled.div`
display: flex;
gap: 8px;
margin-bottom: 16px;
`;
const TimeRangeButton = styled.button`
padding: 8px 16px;
border: 1px solid #ddd;
background: ${props => props.$active ? '#3861FB' : 'white'};
color: ${props => props.$active ? 'white' : '#333'};
border-radius: 6px;
cursor: pointer;
font-size: 14px;
&:hover {
background: ${props => props.$active ? '#3861FB' : '#f5f5f5'};
}
`;
const ChartWrapper = styled.div`
height: 400px;
width: 100%;
`;
const LoadingChart = styled.div`
height: 400px;
display: flex;
justify-content: center;
align-items: center;
background: #f8f9fa;
border-radius: 8px;
color: #666;
`;
const ErrorChart = styled.div`
height: 400px;
display: flex;
justify-content: center;
align-items: center;
background: #f8f9fa;
border-radius: 8px;
color: #EA3943;
`;
const timeRanges = [
{ key: 'week', label: '7D', limit: 7 * 24, aggregate: 1, allData: false },
{ key: 'month', label: '1M', limit: 30 * 24, aggregate: 2, allData: false },
{ key: 'halfyear', label: '6M', limit: 180, aggregate: 1, allData: false },
{ key: 'year', label: '1Y', limit: 365, aggregate: 1, allData: false },
{ key: 'all', label: 'ALL', limit: 1, aggregate: 12, allData: true }
];
function TokenDetails({ token, onClose }) {
const [selectedTimeRange, setSelectedTimeRange] = useState('week');
const [apiRequestCount, setApiRequestCount] = useState(getApiRequestCounter());
// Subscribe to API request counter changes
useEffect(() => {
const unsubscribe = subscribeToApiCounter(setApiRequestCount);
return unsubscribe;
}, []);
// Use hook to get historical data
const { data: chartData, isLoading: isLoadingChart, error: chartError } = useTokenHistory(token.id, selectedTimeRange);
const formatNumber = (num) => {
if (!num && num !== 0) return '—';
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
minimumFractionDigits: 2,
maximumFractionDigits: 2
}).format(num);
};
const formatPercentage = (value) => {
if (!value && value !== 0) return '—';
return `${value >= 0 ? '+' : ''}${value.toFixed(2)}%`;
};
const handleTimeRangeChange = (timeRange) => {
setSelectedTimeRange(timeRange);
};
const handleOverlayClick = (e) => {
if (e.target === e.currentTarget) {
onClose();
}
};
// Extract token data depending on the source
const {
id,
name,
symbol,
image,
current_price,
market_cap,
total_volume,
price_change_percentage_24h,
high_24h,
low_24h,
circulating_supply,
total_supply
} = token;
return (
<Container onClick={handleOverlayClick}>
<Modal>
<CloseButton onClick={onClose}>×</CloseButton>
<TokenHeader>
<TokenImage src={image} alt={name} />
<TokenInfo>
<TokenName>{name}</TokenName>
<TokenSymbol>{symbol}</TokenSymbol>
</TokenInfo>
<ApiCounter>
API Requests: {apiRequestCount}
</ApiCounter>
</TokenHeader>
<PriceInfo>
<PriceCard>
<PriceLabel>Current Price</PriceLabel>
<PriceValue>{formatNumber(current_price)}</PriceValue>
</PriceCard>
<PriceCard>
<PriceLabel>24h Change</PriceLabel>
<PriceValue
$isPositive={price_change_percentage_24h >= 0}
$isNegative={price_change_percentage_24h < 0}
>
{formatPercentage(price_change_percentage_24h)}
</PriceValue>
</PriceCard>
<PriceCard>
<PriceLabel>Market Cap</PriceLabel>
<PriceValue>{formatNumber(market_cap)}</PriceValue>
</PriceCard>
<PriceCard>
<PriceLabel>24h Volume</PriceLabel>
<PriceValue>{formatNumber(total_volume)}</PriceValue>
</PriceCard>
<PriceCard>
<PriceLabel>24h High</PriceLabel>
<PriceValue>{formatNumber(high_24h)}</PriceValue>
</PriceCard>
<PriceCard>
<PriceLabel>24h Low</PriceLabel>
<PriceValue>{formatNumber(low_24h)}</PriceValue>
</PriceCard>
</PriceInfo>
<ChartContainer>
<ChartTitle>Price History</ChartTitle>
<TimeRangeSelector>
{timeRanges.map(range => (
<TimeRangeButton
key={range.key}
$active={selectedTimeRange === range.key}
onClick={() => handleTimeRangeChange(range.key)}
>
{range.label}
</TimeRangeButton>
))}
</TimeRangeSelector>
<ChartWrapper>
{isLoadingChart ? (
<LoadingChart>Loading chart data...</LoadingChart>
) : chartError ? (
<ErrorChart>{chartError}</ErrorChart>
) : (
<ResponsiveContainer width="100%" height="100%">
<LineChart data={chartData}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis
dataKey="date"
tick={{ fontSize: 12 }}
/>
<YAxis
domain={['dataMin', 'dataMax']}
tick={{ fontSize: 12 }}
tickFormatter={(value) => `$${value.toFixed(2)}`}
/>
<Tooltip
formatter={(value) => [`$${value.toFixed(2)}`, 'Price']}
labelFormatter={(label) => `Date: ${label}`}
/>
<Line
type="monotone"
dataKey="price"
stroke="#3861FB"
strokeWidth={2}
dot={false}
/>
</LineChart>
</ResponsiveContainer>
)}
</ChartWrapper>
</ChartContainer>
</Modal>
</Container>
);
}
export default TokenDetails;
+87
View File
@@ -0,0 +1,87 @@
import { useState, useEffect, useCallback } from 'react';
// Global counter for CoinGecko API requests
let globalApiRequestCounter = 0;
const apiRequestListeners = new Set();
// Function to increment the counter
const incrementApiCounter = () => {
globalApiRequestCounter++;
// Notify all listeners about the change
apiRequestListeners.forEach(listener => listener(globalApiRequestCounter));
};
// Function to get current counter value
export const getApiRequestCounter = () => globalApiRequestCounter;
// Function to subscribe to counter changes
export const subscribeToApiCounter = (listener) => {
apiRequestListeners.add(listener);
return () => apiRequestListeners.delete(listener);
};
const useTokenHistory = (tokenId, timeRange) => {
const [data, setData] = useState([]);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);
const fetchHistoricalData = useCallback(async () => {
if (!tokenId || !timeRange) return;
setIsLoading(true);
setError(null);
try {
// Parameters for different time ranges
const timeRanges = {
week: { days: 7, interval: 'hourly' },
month: { days: 30, interval: 'hourly' },
halfyear: { days: 180, interval: 'daily' },
year: { days: 365, interval: 'daily' },
all: { days: 'max', interval: 'daily' }
};
const range = timeRanges[timeRange];
if (!range) {
throw new Error('Invalid time range');
}
// Increment counter before making the request
incrementApiCounter();
// Use CoinGecko API to get historical data
const response = await fetch(
`https://api.coingecko.com/api/v3/coins/${tokenId}/market_chart?vs_currency=usd&days=${range.days}&interval=${range.interval}`
);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
// Transform data into chart format
const formattedData = result.prices?.map(([timestamp, price]) => ({
date: new Date(timestamp).toLocaleDateString(),
fullDate: new Date(timestamp).toLocaleString(),
price: price,
timestamp: timestamp
})) || [];
setData(formattedData);
} catch (err) {
console.error('Error fetching historical data:', err);
setError(err.message || 'Failed to fetch historical data');
} finally {
setIsLoading(false);
}
}, [tokenId, timeRange]);
useEffect(() => {
fetchHistoricalData();
}, [fetchHistoricalData]);
return { data, isLoading, error, refetch: fetchHistoricalData };
};
export default useTokenHistory;