├── .gitignore ├── README.md ├── app ├── alt.js ├── components │ ├── App.jsx │ ├── Element.jsx │ ├── Home.jsx │ ├── Home.less │ ├── IsotopeResponseRenderer.jsx │ ├── NotFoundPage.jsx │ └── Spacer.jsx ├── flux │ ├── actions │ │ ├── ElementActions.js │ │ └── FilterSortActions.js │ └── stores │ │ ├── ElementStore.js │ │ └── FilterSortStore.js ├── index.js ├── router.js ├── routes.jsx └── utils.js ├── config └── loadersByExtension.js ├── make-webpack-config.js ├── package.json ├── src ├── server │ ├── api.js │ ├── index.js │ ├── server-development.js │ ├── server-production.js │ ├── settings.js │ └── views │ │ └── index.ejs └── utils │ └── logger.js ├── webpack-dev-server.config.js ├── webpack-hot-dev-server.config.js ├── webpack-production.config.js └── webpack.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | bower_components/ 3 | npm-debug.log 4 | logs/* 5 | 6 | extensions/chrome/*!manifest.json 7 | extensions/firefox/*!package.json 8 | 9 | ### WebStorm ### 10 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm 11 | 12 | ## Directory-based project format: 13 | .idea/ 14 | 15 | dist/ 16 | 17 | # Webpack output, including for prod, but that is build on prod push to ITOS 18 | build/public 19 | build/stats.json 20 | 21 | ## File-based project format: 22 | *.ipr 23 | *.iws 24 | 25 | # OS generated files # 26 | # ###################### 27 | .DS_Store 28 | .AppleDouble 29 | .LSOverride 30 | Icon 31 | 32 | # Thumbnails 33 | ._* 34 | 35 | # # Files that might appear on external disk 36 | .Spotlight-V100 37 | .Trashes 38 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Overview 2 | 3 | This webapp combines together React, Isotope, and Flux (Alt) so you can see one way of accomplishing this combination of libraries. This example is based on the blog post http://developerblog.redhat.com/2016/01/07/react-js-with-isotope-and-flux 4 | 5 | ![Demo Screen](https://cloud.githubusercontent.com/assets/2019830/12128230/eae6b438-b3c9-11e5-8531-f0e7a339403e.png) 6 | 7 | ## Local Installation and development 8 | 9 | ```bash 10 | npm install 11 | 12 | # In one console 13 | npm run dev-server 14 | 15 | # In another console 16 | npm run start-dev 17 | 18 | # Navigate to http://localhost:8080 19 | ``` 20 | 21 | ## License 22 | 23 | MIT (http://www.opensource.org/licenses/mit-license.php) 24 | -------------------------------------------------------------------------------- /app/alt.js: -------------------------------------------------------------------------------- 1 | import Alt from 'alt/lib/index'; 2 | export default new Alt(); 3 | -------------------------------------------------------------------------------- /app/components/App.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { RouteHandler } from "react-router"; 3 | 4 | export default class App extends React.Component { 5 | render() { 6 | return ; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /app/components/Element.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import shallowEqual from "react-pure-render/shallowEqual" 3 | 4 | export default class Element extends React.Component { 5 | static propTypes = { 6 | element: React.PropTypes.shape({ 7 | classes: React.PropTypes.array, 8 | category: React.PropTypes.string, 9 | name: React.PropTypes.string, 10 | symbol: React.PropTypes.string, 11 | number: React.PropTypes.number, 12 | weight: React.PropTypes.number 13 | }) 14 | }; 15 | shouldComponentUpdate(nextProps, nextState) { 16 | return !shallowEqual(this.props, nextProps); 17 | } 18 | render() { 19 | // Setting the id is so we can quickly lookup an element to tell Isotope to add/remove it 20 | return ( 21 |
24 |

{this.props.element.name}

25 |

{this.props.element.symbol}

26 |

{this.props.element.number}

27 |

{this.props.element.weight}

28 |
29 | ); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /app/components/Home.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import shallowEqual from "react-pure-render/shallowEqual" 3 | import { Grid, Button, ButtonGroup, Alert } from "react-bootstrap"; 4 | 5 | import IsotopeResponseRenderer from "./IsotopeResponseRenderer.jsx"; 6 | import Element from "./Element.jsx"; 7 | import Spacer from "./Spacer"; 8 | 9 | // Flux 10 | import ElementActions from "../flux/actions/ElementActions"; 11 | import ElementStore from "../flux/stores/ElementStore"; 12 | import FilterSortActions from "../flux/actions/FilterSortActions"; 13 | import FilterSortStore from "../flux/stores/FilterSortStore"; 14 | import connectToStores from 'alt/utils/connectToStores'; 15 | 16 | // The Home.less contains the CSS as copied from the Isotope example @ http://codepen.io/desandro/pen/nFrte 17 | require("./Home.less"); 18 | 19 | // Define data to drive the UI and Isotope. This could/should be placed in a separate module. 20 | const loadData = [ 21 | {name: 'all', value: 'all'}, 22 | {name: 'metal', value: 'metal'}, 23 | {name: '-iums', value: 'iums'} 24 | ]; 25 | const sortData = [ 26 | {name: 'original order', value: ''}, 27 | {name: 'name', value: 'name'}, 28 | {name: 'symbol', value: 'symbol'}, 29 | {name: 'number', value: 'number'}, 30 | {name: 'weight', value: 'weight'}, 31 | {name: 'category', value: 'category'} 32 | ]; 33 | const filterData = [ 34 | {name: 'show all', value: '*'}, 35 | {name: 'metal', value: '.metal'}, 36 | {name: 'transition', value: '.transition'}, 37 | {name: 'alkali and alkaline-earth', value: '.alkali, .alkaline-earth'}, 38 | {name: 'not transition', value: ':not(.transition)'}, 39 | {name: 'metal but not transition', value: '.metal:not(.transition)'}, 40 | {name: 'number > 50', value: 'numberGreaterThan50'}, 41 | {name: 'name ends with -ium', value: 'ium'} 42 | ]; 43 | 44 | @connectToStores 45 | export default class Home extends React.Component { 46 | constructor(props, context) { 47 | super(props, context); 48 | } 49 | static getStores() { 50 | return [ElementStore, FilterSortStore]; 51 | } 52 | static getPropsFromStores() { 53 | return _.assign(ElementStore.getState(), FilterSortStore.getState()); 54 | } 55 | shouldComponentUpdate(nextProps, nextState) { 56 | return !shallowEqual(this.props, nextProps) || !shallowEqual(this.state, nextState); 57 | } 58 | renderElements(elements) { 59 | return _.map(elements, e => ); 60 | } 61 | filterElements(filter, e) { 62 | FilterSortActions.filter(filter); 63 | } 64 | sortElements(sort, e) { 65 | FilterSortActions.sort(sort); 66 | } 67 | renderLoadButtons() { 68 | return _.map(loadData, d => , this) 69 | } 70 | renderFilterButtons() { 71 | return _.map(filterData, d => , this) 72 | } 73 | renderSortButtons() { 74 | return _.map(sortData, d => , this) 75 | } 76 | renderNoData() { 77 | if (_.get(this, 'props.elements.length', 0) > 0) return null; 78 | return Please load data by clicking on one of the buttons in the 'Ajax Load Elements' section. 79 | } 80 | render() { 81 | return ( 82 | 83 |

Isotope - filtering & sorting

84 |

First load elements, then filter and sort. Try loading different elements at any point.

85 | 86 | 87 | 88 |

Ajax Load Elements

89 | 90 | {this.renderLoadButtons()} 91 | 92 | 93 | 94 | 95 |

Filter

96 | 97 | {this.renderFilterButtons()} 98 | 99 | 100 |

Sort

101 | 102 | {this.renderSortButtons()} 103 | 104 | 105 | 106 | 107 | {this.renderNoData()} 108 | 109 | {this.renderElements(this.props.elements)} 110 | 111 |
112 | ); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /app/components/Home.less: -------------------------------------------------------------------------------- 1 | /* ---- isotope ---- */ 2 | 3 | .isotope { 4 | border: 1px solid #333; 5 | } 6 | 7 | /* clear fix */ 8 | .isotope:after { 9 | content: ''; 10 | display: block; 11 | clear: both; 12 | } 13 | 14 | /* ---- .element-item ---- */ 15 | 16 | .element-item { 17 | position: relative; 18 | float: left; 19 | width: 100px; 20 | height: 100px; 21 | margin: 5px; 22 | padding: 10px; 23 | background: #888; 24 | color: #262524; 25 | } 26 | 27 | .element-item-sizer { 28 | .element-item; 29 | opacity: 0; 30 | } 31 | 32 | .element-item > * { 33 | margin: 0; 34 | padding: 0; 35 | } 36 | 37 | .element-item .name { 38 | position: absolute; 39 | 40 | left: 10px; 41 | top: 60px; 42 | text-transform: none; 43 | letter-spacing: 0; 44 | font-size: 12px; 45 | font-weight: normal; 46 | } 47 | 48 | .element-item .symbol { 49 | position: absolute; 50 | left: 10px; 51 | top: 0px; 52 | font-size: 42px; 53 | font-weight: bold; 54 | color: white; 55 | } 56 | 57 | .element-item .number { 58 | position: absolute; 59 | right: 8px; 60 | top: 5px; 61 | } 62 | 63 | .element-item .weight { 64 | position: absolute; 65 | left: 10px; 66 | top: 76px; 67 | font-size: 12px; 68 | } 69 | 70 | .element-item.alkali { background: #F00; background: hsl( 0, 100%, 50%); } 71 | .element-item.alkaline-earth { background: #F80; background: hsl( 36, 100%, 50%); } 72 | .element-item.lanthanoid { background: #FF0; background: hsl( 72, 100%, 50%); } 73 | .element-item.actinoid { background: #0F0; background: hsl( 108, 100%, 50%); } 74 | .element-item.transition { background: #0F8; background: hsl( 144, 100%, 50%); } 75 | .element-item.post-transition { background: #0FF; background: hsl( 180, 100%, 50%); } 76 | .element-item.metalloid { background: #08F; background: hsl( 216, 100%, 50%); } 77 | .element-item.diatomic { background: #00F; background: hsl( 252, 100%, 50%); } 78 | .element-item.halogen { background: #F0F; background: hsl( 288, 100%, 50%); } 79 | .element-item.noble-gas { background: #F08; background: hsl( 324, 100%, 50%); } 80 | -------------------------------------------------------------------------------- /app/components/IsotopeResponseRenderer.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import shallowEqual from "react-pure-render/shallowEqual" 3 | 4 | // Flux 5 | import connectToStores from 'alt/utils/connectToStores'; 6 | import FilterSortActions from '../flux/actions/FilterSortActions'; 7 | import FilterSortStore from '../flux/stores/FilterSortStore'; 8 | 9 | @connectToStores 10 | export default class IsotopeResponseRenderer extends React.Component { 11 | // This class takes no attributes, pass in children of type Element 12 | static propTypes = {}; 13 | static getStores() { 14 | return [FilterSortStore]; 15 | } 16 | static getPropsFromStores() { 17 | return FilterSortStore.getState(); 18 | } 19 | constructor(props, context) { 20 | super(props, context); 21 | 22 | // Copied from http://codepen.io/desandro/pen/nFrte 23 | this.filterFns = { 24 | // show if number is greater than 50 25 | numberGreaterThan50: function () { 26 | var number = $(this).find('.number').text(); 27 | return parseInt( number, 10 ) > 50; 28 | }, 29 | // show if name ends with -ium 30 | ium: function () { 31 | var name = $(this).find('.name').text(); 32 | return name.match( /ium$/ ); 33 | } 34 | }; 35 | 36 | this.isoOptions = { 37 | itemSelector: '.element-item', 38 | layoutMode: 'masonry', 39 | masonry: { 40 | // Using a sizer element is necessary to prevent a vertical collapse between data loads 41 | // Ex. load all, then load metal, the metal will collapse into a vertical layout if this masonry: {} 42 | // section is commented out. 43 | columnWidth: '.element-item-sizer' 44 | }, 45 | //sortBy: 'name', // If you want to set the default sort, do that here. 46 | getSortData: { 47 | name: '.name', 48 | symbol: '.symbol', 49 | number: '.number parseInt', 50 | category: '[data-category]', 51 | weight: function( itemElem ) { 52 | var weight = $( itemElem ).find('.weight').text(); 53 | return parseFloat( weight.replace( /[\(\)]/g, '') ); 54 | } 55 | } 56 | }; 57 | } 58 | shouldComponentUpdate(nextProps, nextState) { 59 | return !shallowEqual(this.props, nextProps) || !shallowEqual(this.state, nextState); 60 | } 61 | // Filter and sort are coming from the Parent. 62 | componentWillReceiveProps(nextProps) { 63 | if (nextProps.filter && !_.isEqual(nextProps.filter, this.props.filter)) { 64 | this.iso.arrange({ filter: this.filterFns[nextProps.filter] || nextProps.filter }); 65 | } 66 | if (nextProps.sort != null) { 67 | this.iso.arrange({sortBy: nextProps.sort}); 68 | } 69 | } 70 | componentDidMount() { 71 | this.createIsotopeContainer(); 72 | 73 | // Only arrange if there are elements to arrange 74 | if (_.get(this, 'props.children.length', 0) > 0) { 75 | this.iso.arrange(); 76 | } 77 | } 78 | componentDidUpdate(prevProps) { 79 | // The list of keys seen in the previous render 80 | let currentKeys = _.map(prevProps.children, (n) => n.key); 81 | 82 | // The latest list of keys that have been rendered 83 | let newKeys = _.map(this.props.children, (n) => n.key); 84 | 85 | // Find which keys are new between the current set of keys and any new children passed to this component 86 | let addKeys = _.difference(newKeys, currentKeys); 87 | 88 | // Find which keys have been removed between the current set of keys and any new children passed to this component 89 | let removeKeys = _.difference(currentKeys, newKeys); 90 | 91 | if (removeKeys.length > 0) { 92 | _.each(removeKeys, removeKey => this.iso.remove(document.getElementById(removeKey))); 93 | this.iso.arrange(); 94 | } 95 | if (addKeys.length > 0) { 96 | this.iso.addItems(_.map(addKeys, (addKey) => document.getElementById(addKey))); 97 | this.iso.arrange(); 98 | } 99 | } 100 | componentWillUnmount() { 101 | if (this.iso != null) { 102 | this.iso.destroy(); 103 | } 104 | } 105 | createIsotopeContainer() { 106 | if (this.iso == null) { 107 | this.iso = new Isotope(React.findDOMNode(this.refs.isotopeContainer), this.isoOptions); 108 | } 109 | } 110 | render() { 111 | return
112 |
113 | {this.props.children} 114 |
115 | } 116 | } 117 | -------------------------------------------------------------------------------- /app/components/NotFoundPage.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export default class NotFoundPage extends React.Component { 4 | render() { 5 | return
6 |

Not found

7 |

The page you requested was not found.

8 |
9 | } 10 | } 11 | -------------------------------------------------------------------------------- /app/components/Spacer.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export default class TodoItem extends React.Component { 4 | shouldComponentUpdate(nextProps, nextState) { 5 | return false; 6 | } 7 | render() { 8 | return
9 | } 10 | } -------------------------------------------------------------------------------- /app/flux/actions/ElementActions.js: -------------------------------------------------------------------------------- 1 | import alt from '../../alt'; 2 | import Uri from 'jsuri'; 3 | import Promise from "bluebird"; 4 | Promise.longStackTraces(); 5 | 6 | class QueryActions { 7 | constructor() { 8 | this.generateActions('elementResults'); 9 | this.generateActions('loading'); 10 | } 11 | loadElements(type) { 12 | let uri = new Uri(); 13 | uri.setPath('/api/elements'); 14 | uri.addQueryParam('type', type); 15 | this.actions.loading(true); 16 | Promise.resolve($.ajax({ 17 | url: uri.toString() 18 | })).then((response) => { 19 | this.actions.elementResults({ 20 | type: type, 21 | elements: response, 22 | loading: false, 23 | err: null 24 | }); 25 | }).catch( err => { 26 | console.error(err.stack || err.message || err); 27 | this.actions.elementResults({ 28 | type: type, 29 | elements: null, 30 | loading: false, 31 | err: err 32 | }); 33 | }); 34 | } 35 | } 36 | 37 | export default alt.createActions(QueryActions); -------------------------------------------------------------------------------- /app/flux/actions/FilterSortActions.js: -------------------------------------------------------------------------------- 1 | import alt from '../../alt'; 2 | 3 | class FilterSortActions { 4 | constructor() { 5 | this.generateActions('filter'); 6 | this.generateActions('sort'); 7 | } 8 | } 9 | 10 | export default alt.createActions(FilterSortActions); -------------------------------------------------------------------------------- /app/flux/stores/ElementStore.js: -------------------------------------------------------------------------------- 1 | import alt from "../../alt"; 2 | import { decorate, bind } from 'alt/utils/decorators' 3 | import QueryActions from "../actions/ElementActions"; 4 | 5 | @decorate(alt) 6 | class ElementStore { 7 | constructor() { 8 | this.state = { 9 | type: '', 10 | elements: [], 11 | loading: false, 12 | err: null 13 | } 14 | } 15 | @bind(QueryActions.loading) 16 | onLoading(loading) { 17 | this.setState({ loading: loading }) 18 | } 19 | 20 | @bind(QueryActions.elementResults) 21 | onElementResults(data) { 22 | this.setState({ 23 | type: data.type, 24 | elements: data.elements, 25 | loading: data.loading, 26 | err: data.err 27 | }) 28 | } 29 | 30 | } 31 | 32 | export default alt.createStore(ElementStore, 'ElementStore'); 33 | -------------------------------------------------------------------------------- /app/flux/stores/FilterSortStore.js: -------------------------------------------------------------------------------- 1 | var alt = require('../../alt'); 2 | import { decorate, bind } from 'alt/utils/decorators' 3 | var FilterSortActions = require('../actions/FilterSortActions'); 4 | 5 | @decorate(alt) 6 | class FilterSortStore { 7 | constructor() { 8 | this.object = null; 9 | this.state = { 10 | filter: '*', 11 | sort: '' 12 | } 13 | } 14 | 15 | @bind(FilterSortActions.filter) 16 | onFilter(filter) { 17 | this.setState({ 18 | filter: filter 19 | }) 20 | } 21 | 22 | @bind(FilterSortActions.sort) 23 | onSort(sort) { 24 | this.setState({ 25 | sort: sort 26 | }) 27 | } 28 | } 29 | 30 | export default alt.createStore(FilterSortStore, 'FilterSortStore'); 31 | -------------------------------------------------------------------------------- /app/index.js: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | var router = require('./router'); 3 | 4 | router.run(function (Handler, state) { 5 | React.render(( 6 | 7 | ), document.getElementById('content')); 8 | }); 9 | -------------------------------------------------------------------------------- /app/router.js: -------------------------------------------------------------------------------- 1 | let Router = require('react-router'); 2 | 3 | function location() { 4 | if (typeof window !== 'undefined') { 5 | return Router.HistoryLocation; 6 | } 7 | } 8 | 9 | module.exports = Router.create({ 10 | routes: require('./routes'), 11 | location: location() 12 | }); -------------------------------------------------------------------------------- /app/routes.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Route, NotFoundRoute, Redirect } from 'react-router'; 3 | 4 | import App from './components/App'; 5 | import Home from './components/Home'; 6 | import NotFoundPage from './components/NotFoundPage'; 7 | 8 | let browserPath = require("./utils.js").browserPath; 9 | 10 | module.exports = ( 11 | 12 | 13 | 14 | 15 | 16 | ); 17 | -------------------------------------------------------------------------------- /app/utils.js: -------------------------------------------------------------------------------- 1 | let browserPath = '/'; 2 | let packageJson = require("../package.json"); 3 | 4 | module.exports = { 5 | browserPath: browserPath, 6 | version: packageJson.version 7 | }; -------------------------------------------------------------------------------- /config/loadersByExtension.js: -------------------------------------------------------------------------------- 1 | function extsToRegExp(exts) { 2 | return new RegExp("\\.(" + exts.map(function(ext) { 3 | return ext.replace(/\./g, "\\."); 4 | }).join("|") + ")(\\?.*)?$"); 5 | } 6 | 7 | module.exports = function loadersByExtension(obj) { 8 | var loaders = []; 9 | Object.keys(obj).forEach(function(key) { 10 | var exts = key.split("|"); 11 | var value = obj[key]; 12 | var entry = { 13 | extensions: exts, 14 | test: extsToRegExp(exts) 15 | }; 16 | if(Array.isArray(value)) { 17 | entry.loaders = value; 18 | } else if(typeof value === "string") { 19 | entry.loader = value; 20 | } else { 21 | Object.keys(value).forEach(function(valueKey) { 22 | entry[valueKey] = value[valueKey]; 23 | }); 24 | } 25 | loaders.push(entry); 26 | }); 27 | return loaders; 28 | }; -------------------------------------------------------------------------------- /make-webpack-config.js: -------------------------------------------------------------------------------- 1 | var path = require("path"); 2 | var _ = require("lodash"); 3 | var webpack = require("webpack"); 4 | var ExtractTextPlugin = require("extract-text-webpack-plugin"); 5 | var StatsPlugin = require("stats-webpack-plugin"); 6 | var loadersByExtension = require("./config/loadersByExtension"); 7 | 8 | module.exports = function(options) { 9 | var entry = { 10 | main: "./app/index" 11 | // second: "./app/someOtherPage 12 | }; 13 | var loaders = { 14 | "jsx": options.hotComponents ? ["react-hot-loader", "babel-loader?stage=0"] : "babel-loader?stage=0", 15 | "js": { 16 | loader: "babel-loader?stage=0", 17 | include: path.join(__dirname, "app") 18 | }, 19 | "adoc": "raw-loader", 20 | "json": "json-loader", 21 | "coffee": "coffee-redux-loader", 22 | "json5": "json5-loader", 23 | "txt": "raw-loader", 24 | "png|jpg|jpeg|gif|svg": "url-loader?limit=10000", 25 | "woff|woff2": "url-loader?limit=100000", 26 | "ttf|eot": "file-loader", 27 | "wav|mp3": "file-loader", 28 | "html": "html-loader", 29 | "md|markdown": ["html-loader", "markdown-loader"] 30 | }; 31 | // This modular css is bizarre, it may make sense for hot reloading but it creates mangled names and you have 32 | // to import from the css file and reference the imported name.style. Futhermore styles don't just 33 | // apply given they are localized which makes editing the css in the browser a PITA. 34 | //var cssLoader = options.minimize ? "css-loader?module" : "css-loader?module&localIdentName=[path][name]---[local]---[hash:base64:5]"; 35 | var cssLoader = options.minimize ? "css-loader" : "css-loader?localIdentName=[path][name]---[local]---[hash:base64:5]"; 36 | //var cssLoader = "css-loader"; 37 | var stylesheetLoaders = { 38 | "css": cssLoader, 39 | "less": [cssLoader, "less-loader"], 40 | 41 | "scss|sass": [cssLoader, "sass-loader"] 42 | }; 43 | var additionalLoaders = [ 44 | // { test: /some-reg-exp$/, loader: "any-loader" } 45 | ]; 46 | var alias = { 47 | //alt: 'alt/lib/index' 48 | }; 49 | var aliasLoader = { 50 | 51 | }; 52 | //var externals = [ 53 | // 54 | //]; 55 | var externals = { 56 | // require("jquery") is external and available 57 | // on the global var jQuery 58 | "jquery": "jQuery" 59 | }; 60 | var modulesDirectories = ["web_modules", "node_modules"]; 61 | var extensions = ["", ".web.js", ".js", ".jsx"]; 62 | var root = path.join(__dirname, "app"); 63 | var publicPath = options.devServer ? 64 | "http://localhost:2992/_assets/" : 65 | "/_assets/"; 66 | var output = { 67 | path: options.outputPath || path.join(__dirname, "build", "public"), 68 | publicPath: publicPath, 69 | filename: "[name].js" + (options.longTermCaching ? "?[chunkhash]" : ""), 70 | chunkFilename: (options.devServer ? "[id].js" : "[name].js") + (options.longTermCaching ? "?[chunkhash]" : ""), 71 | sourceMapFilename: "debugging/[file].map", 72 | libraryTarget: undefined, 73 | pathinfo: options.debug 74 | }; 75 | // Excluding all node_module ouput will prevent a lot of spam in the webpack output 76 | var excludeFromStats = [ 77 | /webpack/, 78 | /node_modules/ 79 | ]; 80 | // http://stackoverflow.com/questions/23305599/webpack-provideplugin-vs-externals 81 | var plugins = [ 82 | new StatsPlugin("stats.json", { 83 | chunkModules: true, 84 | exclude: excludeFromStats 85 | }), 86 | new webpack.PrefetchPlugin("react"), 87 | new webpack.PrefetchPlugin("react/lib/ReactComponentBrowserEnvironment"), 88 | new webpack.ProvidePlugin({ 89 | $: 'jquery', 90 | _: 'lodash' 91 | }) 92 | 93 | ]; 94 | //plugins.push(new StatsPlugin(path.join(__dirname, "build", "stats.json"), { 95 | // https://github.com/webpack/docs/wiki/node.js-api 96 | //plugins.push(new StatsPlugin("stats.json", { 97 | // chunkModules: true, 98 | // exclude: excludeFromStats 99 | //})); 100 | if(options.commonsChunk) { 101 | plugins.push(new webpack.optimize.CommonsChunkPlugin("commons", "commons.js" + (options.longTermCaching ? "?[chunkhash]" : ""))); 102 | } 103 | 104 | Object.keys(stylesheetLoaders).forEach(function(ext) { 105 | var stylesheetLoader = stylesheetLoaders[ext]; 106 | if(Array.isArray(stylesheetLoader)) stylesheetLoader = stylesheetLoader.join("!"); 107 | if(options.separateStylesheet) { 108 | stylesheetLoaders[ext] = ExtractTextPlugin.extract("style-loader", stylesheetLoader); 109 | } else { 110 | stylesheetLoaders[ext] = "style-loader!" + stylesheetLoader; 111 | } 112 | }); 113 | if(options.separateStylesheet) { 114 | plugins.push(new ExtractTextPlugin("[name].css" + (options.longTermCaching ? "?[contenthash]" : ""))); 115 | } 116 | if(options.minimize) { 117 | plugins.push( 118 | new webpack.optimize.UglifyJsPlugin({ 119 | compressor: { 120 | warnings: false 121 | } 122 | }), 123 | new webpack.optimize.DedupePlugin() 124 | ); 125 | } 126 | 127 | var defineOptions = { 128 | "ENV": JSON.stringify(options.env) 129 | }; 130 | 131 | if(options.minimize) { 132 | plugins.push( 133 | new webpack.DefinePlugin(_.defaults(defineOptions, { 134 | "process.env": { 135 | NODE_ENV: JSON.stringify("production") 136 | } 137 | })), 138 | new webpack.NoErrorsPlugin() 139 | ); 140 | } else { 141 | plugins.push( 142 | new webpack.DefinePlugin(defineOptions) 143 | ); 144 | } 145 | 146 | return { 147 | entry: entry, 148 | output: output, 149 | target: "web", 150 | module: { 151 | loaders: [].concat(loadersByExtension(loaders)).concat(loadersByExtension(stylesheetLoaders)).concat(additionalLoaders) 152 | }, 153 | devtool: options.devtool, 154 | debug: options.debug, 155 | resolveLoader: { 156 | root: path.join(__dirname, "node_modules"), 157 | alias: aliasLoader 158 | }, 159 | externals: externals, 160 | resolve: { 161 | root: root, 162 | modulesDirectories: modulesDirectories, 163 | extensions: extensions, 164 | alias: alias 165 | }, 166 | plugins: plugins, 167 | devServer: { 168 | stats: { 169 | cached: false, 170 | exclude: excludeFromStats 171 | } 172 | } 173 | }; 174 | }; 175 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-isotope", 3 | "license": "MIT", 4 | "author": "Samuel Mendenhall", 5 | "version": "0.0.1", 6 | "dependencies": { 7 | "less": "^2.5.3", 8 | "winston": "~1.0.2", 9 | "raw-loader": "~0.5.1", 10 | "file-loader": "^0.8.4", 11 | "babel-loader": "~5.3.2", 12 | "alt": "~0.17.3", 13 | "compression": "~1.5.2", 14 | "react-pure-render": "~1.0.2", 15 | "babel": "~5.8.23", 16 | "style-loader": "^0.12.4", 17 | "webpack": "~1.12.2", 18 | "babel-core": "~5.8.25", 19 | "path-is-absolute": "~1.0.0", 20 | "morgan": "~1.6.1", 21 | "ejs": "~2.3.4", 22 | "winston-mail": "~0.5.0", 23 | "react-bootstrap": "~0.25.2", 24 | "get-style-property": "~0.1.1", 25 | "less-loader": "^2.2.1", 26 | "cookie-parser": "~1.4.0", 27 | "loaders.css": "~0.1.1", 28 | "css-loader": "~0.19.0", 29 | "extract-text-webpack-plugin": "^0.8.2", 30 | "express": "~4.13.3", 31 | "react-loaders": "~1.2.3", 32 | "react": "^0.13.3", 33 | "react-hot-loader": "^1.3.0", 34 | "url-loader": "^0.5.6", 35 | "react-proxy-loader": "^0.3.4", 36 | "bluebird": "~2.10.1", 37 | "md5": "~2.0.0", 38 | "jsuri": "~1.3.1", 39 | "lodash": "~3.10.1", 40 | "isotope-layout": "~2.2.2", 41 | "json-loader": "~0.5.3", 42 | "request": "~2.64.0", 43 | "null-loader": "^0.1.1", 44 | "body-parser": "^1.14.1", 45 | "react-router": "~0.13.3", 46 | "classnames": "~2.1.3", 47 | "stats-webpack-plugin": "~0.2.1", 48 | "install": "~0.1.8", 49 | "async": "^1.4.2", 50 | "html-loader": "^0.3.0" 51 | }, 52 | "scripts": { 53 | "dev-server": "webpack-dev-server --config webpack-dev-server.config.js --progress --colors --port 2992 --inline", 54 | "start": "forever src/server/server-production.js", 55 | "hot-dev-server": "webpack-dev-server --config webpack-hot-dev-server.config.js --hot --progress --colors --port 2992 --inline", 56 | "build": "webpack --config webpack-production.config.js --progress --profile --colors", 57 | "start-dev": "node src/server/server-development.js" 58 | }, 59 | "devDependencies": { 60 | "webpack-dev-server": "~1.12.0" 61 | }, 62 | "main": "src/server/server-production.js", 63 | "id": "jid1-97tiMUIQmGAmQA", 64 | "description": "" 65 | } 66 | -------------------------------------------------------------------------------- /src/server/api.js: -------------------------------------------------------------------------------- 1 | let _ = require("lodash"); 2 | let logger = require("../utils/logger"); 3 | 4 | let makeElement = (classes, category, name, symbol, number, weight) => { 5 | return { 6 | classes: classes, 7 | category: category, 8 | name: name, 9 | symbol: symbol, 10 | number: number, 11 | weight: weight 12 | }; 13 | }; 14 | 15 | let elements = [ 16 | makeElement(['transition', 'metal'], 'transition', 'Mercury', 'Hg', 80, 200.59), 17 | makeElement(['metalloid'], 'metalloid', 'Tellurium', 'Te', 52, 127.6), 18 | makeElement(['post-transition', 'metal'], 'post-transition', 'Bismuth', 'Bi', 83, 208.980), 19 | makeElement(['post-transition', 'metal'], 'post-transition', 'Lead', 'Pb', 82, 207.2), 20 | makeElement(['transition', 'metal'], 'transition', 'Gold', 'Au', 79, 196.967), 21 | makeElement(['alkali', 'metal'], 'alkali', 'Potassium', 'K', 19, 39.0983), 22 | makeElement(['alkali', 'metal'], 'alkali', 'Sodium', 'Na', 11, 22.99), 23 | makeElement(['transition', 'metal'], 'transition', 'Cadmium', 'Cd', 48, 112.411), 24 | makeElement(['alkaline-earth', 'metal'], 'alkaline-earth', 'Calcium', 'Ca', 20, 40.078), 25 | makeElement(['transition', 'metal'], 'transition', 'Rhenium', 'Re', 75, 186.207), 26 | makeElement(['post-transition', 'metal'], 'post-transition', 'Thallium', 'Tl', 81, 204.383), 27 | makeElement(['metalloid'], 'metalloid', 'Antimony', 'Sb', 51, 121.76), 28 | makeElement(['transition', 'metal'], 'transition', 'Cobalt', 'Co', 27, 58.933), 29 | makeElement(['lanthanoid', 'metal', 'inner-transition'], 'lanthanoid', 'Ytterbium', 'Yb', 70, 173.054), 30 | makeElement(['noble-gas', 'nonmetal'], 'noble-gas', 'Argon', 'Ar', 18, 39.948), 31 | makeElement(['diatomic', 'nonmetal'], 'diatomic', 'Nitrogen', 'N', 7, 14.007), 32 | makeElement(['actinoid', 'metal', 'inner-transition'], 'actinoid', 'Uranium', 'U', 92, 238.029), 33 | makeElement(['actinoid', 'metal', 'inner-transition'], 'actinoid', 'Plutonium', 'Pu', 94, 244) 34 | ]; 35 | 36 | module.exports = function(app) { 37 | app.get("/api/elements", function(req, res) { 38 | var type = req.query.type; 39 | switch(type) { 40 | case "all": 41 | res.send(elements); 42 | break; 43 | case "metal": 44 | res.send(_.filter(elements, e => _.includes(e.classes, 'metal'))); 45 | break; 46 | case "iums": 47 | res.send(_.filter(elements, e => _.includes(e.name, "ium"))); 48 | break; 49 | default: 50 | res.send(elements); 51 | } 52 | }); 53 | }; -------------------------------------------------------------------------------- /src/server/index.js: -------------------------------------------------------------------------------- 1 | module.exports = function(options) { 2 | require('babel/register')({ 3 | stage: 0, 4 | experimental: true 5 | }); 6 | 7 | var _ = require('lodash'); 8 | var settings = require('./settings'); 9 | var path = require('path'); 10 | var morgan = require('morgan'); 11 | var express = require('express'); 12 | var bodyParser = require('body-parser'); 13 | var cookieParser = require('cookie-parser'); 14 | var compression = require('compression'); 15 | var ejs = require("ejs"); 16 | var logger = require("../utils/logger"); 17 | if (options.env == "development") { 18 | logger.info("Setting the console transports level to debug"); 19 | logger.transports.console.level = 'debug'; 20 | } else { 21 | logger.info("Setting the console transports level to info"); 22 | logger.transports.console.level = 'info'; 23 | } 24 | 25 | var app = express(); 26 | var server = require('http').Server(app); 27 | 28 | // load bundle information from stats 29 | var stats = options.stats || require("../../build/public/stats.json"); 30 | var packageJson = require("../../package.json"); 31 | var publicPath = stats.publicPath; 32 | 33 | // This is how it looks in the stats.json 34 | //assetsByChunkName": { 35 | //"main": [ 36 | // "main.js?21059f1cb71ba8fbd914", 37 | // "main.css?bc8f4539d07f0f272436380df3391431" 38 | //]} 39 | var styleUrl = options.separateStylesheet && (publicPath + "main.css?" + stats.hash); 40 | //var styleUrl = publicPath + [].concat(stats.assetsByChunkName.main)[1]; // + "?" + stats.hash; 41 | var scriptUrl = publicPath + [].concat(stats.assetsByChunkName.main)[0]; // + "?" + stats.hash; 42 | var commonsUrl = stats.assetsByChunkName.commons && publicPath + [].concat(stats.assetsByChunkName.commons)[0]; 43 | logger.debug("main.js" + stats.assetsByChunkName.main); 44 | var mainJsHash = stats.hash; 45 | try { 46 | mainJsHash = /main.js\?(.*)$/.exec(stats.assetsByChunkName.main)[1]; 47 | } catch(e){} 48 | 49 | // Set this in the settings to that it can be sent with each request. Then it can be compared to the 50 | // window.app.mainJsHash, if there is a difference, then the user should refresh the browser. 51 | settings.mainJsHash = mainJsHash; 52 | 53 | // Set this so extensions can read it 54 | settings.version = packageJson.version; 55 | 56 | var ipAddress = options.ipAddress || '127.0.0.1'; 57 | var port = options.port || 8080; 58 | var env = options.env || 'development'; 59 | settings.env = env; 60 | settings.environment = env; 61 | 62 | logger.info("main.js hash: " + mainJsHash); 63 | logger.info("version: " + packageJson.version); 64 | logger.info("styleUrl: " + styleUrl); 65 | logger.info("scriptUrl: " + scriptUrl); 66 | logger.info("commonsUrl: " + commonsUrl); 67 | 68 | var renderOptions = { 69 | STYLE_URL: styleUrl, 70 | SCRIPT_URL: scriptUrl, 71 | COMMONS_URL: commonsUrl, 72 | ENV: env, 73 | body: '', 74 | state: '', 75 | mainJsHash: mainJsHash, 76 | version: packageJson.version 77 | }; 78 | 79 | // Always https in production 80 | if (env == "production") { 81 | renderOptions['STYLE_URL'] = styleUrl.replace("http", "https"); 82 | renderOptions['SCRIPT_URL'] = scriptUrl.replace("http", "https"); 83 | } 84 | 85 | logger.info("Env is " + env + ', running server http://' + ipAddress + ':' + port); 86 | server.listen(port, ipAddress); 87 | 88 | process.on('SIGTERM', function() { 89 | logger.info("SIGTERM, exiting."); 90 | server.close(); 91 | }); 92 | 93 | process.on('uncaughtException', function(err) { 94 | logger.error( " UNCAUGHT EXCEPTION " ); 95 | logger.error( "[Inside 'uncaughtException' event] " + err.stack || err.message ); 96 | }); 97 | 98 | 99 | app.set('views', path.join(__dirname, 'views')); 100 | app.set('view engine', 'ejs'); 101 | app.set('port', port); 102 | 103 | app.use(compression()); 104 | app.use(morgan('dev')); 105 | // Set the limit otherwise larger payloads can cause 'Request Entity Too Large' 106 | app.use(bodyParser.json({limit: '50mb'})); 107 | app.use(bodyParser.urlencoded({limit: '50mb', extended: true})); 108 | app.use(cookieParser()); 109 | 110 | app.use("/_assets", express.static(path.join(__dirname, "..", "..", "build", "public"), { 111 | //etag: false, 112 | //maxAge: "0" 113 | maxAge: "200d" // We can cache them as they include hashes 114 | })); 115 | app.use("/static", express.static("public", { 116 | //etag: false, 117 | //maxAge: "0" 118 | maxAge: "200d" // We can cache them as they include hashes 119 | })); 120 | 121 | logger.info("Using development error handler."); 122 | app.use(function(err, req, res, next) { 123 | res.status(err.status || 500); 124 | res.render('error', { 125 | message: err.message, 126 | error: err 127 | }); 128 | }); 129 | 130 | // load REST API 131 | require("./api")(app); 132 | app.get("/*", function(req, res) { 133 | res.header("Cache-Control", "no-cache, no-store, must-revalidate"); 134 | res.header("Pragma", "no-cache"); 135 | res.header("Expires", 0); 136 | res.render('index', renderOptions); 137 | }); 138 | }; -------------------------------------------------------------------------------- /src/server/server-development.js: -------------------------------------------------------------------------------- 1 | var request = require("request"); 2 | 3 | // Load the webpack stats.json then load the index.js (express) 4 | request({url: 'http://127.0.0.1:2992/_assets/stats.json', json: true}, function(err, response, stats) { 5 | if (err) return console.error(err); 6 | require("./index")({ 7 | env: 'development', 8 | stats: stats, 9 | // I personally prefer a separateStylesheet for manipulating css in the browser 10 | separateStylesheet: true, 11 | prerender: false, 12 | defaultPort: 8080 13 | }); 14 | 15 | }); 16 | -------------------------------------------------------------------------------- /src/server/server-production.js: -------------------------------------------------------------------------------- 1 | require("./index")({ 2 | env: 'production', 3 | separateStylesheet: true, 4 | prerender: true, 5 | ipAddress: ipAddress, 6 | port: port 7 | }); -------------------------------------------------------------------------------- /src/server/settings.js: -------------------------------------------------------------------------------- 1 | var logger = require("../utils/logger"); 2 | 3 | var objToExport = { 4 | env: 'development', // overridden in index.js 5 | environment: 'development', // overridden in index.js 6 | urlPrefix: "/" 7 | }; 8 | 9 | module.exports = objToExport; 10 | -------------------------------------------------------------------------------- /src/server/views/index.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | React.js, Isotope, Flux 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 19 | 20 |
21 | <%- body %> 22 |
23 | 24 | 25 | <% if (ENV == 'development' && COMMONS_URL) { %> 26 | 27 | <% } %> 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /src/utils/logger.js: -------------------------------------------------------------------------------- 1 | var winston = require('winston'); 2 | winston.emitErrs = true; 3 | 4 | var logger = new winston.Logger({ 5 | transports: [ 6 | new winston.transports.Console({ 7 | timestamp: function() { 8 | return new Date(); 9 | }, 10 | formatter: function(options) { 11 | // Return string will be passed to logger. 12 | return options.timestamp().toISOString() +' '+ options.level.toUpperCase() +' '+ (undefined !== options.message ? options.message : '') + 13 | (options.meta && Object.keys(options.meta).length ? '\n\t'+ JSON.stringify(options.meta) : '' ); 14 | }, 15 | level: 'debug', 16 | handleExceptions: true, 17 | json: false, 18 | colorize: true 19 | }) 20 | ], 21 | exitOnError: false 22 | }); 23 | logger.exitOnError = false; 24 | 25 | module.exports = logger; 26 | module.exports.stream = { 27 | write: function(message, encoding){ 28 | logger.info(message); 29 | } 30 | }; 31 | -------------------------------------------------------------------------------- /webpack-dev-server.config.js: -------------------------------------------------------------------------------- 1 | module.exports = require("./make-webpack-config")({ 2 | env: "development", 3 | browserPath: '/', 4 | devServer: true, 5 | separateStylesheet: true, 6 | //devtool: "eval", 7 | devtool: "source-map", 8 | debug: true 9 | }); 10 | -------------------------------------------------------------------------------- /webpack-hot-dev-server.config.js: -------------------------------------------------------------------------------- 1 | module.exports = require("./make-webpack-config")({ 2 | env: "development", 3 | isExtension: false, 4 | devServer: true, 5 | hotComponents: true, 6 | separateStylesheet: true, 7 | //devtool: "eval", 8 | //devtool: "eval-source-map", 9 | devtool: "source-map", 10 | debug: true 11 | }); -------------------------------------------------------------------------------- /webpack-production.config.js: -------------------------------------------------------------------------------- 1 | module.exports = require("./make-webpack-config")({ 2 | env: "production", 3 | isExtension: false, 4 | browserPath: '/', 5 | // For this particular app there are not multiple entry points so commons chunk isn't helpful 6 | //commonsChunk: true, 7 | longTermCaching: true, 8 | separateStylesheet: true, 9 | minimize: true 10 | }); 11 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | module.exports = require("./make-webpack-config")({ 2 | 3 | }); --------------------------------------------------------------------------------