diff --git a/.eslintrc b/.eslintrc index b2a1143..0fc7240 100644 --- a/.eslintrc +++ b/.eslintrc @@ -15,7 +15,7 @@ "brace-style": 2, "comma-dangle": [2, "always-multiline"], "consistent-return": 2, - "curly": 2, + "curly": [2, "multi-line"], "dot-notation": 2, "eol-last": 2, "indent": [2, 2], @@ -30,7 +30,7 @@ "object-shorthand": 2, "quotes": [2, "single"], "space-after-keywords": 2, - "strict": [2, "global"], + "strict": 0, "vars-on-top": 2, "wrap-iife": 2, "react/jsx-boolean-value": [2, "always"], diff --git a/app/actions/AppActions.js b/app/actions/AppActions.js index b28e0be..b453762 100644 --- a/app/actions/AppActions.js +++ b/app/actions/AppActions.js @@ -1,5 +1,3 @@ -'use strict'; - import alt from '../alt'; import io from 'socket.io-client'; @@ -8,16 +6,28 @@ if (typeof window !== 'undefined') { socket = io(); } -class AppActions { - constructor() { - this.generateActions( - 'updateFocusedMarker', - 'updateFlowSuccess', - 'updateInstaData', - 'updateMarkers', - 'updateTimeout' - ); - } +export default alt.createActions({ + displayName: 'AppActions', + + updateFocusedMarker(data) { + this.dispatch(data); + }, + + updateFlowSuccess(data) { + this.dispatch(data); + }, + + updateInstaData(data) { + this.dispatch(data); + }, + + updateMarkers(data) { + this.dispatch(data); + }, + + updateTimeout(data) { + this.dispatch(data); + }, updateFlow(curFlow) { if (curFlow === 'Pause') { @@ -25,7 +35,5 @@ class AppActions { } else { socket.emit('flow:start', () => this.actions.updateFlowSuccess('Pause')); } - } -} - -export default alt.createActions(AppActions); + }, +}); diff --git a/app/alt.js b/app/alt.js index 9478618..ad49b9b 100644 --- a/app/alt.js +++ b/app/alt.js @@ -1,5 +1,3 @@ -'use strict'; - import Alt from 'alt'; export default new Alt(); diff --git a/app/app.js b/app/app.js index a9465bf..d05c48a 100644 --- a/app/app.js +++ b/app/app.js @@ -1,8 +1,8 @@ -'use strict'; - import React from 'react'; import Iso from 'iso'; -import Wrapper from './components/Wrapper'; +import wrapperFactory from './components/Wrapper'; + +const Wrapper = wrapperFactory(React); require('babel/polyfill'); diff --git a/app/components/App.jsx b/app/components/App.jsx index b476149..b31b5bb 100644 --- a/app/components/App.jsx +++ b/app/components/App.jsx @@ -1,15 +1,14 @@ -'use strict'; - const isBrowser = typeof window !== 'undefined'; -// TODO how to handle the es6 way? -const GeoMap = isBrowser ? require('./Map') : undefined; -import React from 'react'; import injectTapEventPlugin from 'react-tap-event-plugin'; -import List from './List'; -import Snackbar from 'material-ui/lib/snackbar'; -import TopBar from './TopBar'; import io from 'socket.io-client'; +import stampit from 'react-stampit'; +import Snackbar from 'material-ui/lib/snackbar'; + +import listFactory from './List'; +import topBarFactory from './TopBar'; +// TODO how to handle conditional imports in ES6? +const mapFactory = isBrowser ? require('./Map') : undefined; let socket; if (isBrowser) { @@ -18,79 +17,102 @@ if (isBrowser) { injectTapEventPlugin(); -class App extends React.Component { - componentDidMount() { - socket.on('data:add', this._onNewData.bind(this)); - socket.on('data:timeout', this._onDataTimeout.bind(this)); - } - - _getStyles() { - return { - root: { - fontFamily: this.context.muiTheme.contentFontFamily, - fontSize: '13px', - lineHeight: '20px', - WebkitFontSmoothing: 'antialiased', - }, - list: { - position: 'absolute', - top: '56px', - bottom: '0', - right: '0', - width: '290px', - overflow: 'hidden', - overflowY: 'scroll', - }, - map: { - position: 'absolute', - top: '56px', - bottom: '0', - left: '0', - right: '290px', - }, - }; - } - - _onNewData(data) { - this.props.updateInstaData(data); - } - - _onDataTimeout(data) { - this.props.updateTimeout(data); - } - - _onListItemClick(item) { - this.props.updateFocusedMarker(item); - } - - render() { - let styles = this._getStyles(); - // cant be executing client side js on prerender - let map, alert; - - if (isBrowser) { - map = ( - - ); - } - - if (this.props.timeout) { - alert = ( - - ); - } - - return ( -
+export default React => { + const GeoMap = mapFactory ? mapFactory(React) : mapFactory; + const List = listFactory(React); + const TopBar = topBarFactory(React); + + return stampit(React, { + contextTypes: { + muiTheme: React.PropTypes.object, + }, + + propTypes: { + flow: React.PropTypes.string, + focusMarker: React.PropTypes.object, + imageData: React.PropTypes.array, + markers: React.PropTypes.object, + newImageData: React.PropTypes.object, + timeout: React.PropTypes.bool, + updateFlow: React.PropTypes.func, + updateFocusedMarker: React.PropTypes.func, + updateInstaData: React.PropTypes.func, + updateMarkers: React.PropTypes.func, + updateTimeout: React.PropTypes.func, + }, + + componentDidMount() { + socket.on('data:add', this._onNewData.bind(this)); + socket.on('data:timeout', this._onDataTimeout.bind(this)); + }, + + _getStyles() { + return { + root: { + fontFamily: this.context.muiTheme.contentFontFamily, + fontSize: '13px', + lineHeight: '20px', + WebkitFontSmoothing: 'antialiased', + }, + list: { + position: 'absolute', + top: '56px', + bottom: '0', + right: '0', + width: '290px', + overflow: 'hidden', + overflowY: 'scroll', + }, + map: { + position: 'absolute', + top: '56px', + bottom: '0', + left: '0', + right: '290px', + }, + }; + }, + + _onNewData(data) { + this.props.updateInstaData(data); + }, + + _onDataTimeout(data) { + this.props.updateTimeout(data); + }, + + _onListItemClick(item) { + this.props.updateFocusedMarker(item); + }, + + render() { + let styles = this._getStyles(); + // cant be executing client side js on prerender + let map, alert; + + if (isBrowser) { + map = ( + + ); + } + + if (this.props.timeout) { + alert = ( + + ); + } + + return ( +
- ); - } -} - -App.contextTypes = { - muiTheme: React.PropTypes.object, -}; - -App.propTypes = { - flow: React.PropTypes.string, - focusMarker: React.PropTypes.object, - imageData: React.PropTypes.array, - markers: React.PropTypes.object, - newImageData: React.PropTypes.object, - timeout: React.PropTypes.bool, - updateFlow: React.PropTypes.func, - updateFocusedMarker: React.PropTypes.func, - updateInstaData: React.PropTypes.func, - updateMarkers: React.PropTypes.func, - updateTimeout: React.PropTypes.func, + ); + }, + }); }; - -export default App; diff --git a/app/components/AuthButton.jsx b/app/components/AuthButton.jsx index c3a3793..8ae8393 100644 --- a/app/components/AuthButton.jsx +++ b/app/components/AuthButton.jsx @@ -1,16 +1,31 @@ -'use strict'; - -import React from 'react'; +import stampit from 'react-stampit'; import FlatButton from 'material-ui/lib/flat-button'; -class AuthButton extends React.Component { +export default React => stampit(React, { + displayName: 'AuthButton', + + contextTypes: { + muiTheme: React.PropTypes.object, + }, + + propTypes: { + label: React.PropTypes.string, + onTouchTap: React.PropTypes.func, + style: React.PropTypes.object, + }, + + defaultProps: { + label: 'Login', + style: {}, + }, + _getStyles() { const theme = this.context.muiTheme.component.flatButton; return { hoverColor: theme.hoverColor, }; - } + }, render() { const styles = this._getStyles.call(this); @@ -21,22 +36,5 @@ class AuthButton extends React.Component { hoverColor={styles.hoverColor} /> ); - } -} - -AuthButton.contextTypes = { - muiTheme: React.PropTypes.object, -}; - -AuthButton.propTypes = { - label: React.PropTypes.string, - onTouchTap: React.PropTypes.func, - style: React.PropTypes.object, -}; - -AuthButton.defaultProps = { - label: 'Login', - style: {}, -}; - -export default AuthButton; + }, +}); diff --git a/app/components/FlowButton.jsx b/app/components/FlowButton.jsx index 3d9808d..cc4e073 100644 --- a/app/components/FlowButton.jsx +++ b/app/components/FlowButton.jsx @@ -1,16 +1,31 @@ -'use strict'; - -import React from 'react'; +import stampit from 'react-stampit'; import FlatButton from 'material-ui/lib/flat-button'; -class FlowButton extends React.Component { +export default React => stampit(React, { + displayName: 'FlowButton', + + contextTypes: { + muiTheme: React.PropTypes.object, + }, + + propTypes: { + label: React.PropTypes.string, + onTouchTap: React.PropTypes.func, + style: React.PropTypes.object, + }, + + defaultProps: { + label: 'Pause', + style: {}, + }, + _getStyles() { const theme = this.context.muiTheme.component.flatButton; return { hoverColor: theme.hoverColor, }; - } + }, render() { const styles = this._getStyles.call(this); @@ -21,22 +36,5 @@ class FlowButton extends React.Component { hoverColor={styles.hoverColor} /> ); - } -} - -FlowButton.contextTypes = { - muiTheme: React.PropTypes.object, -}; - -FlowButton.propTypes = { - label: React.PropTypes.string, - onTouchTap: React.PropTypes.func, - style: React.PropTypes.object, -}; - -FlowButton.defaultProps = { - label: 'Pause', - style: {}, -}; - -export default FlowButton; + }, +}); diff --git a/app/components/GithubButton.jsx b/app/components/GithubButton.jsx index 7e961dc..cad137c 100644 --- a/app/components/GithubButton.jsx +++ b/app/components/GithubButton.jsx @@ -1,9 +1,22 @@ -'use strict'; - -import React from 'react'; +import stampit from 'react-stampit'; import IconButton from 'material-ui/lib/icon-button'; -class GithubButton extends React.Component { +export default React => stampit(React, { + displayName: 'GithubButton', + + contextTypes: { + muiTheme: React.PropTypes.object, + }, + + propTypes: { + repoUrl: React.PropTypes.string, + style: React.PropTypes.object, + }, + + defaultProps: { + style: {}, + }, + _getStyle() { const theme = this.context.muiTheme.component.githubButton; @@ -14,7 +27,7 @@ class GithubButton extends React.Component { iconHoverColor: theme.hoverColor, }, }; - } + }, render() { const styles = this._getStyle(); @@ -29,20 +42,5 @@ class GithubButton extends React.Component { style={this.props.style} touchRippleColor={styles.rippleColor} /> ); - } -} - -GithubButton.contextTypes = { - muiTheme: React.PropTypes.object, -}; - -GithubButton.propTypes = { - repoUrl: React.PropTypes.string, - style: React.PropTypes.object, -}; - -GithubButton.defaultProps = { - style: {}, -}; - -export default GithubButton; + }, +}); diff --git a/app/components/List.jsx b/app/components/List.jsx index 15064db..f77be2f 100644 --- a/app/components/List.jsx +++ b/app/components/List.jsx @@ -1,76 +1,80 @@ -'use strict'; +import stampit from 'react-stampit'; -import React from 'react'; -import ListItem from './ListItem'; -import ReactCSSTransitionGroup from './TimeoutTransitionGroup'; +import listItemFactory from './ListItem'; +import reactCSSTransitionGroupFactory from './TimeoutTransitionGroup'; import { mergeAndPrefix } from '../utils/stylePropable'; -class List extends React.Component { - _getStyles() { - return { - root: { - listStyleType: 'none', - padding: '8px 0', - margin: '0', - }, - transition: { - enter: { - default: { - opacity: '0.01', - transition: 'opacity .3s ease-in', - }, - active: { - opacity: '1', - }, +export default React => { + const ListItem = listItemFactory(React); + const ReactCSSTransitionGroup = reactCSSTransitionGroupFactory(React); + + return stampit(React, { + displayName: 'List', + + propTypes: { + itemData: React.PropTypes.array, + onClick: React.PropTypes.func, + style: React.PropTypes.object, + }, + + defaultProps: { + style: {}, + }, + + _getStyles() { + return { + root: { + listStyleType: 'none', + padding: '8px 0', + margin: '0', }, - leave: { - default: { - opacity: '1', - transition: 'opacity .3s ease-in', + transition: { + enter: { + default: { + opacity: '0.01', + transition: 'opacity .3s ease-in', + }, + active: { + opacity: '1', + }, }, - active: { - opacity: '0.01', + leave: { + default: { + opacity: '1', + transition: 'opacity .3s ease-in', + }, + active: { + opacity: '0.01', + }, }, }, - }, - }; - } - - render() { - let styles = this._getStyles(); + }; + }, - return ( -
    - - {this.props.itemData.map((obj) => { - return ( - - ); - })} - -
- ); - } -} + render() { + let styles = this._getStyles(); -List.propTypes = { - itemData: React.PropTypes.array, - onClick: React.PropTypes.func, - style: React.PropTypes.object, + return ( +
    + + {this.props.itemData.map((obj) => { + return ( + + ); + })} + +
+ ); + }, + }); }; - -List.defaultProps = { - style: {}, -}; - -export default List; diff --git a/app/components/ListItem.jsx b/app/components/ListItem.jsx index 3f254a6..73c7c91 100644 --- a/app/components/ListItem.jsx +++ b/app/components/ListItem.jsx @@ -1,17 +1,33 @@ -'use strict'; - -import React from 'react'; +import stampit from 'react-stampit'; import Paper from 'material-ui/lib/paper'; import transitions from 'material-ui/lib/styles/transitions'; import typography from 'material-ui/lib/styles/typography'; + import { mergeAndPrefix } from '../utils/stylePropable'; -class ListItem extends React.Component { - constructor(props) { - super(props); +export default React => stampit(React, { + displayName: 'ListItem', + + state: { hovered: false }, + + contextTypes: { + muiTheme: React.PropTypes.object, + }, + + propTypes: { + description: React.PropTypes.string, + hoverColor: React.PropTypes.string, + icon: React.PropTypes.string, + onClick: React.PropTypes.func, + onMouseOut: React.PropTypes.func, + onMouseOver: React.PropTypes.func, + title: React.PropTypes.string, + }, - this.state = {hovered: false}; - } + defaultProps: { + description: '', + title: '', + }, _getStyles() { const theme = this.context.muiTheme.component.listItem; @@ -68,21 +84,21 @@ class ListItem extends React.Component { borderBottom: `1px solid ${theme.borderColor}`, }, }; - } + }, _handleMouseOver(e) { this.setState({hovered: true}); if (this.props.onMouseOver) { this.props.onMouseOver(e); } - } + }, _handleMouseOut(e) { this.setState({hovered: false}); if (this.props.onMouseOut) { this.props.onMouseOut(e); } - } + }, render() { const styles = this._getStyles(); @@ -116,26 +132,5 @@ class ListItem extends React.Component {
); - } -} - -ListItem.contextTypes = { - muiTheme: React.PropTypes.object, -}; - -ListItem.propTypes = { - description: React.PropTypes.string, - hoverColor: React.PropTypes.string, - icon: React.PropTypes.string, - onClick: React.PropTypes.func, - onMouseOut: React.PropTypes.func, - onMouseOver: React.PropTypes.func, - title: React.PropTypes.string, -}; - -ListItem.defaultProps = { - description: '', - title: '', -}; - -export default ListItem; + }, +}); diff --git a/app/components/Map.jsx b/app/components/Map.jsx index cc732f9..d44158b 100644 --- a/app/components/Map.jsx +++ b/app/components/Map.jsx @@ -1,56 +1,59 @@ -'use strict'; - -import React from 'react'; -import MarkerCluster from './MarkerCluster'; +import stampit from 'react-stampit'; import { Map as LeafletMap, TileLayer } from 'react-leaflet'; -class Map extends React.Component { - render() { - return ( - - - - - ); - } -} +import markerClusterFactory from './MarkerCluster'; -Map.propTypes = { - attribution: React.PropTypes.string, - center: React.PropTypes.array, - focusMarker: React.PropTypes.object, - markers: React.PropTypes.object, - maxZoom: React.PropTypes.number, - minZoom: React.PropTypes.number, - newMarkerData: React.PropTypes.array, - style: React.PropTypes.object, - updateMarkers: React.PropTypes.func, - url: React.PropTypes.string, - zoom: React.PropTypes.number, -}; +export default React => { + const MarkerCluster = markerClusterFactory(React); -Map.defaultProps = { - attribution: 'Imagery from GIScience Research Group @ University of Heidelberg — Map data © OpenStreetMap', - center: [0, 0], - maxZoom: 16, - minZoom: 3, - style: {}, - url: 'http://openmapsurfer.uni-hd.de/tiles/roadsg/x={x}&y={y}&z={z}', - zoom: 3, -}; + return stampit(React, { + displayName: 'GeoMap', -export default Map; + propTypes: { + attribution: React.PropTypes.string, + center: React.PropTypes.array, + focusMarker: React.PropTypes.object, + markers: React.PropTypes.object, + maxZoom: React.PropTypes.number, + minZoom: React.PropTypes.number, + newMarkerData: React.PropTypes.array, + style: React.PropTypes.object, + updateMarkers: React.PropTypes.func, + url: React.PropTypes.string, + zoom: React.PropTypes.number, + }, + + defaultProps: { + attribution: 'Imagery from GIScience Research Group @ University of Heidelberg — Map data © OpenStreetMap', + center: [0, 0], + maxZoom: 16, + minZoom: 3, + style: {}, + url: 'http://openmapsurfer.uni-hd.de/tiles/roadsg/x={x}&y={y}&z={z}', + zoom: 3, + }, + + render() { + return ( + + + + + ); + }, + }); +}; diff --git a/app/components/MarkerCluster.jsx b/app/components/MarkerCluster.jsx index a405ded..6fb4ff2 100644 --- a/app/components/MarkerCluster.jsx +++ b/app/components/MarkerCluster.jsx @@ -1,83 +1,81 @@ -'use strict'; - -import React from 'react'; import Leaflet from 'leaflet'; -import MarkerPopup from './MarkerPopup'; -import { MapLayer } from 'react-leaflet'; - +import stampit from 'react-stampit'; require('leaflet.markercluster'); -class MarkerCluster extends MapLayer { - componentWillMount() { - super.componentWillMount(); - - this.leafletElement = Leaflet.markerClusterGroup(); - } - - componentWillReceiveProps(nextProps) { - super.componentWillReceiveProps(nextProps); - - // add markers to cluster layer - if (nextProps.newMarkerData.length > 0) { - let markers = Object.assign({}, this.props.markers); - let newMarkers = []; - - nextProps.newMarkerData.forEach((obj) => { - let markerPopup = React.renderToStaticMarkup( - - ); - - let leafletMarker = Leaflet.marker(obj.latLng) - .bindPopup(markerPopup, {maxHeight: 350, maxWidth: 250, minWidth: 250}) - .on('click', () => this.props.map.panTo(obj.latLng)); - - markers[obj.id] = leafletMarker; - newMarkers.push(leafletMarker); - }); - - this.leafletElement.addLayers(newMarkers); - - setTimeout(() => { - this.props.updateMarkers(markers); - }, 0); - } - - // zoom to particular marker - if (Object.keys(nextProps.focusMarker).length > 0) { - let marker = this.props.markers[nextProps.focusMarker.id]; - - this.leafletElement.zoomToShowLayer(marker, () => { - this.props.map.panTo(nextProps.focusMarker.latLng); - marker.openPopup(); - }); - } - } - - shouldComponentUpdate() { - return false; - } - - render() { - return null; - } -} - -MarkerCluster.propTypes = { - focusMarker: React.PropTypes.object, - map: React.PropTypes.object, - markers: React.PropTypes.object, - newMarkerData: React.PropTypes.array, - updateMarkers: React.PropTypes.func, +import leafletMap from './lib/leafletMap'; +import leafletUtil from './lib/leafletUtil'; +import markerPopupFactory from './MarkerPopup'; + +export default React => { + const MarkerPopup = markerPopupFactory(React); + + return stampit(React, { + displayName: 'MarkerCluster', + + propTypes: { + focusMarker: React.PropTypes.object, + markers: React.PropTypes.object, + newMarkerData: React.PropTypes.array, + updateMarkers: React.PropTypes.func, + }, + + defaultProps: { + markers: {}, + newMarkerData: [], + focusMarker: {}, + }, + + componentWillMount() { + this.leafletElement = Leaflet.markerClusterGroup(); + }, + + componentWillReceiveProps(nextProps) { + // add markers to cluster layer + if (nextProps.newMarkerData.length > 0) { + let markers = Object.assign({}, this.props.markers); + let newMarkers = []; + + nextProps.newMarkerData.forEach((obj) => { + let markerPopup = React.renderToStaticMarkup( + + ); + + let leafletMarker = Leaflet.marker(obj.latLng) + .bindPopup(markerPopup, {maxHeight: 350, maxWidth: 250, minWidth: 250}) + .on('click', () => this.props.map.panTo(obj.latLng)); + + markers[obj.id] = leafletMarker; + newMarkers.push(leafletMarker); + }); + + this.leafletElement.addLayers(newMarkers); + + setTimeout(() => { + this.props.updateMarkers(markers); + }, 0); + } + + // zoom to particular marker + if (Object.keys(nextProps.focusMarker).length > 0) { + let marker = this.props.markers[nextProps.focusMarker.id]; + + this.leafletElement.zoomToShowLayer(marker, () => { + this.props.map.panTo(nextProps.focusMarker.latLng); + marker.openPopup(); + }); + } + }, + + shouldComponentUpdate() { + return false; + }, + + render() { + return null; + }, + }).compose(leafletUtil, leafletMap); }; - -MarkerCluster.defaultProps = { - markers: {}, - newMarkerData: [], - focusMarker: {}, -}; - -export default MarkerCluster; diff --git a/app/components/MarkerPopup.jsx b/app/components/MarkerPopup.jsx index 5e8fc97..139050f 100644 --- a/app/components/MarkerPopup.jsx +++ b/app/components/MarkerPopup.jsx @@ -1,8 +1,14 @@ -'use strict'; +import stampit from 'react-stampit'; -import React from 'react'; +export default React => stampit(React, { + displayName: 'MarkerPopup', + + propTypes: { + caption: React.PropTypes.string, + imgUrl: React.PropTypes.string, + profileUrl: React.PropTypes.string, + }, -class MarkerPopup extends React.Component { _getStyles() { return { caption: { @@ -13,7 +19,7 @@ class MarkerPopup extends React.Component { margin: 'auto', }, }; - } + }, render() { const styles = this._getStyles(); @@ -27,13 +33,5 @@ class MarkerPopup extends React.Component {
{this.props.caption}
); - } -} - -MarkerPopup.propTypes = { - caption: React.PropTypes.string, - imgUrl: React.PropTypes.string, - profileUrl: React.PropTypes.string, -}; - -export default MarkerPopup; + }, +}); diff --git a/app/components/TimeoutTransitionGroup.jsx b/app/components/TimeoutTransitionGroup.jsx index d629250..099df8b 100644 --- a/app/components/TimeoutTransitionGroup.jsx +++ b/app/components/TimeoutTransitionGroup.jsx @@ -1,5 +1,3 @@ -'use strict'; - /** * The CSSTransitionGroup component uses the 'transitionend' event, which * browsers will not send for any number of reasons, including the @@ -16,8 +14,7 @@ * addons and under the Apache 2.0 License. */ -import React from 'react/addons'; -const ReactTransitionGroup = React.addons.TransitionGroup; +import stampit from 'react-stampit'; const TICK = 17; @@ -97,15 +94,7 @@ function removeStyle(element, style) { return element; } -class TimeoutTransitionGroupChild extends React.Component { - constructor() { - super(); - - this._transition = this._transition.bind(this); - this._queueStyle = this._queueStyle.bind(this); - this._flushClassNameQueue = this._flushClassNameQueue.bind(this); - } - +const timeoutTransitionGroupChildFactory = React => stampit(React, { _transition(animationType, finishCallback) { let node = React.findDOMNode(this); @@ -133,15 +122,15 @@ class TimeoutTransitionGroupChild extends React.Component { // Need to do this to actually trigger a transition. this._queueStyle(this.props.style[animationType].active); - } + }, _queueStyle(style) { this.styleQueue.push(style); if (!this.timeout) { - this.timeout = setTimeout(this._flushClassNameQueue, TICK); + this.timeout = setTimeout(this._flushClassNameQueue.bind(this), TICK); } - } + }, _flushClassNameQueue() { this.styleQueue.forEach((style) => { @@ -150,11 +139,11 @@ class TimeoutTransitionGroupChild extends React.Component { this.styleQueue.length = 0; this.timeout = null; - } + }, componentWillMount() { this.styleQueue = []; - } + }, componentWillUnmount() { if (this.timeout) { @@ -164,7 +153,7 @@ class TimeoutTransitionGroupChild extends React.Component { if (this.animationTimeout) { clearTimeout(this.animationTimeout); } - } + }, componentWillEnter(done) { if (this.props.enter) { @@ -172,7 +161,7 @@ class TimeoutTransitionGroupChild extends React.Component { } else { done(); } - } + }, componentWillLeave(done) { if (this.props.leave) { @@ -180,59 +169,58 @@ class TimeoutTransitionGroupChild extends React.Component { } else { done(); } - } + }, render() { return React.Children.only(this.props.children); - } -} - -class TimeoutTransitionGroup extends React.Component { - constructor() { - super(); - - this._wrapChild = this._wrapChild.bind(this); - } - - _wrapChild(child) { - return ( - - {child} - - ); - } - - render() { - /* eslint-disable */ - const {style, ...props} = this.props; - /* eslint-enable */ - - return ( - - ); - } -} - -TimeoutTransitionGroup.propTypes = { - enterTimeout: React.PropTypes.number.isRequired, - leaveTimeout: React.PropTypes.number.isRequired, - style: React.PropTypes.object.isRequired, - transitionEnter: React.PropTypes.bool, - transitionLeave: React.PropTypes.bool, -}; - -TimeoutTransitionGroup.defaultProps = { - transitionEnter: true, - transitionLeave: true, + }, +}); + +export default React => { + const ReactTransitionGroup = React.addons.TransitionGroup; + const TimeoutTransitionGroupChild = timeoutTransitionGroupChildFactory(React); + + return stampit(React, { + displayName: 'ReactCSSTransitionGroup', + + propTypes: { + enterTimeout: React.PropTypes.number.isRequired, + leaveTimeout: React.PropTypes.number.isRequired, + style: React.PropTypes.object.isRequired, + transitionEnter: React.PropTypes.bool, + transitionLeave: React.PropTypes.bool, + }, + + defaultProps: { + transitionEnter: true, + transitionLeave: true, + }, + + _wrapChild(child) { + return ( + + {child} + + ); + }, + + render() { + /* eslint-disable */ + const {style, ...props} = this.props; + /* eslint-enable */ + + return ( + + ); + }, + }); }; - -export default TimeoutTransitionGroup; diff --git a/app/components/Title.jsx b/app/components/Title.jsx index 38484bb..e4a494b 100644 --- a/app/components/Title.jsx +++ b/app/components/Title.jsx @@ -1,15 +1,19 @@ -'use strict'; - -import React from 'react'; +import stampit from 'react-stampit'; import transitions from 'material-ui/lib/styles/transitions'; import { ToolbarTitle } from 'material-ui/lib/toolbar'; -class Title extends React.Component { - constructor(props) { - super(props); +export default React => stampit(React, { + displayName: 'Title', + + state: { hovered: false }, + + contextTypes: { + muiTheme: React.PropTypes.object, + }, - this.state = {hovered: false}; - } + propTypes: { + text: React.PropTypes.string, + }, _getStyle() { const theme = this.context.muiTheme.component.title; @@ -20,15 +24,15 @@ class Title extends React.Component { paddingRight: '0', transition: transitions.easeOut(), }; - } + }, _handleMouseOver() { this.setState({hovered: true}); - } + }, _handleMouseOut() { this.setState({hovered: false}); - } + }, render() { const style = this._getStyle.call(this); @@ -41,15 +45,5 @@ class Title extends React.Component { text='GeoShare' /> ); - } -} - -Title.contextTypes = { - muiTheme: React.PropTypes.object, -}; - -Title.propTypes = { - text: React.PropTypes.string, -}; - -export default Title; + }, +}); diff --git a/app/components/TopBar.jsx b/app/components/TopBar.jsx index 13c3f5a..7132f49 100644 --- a/app/components/TopBar.jsx +++ b/app/components/TopBar.jsx @@ -1,112 +1,118 @@ -'use strict'; - -import React from 'react'; -import AuthButton from './AuthButton'; -import FlowButton from './FlowButton'; -import GithubButton from './GithubButton'; -import Title from './Title'; +import stampit from 'react-stampit'; import { Toolbar, ToolbarGroup, ToolbarSeparator } from 'material-ui/lib/toolbar'; -class TopBar extends React.Component { - _getStyles() { - const theme = this.context.muiTheme.component.toolbar; - - return { - root: { - borderBottom: `1px solid ${theme.borderColor}`, - boxShadow: '0px 1px 6px rgba(0, 0, 0, 0.12)', - }, - hashtag: { - position: 'absolute', - top: '0', - left: '50%', - transform: 'translate(-50%, 0px)', - color: theme.textColor, - fontSize: '15px', - lineHeight: '56px', - }, - toolbarGroup: { +import authButtonFactory from './AuthButton'; +import flowButtonFactory from './FlowButton'; +import githubButtonFactory from './GithubButton'; +import titleFactory from './Title'; + +export default React => { + const AuthButton = authButtonFactory(React); + const FlowButton = flowButtonFactory(React); + const GithubButton = githubButtonFactory(React); + const Title = titleFactory(React); + + return stampit(React, { + displayName: 'TopBar', + + contextTypes: { + loggedIn: React.PropTypes.bool.isRequired, + muiTheme: React.PropTypes.object, + repoUrl: React.PropTypes.string.isRequired, + tag: React.PropTypes.string.isRequired, + }, + + propTypes: { + flow: React.PropTypes.string, + itemCount: React.PropTypes.number, + style: React.PropTypes.object, + updateFlow: React.PropTypes.func, + }, + + defaultProps: { + itemCount: 0, + style: {}, + }, + + _getStyles() { + const theme = this.context.muiTheme.component.toolbar; + + return { root: { - float: 'right', - }, - flatButton: { - bottom: '1px', - margin: '0 24px', + borderBottom: `1px solid ${theme.borderColor}`, + boxShadow: '0px 1px 6px rgba(0, 0, 0, 0.12)', }, - iconButton: { - top: '4px', - margin: '0 -12px', + hashtag: { + position: 'absolute', + top: '0', + left: '50%', + transform: 'translate(-50%, 0px)', + color: theme.textColor, + fontSize: '15px', + lineHeight: '56px', }, - separator: { - float: 'none', - marginLeft: 'auto', + toolbarGroup: { + root: { + float: 'right', + }, + flatButton: { + bottom: '1px', + margin: '0 24px', + }, + iconButton: { + top: '4px', + margin: '0 -12px', + }, + separator: { + float: 'none', + marginLeft: 'auto', + }, }, - }, - }; - } - - _onLoginTouch() { - window.location.href = '/auth'; - } - - _onLogoutTouch() { - window.location.href = '/logout'; - } - - _onFlowTouch() { - this.props.updateFlow(this.props.flow); - } - - render() { - let styles = this._getStyles.call(this); - - let hashtag = `#${this.context.tag}`; - let authTouch = this._onLoginTouch; - let authLabel = 'Login'; - - if (this.context.loggedIn) { - authTouch = this._onLogoutTouch; - authLabel = 'Logout'; - } - - if (this.props.itemCount > 0) { - hashtag = `${hashtag} (${this.props.itemCount})`; - } - - return ( - - - <div className='hashtag' style={styles.hashtag}> - {hashtag} - </div> - <ToolbarGroup key={1} style={styles.toolbarGroup.root}> - <FlowButton label={this.props.flow} onTouchTap={this._onFlowTouch.bind(this)} style={styles.toolbarGroup.flatButton} /> - <ToolbarSeparator style={styles.toolbarGroup.separator} /> - <AuthButton label={authLabel} onTouchTap={authTouch} style={styles.toolbarGroup.flatButton} /> - <GithubButton repoUrl={this.context.repoUrl} style={styles.toolbarGroup.iconButton} /> - </ToolbarGroup> - </Toolbar> - ); - } -} - -TopBar.contextTypes = { - loggedIn: React.PropTypes.bool.isRequired, - muiTheme: React.PropTypes.object, - repoUrl: React.PropTypes.string.isRequired, - tag: React.PropTypes.string.isRequired, -}; + }; + }, -TopBar.propTypes = { - flow: React.PropTypes.string, - itemCount: React.PropTypes.number, - style: React.PropTypes.object, - updateFlow: React.PropTypes.func, -}; + _onLoginTouch() { + window.location.href = '/auth'; + }, -TopBar.defaultProps = { - itemCount: 0, - style: {}, -}; + _onLogoutTouch() { + window.location.href = '/logout'; + }, + + _onFlowTouch() { + this.props.updateFlow(this.props.flow); + }, + + render() { + let styles = this._getStyles.call(this); -export default TopBar; + let hashtag = `#${this.context.tag}`; + let authTouch = this._onLoginTouch; + let authLabel = 'Login'; + + if (this.context.loggedIn) { + authTouch = this._onLogoutTouch; + authLabel = 'Logout'; + } + + if (this.props.itemCount > 0) { + hashtag = `${hashtag} (${this.props.itemCount})`; + } + + return ( + <Toolbar style={styles.root}> + <Title /> + <div className='hashtag' style={styles.hashtag}> + {hashtag} + </div> + <ToolbarGroup key={1} style={styles.toolbarGroup.root}> + <FlowButton label={this.props.flow} onTouchTap={this._onFlowTouch.bind(this)} style={styles.toolbarGroup.flatButton} /> + <ToolbarSeparator style={styles.toolbarGroup.separator} /> + <AuthButton label={authLabel} onTouchTap={authTouch} style={styles.toolbarGroup.flatButton} /> + <GithubButton repoUrl={this.context.repoUrl} style={styles.toolbarGroup.iconButton} /> + </ToolbarGroup> + </Toolbar> + ); + }, + }); +}; diff --git a/app/components/Wrapper.jsx b/app/components/Wrapper.jsx index eea76f8..c4744c8 100644 --- a/app/components/Wrapper.jsx +++ b/app/components/Wrapper.jsx @@ -1,60 +1,53 @@ -'use strict'; +import AltContainer from 'alt/AltContainer'; +import stampit from 'react-stampit'; +import themeManager from 'material-ui/lib/styles/theme-manager'; -import React from 'react'; import alt from '../alt'; -import AltContainer from 'alt/AltContainer'; -import App from './App'; +import appFactory from './App'; import AppActions from '../actions/AppActions'; import AppStore from '../stores/AppStore'; - import theme from '../style/themes/default-theme'; -import ThemeManager from 'material-ui/lib/styles/theme-manager'; - -const themeManager = new ThemeManager(); -themeManager.setTheme(theme); // webpack require('../style/app.less'); -class Wrapper extends React.Component { - getChildContext() { - return Object.assign( - {}, this.props.initData.ctx, { muiTheme: themeManager.getCurrentTheme() } - ); - } - - componentWillMount() { - alt.bootstrap(JSON.stringify( - { - AppStore: Object.assign( - AppStore.getState(), - this.props.initData.state.AppStore - ), - }, - )); - - // force state update this render cycle - this.setState(AppStore.getState()); - } - - render() { - return ( - <AltContainer actions={AppActions} store={AppStore} > - <App /> - </AltContainer> - ); - } -} - -Wrapper.childContextTypes = { - tag: React.PropTypes.string.isRequired, - loggedIn: React.PropTypes.bool.isRequired, - muiTheme: React.PropTypes.object, - repoUrl: React.PropTypes.string.isRequired, +const themeMgr = themeManager(); +themeMgr.setTheme(theme); + +export default React => { + const App = appFactory(React); + + return stampit(React, { + childContextTypes: { + tag: React.PropTypes.string.isRequired, + loggedIn: React.PropTypes.bool.isRequired, + muiTheme: React.PropTypes.object, + repoUrl: React.PropTypes.string.isRequired, + }, + + propTypes: { + initData: React.PropTypes.object.isRequired, + }, + + getChildContext() { + return Object.assign( + {}, this.props.initData.ctx, { muiTheme: themeMgr } + ); + }, + + componentWillMount() { + alt.bootstrap(JSON.stringify(this.props.initData.state)); + + // force state update this render cycle + this.setState(AppStore.getState()); + }, + + render() { + return ( + <AltContainer actions={AppActions} store={AppStore} > + <App /> + </AltContainer> + ); + }, + }); }; - -Wrapper.propTypes = { - initData: React.PropTypes.object.isRequired, -}; - -export default Wrapper; diff --git a/app/components/lib/leafletMap.js b/app/components/lib/leafletMap.js new file mode 100644 index 0000000..22c971c --- /dev/null +++ b/app/components/lib/leafletMap.js @@ -0,0 +1,28 @@ +export default { + componentWillMount() { + this._leafletEvents = this.extractLeafletEvents(this.props); + }, + + componentDidMount() { + this.bindLeafletEvents(this._leafletEvents); + this.props.map.addLayer(this.leafletElement); + }, + + componentWillReceiveProps(nextProps) { + const next = this.extractLeafletEvents(nextProps); + this._leafletEvents = this.bindLeafletEvents(next, this._leafletEvents); + }, + + componentWillUnmount() { + const el = this.leafletElement; + if (!el) { + return; + } + + Object.keys(this._leafletEvents).forEach((ev) => { + el.off(ev, this._leafletEvents[ev]); + }); + + this.props.map.removeLayer(this.leafletElement); + }, +}; diff --git a/app/components/lib/leafletUtil.js b/app/components/lib/leafletUtil.js new file mode 100644 index 0000000..0d01699 --- /dev/null +++ b/app/components/lib/leafletUtil.js @@ -0,0 +1,52 @@ +const EVENTS_RE = /on(?:Leaflet)?(.+)/i; + +export default { + getLeafletElement() { + return this.leafletElement; + }, + + extractLeafletEvents(props) { + return Object.keys(props).reduce((res, ev) => { + if (EVENTS_RE.test(ev)) { + const key = ev.replace(EVENTS_RE, (match, p) => p.toLowerCase()); + res[key] = props[ev]; + } + + return res; + }, {}); + }, + + bindLeafletEvents(next = {}, prev = {}) { + const el = this.leafletElement; + + if (!el) { + return undefined; + } + + const diff = Object.assign({}, prev); + + Object.keys(prev).forEach((ev) => { + if (!next[ev] || prev[ev] !== next[ev]) { + delete diff[ev]; + el.off(ev, prev[ev]); + } + }); + + Object.keys(next).forEach((ev) => { + if (!prev[ev] || next[ev] !== prev[ev]) { + diff[ev] = next[ev]; + el.on(ev, next[ev]); + } + }); + + return diff; + }, + + fireLeafletEvent(type, data) { + const el = this.leafletElement; + + if (el) { + el.fire(type, data); + } + }, +}; diff --git a/app/stores/AppStore.js b/app/stores/AppStore.js index 564be6f..fc5a2da 100644 --- a/app/stores/AppStore.js +++ b/app/stores/AppStore.js @@ -1,49 +1,51 @@ -'use strict'; - import alt from '../alt'; import AppActions from '../actions/AppActions'; -class AppStore { - constructor() { - this.bindActions(AppActions); - - this.newImageData = []; - this.imageData = []; - this.markers = {}; - this.focusMarker = {}; - this.flow = 'Pause'; - this.timeout = false; - } +export default alt.createStore({ + state: { + newImageData: [], + imageData: [], + markers: {}, + focusMarker: {}, + flow: 'Pause', + timeout: false, + }, + + bindListeners: { + onUpdateInstaData: AppActions.updateInstaData, + onUpdateMarkers: AppActions.updateMarkers, + onUpdateFocusedMarker: AppActions.updateFocusedMarker, + onUpdateFlowSuccess: AppActions.updateFlowSuccess, + onUpdateTimeout: AppActions.updateTimeout, + }, onUpdateInstaData(data) { - let imageData = data.concat(this.imageData); + let imageData = data.concat(this.state.imageData); // limit imageData (used in list) to last 100 while (imageData.length > 100) { imageData.pop(); } - this.focusMarker = {}; - this.newImageData = data; - this.imageData = imageData; - } + this.state.focusMarker = {}; + this.state.newImageData = data; + this.state.imageData = imageData; + }, onUpdateMarkers(markers) { - this.newImageData = []; - this.markers = markers; - } + this.state.newImageData = []; + this.state.markers = markers; + }, onUpdateFocusedMarker(marker) { - this.focusMarker = marker; - } + this.state.focusMarker = marker; + }, onUpdateFlowSuccess(newFlow) { - this.flow = newFlow; - } + this.state.flow = newFlow; + }, onUpdateTimeout(isActive) { - this.timeout = isActive; - } -} - -export default alt.createStore(AppStore, 'AppStore'); + this.state.timeout = isActive; + }, +}, 'AppStore'); diff --git a/app/style/themes/default-theme.js b/app/style/themes/default-theme.js index 0505124..f514236 100644 --- a/app/style/themes/default-theme.js +++ b/app/style/themes/default-theme.js @@ -1,5 +1,3 @@ -'use strict'; - import colors from 'material-ui/lib/styles/colors'; import colorManipulator from 'material-ui/lib/utils/color-manipulator'; import spacing from 'material-ui/lib/styles/spacing'; diff --git a/app/utils/prerender.js b/app/utils/prerender.js index e177e1f..d233045 100644 --- a/app/utils/prerender.js +++ b/app/utils/prerender.js @@ -1,12 +1,12 @@ -'use strict'; - import React from 'react'; import Iso from 'iso'; -import Wrapper from '../components/Wrapper'; +import wrapperFactory from '../components/Wrapper'; // webpack import html from '../index.html'; +const Wrapper = wrapperFactory(React); + export default (initData, scriptUrl, styleUrl, callback) => { const Component = React.createElement(Wrapper, { initData }); diff --git a/app/utils/stylePropable.js b/app/utils/stylePropable.js index e4f1600..b3d6b6b 100644 --- a/app/utils/stylePropable.js +++ b/app/utils/stylePropable.js @@ -1,5 +1,3 @@ -'use strict'; - import autoPrefix from 'material-ui/lib/styles/auto-prefix'; import extend from 'material-ui/lib/utils/extend'; diff --git a/config/serverDev.js b/config/serverDev.js index 797e6e8..ed8fa8d 100644 --- a/config/serverDev.js +++ b/config/serverDev.js @@ -1,5 +1,5 @@ -'use strict'; +import server from '../server'; -require('../server')({ +server({ testData: true, }); diff --git a/config/serverProd.js b/config/serverProd.js index a36f69f..7315e0e 100644 --- a/config/serverProd.js +++ b/config/serverProd.js @@ -1,6 +1,6 @@ -'use strict'; +import server from '../server'; -require('../server')({ +server({ testData: false, separateStylesheet: true, }); diff --git a/lib/db.js b/lib/db.js index 38f2ef0..85af913 100644 --- a/lib/db.js +++ b/lib/db.js @@ -1,5 +1,3 @@ -'use strict'; - export function addUser(client, data, next) { client.sismember('users', `user:${data.access_token}`, (err, exists) => { if (err) { diff --git a/lib/instagram.js b/lib/instagram.js index e08ca75..faf5e35 100644 --- a/lib/instagram.js +++ b/lib/instagram.js @@ -1,5 +1,3 @@ -'use strict'; - import config from 'config'; import { instagram as instaApi } from 'instagram-node'; diff --git a/lib/services.js b/lib/services.js index ac89931..b74bf53 100644 --- a/lib/services.js +++ b/lib/services.js @@ -1,5 +1,3 @@ -'use strict'; - import config from 'config'; import * as instagram from './instagram'; import { addUser, deleteUser } from './db'; diff --git a/package.json b/package.json index e535c5b..06480d6 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,7 @@ "react": "^0.13.3", "react-hot-loader": "^1.2.7", "react-leaflet": "^0.6.2", + "react-stampit": "^0.6.1", "react-tap-event-plugin": "^0.1.7", "redis": "^0.12.1", "socket.io": "^1.3.5", @@ -64,7 +65,7 @@ "babelify": "^6.1.2", "chai": "^3.0.0", "eslint": "^0.24.0", - "eslint-plugin-react": "^2.6.3", + "eslint-plugin-react": "^2.6.4", "mocha": "^2.2.5", "mochify": "^2.10.0", "phantomjs": "^1.9.17", diff --git a/server.js b/server.js index e8889b6..abf9458 100644 --- a/server.js +++ b/server.js @@ -1,5 +1,3 @@ -'use strict'; - import config from 'config'; import Hapi from 'hapi'; import Redis from 'redis'; diff --git a/test/app/actions/AppActions.js b/test/app/actions/AppActions.js index 0e6fc93..eebb353 100644 --- a/test/app/actions/AppActions.js +++ b/test/app/actions/AppActions.js @@ -1,5 +1,3 @@ -'use strict'; - import rewire from 'rewire'; import sinon from 'sinon'; import { assert } from 'chai'; diff --git a/test/app/components/AuthButton.jsx b/test/app/components/AuthButton.jsx index c1e81a4..09cec26 100644 --- a/test/app/components/AuthButton.jsx +++ b/test/app/components/AuthButton.jsx @@ -1,16 +1,13 @@ -'use strict'; - import React from 'react/addons'; import sinon from 'sinon'; import { assert } from 'chai'; import compWithContext from '../../utils/compWithContext'; -import AuthButtonRaw from '../../../app/components/AuthButton'; +import authButtonFactory from '../../../app/components/AuthButton'; -const AuthButton = compWithContext(AuthButtonRaw); +const AuthButton = compWithContext(React, authButtonFactory(React)); const TestUtils = React.addons.TestUtils; - describe('AuthButton component', function () { it('should set value using label property', function() { const label = 'Foo'; diff --git a/test/app/components/FlowButton.jsx b/test/app/components/FlowButton.jsx index 226588f..c49801f 100644 --- a/test/app/components/FlowButton.jsx +++ b/test/app/components/FlowButton.jsx @@ -1,13 +1,11 @@ -'use strict'; - import React from 'react/addons'; import sinon from 'sinon'; import { assert } from 'chai'; import compWithContext from '../../utils/compWithContext'; -import FlowButtonRaw from '../../../app/components/FlowButton'; +import flowButtonFactory from '../../../app/components/FlowButton'; -const FlowButton = compWithContext(FlowButtonRaw); +const FlowButton = compWithContext(React, flowButtonFactory(React)); const TestUtils = React.addons.TestUtils; describe('FlowButton component', function () { diff --git a/test/app/components/GithubButton.jsx b/test/app/components/GithubButton.jsx index 1dc45e7..47f13d0 100644 --- a/test/app/components/GithubButton.jsx +++ b/test/app/components/GithubButton.jsx @@ -1,12 +1,10 @@ -'use strict'; - import React from 'react/addons'; import { assert } from 'chai'; import compWithContext from '../../utils/compWithContext'; -import GithubButtonRaw from '../../../app/components/GithubButton'; +import githubButtonFactory from '../../../app/components/GithubButton'; -const GithubButton = compWithContext(GithubButtonRaw); +const GithubButton = compWithContext(React, githubButtonFactory(React)); const TestUtils = React.addons.TestUtils; describe('GithubButton component', function () { diff --git a/test/app/components/List.jsx b/test/app/components/List.jsx index c65809f..f012008 100644 --- a/test/app/components/List.jsx +++ b/test/app/components/List.jsx @@ -1,13 +1,12 @@ -'use strict'; - import React from 'react/addons'; import { assert } from 'chai'; import compWithContext from '../../utils/compWithContext'; -import ListItem from '../../../app/components/ListItem'; -import ListRaw from '../../../app/components/List'; +import listFactory from '../../../app/components/List'; +import listItemFactory from '../../../app/components/ListItem'; -const List = compWithContext(ListRaw); +const List = compWithContext(React, listFactory(React)); +const ListItem = listItemFactory(React); const TestUtils = React.addons.TestUtils; describe('List component', function () { diff --git a/test/app/components/ListItem.jsx b/test/app/components/ListItem.jsx index ce55923..a5bfb09 100644 --- a/test/app/components/ListItem.jsx +++ b/test/app/components/ListItem.jsx @@ -1,13 +1,11 @@ -'use strict'; - import React from 'react/addons'; import sinon from 'sinon'; import { assert } from 'chai'; import compWithContext from '../../utils/compWithContext'; -import ListItemRaw from '../../../app/components/ListItem'; +import listItemFactory from '../../../app/components/ListItem'; -const ListItem = compWithContext(ListItemRaw); +const ListItem = compWithContext(React, listItemFactory(React)); const TestUtils = React.addons.TestUtils; describe('ListItem component', function () { diff --git a/test/app/components/Map.jsx b/test/app/components/Map.jsx index c084170..86ed96c 100644 --- a/test/app/components/Map.jsx +++ b/test/app/components/Map.jsx @@ -1,13 +1,13 @@ -'use strict'; - -import React from 'react/addons'; +import React from 'react'; import { assert } from 'chai'; - -import MarkerCluster from '../../../app/components/MarkerCluster'; -import MyMap from '../../../app/components/Map'; import { Map, TileLayer } from 'react-leaflet'; -const TestUtils = React.addons.TestUtils; +import mapFactory from '../../../app/components/Map'; +import markerClusterFactory from '../../../app/components/MarkerCluster'; +import TestUtils from '../../utils/testUtils'; + +const MyMap = mapFactory(React); +const MarkerCluster = markerClusterFactory(React); describe('Map component', function () { it('should be centered at value of center property', function () { diff --git a/test/app/components/MarkerCluster.jsx b/test/app/components/MarkerCluster.jsx index 6eb7e9c..6d39c4f 100644 --- a/test/app/components/MarkerCluster.jsx +++ b/test/app/components/MarkerCluster.jsx @@ -1,12 +1,12 @@ -'use strict'; - import React from 'react/addons'; import { assert } from 'chai'; -import MarkerCluster from '../../../app/components/MarkerCluster'; -import MyMap from '../../../app/components/Map'; +import markerClusterFactory from '../../../app/components/MarkerCluster'; +import mapFactory from '../../../app/components/Map'; // import { Map } from 'react-leaflet'; +const MarkerCluster = markerClusterFactory(React); +const MyMap = mapFactory(React); const TestUtils = React.addons.TestUtils; describe('MarkerCluster component', function () { diff --git a/test/app/components/MarkerPopup.jsx b/test/app/components/MarkerPopup.jsx index 40b9e1f..4a23b3b 100644 --- a/test/app/components/MarkerPopup.jsx +++ b/test/app/components/MarkerPopup.jsx @@ -1,10 +1,9 @@ -'use strict'; - import React from 'react/addons'; import { assert } from 'chai'; -import MarkerPopup from '../../../app/components/MarkerPopup'; +import markerPopupFactory from '../../../app/components/MarkerPopup'; +const MarkerPopup = markerPopupFactory(React); const TestUtils = React.addons.TestUtils; describe('MarkerPopup component', function () { diff --git a/test/app/components/TimeoutTransitionGroup.jsx b/test/app/components/TimeoutTransitionGroup.jsx index 4bcc162..d3198e3 100644 --- a/test/app/components/TimeoutTransitionGroup.jsx +++ b/test/app/components/TimeoutTransitionGroup.jsx @@ -1,11 +1,10 @@ -'use strict'; - import React from 'react/addons'; import sinon from 'sinon'; import { assert } from 'chai'; -import ReactCSSTransitionGroup from '../../../app/components/TimeoutTransitionGroup'; +import reactCSSTransitionGroupFactory from '../../../app/components/TimeoutTransitionGroup'; +const ReactCSSTransitionGroup = reactCSSTransitionGroupFactory(React); const TestUtils = React.addons.TestUtils; describe('TimeoutTransitionGroup component', function () { diff --git a/test/app/components/TopBar.jsx b/test/app/components/TopBar.jsx index a5f552c..c263bd8 100644 --- a/test/app/components/TopBar.jsx +++ b/test/app/components/TopBar.jsx @@ -1,16 +1,18 @@ -'use strict'; - import React from 'react/addons'; import { assert } from 'chai'; import compWithContext from '../../utils/compWithContext'; -import AuthButton from '../../../app/components/AuthButton'; -import FlowButton from '../../../app/components/FlowButton'; -import GithubButton from '../../../app/components/GithubButton'; -import Title from '../../../app/components/Title'; -import TopBarRaw from '../../../app/components/TopBar'; +import authButtonFactory from '../../../app/components/AuthButton'; +import flowButtonFactory from '../../../app/components/FlowButton'; +import githubButtonFactory from '../../../app/components/GithubButton'; +import titleFactory from '../../../app/components/Title'; +import topBarFactory from '../../../app/components/TopBar'; -const TopBar = compWithContext(TopBarRaw); +const AuthButton = authButtonFactory(React); +const FlowButton = flowButtonFactory(React); +const GithubButton = githubButtonFactory(React); +const Title = titleFactory(React); +const TopBar = compWithContext(React, topBarFactory(React)); const TestUtils = React.addons.TestUtils; describe('TopBar component', function () { diff --git a/test/app/stores/AppStore.js b/test/app/stores/AppStore.js index 7623fae..fba73a9 100644 --- a/test/app/stores/AppStore.js +++ b/test/app/stores/AppStore.js @@ -1,19 +1,17 @@ -'use strict'; - import AppStore from '../../../app/stores/AppStore'; import { assert } from 'chai'; describe('AppStore', function () { before(function () { - this.UnwrappedStore = AppStore[Object.getOwnPropertySymbols(AppStore)[2]]; + this.UnwrappedStore = AppStore.StoreModel; this.defaultState = AppStore.getState(); }); afterEach(function () { - this.UnwrappedStore.newImageData = this.defaultState.newImageData; - this.UnwrappedStore.imageData = this.defaultState.imageData; - this.UnwrappedStore.focusMarker = this.defaultState.focusMarker; - this.UnwrappedStore.flow = this.defaultState.flow; + this.UnwrappedStore.state.newImageData = this.defaultState.newImageData; + this.UnwrappedStore.state.imageData = this.defaultState.imageData; + this.UnwrappedStore.state.focusMarker = this.defaultState.focusMarker; + this.UnwrappedStore.state.flow = this.defaultState.flow; }); describe('onUpdateInstaData()', function () { diff --git a/test/lib/db.js b/test/lib/db.js index 6bb2238..17bce82 100644 --- a/test/lib/db.js +++ b/test/lib/db.js @@ -1,5 +1,3 @@ -'use strict'; - import * as db from '../../lib/db'; import sinon from 'sinon'; import { assert } from 'chai'; diff --git a/test/lib/instagram.js b/test/lib/instagram.js index fbaba7f..1882e5b 100644 --- a/test/lib/instagram.js +++ b/test/lib/instagram.js @@ -1,5 +1,3 @@ -'use strict'; - import rewire from 'rewire'; import sinon from 'sinon'; import { assert } from 'chai'; diff --git a/test/utils/compWithContext.js b/test/utils/compWithContext.js index 0f89a82..3767807 100644 --- a/test/utils/compWithContext.js +++ b/test/utils/compWithContext.js @@ -1,28 +1,24 @@ -'use strict'; - -import React from 'react'; +import stampit from 'react-stampit'; import theme from '../../app/style/themes/default-theme'; -import ThemeManager from 'material-ui/lib/styles/theme-manager'; +import themeManager from 'material-ui/lib/styles/theme-manager'; + +const themeMgr = themeManager(); +themeMgr.setTheme(theme); -const themeManager = new ThemeManager(); -themeManager.setTheme(theme); +export default (React, Component) => { + return stampit(React, { + childContextTypes: { + muiTheme: React.PropTypes.object, + }, -export default (Component) => { - class CompWithContext extends React.Component { getChildContext() { return { - muiTheme: themeManager.getCurrentTheme(), + muiTheme: themeMgr, }; - } + }, render() { return React.createElement(Component, this.props); - } - } - - CompWithContext.childContextTypes = { - muiTheme: React.PropTypes.object, - }; - - return CompWithContext; + }, + }); }; diff --git a/test/utils/testUtils.js b/test/utils/testUtils.js new file mode 100644 index 0000000..895f565 --- /dev/null +++ b/test/utils/testUtils.js @@ -0,0 +1,21 @@ +/** + * Monkey patch for React's test utils to + * allow testing stamp components + * + * Patched upstream, remove when React 0.14 hits + */ + +import React from 'react/addons'; +import ReactInstanceMap from 'react/lib/ReactInstanceMap'; + +export default Object.assign(React.addons.TestUtils, { + isCompositeComponentWithType(inst, type) { + var internalInstance = ReactInstanceMap.get(inst); + var constructor = internalInstance + ._currentElement + .type; + + return !!(this.isCompositeComponent(inst) && + (constructor === type)); + }, +});