├── .eslintrc ├── .gitignore ├── LICENSE ├── README.md ├── config ├── styleguidist │ ├── config.js │ └── index.html └── webpack │ ├── development.js │ ├── production.js │ └── server.js ├── package.json ├── scripts └── create-component.js ├── src ├── client │ └── index.js ├── server │ ├── getAssetsPath.js │ ├── handleRequest.js │ ├── index.js │ └── renderFullPage.js └── shared │ ├── components │ ├── About │ │ ├── About.js │ │ ├── About.md │ │ └── index.js │ ├── Button │ │ ├── Button.js │ │ ├── Button.md │ │ └── index.js │ ├── Header │ │ ├── Header.js │ │ └── index.js │ ├── Home │ │ ├── Home.js │ │ ├── Home.md │ │ └── index.js │ ├── NotFound │ │ ├── NotFound.js │ │ └── index.js │ ├── Paper │ │ ├── Paper.js │ │ ├── Paper.md │ │ └── index.js │ ├── Ripple │ │ ├── Ripple.js │ │ └── index.js │ ├── Status │ │ ├── Status.js │ │ └── index.js │ └── User │ │ ├── User.js │ │ ├── User.md │ │ └── index.js │ ├── containers │ └── Home.js │ ├── index.js │ ├── reducers │ └── index.js │ ├── routes.js │ └── theme.js └── yarn.lock /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["standard", "standard-react"], 3 | "env": { 4 | "browser": true 5 | } 6 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # App 2 | build 3 | public 4 | 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | 10 | # Runtime data 11 | pids 12 | *.pid 13 | *.seed 14 | 15 | # Directory for instrumented libs generated by jscoverage/JSCover 16 | lib-cov 17 | 18 | # Coverage directory used by tools like istanbul 19 | coverage 20 | 21 | # nyc test coverage 22 | .nyc_output 23 | 24 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 25 | .grunt 26 | 27 | # node-waf configuration 28 | .lock-wscript 29 | 30 | # Compiled binary addons (http://nodejs.org/api/addons.html) 31 | build/Release 32 | 33 | # Dependency directories 34 | node_modules 35 | jspm_packages 36 | 37 | # Optional npm cache directory 38 | .npm 39 | 40 | # Optional REPL history 41 | .node_repl_history 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Anton Kulakov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # isomorphic-react 2 | 3 | - React router 4 4 | - Webpack 2 5 | - Express 6 | - Redux 7 | - JSS 8 | - Server side rendering 9 | - React hot loader 10 | - React Styleguidist 11 | - ES Lint 12 | - Standard.js 13 | 14 | ```bash 15 | npm run client 16 | npm run server 17 | npm run build:client 18 | npm run build:server 19 | npm run prod:server 20 | npm run styleguide 21 | npm run styleguide:build 22 | npm run lint 23 | npm run cc 24 | ``` 25 | 26 | ### React styleguidist 27 | 28 | ![2017-04-08 23 06 06](https://cloud.githubusercontent.com/assets/557190/24832044/02a8c7dc-1cb0-11e7-9138-2a4508cc9113.png) 29 | -------------------------------------------------------------------------------- /config/styleguidist/config.js: -------------------------------------------------------------------------------- 1 | const { resolve } = require('path') 2 | 3 | module.exports = { 4 | title: 'UI Kit', 5 | // getComponentPathLine (componentPath) { 6 | // const name = basename(componentPath, '.js') 7 | // const dir = dirname(componentPath).replace(/^src\/components/, 'netology-ui-kit/lib') 8 | // return `import ${name} from '${dir}'` 9 | // }, 10 | getExampleFilename (componentPath) { 11 | return componentPath.replace(/\.jsx?$/, '.md') 12 | }, 13 | webpackConfig: require('../webpack/development.js'), 14 | highlightTheme: 'material', 15 | serverHost: 'localhost', 16 | showSidebar: true, 17 | serverPort: 6060, 18 | showCode: false, 19 | template: resolve(__dirname, 'index.html'), 20 | sections: [ 21 | { 22 | name: 'Components', 23 | components: resolve(__dirname, '../../src/shared/components/**/[A-Z]*.js') 24 | } 25 | ], 26 | styleguideDir: resolve(__dirname, '../../build/static/styleguide'), 27 | skipComponentsWithoutExample: true, 28 | styles: { 29 | Markdown: { 30 | blockquote: { 31 | margin: '1em 0', 32 | background: '#F8F8F9', 33 | padding: '1em 1.5em', 34 | lineHeight: '1.4285em', 35 | color: 'rgba(0,0,0,.87)', 36 | transition: 'opacity .1s ease,color .1s ease,background .1s ease,box-shadow .1s ease', 37 | borderRadius: '3px' 38 | }, 39 | h5: { 40 | color: '#222', 41 | fontWeight: 700 42 | } 43 | }, 44 | Playground: { 45 | root: { 46 | backgroundColor: 'white', 47 | backgroundImage: 'linear-gradient(45deg,#efefef 25%,transparent 0,transparent 75%,#efefef 0,#efefef),linear-gradient(45deg,#efefef 25%,transparent 0,transparent 75%,#efefef 0,#efefef)', 48 | backgroundPosition: '0 0,10px 10px', 49 | backgroundSize: '21px 21px', 50 | fontSize: '1rem' 51 | } 52 | }, 53 | StyleGuide: { 54 | root: { 55 | fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif' 56 | } 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /config/styleguidist/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | <%=htmlWebpackPlugin.options.title%> 7 | 8 | 9 |
10 | 11 | -------------------------------------------------------------------------------- /config/webpack/development.js: -------------------------------------------------------------------------------- 1 | const { resolve } = require('path') 2 | const webpack = require('webpack') 3 | 4 | module.exports = { 5 | entry: [ 6 | 'react-hot-loader/patch', 7 | 'webpack-dev-server/client?http://localhost:3000', 8 | 'webpack/hot/only-dev-server', 9 | './src/client/index.js' 10 | ], 11 | output: { 12 | filename: 'bundle.js', 13 | path: resolve(__dirname, 'public'), 14 | publicPath: '/static/' 15 | }, 16 | devtool: 'inline-source-map', 17 | module: { 18 | rules: [ 19 | { 20 | test: /\.jsx?$/, 21 | exclude: /node_modules/, 22 | use: [ 23 | { 24 | loader: 'babel-loader', 25 | options: { 26 | presets: [ 27 | ['env', { 28 | modules: false, 29 | targets: { 30 | browsers: [ 31 | 'last 2 versions', 32 | 'safari >= 7' 33 | ] 34 | } 35 | }], 36 | 'react' 37 | ], 38 | 'plugins': [ 39 | 'react-hot-loader/babel' 40 | ] 41 | } 42 | }, 43 | 'eslint-loader' 44 | ] 45 | } 46 | ] 47 | }, 48 | stats: 'errors-only', 49 | bail: true, 50 | plugins: [ 51 | new webpack.DefinePlugin({ 52 | 'process.env.NODE_ENV': JSON.stringify('development') 53 | }), 54 | new webpack.HotModuleReplacementPlugin(), 55 | new webpack.NamedModulesPlugin(), 56 | new webpack.NoEmitOnErrorsPlugin() 57 | ], 58 | devServer: { 59 | host: 'localhost', 60 | port: 3000, 61 | hot: true, 62 | proxy: { 63 | '*': 'http://localhost:3001' 64 | }, 65 | overlay: { 66 | warnings: true, 67 | errors: true 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /config/webpack/production.js: -------------------------------------------------------------------------------- 1 | const { resolve } = require('path') 2 | const webpack = require('webpack') 3 | // const ChunkManifestPlugin = require('chunk-manifest-webpack-plugin') 4 | // const WebpackChunkHash = require('webpack-chunk-hash') 5 | const ManifestPlugin = require('webpack-manifest-plugin') 6 | 7 | module.exports = { 8 | // context: resolve(__dirname, 'src'), 9 | entry: { 10 | main: './src/client/index.js' 11 | }, 12 | output: { 13 | filename: '[name].[chunkhash].js', 14 | path: resolve(__dirname, 'build/static'), 15 | publicPath: '/static/' 16 | }, 17 | devtool: 'cheap-module-source-map', 18 | module: { 19 | rules: [ 20 | { 21 | test: /\.jsx?$/, 22 | exclude: /node_modules/, 23 | use: [{ 24 | loader: 'babel-loader', 25 | options: { 26 | presets: [ 27 | ['env', { 28 | modules: false, 29 | targets: { 30 | browsers: [ 31 | 'last 2 versions', 32 | 'safari >= 7' 33 | ] 34 | } 35 | }], 36 | 'react' 37 | ] 38 | } 39 | }] 40 | } 41 | ] 42 | }, 43 | // performance: { 44 | // hints: 'error' 45 | // }, 46 | // stats: 'errors-only', 47 | plugins: [ 48 | new webpack.optimize.CommonsChunkPlugin({ 49 | name: 'vendor', 50 | minChunks: function (module) { 51 | // this assumes your vendor imports exist in the node_modules directory 52 | return module.context && module.context.indexOf('node_modules') !== -1 53 | } 54 | // names: ['vendor', 'manifest'] // Specify the common bundle's name. 55 | }), 56 | // CommonChunksPlugin will now extract all the common modules from vendor and main bundles 57 | // new webpack.optimize.CommonsChunkPlugin({ 58 | // filename: 'manifest.json', 59 | // name: 'manifest' // But since there are no more common modules between them we end up with just the runtime code included in the manifest file 60 | // }), 61 | new ManifestPlugin(), 62 | new webpack.HashedModuleIdsPlugin(), 63 | // new WebpackChunkHash(), 64 | // new ChunkManifestPlugin(), 65 | new webpack.LoaderOptionsPlugin({ 66 | minimize: true, 67 | debug: false 68 | }), 69 | new webpack.optimize.OccurrenceOrderPlugin(), 70 | new webpack.optimize.UglifyJsPlugin({ 71 | beautify: false, 72 | mangle: { 73 | screw_ie8: true, 74 | keep_fnames: true 75 | }, 76 | compress: { 77 | screw_ie8: true 78 | }, 79 | compressor: { 80 | warnings: false 81 | }, 82 | comments: false 83 | }), 84 | new webpack.DefinePlugin({ 85 | 'process.env.NODE_ENV': JSON.stringify('production') 86 | }) 87 | ] 88 | } 89 | -------------------------------------------------------------------------------- /config/webpack/server.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kulakowka/isomorphic-react/9cce31b216169ee4728cd6301b5d748f8d311c05/config/webpack/server.js -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "isomorphic-react", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "scripts": { 7 | "start": "", 8 | "client": "webpack-dev-server --config config/webpack/development.js --progress", 9 | "server": "nodemon src/server/index.js --exec babel-node --presets=env,react", 10 | "build:client": "NODE_ENV=production rm -rf public && webpack --config webpack.production.config.js --progress", 11 | "build:server": "NODE_ENV=production babel src -d build --presets=env,react", 12 | "prod:server": "NODE_ENV=production node build/server.js", 13 | "styleguide": "styleguidist server --config config/styleguidist/config.js", 14 | "styleguide:build": "styleguidist build --config config/styleguidist/config.js", 15 | "lint": "standard src/**/*.js", 16 | "cc": "node scripts/create-component" 17 | }, 18 | "dependencies": { 19 | "babel-cli": "^6.24.1", 20 | "babel-core": "^6.24.1", 21 | "babel-loader": "^6.4.1", 22 | "babel-preset-env": "^1.3.3", 23 | "babel-preset-es2015": "^6.24.1", 24 | "babel-preset-latest": "^6.24.1", 25 | "babel-preset-react": "^6.24.1", 26 | "chalk": "^1.1.3", 27 | "chunk-manifest-webpack-plugin": "^1.0.0", 28 | "classnames": "^2.2.5", 29 | "eslint": "^3.19.0", 30 | "eslint-config-standard": "^10.2.0", 31 | "eslint-config-standard-react": "^4.3.0", 32 | "eslint-loader": "^1.7.1", 33 | "eslint-plugin-promise": "^3.5.0", 34 | "eslint-plugin-react": "^6.10.3", 35 | "eslint-plugin-standard": "^3.0.1", 36 | "express": "^4.15.2", 37 | "fs-promise": "^2.0.2", 38 | "inquirer": "^3.0.6", 39 | "jss": "^6.5.0", 40 | "jss-preset-default": "^1.3.1", 41 | "nodemon": "^1.11.0", 42 | "prop-types": "^15.5.4", 43 | "react": "^15.5.3", 44 | "react-dom": "^15.5.3", 45 | "react-helmet": "^5.0.2", 46 | "react-hot-loader": "^3.0.0-beta.6", 47 | "react-jss": "^5.4.1", 48 | "react-redux": "^5.0.3", 49 | "react-ripple-effect": "^1.0.4", 50 | "react-router": "^4.0.0", 51 | "react-router-dom": "^4.0.0", 52 | "redux": "^3.6.0", 53 | "redux-devtools-extension": "^2.13.0", 54 | "standard": "^10.0.1", 55 | "webpack": "^2.3.3", 56 | "webpack-chunk-hash": "^0.4.0", 57 | "webpack-dev-server": "^2.4.2", 58 | "webpack-manifest-plugin": "^1.1.0" 59 | }, 60 | "devDependencies": { 61 | "react-styleguidist": "^5.0.6" 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /scripts/create-component.js: -------------------------------------------------------------------------------- 1 | const inquirer = require('inquirer') 2 | const fsp = require('fs-promise') 3 | const { resolve } = require('path') 4 | const chalk = require('chalk') 5 | 6 | const COMPONENTS_PATH = resolve(__dirname, `../src/shared/components`) 7 | 8 | inquirer.prompt([ 9 | { 10 | type: 'input', 11 | name: 'name', 12 | message: 'What component name?', 13 | filter: val => val[0].toUpperCase() + val.slice(1) 14 | } 15 | ]) 16 | .then(createComponentFiles) 17 | .catch(console.log) 18 | 19 | function isComponentExist ({ name }) { 20 | const filePath = resolve(COMPONENTS_PATH, name) 21 | const isExist = fsp.existsSync(filePath) 22 | if (isExist) console.log('Component %s already exist', chalk.red(filePath)) 23 | return isExist 24 | } 25 | 26 | function createComponentFiles (answers) { 27 | if (isComponentExist(answers)) return false 28 | 29 | return Promise.all([ 30 | createIndexFile, 31 | createComponentFile, 32 | createDocsFile 33 | ].map(f => f(answers))) 34 | } 35 | 36 | function createIndexFile ({ name }) { 37 | const template = `import ${name} from './${name}'\n\nexport default ${name}\n` 38 | const filePath = resolve(COMPONENTS_PATH, name, 'index.js') 39 | console.log('Created index file: ', chalk.blue(filePath)) 40 | return fsp.outputFile(filePath, template) 41 | } 42 | 43 | function createComponentFile ({ name }) { 44 | const template = `import React from 'react' 45 | import PropTypes from 'prop-types' 46 | import injectSheet from 'react-jss' 47 | 48 | /** 49 | * A ${name} component. 50 | */ 51 | function ${name} (props) { 52 | const { 53 | children, 54 | classes 55 | } = props 56 | 57 | return ( 58 |
59 | {children} 60 |
61 | ) 62 | } 63 | 64 | ${name}.displayName = '${name}' 65 | 66 | ${name}.propTypes = { 67 | /** 68 | * Primary content. 69 | */ 70 | children: PropTypes.node, 71 | 72 | /** 73 | * Classes from JSS 74 | */ 75 | classes: PropTypes.object.isRequired 76 | } 77 | 78 | ${name}.defaultProps = {} 79 | 80 | const styles = { 81 | root: { 82 | display: 'flex' 83 | } 84 | } 85 | 86 | export default injectSheet(styles)(${name}) 87 | ` 88 | const filePath = resolve(COMPONENTS_PATH, name, `${name}.js`) 89 | 90 | console.log('Created component file: ', chalk.blue(filePath)) 91 | return fsp.outputFile(filePath, template) 92 | } 93 | 94 | function createDocsFile ({ name }) { 95 | const template = `A standard ${name}. 96 | 97 | ${'```'}example 98 | <${name} /> 99 | ${'```'} 100 | ` 101 | const filePath = resolve(COMPONENTS_PATH, name, `${name}.md`) 102 | 103 | console.log('Created docs file: ', chalk.blue(filePath)) 104 | return fsp.outputFile(filePath, template) 105 | } 106 | -------------------------------------------------------------------------------- /src/client/index.js: -------------------------------------------------------------------------------- 1 | 2 | import React from 'react' 3 | import ReactDOM from 'react-dom' 4 | import { composeWithDevTools } from 'redux-devtools-extension' 5 | import { createStore, applyMiddleware } from 'redux' 6 | import { Provider } from 'react-redux' 7 | import reducers from '../shared/reducers' 8 | import { BrowserRouter } from 'react-router-dom' 9 | import { AppContainer } from 'react-hot-loader' 10 | import jss from 'jss' 11 | import preset from 'jss-preset-default' 12 | import App from '../shared' 13 | 14 | // One time setup with default plugins and settings. 15 | jss.setup(preset()) 16 | 17 | // Grab the state from a global variable injected into the server-generated HTML 18 | const preloadedState = window.__PRELOADED_STATE__ 19 | 20 | // Allow the passed state to be garbage-collected 21 | delete window.__PRELOADED_STATE__ 22 | 23 | // Create Redux store with initial state 24 | const store = createStore(reducers, preloadedState, composeWithDevTools( 25 | applyMiddleware() 26 | )) 27 | 28 | const render = (Component) => { 29 | ReactDOM.render( 30 | 31 | 32 | 33 | 34 | 35 | 36 | , 37 | document.getElementById('root'), 38 | () => { 39 | // We don't need the static css any more once we have launched our application. 40 | const ssStyles = document.getElementById('server-side-styles') 41 | ssStyles.parentNode.removeChild(ssStyles) 42 | } 43 | ) 44 | } 45 | 46 | render(App) 47 | 48 | if (module.hot) { 49 | module.hot.accept('../shared', () => render(App)) 50 | module.hot.accept('../shared/reducers', () => { 51 | const nextRootReducer = require('../shared/reducers').default 52 | store.replaceReducer(nextRootReducer) 53 | }) 54 | } 55 | -------------------------------------------------------------------------------- /src/server/getAssetsPath.js: -------------------------------------------------------------------------------- 1 | export default function getAssetsPath () { 2 | if (process.env.NODE_ENV === 'production') { 3 | const manifest = require('../static/manifest.json') 4 | return ` 5 | ` 6 | } 7 | return '' 8 | } 9 | -------------------------------------------------------------------------------- /src/server/handleRequest.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOMServer from 'react-dom/server' 3 | import { createStore } from 'redux' 4 | import { Provider } from 'react-redux' 5 | import reducers from '../shared/reducers' 6 | import routes from '../shared/routes' 7 | import { StaticRouter } from 'react-router' 8 | import App from '../shared' 9 | import { matchPath } from 'react-router-dom' 10 | import {SheetsRegistryProvider, SheetsRegistry} from 'react-jss' 11 | import renderFullPage from './renderFullPage' 12 | 13 | export default function handleRequest (req, res, next) { 14 | const context = {} 15 | 16 | const sheets = new SheetsRegistry() 17 | 18 | // inside a request 19 | const promises = [] 20 | // use `some` to imitate `` behavior of selecting only 21 | // the first to match 22 | routes.some(route => { 23 | // use `matchPath` here 24 | const match = matchPath(req.url, route) 25 | if (match) promises.push(route.component.loadData(match)) 26 | return match 27 | }) 28 | 29 | Promise.all(promises).then(data => { 30 | // do something w/ the data so the client 31 | // can access it then render the app 32 | const counter = parseInt(req.query.counter, 10) || 0 33 | 34 | // Compile an initial state 35 | let preloadedState = { counter, data } 36 | 37 | // Create a new Redux store instance 38 | const store = createStore(reducers, preloadedState) 39 | 40 | const html = ReactDOMServer.renderToString( 41 | 42 | 46 | 47 | 48 | 49 | 50 | 51 | ) 52 | 53 | if (context.status) { 54 | res.status(context.status) 55 | } 56 | 57 | if (context.url) { 58 | res.redirect(301, context.url) 59 | } 60 | 61 | // Grab the initial state from our Redux store 62 | const finalState = store.getState() 63 | 64 | // Send the rendered page back to the client 65 | res.send(renderFullPage({ sheets, html, finalState })) 66 | }) 67 | } 68 | -------------------------------------------------------------------------------- /src/server/index.js: -------------------------------------------------------------------------------- 1 | import express from 'express' 2 | import { resolve } from 'path' 3 | import handleRequest from './handleRequest' 4 | 5 | const staticPath = express.static(resolve(__dirname, 'static')) 6 | 7 | const app = express() 8 | 9 | app.use('/static', staticPath) 10 | app.use(handleRequest) 11 | app.listen(3001) 12 | -------------------------------------------------------------------------------- /src/server/renderFullPage.js: -------------------------------------------------------------------------------- 1 | import getAssetsPath from './getAssetsPath' 2 | 3 | export default function renderFullPage ({ sheets, html, finalState }) { 4 | return ` 5 | 6 | 7 | 8 | Redux Universal Example 9 | 10 | 13 | 14 | 15 |
${html}
16 | 19 | ${getAssetsPath()} 20 | 21 | 22 | ` 23 | } 24 | -------------------------------------------------------------------------------- /src/shared/components/About/About.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Paper from '../Paper' 3 | 4 | /** 5 | * About page 6 | */ 7 | const About = () => ( 8 | 9 |

About

10 |

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

11 |

But I must explain to you how all this mistaken idea of denouncing pleasure and praising pain was born and I will give you a complete account of the system, and expound the actual teachings of the great explorer of the truth, the master-builder of human happiness. No one rejects, dislikes, or avoids pleasure itself, because it is pleasure, but because those who do not know how to pursue pleasure rationally encounter consequences that are extremely painful. Nor again is there anyone who loves or pursues or desires to obtain pain of itself, because it is pain, but because occasionally circumstances occur in which toil and pain can procure him some great pleasure. To take a trivial example, which of us ever undertakes laborious physical exercise, except to obtain some advantage from it? But who has any right to find fault with a man who chooses to enjoy a pleasure that has no annoying consequences, or one who avoids a pain that produces no resultant pleasure?

12 |
13 | ) 14 | 15 | About.loadData = (match) => Promise.resolve({ data: 2, match }) 16 | 17 | export default About 18 | -------------------------------------------------------------------------------- /src/shared/components/About/About.md: -------------------------------------------------------------------------------- 1 | ```example 2 | 3 | ``` -------------------------------------------------------------------------------- /src/shared/components/About/index.js: -------------------------------------------------------------------------------- 1 | import About from './About' 2 | 3 | export default About 4 | -------------------------------------------------------------------------------- /src/shared/components/Button/Button.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import PropTypes from 'prop-types' 3 | import injectSheet from 'react-jss' 4 | import cx from 'classnames' 5 | import { Link } from 'react-router-dom' 6 | import { DEFAULT_FONT } from '../../theme' 7 | import Ripple from '../Ripple' 8 | 9 | /** 10 | * A Button component. 11 | */ 12 | class Button extends Component { 13 | constructor () { 14 | super() 15 | this.state = { 16 | ripples: [] 17 | } 18 | this.handleClick = this.handleClick.bind(this) 19 | } 20 | handleClick (e, b) { 21 | // console.log(e, b) 22 | // console.log('this.button', this.button) 23 | // console.log('event', e) 24 | console.dir(e.currentTarget) 25 | // Get Cursor Position 26 | let cursorPos = { 27 | top: e.clientY - e.currentTarget.offsetTop - 25, 28 | left: e.clientX - e.currentTarget.offsetLeft - 25 29 | } 30 | // console.log('cursorPos', cursorPos, { 31 | // offsetLeft: this.button.offsetLeft, 32 | // offsetTop: this.button.offsetTop 33 | // }) 34 | 35 | this.setState({ 36 | ripples: this.state.ripples.concat() 37 | }) 38 | 39 | setTimeout(() => { 40 | this.setState({ 41 | ripples: this.state.ripples.filter((r, i) => !!i) 42 | }) 43 | }, 1000) 44 | } 45 | render () { 46 | const { 47 | children, 48 | href, 49 | content, 50 | classes, 51 | primary, 52 | positive, 53 | negative, 54 | rounded, 55 | size, 56 | onClick 57 | } = this.props 58 | 59 | const classNames = cx( 60 | classes.button, 61 | primary && classes.primary, 62 | positive && classes.positive, 63 | negative && classes.negative, 64 | rounded && classes.rounded, 65 | size && classes[`${size}-size`] 66 | ) 67 | 68 | const otherProps = { 69 | onClick 70 | } 71 | 72 | if (href) { 73 | return ( 74 | { this.button = button }} 77 | className={classNames} 78 | {...otherProps} 79 | onMouseUp={this.handleClick} 80 | onTouchEnd={this.handleClick} 81 | > 82 | {content || children} 83 | {this.state.ripples} 84 | 85 | ) 86 | } 87 | 88 | return ( 89 | 99 | ) 100 | } 101 | } 102 | 103 | Button.displayName = 'Button' 104 | 105 | Button.propTypes = { 106 | /** Link href */ 107 | href: PropTypes.string, 108 | /** Primary content. */ 109 | children: PropTypes.node, 110 | /** Primary content. */ 111 | content: PropTypes.node, 112 | /** Classes from JSS */ 113 | classes: PropTypes.object.isRequired, 114 | /** Is primary */ 115 | primary: PropTypes.bool, 116 | /** Is positive */ 117 | positive: PropTypes.bool, 118 | /** Is negative */ 119 | negative: PropTypes.bool, 120 | /** Is rounded */ 121 | rounded: PropTypes.bool, 122 | /** Size: small|default|big */ 123 | size: PropTypes.string.isRequired, 124 | /** On click callback */ 125 | onClick: PropTypes.func 126 | } 127 | 128 | Button.defaultProps = { 129 | size: 'default', 130 | rounded: true 131 | } 132 | 133 | const styles = { 134 | button: { 135 | fontFamily: DEFAULT_FONT, 136 | position: 'relative', 137 | overflow: 'hidden', 138 | display: 'inline-flex', 139 | padding: 0, 140 | margin: 0, 141 | border: 0, 142 | cursor: 'pointer', 143 | userSelect: 'none', 144 | background: '#ddd', 145 | color: '#222', 146 | textDecoration: 'none', 147 | textTransform: 'uppercase', 148 | 'box-shadow': '0 9px 3px 0px rgba(0,0,0,0.15)', 149 | transition: '0.1s all', 150 | '&:focus': { 151 | outline: 0 152 | }, 153 | '&:hover': { 154 | background: '#eee', 155 | // boxShadow: '0 0 0 3px #ddd', 156 | 'box-shadow': '0 5px 3px 0px rgba(0,0,0,0.1)', 157 | transform: 'scale(1.1)', 158 | position: 'relative', 159 | zIndex: 2 160 | // transform: 'translateY(4px)' 161 | }, 162 | '&:active': { 163 | background: '#ddd', 164 | transform: 'scale(1)', 165 | 'box-shadow': '0 9px 3px 0px rgba(0,0,0,0.15)' 166 | } 167 | }, 168 | primary: { 169 | background: 'darkblue', 170 | color: 'white', 171 | '&:hover': { 172 | background: 'blue' 173 | }, 174 | '&:active': { 175 | background: 'darkblue' 176 | // boxShadow: '0 0 0 3px darkblue' 177 | } 178 | }, 179 | positive: { 180 | background: '#3e8e41', 181 | color: 'white', 182 | '&:hover': { 183 | background: '#4CAF50' 184 | }, 185 | '&:active': { 186 | background: '#3e8e41' 187 | // boxShadow: '0 0 0 3px darkgreen' 188 | } 189 | }, 190 | negative: { 191 | background: 'darkred', 192 | color: 'white', 193 | '&:hover': { 194 | background: 'red' 195 | }, 196 | '&:active': { 197 | background: 'darkred' 198 | // boxShadow: '0 0 0 3px darkred' 199 | } 200 | }, 201 | rounded: { 202 | borderRadius: '5px' 203 | }, 204 | 'small-size': { 205 | padding: '15px 30px', 206 | fontSize: '16px', 207 | lineHeight: '16px' 208 | }, 209 | 'default-size': { 210 | padding: '20px 40px', 211 | fontSize: '20px', 212 | lineHeight: '20px' 213 | }, 214 | 'big-size': { 215 | padding: '30px 60px', 216 | fontSize: '28px', 217 | lineHeight: '28px' 218 | } 219 | } 220 | 221 | export default injectSheet(styles)(Button) 222 | -------------------------------------------------------------------------------- /src/shared/components/Button/Button.md: -------------------------------------------------------------------------------- 1 | A standard Button. 2 | 3 | ```example 4 | 5 | ``` 6 | ```example 7 | 9 | 10 | 11 | 12 | 13 | 14 | ) 15 | 16 | Header.propTypes = { 17 | /** 18 | * Classes from JSS 19 | */ 20 | classes: PropTypes.object.isRequired 21 | } 22 | 23 | const styles = { 24 | header: { 25 | display: 'flex', 26 | justifyContent: 'space-start' 27 | } 28 | } 29 | 30 | export default injectSheet(styles)(Header) 31 | -------------------------------------------------------------------------------- /src/shared/components/Header/index.js: -------------------------------------------------------------------------------- 1 | import Header from './Header' 2 | 3 | export default Header 4 | -------------------------------------------------------------------------------- /src/shared/components/Home/Home.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import Paper from '../Paper' 4 | import Button from '../Button' 5 | import injectSheet from 'react-jss' 6 | import { DEFAULT_FONT } from '../../theme' 7 | 8 | /** 9 | * A home page root comoponent 10 | */ 11 | const Home = ({ classes, counter, incrementCounter }) => ( 12 | 13 |

Home

14 |
{`Counter: ${counter} 2`}
15 | 18 |
19 | ) 20 | 21 | Home.propTypes = { 22 | /** Counter value */ 23 | counter: PropTypes.number.isRequired, 24 | /** Increment counter action creator */ 25 | incrementCounter: PropTypes.func.isRequired, 26 | /** Classes from JSS */ 27 | classes: PropTypes.object.isRequired 28 | } 29 | 30 | Home.loadData = (match) => Promise.resolve({ data: 1, match }) 31 | 32 | const styles = { 33 | title: { 34 | fontFamily: DEFAULT_FONT, 35 | fontSize: '50px', 36 | fontWeight: 'bold', 37 | margin: 0 38 | }, 39 | counter: { 40 | fontFamily: DEFAULT_FONT, 41 | fontSize: '20px', 42 | margin: '50px 0' 43 | } 44 | } 45 | 46 | export default injectSheet(styles)(Home) 47 | -------------------------------------------------------------------------------- /src/shared/components/Home/Home.md: -------------------------------------------------------------------------------- 1 | ```example 2 | console.log('incrementCounter')} /> 3 | ``` -------------------------------------------------------------------------------- /src/shared/components/Home/index.js: -------------------------------------------------------------------------------- 1 | import Home from './Home' 2 | 3 | export default Home 4 | -------------------------------------------------------------------------------- /src/shared/components/NotFound/NotFound.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Status from '../Status' 3 | import Paper from '../Paper' 4 | 5 | const NotFound = () => ( 6 | 7 | 8 |

Not Found

9 |
10 |
11 | ) 12 | 13 | export default NotFound 14 | -------------------------------------------------------------------------------- /src/shared/components/NotFound/index.js: -------------------------------------------------------------------------------- 1 | import NotFound from './NotFound' 2 | 3 | export default NotFound 4 | -------------------------------------------------------------------------------- /src/shared/components/Paper/Paper.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import injectSheet from 'react-jss' 4 | import cx from 'classnames' 5 | import { DEFAULT_FONT } from '../../theme' 6 | 7 | /** 8 | * A Paper component. 9 | */ 10 | function Paper (props) { 11 | const { 12 | children, 13 | classes, 14 | circle, 15 | rounded, 16 | zDepth 17 | } = props 18 | 19 | const classNames = cx( 20 | classes.paper, 21 | circle && classes.circle, 22 | rounded && classes.rounded, 23 | zDepth && classes[`zDepth${zDepth}`] 24 | ) 25 | 26 | return ( 27 |
28 | {children} 29 |
30 | ) 31 | } 32 | 33 | Paper.displayName = 'Paper' 34 | 35 | Paper.propTypes = { 36 | /** Primary content. */ 37 | children: PropTypes.node, 38 | /** Classes from JSS */ 39 | classes: PropTypes.object.isRequired, 40 | /** Set to true to generate a circlular paper container. */ 41 | circle: PropTypes.bool, 42 | /** By default, the paper container will have a border radius. Set this to false to generate a container with sharp corners. */ 43 | rounded: PropTypes.bool, 44 | /** This number represents the zDepth of the paper shadow. */ 45 | zDepth: PropTypes.number 46 | } 47 | 48 | Paper.defaultProps = { 49 | circle: false, 50 | rounded: false, 51 | zDepth: 1 52 | } 53 | 54 | const styles = { 55 | paper: { 56 | 'background-color': 'white', 57 | 'transition': '.3s', 58 | '-webkit-tap-highlight-color': 'rgba(0, 0, 0, 0)', 59 | 'padding': '1rem', 60 | 'align-items': 'center', 61 | 'min-width': '30px', 62 | 'min-height': '30px', 63 | 'font-family': DEFAULT_FONT 64 | }, 65 | circle: { 66 | 'border-radius': '50%', 67 | 'justify-content': 'center' 68 | }, 69 | rounded: { 70 | 'border-radius': '2px' 71 | }, 72 | zDepth1: { 73 | 'box-shadow': 'rgba(0, 0, 0, 0.117647) 0px 1px 6px, rgba(0, 0, 0, 0.117647) 0px 1px 4px' 74 | }, 75 | zDepth2: { 76 | 'box-shadow': 'rgba(0, 0, 0, 0.156863) 0px 3px 10px, rgba(0, 0, 0, 0.227451) 0px 3px 10px' 77 | }, 78 | zDepth3: { 79 | 'box-shadow': 'rgba(0, 0, 0, 0.188235) 0px 10px 30px, rgba(0, 0, 0, 0.227451) 0px 6px 10px' 80 | }, 81 | zDepth4: { 82 | 'box-shadow': 'rgba(0, 0, 0, 0.247059) 0px 14px 45px, rgba(0, 0, 0, 0.219608) 0px 10px 18px' 83 | }, 84 | zDepth5: { 85 | 'box-shadow': 'rgba(0, 0, 0, 0.298039) 0px 19px 60px, rgba(0, 0, 0, 0.219608) 0px 15px 20px' 86 | } 87 | } 88 | 89 | export default injectSheet(styles)(Paper) 90 | -------------------------------------------------------------------------------- /src/shared/components/Paper/Paper.md: -------------------------------------------------------------------------------- 1 | A standard Paper. 2 | 3 | ```example 4 |
5 | 6 | 7 | 8 | 9 | 10 |
11 | ``` 12 | 13 | A circle Paper. 14 | 15 | ```example 16 |
17 | 18 | 19 | 20 | 21 | 22 |
23 | ``` 24 | 25 | A rounded Paper. 26 | 27 | ```example 28 |
29 | 30 | 31 | 32 | 33 | 34 |
35 | ``` 36 | 37 | 38 | -------------------------------------------------------------------------------- /src/shared/components/Paper/index.js: -------------------------------------------------------------------------------- 1 | import Paper from './Paper' 2 | 3 | export default Paper 4 | -------------------------------------------------------------------------------- /src/shared/components/Ripple/Ripple.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import injectSheet from 'react-jss' 4 | 5 | /** 6 | * A Ripple component. 7 | */ 8 | var Ripple = ({ classes, cursorPos, isVisible }) => { 9 | let style = { 10 | left: cursorPos.left, 11 | top: cursorPos.top 12 | } 13 | 14 | return ( 15 |
16 | ) 17 | } 18 | 19 | const styles = { 20 | ripple: { 21 | position: 'absolute', 22 | 'border-radius': '50%', 23 | width: '50px', 24 | height: '50px', 25 | animation: 'ripple-animation 1s', 26 | 'animation-fill-mode': 'forwards', 27 | background: 'rgba(0, 0, 0, 0.5)' 28 | }, 29 | '@keyframes ripple-animation': { 30 | from: { 31 | transform: 'scale(1)', 32 | opacity: 0.4 33 | }, 34 | to: { 35 | transform: 'scale(10)', 36 | opacity: 0 37 | } 38 | } 39 | } 40 | 41 | Ripple.propTypes = { 42 | cursorPos: PropTypes.object, 43 | isVisible: PropTypes.bool, 44 | /** Classes from JSS */ 45 | classes: PropTypes.object.isRequired 46 | } 47 | 48 | Ripple.displayName = 'Ripple' 49 | 50 | Ripple.defaultProps = { 51 | isVisible: false, 52 | cursorPos: { 53 | left: 0, 54 | top: 0 55 | } 56 | } 57 | 58 | export default injectSheet(styles)(Ripple) 59 | -------------------------------------------------------------------------------- /src/shared/components/Ripple/index.js: -------------------------------------------------------------------------------- 1 | import Ripple from './Ripple' 2 | 3 | export default Ripple 4 | -------------------------------------------------------------------------------- /src/shared/components/Status/Status.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import { Route } from 'react-router' 4 | 5 | const Status = ({ code, children }) => ( 6 | { 7 | if (staticContext) staticContext.status = code 8 | return children 9 | }} /> 10 | ) 11 | 12 | Status.propTypes = { 13 | children: PropTypes.node.isRequired, 14 | code: PropTypes.number.isRequired 15 | } 16 | 17 | export default Status 18 | -------------------------------------------------------------------------------- /src/shared/components/Status/index.js: -------------------------------------------------------------------------------- 1 | import Status from './Status' 2 | 3 | export default Status 4 | -------------------------------------------------------------------------------- /src/shared/components/User/User.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Paper from '../Paper' 3 | 4 | const User = () => ( 5 | 6 |

User

7 |
8 | ) 9 | 10 | User.loadData = (match) => Promise.resolve({ data: 3, match }) 11 | 12 | export default User 13 | -------------------------------------------------------------------------------- /src/shared/components/User/User.md: -------------------------------------------------------------------------------- 1 | ```example 2 | 3 | ``` -------------------------------------------------------------------------------- /src/shared/components/User/index.js: -------------------------------------------------------------------------------- 1 | import User from './User' 2 | 3 | export default User 4 | -------------------------------------------------------------------------------- /src/shared/containers/Home.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux' 2 | import Home from '../components/Home' 3 | 4 | export default connect( 5 | (state, props) => ({ 6 | counter: state.counter 7 | }), 8 | (dispatch, props) => ({ 9 | incrementCounter: () => dispatch({ type: 'INCREMENT' }) 10 | }) 11 | )(Home) 12 | -------------------------------------------------------------------------------- /src/shared/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import { Switch, Route, Redirect } from 'react-router' 4 | import Header from './components/Header' 5 | import NotFound from './components/NotFound' 6 | import routes from './routes' 7 | import injectSheet from 'react-jss' 8 | 9 | /** 10 | * App component 11 | */ 12 | const App = ({ classes }) => ( 13 |
14 |
15 | 16 | {routes.map(route => ( 17 | 18 | ))} 19 | 20 | 21 | 22 |
23 | ) 24 | 25 | App.propTypes = { 26 | /** 27 | * Classes from JSS 28 | */ 29 | classes: PropTypes.object.isRequired 30 | } 31 | 32 | const styles = { 33 | root: { 34 | background: 'white' 35 | }, 36 | '@global': { 37 | body: { 38 | color: '#333' 39 | } 40 | } 41 | } 42 | 43 | export default injectSheet(styles)(App) 44 | -------------------------------------------------------------------------------- /src/shared/reducers/index.js: -------------------------------------------------------------------------------- 1 | // Reducers 2 | export default function reducer (state = {}, action) { 3 | switch (action.type) { 4 | case 'INCREMENT': 5 | return Object.assign({}, state, { 6 | counter: state.counter + 1 7 | }) 8 | default: 9 | return state 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/shared/routes.js: -------------------------------------------------------------------------------- 1 | import Home from './containers/Home' 2 | import About from './components/About' 3 | import User from './components/User' 4 | 5 | const routes = [ 6 | { path: '/', 7 | exact: true, 8 | component: Home 9 | }, 10 | 11 | { path: '/about', 12 | component: About 13 | }, 14 | 15 | { path: '/users/:name', 16 | component: User 17 | } 18 | ] 19 | 20 | export default routes 21 | -------------------------------------------------------------------------------- /src/shared/theme.js: -------------------------------------------------------------------------------- 1 | export const DEFAULT_FONT = `'Noto Serif', -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif` 2 | --------------------------------------------------------------------------------