├── .editorconfig
├── .gitignore
├── README.md
├── package.json
├── public
├── favicon.ico
├── index.html
└── manifest.json
├── src
├── App.js
├── App.scss
├── App.test.js
├── components
│ └── Orderbook
│ │ ├── Orderbook.scss
│ │ ├── actions.js
│ │ ├── index.js
│ │ └── reducer.js
├── configureStore.js
├── index.js
├── index.scss
├── reducers.js
└── registerServiceWorker.js
└── yarn.lock
/.editorconfig:
--------------------------------------------------------------------------------
1 | # http://editorconfig.org
2 | root = true
3 |
4 | [*]
5 | indent_style = space
6 | indent_size = 2
7 | end_of_line = lf
8 | charset = utf-8
9 | trim_trailing_whitespace = true
10 | insert_final_newline = true
11 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # ----------------------------------------------------------------------------------------------
2 | # Git Ignores @see https://github.com/github/gitignore
3 | # ----------------------------------------------------------------------------------------------
4 | !.gitignore
5 |
6 | # Application Specific
7 | # ----------------------------------------------------------------------------------------------
8 | **/*.css
9 | /build
10 |
11 | # Bower / Node / Grunt / SASS
12 | # @see - http://stackoverflow.com/questions/39990017/should-i-commit-the-yarn-lock-file-and-what-is-it-for
13 | # ----------------------------------------------------------------------------------------------
14 | *node_modules
15 | *bower_components
16 | *.grunt
17 | *.sass-cache
18 | *.tmp
19 | *tmp
20 | *.seed
21 | *.log
22 | *.csv
23 | *.dat
24 | *.out
25 | *.pid
26 | *.gz
27 | *.swp
28 | *.tar.gz
29 | *.cache-loader
30 |
31 |
32 | # OSX
33 | # ----------------------------------------------------------------------------------------------
34 | .DS_Store
35 | **/.DS_Store
36 | ._*
37 |
38 | # Files that might appear on external disk
39 | .Spotlight-V100
40 | .Trashes
41 | .com.apple.timemachine.supported
42 |
43 |
44 | # Windows
45 | # ----------------------------------------------------------------------------------------------
46 | Thumbs.db
47 |
48 |
49 | # Sublime / Other Editors
50 | # ----------------------------------------------------------------------------------------------
51 | # cache files for sublime text
52 | *.tmlanguage.cache
53 | *.tmPreferences.cache
54 | *.stTheme.cache
55 | .idea
56 | *.iml
57 | *.sublime-*
58 |
59 |
60 | # Tags
61 | # ----------------------------------------------------------------------------------------------
62 | # Ignore tags created by etags and ctags
63 | TAGS
64 | tags
65 |
66 |
67 | # Vim
68 | # ----------------------------------------------------------------------------------------------
69 | #.*.sw[a-z]
70 | #*.un~
71 | Session.vim
72 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | React Orderbook
2 | ============================
3 |
4 | * Version: 1.0.3
5 | * Developer: Ryan Powszok
6 | * Website: [ryanpowszok.com](https://ryanpowszok.com)
7 | * Copyright: (c) 2018 Ryan Powszok
8 | * Last Updated: 08/24/2018 Created: 08/23/2018
9 |
10 | ## Overview
11 |
12 | This project was bootstrapped with [Create React App](https://github.com/facebookincubator/create-react-app).
13 |
14 | ---
15 | ## Helpful URLs
16 | - [React Orderbook](https://ryanpowszok.github.io/react-orderbook/)
17 |
18 | ---
19 | ## Project Structure
20 |
21 | ```
22 | project/ # → Root folder for the project.
23 | ├── .editorconfig # → Editor config used for defining indent style/spaces.
24 | ├── .gitignore # → Git config file to ignore files and directories.
25 | ├── public/ # → Static site files.
26 | ├── README.md # → Markdown readme file.
27 | ```
28 |
29 | ## Commands
30 |
31 | In the project directory, you can run:
32 |
33 | ### `npm start`
34 |
35 | Runs the app in the development mode.
36 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
37 |
38 | The page will reload if you make edits.
39 | You will also see any lint errors in the console.
40 |
41 | ### `npm test`
42 |
43 | Launches the test runner in the interactive watch mode.
44 | See the section about [running tests](#running-tests) for more information.
45 |
46 | ### `npm run build`
47 |
48 | Builds the app for production to the `build` folder.
49 | It correctly bundles React in production mode and optimizes the build for the best performance.
50 |
51 | The build is minified and the filenames include the hashes.
52 | Your app is ready to be deployed!
53 |
54 | ### `npm run deploy`
55 |
56 | Builds the app for production.
57 | Deploys to Github pages.
58 |
59 | ---
60 | ## Additional Information
61 |
62 | ### Changelog
63 |
64 | ---
65 |
66 | Project repo [React Orderbook](https://github.com/ryanpowszok/react-orderbook).
67 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "my-app",
3 | "version": "0.1.0",
4 | "private": true,
5 | "homepage": "https://ryanpowszok.github.io/react-orderbook",
6 | "repository": {
7 | "type": "git",
8 | "url": "https://github.com/ryanpowszok/react-orderbook"
9 | },
10 | "dependencies": {
11 | "babel-polyfill": "6.26.0",
12 | "debounce": "1.2.0",
13 | "gh-pages": "^1.2.0",
14 | "node-sass-chokidar": "^1.3.3",
15 | "npm-run-all": "^4.1.3",
16 | "react": "^16.4.2",
17 | "react-dom": "^16.4.2",
18 | "react-redux": "5.0.7",
19 | "react-scripts": "1.1.5",
20 | "redux": "4.0.0",
21 | "redux-thunk": "2.3.0"
22 | },
23 | "scripts": {
24 | "reactjs:watch": "react-scripts start",
25 | "reactjs:build": "react-scripts build",
26 | "start": "npm-run-all -p styles:watch reactjs:watch",
27 | "build": "npm-run-all styles:build reactjs:build",
28 | "test": "react-scripts test --env=jsdom",
29 | "eject": "react-scripts eject",
30 | "styles:build": "node-sass-chokidar --include-path ./node_modules src/ -o src/",
31 | "styles:watch": "npm run styles:build && node-sass-chokidar --include-path ./node_modules src/ -o src/ --watch --recursive",
32 | "predeploy": "npm run build",
33 | "deploy": "gh-pages -d build"
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ryanpowszok/react-orderbook/6d4ee17c995a3b1b4672689b107c2c02a84e3ee3/public/favicon.ico
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | React Orderbook
10 |
11 |
12 |
13 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React Orderbook",
3 | "name": "React Orderbook",
4 | "start_url": "./index.html",
5 | "display": "standalone",
6 | "theme_color": "#000000",
7 | "background_color": "#ffffff"
8 | }
9 |
--------------------------------------------------------------------------------
/src/App.js:
--------------------------------------------------------------------------------
1 | import 'babel-polyfill';
2 | import React, { Component } from 'react';
3 |
4 | import './App.css';
5 | import Orderbook from './components/Orderbook';
6 |
7 | class App extends Component {
8 | render() {
9 | return (
10 |
11 |
12 |
13 | );
14 | }
15 | }
16 |
17 | export default App;
18 |
--------------------------------------------------------------------------------
/src/App.scss:
--------------------------------------------------------------------------------
1 | .App {
2 | //
3 | }
4 |
--------------------------------------------------------------------------------
/src/App.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import App from './App';
4 |
5 | it('renders without crashing', () => {
6 | const div = document.createElement('div');
7 | ReactDOM.render(, div);
8 | ReactDOM.unmountComponentAtNode(div);
9 | });
10 |
--------------------------------------------------------------------------------
/src/components/Orderbook/Orderbook.scss:
--------------------------------------------------------------------------------
1 | .Orderbook {
2 | display: flex;
3 | height: 100vh;
4 | flex-direction: column;
5 | flex-wrap: nowrap;
6 |
7 | &__header {
8 | color: #fff;
9 | background: #1a2125;
10 |
11 | &__top {
12 | position: relative;
13 | border-bottom: 1px solid #15181c;
14 | padding: 15px 40px;
15 | padding-left: 65px;
16 | font-size: 14px;
17 | font-weight: bold;
18 |
19 | // Spacer
20 | &::before {
21 | background: #1a2125;
22 | position: absolute;
23 | left: 0;
24 | top: 0;
25 | bottom: 0;
26 | width: 25px;
27 | content: '';
28 | display: block;
29 | border-right: 1px solid #15181c;
30 | }
31 | }
32 |
33 | &__bottom {
34 | position: relative;
35 | border-bottom: 1px solid #15181c;
36 | padding: 15px 40px;
37 | padding-left: 65px;
38 |
39 | // Spacer
40 | &::before {
41 | background: #1a2125;
42 | position: absolute;
43 | left: 0;
44 | top: 0;
45 | bottom: 0;
46 | width: 25px;
47 | content: '';
48 | display: block;
49 | border-right: 1px solid #15181c;
50 | }
51 |
52 | .bold {
53 | margin-right: 5px;
54 | display: inline-block;
55 | font-size: 14px;
56 | font-weight: bold;
57 | }
58 |
59 | .help-text {
60 | margin-right: 30px;
61 | display: inline-block;
62 | font-size: 11px;
63 | color: #767a7c;
64 | }
65 | }
66 | }
67 |
68 |
69 | &__book {
70 | display: flex;
71 | flex-direction: column;
72 | flex-wrap: nowrap;
73 | flex-grow: 1;
74 | max-width: 420px;
75 | color: #fff;
76 | background: #18232b;
77 | border-right: 1px solid #15181c;
78 |
79 | &__header {
80 | position: relative;
81 | border-bottom: 1px solid #15181c;
82 | background: #323d44;
83 | color: #fff;
84 | padding: 15px 20px;
85 | padding-left: 65px;
86 |
87 | // Spacer
88 | &::before {
89 | background: #323d44;
90 | position: absolute;
91 | left: 0;
92 | top: 0;
93 | bottom: 0;
94 | width: 25px;
95 | content: '';
96 | display: block;
97 | border-right: 1px solid #15181c;
98 | }
99 |
100 | .heading {
101 | font-size: 14px;
102 | font-weight: bold;
103 | }
104 | }
105 |
106 | &__subheader {
107 | text-align: right;
108 | position: relative;
109 | border-bottom: 1px solid #15181c;
110 | color: #fff;
111 | padding: 10px 0;
112 | padding-left: 25px;
113 |
114 | // Spacer
115 | &::before {
116 | background: #323d44;
117 | position: absolute;
118 | left: 0;
119 | top: 0;
120 | bottom: 0;
121 | width: 25px;
122 | content: '';
123 | display: block;
124 | border-right: 1px solid #15181c;
125 | }
126 |
127 | .heading {
128 | font-size: 12px;
129 | }
130 |
131 | .columns {
132 | padding: 0 20px;
133 |
134 | &::after {
135 | content: "";
136 | clear: both;
137 | display: table;
138 | }
139 | }
140 |
141 | .column {
142 | width: percentage(1/3);
143 | float: left;
144 | }
145 | }
146 |
147 | &__content {
148 | flex-grow: 1;
149 | position: relative;
150 | border-bottom: 1px solid #15181c;
151 | color: #fff;
152 |
153 | // Spacer
154 | &::before {
155 | background: #323d44;
156 | position: absolute;
157 | left: 0;
158 | top: 0;
159 | bottom: 0;
160 | width: 25px;
161 | content: '';
162 | display: block;
163 | border-right: 1px solid #15181c;
164 | }
165 |
166 | &-inner {
167 | padding: 10px 0;
168 | overflow: auto;
169 | position: absolute;
170 | top: 0;
171 | left: 25px;
172 | right: 0;
173 | bottom: 0;
174 | z-index: 1;
175 | }
176 |
177 | .loading--,
178 | .error-- {
179 | padding: 10px;
180 | padding-left: 20px;
181 | }
182 |
183 | .asks {
184 | .price {
185 | color: #ee7248;
186 | }
187 | }
188 |
189 | .bids {
190 | .price {
191 | color: #a1f379;
192 | }
193 | }
194 | }
195 |
196 | &__item {
197 | text-align: right;
198 | padding: 5px 0;
199 | font-size: 10px;
200 |
201 | &:first-child {
202 | padding-top: 0;
203 | }
204 |
205 | &:last-child {
206 | padding-bottom: 0;
207 | }
208 |
209 | .columns {
210 | padding: 0 20px;
211 |
212 | &::after {
213 | content: "";
214 | clear: both;
215 | display: table;
216 | }
217 | }
218 |
219 | .column {
220 | width: percentage(1/3);
221 | float: left;
222 | }
223 | }
224 |
225 | #midpointPrice {
226 | padding-top: 15px;
227 | padding-bottom: 15px;
228 | margin-top: 15px;
229 | margin-bottom: 15px;
230 | border-bottom: 1px solid #15181c;
231 | border-top: 1px solid #15181c;
232 | font-size: 12px;
233 | font-weight: bold;
234 |
235 | .percentage {
236 | margin-left: 5px;
237 | }
238 | }
239 | }
240 | }
241 |
242 | .color-green {
243 | color: #a1f379;
244 | }
245 |
246 | .color-red {
247 | color: #ee7248;
248 | }
249 |
250 | .error-- {
251 | color: #ee7248;
252 | font-size: 12px;
253 | }
254 |
--------------------------------------------------------------------------------
/src/components/Orderbook/actions.js:
--------------------------------------------------------------------------------
1 | export function hasErrored(bool) {
2 | return {
3 | type: 'ORDERBOOK_HAS_ERRORED',
4 | hasErrored: bool
5 | }
6 | }
7 |
8 | export function isLoading(bool) {
9 | return {
10 | type: 'ORDERBOOK_IS_LOADING',
11 | isLoading: bool
12 | }
13 | }
14 | export function hasFetched(bool) {
15 | return {
16 | type: 'ORDERBOOK_HAS_FETCHED',
17 | hasFetched: bool
18 | }
19 | }
20 |
21 | export function socketUpdate(data) {
22 | return {
23 | type: 'ORDERBOOK_WS_UPDATE',
24 | data: data
25 | }
26 | }
27 |
28 | export function connectToSocket() {
29 | return (dispatch) => {
30 | dispatch(isLoading(true))
31 |
32 | const socket = new WebSocket('wss://ws-feed.pro.coinbase.com')
33 |
34 | const handleData = (event) => {
35 | dispatch(isLoading(false))
36 | dispatch(hasFetched(true))
37 |
38 | const data = JSON.parse(event.data)
39 | dispatch(socketUpdate({
40 | type: data.type,
41 | response: data
42 | }))
43 | }
44 |
45 | socket.addEventListener('message', handleData)
46 |
47 | socket.addEventListener('open', () => {
48 | socket.send(JSON.stringify({
49 | type: 'subscribe',
50 | product_ids: [
51 | 'ETH-USD'
52 | ],
53 | channels: [
54 | 'level2',
55 | 'ticker'
56 | ]
57 | }))
58 | })
59 |
60 | socket.addEventListener('close', () => {
61 | console.info('WebSocket disconnected.')
62 | dispatch(hasErrored(true))
63 | })
64 |
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/src/components/Orderbook/index.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import { connect } from 'react-redux'
3 | import { debounce } from 'lodash';
4 |
5 | import './Orderbook.css'
6 | import { connectToSocket } from './actions'
7 |
8 | class Orderbook extends Component {
9 | constructor(props) {
10 | super(props)
11 |
12 | this.midpointRef = React.createRef()
13 | this.renderHeaderBottom = debounce(this.renderHeaderBottom, 100, { leading: true, maxWait: 100 })
14 | this.renderOrdersContainer = debounce(this.renderOrdersContainer, 100, { leading: true, maxWait: 100 })
15 |
16 | this.state = {
17 | hasScrolled: false
18 | }
19 | }
20 |
21 | componentDidMount() {
22 | this.props.connectToSocket()
23 | }
24 |
25 | componentWillReceiveProps(props) {
26 | if(!this.state.hasScrolled) {
27 | if (this.props.asks.length > 0 && this.props.bids.length > 0) {
28 | if(this.midpointRef.current) {
29 | this.midpointRef.current.scrollIntoView({block: 'center'})
30 | this.setState({ hasScrolled: true })
31 | }
32 | }
33 | }
34 | }
35 |
36 | renderOrders(orders) {
37 | return (
38 |
39 | {orders.map((order, index) => (
40 |
41 |
42 |
43 | {parseFloat(order[1]).toFixed(4)}
44 |
45 |
46 | {parseFloat(order[0]).toFixed(2)}
47 |
48 |
49 |
50 |
51 |
52 |
53 | ))}
54 |
55 | )
56 | }
57 |
58 | renderOrdersMidpoint() {
59 | return (
60 |
61 |
62 |
63 | Midpoint Price:
64 |
65 |
66 | {this.props.price} {this.calcPriceChange()}
67 |
68 |
69 |
70 |
71 |
72 |
73 | )
74 | }
75 |
76 | calcPriceChange() {
77 | const perc = ( this.props.price / this.props.open ) - 1;
78 | const className = perc >= 0 ? 'color-green' : 'color-red';
79 | const prefix = perc >= 0 ? '+' : '';
80 | return {prefix}{(perc * 100).toFixed(2)}%;
81 | }
82 |
83 | renderOrdersContainer(order) {
84 | if (this.props.hasErrored) {
85 | return Sorry! There was an error loading the items
86 | }
87 |
88 | if (!this.props.hasFetched || this.props.isLoading) {
89 | return Loading…
90 | }
91 |
92 | return (
93 |
94 |
95 | {this.renderOrders(this.limitOrders(this.props.asks, 50).reverse())}
96 |
97 | {this.renderOrdersMidpoint()}
98 |
99 | {this.renderOrders(this.limitOrders(this.props.bids, 50))}
100 |
101 |
102 | )
103 | }
104 |
105 | limitOrders(orders, amount) {
106 | return [...orders.slice(0, amount)]
107 | }
108 |
109 | renderHeaderBottom() {
110 | if (this.props.hasErrored) {
111 | return Sorry! There was an error loading the items
112 | }
113 | if (!this.props.hasFetched || this.props.isLoading || !this.props.price) {
114 | return Loading…
115 | }
116 | return (
117 |
118 | {this.props.price} USD Last trade price
119 | {this.calcPriceChange()} 24h price
120 | {this.props.volume} ETH 24h volume
121 |
122 | )
123 | }
124 |
125 | render() {
126 | return (
127 |
128 |
136 |
137 |
138 | Order Book
139 |
140 |
141 |
142 |
143 | Market Size
144 |
145 |
146 | Price (USD)
147 |
148 |
149 |
150 |
151 |
152 |
153 |
154 |
155 | {this.renderOrdersContainer()}
156 |
157 |
158 |
159 |
160 | )
161 | }
162 | }
163 |
164 | const mapStateToProps = (state) => {
165 | return {
166 | isLoading: state.orderbook.isLoading,
167 | hasErrored: state.orderbook.hasErrored,
168 | hasFetched: state.orderbook.hasFetched,
169 | price: state.orderbook.price,
170 | open: state.orderbook.open,
171 | volume: state.orderbook.volume,
172 | asks: state.orderbook.asks,
173 | bids: state.orderbook.bids
174 | }
175 | }
176 |
177 | const mapDispatchToProps = (dispatch) => {
178 | return {
179 | connectToSocket: () => dispatch(connectToSocket())
180 | }
181 | }
182 |
183 | export default connect(mapStateToProps, mapDispatchToProps)(Orderbook)
184 |
--------------------------------------------------------------------------------
/src/components/Orderbook/reducer.js:
--------------------------------------------------------------------------------
1 | const DEFAULT = {
2 | hasErrored: false,
3 | isLoading: false,
4 | hasFetched: false,
5 | price: '',
6 | open: '',
7 | volume: '',
8 | asks: [],
9 | bids: []
10 | }
11 |
12 | export default function(state = DEFAULT, action) {
13 | switch (action.type) {
14 | case 'ORDERBOOK_HAS_ERRORED':
15 | return {
16 | ...state,
17 | hasErrored: action.hasErrored
18 | }
19 |
20 | case 'ORDERBOOK_IS_LOADING':
21 | return {
22 | ...state,
23 | isLoading: action.isLoading
24 | }
25 |
26 | case 'ORDERBOOK_HAS_FETCHED':
27 | return {
28 | ...state,
29 | hasFetched: action.hasFetched
30 | }
31 |
32 | case 'ORDERBOOK_WS_UPDATE':
33 | switch (action.data.type) {
34 | case 'snapshot':
35 | // console.log('------------------');
36 | // console.log('ORDERBOOK_WS_UPDATE: snapshot');
37 | // console.log('------------------');
38 | // console.log('asks:', [...action.data.response.asks.slice(0, 10)]);
39 | // console.log('bids:', [...action.data.response.bids.slice(0, 10)]);
40 |
41 | return {
42 | ...state,
43 | asks: [...action.data.response.asks],
44 | bids: [...action.data.response.bids],
45 | }
46 |
47 | case 'l2update':
48 |
49 | action.data.response.changes.forEach((change, i) => {
50 | let [saleType, price, size] = change
51 |
52 | // Inherit previous state
53 | let updateArr = [...state.asks]
54 | if(saleType === 'buy') {
55 | updateArr = [...state.bids]
56 | }
57 |
58 | const index = updateArr.findIndex(order => {
59 | if(saleType === 'buy') {
60 | return parseFloat(order[0]) <= parseFloat(price)
61 | }
62 | return parseFloat(order[0]) >= parseFloat(price)
63 | })
64 |
65 | // console.log('------------------');
66 | // console.log('ORDERBOOK_WS_UPDATE: l2update');
67 | // console.log('------------------');
68 | // console.log('saleType:', saleType);
69 | // console.log('price:', price);
70 | // console.log('size:', size);
71 | // console.log(updateArr[index]);
72 |
73 | // If order is found in array, then update size or remove it
74 | if (updateArr[index] && parseFloat(updateArr[index][0]) === parseFloat(price)) {
75 | // If size is not zero, then update size since its changed
76 | if (parseFloat(size) > 0) {
77 | updateArr[index][1] = size
78 | // If update size is zero then just remove order node
79 | } else {
80 | updateArr.splice(index, 1)
81 | }
82 | // If no index is found then we need to add it
83 | } else {
84 | // Size should be great than zero but just in case
85 | if (parseFloat(size) > 0) {
86 | updateArr.splice(index, 0, [price, size])
87 | }
88 | }
89 |
90 | state = {
91 | ...state,
92 | asks: saleType !== 'buy' ? updateArr : state.asks,
93 | bids: saleType === 'buy' ? updateArr : state.bids
94 | }
95 | })
96 |
97 | return state
98 |
99 | case 'ticker':
100 | // console.log('------------------');
101 | // console.log('ORDERBOOK_WS_UPDATE: ticker');
102 | // console.log('------------------');
103 | // console.log('price:', action.data.response.price);
104 | // console.log('open:', action.data.response.open_24h);
105 | // console.log('volume:', action.data.response.volume_24h);
106 |
107 | return {
108 | ...state,
109 | price: parseFloat(action.data.response.price).toFixed(2),
110 | open: parseFloat(action.data.response.open_24h).toFixed(2),
111 | volume: parseFloat(action.data.response.volume_24h).toFixed(2)
112 | }
113 |
114 | default:
115 | return state
116 | }
117 |
118 | default:
119 | return state
120 | }
121 | }
122 |
--------------------------------------------------------------------------------
/src/configureStore.js:
--------------------------------------------------------------------------------
1 | import { createStore, applyMiddleware } from 'redux';
2 | import thunk from 'redux-thunk';
3 | import rootReducer from './reducers';
4 | export default function configureStore(initialState) {
5 | return createStore(
6 | rootReducer,
7 | initialState,
8 | applyMiddleware(thunk)
9 | );
10 | }
11 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import { Provider } from 'react-redux';
4 | import configureStore from './configureStore';
5 |
6 | import './index.css';
7 | import App from './App';
8 | import registerServiceWorker from './registerServiceWorker';
9 |
10 | const store = configureStore();
11 |
12 | ReactDOM.render(
13 |
14 |
15 | ,
16 | document.getElementById('root')
17 | );
18 | registerServiceWorker();
19 |
--------------------------------------------------------------------------------
/src/index.scss:
--------------------------------------------------------------------------------
1 | * {
2 | box-sizing: border-box;
3 | }
4 |
5 | *,
6 | *::before,
7 | *::after {
8 | box-sizing: border-box;
9 | }
10 |
11 | html,
12 | body {
13 | margin: 0;
14 | min-height: 100%;
15 | min-width: 300px;
16 | padding: 0;
17 | }
18 |
19 | body {
20 | background: #18232b;
21 | font-family: sans-serif;
22 | -webkit-font-smoothing: antialiased;
23 | font-smoothing: antialiased;
24 | }
25 |
26 | #root,
27 | .App {
28 | height: 100vh;
29 | }
30 |
31 | h1, h2, h3 {
32 | margin: 0;
33 | }
34 |
--------------------------------------------------------------------------------
/src/reducers.js:
--------------------------------------------------------------------------------
1 | import { combineReducers } from 'redux';
2 | import orderbookReducer from './components/Orderbook/reducer';
3 |
4 | export default combineReducers({
5 | orderbook: orderbookReducer
6 | });
7 |
--------------------------------------------------------------------------------
/src/registerServiceWorker.js:
--------------------------------------------------------------------------------
1 | // In production, we register a service worker to serve assets from local cache.
2 |
3 | // This lets the app load faster on subsequent visits in production, and gives
4 | // it offline capabilities. However, it also means that developers (and users)
5 | // will only see deployed updates on the "N+1" visit to a page, since previously
6 | // cached resources are updated in the background.
7 |
8 | // To learn more about the benefits of this model, read https://goo.gl/KwvDNy.
9 | // This link also includes instructions on opting out of this behavior.
10 |
11 | const isLocalhost = Boolean(
12 | window.location.hostname === 'localhost' ||
13 | // [::1] is the IPv6 localhost address.
14 | window.location.hostname === '[::1]' ||
15 | // 127.0.0.1/8 is considered localhost for IPv4.
16 | window.location.hostname.match(
17 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
18 | )
19 | );
20 |
21 | export default function register() {
22 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
23 | // The URL constructor is available in all browsers that support SW.
24 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location);
25 | if (publicUrl.origin !== window.location.origin) {
26 | // Our service worker won't work if PUBLIC_URL is on a different origin
27 | // from what our page is served on. This might happen if a CDN is used to
28 | // serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374
29 | return;
30 | }
31 |
32 | window.addEventListener('load', () => {
33 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
34 |
35 | if (isLocalhost) {
36 | // This is running on localhost. Lets check if a service worker still exists or not.
37 | checkValidServiceWorker(swUrl);
38 |
39 | // Add some additional logging to localhost, pointing developers to the
40 | // service worker/PWA documentation.
41 | navigator.serviceWorker.ready.then(() => {
42 | console.log(
43 | 'This web app is being served cache-first by a service ' +
44 | 'worker. To learn more, visit https://goo.gl/SC7cgQ'
45 | );
46 | });
47 | } else {
48 | // Is not local host. Just register service worker
49 | registerValidSW(swUrl);
50 | }
51 | });
52 | }
53 | }
54 |
55 | function registerValidSW(swUrl) {
56 | navigator.serviceWorker
57 | .register(swUrl)
58 | .then(registration => {
59 | registration.onupdatefound = () => {
60 | const installingWorker = registration.installing;
61 | installingWorker.onstatechange = () => {
62 | if (installingWorker.state === 'installed') {
63 | if (navigator.serviceWorker.controller) {
64 | // At this point, the old content will have been purged and
65 | // the fresh content will have been added to the cache.
66 | // It's the perfect time to display a "New content is
67 | // available; please refresh." message in your web app.
68 | console.log('New content is available; please refresh.');
69 | } else {
70 | // At this point, everything has been precached.
71 | // It's the perfect time to display a
72 | // "Content is cached for offline use." message.
73 | console.log('Content is cached for offline use.');
74 | }
75 | }
76 | };
77 | };
78 | })
79 | .catch(error => {
80 | console.error('Error during service worker registration:', error);
81 | });
82 | }
83 |
84 | function checkValidServiceWorker(swUrl) {
85 | // Check if the service worker can be found. If it can't reload the page.
86 | fetch(swUrl)
87 | .then(response => {
88 | // Ensure service worker exists, and that we really are getting a JS file.
89 | if (
90 | response.status === 404 ||
91 | response.headers.get('content-type').indexOf('javascript') === -1
92 | ) {
93 | // No service worker found. Probably a different app. Reload the page.
94 | navigator.serviceWorker.ready.then(registration => {
95 | registration.unregister().then(() => {
96 | window.location.reload();
97 | });
98 | });
99 | } else {
100 | // Service worker found. Proceed as normal.
101 | registerValidSW(swUrl);
102 | }
103 | })
104 | .catch(() => {
105 | console.log(
106 | 'No internet connection found. App is running in offline mode.'
107 | );
108 | });
109 | }
110 |
111 | export function unregister() {
112 | if ('serviceWorker' in navigator) {
113 | navigator.serviceWorker.ready.then(registration => {
114 | registration.unregister();
115 | });
116 | }
117 | }
118 |
--------------------------------------------------------------------------------