",
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
--------------------------------------------------------------------------------