├── .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 | 
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 | });
--------------------------------------------------------------------------------