├── .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 |
--------------------------------------------------------------------------------