├── .gitattributes ├── config ├── development.json ├── production.json ├── default.json └── production.js ├── gulpfile.js ├── .babelrc ├── .travis.yml ├── client ├── images │ ├── jchip-128.jpg │ └── nature-600-337.jpg ├── styles │ └── base.css ├── routes.jsx ├── components │ ├── home.jsx │ ├── AppBarExampleIconMenu.jsx │ ├── CardExampleWithAvatar.jsx │ └── BottomNavigationExampleSimple.jsx └── app.jsx ├── .editorconfig ├── server ├── views │ └── index-view.jsx ├── plugins │ └── webapp │ │ ├── index.html │ │ └── index.js └── index.js ├── test └── client │ └── components │ └── home.spec.jsx ├── LICENSE ├── package.json ├── .gitignore └── README.md /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | -------------------------------------------------------------------------------- /config/development.json: -------------------------------------------------------------------------------- 1 | { 2 | } 3 | -------------------------------------------------------------------------------- /config/production.json: -------------------------------------------------------------------------------- 1 | { 2 | } 3 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | require("electrode-archetype-react-app")(); 2 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./node_modules/electrode-archetype-react-app/config/babel/.babelrc" 3 | } 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - v6 4 | - v5 5 | - v4 6 | - '0.12' 7 | - '0.10' 8 | -------------------------------------------------------------------------------- /client/images/jchip-128.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/electrode-samples/electrode-react-sample-material-ui/HEAD/client/images/jchip-128.jpg -------------------------------------------------------------------------------- /client/images/nature-600-337.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/electrode-samples/electrode-react-sample-material-ui/HEAD/client/images/nature-600-337.jpg -------------------------------------------------------------------------------- /client/styles/base.css: -------------------------------------------------------------------------------- 1 | html { 2 | font-family: 'Roboto', sans-serif; 3 | } 4 | 5 | body { 6 | font-size: 13px; 7 | line-height: 20px; 8 | } 9 | -------------------------------------------------------------------------------- /client/routes.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import {Route} from "react-router"; 3 | import {Home} from "./components/home"; 4 | 5 | export const routes = ( 6 | 7 | ); 8 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | 10 | [*.md] 11 | trim_trailing_whitespace = false 12 | -------------------------------------------------------------------------------- /server/views/index-view.jsx: -------------------------------------------------------------------------------- 1 | import RouterResolverEngine from "electrode-router-resolver-engine"; 2 | import { routes } from "../../client/routes"; 3 | 4 | module.exports = (req) => { 5 | if (!req.server.app.routesEngine) { 6 | req.server.app.routesEngine = RouterResolverEngine(routes); 7 | } 8 | 9 | return req.server.app.routesEngine(req); 10 | }; 11 | -------------------------------------------------------------------------------- /config/default.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": { 3 | "inert": { 4 | "enable": true 5 | }, 6 | "electrodeStaticPaths": { 7 | "enable": true, 8 | "options": { 9 | "pathPrefix": "dist" 10 | } 11 | }, 12 | "webapp": { 13 | "module": "./server/plugins/webapp", 14 | "options": { 15 | "pageTitle": "Electrode React Sample App with material-ui", 16 | "paths": { 17 | "/{args*}": { 18 | "content": { 19 | "module": "./server/views/index-view" 20 | } 21 | } 22 | } 23 | } 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /test/client/components/home.spec.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import { Home } from "client/components/home"; 4 | 5 | describe("Home", () => { 6 | let component; 7 | let container; 8 | 9 | beforeEach(() => { 10 | container = document.createElement("div"); 11 | }); 12 | 13 | afterEach(() => { 14 | ReactDOM.unmountComponentAtNode(container); 15 | }); 16 | 17 | it("has expected content with deep render", () => { 18 | component = ReactDOM.render( 19 | , 20 | container 21 | ); 22 | 23 | expect(component).to.not.be.false; 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2016 Joel Chen (https://github.com/jchip) 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /client/components/home.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import MuiThemeProvider from "material-ui/styles/MuiThemeProvider"; 3 | import AppBarExampleIconMenu from "./AppBarExampleIconMenu"; 4 | import BottomNavigationExampleSimple from "./BottomNavigationExampleSimple"; 5 | import CardExampleWithAvatar from "./CardExampleWithAvatar"; 6 | 7 | export class Home extends React.Component { 8 | render() { 9 | return ( 10 | 11 |
12 | 13 | 14 | 15 |
16 |
17 | ); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /client/app.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import {routes} from "./routes"; 3 | import {Router} from "react-router"; 4 | import {Resolver} from "react-resolver"; 5 | import "./styles/base.css"; 6 | import injectTapEventPlugin from "react-tap-event-plugin"; 7 | 8 | // 9 | // Add the client app start up code to a function as window.webappStart. 10 | // The webapp's full HTML will check and call it once the js-content 11 | // DOM is created. 12 | // 13 | 14 | window.webappStart = () => { 15 | injectTapEventPlugin(); // https://github.com/callemall/material-ui/issues/4670 16 | 17 | Resolver.render( 18 | () => {routes}, 19 | document.querySelector(".js-content") 20 | 21 | ); 22 | }; 23 | -------------------------------------------------------------------------------- /config/production.js: -------------------------------------------------------------------------------- 1 | // 2 | // This file is here to allow enabling the plugins inert and electrodeStaticPaths, overriding the 3 | // settings in production.json, in order to serve the static JS and CSS bundle files from 4 | // the dist directory so you can test your app server locally in production mode. 5 | // 6 | // When running in a real production environment where your static files are most likely served 7 | // by a dedicated CDN server, you might want to turn these plugins off. 8 | // 9 | 10 | const serveStaticFiles = () => { 11 | return process.env.STATIC_FILES_OFF !== "true"; 12 | }; 13 | 14 | module.exports = { 15 | "plugins": { 16 | "inert": { 17 | "enable": serveStaticFiles() 18 | }, 19 | "electrodeStaticPaths": { 20 | "enable": serveStaticFiles() 21 | } 22 | } 23 | }; 24 | -------------------------------------------------------------------------------- /server/plugins/webapp/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {{PAGE_TITLE}} 6 | {{PREFETCH_BUNDLES}} 7 | {{WEBAPP_BUNDLES}} 8 | 9 | 10 |
{{SSR_CONTENT}}
11 | 12 | 13 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /client/components/AppBarExampleIconMenu.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import AppBar from "material-ui/AppBar"; 3 | import IconButton from "material-ui/IconButton"; 4 | import IconMenu from "material-ui/IconMenu"; 5 | import MenuItem from "material-ui/MenuItem"; 6 | import MoreVertIcon from "material-ui/svg-icons/navigation/more-vert"; 7 | import NavigationClose from "material-ui/svg-icons/navigation/close"; 8 | 9 | const AppBarExampleIconMenu = () => ( 10 | } 13 | iconElementRight={ 14 | 17 | } 18 | targetOrigin={{horizontal: "right", vertical: "top"}} 19 | anchorOrigin={{horizontal: "right", vertical: "top"}} 20 | > 21 | 22 | 23 | 24 | 25 | } 26 | /> 27 | ); 28 | 29 | export default AppBarExampleIconMenu; 30 | -------------------------------------------------------------------------------- /client/components/CardExampleWithAvatar.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Card, CardActions, CardHeader, CardMedia, CardTitle, CardText} from 'material-ui/Card'; 3 | import FlatButton from 'material-ui/FlatButton'; 4 | import natureJpg from "../images/nature-600-337.jpg"; 5 | import avatarJpg from "../images/jchip-128.jpg"; 6 | 7 | const CardExampleWithAvatar = () => ( 8 | 9 | 14 | } 16 | > 17 | 18 | 19 | 20 | 21 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. 22 | Donec mattis pretium massa. Aliquam erat volutpat. Nulla facilisi. 23 | Donec vulputate interdum sollicitudin. Nunc lacinia auctor quam sed pellentesque. 24 | Aliquam dui mauris, mattis quis lacus id, pellentesque lobortis odio. 25 | 26 | 27 | 28 | 29 | 30 | 31 | ); 32 | 33 | export default CardExampleWithAvatar; 34 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "electrode-react-sample-material-ui", 3 | "version": "0.0.0", 4 | "description": "Electrode Universal React Sample App with material-ui components", 5 | "homepage": "http://electrode.io", 6 | "author": { 7 | "name": "Joel Chen", 8 | "email": "xchen@walmartlabs.com", 9 | "url": "https://github.com/jchip" 10 | }, 11 | "contributors": [], 12 | "files": [ 13 | "lib" 14 | ], 15 | "main": "lib/index.js", 16 | "keywords": [ 17 | "Electrode", 18 | "Universal", 19 | "React", 20 | "Sample", 21 | "material-ui" 22 | ], 23 | "repository": { 24 | "type": "git", 25 | "url": "jchip/electrode-react-sample-material-ui" 26 | }, 27 | "license": "Apache-2.0", 28 | "scripts": { 29 | "start": "gulp dev", 30 | "test": "gulp test", 31 | "coverage": "gulp check" 32 | }, 33 | "dependencies": { 34 | "bluebird": "^3.4.6", 35 | "electrode-archetype-react-app": "^1.4.1", 36 | "electrode-confippet": "^1.0.0", 37 | "electrode-router-resolver-engine": "^1.0.0", 38 | "electrode-server": "^1.0.0", 39 | "electrode-static-paths": "^1.0.0", 40 | "lodash": "^4.10.1", 41 | "material-ui": "^0.16.4", 42 | "react-tap-event-plugin": "^2.0.0" 43 | }, 44 | "devDependencies": { 45 | "electrode-archetype-react-app-dev": "^1.4.1", 46 | "gulp": "^3.9.1" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /server/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | global.navigator = {userAgent: "all"}; 4 | 5 | process.on("SIGINT", () => { 6 | process.exit(0); 7 | }); 8 | 9 | const config = require("electrode-confippet").config; 10 | const staticPathsDecor = require("electrode-static-paths"); 11 | const supports = require("electrode-archetype-react-app/supports"); 12 | 13 | /** 14 | * Use babel register to transpile any JSX code on the fly to run 15 | * in server mode, and also transpile react code to apply process.env.NODE_ENV 16 | * removal to improve performance in production mode. 17 | */ 18 | supports.babelRegister({ 19 | ignore: /node_modules\/(?!react\/)/ 20 | }); 21 | 22 | /** 23 | * css-modules-require-hook: handle css-modules on node.js server. 24 | * similar to Babel's babel/register it compiles CSS modules in runtime. 25 | * 26 | * generateScopedName - Short alias for the postcss-modules-scope plugin's option. 27 | * Helps you to specify the custom way to build generic names for the class selectors. 28 | * You may also use a string pattern similar to the webpack's css-loader. 29 | * 30 | * https://github.com/css-modules/css-modules-require-hook#generatescopedname-function 31 | * https://github.com/webpack/css-loader#local-scope 32 | * https://github.com/css-modules/postcss-modules-scope 33 | */ 34 | supports.cssModuleHook({ 35 | generateScopedName: "[name]__[local]___[hash:base64:5]" 36 | }); 37 | 38 | supports.isomorphicExtendRequire().then(() => { 39 | require("electrode-server")(config, [staticPathsDecor()]); 40 | }); 41 | -------------------------------------------------------------------------------- /client/components/BottomNavigationExampleSimple.jsx: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import FontIcon from 'material-ui/FontIcon'; 3 | import {BottomNavigation, BottomNavigationItem} from 'material-ui/BottomNavigation'; 4 | import Paper from 'material-ui/Paper'; 5 | import IconLocationOn from 'material-ui/svg-icons/communication/location-on'; 6 | 7 | const recentsIcon = restore; 8 | const favoritesIcon = favorite; 9 | const nearbyIcon = ; 10 | 11 | /** 12 | * A simple example of `BottomNavigation`, with three labels and icons 13 | * provided. The selected `BottomNavigationItem` is determined by application 14 | * state (for instance, by the URL). 15 | */ 16 | class BottomNavigationExampleSimple extends Component { 17 | constructor() { 18 | super(); 19 | this.state = { 20 | selectedIndex: 0 21 | }; 22 | } 23 | 24 | select(index) { 25 | return this.setState({selectedIndex: index}); 26 | } 27 | 28 | render() { 29 | return ( 30 | 31 | 32 | this.select(0)} 36 | /> 37 | this.select(1)} 41 | /> 42 | this.select(2)} 46 | /> 47 | 48 | 49 | ); 50 | } 51 | } 52 | 53 | export default BottomNavigationExampleSimple; 54 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Created by https://www.gitignore.io/api/gitbook,node,webstorm,bower,osx 4 | 5 | ### GitBook ### 6 | # Node rules: 7 | ## Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 8 | .grunt 9 | 10 | ## Dependency directory 11 | ## Commenting this out is preferred by some people, see 12 | ## https://docs.npmjs.com/misc/faq#should-i-check-my-node_modules-folder-into-git 13 | node_modules 14 | 15 | # Book build output 16 | _book 17 | 18 | # eBook build output 19 | *.epub 20 | *.mobi 21 | *.pdf 22 | 23 | 24 | ### Node ### 25 | # Logs 26 | logs 27 | *.log 28 | npm-debug.log* 29 | 30 | # Runtime data 31 | pids 32 | *.pid 33 | *.seed 34 | 35 | # Directory for instrumented libs generated by jscoverage/JSCover 36 | lib-cov 37 | 38 | # Coverage directory used by tools like istanbul 39 | coverage 40 | 41 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 42 | .grunt 43 | 44 | # node-waf configuration 45 | .lock-wscript 46 | 47 | # Compiled binary addons (http://nodejs.org/api/addons.html) 48 | build/Release 49 | 50 | # Dependency directory 51 | # https://docs.npmjs.com/misc/faq#should-i-check-my-node-modules-folder-into-git 52 | node_modules 53 | 54 | # Optional npm cache directory 55 | .npm 56 | 57 | # Optional REPL history 58 | .node_repl_history 59 | 60 | 61 | ### WebStorm ### 62 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 63 | 64 | *.iml 65 | 66 | ## Directory-based project format: 67 | .idea/ 68 | # if you remove the above rule, at least ignore the following: 69 | 70 | # User-specific stuff: 71 | # .idea/workspace.xml 72 | # .idea/tasks.xml 73 | # .idea/dictionaries 74 | # .idea/shelf 75 | 76 | # Sensitive or high-churn files: 77 | # .idea/dataSources.ids 78 | # .idea/dataSources.xml 79 | # .idea/sqlDataSources.xml 80 | # .idea/dynamic.xml 81 | # .idea/uiDesigner.xml 82 | 83 | # Gradle: 84 | # .idea/gradle.xml 85 | # .idea/libraries 86 | 87 | # Mongo Explorer plugin: 88 | # .idea/mongoSettings.xml 89 | 90 | ## File-based project format: 91 | *.ipr 92 | *.iws 93 | 94 | ## Plugin-specific files: 95 | 96 | # IntelliJ 97 | /out/ 98 | 99 | # mpeltonen/sbt-idea plugin 100 | .idea_modules/ 101 | 102 | # JIRA plugin 103 | atlassian-ide-plugin.xml 104 | 105 | # Crashlytics plugin (for Android Studio and IntelliJ) 106 | com_crashlytics_export_strings.xml 107 | crashlytics.properties 108 | crashlytics-build.properties 109 | fabric.properties 110 | 111 | 112 | ### Bower ### 113 | bower_components 114 | .bower-cache 115 | .bower-registry 116 | .bower-tmp 117 | 118 | 119 | ### OSX ### 120 | .DS_Store 121 | .AppleDouble 122 | .LSOverride 123 | 124 | # Icon must end with two \r 125 | Icon 126 | 127 | 128 | # Thumbnails 129 | ._* 130 | 131 | # Files that might appear in the root of a volume 132 | .DocumentRevisions-V100 133 | .fseventsd 134 | .Spotlight-V100 135 | .TemporaryItems 136 | .Trashes 137 | .VolumeIcon.icns 138 | 139 | # Directories potentially created on remote AFP share 140 | .AppleDB 141 | .AppleDesktop 142 | Network Trash Folder 143 | Temporary Items 144 | .apdisk 145 | 146 | # others 147 | .hg 148 | .project 149 | .tmp 150 | 151 | # Build 152 | dist 153 | Procfile 154 | config/assets.json 155 | npm-shrinkwrap.json 156 | 157 | .isomorphic-loader-config.json 158 | -------------------------------------------------------------------------------- /server/plugins/webapp/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const _ = require("lodash"); 4 | const Promise = require("bluebird"); 5 | const fs = require("fs"); 6 | const Path = require("path"); 7 | const assert = require("assert"); 8 | 9 | const HTTP_ERROR_500 = 500; 10 | const HTTP_REDIRECT = 302; 11 | 12 | /** 13 | * Load stats.json which is created during build. 14 | * The file contains bundle files which are to be loaded on the client side. 15 | * 16 | * @param {string} statsFilePath - path of stats.json 17 | * @returns {Promise.} an object containing an array of file names 18 | */ 19 | function loadAssetsFromStats(statsFilePath) { 20 | return Promise.resolve(Path.resolve(statsFilePath)) 21 | .then(require) 22 | .then((stats) => { 23 | const assets = {}; 24 | _.each(stats.assetsByChunkName.main, (v) => { 25 | if (v.endsWith(".js")) { 26 | assets.js = v; 27 | } else if (v.endsWith(".css")) { 28 | assets.css = v; 29 | } 30 | }); 31 | return assets; 32 | }) 33 | .catch(() => ({})); 34 | } 35 | 36 | function makeRouteHandler(options, userContent) { 37 | const CONTENT_MARKER = "{{SSR_CONTENT}}"; 38 | const BUNDLE_MARKER = "{{WEBAPP_BUNDLES}}"; 39 | const TITLE_MARKER = "{{PAGE_TITLE}}"; 40 | const PREFETCH_MARKER = "{{PREFETCH_BUNDLES}}"; 41 | const WEBPACK_DEV = options.webpackDev; 42 | const RENDER_JS = options.renderJS; 43 | const RENDER_SS = options.serverSideRendering; 44 | const html = fs.readFileSync(Path.join(__dirname, "index.html")).toString(); 45 | const assets = options.__internals.assets; 46 | const devJSBundle = options.__internals.devJSBundle; 47 | const devCSSBundle = options.__internals.devCSSBundle; 48 | 49 | /* Create a route handler */ 50 | return (request, reply) => { 51 | const mode = request.query.__mode || ""; 52 | const renderJs = RENDER_JS && mode !== "nojs"; 53 | const renderSs = RENDER_SS && mode !== "noss"; 54 | 55 | const bundleCss = () => { 56 | return WEBPACK_DEV ? devCSSBundle : assets.css && `/js/${assets.css}` || ""; 57 | }; 58 | 59 | const bundleJs = () => { 60 | if (!renderJs) { 61 | return ""; 62 | } 63 | return WEBPACK_DEV ? devJSBundle : assets.js && `/js/${assets.js}` || ""; 64 | }; 65 | 66 | const callUserContent = (content) => { 67 | const x = content(request); 68 | return !x.catch ? x : x.catch((err) => { 69 | return Promise.reject({ 70 | status: err.status || HTTP_ERROR_500, 71 | html: err.message || err.toString() 72 | }); 73 | }); 74 | }; 75 | 76 | const makeBundles = () => { 77 | const css = bundleCss(); 78 | const cssLink = css ? `` : ""; 79 | const js = bundleJs(); 80 | const jsLink = js ? `` : ""; 81 | return `${cssLink}${jsLink}`; 82 | }; 83 | 84 | const addPrefetch = (prefetch) => { 85 | return prefetch ? `` : ""; 86 | }; 87 | 88 | const renderPage = (content) => { 89 | return html.replace(/{{[A-Z_]*}}/g, (m) => { 90 | switch (m) { 91 | case CONTENT_MARKER: 92 | return content.html || ""; 93 | case TITLE_MARKER: 94 | return options.pageTitle; 95 | case BUNDLE_MARKER: 96 | return makeBundles(); 97 | case PREFETCH_MARKER: 98 | return addPrefetch(content.prefetch); 99 | default: 100 | return `Unknown marker ${m}`; 101 | } 102 | }); 103 | }; 104 | 105 | const renderSSRContent = (content) => { 106 | const p = _.isFunction(content) ? 107 | callUserContent(content) : 108 | Promise.resolve(_.isObject(content) ? content : {html: content}); 109 | return p.then((c) => renderPage(c)); 110 | }; 111 | 112 | const handleStatus = (data) => { 113 | const status = data.status; 114 | if (status === HTTP_REDIRECT) { 115 | reply.redirect(data.path); 116 | } else { 117 | reply({message: "error"}).code(status); 118 | } 119 | }; 120 | 121 | const doRender = () => { 122 | return renderSs ? renderSSRContent(userContent) : renderPage(""); 123 | }; 124 | 125 | Promise.try(doRender) 126 | .then((data) => { 127 | return data.status ? handleStatus(data) : reply(data); 128 | }) 129 | .catch((err) => { 130 | reply(err.html).code(err.status || HTTP_ERROR_500); 131 | }); 132 | }; 133 | } 134 | 135 | const registerRoutes = (server, options, next) => { 136 | 137 | const pluginOptionsDefaults = { 138 | pageTitle: "Untitled Electrode Web Application", 139 | webpackDev: process.env.WEBPACK_DEV === "true", 140 | renderJS: true, 141 | serverSideRendering: true, 142 | devServer: { 143 | host: "127.0.0.1", 144 | port: "2992" 145 | }, 146 | paths: {}, 147 | stats: "dist/server/stats.json" 148 | }; 149 | 150 | const resolveContent = (content) => { 151 | if (!_.isString(content) && !_.isFunction(content) && content.module) { 152 | const module = content.module.startsWith(".") ? Path.join(process.cwd(), content.module) : content.module; // eslint-disable-line 153 | return require(module); // eslint-disable-line 154 | } 155 | 156 | return content; 157 | }; 158 | 159 | const pluginOptions = _.defaultsDeep({}, options, pluginOptionsDefaults); 160 | 161 | return Promise.try(() => loadAssetsFromStats(pluginOptions.stats)) 162 | .then((assets) => { 163 | const devServer = pluginOptions.devServer; 164 | pluginOptions.__internals = { 165 | assets, 166 | devJSBundle: `http://${devServer.host}:${devServer.port}/js/bundle.dev.js`, 167 | devCSSBundle: `http://${devServer.host}:${devServer.port}/js/style.css` 168 | }; 169 | 170 | _.each(options.paths, (v, path) => { 171 | assert(v.content, `You must define content for the webapp plugin path ${path}`); 172 | server.route({ 173 | method: "GET", 174 | path, 175 | config: v.config || {}, 176 | handler: makeRouteHandler(pluginOptions, resolveContent(v.content)) 177 | }); 178 | }); 179 | next(); 180 | }) 181 | .catch(next); 182 | }; 183 | 184 | registerRoutes.attributes = { 185 | pkg: { 186 | name: "webapp", 187 | version: "1.0.0" 188 | } 189 | }; 190 | 191 | module.exports = registerRoutes; 192 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # electrode-react-sample-material-ui 2 | 3 | # Deprecated 4 | 5 | This repo has been moved [here](https://github.com/electrode-io/electrode/tree/c31adbfb247da790b0773f1b16c1d73e9b2a2538/samples/universal-material-ui) 6 | 7 | [![NPM version][npm-image]][npm-url] [![Build Status][travis-image]][travis-url] [![Dependency Status][daviddm-image]][daviddm-url] 8 | 9 | > Electrode Universal React Sample App with [material-ui] components 10 | 11 | ![screenshot][screenshot] 12 | 13 | ## Installation 14 | 15 | ### Prerequisites 16 | 17 | Make sure you have installed NodeJS >= 4.x and npm >= 3.x, and [gulp-cli]. 18 | 19 | ```bash 20 | $ node -v 21 | v6.6.0 22 | $ npm -v 23 | 3.10.3 24 | $ npm install -g gulp-cli 25 | ``` 26 | 27 | ### Check it out 28 | 29 | To try out this ready made sample app, use git to clone the repo: 30 | 31 | ```sh 32 | $ git clone https://github.com/electrode-io/electrode-react-sample-material-ui.git 33 | $ cd electrode-react-sample-material-ui 34 | $ npm install 35 | $ gulp dev 36 | ``` 37 | 38 | Now navigate your browser to `http://localhost:3000` to see the sample app with [material-ui] components. 39 | 40 | ## About 41 | 42 | This app was created with the following steps. 43 | 44 | 45 | ### Generate Electrode App 46 | 47 | First part of the process is to generate an Electrode Universal App using the [yeoman] generator. Follow the steps below: 48 | 49 | 1. First generate the Electrode Universal App with the following commands: 50 | 51 | ```bash 52 | $ npm install -g yo generator-electrode 53 | $ mkdir electrode-react-sample-material-ui 54 | $ cd electrode-react-sample-material-ui 55 | $ yo electrode 56 | # ... answer questions and wait for app to be generated and npm install completed ... 57 | ``` 58 | 59 | 2. Run `gulp dev` in the newly generated app 60 | 3. Navigate to `http://localhost:3000` to make sure app is working. 61 | 62 | ### Add [material-ui] 63 | 64 | Second part of the process is to add [material-ui] dependencies. Follow the steps below: 65 | 66 | 1. Stop the app and install [material-ui] dependencies 67 | 68 | ```bash 69 | $ npm install material-ui react-tap-event-plugin --save 70 | ``` 71 | 72 | 1. Restart `gulp dev` and reload browser to make sure things are still working. 73 | 1. Add [material-ui]'s required font *Roboto* to `server/plugins/webapp/index.html` 74 | 1. Update `client/styles/base.css` with styles for [material-ui]. 75 | 1. Test [material-ui] component by adding a [RaisedButton] to `client/components/home.jsx` 76 | 1. Watch [webpack-dev-server] update your bundle and refresh browser to see changes. 77 | 1. Add `global.navigator.userAgent` to `server/index.js` as required by [material-ui] for [Server Rendering]. 78 | 1. Watch [webpack-dev-server] update your bundle and refresh browser to see changes. 79 | 80 | ### Add [material-ui] Examples 81 | 82 | Now we are ready to add some of the [material-ui examples] to the app. 83 | 84 | > Note that the examples are written with babel stage-1 which is not supported in Electrode so you might have to rewrite some of them. 85 | 86 | #### Enable tapping 87 | 88 | First we have to add the resolution for this issue https://github.com/callemall/material-ui/issues/4670. 89 | 90 | Add the following code to `client/app.jsx` 91 | 92 | ```js 93 | import injectTapEventPlugin from "react-tap-event-plugin"; 94 | 95 | window.webappStart = () => { 96 | injectTapEventPlugin(); // https://github.com/callemall/material-ui/issues/4670 97 | 98 | }; 99 | ``` 100 | 101 | #### IconMenu [AppBar example] 102 | 103 | First add the IconMenu [AppBar example] by following the steps below. 104 | 105 | 1. Copy the source from the example into a file `client/components/AppBarExampleIconMenu.jsx` 106 | 2. Replace the `Hello Electrode` and the RaisedButton content in `client/components/home.jsx` with ``; 107 | 3. Watch [webpack-dev-server] update your bundle and refresh browser to see changes. 108 | 4. If the AppBar shows up, click on the right menu button, you should see a menu pops up. 109 | 110 | #### [BottomNavigation example] 111 | 112 | Next add the [BottomNavigation example] 113 | 114 | 1. Copy the source from the example into a file `client/components/BottomNavigationExampleSimple.jsx` 115 | 2. Import the component in `client/components/home.jsx` and add it to `render` after the `AppBarExampleIconMenu` component. 116 | 3. Watch [webpack-dev-server] update your bundle and refresh browser to see changes. 117 | 4. You should see AppBar and BottomNavigation show up. You should be able to interact with the buttons on the BottomNavigation component. 118 | 119 | #### [Card example] 120 | 121 | In this section we add the [Card example]. 122 | 123 | 1. Copy the source from the [Card example] into a file `client/components/CardExampleWithAvatar.jsx` 124 | 2. Import the component in `client/components/home.jsx` and add it to `render` after the `AppBarExampleIconMenu` component. 125 | 3. Watch [webpack-dev-server] update your bundle and refresh browser to see changes. 126 | 4. You should see Card show up but with broken images 127 | 128 | > You can replace the image URLs with the full URLs to the images by adding `http://www.material-ui.com/` to them to fix the broken images, but we will explore isomorphic images next. 129 | 130 | #### Isomorphic Images 131 | 132 | Electrode core comes with isomorphic images support built in using [isomorphic-loader]. In this section we explore using that feature to load the images for the [Card example]. 133 | 134 | 1. Create a directory `client/images` and copy the following images there 135 | - http://www.material-ui.com/images/nature-600-337.jpg 136 | - http://www.material-ui.com/images/jsa-128.jpg (Or your own favorite 128x128 Avatar image) 137 | - Note that in my sample I use `jchip-128.jpg` as my avatar. 138 | 1. In `client/components/CardExampleWithAvatar.jsx`, import the images: 139 | 140 | ```js 141 | import natureJpg from "../images/nature-600-337.jpg"; 142 | import avatarJpg from "../images/jsa-128.jpg"; 143 | ``` 144 | 145 | 1. Replace the URLs for `avatar` and `CarMedia` img `src`, as follows: 146 | 147 | ``` 148 | ... 149 | avatar={avatarJpg} 150 | ... 151 | src={natureJpg} 152 | ``` 153 | 154 | 1. In `server/index.js`, activate [isomorphic-loader]'s `extend-require` by changing the last line to: 155 | 156 | ```js 157 | supports.isomorphicExtendRequire().then(() => { 158 | require("electrode-server")(config, [staticPathsDecor()]); 159 | }); 160 | ``` 161 | 162 | 1. Watch [webpack-dev-server] update your bundle and refresh browser to see changes. 163 | 164 | > Note that you will see the message `Warning: Unknown prop onTouchTap on