├── .gitignore ├── README.md ├── package-lock.json ├── package.json ├── public ├── favicon.ico ├── index.html ├── logo192.png ├── logo512.png ├── manifest.json └── robots.txt └── src ├── App.js ├── components └── Dashboard.js ├── index.css ├── index.js ├── styles.css └── utils.js /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ReactJS Cryptocurrency dashboard tutorial 2 | This is a basic template for building a ReactJS dashboard using the Coinbase API to retreive data using their REST and Websocket API for real time data. For more information check out the full video tutorial going over the code line by line. I also go over some more ideas for how to extend the project and use Coinbase's API 3 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "reactjs-crypto-dashboard", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@testing-library/jest-dom": "^5.11.9", 7 | "@testing-library/react": "^11.2.5", 8 | "@testing-library/user-event": "^12.6.3", 9 | "chart.js": "^2.9.4", 10 | "react": "^17.0.1", 11 | "react-chartjs-2": "^2.11.1", 12 | "react-dom": "^17.0.1", 13 | "react-scripts": "4.0.2", 14 | "web-vitals": "^1.1.0" 15 | }, 16 | "scripts": { 17 | "start": "react-scripts start", 18 | "build": "react-scripts build", 19 | "test": "react-scripts test", 20 | "eject": "react-scripts eject" 21 | }, 22 | "eslintConfig": { 23 | "extends": [ 24 | "react-app", 25 | "react-app/jest" 26 | ] 27 | }, 28 | "browserslist": { 29 | "production": [ 30 | ">0.2%", 31 | "not dead", 32 | "not op_mini all" 33 | ], 34 | "development": [ 35 | "last 1 chrome version", 36 | "last 1 firefox version", 37 | "last 1 safari version" 38 | ] 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/renaissancetroll/reactjs-crypto-api-dashboard/73d637f9cca2438d4c6bdbc7c278dbb855c4c1ad/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/renaissancetroll/reactjs-crypto-api-dashboard/73d637f9cca2438d4c6bdbc7c278dbb855c4c1ad/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/renaissancetroll/reactjs-crypto-api-dashboard/73d637f9cca2438d4c6bdbc7c278dbb855c4c1ad/public/logo512.png -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, useRef } from "react"; 2 | import Dashboard from "./components/Dashboard"; 3 | import { formatData } from "./utils"; 4 | import "./styles.css"; 5 | 6 | export default function App() { 7 | const [currencies, setcurrencies] = useState([]); 8 | const [pair, setpair] = useState(""); 9 | const [price, setprice] = useState("0.00"); 10 | const [pastData, setpastData] = useState({}); 11 | const ws = useRef(null); 12 | 13 | let first = useRef(false); 14 | const url = "https://api.pro.coinbase.com"; 15 | 16 | useEffect(() => { 17 | ws.current = new WebSocket("wss://ws-feed.pro.coinbase.com"); 18 | 19 | let pairs = []; 20 | 21 | const apiCall = async () => { 22 | await fetch(url + "/products") 23 | .then((res) => res.json()) 24 | .then((data) => (pairs = data)); 25 | 26 | let filtered = pairs.filter((pair) => { 27 | if (pair.quote_currency === "USD") { 28 | return pair; 29 | } 30 | }); 31 | 32 | filtered = filtered.sort((a, b) => { 33 | if (a.base_currency < b.base_currency) { 34 | return -1; 35 | } 36 | if (a.base_currency > b.base_currency) { 37 | return 1; 38 | } 39 | return 0; 40 | }); 41 | 42 | 43 | setcurrencies(filtered); 44 | 45 | first.current = true; 46 | }; 47 | 48 | apiCall(); 49 | }, []); 50 | 51 | useEffect(() => { 52 | if (!first.current) { 53 | 54 | return; 55 | } 56 | 57 | 58 | let msg = { 59 | type: "subscribe", 60 | product_ids: [pair], 61 | channels: ["ticker"] 62 | }; 63 | let jsonMsg = JSON.stringify(msg); 64 | ws.current.send(jsonMsg); 65 | 66 | let historicalDataURL = `${url}/products/${pair}/candles?granularity=86400`; 67 | const fetchHistoricalData = async () => { 68 | let dataArr = []; 69 | await fetch(historicalDataURL) 70 | .then((res) => res.json()) 71 | .then((data) => (dataArr = data)); 72 | 73 | let formattedData = formatData(dataArr); 74 | setpastData(formattedData); 75 | }; 76 | 77 | fetchHistoricalData(); 78 | 79 | ws.current.onmessage = (e) => { 80 | let data = JSON.parse(e.data); 81 | if (data.type !== "ticker") { 82 | return; 83 | } 84 | 85 | if (data.product_id === pair) { 86 | setprice(data.price); 87 | } 88 | }; 89 | }, [pair]); 90 | 91 | const handleSelect = (e) => { 92 | let unsubMsg = { 93 | type: "unsubscribe", 94 | product_ids: [pair], 95 | channels: ["ticker"] 96 | }; 97 | let unsub = JSON.stringify(unsubMsg); 98 | 99 | ws.current.send(unsub); 100 | 101 | setpair(e.target.value); 102 | }; 103 | return ( 104 |
105 | { 106 | 115 | } 116 | 117 |
118 | ); 119 | } 120 | -------------------------------------------------------------------------------- /src/components/Dashboard.js: -------------------------------------------------------------------------------- 1 | import React, { useRef } from "react"; 2 | import { Line } from "react-chartjs-2"; 3 | 4 | function Dashboard({ price, data }) { 5 | const opts = { 6 | tooltips: { 7 | intersect: false, 8 | mode: "index" 9 | }, 10 | responsive: true, 11 | maintainAspectRatio: false 12 | }; 13 | if (price === "0.00") { 14 | return

please select a currency pair

; 15 | } 16 | return ( 17 |
18 |

{`$${price}`}

19 | 20 |
21 | 22 |
23 |
24 | ); 25 | } 26 | 27 | export default Dashboard; -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/renaissancetroll/reactjs-crypto-api-dashboard/73d637f9cca2438d4c6bdbc7c278dbb855c4c1ad/src/index.css -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | 5 | ReactDOM.render( 6 | 7 | 8 | , 9 | document.getElementById('root') 10 | ); 11 | -------------------------------------------------------------------------------- /src/styles.css: -------------------------------------------------------------------------------- 1 | .container { 2 | margin: 20px; 3 | font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif; 4 | text-align: center; 5 | height: 100vh; 6 | } 7 | .dashboard { 8 | display: flex; 9 | flex-direction: column; 10 | align-items: center; 11 | height: 80vh; 12 | } 13 | 14 | .chart-container { 15 | width: 80%; 16 | height: 100%; 17 | } -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | export const formatData = (data) => { 2 | let finalData = { 3 | labels: [], 4 | datasets: [ 5 | { 6 | label: "Price", 7 | data: [], 8 | backgroundColor: "rgb(255, 99, 132, 0.8)", 9 | borderColor: "rgba(255, 99, 132, 0.2)", 10 | fill: false 11 | } 12 | ] 13 | }; 14 | 15 | let dates = data.map((val) => { 16 | const ts = val[0]; 17 | let date = new Date(ts * 1000); 18 | let day = date.getDate(); 19 | let month = date.getMonth() + 1; 20 | let year = date.getFullYear(); 21 | 22 | let final = `${month}-${day}-${year}`; 23 | return final; 24 | }); 25 | 26 | let priceArr = data.map((val) => { 27 | return val[4]; 28 | }); 29 | 30 | priceArr.reverse(); 31 | dates.reverse(); 32 | finalData.labels = dates; 33 | finalData.datasets[0].data = priceArr; 34 | 35 | return finalData; 36 | }; 37 | --------------------------------------------------------------------------------