├── .coveralls.yml ├── .editorconfig ├── .env ├── .esdoc.js ├── .gitignore ├── .storybook ├── addons.js ├── config.js └── webpack.config.js ├── .travis.yml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── package.json ├── public ├── favicon.ico ├── index.html └── manifest.json ├── src ├── actions │ └── wallet.js ├── bootstrap │ ├── app.js │ ├── app.scss │ ├── configure-store.js │ ├── dapp-api.js │ ├── initializer.js │ ├── register-service-worker.js │ └── setup-integration-test.js ├── components │ ├── __snapshots__ │ │ └── storyshots.test.js.snap │ ├── button │ │ ├── button.scss │ │ └── index.js │ ├── identicon │ │ ├── identicon.scss │ │ └── index.js │ ├── page-not-found │ │ ├── index.js │ │ └── page-not-found.scss │ ├── requires-meta-mask │ │ ├── index.js │ │ └── require-meta-mask.scss │ ├── storyshots.test.js │ └── text-input │ │ ├── index.js │ │ └── text-input.scss ├── constants │ ├── error.js │ └── testing.js ├── containers │ └── balance │ │ ├── balance.scss │ │ ├── balance.test.js │ │ └── index.js ├── index.js ├── reducers │ ├── index.js │ └── wallet.js ├── sagas │ ├── index.js │ ├── index.test.js │ └── wallet.js ├── setupTests.js ├── styles │ ├── _colors.scss │ └── _form.scss └── utils │ ├── action.js │ ├── form-generator.js │ ├── form-generator.test.js │ ├── functional.js │ ├── functional.test.js │ ├── saga.js │ ├── string.js │ ├── string.test.js │ └── validation.js ├── stories ├── button.js ├── identicon.js ├── index.js ├── page-not-found.js ├── requires-meta-mask.js └── text-input.js └── yarn.lock /.coveralls.yml: -------------------------------------------------------------------------------- 1 | repo_token: $COVERALLS_REPO_TOKEN 2 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | 14 | [*.js] 15 | max_line_length = 80 16 | indent_brace_style = 1TBS 17 | spaces_around_operators = true 18 | quote_type = single 19 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | # Development 2 | REACT_APP_DEV_ETHEREUM_PROVIDER=http://localhost:8545 3 | 4 | # Production 5 | REACT_APP_PROD_ETHEREUM_PROVIDER= 6 | -------------------------------------------------------------------------------- /.esdoc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | source: './src', 3 | destination: './docs', 4 | plugins: [ 5 | { name: 'esdoc-standard-plugin' }, 6 | { name: 'esdoc-ecmascript-proposal-plugin', option: { all: true } } 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # Dependencies 4 | /node_modules/ 5 | 6 | # Styles 7 | /src/**/*.css 8 | 9 | # Testing 10 | /coverage/ 11 | 12 | # Documentation 13 | /docs/ 14 | 15 | # Production 16 | /build/ 17 | 18 | # Logs 19 | /yarn-debug.log* 20 | /yarn-error.log* 21 | 22 | # Editors 23 | /.vscode/ 24 | /.idea/* 25 | 26 | # Misc 27 | /.DS_Store 28 | -------------------------------------------------------------------------------- /.storybook/addons.js: -------------------------------------------------------------------------------- 1 | import '@storybook/addon-actions/register' 2 | import '@dump247/storybook-state/register' 3 | -------------------------------------------------------------------------------- /.storybook/config.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { configure, addDecorator } from '@storybook/react' 3 | import { host } from 'storybook-host' 4 | import { combineReducers, applyMiddleware, createStore } from 'redux' 5 | import { Provider } from 'react-redux' 6 | import { MemoryRouter } from 'react-router-dom' 7 | 8 | import '../src/bootstrap/app.css' 9 | 10 | // Storybook Host 11 | addDecorator( 12 | host({ 13 | title: 'Dapp Front Boilerplate UI-Kit', 14 | align: 'center middle' 15 | }) 16 | ) 17 | 18 | // Integration Wrapper 19 | const store = createStore( 20 | combineReducers({}), 21 | applyMiddleware(store => next => action => { 22 | console.log(action) 23 | return next(action) 24 | }) 25 | ) 26 | addDecorator(story => ( 27 | 28 |
29 | {console.log(store.getState())} 30 | {story()} 31 |
32 |
33 | )) 34 | 35 | // Configure 36 | configure(() => require('../stories/index.js'), module) 37 | -------------------------------------------------------------------------------- /.storybook/webpack.config.js: -------------------------------------------------------------------------------- 1 | const { join } = require('path') 2 | 3 | module.exports = { 4 | module: { 5 | rules: [ 6 | { 7 | test: /\.css$/, 8 | use: ['style-loader', 'css-loader'] 9 | }, 10 | { 11 | test: /\.(woff|woff2|png)$/, 12 | use: ['url-loader'] 13 | } 14 | ] 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - v9.4.0 4 | cache: 5 | directories: 6 | - node_modules 7 | yarn: true 8 | install: yarn install --pure-lockfile 9 | script: 10 | - yarn run build:scss 11 | - yarn run lint 12 | - yarn test 13 | - if [ -n "$COVERALLS_REPO_TOKEN" ]; then yarn run test:coveralls; fi 14 | - yarn run build 15 | notifications: 16 | slack: 'kleros:Ub8n81EgKJ3iRrMDyWyQIVJp' 17 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. 4 | 5 | 6 | 7 | # [0.2.0](https://github.com/kleros/dapper/compare/v0.1.0...v0.2.0) (2018-03-14) 8 | 9 | ### Bug Fixes 10 | 11 | * **create-reducer:** check that reducer owns resource before reacting ([ac8c2ac](https://github.com/kleros/dapper/commit/ac8c2ac)) 12 | * **validation:** fix number validation ([4b440e1](https://github.com/kleros/dapper/commit/4b440e1)) 13 | 14 | ### Features 15 | 16 | * **app:** handle 404 routes ([a7f3d78](https://github.com/kleros/dapper/commit/a7f3d78)), closes [#14](https://github.com/kleros/dapper/issues/14) 17 | * **components:** pad Requires MetaMask ([d381578](https://github.com/kleros/dapper/commit/d381578)) 18 | * **create-form-generator:** implement wizard form ([e81e81f](https://github.com/kleros/dapper/commit/e81e81f)) 19 | * **dapp-api:** remove kleros-api ([801f4e2](https://github.com/kleros/dapper/commit/801f4e2)) 20 | * **skip:** get ethereum balance ([682a905](https://github.com/kleros/dapper/commit/682a905)) 21 | * add new redux utils and dapp-api flow ([ba86ecb](https://github.com/kleros/dapper/commit/ba86ecb)) 22 | * add travis badge ([84ba037](https://github.com/kleros/dapper/commit/84ba037)) 23 | * update with new storybook config and file names ([aec7b90](https://github.com/kleros/dapper/commit/aec7b90)) 24 | 25 | 26 | 27 | # 0.1.0 (2018-01-21) 28 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | See [kleros.md](https://kleros.gitbooks.io/kleros-md). 2 | 3 | Additionally, This project also uses [lessdux](https://github.com/kleros/lessdux) and [create-redux-form](https://github.com/kleros/create-redux-form). 4 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Smart Contract Solutions, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included 14 | in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 17 | OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | dapper 3 |

4 | 5 |

6 | JavaScript Style Guide 7 | Build Status 8 | Dependencies 9 | Dev Dependencies 10 | Tested with Jest 11 | Coverage Status 12 | Conventional Commits 13 | Commitizen Friendly 14 | Styled with Prettier 15 |

