├── .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 |
129 |
130 | Ethereum (ETH) 131 |
132 |
133 | {this.renderHeaderBottom()} 134 |
135 |
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 | --------------------------------------------------------------------------------