├── .gitignore ├── README.md ├── package-lock.json ├── package.json ├── public ├── favicon.ico ├── index.html ├── logo192.png ├── logo512.png ├── manifest.json └── robots.txt ├── src ├── App.scss ├── App.test.tsx ├── App.tsx ├── assets │ └── fonts │ │ ├── FontAwesome.otf │ │ ├── PontanoSans-Regular.ttf │ │ ├── fontawesome-webfont.eot │ │ ├── fontawesome-webfont.svg │ │ ├── fontawesome-webfont.ttf │ │ ├── fontawesome-webfont.woff │ │ └── fontawesome-webfont.woff2 ├── components │ ├── CryptoData │ │ ├── Analysis.tsx │ │ ├── AnalysisCard.tsx │ │ ├── CryptoRadarChart.tsx │ │ ├── CryptoTimeSeries.tsx │ │ ├── CryptoTimeSeriesActions.tsx │ │ ├── ErrorNoData.tsx │ │ ├── News.tsx │ │ ├── Trades.tsx │ │ └── index.tsx │ ├── CryptoHeader │ │ ├── Asset.tsx │ │ ├── CryptoHeader.scss │ │ ├── CryptoHeader.tsx │ │ └── CryptoPrice.tsx │ ├── Dashboard │ │ ├── Dashboard.scss │ │ ├── SignupModal.tsx │ │ └── index.tsx │ ├── Icons │ │ ├── BAT.tsx │ │ ├── BarChart.tsx │ │ ├── Bitcoin.tsx │ │ ├── Dash.tsx │ │ ├── Ethereum.tsx │ │ ├── JBIcon.tsx │ │ ├── Litecoin.tsx │ │ ├── Loader.tsx │ │ ├── Monero.tsx │ │ ├── Nem.tsx │ │ ├── PieChartIcon.tsx │ │ ├── Ripple.tsx │ │ ├── SadIcon.tsx │ │ ├── Stellar.tsx │ │ └── index.ts │ ├── Metrics │ │ ├── Metric.tsx │ │ ├── Metrics.scss │ │ └── Metrics.tsx │ ├── Navbar │ │ ├── FilteredSearch.tsx │ │ ├── Navbar.scss │ │ ├── Navbar.tsx │ │ ├── SearchBar.tsx │ │ └── SideNavbar.tsx │ └── type.ts ├── config.ts ├── context │ ├── cryptocurrency │ │ ├── CryptoCurrencyState.tsx │ │ ├── cryptoCurrencyContext.ts │ │ └── cryptoCurrencyReducer.ts │ ├── pricing │ │ ├── pricingContext.ts │ │ ├── pricingReducer.ts │ │ └── pricingState.tsx │ └── types.ts ├── index.scss ├── index.tsx ├── react-app-env.d.ts ├── serviceWorker.ts ├── setupTests.ts └── styles │ ├── colors.scss │ ├── colors.ts │ └── index.ts └── tsconfig.json /.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 | # Crypto Dashboard 2 | 3 | A fun personal realtime Dashboard using Coinbase Pro, CryptoCompare, and CoinCap API to demonstrate modern react patterns and architecture. 4 | 5 | 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "crypto-dashboard", 3 | "version": "0.1.0", 4 | "author": "Jared Booker ", 5 | "private": true, 6 | "dependencies": { 7 | "@testing-library/jest-dom": "^4.2.4", 8 | "@testing-library/react": "^9.5.0", 9 | "@testing-library/user-event": "^7.2.1", 10 | "@types/d3": "^5.7.2", 11 | "@types/jest": "^24.9.1", 12 | "@types/node": "^12.12.32", 13 | "@types/numeral": "0.0.26", 14 | "@types/react": "^16.9.26", 15 | "@types/react-dom": "^16.9.5", 16 | "@types/react-faux-dom": "^4.1.3", 17 | "@types/react-modal": "^3.10.5", 18 | "@types/react-tabs": "^2.3.1", 19 | "@types/recharts": "^1.8.9", 20 | "axios": "^0.19.2", 21 | "chart.js": "^2.9.3", 22 | "chartjs-plugin-datalabels": "^0.7.0", 23 | "moment": "^2.24.0", 24 | "node-sass": "^4.13.1", 25 | "numeral": "^2.0.6", 26 | "react": "^16.13.1", 27 | "react-chartjs-2": "^2.9.0", 28 | "react-dom": "^16.13.1", 29 | "react-ga": "^2.7.0", 30 | "react-github-btn": "^1.1.1", 31 | "react-modal": "^3.11.2", 32 | "react-scripts": "3.4.1", 33 | "react-simple-dropdown": "^3.2.3", 34 | "react-tabs": "^3.1.0", 35 | "recharts": "^1.8.5", 36 | "spectre.css": "^0.5.8", 37 | "typescript": "^3.7.5" 38 | }, 39 | "scripts": { 40 | "start": "react-scripts start", 41 | "build": "react-scripts build", 42 | "test": "react-scripts test", 43 | "eject": "react-scripts eject", 44 | "start-app": "node build/build.js" 45 | }, 46 | "eslintConfig": { 47 | "extends": "react-app" 48 | }, 49 | "browserslist": { 50 | "production": [ 51 | ">0.2%", 52 | "not dead", 53 | "not op_mini all" 54 | ], 55 | "development": [ 56 | "last 1 chrome version", 57 | "last 1 firefox version", 58 | "last 1 safari version" 59 | ] 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JBooker10/react-crypto-dashboard/99838c28bdb0c495faca84e5bed0859e50898065/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | Crypto Dashboard 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JBooker10/react-crypto-dashboard/99838c28bdb0c495faca84e5bed0859e50898065/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JBooker10/react-crypto-dashboard/99838c28bdb0c495faca84e5bed0859e50898065/public/logo512.png -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Crypto Dashboard", 3 | "name": "RealTime Crypto Dashboard", 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.scss: -------------------------------------------------------------------------------- 1 | @import "./styles/colors.scss"; 2 | 3 | .App { 4 | display: flex; 5 | height: auto; 6 | } 7 | 8 | body { 9 | // overflow: hidden; 10 | background: $NavyLight; 11 | font-size: 60%; 12 | font-size: 0.7em; 13 | } 14 | 15 | @media only screen and (max-width: 900px) { 16 | .container { 17 | width: 100vw; 18 | margin: 8em 2em 2em 2em; 19 | } 20 | 21 | h1 { 22 | font-size: 2em; 23 | } 24 | 25 | body { 26 | font-size: 70%; 27 | } 28 | } 29 | 30 | @media only screen and (max-width: 1440px) { 31 | .container { 32 | margin: 7em 2em 2em 2em; 33 | } 34 | 35 | h1 { 36 | font-size: 3em; 37 | } 38 | 39 | body { 40 | font-size: 60%; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/App.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from '@testing-library/react'; 3 | import App from './App'; 4 | 5 | test('renders learn react link', () => { 6 | const { getByText } = render(); 7 | const linkElement = getByText(/learn react/i); 8 | expect(linkElement).toBeInTheDocument(); 9 | }); 10 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import SideNavbar from "./components/Navbar/SideNavbar"; 3 | import Dashboard from "./components/Dashboard"; 4 | import CryptoCurrencyState from "./context/cryptocurrency/CryptoCurrencyState"; 5 | import PriceStreamingState from "./context/pricing/pricingState"; 6 | import "./App.scss"; 7 | 8 | function App() { 9 | return ( 10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | ); 19 | } 20 | 21 | export default App; 22 | -------------------------------------------------------------------------------- /src/assets/fonts/FontAwesome.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JBooker10/react-crypto-dashboard/99838c28bdb0c495faca84e5bed0859e50898065/src/assets/fonts/FontAwesome.otf -------------------------------------------------------------------------------- /src/assets/fonts/PontanoSans-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JBooker10/react-crypto-dashboard/99838c28bdb0c495faca84e5bed0859e50898065/src/assets/fonts/PontanoSans-Regular.ttf -------------------------------------------------------------------------------- /src/assets/fonts/fontawesome-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JBooker10/react-crypto-dashboard/99838c28bdb0c495faca84e5bed0859e50898065/src/assets/fonts/fontawesome-webfont.eot -------------------------------------------------------------------------------- /src/assets/fonts/fontawesome-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JBooker10/react-crypto-dashboard/99838c28bdb0c495faca84e5bed0859e50898065/src/assets/fonts/fontawesome-webfont.ttf -------------------------------------------------------------------------------- /src/assets/fonts/fontawesome-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JBooker10/react-crypto-dashboard/99838c28bdb0c495faca84e5bed0859e50898065/src/assets/fonts/fontawesome-webfont.woff -------------------------------------------------------------------------------- /src/assets/fonts/fontawesome-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JBooker10/react-crypto-dashboard/99838c28bdb0c495faca84e5bed0859e50898065/src/assets/fonts/fontawesome-webfont.woff2 -------------------------------------------------------------------------------- /src/components/CryptoData/Analysis.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import AnalysisCard from "./AnalysisCard"; 3 | import CryptoRadarChart from "./CryptoRadarChart"; 4 | import { TradingSignals } from "../type"; 5 | 6 | export default function Analysis({ tradingSignals }: any) { 7 | const { 8 | largetxsVar, 9 | addressesNetGrowth, 10 | concentrationVar, 11 | inOutVar, 12 | }: TradingSignals = tradingSignals; 13 | return ( 14 |
15 |
16 |
17 | 21 | 25 | 29 | 30 |
31 |
32 | 33 |
34 |
35 |
36 | ); 37 | } 38 | -------------------------------------------------------------------------------- /src/components/CryptoData/AnalysisCard.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Signal } from "./../type"; 3 | 4 | interface Props { 5 | name: string; 6 | analysis: Signal; 7 | } 8 | 9 | export default function AnalysisCard({ name, analysis }: Props) { 10 | return ( 11 |
12 |
13 |

{name}

14 |

{analysis && analysis.score.toFixed(4)}

15 |

16 | sentiment:{" "} 17 | 24 | {analysis && analysis.sentiment} 25 | 26 |

27 | {analysis.sentiment !== "bearish" ? ( 28 | 29 | ) : ( 30 | 31 | )} 32 |
33 |
34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /src/components/CryptoData/CryptoRadarChart.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { 3 | Radar, 4 | RadarChart, 5 | PolarGrid, 6 | PolarAngleAxis, 7 | PolarRadiusAxis, 8 | } from "recharts"; 9 | import { Primary, Light } from "./../../styles/colors"; 10 | 11 | export default function CryptoRadarChart({ tradingSignals, primary }: any) { 12 | const { 13 | largetxsVar, 14 | addressesNetGrowth, 15 | concentrationVar, 16 | inOutVar, 17 | } = tradingSignals; 18 | 19 | largetxsVar.name = "Largest Transaction Var"; 20 | addressesNetGrowth.name = "Addresses Net Growth"; 21 | concentrationVar.name = "Concentration Var"; 22 | inOutVar.name = "In/Out Var"; 23 | 24 | const [chartData] = useState([ 25 | largetxsVar, 26 | addressesNetGrowth, 27 | concentrationVar, 28 | inOutVar, 29 | ]); 30 | 31 | const size = 1.3; 32 | return ( 33 |
34 | 42 | 43 | 44 | 45 | 51 | 52 |
53 | ); 54 | } 55 | -------------------------------------------------------------------------------- /src/components/CryptoData/CryptoTimeSeries.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | 3 | import { Primary, Lighter, Light, White, Navy } from "./../../styles/colors"; 4 | import { chartWidth, chartToolTipStyle } from "./../../styles"; 5 | import moment from "moment"; 6 | import numeral from "numeral"; 7 | import { 8 | Area, 9 | AreaChart, 10 | YAxis, 11 | XAxis, 12 | CartesianGrid, 13 | BarChart, 14 | Tooltip, 15 | Bar, 16 | } from "recharts"; 17 | 18 | import CryptoTimeSeriesActions from "./CryptoTimeSeriesActions"; 19 | 20 | interface CryptoTimeSeries { 21 | getDailyOHLCV: Function; 22 | dailyOHLCV: []; 23 | symbol: string; 24 | } 25 | 26 | export default function CryptoTimeSeries({ 27 | getDailyOHLCV, 28 | dailyOHLCV, 29 | symbol, 30 | }: CryptoTimeSeries) { 31 | const [hoverData, setHoverData] = useState({} as any); 32 | 33 | const showTooltipData = (data: any) => { 34 | if (data.payload && data.payload[0]) { 35 | setHoverData(data.payload[0].payload); 36 | } 37 | 38 | return
; 39 | }; 40 | 41 | return ( 42 | <> 43 | 44 | 45 |
46 |
47 |
48 | CryptoCompare Index:{symbol}{" "} 49 | ${hoverData.open} 50 |
51 |
{moment.unix(hoverData.time).format("MM/DD/YYYY hh:mm a")}
52 |
53 | 59 | 60 | 61 | 65 | 69 | 70 | 71 | 72 | 82 | 83 | 92 | 99 | moment.unix(label as any).format("MM/DD/YYYY hh:mm a") 100 | } 101 | contentStyle={chartToolTipStyle} 102 | /> 103 | 104 |
105 |
106 |
107 | Volume: {symbol}{" "} 108 | 109 | {numeral(hoverData.volumeto).format("0.0a")} 110 | 111 |
112 | 113 | numeral(tick).format("0.00a")} 116 | tickLine={false} 117 | axisLine={false} 118 | tickCount={1} 119 | stroke={White} 120 | /> 121 | 122 | 128 | moment.unix(label as any).format("MM/DD/YYYY hh:mm a") 129 | } 130 | /> 131 | 132 | 133 |
134 | 135 | ); 136 | } 137 | -------------------------------------------------------------------------------- /src/components/CryptoData/CryptoTimeSeriesActions.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | interface CryptoTimeSeriesActionProps { 4 | getDailyOHLCV: Function; 5 | symbol: string; 6 | } 7 | 8 | export default function CryptoTimeSeriesActions({ 9 | getDailyOHLCV, 10 | symbol 11 | }: CryptoTimeSeriesActionProps) { 12 | return ( 13 |
14 |
15 | 18 | 21 | 24 | 25 | 28 | 29 |
30 | 31 |
32 | 33 | 34 | 35 |
36 |
37 | ); 38 | } 39 | -------------------------------------------------------------------------------- /src/components/CryptoData/ErrorNoData.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { BarChart } from "./../Icons"; 3 | 4 | export default function ErrorNoData() { 5 | return ( 6 |
15 | 16 |

Analysis Unavaliable

17 |
18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /src/components/CryptoData/News.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import moment from "moment"; 3 | 4 | export default function News({ news }: any) { 5 | function truncate(input: string, length: number) { 6 | if (input.length > length) return input.substring(0, length) + "..."; 7 | else return input; 8 | } 9 | 10 | return ( 11 |
20 | {news.map((n: any) => ( 21 |
22 |
23 |
{n.title}
24 |

25 | {truncate(n.body, 220)} 26 |

27 |
28 | {n.categories.split("|").map((c: string) => ( 29 | 30 | {c} 31 | 32 | ))} 33 |
34 |
35 |
36 | 37 | 38 | {moment.unix(n.published_on).format("MM/DD/YYYY hh:mm a")} 39 | 40 |
41 | 42 | 43 | source:{" "} 44 | 45 | {n.source}{" "} 46 | 47 | 48 |
49 |
50 |
51 | ))} 52 |
53 | ); 54 | } 55 | 56 | News.defaultProps = { 57 | news: { 58 | title: "Lorem ETC", 59 | body: 60 | "There are various ways to represent undirected graphs as a data structure class. Two of the most common ways to do this are by using an adjacency matrix or an adjacency list. The adjacency list uses a vertex as the key for nodes with its neighbors stored into a list, whereas an adjacency matrix is a V by V", 61 | }, 62 | }; 63 | -------------------------------------------------------------------------------- /src/components/CryptoData/Trades.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext, useEffect } from "react"; 2 | import cryptoStreamCTX from "./../../context/pricing/pricingContext"; 3 | import Loader from "./../Icons/Loader"; 4 | import moment from "moment"; 5 | import Numeral from "numeral"; 6 | import { HorizontalBar } from "react-chartjs-2"; 7 | import "chartjs-plugin-datalabels"; 8 | 9 | class TradingQueue { 10 | data: any; 11 | limit: number; 12 | constructor(length: number) { 13 | this.data = []; 14 | this.limit = length; 15 | } 16 | 17 | push(trade: any) { 18 | if (this.data.length <= this.limit) { 19 | this.data.push(trade); 20 | } else { 21 | this.data.shift(); 22 | this.data.push(trade); 23 | } 24 | } 25 | } 26 | 27 | const queue = new TradingQueue(8); 28 | 29 | export default function Trades({ symbol }: any) { 30 | const cryptoStream = useContext(cryptoStreamCTX); 31 | const { streamQuote, quote } = cryptoStream; 32 | const length: number = Object.keys(quote).length; 33 | 34 | useEffect(() => { 35 | streamQuote(symbol); 36 | }, [length, streamQuote, symbol]); 37 | 38 | if ( 39 | (quote.TYPE === "0" && quote.F === "2") || 40 | quote.F === "1" || 41 | quote.F === "9" 42 | ) { 43 | setInterval(() => queue.push(quote), 1000); 44 | } 45 | 46 | const data = { 47 | labels: queue.data.map((q: any) => moment.unix(q.TS).format(" hh:mm:ss a")), 48 | datasets: [ 49 | { 50 | label: "Trade Amount", 51 | backgroundColor: "rgba(47, 132, 252, 1)", 52 | borderColor: "rgba(47, 132, 252, 1)", 53 | hoverBackgroundColor: "rgba(47, 132, 252, 1)", 54 | hoverBorderColor: "rgba(47, 132, 252, 1)", 55 | data: queue.data.map((q: any) => q.TOTAL), 56 | }, 57 | 58 | // { 59 | // label: "Purchased Amount", 60 | // backgroundColor: "#1f90ff", 61 | // borderColor: "#1f90ff", 62 | // hoverBackgroundColor: "#1f90ff", 63 | // hoverBorderColor: "#1f90ff", 64 | // data: queue.data 65 | // 66 | // .filter((q: any) => q.F === "2") 67 | // .map((q: any) => q.TOTAL), 68 | // }, 69 | // { 70 | // label: "Sell Amount", 71 | // backgroundColor: "#f06363", 72 | // borderColor: "#f06363", 73 | // hoverBackgroundColor: "#f06363", 74 | // hoverBorderColor: "#f06363", 75 | // data: queue.data 76 | // 77 | // .filter((q: any) => q.F === "1" || q.F === "9") 78 | // .map((q: any) => q.TOTAL), 79 | // }, 80 | ], 81 | }; 82 | 83 | const options = { 84 | // animation: { 85 | // // easing: "easeOutBack", 86 | // currentStep: 5, 87 | // numSteps: 5, 88 | // }, 89 | scales: { 90 | yAxes: [ 91 | { 92 | ticks: { 93 | beginAtZero: false, 94 | fontColor: "#fff", 95 | }, 96 | gridLines: { 97 | display: false, 98 | drawBorder: false, 99 | }, 100 | }, 101 | ], 102 | xAxes: [ 103 | { 104 | gridLines: { 105 | display: false, 106 | drawBorder: false, 107 | }, 108 | ticks: { 109 | fontColor: "rgba(255,255,255)", 110 | display: false, 111 | }, 112 | }, 113 | ], 114 | }, 115 | plugins: { 116 | datalabels: { 117 | display: true, 118 | color: "rgba(255,255,255)", 119 | align: "right", 120 | formatter: (val: any, ctx: any) => Numeral(val).format("$0.00"), 121 | }, 122 | }, 123 | }; 124 | 125 | return ( 126 |
127 | {queue.data.length ? ( 128 | { 132 | return queue.data[0].ID; 133 | }} 134 | /> 135 | ) : ( 136 |
137 | 138 |
139 | )} 140 |
141 | ); 142 | } 143 | -------------------------------------------------------------------------------- /src/components/CryptoData/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext, useEffect } from "react"; 2 | import { Tab, Tabs, TabList, TabPanel } from "react-tabs"; 3 | import CryptoTimeSeries from "./CryptoTimeSeries"; 4 | import cryptoCurrencyCTX from "../../context/cryptocurrency/cryptoCurrencyContext"; 5 | import News from "./News"; 6 | import Trades from "./Trades"; 7 | import Analysis from "./Analysis"; 8 | import ErrorNoData from "./ErrorNoData"; 9 | import { TradingSignals } from "../type"; 10 | 11 | export default function CryptoData() { 12 | const cryptoCTX = useContext(cryptoCurrencyCTX); 13 | const { 14 | tradingSignals, 15 | getTradingSignals, 16 | loading, 17 | searchAsset, 18 | getDailyOHLCV, 19 | dailyOHLCV, 20 | news, 21 | getNews, 22 | }: { 23 | tradingSignals: TradingSignals; 24 | getTradingSignals: Function; 25 | getDailyOHLCV: Function; 26 | getNews: Function; 27 | dailyOHLCV: any; 28 | searchAsset: any; 29 | news: any; 30 | loading: Boolean; 31 | } = cryptoCTX; 32 | 33 | useEffect(() => { 34 | getTradingSignals(searchAsset.symbol); 35 | getDailyOHLCV("180", searchAsset.symbol); 36 | getNews(); 37 | 38 | // eslint-disable-next-line 39 | }, [searchAsset, loading, news.length !== 0]); 40 | 41 | return ( 42 | 43 | 44 | Overview 45 | Analysis 46 | News 47 | Trades 48 | Timeline 49 | 50 | 51 | 52 | 57 | 58 | 59 | {Object.keys(tradingSignals).length !== 0 ? ( 60 | 61 | ) : ( 62 | 63 | )} 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 |

Timeline

73 |
74 |
75 | ); 76 | } 77 | -------------------------------------------------------------------------------- /src/components/CryptoHeader/Asset.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Asset } from "./../type"; 3 | import { 4 | Ethereum, 5 | Bitcoin, 6 | Ripple, 7 | Monero, 8 | Litecoin, 9 | BasicAttentionToken, 10 | Nem, 11 | Stellar, 12 | Dash 13 | } from "../Icons"; 14 | 15 | export default function CryptoAsset({ symbol, name }: Asset) { 16 | const getAssetIcon = (symbol: string) => { 17 | switch (symbol) { 18 | case "ETC": 19 | case "ETH": 20 | return ; 21 | case "BTC": 22 | return ; 23 | case "XRP": 24 | return ; 25 | case "XMR": 26 | return ; 27 | case "LTC": 28 | return ; 29 | case "BAT": 30 | return ; 31 | case "XEM": 32 | return ; 33 | case "XLM": 34 | return ; 35 | case "DASH": 36 | return ; 37 | default: 38 | return; 39 | } 40 | }; 41 | 42 | return ( 43 | <> 44 |
{getAssetIcon(symbol)}
45 |

46 | {name} {symbol} 47 |

48 | 49 | ); 50 | } 51 | -------------------------------------------------------------------------------- /src/components/CryptoHeader/CryptoHeader.scss: -------------------------------------------------------------------------------- 1 | @import "./../../styles/colors.scss"; 2 | 3 | .crypto-header { 4 | padding: 2em 0; 5 | display: flex; 6 | justify-content: space-between; 7 | 8 | .crypto-head { 9 | display: flex; 10 | align-items: center; 11 | h1 { 12 | font-weight: 200; 13 | } 14 | } 15 | 16 | .crypto-price { 17 | display: flex; 18 | align-items: center; 19 | 20 | strong { 21 | margin-left: 0.5em; 22 | width: 100%; 23 | margin-bottom: 3em; 24 | } 25 | 26 | h1 { 27 | padding: 0; 28 | } 29 | } 30 | 31 | h1 { 32 | color: $White; 33 | } 34 | 35 | span.symbol { 36 | color: $Light; 37 | } 38 | 39 | .crypto-type { 40 | height: 7em; 41 | width: 7em; 42 | border-radius: 0.25em; 43 | margin-right: 2em; 44 | background: $NavyDark; 45 | display: flex; 46 | justify-content: center; 47 | align-items: center; 48 | } 49 | 50 | .crypto-icon { 51 | fill: $Primary; 52 | height: 60%; 53 | } 54 | 55 | .bitcoin-icon { 56 | height: 70%; 57 | 58 | fill: transparent; 59 | // background: red; 60 | } 61 | } 62 | 63 | -webkit-keyframes neon3 { 64 | from { 65 | text-shadow: 0 0 10px #fff, 0 0 20px #fff, 0 0 30px #fff, 0 0 40px #fff, 66 | 0 0 70px #fff, 0 0 120px #fff, 0 0 140px #fff; 67 | } 68 | to { 69 | text-shadow: 0 0 5px #fff, 0 0 10px #fff, 0 0 15px #fff, 0 0 20px #fff, 70 | 0 0 35px #fff, 0 0 70px #fff, 0 0; 71 | } 72 | } 73 | 74 | @-moz-keyframes neon3 { 75 | from { 76 | text-shadow: 0 0 10px #fff, 0 0 20px #fff, 0 0 30px #fff, 0 0 40px #fff, 77 | 0 0 70px #fff, 0 0 140px #fff; 78 | } 79 | to { 80 | text-shadow: 0 0 5px #fff, 0 0 10px #fff, 0 0 15px #fff, 0 0 20px #fff, 81 | 0 0 35px #fff, 0 0; 82 | } 83 | } 84 | 85 | @keyframes neon3 { 86 | from { 87 | text-shadow: 0 0 10px #fff, 0 0 20px #fff, 0 0 30px #fff, 0 0 40px #fff, 88 | 0 0 70px #fff, 0 0 140px #fff; 89 | } 90 | to { 91 | text-shadow: 0 0 5px #fff, 0 0 10px #fff, 0 0 15px #fff, 0 0 20px #fff, 92 | 0 0 35px #fff, 0 0; 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/components/CryptoHeader/CryptoHeader.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Asset from "./Asset"; 3 | import CryptoPrice from "./CryptoPrice"; 4 | import "./CryptoHeader.scss"; 5 | 6 | interface CyryptoHeaderProps { 7 | name: string; 8 | symbol: string; 9 | price: number; 10 | changePercent?: number; 11 | priceUsd: number; 12 | } 13 | 14 | export default function CryptoHeader({ 15 | name, 16 | symbol, 17 | price, 18 | changePercent, 19 | priceUsd 20 | }: CyryptoHeaderProps) { 21 | return ( 22 |
23 |
24 | 25 |
26 | 31 |
32 | ); 33 | } 34 | -------------------------------------------------------------------------------- /src/components/CryptoHeader/CryptoPrice.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export default function CryptoPrice({ price, priceUsd, changePercent }: any) { 4 | const isPositive = Math.sign(changePercent); 5 | return ( 6 |
7 |

8 | $ 9 | {price ? parseFloat(price).toFixed(2) : parseFloat(priceUsd).toFixed(2)} 10 |

11 | 12 | {isPositive === 1 ? "+" : ""} 13 | {changePercent} 14 | 15 |
16 |
17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /src/components/Dashboard/Dashboard.scss: -------------------------------------------------------------------------------- 1 | @import "./../../styles/colors.scss"; 2 | 3 | .button { 4 | border: none; 5 | margin: 0.5em 0; 6 | width: 100%; 7 | font-weight: 600; 8 | border-radius: 4px; 9 | min-width: 200px; 10 | max-width: 315px; 11 | height: 45px; 12 | font-size: 14px !important; 13 | text-transform: uppercase; 14 | cursor: pointer; 15 | } 16 | 17 | .btn-primary:hover { 18 | background: $PrimaryGradient; 19 | } 20 | 21 | .btn-primary:active { 22 | background: $PrimaryGradient; 23 | transform: scale(1.01); 24 | } 25 | 26 | .btn-secondary:active { 27 | transform: scale(1.01); 28 | } 29 | 30 | .btn-primary { 31 | background: $Primary; 32 | color: $NavyDark; 33 | } 34 | 35 | .btn-secondary { 36 | background: $Secondary; 37 | color: $White; 38 | } 39 | 40 | .form-input { 41 | background: $NavyLight; 42 | border: 1px solid $Light; 43 | height: 50px; 44 | border-radius: 5px; 45 | } 46 | 47 | .main { 48 | flex: auto; 49 | background: $NavyLight; 50 | display: flex; 51 | flex-direction: column; 52 | align-items: center; 53 | position: absolute; 54 | left: 15vw; 55 | } 56 | 57 | .up { 58 | color: $Green; 59 | } 60 | 61 | .down { 62 | color: $Red; 63 | } 64 | 65 | .error-no-data { 66 | display: flex; 67 | flex-direction: column; 68 | .bar-chart { 69 | fill: $NavyDark; 70 | height: 10vw; 71 | } 72 | 73 | h2 { 74 | color: $NavyDark; 75 | font-weight: 700; 76 | } 77 | } 78 | 79 | .percentage { 80 | color: $Green; 81 | } 82 | 83 | .container { 84 | height: auto; 85 | position: absolute; 86 | left: 0; 87 | width: 82vw; 88 | margin-top: 6em; 89 | margin-left: 2em; 90 | margin-right: 2em; 91 | margin-bottom: 0; 92 | 93 | .main__container { 94 | // padding: 0 3em !important; 95 | } 96 | } 97 | 98 | .crypto-data { 99 | .tab-block { 100 | border: none; 101 | } 102 | 103 | .news { 104 | margin: 1em; 105 | flex: 43%; 106 | 107 | min-height: 270px; 108 | // border: 1px solid $Light; 109 | padding: 1.5em; 110 | border-radius: 0.5em; 111 | display: flex; 112 | align-items: center; 113 | 114 | h5:hover { 115 | background: $PrimaryGradient; 116 | -webkit-background-clip: text; 117 | -webkit-text-fill-color: transparent; 118 | cursor: pointer; 119 | } 120 | 121 | .time-news { 122 | display: flex; 123 | justify-content: space-between; 124 | align-items: center; 125 | 126 | i { 127 | margin-right: 0.5em; 128 | margin-bottom: 0.15em; 129 | } 130 | } 131 | 132 | small { 133 | font-size: 13px !important; 134 | } 135 | 136 | .meta-news-data { 137 | display: flex; 138 | justify-content: space-between; 139 | } 140 | .pill { 141 | background: $Light; 142 | // max-width: 100px; 143 | font-size: 12px !important; 144 | text-align: center; 145 | border-radius: 2em; 146 | color: white; 147 | padding: 0.25em 1.25em; 148 | margin: 1em 0.25em; 149 | } 150 | 151 | .pill:hover { 152 | background: $PrimaryGradient; 153 | } 154 | 155 | a { 156 | color: $Primary; 157 | cursor: pointer; 158 | outline: none; 159 | background: $PrimaryGradient; 160 | -webkit-background-clip: text; 161 | -webkit-text-fill-color: transparent; 162 | } 163 | 164 | a:hover { 165 | text-decoration: none; 166 | } 167 | } 168 | 169 | .tab-item { 170 | padding-bottom: 1em; 171 | color: $Grey; 172 | text-transform: uppercase; 173 | font-weight: 500; 174 | border-bottom: 1px solid $Light; 175 | cursor: pointer; 176 | } 177 | 178 | .react-tabs__tab--selected { 179 | color: $Primary; 180 | padding-bottom: 1em; 181 | border-bottom: 1px solid; 182 | } 183 | 184 | .analysis { 185 | h2 { 186 | color: white; 187 | text-align: center; 188 | margin: 0; 189 | padding: 0; 190 | } 191 | 192 | p { 193 | text-align: center; 194 | margin: 0; 195 | padding: 0; 196 | } 197 | 198 | .analysis-card { 199 | border: 1px solid $Light; 200 | background: $NavyDark; 201 | height: 135px; 202 | display: flex; 203 | flex-direction: column; 204 | justify-content: center; 205 | width: 300px; 206 | margin: 1em; 207 | border-radius: 3px; 208 | padding: 3em 1em; 209 | 210 | .sentiment { 211 | color: white; 212 | font-weight: 100; 213 | margin-bottom: 1em; 214 | } 215 | 216 | .bar { 217 | background: $NavyDark; 218 | .bar-item { 219 | background: $Primary; 220 | } 221 | } 222 | } 223 | 224 | .crypto-radar-chart { 225 | display: flex; 226 | justify-content: center; 227 | 228 | .recharts-surface { 229 | display: flex; 230 | justify-content: center; 231 | } 232 | 233 | tspan { 234 | fill: $White; 235 | color: $White; 236 | } 237 | } 238 | 239 | .crypto-analysis { 240 | text-align: center; 241 | border: 1px solid $Light; 242 | display: flex; 243 | flex-direction: column; 244 | margin: 1em; 245 | height: 20vh; 246 | justify-content: center; 247 | border-radius: 3px; 248 | 249 | h1 { 250 | margin: 0; 251 | padding: 0; 252 | } 253 | 254 | p { 255 | margin: 0; 256 | padding: 0; 257 | } 258 | } 259 | } 260 | } 261 | 262 | .crypto-actions { 263 | display: flex; 264 | justify-content: space-between; 265 | 266 | button { 267 | border: none; 268 | background: none; 269 | margin-top: 2em; 270 | font-size: 1em; 271 | padding: 0 1em; 272 | color: $Grey; 273 | cursor: pointer; 274 | } 275 | 276 | button:active { 277 | color: $Primary; 278 | } 279 | 280 | .active { 281 | color: $Primary; 282 | } 283 | 284 | button:hover { 285 | color: $Primary; 286 | } 287 | } 288 | 289 | h5 { 290 | font-weight: 200; 291 | } 292 | .chart-price { 293 | color: $White; 294 | font-weight: 500; 295 | margin-left: 0.5em; 296 | } 297 | 298 | .line-chart { 299 | margin-top: 2em; 300 | // background: red; 301 | 302 | // .recharts-area-area { 303 | // fill: red !important; 304 | // } 305 | 306 | h5:last-child { 307 | font-size: 1.2em; 308 | color: rgba(216, 216, 216, 0.865); 309 | margin-right: 0.5em; 310 | } 311 | 312 | .header { 313 | display: flex; 314 | align-items: center; 315 | justify-content: space-between; 316 | margin-bottom: 1em; 317 | } 318 | } 319 | -------------------------------------------------------------------------------- /src/components/Dashboard/SignupModal.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Modal from "react-modal"; 3 | 4 | const customStyles = { 5 | content: { 6 | top: "50%", 7 | left: "50%", 8 | right: "auto", 9 | bottom: "auto", 10 | marginRight: "-50%", 11 | transform: "translate(-50%, -50%)", 12 | zIndex: 100 13 | } 14 | }; 15 | 16 | export default function SignupModal({ modalIsOpen, closeModal }: any) { 17 | return ( 18 | 24 |
25 |
26 |

UnAuthorized

27 |
28 |
29 |
30 |
31 |
32 | 33 | 34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 | 42 | 47 |
48 |
49 |
50 | 59 |
60 |
61 |
62 | ); 63 | } 64 | -------------------------------------------------------------------------------- /src/components/Dashboard/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useContext } from "react"; 2 | 3 | import Navbar from "../Navbar/Navbar"; 4 | import Metrics from "../Metrics/Metrics"; 5 | import CryptoHeader from "../CryptoHeader/CryptoHeader"; 6 | import CryptoData from "../CryptoData"; 7 | import cryptoCurrencyCTX from "../../context/cryptocurrency/cryptoCurrencyContext"; 8 | import pricingContext from "../../context/pricing/pricingContext"; 9 | import "./Dashboard.scss"; 10 | 11 | export default function Dashboard() { 12 | const cryptoCTX = useContext(cryptoCurrencyCTX); 13 | const pricingCTX = useContext(pricingContext); 14 | const { streamTopPrices, topPrices } = pricingCTX; 15 | const { getAsset, getStats, searchAsset, asset, loading, stats } = cryptoCTX; 16 | 17 | useEffect(() => { 18 | getAsset(searchAsset.name); 19 | getStats(searchAsset.symbol); 20 | streamTopPrices( 21 | "ethereum", 22 | "bitcoin", 23 | "monero", 24 | "litecoin", 25 | "dash", 26 | "basic-attention-token", 27 | "ripple", 28 | "stellar" 29 | ); 30 | // eslint-disable-next-line 31 | }, [loading, searchAsset]); 32 | 33 | return ( 34 |
35 | 36 |
37 |
38 |
39 |
40 | 49 |
50 | 51 |
52 |
53 |
54 |
55 | 62 |
63 |
64 |
65 |
66 | ); 67 | } 68 | -------------------------------------------------------------------------------- /src/components/Icons/BAT.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { SVGIconProps } from "../type"; 3 | 4 | export default function BasicAttentionToken({ 5 | className, 6 | style 7 | }: SVGIconProps) { 8 | return ( 9 | 15 | 19 | 23 | 24 | 28 | 29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /src/components/Icons/BarChart.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { SVGIconProps } from "./../type"; 3 | 4 | export default function BarChart({ className, style }: SVGIconProps) { 5 | return ( 6 | 16 | 17 | 18 | 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /src/components/Icons/Bitcoin.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { SVGIconProps } from "./../type"; 3 | 4 | export default function Bitcoin({ style, className }: SVGIconProps) { 5 | return ( 6 | 7 | 8 | 12 | 13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /src/components/Icons/Dash.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { SVGIconProps } from "../type"; 3 | 4 | export default function Dash({ className, style }: SVGIconProps) { 5 | return ( 6 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /src/components/Icons/Ethereum.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { SVGIconProps } from "./../type"; 3 | 4 | function Ethereum({ style, className }: SVGIconProps) { 5 | return ( 6 | 15 | 19 | 20 | 24 | 25 | 26 | 27 | 28 | ); 29 | } 30 | 31 | export default Ethereum; 32 | -------------------------------------------------------------------------------- /src/components/Icons/JBIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export default function JBIcon({ style, className }: any) { 4 | return ( 5 | 6 | 10 | 14 | 15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /src/components/Icons/Litecoin.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { SVGIconProps } from "../type"; 3 | 4 | export default function Litecoin({ className, style }: SVGIconProps) { 5 | return ( 6 | 12 | 16 | 20 | 24 | 25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /src/components/Icons/Loader.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export default function Loader({ width, height }: any) { 4 | return ( 5 | 16 | 17 | 18 | 26 | 27 | 28 | 36 | 37 | 38 | 46 | 47 | 48 | 49 | ); 50 | } 51 | -------------------------------------------------------------------------------- /src/components/Icons/Monero.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { SVGIconProps } from "../type"; 3 | 4 | export default function Monero({ className, style }: SVGIconProps) { 5 | return ( 6 | 13 | 17 | 21 | 22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /src/components/Icons/Nem.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { SVGIconProps } from "../type"; 3 | 4 | export default function Nem({ className, style }: SVGIconProps) { 5 | return ( 6 | 12 | 13 | 14 | 15 | 19 | 20 | 21 | 26 | 27 | 28 | 33 | 34 | 35 | 40 | 41 | 42 | 43 | 44 | ); 45 | } 46 | -------------------------------------------------------------------------------- /src/components/Icons/PieChartIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { SVGIconProps } from "./../type"; 3 | 4 | export default function PieChartIcon({ style, className }: SVGIconProps) { 5 | return ( 6 | 15 | 16 | 17 | 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /src/components/Icons/Ripple.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { SVGIconProps } from "./../type"; 3 | 4 | export default function Ripple({ style, className }: SVGIconProps) { 5 | return ( 6 | 13 | 17 | 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /src/components/Icons/SadIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export default function SadIcon({ className, style }: any) { 4 | return ( 5 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 25 | 33 | 37 | 38 | 42 | 50 | 51 | 52 | 53 | 54 | ); 55 | } 56 | -------------------------------------------------------------------------------- /src/components/Icons/Stellar.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { SVGIconProps } from "../type"; 3 | 4 | export default function Stellar({ className, style }: SVGIconProps) { 5 | return ( 6 | 12 | 16 | 20 | 24 | 28 | 32 | 36 | 40 | 44 | 45 | ); 46 | } 47 | -------------------------------------------------------------------------------- /src/components/Icons/index.ts: -------------------------------------------------------------------------------- 1 | import Bitcoin from "./Bitcoin"; 2 | import Ethereum from "./Ethereum"; 3 | import Ripple from "./Ripple"; 4 | import Monero from "./Monero"; 5 | import Litecoin from "./Litecoin"; 6 | import BasicAttentionToken from "./BAT"; 7 | import Nem from "./Nem"; 8 | import Stellar from "./Stellar"; 9 | import Dash from "./Dash"; 10 | 11 | import PieChartIcon from "./PieChartIcon"; 12 | import SadIcon from "./SadIcon"; 13 | import BarChart from "./BarChart"; 14 | 15 | export { 16 | Bitcoin, 17 | Ethereum, 18 | Dash, 19 | Ripple, 20 | PieChartIcon, 21 | SadIcon, 22 | BarChart, 23 | Monero, 24 | BasicAttentionToken, 25 | Nem, 26 | Litecoin, 27 | Stellar 28 | }; 29 | -------------------------------------------------------------------------------- /src/components/Metrics/Metric.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export default function Metric({ name, value, percentage }: any) { 4 | const isPositive = Math.sign(percentage); 5 | return ( 6 |
7 |

{name}

8 |
9 |
10 | {value}{" "} 11 | {percentage} 12 |
13 |
14 |
15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /src/components/Metrics/Metrics.scss: -------------------------------------------------------------------------------- 1 | @import "./../../styles/colors.scss"; 2 | 3 | .metrics { 4 | display: block; 5 | position: -webkit-sticky; 6 | position: sticky; 7 | top: 8em; 8 | margin: 2.5em auto; 9 | width: 17vw; 10 | max-width: 320px; 11 | min-width: 220px; 12 | 13 | .metric-actions { 14 | display: flex; 15 | flex-direction: column; 16 | } 17 | 18 | .metrics-card { 19 | width: 100%; 20 | display: flex; 21 | flex-direction: column; 22 | margin: 2em 0; 23 | 24 | p { 25 | margin: 0; 26 | padding: 0; 27 | width: 100%; 28 | color: $White; 29 | font-weight: 400; 30 | } 31 | .card { 32 | margin: 0.2em 0; 33 | background: $NavyDark; 34 | border: 1px solid $Light; 35 | height: 100px; 36 | max-width: 315px; 37 | min-width: 200px; 38 | border-radius: 3px; 39 | width: inherit; 40 | color: $White; 41 | display: flex; 42 | text-align: center; 43 | justify-content: center; 44 | align-items: center; 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/components/Metrics/Metrics.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import SignupModal from "./../Dashboard/SignupModal"; 3 | import Metric from "./Metric"; 4 | import Numeral from "numeral"; 5 | import "./Metrics.scss"; 6 | 7 | interface MetricProps { 8 | volume: number; 9 | high24: number; 10 | open24: number; 11 | change24: number; 12 | changePercent?: number; 13 | } 14 | 15 | export default function Metrics({ 16 | volume, 17 | high24, 18 | open24, 19 | change24, 20 | changePercent, 21 | }: MetricProps) { 22 | const [modalIsOpen, setIsOpen] = useState(false); 23 | 24 | function openModal() { 25 | setIsOpen(true); 26 | } 27 | 28 | function closeModal() { 29 | setIsOpen(false); 30 | } 31 | return ( 32 |
33 |
34 | 37 | 40 |
41 | 42 | 47 | 51 | 52 | 53 |
54 | ); 55 | } 56 | -------------------------------------------------------------------------------- /src/components/Navbar/FilteredSearch.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from "react"; 2 | 3 | import cryptoCurrencyCTX from "./../../context/cryptocurrency/cryptoCurrencyContext"; 4 | 5 | export default function FilteredSearch({ search, setFind }: any) { 6 | const cryptoCTX = useContext(cryptoCurrencyCTX); 7 | const { searchNewAsset } = cryptoCTX; 8 | 9 | const handleOnClick = (symbol: string, name: string) => { 10 | searchNewAsset(symbol, name); 11 | setFind([]); 12 | }; 13 | 14 | return ( 15 |
16 | {search.map((c: any) => ( 17 |
handleOnClick(c.symbol, c.id)} 20 | className="search-item" 21 | > 22 |

{c.name}

23 |

{c.symbol}

24 |
25 | ))} 26 |
27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /src/components/Navbar/Navbar.scss: -------------------------------------------------------------------------------- 1 | @import "./../../styles/colors"; 2 | @import "./../../../node_modules/spectre.css/src/variables"; 3 | @import "./../../../node_modules/spectre.css/src/navbar"; 4 | 5 | // .ReactModalPortal { 6 | // background: rgba(0, 0, 0, 0.5) !important; 7 | // } 8 | 9 | .ReactModal__Overlay { 10 | z-index: 100; 11 | background: rgba(0, 0, 0, 0.3) !important; 12 | backdrop-filter: 5px !important; 13 | backdrop-filter: blur(5px); 14 | -moz-backdrop-filter: blur(5px); 15 | -webkit-backdrop-filter: blur(5px); 16 | } 17 | 18 | .ReactModal__Content { 19 | width: 25vw; 20 | height: 60vh; 21 | background: $NavyLight !important; 22 | box-shadow: 0px 0px 60px 0px rgba(0, 0, 0, 0.5); 23 | border: none !important; 24 | border-radius: 0.5em !important; 25 | padding: 2.5em !important; 26 | 27 | h2 { 28 | text-align: center; 29 | color: $White; 30 | font-weight: 100; 31 | } 32 | } 33 | 34 | .side-navbar { 35 | display: flex; 36 | flex-direction: column; 37 | width: 15vw; 38 | position: fixed; 39 | 40 | background: $NavyDark; 41 | height: 100vh; 42 | 43 | .navbar-brand { 44 | height: 89px; 45 | display: flex; 46 | justify-content: center; 47 | align-items: center; 48 | 49 | .logo { 50 | fill: #fff; 51 | height: 20px; 52 | } 53 | } 54 | 55 | .navbar-item { 56 | padding: 1em 2.5em; 57 | display: flex; 58 | align-items: center; 59 | color: $White; 60 | background: none; 61 | font-weight: 100; 62 | font-size: 1.1em; 63 | cursor: pointer; 64 | 65 | .nav-icon { 66 | fill: $White; 67 | height: 1.25em; 68 | margin-right: 1.5em; 69 | } 70 | } 71 | 72 | .active { 73 | color: $Primary; 74 | 75 | .nav-icon { 76 | fill: $Primary; 77 | } 78 | } 79 | } 80 | 81 | .navbar { 82 | padding: 1.25em 4em; 83 | border-bottom: 1px solid $Light; 84 | background: $Navy; 85 | opacity: 0.98; 86 | width: 85vw; 87 | position: fixed; 88 | z-index: 2; 89 | left: 15vw; 90 | .icon-search { 91 | transform: scale(0.75); 92 | } 93 | 94 | span { 95 | display: flex; 96 | align-items: center; 97 | } 98 | 99 | .navbar-icons { 100 | color: $Lighter; 101 | margin: 0 1em; 102 | font-size: 26px; 103 | } 104 | 105 | .navbar-icons:hover { 106 | color: $White; 107 | } 108 | 109 | .dropdown { 110 | display: inline-block; 111 | } 112 | 113 | .dropdown__content { 114 | display: none; 115 | position: absolute; 116 | } 117 | 118 | .dropdown--active .dropdown__content { 119 | display: block; 120 | padding: 2em 0; 121 | background: $NavyLight; 122 | width: 250px; 123 | font-size: 1em; 124 | border-radius: 0.5em; 125 | color: white; 126 | box-shadow: 0px 0px 10px 10px rgba(0, 0, 0, 0.1); 127 | position: absolute; 128 | margin: 0.75em 0; 129 | right: 0.25em; 130 | list-style: none; 131 | 132 | p { 133 | padding: 0.75em 2em; 134 | margin: 0; 135 | text-align: center; 136 | text-transform: uppercase; 137 | } 138 | 139 | p:hover { 140 | background: $Primary; 141 | cursor: pointer; 142 | } 143 | } 144 | 145 | input { 146 | background: none; 147 | border: none; 148 | color: $White; 149 | font-size: 24px; 150 | padding: 0 0.5em; 151 | width: 60vw; 152 | outline: none; 153 | // max-width: 400px; 154 | } 155 | 156 | input::placeholder { 157 | color: $Lighter !important; 158 | text-transform: none; 159 | font-weight: 100; 160 | } 161 | } 162 | 163 | .filter-search { 164 | background: rgba(0, 0, 0, 0.5); 165 | display: flex; 166 | flex-direction: column; 167 | color: rgb(218, 218, 218); 168 | padding: 1.5em 4em; 169 | backdrop-filter: blur(5px); 170 | filter: blur(120%); 171 | -moz-backdrop-filter: blur(5px); 172 | -webkit-backdrop-filter: blur(5px); 173 | position: fixed; 174 | z-index: 1000 !important; 175 | left: 0; 176 | top: 80px; 177 | height: 100vh; 178 | width: 100vw; 179 | 180 | .search-item { 181 | padding: 0.75em; 182 | display: flex; 183 | max-width: 80vw; 184 | 185 | h1 { 186 | padding: 0; 187 | margin: 0; 188 | margin-right: 0.5em; 189 | } 190 | 191 | p { 192 | margin: 0; 193 | padding: 0; 194 | } 195 | } 196 | 197 | .search-item:hover { 198 | color: white; 199 | cursor: pointer; 200 | text-shadow: 0px 30px 10px rgba(0, 0, 0, 1); 201 | transition: 500ms; 202 | transform: scale(1.005); 203 | } 204 | 205 | .secondary { 206 | font-weight: 100; 207 | } 208 | } 209 | 210 | /* Extra small devices (phones, 600px and down) */ 211 | @media only screen and (max-width: 900px) { 212 | .side-navbar { 213 | transition: 3000ms; 214 | width: 0; 215 | display: none !important; 216 | } 217 | 218 | .navbar { 219 | width: 100vw; 220 | } 221 | } 222 | -------------------------------------------------------------------------------- /src/components/Navbar/Navbar.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import SearchBar from "./SearchBar"; 3 | // import GitHubButton from "react-github-btn"; 4 | 5 | import Dropdown, { 6 | DropdownTrigger, 7 | DropdownContent, 8 | // @ts-ignore 9 | } from "react-simple-dropdown"; 10 | 11 | import "./Navbar.scss"; 12 | 13 | export default function Navbar() { 14 | const [isActive, setActive] = useState(false); 15 | 16 | return ( 17 |
18 |
19 | 20 |
21 |
22 | {/* 28 | Star 29 | */} 30 | 36 | 37 | 38 | setActive(!isActive)}> 39 | 40 |
41 | 42 |
43 |
44 | 45 |

Login

46 |

Profile

47 |

Settings

48 |
49 |
50 |
51 |
52 | ); 53 | } 54 | -------------------------------------------------------------------------------- /src/components/Navbar/SearchBar.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, useContext } from "react"; 2 | import cryptoCurrencyCTX from "./../../context/cryptocurrency/cryptoCurrencyContext"; 3 | 4 | import FilteredSearch from "./FilteredSearch"; 5 | 6 | export default function SearchBar() { 7 | const cryptoCTX = useContext(cryptoCurrencyCTX); 8 | const { getTopAssets, assets, loading } = cryptoCTX; 9 | const [find, setFind] = useState([]); 10 | 11 | // eslint-disable-next-line 12 | useEffect(() => getTopAssets(), [loading]); 13 | // eslint-disable-next-line 14 | useEffect(() => {}, [find.length <= 0]); 15 | 16 | const handleOnChange = (e: any) => { 17 | let filteredAssets = assets.filter((asset: any) => 18 | asset.name.toLowerCase().match(new RegExp(e.target.value, "gi")) 19 | ); 20 | if (e.target.value) { 21 | setFind(filteredAssets); 22 | return; 23 | } 24 | setFind([]); 25 | }; 26 | 27 | return ( 28 | <> 29 |
30 | 0 34 | ? { color: "white", transform: "scale(1.02)", animation: "500ms" } 35 | : {} 36 | } 37 | > 38 | 45 |
46 | {find.length > 0 && } 47 | 48 | ); 49 | } 50 | -------------------------------------------------------------------------------- /src/components/Navbar/SideNavbar.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PieChartIcon from "./../Icons/PieChartIcon"; 3 | import JBIcon from "./../Icons/JBIcon"; 4 | import "./Navbar.scss"; 5 | 6 | export default function SideNavbar() { 7 | return ( 8 |
9 |
10 | 11 |
12 |
13 |
14 |
15 | 16 | Dashboard 17 |
18 |
19 |
20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /src/components/type.ts: -------------------------------------------------------------------------------- 1 | export interface SVGIconProps { 2 | className?: string; 3 | style?: React.CSSProperties; 4 | } 5 | 6 | export interface Asset { 7 | symbol: string; 8 | name: string; 9 | } 10 | 11 | export type Sentiment = "bullish" | "bearish"; 12 | 13 | export interface TradingSignals { 14 | id: number; 15 | partner_symbol: string; 16 | symbol: string; 17 | time: number; 18 | largetxsVar: Signal; 19 | addressesNetGrowth: Signal; 20 | concentrationVar: Signal; 21 | inOutVar: Signal; 22 | } 23 | 24 | export interface Signal { 25 | category: string; 26 | score: number; 27 | score_threshold_bearish: number; 28 | score_threshold_bullish: number; 29 | sentiment: Sentiment; 30 | value: number; 31 | } 32 | -------------------------------------------------------------------------------- /src/config.ts: -------------------------------------------------------------------------------- 1 | const CRYPTO_COMPARE_URI = "https://min-api.cryptocompare.com/data/"; 2 | const COIN_CAP_WS_URI = "wss://ws.coincap.io/"; 3 | const COIN_CAP_URI = "https://api.coincap.io/v2/"; 4 | const CRYPTO_COMPARE_API_KEY = 5 | "0ffd426e05f6800fd35e540bcc7d3199a05b7993d7acb6e45144c11adfde6393"; 6 | 7 | export { 8 | CRYPTO_COMPARE_URI, 9 | COIN_CAP_URI, 10 | COIN_CAP_WS_URI, 11 | CRYPTO_COMPARE_API_KEY, 12 | }; 13 | -------------------------------------------------------------------------------- /src/context/cryptocurrency/CryptoCurrencyState.tsx: -------------------------------------------------------------------------------- 1 | import React, { useReducer } from "react"; 2 | import axios from "axios"; 3 | 4 | import { cryptoCurrencyReducer } from "./cryptoCurrencyReducer"; 5 | import CryptoCurrencyContext from "./cryptoCurrencyContext"; 6 | 7 | import { 8 | GET_DAILY_OHLCV, 9 | GET_ASSET, 10 | GET_TOP_ASSETS, 11 | GET_STATISTICS, 12 | GET_TRADING_SIGNALS, 13 | GET_NEWS, 14 | SEARCH_ASSET, 15 | STREAM_TICKER, 16 | } from "../types"; 17 | import { COIN_CAP_URI, CRYPTO_COMPARE_URI } from "./../../config"; 18 | 19 | export default function CryptoCurrencyState(props: any): JSX.Element { 20 | const initialState: any = { 21 | dailyOHLCV: [], 22 | asset: {}, 23 | assets: [], 24 | stats: {}, 25 | loading: true, 26 | quote: {}, 27 | news: [], 28 | tradingSignals: {}, 29 | searchAsset: { 30 | symbol: "BTC", 31 | name: "bitcoin", 32 | }, 33 | }; 34 | 35 | const [state, dispatch] = useReducer(cryptoCurrencyReducer, initialState); 36 | 37 | const getDailyOHLCV = ( 38 | period: string, 39 | symbol: string, 40 | history = "histoday" 41 | ) => { 42 | axios 43 | .get( 44 | `${CRYPTO_COMPARE_URI}v2/${history}?fsym=${symbol}&tsym=USD&limit=${period}` 45 | ) 46 | .then((res) => { 47 | dispatch({ 48 | type: GET_DAILY_OHLCV, 49 | payload: res.data.Data.Data, 50 | }); 51 | }) 52 | .catch((err) => console.error(err)); 53 | }; 54 | 55 | const getAsset = (symbol: string) => { 56 | axios(`${COIN_CAP_URI}assets/${symbol}`) 57 | .then((res) => { 58 | dispatch({ 59 | type: GET_ASSET, 60 | payload: res.data.data, 61 | }); 62 | }) 63 | .catch((err) => console.error(err)); 64 | }; 65 | 66 | const getStats = (symbol: string) => { 67 | axios 68 | .get(`${CRYPTO_COMPARE_URI}pricemultifull?fsyms=${symbol}&tsyms=USD`) 69 | .then((res) => { 70 | dispatch({ 71 | type: GET_STATISTICS, 72 | payload: res.data.RAW[symbol.toUpperCase()].USD, 73 | }); 74 | }) 75 | .catch((err) => console.error(err)); 76 | }; 77 | 78 | const getTopAssets = (): void => { 79 | axios(`${COIN_CAP_URI}assets`) 80 | .then((res) => { 81 | dispatch({ 82 | type: GET_TOP_ASSETS, 83 | payload: res.data.data, 84 | }); 85 | }) 86 | .catch((err) => console.error(err)); 87 | }; 88 | 89 | const getTradingSignals = (symbol: string) => { 90 | axios( 91 | `${CRYPTO_COMPARE_URI}tradingsignals/intotheblock/latest?fsym=${symbol}` 92 | ) 93 | .then((res) => { 94 | dispatch({ 95 | type: GET_TRADING_SIGNALS, 96 | payload: res.data.Data, 97 | }); 98 | }) 99 | .catch((err) => console.error(err)); 100 | }; 101 | 102 | const streamQuote = (symbol: string) => { 103 | var ccStreamer = new WebSocket("wss://streamer.cryptocompare.com/v2"); 104 | 105 | ccStreamer.onopen = function onStreamOpen() { 106 | var subRequest = { 107 | action: "SubAdd", 108 | subs: [`0~Coinbase~${symbol}~USD`], 109 | }; 110 | ccStreamer.send(JSON.stringify(subRequest)); 111 | }; 112 | 113 | ccStreamer.onmessage = function onStreamMessage(message) { 114 | const data = JSON.stringify(message.data); 115 | dispatch({ 116 | type: STREAM_TICKER, 117 | payload: data, 118 | }); 119 | console.error("Received from Cryptocompare: " + data); 120 | }; 121 | }; 122 | 123 | const searchNewAsset = (symbol: string, name: string) => { 124 | dispatch({ 125 | type: SEARCH_ASSET, 126 | payload: { 127 | symbol, 128 | name, 129 | }, 130 | }); 131 | }; 132 | 133 | const getNews = () => { 134 | axios(`${CRYPTO_COMPARE_URI}v2/news/?lang=EN`) 135 | .then((res) => { 136 | dispatch({ 137 | type: GET_NEWS, 138 | payload: res.data.Data, 139 | }); 140 | }) 141 | .catch((err) => console.error(err)); 142 | }; 143 | 144 | const { 145 | searchAsset, 146 | asset, 147 | assets, 148 | dailyOHLCV, 149 | news, 150 | loading, 151 | stats, 152 | pricesWs, 153 | tradingSignals, 154 | quote, 155 | } = state; 156 | 157 | return ( 158 | 181 | {props.children} 182 | 183 | ); 184 | } 185 | -------------------------------------------------------------------------------- /src/context/cryptocurrency/cryptoCurrencyContext.ts: -------------------------------------------------------------------------------- 1 | import { createContext } from "react"; 2 | 3 | const defaultValue = {} as any; 4 | const cryptoCurrencyContext = createContext(defaultValue); 5 | export default cryptoCurrencyContext; 6 | -------------------------------------------------------------------------------- /src/context/cryptocurrency/cryptoCurrencyReducer.ts: -------------------------------------------------------------------------------- 1 | import { 2 | GET_DAILY_OHLCV, 3 | GET_ASSET, 4 | GET_STATISTICS, 5 | GET_TOP_ASSETS, 6 | GET_NEWS, 7 | SEARCH_ASSET, 8 | GET_TRADING_SIGNALS, 9 | STREAM_TICKER 10 | } from "../types"; 11 | 12 | export function cryptoCurrencyReducer(state: any, action: any) { 13 | switch (action.type) { 14 | case GET_DAILY_OHLCV: 15 | return { 16 | ...state, 17 | dailyOHLCV: action.payload, 18 | loading: false 19 | }; 20 | case GET_ASSET: 21 | return { 22 | ...state, 23 | asset: action.payload, 24 | loading: false 25 | }; 26 | case GET_STATISTICS: 27 | return { 28 | ...state, 29 | stats: action.payload, 30 | loading: false 31 | }; 32 | case GET_TOP_ASSETS: 33 | return { 34 | ...state, 35 | assets: action.payload, 36 | loading: false 37 | }; 38 | case GET_TRADING_SIGNALS: 39 | return { 40 | ...state, 41 | tradingSignals: action.payload, 42 | loading: false 43 | }; 44 | case GET_NEWS: 45 | return { 46 | ...state, 47 | news: action.payload, 48 | loading: false 49 | }; 50 | case SEARCH_ASSET: 51 | return { 52 | ...state, 53 | searchAsset: action.payload, 54 | loading: false 55 | }; 56 | case STREAM_TICKER: 57 | return { 58 | ...state, 59 | quote: action.payload, 60 | loading: false 61 | }; 62 | default: 63 | throw new Error(); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/context/pricing/pricingContext.ts: -------------------------------------------------------------------------------- 1 | import { createContext } from "react"; 2 | 3 | interface PriceStreaming { 4 | price: number; 5 | prices: any; 6 | topPrices: any; 7 | quote: any; 8 | priceWebSocket: WebSocket; 9 | pricesWebSocket: WebSocket; 10 | topPriceWebSocket: WebSocket; 11 | } 12 | 13 | const defaultValue = {} as any; 14 | const pricingContext = createContext(defaultValue); 15 | export default pricingContext; 16 | -------------------------------------------------------------------------------- /src/context/pricing/pricingReducer.ts: -------------------------------------------------------------------------------- 1 | import { 2 | STREAM_PRICE, 3 | STREAM_TOP_PRICES, 4 | STREAM_PRICES, 5 | STREAM_TICKER, 6 | } from "./../types"; 7 | 8 | export function pricingReducer(state: any, action: any) { 9 | switch (action.type) { 10 | case STREAM_PRICE: 11 | return { 12 | ...state, 13 | price: action.payload, 14 | loading: false, 15 | }; 16 | case STREAM_PRICES: 17 | return { 18 | ...state, 19 | prices: action.payload, 20 | loading: false, 21 | }; 22 | case STREAM_TOP_PRICES: 23 | return { 24 | ...state, 25 | topPrices: action.payload, 26 | loading: false, 27 | }; 28 | case STREAM_TICKER: 29 | return { 30 | ...state, 31 | quote: action.payload, 32 | loading: false, 33 | }; 34 | default: 35 | throw new Error(); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/context/pricing/pricingState.tsx: -------------------------------------------------------------------------------- 1 | import React, { useReducer } from "react"; 2 | import { pricingReducer } from "./pricingReducer"; 3 | import PricingContext from "./pricingContext"; 4 | 5 | import { COIN_CAP_WS_URI, CRYPTO_COMPARE_API_KEY } from "./../../config"; 6 | import { 7 | STREAM_PRICE, 8 | STREAM_PRICES, 9 | STREAM_TOP_PRICES, 10 | STREAM_TICKER, 11 | } from "../types"; 12 | 13 | export default function PriceStreamingState(props: any) { 14 | const initialState: any = { 15 | price: null, 16 | prices: {}, 17 | topPrices: {}, 18 | quote: {}, 19 | priceWebSocket: null, 20 | pricesWebSocket: null, 21 | topPriceWebSocket: null, 22 | }; 23 | 24 | const [state, dispatch] = useReducer(pricingReducer, initialState); 25 | 26 | const streamPrice = (symbol: string) => { 27 | state.priceWebSocket = new WebSocket( 28 | `${COIN_CAP_WS_URI}prices?assets=${symbol}` 29 | ); 30 | state.priceWebSocket.onmessage = (msg: any) => { 31 | const data = JSON.parse(msg.data); 32 | dispatch({ 33 | type: STREAM_PRICE, 34 | payload: data[symbol], 35 | }); 36 | }; 37 | state.priceWebSocket.onerror = (err: Error) => console.warn(err); 38 | }; 39 | 40 | const streamPrices = () => { 41 | state.pricesWebSocket = new WebSocket( 42 | `${COIN_CAP_WS_URI}prices?assets=ALL` 43 | ); 44 | state.pricesWebSocket.onmessage = (msg: any) => { 45 | const data = JSON.parse(msg.data); 46 | dispatch({ 47 | type: STREAM_PRICES, 48 | payload: data, 49 | }); 50 | }; 51 | state.pricesWebSocket.onerror = (err: Error) => console.warn(err); 52 | }; 53 | 54 | const streamTopPrices = (...symbols: string[]) => { 55 | state.topPriceWebSocket = new WebSocket( 56 | `${COIN_CAP_WS_URI}prices?assets=${symbols.join()}` 57 | ); 58 | state.topPriceWebSocket.onmessage = (msg: any) => { 59 | const data = JSON.parse(msg.data); 60 | dispatch({ 61 | type: STREAM_TOP_PRICES, 62 | payload: data, 63 | }); 64 | }; 65 | state.topPriceWebSocket.onerror = (err: Error) => console.warn(err); 66 | }; 67 | 68 | const streamQuote = (symbol: string) => { 69 | var ccStreamer = new WebSocket( 70 | `wss://streamer.cryptocompare.com/v2?apiKey=${CRYPTO_COMPARE_API_KEY}` 71 | ); 72 | 73 | ccStreamer.onopen = function onStreamOpen() { 74 | var subRequest = { 75 | action: "SubAdd", 76 | subs: [`0~Coinbase~${symbol}~USD`], 77 | }; 78 | ccStreamer.send(JSON.stringify(subRequest)); 79 | }; 80 | 81 | ccStreamer.onmessage = function onStreamMessage(message) { 82 | const data = JSON.parse(message.data); 83 | dispatch({ 84 | type: STREAM_TICKER, 85 | payload: data, 86 | }); 87 | console.error("Received from Cryptocompare: " + data); 88 | }; 89 | }; 90 | 91 | const { price, prices, topPrices, quote } = state; 92 | 93 | return ( 94 | 107 | {props.children} 108 | 109 | ); 110 | } 111 | -------------------------------------------------------------------------------- /src/context/types.ts: -------------------------------------------------------------------------------- 1 | export const GET_WS_PRICE = Symbol("Get realtime Price"); 2 | export const CLOSE_WS_PRICE = Symbol("Close realtime price socket"); 3 | export const GET_DAILY_OHLCV = Symbol("Gets Open, High, Low, Close and Volume"); 4 | export const GET_ASSET = Symbol("Gets a cryptocurrency asset"); 5 | export const GET_STATISTICS = Symbol("Get crypto statistics"); 6 | export const GET_TOP_ASSETS = Symbol("Get crypto assets"); 7 | export const SEARCH_ASSET = Symbol("Search cryptocurrency asset"); 8 | export const GET_TRADING_SIGNALS = Symbol("Get crypto trading signals"); 9 | export const GET_NEWS = Symbol("Get crypto news"); 10 | export const STREAM_TICKER = Symbol("Stream crypto ticker"); 11 | 12 | // Pricing 13 | export const STREAM_PRICES = Symbol("Stream crypto prices"); 14 | export const STREAM_TOP_PRICES = Symbol("Stream top crypto prices"); 15 | export const STREAM_PRICE = Symbol("Stream crypto price"); 16 | -------------------------------------------------------------------------------- /src/index.scss: -------------------------------------------------------------------------------- 1 | @import "./../node_modules/spectre.css/dist/spectre.css"; 2 | @import "./../node_modules/spectre.css/dist/spectre-icons.css"; 3 | 4 | @font-face { 5 | font-family: "FontAwesome"; 6 | src: url("./assets/fonts/fontawesome-webfont.eot") format("embedded-opentype"), 7 | url("./assets/fonts/fontawesome-webfont.woff2") format("woff2"), 8 | url("./assets/fonts/fontawesome-webfont.woff") format("woff"), 9 | url("./assets/fonts/fontawesome-webfont.ttf") format("truetype"); 10 | font-weight: normal; 11 | font-style: normal; 12 | } 13 | 14 | .fa { 15 | display: inline-block; 16 | font: normal normal normal 14px/1 FontAwesome; 17 | font-size: inherit; 18 | text-rendering: auto; 19 | -webkit-font-smoothing: antialiased; 20 | -moz-osx-font-smoothing: grayscale; 21 | } 22 | 23 | .fa-github:before { 24 | content: "\f09b"; 25 | } 26 | 27 | .fa-github-square:before { 28 | content: "\f113"; 29 | } 30 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import "./index.scss"; 4 | import App from "./App"; 5 | import * as serviceWorker from "./serviceWorker"; 6 | import ReactGA from "react-ga"; 7 | import Modal from "react-modal"; 8 | 9 | Modal.setAppElement("#root"); 10 | 11 | ReactGA.initialize("UA-162672611-1"); 12 | 13 | ReactDOM.render( 14 | 15 | 16 | , 17 | document.getElementById("root") 18 | ); 19 | 20 | // If you want your app to work offline and load faster, you can change 21 | // unregister() to register() below. Note this comes with some pitfalls. 22 | // Learn more about service workers: https://bit.ly/CRA-PWA 23 | serviceWorker.unregister(); 24 | -------------------------------------------------------------------------------- /src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /src/serviceWorker.ts: -------------------------------------------------------------------------------- 1 | // This optional code is used to register a service worker. 2 | // register() is not called by default. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on subsequent visits to a page, after all the 7 | // existing tabs open on the page have been closed, since previously cached 8 | // resources are updated in the background. 9 | 10 | // To learn more about the benefits of this model and instructions on how to 11 | // opt-in, read https://bit.ly/CRA-PWA 12 | 13 | const isLocalhost = Boolean( 14 | window.location.hostname === 'localhost' || 15 | // [::1] is the IPv6 localhost address. 16 | window.location.hostname === '[::1]' || 17 | // 127.0.0.0/8 are considered localhost for IPv4. 18 | window.location.hostname.match( 19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 20 | ) 21 | ); 22 | 23 | type Config = { 24 | onSuccess?: (registration: ServiceWorkerRegistration) => void; 25 | onUpdate?: (registration: ServiceWorkerRegistration) => void; 26 | }; 27 | 28 | export function register(config?: Config) { 29 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 30 | // The URL constructor is available in all browsers that support SW. 31 | const publicUrl = new URL( 32 | process.env.PUBLIC_URL, 33 | window.location.href 34 | ); 35 | if (publicUrl.origin !== window.location.origin) { 36 | // Our service worker won't work if PUBLIC_URL is on a different origin 37 | // from what our page is served on. This might happen if a CDN is used to 38 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 39 | return; 40 | } 41 | 42 | window.addEventListener('load', () => { 43 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 44 | 45 | if (isLocalhost) { 46 | // This is running on localhost. Let's check if a service worker still exists or not. 47 | checkValidServiceWorker(swUrl, config); 48 | 49 | // Add some additional logging to localhost, pointing developers to the 50 | // service worker/PWA documentation. 51 | navigator.serviceWorker.ready.then(() => { 52 | console.log( 53 | 'This web app is being served cache-first by a service ' + 54 | 'worker. To learn more, visit https://bit.ly/CRA-PWA' 55 | ); 56 | }); 57 | } else { 58 | // Is not localhost. Just register service worker 59 | registerValidSW(swUrl, config); 60 | } 61 | }); 62 | } 63 | } 64 | 65 | function registerValidSW(swUrl: string, config?: Config) { 66 | navigator.serviceWorker 67 | .register(swUrl) 68 | .then(registration => { 69 | registration.onupdatefound = () => { 70 | const installingWorker = registration.installing; 71 | if (installingWorker == null) { 72 | return; 73 | } 74 | installingWorker.onstatechange = () => { 75 | if (installingWorker.state === 'installed') { 76 | if (navigator.serviceWorker.controller) { 77 | // At this point, the updated precached content has been fetched, 78 | // but the previous service worker will still serve the older 79 | // content until all client tabs are closed. 80 | console.log( 81 | 'New content is available and will be used when all ' + 82 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.' 83 | ); 84 | 85 | // Execute callback 86 | if (config && config.onUpdate) { 87 | config.onUpdate(registration); 88 | } 89 | } else { 90 | // At this point, everything has been precached. 91 | // It's the perfect time to display a 92 | // "Content is cached for offline use." message. 93 | console.log('Content is cached for offline use.'); 94 | 95 | // Execute callback 96 | if (config && config.onSuccess) { 97 | config.onSuccess(registration); 98 | } 99 | } 100 | } 101 | }; 102 | }; 103 | }) 104 | .catch(error => { 105 | console.error('Error during service worker registration:', error); 106 | }); 107 | } 108 | 109 | function checkValidServiceWorker(swUrl: string, config?: Config) { 110 | // Check if the service worker can be found. If it can't reload the page. 111 | fetch(swUrl, { 112 | headers: { 'Service-Worker': 'script' } 113 | }) 114 | .then(response => { 115 | // Ensure service worker exists, and that we really are getting a JS file. 116 | const contentType = response.headers.get('content-type'); 117 | if ( 118 | response.status === 404 || 119 | (contentType != null && contentType.indexOf('javascript') === -1) 120 | ) { 121 | // No service worker found. Probably a different app. Reload the page. 122 | navigator.serviceWorker.ready.then(registration => { 123 | registration.unregister().then(() => { 124 | window.location.reload(); 125 | }); 126 | }); 127 | } else { 128 | // Service worker found. Proceed as normal. 129 | registerValidSW(swUrl, config); 130 | } 131 | }) 132 | .catch(() => { 133 | console.log( 134 | 'No internet connection found. App is running in offline mode.' 135 | ); 136 | }); 137 | } 138 | 139 | export function unregister() { 140 | if ('serviceWorker' in navigator) { 141 | navigator.serviceWorker.ready 142 | .then(registration => { 143 | registration.unregister(); 144 | }) 145 | .catch(error => { 146 | console.error(error.message); 147 | }); 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /src/setupTests.ts: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom/extend-expect'; 6 | -------------------------------------------------------------------------------- /src/styles/colors.scss: -------------------------------------------------------------------------------- 1 | // Background 2 | $Navy: #1f232b; 3 | $NavyDark: #1c2128; 4 | $NavyLight: #20242d; 5 | $Light: rgba(56, 62, 70, 0.4); 6 | $Lighter: rgb(64, 71, 80); 7 | 8 | // Buttons and Links 9 | $Primary: #1f90ff; 10 | $PrimaryGradient: linear-gradient( 11 | 90deg, 12 | rgba(47, 132, 252, 1) 0%, 13 | rgba(77, 189, 245, 1) 100% 14 | ); 15 | $Secondary: #3e485b; 16 | 17 | // Text 18 | $Grey: rgba(255, 255, 255, 0.7); 19 | $White: #ffffff; 20 | 21 | // Highs and Lows 22 | $Green: #72e8a1; 23 | $Red: #e97373; 24 | -------------------------------------------------------------------------------- /src/styles/colors.ts: -------------------------------------------------------------------------------- 1 | export const Primary: string = "#1f90ff"; 2 | export const Light: string = "rgba(56, 62, 70, 0.3)"; 3 | export const Lighter: string = "rgb(64, 71, 80)"; 4 | export const White: string = "#fff"; 5 | export const Navy: string = "#1b1f26"; 6 | -------------------------------------------------------------------------------- /src/styles/index.ts: -------------------------------------------------------------------------------- 1 | import { Light } from "./colors"; 2 | 3 | const chartWidth = 4 | window.innerWidth >= 900 ? window.innerWidth / 1.68 : window.innerWidth - 20; 5 | 6 | const chartToolTipStyle = { 7 | background: "rgba(24, 27, 33, .95)", 8 | border: `1px solid ${Light}`, 9 | borderRadius: "3px" 10 | }; 11 | 12 | export { chartWidth, chartToolTipStyle }; 13 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "module": "esnext", 16 | "moduleResolution": "node", 17 | "resolveJsonModule": true, 18 | "isolatedModules": true, 19 | "noEmit": true, 20 | "jsx": "react" 21 | }, 22 | "include": [ 23 | "src" 24 | ] 25 | } 26 | --------------------------------------------------------------------------------