16 | 17 | A boilerplate for Ethereum dapps. 18 | 19 | ## Get Started 20 | 21 | 1. Clone this repo. 22 | 2. Install and set up the [MetaMask](https://chrome.google.com/webstore/detail/metamask/nkbihfbeogaeaoehlefnkodbefgpgknn?hl=en) chrome extension. 23 | 3. Configure MetaMask on the Kovan Test Network. 24 | 4. Run `yarn` to install dependencies and then `yarn start` to start the dev server. 25 | 26 | ## Other Scripts 27 | 28 | * `yarn run prettify` - Apply prettier to the entire project. 29 | * `yarn run lint:scss` - Lint the entire project's .scss files. 30 | * `yarn run lint:js` - Lint the entire project's .js files. 31 | * `yarn run lint:scss --fix` - Fix fixable linting errors in .scss files. 32 | * `yarn run lint:js --fix` - Fix fixable linting errors in .js files. 33 | * `yarn run lint` - Lint the entire project's .scss and .js files. 34 | * `yarn test` - Run the jest test suites + storyshots. 35 | * `yarn run storybook` - Start the storybook. 36 | * `yarn run cz` - Run commitizen. 37 | * `yarn run build` - Create a production build. 38 | * `yarn run build:analyze` - Analyze the production build using source-map-explorer. 39 | 40 | ## Testing 41 | 42 | Storybook Storyshots for `components` and jest integration tests for `containers`. 43 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dapper", 3 | "version": "0.2.0", 4 | "description": "A boilerplate for Ethereum dapps.", 5 | "keywords": [ 6 | "blockchain", 7 | "ethereum", 8 | "dapp", 9 | "boilerplate", 10 | "frontend" 11 | ], 12 | "repository": "https://github.com/kleros/dapper", 13 | "author": "Kleros", 14 | "license": "MIT", 15 | "private": true, 16 | "scripts": { 17 | "prettify": "kleros-scripts prettify", 18 | "lint:scss": "kleros-scripts lint:scss", 19 | "lint:js": "kleros-scripts lint:js", 20 | "lint": "yarn run lint:scss && yarn run lint:js", 21 | "test": "react-scripts test --env=jsdom --coverage", 22 | "test:coveralls": "coveralls < ./coverage/lcov.info", 23 | "precommit": "kleros-scripts precommit", 24 | "commitmsg": "kleros-scripts commitmsg", 25 | "cz": "kleros-scripts cz", 26 | "start:scss": "yarn run build:scss && yarn run build:scss --watch", 27 | "start:js": "react-scripts start", 28 | "start:storybook": "start-storybook -p 9001 -c .storybook", 29 | "storybook": "run-p start:scss start:storybook", 30 | "build:scss": "node-sass-chokidar ./src -o ./src", 31 | "build:js": "react-scripts build", 32 | "start": "run-p start:scss start:js", 33 | "build": "yarn run build:scss && yarn run build:js", 34 | "build:analyze": "source-map-explorer build/static/js/main.*" 35 | }, 36 | "jest": { 37 | "collectCoverageFrom": [ 38 | "src/**/*.js", 39 | "!src/*.js", 40 | "!src/bootstrap/**/*.js", 41 | "!src/components/identicon/index.js", 42 | "!**/node_modules/**" 43 | ] 44 | }, 45 | "commitlint": { 46 | "extends": [ 47 | "@commitlint/config-conventional" 48 | ] 49 | }, 50 | "devDependencies": { 51 | "@dump247/storybook-state": "^1.2.1", 52 | "@storybook/addon-actions": "^3.3.11", 53 | "@storybook/addon-storyshots": "^3.3.10", 54 | "@storybook/addons": "^3.3.11", 55 | "@storybook/react": "^3.3.10", 56 | "@storybook/storybook-deployer": "^2.2.0", 57 | "coveralls": "^3.0.0", 58 | "enzyme": "^3.3.0", 59 | "enzyme-adapter-react-16": "^1.1.1", 60 | "esdoc": "^1.0.4", 61 | "esdoc-ecmascript-proposal-plugin": "^1.0.0", 62 | "esdoc-standard-plugin": "^1.0.0", 63 | "eslint-plugin-react": "^7.6.1", 64 | "ganache-cli": "^6.1.0", 65 | "husky": "^0.14.3", 66 | "jest-enzyme": "^6.0.0", 67 | "kleros-scripts": "^0.6.0", 68 | "node-sass-chokidar": "^1.2.2", 69 | "npm-run-all": "^4.1.2", 70 | "prettier": "^1.10.2", 71 | "prop-types": "^15.6.0", 72 | "react-test-renderer": "^16.2.0", 73 | "redux-immutable-state-invariant": "^2.1.0", 74 | "redux-unhandled-action": "^1.3.0", 75 | "sass-loader": "^6.0.6", 76 | "standard-version": "^4.3.0", 77 | "storybook-host": "^4.1.5", 78 | "timezone-mock": "^0.0.7" 79 | }, 80 | "dependencies": { 81 | "create-redux-form": "^0.1.2", 82 | "history": "^4.7.2", 83 | "lessdux": "^0.7.3", 84 | "normalize.css": "^8.0.0", 85 | "react": "^16.2.0", 86 | "react-addons-css-transition-group": "^15.6.2", 87 | "react-blockies": "^1.2.2", 88 | "react-dom": "^16.2.0", 89 | "react-helmet": "^5.2.0", 90 | "react-redux": "^5.0.6", 91 | "react-router-dom": "^4.2.2", 92 | "react-router-redux": "^5.0.0-alpha.9", 93 | "react-scripts": "1.1.1", 94 | "redux": "^3.7.2", 95 | "redux-form": "^7.2.3", 96 | "redux-saga": "^0.16.0", 97 | "web3": "^1.0.0-beta.34" 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kleros/dapper/c3eda47d256c4133d6ffb6f985a31f34e4c106a4/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 14 | 23 | Dapp Front Boilerplate 24 | 25 | 26 | 27 | 30 |
31 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Dapp Front Boilerplate", 3 | "name": "Dapp Front Boilerplate", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": "./index.html", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /src/actions/wallet.js: -------------------------------------------------------------------------------- 1 | import { createActions } from 'lessdux' 2 | 3 | /* Actions */ 4 | 5 | // Accounts 6 | export const accounts = createActions('ACCOUNTS') 7 | 8 | // Balance 9 | export const balance = createActions('BALANCE') 10 | 11 | /* Action Creators */ 12 | 13 | // Accounts 14 | export const fetchAccounts = () => ({ type: accounts.FETCH }) 15 | 16 | // Balance 17 | export const fetchBalance = () => ({ type: balance.FETCH }) 18 | -------------------------------------------------------------------------------- /src/bootstrap/app.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import { Helmet } from 'react-helmet' 4 | import { Provider } from 'react-redux' 5 | import { ConnectedRouter } from 'react-router-redux' 6 | import { Switch, Route } from 'react-router-dom' 7 | 8 | import Balance from '../containers/balance' 9 | import PageNotFound from '../components/page-not-found' 10 | 11 | import Initializer from './initializer' 12 | 13 | import './app.css' 14 | 15 | const App = ({ store, history, testElement }) => ( 16 | 17 | 18 | 19 |
20 | 21 | Dapper 22 | 23 |
24 | 25 | 26 | 27 | 28 |
29 | {testElement} 30 |
31 |
32 |
33 |
34 | ) 35 | 36 | App.propTypes = { 37 | // State 38 | store: PropTypes.shape({}).isRequired, 39 | history: PropTypes.shape({}).isRequired, 40 | 41 | // Testing 42 | testElement: PropTypes.element 43 | } 44 | 45 | App.defaultProps = { 46 | // Testing 47 | testElement: null 48 | } 49 | 50 | export default App 51 | -------------------------------------------------------------------------------- /src/bootstrap/app.scss: -------------------------------------------------------------------------------- 1 | @import '~normalize.css'; 2 | @import '~create-redux-form/animations/carousel.css'; 3 | @import '../styles/_colors.scss'; 4 | @import '../styles/_form.scss'; 5 | 6 | // Global Overrides 7 | * { 8 | box-sizing: border-box !important; 9 | font-family: 'Roboto', sans-serif !important; 10 | scroll-behavior: smooth !important; 11 | } 12 | 13 | // Document Root 14 | html { 15 | color: $text; 16 | font-size: 16px; 17 | } 18 | 19 | // App Containers 20 | html, 21 | body, 22 | #root, 23 | #router-root, 24 | #scroll-root { 25 | border: 0; 26 | height: 100%; 27 | margin: 0; 28 | padding: 0; 29 | width: 100%; 30 | } 31 | 32 | // Scroll Root 33 | #scroll-root { 34 | background: $background; 35 | display: flex; 36 | flex-direction: column; 37 | overflow: scroll; 38 | position: relative; 39 | } 40 | 41 | /* Element Overrides */ 42 | 43 | // Horizontal Rule 44 | hr { 45 | border: 1px solid $light3; 46 | } 47 | -------------------------------------------------------------------------------- /src/bootstrap/configure-store.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable global-require */ 2 | import { applyMiddleware, compose, createStore } from 'redux' 3 | import createSagaMiddleware from 'redux-saga' 4 | import { routerMiddleware } from 'react-router-redux' 5 | import createHistory from 'history/createBrowserHistory' 6 | 7 | import rootReducer from '../reducers' 8 | import rootSaga from '../sagas' 9 | 10 | let store 11 | let sagaMiddleware 12 | let rootSagaTask 13 | 14 | /** 15 | * Sets up the redux store. 16 | * @param {object} [initialState={}] - The initial state for the redux store, defaults to an empty object. 17 | * @param {{ dispatchSpy: function }} [integrationTestParams=[]] - Parameters necessary to setup integration tests. 18 | * @returns {{ store: object, history: object }} - An object with the store and the history objects. 19 | */ 20 | export default function configureStore( 21 | initialState = {}, 22 | { dispatchSpy } = {} 23 | ) { 24 | sagaMiddleware = createSagaMiddleware() 25 | const history = createHistory() 26 | const enhancers = [] 27 | const middleware = [] 28 | const composeEnhancers = 29 | process.env.NODE_ENV === 'development' && 30 | window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ 31 | ? window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ 32 | : compose 33 | 34 | // Development Tools 35 | if (process.env.NODE_ENV === 'development') { 36 | const reduxImmutableState = require('redux-immutable-state-invariant') 37 | .default 38 | const reduxUnhandledAction = require('redux-unhandled-action').default 39 | middleware.push( 40 | reduxImmutableState(), 41 | reduxUnhandledAction(action => 42 | console.error( 43 | `${action} didn't lead to creation of a new state object`, 44 | action 45 | ) 46 | ) 47 | ) 48 | } 49 | 50 | // Testing Tools 51 | if (dispatchSpy) { 52 | middleware.push(_store => next => action => { 53 | dispatchSpy(action) 54 | return next(action) 55 | }) 56 | } 57 | 58 | middleware.push(sagaMiddleware, routerMiddleware(history)) 59 | enhancers.unshift(applyMiddleware(...middleware)) 60 | store = createStore(rootReducer, initialState, composeEnhancers(...enhancers)) 61 | rootSagaTask = sagaMiddleware.run(rootSaga) 62 | return { store, history } 63 | } 64 | 65 | if (module.hot) { 66 | module.hot.accept('../reducers', () => { 67 | store.replaceReducer(rootReducer) 68 | }) 69 | module.hot.accept('../sagas', () => { 70 | rootSagaTask.cancel() 71 | rootSagaTask.done.then(() => (rootSagaTask = sagaMiddleware.run(rootSaga))) 72 | }) 73 | } 74 | -------------------------------------------------------------------------------- /src/bootstrap/dapp-api.js: -------------------------------------------------------------------------------- 1 | import Web3 from 'web3' 2 | 3 | const env = process.env.NODE_ENV === 'production' ? 'PROD' : 'DEV' 4 | const ETHEREUM_PROVIDER = process.env[`REACT_APP_${env}_ETHEREUM_PROVIDER`] 5 | 6 | let web3 7 | if (process.env.NODE_ENV === 'test') 8 | web3 = new Web3(require('ganache-cli').provider()) 9 | else if (window.web3 && window.web3.currentProvider) 10 | web3 = new Web3(window.web3.currentProvider) 11 | else web3 = new Web3(new Web3.providers.HttpProvider(ETHEREUM_PROVIDER)) 12 | 13 | const network = 14 | web3.eth && 15 | web3.eth.net 16 | .getId() 17 | .then(networkID => { 18 | switch (networkID) { 19 | case 1: 20 | return 'main' 21 | case 3: 22 | return 'ropsten' 23 | case 4: 24 | return 'rinkeby' 25 | case 42: 26 | return 'kovan' 27 | default: 28 | return null 29 | } 30 | }) 31 | .catch(() => null) 32 | 33 | const ETHAddressRegExpCaptureGroup = '(0x[a-fA-F0-9]{40})' 34 | const ETHAddressRegExp = /0x[a-fA-F0-9]{40}/ 35 | const strictETHAddressRegExp = /^0x[a-fA-F0-9]{40}$/ 36 | 37 | export { 38 | web3, 39 | network, 40 | ETHAddressRegExpCaptureGroup, 41 | ETHAddressRegExp, 42 | strictETHAddressRegExp 43 | } 44 | -------------------------------------------------------------------------------- /src/bootstrap/initializer.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react' 2 | import PropTypes from 'prop-types' 3 | import { connect } from 'react-redux' 4 | import { RenderIf } from 'lessdux' 5 | 6 | import * as walletSelectors from '../reducers/wallet' 7 | import * as walletActions from '../actions/wallet' 8 | import RequiresMetaMask from '../components/requires-meta-mask' 9 | 10 | import { web3 } from './dapp-api' 11 | 12 | class Initializer extends PureComponent { 13 | static propTypes = { 14 | // Redux State 15 | accounts: walletSelectors.accountsShape.isRequired, 16 | 17 | // Action Dispatchers 18 | fetchAccounts: PropTypes.func.isRequired, 19 | 20 | // State 21 | children: PropTypes.oneOfType([ 22 | PropTypes.element, 23 | PropTypes.arrayOf(PropTypes.element.isRequired) 24 | ]).isRequired 25 | } 26 | 27 | componentDidMount() { 28 | const { fetchAccounts } = this.props 29 | fetchAccounts() 30 | } 31 | 32 | render() { 33 | const { accounts, children } = this.props 34 | 35 | return ( 36 | } 41 | extraValues={[accounts.data && accounts.data[0]]} 42 | extraFailedValues={[!web3.eth]} 43 | /> 44 | ) 45 | } 46 | } 47 | 48 | export default connect( 49 | state => ({ 50 | accounts: state.wallet.accounts 51 | }), 52 | { fetchAccounts: walletActions.fetchAccounts } 53 | )(Initializer) 54 | -------------------------------------------------------------------------------- /src/bootstrap/register-service-worker.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable require-jsdoc */ 2 | // In production, we register a service worker to serve assets from local cache. 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 the "N+1" visit to a page, since previously 7 | // cached resources are updated in the background. 8 | 9 | // To learn more about the benefits of this model, read https://goo.gl/KwvDNy. 10 | // This link also includes instructions on opting out of this behavior. 11 | 12 | const isLocalhost = Boolean( 13 | window.location.hostname === 'localhost' || 14 | // [::1] is the IPv6 localhost address. 15 | window.location.hostname === '[::1]' || 16 | // 127.0.0.1/8 is considered localhost for IPv4. 17 | window.location.hostname.match( 18 | /^127(?:\.(?:25[0-5]|2[0-4]\d|[01]?\d\d?)){3}$/ 19 | ) 20 | ) 21 | 22 | function registerValidSW(swUrl) { 23 | navigator.serviceWorker 24 | .register(swUrl) 25 | .then(registration => { 26 | registration.onupdatefound = () => { 27 | const installingWorker = registration.installing 28 | installingWorker.onstatechange = () => { 29 | if (installingWorker.state === 'installed') { 30 | if (navigator.serviceWorker.controller) { 31 | // At this point, the old content will have been purged and 32 | // the fresh content will have been added to the cache. 33 | // It's the perfect time to display a "New content is 34 | // available; please refresh." message in your web app. 35 | console.log('New content is available; please refresh.') 36 | } else { 37 | // At this point, everything has been precached. 38 | // It's the perfect time to display a 39 | // "Content is cached for offline use." message. 40 | console.log('Content is cached for offline use.') 41 | } 42 | } 43 | } 44 | } 45 | }) 46 | .catch(err => { 47 | console.error('Error during service worker registration:', err) 48 | }) 49 | } 50 | 51 | function checkValidServiceWorker(swUrl) { 52 | // Check if the service worker can be found. If it can't reload the page. 53 | fetch(swUrl) 54 | .then(response => { 55 | // Ensure service worker exists, and that we really are getting a JS file. 56 | if ( 57 | response.status === 404 || 58 | response.headers.get('content-type').indexOf('javascript') === -1 59 | ) { 60 | // No service worker found. Probably a different app. Reload the page. 61 | navigator.serviceWorker.ready.then(registration => { 62 | registration.unregister().then(() => { 63 | window.location.reload() 64 | }) 65 | }) 66 | } else { 67 | // Service worker found. Proceed as normal. 68 | registerValidSW(swUrl) 69 | } 70 | }) 71 | .catch(() => { 72 | console.log( 73 | 'No internet connection found. App is running in offline mode.' 74 | ) 75 | }) 76 | } 77 | 78 | export default function registerServiceWorker() { 79 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 80 | // The URL constructor is available in all browsers that support SW. 81 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location) 82 | if (publicUrl.origin !== window.location.origin) { 83 | // Our service worker won't work if PUBLIC_URL is on a different origin 84 | // from what our page is served on. This might happen if a CDN is used to 85 | // serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374 86 | return 87 | } 88 | 89 | window.addEventListener('load', () => { 90 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js` 91 | 92 | if (isLocalhost) { 93 | // This is running on localhost. Lets check if a service worker still exists or not. 94 | checkValidServiceWorker(swUrl) 95 | 96 | // Add some additional logging to localhost, pointing developers to the 97 | // service worker/PWA documentation. 98 | navigator.serviceWorker.ready.then(() => { 99 | console.log( 100 | 'This web app is being served cache-first by a service ' + 101 | 'worker. To learn more, visit https://goo.gl/SC7cgQ' 102 | ) 103 | }) 104 | } else { 105 | // Is not local host. Just register service worker 106 | registerValidSW(swUrl) 107 | } 108 | }) 109 | } 110 | } 111 | 112 | export function unregisterServiceWorker() { 113 | if ('serviceWorker' in navigator) { 114 | navigator.serviceWorker.ready.then(registration => { 115 | registration.unregister() 116 | }) 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/bootstrap/setup-integration-test.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { mount } from 'enzyme' 3 | 4 | import configureStore from './configure-store' 5 | import App from './app' 6 | 7 | /** 8 | * Wait for all promises to resolve in a test environment and update the app. 9 | * @param {object} app - The app wrapper to update. 10 | * @returns {Promise} - A promise that resolves when the timeout handler is called. 11 | */ 12 | export function flushPromises(app) { 13 | return new Promise(resolve => 14 | setTimeout(() => { 15 | app.update() 16 | resolve() 17 | }, 1000) 18 | ) 19 | } 20 | 21 | /** 22 | * Sets up an integration test environment. 23 | * @param {object} [initialState={}] - The initial state. 24 | * @returns {{ store: object, history: object, dispatchSpy: function, mountApp: function }} - Utilities for testing. 25 | */ 26 | export default function setupIntegrationTest(initialState = {}) { 27 | const dispatchSpy = jest.fn(() => ({})) 28 | const { store, history } = configureStore(initialState, { 29 | dispatchSpy 30 | }) 31 | const mountApp = testElement => 32 | mount() 33 | 34 | return { store, history, dispatchSpy, mountApp } 35 | } 36 | -------------------------------------------------------------------------------- /src/components/__snapshots__/storyshots.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Storyshots Button disabled 1`] = ` 4 |
5 |
20 |
33 |

45 | Dapp Front Boilerplate UI-Kit 46 |

47 |
48 |
59 |
75 |
87 |
91 |
94 | CLICK ME 95 |
96 |
97 |
111 |
124 |
137 |
138 |
152 |
165 |
178 |
179 |
193 |
206 |
219 |
220 |
234 |
247 |
260 |
261 |
262 |
263 |
264 |
265 |
266 | `; 267 | 268 | exports[`Storyshots Button primary 1`] = ` 269 |
270 |
285 |
298 |

310 | Dapp Front Boilerplate UI-Kit 311 |

312 |
313 |
324 |
340 |
352 |
356 |
359 | CLICK ME 360 |
361 |
362 |
376 |
389 |
402 |
403 |
417 |
430 |
443 |
444 |
458 |
471 |
484 |
485 |
499 |
512 |
525 |
526 |
527 |
528 |
529 |
530 |
531 | `; 532 | 533 | exports[`Storyshots Button with lots of text 1`] = ` 534 |
535 |
550 |
563 |

575 | Dapp Front Boilerplate UI-Kit 576 |

577 |
578 |
589 |
605 |
617 |
621 |
624 | CLICK ME, CLICK ME, CLICK ME 625 |
626 |
627 |
641 |
654 |
667 |
668 |
682 |
695 |
708 |
709 |
723 |
736 |
749 |
750 |
764 |
777 |
790 |
791 |
792 |
793 |
794 |
795 |
796 | `; 797 | 798 | exports[`Storyshots Identicon with placeholder seed 1`] = ` 799 |
800 |
815 |
828 |

840 | Dapp Front Boilerplate UI-Kit 841 |

842 |
843 |
854 |
870 |
882 |
883 | [Identicon] 884 |
885 |
899 |
912 |
925 |
926 |
940 |
953 |
966 |
967 |
981 |
994 |
1007 |
1008 |
1022 |
1035 |
1048 |
1049 |
1050 |
1051 |
1052 |
1053 |
1054 | `; 1055 | 1056 | exports[`Storyshots Page Not Found default 1`] = ` 1057 |
1058 |
1073 |
1086 |

1098 | Dapp Front Boilerplate UI-Kit 1099 |

1100 |
1101 |
1112 |
1128 |
1140 |
1143 |
1146 | 404. This is not the page you are looking for. 1147 |
1148 |
1149 |
1163 |
1176 |
1189 |
1190 |
1204 |
1217 |
1230 |
1231 |
1245 |
1258 |
1271 |
1272 |
1286 |
1299 |
1312 |
1313 |
1314 |
1315 |
1316 |
1317 |
1318 | `; 1319 | 1320 | exports[`Storyshots Requires MetaMask needs unlock 1`] = ` 1321 |
1322 |
1337 |
1350 |

1362 | Dapp Front Boilerplate UI-Kit 1363 |

1364 |
1365 |
1376 |
1392 |
1404 |
1407 |
1410 | 1411 | This is a decentralized application. In order to use this site please 1412 | 1413 | 1414 | unlock 1415 | 1419 | MetaMask 1420 | 1421 |
1422 |
1423 |
1437 |
1450 |
1463 |
1464 |
1478 |
1491 |
1504 |
1505 |
1519 |
1532 |
1545 |
1546 |
1560 |
1573 |
1586 |
1587 |
1588 |
1589 |
1590 |
1591 |
1592 | `; 1593 | 1594 | exports[`Storyshots Requires MetaMask not found 1`] = ` 1595 |
1596 |
1611 |
1624 |

1636 | Dapp Front Boilerplate UI-Kit 1637 |

1638 |
1639 |
1650 |
1666 |
1678 |
1681 |
1684 | 1685 | This is a decentralized application. In order to use this site please 1686 | 1687 | 1688 | download 1689 | 1693 | MetaMask 1694 | 1695 |
1696 |
1697 |
1711 |
1724 |
1737 |
1738 |
1752 |
1765 |
1778 |
1779 |
1793 |
1806 |
1819 |
1820 |
1834 |
1847 |
1860 |
1861 |
1862 |
1863 |
1864 |
1865 |
1866 | `; 1867 | 1868 | exports[`Storyshots Text Input default 1`] = ` 1869 |
1870 |
1885 |
1898 |

1910 | Dapp Front Boilerplate UI-Kit 1911 |

1912 |
1913 |
1924 |
1940 |
1952 |
1955 | 1961 |
1964 | EMAIL 1965 |
1966 |
1967 |
1981 |
1994 |
2007 |
2008 |
2022 |
2035 |
2048 |
2049 |
2063 |
2076 |
2089 |
2090 |
2104 |
2117 |
2130 |
2131 |
2132 |
2133 |
2134 |
2135 |
2136 | `; 2137 | 2138 | exports[`Storyshots Text Input error 1`] = ` 2139 |
2140 |
2155 |
2168 |

2180 | Dapp Front Boilerplate UI-Kit 2181 |

2182 |
2183 |
2194 |
2210 |
2222 |
2225 | 2231 |
2234 | EMAIL 2235 |
2236 |
2239 | Please enter a valid email. 2240 |
2241 |
2242 |
2256 |
2269 |
2282 |
2283 |
2297 |
2310 |
2323 |
2324 |
2338 |
2351 |
2364 |
2365 |
2379 |
2392 |
2405 |
2406 |
2407 |
2408 |
2409 |
2410 |
2411 | `; 2412 | 2413 | exports[`Storyshots Text Input touched 1`] = ` 2414 |
2415 |
2430 |
2443 |

2455 | Dapp Front Boilerplate UI-Kit 2456 |

2457 |
2458 |
2469 |
2485 |
2497 |
2500 | 2506 |
2509 | EMAIL 2510 |
2511 |
2512 |
2526 |
2539 |
2552 |
2553 |
2567 |
2580 |
2593 |
2594 |
2608 |
2621 |
2634 |
2635 |
2649 |
2662 |
2675 |
2676 |
2677 |
2678 |
2679 |
2680 |
2681 | `; 2682 | 2683 | exports[`Storyshots Text Input valid 1`] = ` 2684 |
2685 |
2700 |
2713 |

2725 | Dapp Front Boilerplate UI-Kit 2726 |

2727 |
2728 |
2739 |
2755 |
2767 |
2770 | 2776 |
2779 | EMAIL 2780 |
2781 |
2782 |
2796 |
2809 |
2822 |
2823 |
2837 |
2850 |
2863 |
2864 |
2878 |
2891 |
2904 |
2905 |
2919 |
2932 |
2945 |
2946 |
2947 |
2948 |
2949 |
2950 |
2951 | `; 2952 | -------------------------------------------------------------------------------- /src/components/button/button.scss: -------------------------------------------------------------------------------- 1 | @import '../../styles/_colors.scss'; 2 | 3 | /* @define Button */ 4 | .Button { 5 | // stylelint-disable-next-line declaration-colon-newline-after 6 | background: linear-gradient( 7 | 0deg, 8 | $buttonGradientStart 0%, 9 | $buttonGradientEnd 100% 10 | ); 11 | border-radius: 4px; 12 | box-shadow: 0 1px 0 0 $buttonBoxShadow; 13 | cursor: pointer; 14 | height: 50px; 15 | min-width: 200px; 16 | padding: 0 20px; 17 | text-align: center; 18 | 19 | &.is-disabled { 20 | background: $disabled; 21 | box-shadow: none; 22 | cursor: initial; 23 | } 24 | 25 | &-label { 26 | color: $buttonText; 27 | line-height: 50px; 28 | margin: 0; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/components/button/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | import './button.css' 5 | 6 | const Button = ({ 7 | children, 8 | onClick, 9 | disabled, 10 | className, 11 | labelClassName, 12 | ...rest 13 | }) => ( 14 |
19 |
{children}
20 |
21 | ) 22 | 23 | Button.propTypes = { 24 | // State 25 | children: PropTypes.oneOfType([PropTypes.string, PropTypes.element]) 26 | .isRequired, 27 | 28 | // Handlers 29 | onClick: PropTypes.func.isRequired, 30 | 31 | // Modifiers 32 | disabled: PropTypes.bool, 33 | className: PropTypes.string, 34 | labelClassName: PropTypes.string 35 | } 36 | 37 | Button.defaultProps = { 38 | // Modifiers 39 | disabled: false, 40 | className: '', 41 | labelClassName: '' 42 | } 43 | 44 | export default Button 45 | -------------------------------------------------------------------------------- /src/components/identicon/identicon.scss: -------------------------------------------------------------------------------- 1 | /* @define Identicon */ 2 | .Identicon { 3 | border-radius: 4px; 4 | overflow: hidden; 5 | } 6 | -------------------------------------------------------------------------------- /src/components/identicon/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import Blockies from 'react-blockies' 4 | 5 | import './identicon.css' 6 | 7 | const Identicon = ({ seed, size, scale, className, ...rest }) => { 8 | const length = `${size * scale}px` 9 | return ( 10 |
14 | 19 | 20 | 21 |
22 | ) 23 | } 24 | 25 | Identicon.propTypes = { 26 | // React Blockies 27 | seed: PropTypes.number.isRequired, 28 | size: PropTypes.number, 29 | scale: PropTypes.number, 30 | ...Blockies.propTypes, 31 | 32 | // Modifiers 33 | className: PropTypes.string 34 | } 35 | 36 | Identicon.defaultProps = { 37 | // React Blockies 38 | size: 15, 39 | scale: 4, 40 | 41 | // Modifiers 42 | className: '' 43 | } 44 | 45 | export default Identicon 46 | -------------------------------------------------------------------------------- /src/components/page-not-found/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import './page-not-found.css' 4 | 5 | export default () => ( 6 |
7 |
8 | 404. This is not the page you are looking for. 9 |
10 |
11 | ) 12 | -------------------------------------------------------------------------------- /src/components/page-not-found/page-not-found.scss: -------------------------------------------------------------------------------- 1 | /* @define PageNotFound */ 2 | .PageNotFound { 3 | background: #273142; 4 | height: 100%; 5 | 6 | &-message { 7 | color: white; 8 | font-size: 40px; 9 | padding: 70px 50px; 10 | text-align: center; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/components/requires-meta-mask/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | import './require-meta-mask.css' 5 | 6 | const RequiresMetaMask = ({ needsUnlock }) => ( 7 |
8 |
9 | 10 | This is a decentralized application. In order to use this site please{' '} 11 | 12 | {needsUnlock ? 'unlock ' : 'download '} 13 | 17 | MetaMask 18 | 19 |
20 |
21 | ) 22 | 23 | RequiresMetaMask.propTypes = { 24 | // State 25 | needsUnlock: PropTypes.bool 26 | } 27 | 28 | RequiresMetaMask.defaultProps = { 29 | // State 30 | needsUnlock: false 31 | } 32 | 33 | export default RequiresMetaMask 34 | -------------------------------------------------------------------------------- /src/components/requires-meta-mask/require-meta-mask.scss: -------------------------------------------------------------------------------- 1 | /* @define RequiresMetaMask */ 2 | .RequiresMetaMask { 3 | background: #273142; 4 | height: 100%; 5 | 6 | &-message { 7 | color: white; 8 | font-size: 40px; 9 | padding: 70px 50px; 10 | text-align: center; 11 | 12 | &-link { 13 | color: #999; 14 | text-decoration: none; 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/components/storyshots.test.js: -------------------------------------------------------------------------------- 1 | import initStoryshots from '@storybook/addon-storyshots' 2 | 3 | initStoryshots() 4 | -------------------------------------------------------------------------------- /src/components/text-input/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | import './text-input.css' 5 | 6 | const TextInput = ({ 7 | input: { value, onChange }, 8 | meta: { touched, valid, error }, 9 | placeholder, 10 | type, 11 | className 12 | }) => ( 13 |
18 | 24 | {placeholder && ( 25 |
30 | {placeholder} 31 |
32 | )} 33 | {error &&
{error}
} 34 |
35 | ) 36 | 37 | TextInput.propTypes = { 38 | // Redux Form 39 | input: PropTypes.shape({ 40 | value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired, 41 | onChange: PropTypes.func.isRequired 42 | }).isRequired, 43 | meta: PropTypes.shape({ 44 | touched: PropTypes.bool, 45 | valid: PropTypes.bool, 46 | error: PropTypes.string 47 | }), 48 | 49 | // State 50 | placeholder: PropTypes.oneOfType([PropTypes.element, PropTypes.string]) 51 | .isRequired, 52 | 53 | // Modifiers 54 | type: PropTypes.string, 55 | className: PropTypes.string 56 | } 57 | 58 | TextInput.defaultProps = { 59 | // Redux Form 60 | meta: {}, 61 | 62 | // Modifiers 63 | type: 'text', 64 | className: '' 65 | } 66 | 67 | export default TextInput 68 | -------------------------------------------------------------------------------- /src/components/text-input/text-input.scss: -------------------------------------------------------------------------------- 1 | @import '../../styles/_colors.scss'; 2 | 3 | /* @define TextInput */ 4 | .TextInput { 5 | background: #fff; 6 | border: 1px solid #dfe3e9; 7 | border-radius: 4px; 8 | font-size: 14px; 9 | height: 45px; 10 | margin: 0.5rem; 11 | position: relative; 12 | width: 200px; 13 | 14 | &.is-error { 15 | border-color: $error; 16 | } 17 | 18 | &.is-valid { 19 | border-color: $success; 20 | } 21 | 22 | &-input { 23 | background: none; 24 | border: none; 25 | height: 100%; 26 | outline: none; 27 | padding: 0 5px; 28 | width: 100%; 29 | } 30 | 31 | &-placeholder { 32 | color: $disabled; 33 | left: 5px; 34 | pointer-events: none; 35 | position: absolute; 36 | text-transform: uppercase; 37 | top: 50%; 38 | transform: translateY(-50%); 39 | transition: 0.2s font-size, 0.2s transform, 0.2s top; 40 | 41 | .TextInput-input:focus + &, 42 | &.is-touched { 43 | font-size: 8px; 44 | line-height: 8px; 45 | top: 5px; 46 | transform: none; 47 | } 48 | } 49 | 50 | &-error { 51 | bottom: 0; 52 | color: $error; 53 | font-size: 8px; 54 | left: 5px; 55 | position: absolute; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/constants/error.js: -------------------------------------------------------------------------------- 1 | export const ETH_NO_ACCOUNTS = 2 | 'web3 accounts not found. Do you have MetaMask installed and unlocked?' 3 | -------------------------------------------------------------------------------- /src/constants/testing.js: -------------------------------------------------------------------------------- 1 | export const TEST_ACCOUNT = '0x48c9cb08beb6f859a0544a01a9f2c4db8e5400f1' 2 | -------------------------------------------------------------------------------- /src/containers/balance/balance.scss: -------------------------------------------------------------------------------- 1 | /* @define Balance */ 2 | .Balance { 3 | background: #273142; 4 | height: 100%; 5 | 6 | &-message { 7 | color: white; 8 | font-size: 40px; 9 | padding-top: 10%; 10 | text-align: center; 11 | 12 | &-identicon { 13 | display: inline-block; 14 | } 15 | 16 | &-link { 17 | color: #999; 18 | text-decoration: none; 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/containers/balance/balance.test.js: -------------------------------------------------------------------------------- 1 | import setupIntegrationTest, { 2 | flushPromises 3 | } from '../../bootstrap/setup-integration-test' 4 | 5 | import Balance from '.' 6 | 7 | let integration = { 8 | store: null, 9 | history: null, 10 | dispatchSpy: null, 11 | mountApp: null 12 | } 13 | 14 | beforeEach(() => { 15 | integration = setupIntegrationTest({ router: { location: '/' } }) 16 | }) 17 | 18 | it('Renders and loads balance correctly.', async () => { 19 | const app = integration.mountApp() 20 | await flushPromises(app) 21 | expect(app.find(Balance).text()).toBe( 22 | 'Hello CryptoWorldWelcome [Identicon], You have 100 ETH.' 23 | ) 24 | }) 25 | -------------------------------------------------------------------------------- /src/containers/balance/index.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react' 2 | import PropTypes from 'prop-types' 3 | import { connect } from 'react-redux' 4 | import { RenderIf } from 'lessdux' 5 | 6 | import * as walletActions from '../../actions/wallet' 7 | import * as walletSelectors from '../../reducers/wallet' 8 | import Identicon from '../../components/identicon' 9 | 10 | import './balance.css' 11 | 12 | class Balance extends PureComponent { 13 | static propTypes = { 14 | // Redux State 15 | balance: walletSelectors.balanceShape.isRequired, 16 | 17 | // Action Dispatchers 18 | fetchBalance: PropTypes.func.isRequired 19 | } 20 | 21 | componentDidMount() { 22 | const { fetchBalance } = this.props 23 | fetchBalance() 24 | } 25 | 26 | render() { 27 | const { balance } = this.props 28 | 29 | return ( 30 |
31 |
32 | Hello CryptoWorld 33 |
34 |
35 |
36 |
37 | 43 | Welcome{' '} 44 | , You have {balance.data.toString()} ETH. 48 | 49 | ) 50 | } 51 | failedLoading={ 52 | 53 | There was an error fetching your balance. Make sure{' '} 54 | 58 | MetaMask 59 | {' '} 60 | is unlocked and refresh the page. 61 | 62 | } 63 | /> 64 |
65 |
66 | ) 67 | } 68 | } 69 | 70 | export default connect( 71 | state => ({ 72 | balance: state.wallet.balance 73 | }), 74 | { 75 | fetchBalance: walletActions.fetchBalance 76 | } 77 | )(Balance) 78 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | 4 | import configureStore from './bootstrap/configure-store' 5 | import App from './bootstrap/app' 6 | import registerServiceWorker from './bootstrap/register-service-worker' 7 | 8 | const { store, history } = configureStore() 9 | export default store 10 | 11 | // Random number is used so hot reloading works with `react-loadable` 12 | const render = Component => { 13 | ReactDOM.render( 14 | , 19 | document.getElementById('root') 20 | ) 21 | } 22 | 23 | render(App) 24 | 25 | if (module.hot) { 26 | module.hot.accept('./bootstrap/app', () => { 27 | render(App) 28 | }) 29 | } 30 | 31 | registerServiceWorker() 32 | -------------------------------------------------------------------------------- /src/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux' 2 | import { routerReducer as router } from 'react-router-redux' 3 | import { reducer as form } from 'redux-form' 4 | 5 | import wallet from './wallet' 6 | 7 | // Export root reducer 8 | export default combineReducers({ 9 | router, 10 | form, 11 | wallet 12 | }) 13 | -------------------------------------------------------------------------------- /src/reducers/wallet.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types' 2 | import createReducer, { createResource } from 'lessdux' 3 | 4 | // Shapes 5 | const { 6 | shape: accountsShape, 7 | initialState: accountsInitialState 8 | } = createResource(PropTypes.arrayOf(PropTypes.string.isRequired)) 9 | const { 10 | shape: balanceShape, 11 | initialState: balanceInitialState 12 | } = createResource(PropTypes.string) 13 | export { accountsShape, balanceShape } 14 | 15 | // Reducer 16 | export default createReducer({ 17 | accounts: accountsInitialState, 18 | balance: balanceInitialState 19 | }) 20 | 21 | // Selectors 22 | export const getAccount = state => 23 | state.wallet.accounts.data && state.wallet.accounts.data[0] 24 | -------------------------------------------------------------------------------- /src/sagas/index.js: -------------------------------------------------------------------------------- 1 | import { delay } from 'redux-saga' 2 | 3 | import { spawn, call, all } from 'redux-saga/effects' 4 | 5 | import walletSaga from './wallet' 6 | 7 | /** 8 | * Makes a saga restart after an uncaught error. 9 | * @param {object} saga - A generator function. 10 | * @returns {object} - A new generator function with the added functionality. 11 | */ 12 | export function makeRestartable(saga) { 13 | return function*() { 14 | // eslint-disable-next-line no-constant-condition 15 | while (true) { 16 | try { 17 | yield call(saga) 18 | throw new Error( 19 | `Unexpected root saga termination. The root sagas are supposed to be sagas that live during the whole app lifetime! ${saga}` 20 | ) 21 | } catch (err) { 22 | /* istanbul ignore if */ 23 | if (process.env.NODE_ENV !== 'test') 24 | console.info( 25 | 'Saga error, the saga will be restarted after 3 seconds.', 26 | err 27 | ) 28 | yield call(delay, 3000) 29 | } 30 | } 31 | } 32 | } 33 | 34 | const rootSagas = [walletSaga].map(makeRestartable) 35 | 36 | /** 37 | * The root saga. 38 | */ 39 | export default function* rootSaga() { 40 | yield all(rootSagas.map(saga => spawn(saga))) 41 | } 42 | -------------------------------------------------------------------------------- /src/sagas/index.test.js: -------------------------------------------------------------------------------- 1 | import { delay } from 'redux-saga' 2 | 3 | import { call } from 'redux-saga/effects' 4 | 5 | import { makeRestartable } from '.' 6 | 7 | it('Restarts terminated sagas.', async () => { 8 | const saga = function*() {} 9 | const gen = makeRestartable(saga)() 10 | expect(gen.next().value).toEqual(call(saga)) 11 | expect(gen.next().value).toEqual(call(delay, 3000)) 12 | expect(gen.next().value).toEqual(call(saga)) 13 | }) 14 | -------------------------------------------------------------------------------- /src/sagas/wallet.js: -------------------------------------------------------------------------------- 1 | import { takeLatest, select, call } from 'redux-saga/effects' 2 | 3 | import * as walletSelectors from '../reducers/wallet' 4 | import * as walletActions from '../actions/wallet' 5 | import { web3 } from '../bootstrap/dapp-api' 6 | import { lessduxSaga } from '../utils/saga' 7 | import * as errorConstants from '../constants/error' 8 | 9 | /** 10 | * Fetches the current wallet's accounts. 11 | * @returns {object[]} - The accounts. 12 | */ 13 | export function* fetchAccounts() { 14 | const accounts = yield call(web3.eth.getAccounts) 15 | if (!accounts[0]) throw new Error(errorConstants.ETH_NO_ACCOUNTS) 16 | 17 | return accounts 18 | } 19 | 20 | /** 21 | * Fetches the current wallet's ethereum balance. 22 | * @returns {number} - The balance. 23 | */ 24 | export function* fetchBalance() { 25 | const balance = yield call( 26 | web3.eth.getBalance, 27 | yield select(walletSelectors.getAccount) 28 | ) 29 | 30 | return web3.utils.fromWei(balance, 'ether') 31 | } 32 | 33 | /** 34 | * The root of the wallet saga. 35 | */ 36 | export default function* walletSaga() { 37 | // Accounts 38 | yield takeLatest( 39 | walletActions.accounts.FETCH, 40 | lessduxSaga, 41 | 'fetch', 42 | walletActions.accounts, 43 | fetchAccounts 44 | ) 45 | 46 | // Balance 47 | yield takeLatest( 48 | walletActions.balance.FETCH, 49 | lessduxSaga, 50 | 'fetch', 51 | walletActions.balance, 52 | fetchBalance 53 | ) 54 | } 55 | -------------------------------------------------------------------------------- /src/setupTests.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line unicorn/filename-case 2 | import React from 'react' 3 | import { configure } from 'enzyme' 4 | import Adapter from 'enzyme-adapter-react-16' 5 | import timezoneMock from 'timezone-mock' 6 | import 'jest-enzyme' 7 | 8 | // Configure 9 | configure({ adapter: new Adapter() }) 10 | 11 | // Mock Modules 12 | jest.mock('./components/identicon', () => () =>
[Identicon]
) 13 | 14 | // Mock Time 15 | timezoneMock.register('UTC') 16 | Date.now = jest.fn(() => 1516916214006) 17 | -------------------------------------------------------------------------------- /src/styles/_colors.scss: -------------------------------------------------------------------------------- 1 | // Bases 2 | $light: #fff; 3 | $light2: #e6eaee; 4 | $light3: rgba(216, 216, 216, 0.38); 5 | $dark: #3d464d; 6 | $dark2: #47525d; 7 | $dark3: #a3a2a5; 8 | 9 | // States 10 | $disabled: #77909d; 11 | $info: #337ab7; 12 | $warning: #f0ad4e; 13 | $success: #8dc572; 14 | $error: #be6464; 15 | 16 | // Typography 17 | $heading: $dark; 18 | $text: $dark2; 19 | 20 | // Backgrounds 21 | $background: #ededed; 22 | $darkBackground: $dark; 23 | 24 | /* Specifics */ 25 | 26 | // Button 27 | $buttonGradientStart: #0059ab; 28 | $buttonGradientEnd: #0468c4; 29 | $buttonBoxShadow: #024e95; 30 | $buttonText: $light; 31 | -------------------------------------------------------------------------------- /src/styles/_form.scss: -------------------------------------------------------------------------------- 1 | /* @define Form */ 2 | .Form { 3 | &-noMargins { 4 | margin: 0; 5 | } 6 | 7 | &-noSideMargins { 8 | margin-left: 0; 9 | margin-right: 0; 10 | } 11 | } 12 | 13 | /* @define WizardForm */ 14 | .WizardForm { 15 | &-form { 16 | &-noMarginsWizard { 17 | margin: 0; 18 | } 19 | 20 | &-noSideMarginsWizard { 21 | margin-left: 0; 22 | margin-right: 0; 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/utils/action.js: -------------------------------------------------------------------------------- 1 | export const action = (type, payload, meta) => ({ 2 | type, 3 | payload, 4 | meta 5 | }) 6 | export const errorAction = (type, err) => 7 | console.error(err) || { type, payload: err, error: true } 8 | -------------------------------------------------------------------------------- /src/utils/form-generator.js: -------------------------------------------------------------------------------- 1 | import createReduxForm from 'create-redux-form' 2 | 3 | import TextInput from '../components/text-input' 4 | 5 | export const { form, wizardForm } = createReduxForm({ text: TextInput }) 6 | -------------------------------------------------------------------------------- /src/utils/form-generator.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { submit as reduxFormSubmit } from 'redux-form' 3 | 4 | import setupIntegrationTest, { 5 | flushPromises 6 | } from '../bootstrap/setup-integration-test' 7 | 8 | import { form, wizardForm } from './form-generator' 9 | import { required, number } from './validation' 10 | 11 | jest.mock('..', () => ({})) 12 | 13 | let integration = { 14 | store: null, 15 | history: null, 16 | dispatchSpy: null, 17 | mountApp: null 18 | } 19 | 20 | beforeEach(() => { 21 | integration = setupIntegrationTest({ router: { location: '/' } }) 22 | }) 23 | 24 | const schema = { 25 | payment: { 26 | type: 'text', 27 | placeholder: 'Payment (ETH)', 28 | validate: [required, number], 29 | props: { 30 | type: 'number' 31 | } 32 | }, 33 | timeout: { 34 | type: 'text', 35 | visibleIf: 'payment', 36 | validate: [required, number], 37 | props: { 38 | type: 'number' 39 | } 40 | }, 41 | partyB: { 42 | type: 'text', 43 | formValues: 'arbitratorExtraData', 44 | visibleIf: 'email' 45 | }, 46 | arbitratorExtraData: { 47 | type: 'text', 48 | visibleIf: '!payment' 49 | }, 50 | email: { 51 | type: 'text' 52 | }, 53 | description: { 54 | type: 'text' 55 | } 56 | } 57 | 58 | const schema2 = { 59 | payment2: { 60 | type: 'text', 61 | placeholder: 'Payment (ETH)', 62 | props: { 63 | type: 'number' 64 | } 65 | }, 66 | timeout2: { 67 | type: 'text', 68 | visibleIf: 'payment', 69 | props: { 70 | type: 'number' 71 | } 72 | }, 73 | partyB2: { 74 | type: 'text', 75 | formValues: 'arbitratorExtraData', 76 | visibleIf: 'email' 77 | }, 78 | arbitratorExtraData2: { 79 | type: 'text', 80 | visibleIf: '!payment' 81 | }, 82 | email2: { 83 | type: 'text' 84 | }, 85 | description2: { 86 | type: 'text' 87 | } 88 | } 89 | 90 | const schema3 = { 91 | payment3: { 92 | type: 'text', 93 | placeholder: 'Payment (ETH)', 94 | props: { 95 | type: 'number' 96 | } 97 | }, 98 | timeout3: { 99 | type: 'text', 100 | visibleIf: 'payment', 101 | props: { 102 | type: 'number' 103 | } 104 | }, 105 | partyB3: { 106 | type: 'text', 107 | formValues: 'arbitratorExtraData', 108 | visibleIf: 'email' 109 | }, 110 | arbitratorExtraData3: { 111 | type: 'text', 112 | visibleIf: '!payment' 113 | }, 114 | email3: { 115 | type: 'text' 116 | }, 117 | description3: { 118 | type: 'text' 119 | } 120 | } 121 | 122 | describe('form', () => 123 | it('Takes a schema and returns a form component with utils.', async () => { 124 | const formName = 'testForm' 125 | const { Form, isInvalid, submit } = form(formName, schema) 126 | expect(isInvalid({})).toBe(false) 127 | expect(submit()).toEqual(reduxFormSubmit(formName)) 128 | 129 | // Mount form 130 | const app = integration.mountApp( 131 |
132 | ) 133 | await flushPromises(app) 134 | })) 135 | 136 | describe('wizardForm', () => 137 | it('Takes a nested schema and returns a wizard form component with utils.', async () => { 138 | const formName = 'testWizardForm' 139 | const { Form, isInvalid, submit } = wizardForm(formName, { 140 | step1: schema, 141 | step2: schema2, 142 | step3: schema3 143 | }) 144 | expect(isInvalid({})).toBe(false) 145 | expect(submit()).toEqual(reduxFormSubmit(formName)) 146 | 147 | // Handlers 148 | let backHandlerRef 149 | const getBackHandlerRef = func => (backHandlerRef = func) 150 | const onPageChange = jest.fn() 151 | const handleSubmit = jest.fn() 152 | 153 | // Mount wizard form 154 | const app = integration.mountApp( 155 | 161 | ) 162 | await flushPromises(app) 163 | expect(onPageChange).toHaveBeenCalledTimes(1) 164 | 165 | // Go to the next page 166 | integration.store.dispatch(submit()) 167 | expect(onPageChange).toHaveBeenCalledTimes(2) 168 | 169 | // Go back to the previous page 170 | backHandlerRef() 171 | expect(onPageChange).toHaveBeenCalledTimes(3) 172 | 173 | // Go to the next page 174 | integration.store.dispatch(submit()) 175 | expect(onPageChange).toHaveBeenCalledTimes(4) 176 | 177 | // Go to the next page 178 | integration.store.dispatch(submit()) 179 | expect(onPageChange).toHaveBeenCalledTimes(5) 180 | 181 | // Submit the form 182 | integration.store.dispatch(submit()) 183 | expect(onPageChange).toHaveBeenCalledTimes(5) 184 | expect(handleSubmit).toHaveBeenCalledTimes(1) 185 | 186 | // destroy() form 187 | app.unmount() 188 | })) 189 | -------------------------------------------------------------------------------- /src/utils/functional.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Maps object into an array or a new object and optionally transforms keys. 3 | * @param {object} obj - The obj to map over. 4 | * @param {function} func - The function to call with (value, key). 5 | * @param {{ returnObj: boolean, transformKeyFunc: function }} [options={ returnObj: false }] - Options object. 6 | * @returns {any[]|object} - An array or object, (with optionally transformed keys), with the results of calling func on every property of obj. 7 | */ 8 | export function objMap( 9 | obj, 10 | func, 11 | { returnObj = false, transformKeyFunc } = {} 12 | ) { 13 | const keys = Object.keys(obj) 14 | const keysLen = keys.length 15 | const result = returnObj ? {} : [] 16 | 17 | for (let i = 0; i < keysLen; i++) { 18 | const res = func(obj[keys[i]], keys[i]) 19 | if (returnObj) 20 | result[ 21 | transformKeyFunc ? transformKeyFunc(obj[keys[i]], keys[i]) : keys[i] 22 | ] = res 23 | else result.push(res) 24 | } 25 | 26 | return result 27 | } 28 | -------------------------------------------------------------------------------- /src/utils/functional.test.js: -------------------------------------------------------------------------------- 1 | import { objMap } from './functional' 2 | 3 | describe('objMap', () => { 4 | const obj = { a: 1, b: 2, c: 3 } 5 | const add1 = value => value + 1 6 | 7 | const add1Arr = [2, 3, 4] 8 | const add1Obj = { a: 2, b: 3, c: 4 } 9 | const transformKeyadd1Obj = { a1: 2, b2: 3, c3: 4 } 10 | 11 | it('maps objects into arrays.', () => 12 | expect(objMap(obj, add1)).toEqual(add1Arr)) 13 | 14 | it('maps objects into objects.', () => 15 | expect(objMap(obj, add1, { returnObj: true })).toEqual(add1Obj)) 16 | 17 | it('maps objects into objects and transforms their keys.', () => 18 | expect( 19 | objMap(obj, add1, { 20 | returnObj: true, 21 | transformKeyFunc: (value, key) => key + value 22 | }) 23 | ).toEqual(transformKeyadd1Obj)) 24 | }) 25 | -------------------------------------------------------------------------------- /src/utils/saga.js: -------------------------------------------------------------------------------- 1 | import { call, put } from 'redux-saga/effects' 2 | 3 | import { action as _action, errorAction } from './action' 4 | 5 | /** 6 | * Calls a saga with the `lessdux` flow. 7 | * @param {string} flow - The `lessdux` flow that should be used, (create, fetch, update, delete). 8 | * @param {object} resourceActions - The `lessdux` resource actions object. 9 | * @param {object} saga - The saga being called. 10 | * @param {{ type: string, payload: ?object, meta: ?object }} action - The action object that triggered the saga. 11 | */ 12 | export function* lessduxSaga(flow, resourceActions, saga, action) { 13 | let receiveWord 14 | let failWord 15 | switch (flow) { 16 | case 'create': 17 | receiveWord = '_CREATED' 18 | failWord = '_CREATE' 19 | break 20 | case 'fetch': 21 | receiveWord = '' 22 | failWord = '_FETCH' 23 | break 24 | case 'update': 25 | receiveWord = '_UPDATED' 26 | failWord = '_UPDATE' 27 | yield put(_action(resourceActions.UPDATE)) // Updates are not called directly so call it here to set .updating on the resource 28 | break 29 | case 'delete': 30 | receiveWord = '_DELETED' 31 | failWord = '_DELETE' 32 | break 33 | default: 34 | throw new TypeError('Invalid lessdux flow.') 35 | } 36 | 37 | try { 38 | const result = yield call(saga, action) 39 | 40 | yield put( 41 | _action(resourceActions['RECEIVE' + receiveWord], { 42 | [result.collection ? 'collectionMod' : resourceActions.self]: result 43 | }) 44 | ) 45 | } catch (err) { 46 | err.message && 47 | console.info( 48 | 'Your connection is unstable, please check your network and refresh the page.' 49 | ) 50 | yield put(errorAction(resourceActions['FAIL' + failWord], err)) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/utils/string.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Converts a string in constant case to camel case. e.g. HELLO_WORLD => helloWorld. It also ignores characters between $ chars. e.g. $HELLO$_WORLD => HELLOWorld. 3 | * @param {string} str - The string to convert. 4 | * @param {{ capitalizeFirst: boolean }} [options={ capitalizeFirst: false }] - An options object with sensible defaults. 5 | * @returns {string} - The converted string. 6 | */ 7 | export function constantToCamelCase(str, { capitalizeFirst = false } = {}) { 8 | const newStr = str 9 | .toLowerCase() 10 | .replace(/_./g, match => match[1].toUpperCase()) 11 | 12 | return (capitalizeFirst 13 | ? newStr[0].toUpperCase() + newStr.slice(1) 14 | : newStr 15 | ).replace(/\$(.+?)\$/g, (_m, p1) => p1.toUpperCase()) 16 | } 17 | 18 | /** 19 | * Converts a string in camel case to title case. e.g. helloWorld => Hello World. 20 | * @param {string} str - The string to convert. 21 | * @returns {string} - The converted string. 22 | */ 23 | export function camelToTitleCase(str) { 24 | return str.replace( 25 | /(^[a-z])|([a-z][A-Z])|([A-Z][a-z])/g, 26 | (_m, p1, p2, p3) => 27 | p1 ? p1.toUpperCase() : p2 ? p2[0] + ' ' + p2[1] : ' ' + p3 28 | ) 29 | } 30 | -------------------------------------------------------------------------------- /src/utils/string.test.js: -------------------------------------------------------------------------------- 1 | import { constantToCamelCase, camelToTitleCase } from './string' 2 | 3 | const constant = 'HELLO_CRYPTO_WORLD' 4 | const camelCase = 'helloCryptoWorld' 5 | const capitalizeFirstCamelCase = 'HelloCryptoWorld' 6 | const constantWith$ = '$HELLO$_WORLD' 7 | const camelCaseWith$ = 'HELLOWorld' 8 | const titleCase = 'Hello Crypto World' 9 | 10 | describe('constantToCamelCase', () => { 11 | it('converts constant case strings to camel case.', () => 12 | expect(constantToCamelCase(constant)).toBe(camelCase)) 13 | it('converts constant case strings to camel case and capitalizes the first letter.', () => 14 | expect(constantToCamelCase(constant, { capitalizeFirst: true })).toBe( 15 | capitalizeFirstCamelCase 16 | )) 17 | it('converts constant case strings to camel case and ignores chars between `$` chars.', () => 18 | expect(constantToCamelCase(constantWith$)).toBe(camelCaseWith$)) 19 | }) 20 | 21 | describe('camelToTitleCase', () => 22 | it('converts camel case strings to title case.', () => 23 | expect(camelToTitleCase(camelCase)).toBe(titleCase))) 24 | -------------------------------------------------------------------------------- /src/utils/validation.js: -------------------------------------------------------------------------------- 1 | export const required = name => v => (v ? undefined : `${name} is required.`) 2 | export const number = name => v => 3 | Number.isNaN(Number(v)) ? `${name} must be a number.` : undefined 4 | -------------------------------------------------------------------------------- /stories/button.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { storiesOf } from '@storybook/react' 3 | import { action } from '@storybook/addon-actions' 4 | 5 | import Button from '../src/components/button' 6 | 7 | storiesOf('Button', module) 8 | .add('primary', () => ) 9 | .add('disabled', () => ( 10 | 13 | )) 14 | .add('with lots of text', () => ( 15 | 16 | )) 17 | -------------------------------------------------------------------------------- /stories/identicon.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { storiesOf } from '@storybook/react' 3 | 4 | import Identicon from '../src/components/identicon' 5 | 6 | storiesOf('Identicon', module).add('with placeholder seed', () => ( 7 | 8 | )) 9 | -------------------------------------------------------------------------------- /stories/index.js: -------------------------------------------------------------------------------- 1 | import './button' 2 | import './identicon' 3 | import './page-not-found' 4 | import './requires-meta-mask' 5 | import './text-input' 6 | -------------------------------------------------------------------------------- /stories/page-not-found.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { storiesOf } from '@storybook/react' 3 | 4 | import PageNotFound from '../src/components/page-not-found' 5 | 6 | storiesOf('Page Not Found', module).add('default', () => ) 7 | -------------------------------------------------------------------------------- /stories/requires-meta-mask.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { storiesOf } from '@storybook/react' 3 | 4 | import RequiresMetaMask from '../src/components/requires-meta-mask' 5 | 6 | storiesOf('Requires MetaMask', module) 7 | .add('not found', () => ) 8 | .add('needs unlock', () => ) 9 | -------------------------------------------------------------------------------- /stories/text-input.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { storiesOf } from '@storybook/react' 3 | import { withState } from '@dump247/storybook-state' 4 | 5 | import TextInput from '../src/components/text-input' 6 | 7 | const render = store => ( 8 | 13 | store.set({ 14 | input: { value: event.currentTarget.value, onChange: null } 15 | }) 16 | }} 17 | /> 18 | ) 19 | 20 | storiesOf('Text Input', module) 21 | .add( 22 | 'default', 23 | withState( 24 | { 25 | input: { value: '', onChange: null }, 26 | meta: { valid: undefined, touched: undefined, error: undefined }, 27 | placeholder: 'EMAIL' 28 | }, 29 | render 30 | ) 31 | ) 32 | .add( 33 | 'touched', 34 | withState( 35 | { 36 | input: { value: '', onChange: null }, 37 | meta: { valid: undefined, touched: true, error: undefined }, 38 | placeholder: 'EMAIL' 39 | }, 40 | render 41 | ) 42 | ) 43 | .add( 44 | 'valid', 45 | withState( 46 | { 47 | input: { value: '', onChange: null }, 48 | meta: { valid: true, touched: undefined, error: undefined }, 49 | placeholder: 'EMAIL' 50 | }, 51 | render 52 | ) 53 | ) 54 | .add( 55 | 'error', 56 | withState( 57 | { 58 | input: { value: '', onChange: null }, 59 | meta: { 60 | valid: undefined, 61 | touched: undefined, 62 | error: 'Please enter a valid email.' 63 | }, 64 | placeholder: 'EMAIL' 65 | }, 66 | render 67 | ) 68 | ) 69 | --------------------------------------------------------------------------------