<|start_filename|>package.json<|end_filename|>
{
  "name": "geoshare",
  "version": "1.0.0",
  "description": "View tag filtered Instagram images in real time",
  "main": "app/app.js",
  "scripts": {
    "build": "webpack --config config/webpack/production.config.js --progress --profile --colors",
    "dev-server": "webpack --config config/webpack/prerender.config.js & webpack-dev-server --config config/webpack/devServer.config.js --progress --colors --hot --inline",
    "dev": "babel-node --ignore 'node_modules','build','app' config/serverDev",
    "lint": "eslint --ext .js,.jsx app/ config/ lib/ test/ server.js",
    "prod": "babel-node --ignore 'node_modules','build','app' config/serverProd",
    "test": "npm run lint && npm run test-jsx && npm run test-js",
    "test-js": "mocha ./test/lib ./test/app/actions ./test/app/stores --compilers js:babel/register",
    "test-jsx": "mochify node_modules/babel/polyfill ./test/app/components/* --reporter spec --transform [ babelify --stage 1 --ignore 'node_modules'] --extension .jsx"
  },
  "repository": {
    "type": "git",
    "url": "https://github.com/troutowicz/geoshare.git"
  },
  "author": "<NAME>",
  "license": "MIT",
  "bugs": {
    "url": "https://github.com/troutowicz/geoshare/issues"
  },
  "homepage": "https://github.com/troutowicz/geoshare",
  "dependencies": {
    "alt": "^0.17.3",
    "autoprefixer-loader": "^3.0.0",
    "babel": "^5.8.23",
    "babel-loader": "^5.3.2",
    "config": "^1.16.0",
    "css-loader": "^0.17.0",
    "extract-text-webpack-plugin": "^0.8.2",
    "good": "^6.3.0",
    "good-console": "^5.0.3",
    "hapi-auth-cookie": "^3.1.0",
    "hapi-shutdown": "^0.1.2",
    "html-loader": "^0.3.0",
    "instagram-node": "^0.5.8",
    "iso": "^4.2.0",
    "json5": "^0.4.0",
    "leaflet": "^0.7.5",
    "leaflet.markercluster": "Leaflet/Leaflet.markercluster#44bd117",
    "less": "^2.5.1",
    "less-loader": "^2.2.0",
    "material-ui": "0.9.0",
    "react": "^0.13.3",
    "react-hot-loader": "^1.3.0",
    "react-leaflet": "^0.6.2",
    "react-tap-event-plugin": "^0.1.7",
    "redis": "^1.0.0",
    "socket.io": "^1.3.6",
    "socket.io-client": "^1.3.6",
    "stats-webpack-plugin": "0.1.2",
    "style-loader": "^0.12.3",
    "url-loader": "^0.5.6",
    "webpack": "^1.12.1"
  },
  "devDependencies": {
    "babel-eslint": "^4.1.1",
    "babelify": "^6.3.0",
    "chai": "^3.2.0",
    "eslint": "^1.3.1",
    "eslint-plugin-react": "^3.3.1",
    "mocha": "^2.3.2",
    "mochify": "^2.13.0",
    "phantomjs": "^1.9.18",
    "react-tools": "^0.13.3",
    "reactify": "^1.1.1",
    "rewire": "^2.3.4",
    "sinon": "^1.16.1",
    "webpack-dev-server": "^1.10.1"
  }
}


<|start_filename|>config/serverProd.js<|end_filename|>
'use strict';

require('../server')({
  testData: false,
  separateStylesheet: true,
});


<|start_filename|>test/lib/db.js<|end_filename|>
'use strict';

import * as db from '../../lib/db';
import sinon from 'sinon';
import { assert } from 'chai';

describe('db lib', function () {
  describe('addUser()', function () {
    before(function () {
      this.client = {
        sismember() {},
        hgetall() {},
        hmset() {},
        sadd() {},
      };

      /* eslint-disable */
      this.data = {
        user: {
          id: 1,
          username: 'troutowicz',
          full_name: '<NAME>',
          profile_picture: 'localhost/me.png',
        },
        access_token: '<PASSWORD>',
      };

      this.testUser = {
        id: 1,
        username: 'troutowicz',
        full_name: '<NAME>',
        profile_picture: 'localhost/me.png',
        access_token: '<PASSWORD>',
      };
      /* eslint-enable */
    });

    it('should return user object when user found in db', function (done) {
      const client = {
        sismember() {},
        hgetall() {},
      };

      sinon.stub(client, 'sismember').yields(null, true);
      sinon.stub(client, 'hgetall').yields(null, this.testUser);

      db.addUser(client, this.data, (err, user) => {
        assert.isNull(err);
        assert.deepEqual(this.testUser, user);

        done();
      });
    });

    it('should return new user object when user added to db', function (done) {
      const client = {
        sismember() {},
        hmset() {},
        sadd() {},
      };

      sinon.stub(client, 'sismember').yields(null, false);
      sinon.stub(client, 'hmset').yields(null);
      sinon.stub(client, 'sadd').returns(true);

      db.addUser(client, this.data, (err, user) => {
        assert.isNull(err);
        assert.deepEqual(this.testUser, user);

        done();
      });
    });
  });

  describe('deleteUser()', function () {
    it('should return true on deletion from db', function (done) {
      const token = '<PASSWORD>';
      const client = {
        del() {},
        srem() {},
      };

      sinon.stub(client, 'del').yields(null);
      sinon.stub(client, 'srem').returns(true);

      db.deleteUser(client, token, (err) => {
        assert.ok(!err);

        done();
      });
    });
  });

  describe('getTokens()', function () {
    it('should return tokens object', function (done) {
      const client = {
        smembers() {},
      };

      sinon.stub(client, 'smembers').yields(null, {});

      db.getTokens(client, (err, tokens) => {
        assert.isNull(err);
        assert.deepEqual({}, tokens);

        done();
      });
    });
  });
});


<|start_filename|>lib/db.js<|end_filename|>
'use strict';

export function addUser(client, data, next) {
  client.sismember('users', `user:${data.access_token}`, (err, exists) => {
    if (err) {
      return next(err);
    }

    // Existing user
    if (exists) {
      client.hgetall(`user:${data.access_token}`, (err, user) => {
        if (err) {
          return next(err);
        }

        return next(null, user);
      });
    // New user
    } else {
      const user = {
        id: data.user.id,
        username: data.user.username,
        /* eslint-disable */
        full_name: data.user.full_name,
        profile_picture: data.user.profile_picture,
        access_token: data.access_token,
        /* eslint-enable */
      };

      client.hmset(`user:${data.access_token}`, user, (err) => {
        if (err) {
          return next(err);
        }

        client.sadd('users', `user:${data.access_token}`);
        client.sadd('tokens', data.access_token);

        return next(null, user);
      });
    }
  });
}

export function deleteUser(client, token, next) {
  client.del(`user:${token}`, (err) => {
    if (err) {
      return next(err);
    }

    client.srem('users', `user:${token}`);
    client.srem('tokens', token);

    return next();
  });
}

export function getTokens(client, next) {
  client.smembers('tokens', (err, tokens) => {
    if (err) {
      return next(err);
    }

    return next(null, tokens);
  });
}


<|start_filename|>config/serverDev.js<|end_filename|>
'use strict';

require('../server')({
  testData: true,
});


<|start_filename|>test/lib/instagram.js<|end_filename|>
'use strict';

import rewire from 'rewire';
import sinon from 'sinon';
import { assert } from 'chai';

const instagram = rewire('../../lib/instagram');

describe('instagram lib', function () {
  beforeEach(function () {
    this.tokens = {
      valid: [],
      expired: {},
    };
  });

  afterEach(function () {
    instagram.__set__('apiTokens', { valid: [], expired: {} });
  });

  describe('getValidTokens()', function () {
    it('should return all valid tokens', function () {
      this.tokens.valid.push('abc123');
      instagram.__set__('apiTokens', this.tokens);

      const validTokens = instagram.getValidTokens();
      assert.equal(validTokens, this.tokens.valid);
    });
  });

  describe('setValidTokens()', function () {
    it('should set valid tokens', function () {
      this.tokens.valid.push('abc123');

      instagram.setValidTokens(this.tokens.valid);
      assert.lengthOf(instagram.__get__('apiTokens').valid, 1);
    });
  });

  describe('addValidToken()', function () {
    it('should add valid token', function () {
      const token = 'abc123';

      instagram.addValidToken(token);
      assert.lengthOf(instagram.__get__('apiTokens').valid, 1);
    });
  });

  describe('expireToken()', function () {
    it('should expire current valid token', function () {
      this.tokens.valid.push('abc123');
      instagram.__set__('apiTokens', this.tokens);

      instagram.expireToken();
      const tokens = instagram.__get__('apiTokens');
      const expiredTokenKeys = Object.keys(this.tokens.expired);

      assert.lengthOf(tokens.valid, 0);
      assert.lengthOf(expiredTokenKeys, 1);
    });
  });

  describe('removeToken()', function () {
    it('should remove current token', function () {
      this.tokens.valid.push('abc123');
      instagram.__set__('apiTokens', this.tokens);

      instagram.removeToken();
      const tokens = instagram.__get__('apiTokens');

      assert.lengthOf(tokens.valid, 0);
    });
  });

  describe('recycleTokens()', function () {
    it('should unexpire applicable tokens', function () {
      const stub = sinon.stub().returns(true);
      const restoreAddSubscription = instagram.__set__('addSubscription', stub);
      this.tokens.valid.push('abc123');
      this.tokens.expired[Date.now()] = 'def<PASSWORD>';
      this.tokens.expired[Date.now() + 1000] = '<PASSWORD>';

      instagram.__set__('apiTokens', this.tokens);
      instagram.recycleTokens();

      const apiTokens = instagram.__get__('apiTokens');
      assert.lengthOf(Object.keys(apiTokens.expired), 1);
      assert.lengthOf(apiTokens.valid, 2);
      assert.ok(stub.calledOnce);
      restoreAddSubscription();
    });

    it('should retry when no tokens get validated', function () {
      const stub = sinon.stub().returns(true);
      const restoreSetTimeout = instagram.__set__('setTimeout', stub);
      this.tokens.expired[Date.now() + 1000] = 'abc123';

      instagram.__set__('apiTokens', this.tokens);
      instagram.recycleTokens();

      const apiTokens = instagram.__get__('apiTokens');
      assert.lengthOf(Object.keys(apiTokens.expired), 1);
      assert.lengthOf(apiTokens.valid, 0);
      assert.ok(stub.calledOnce);
      restoreSetTimeout();
    });
  });

  describe('emitImageMedia()', function () {
    beforeEach(function() {
      this.io = {
        to() {
          return this;
        },
        emit() {
          return true;
        },
      };

      this.emit = sinon.spy(this.io, 'emit');
    });

    afterEach(function() {
      this.emit.restore();
    });

    it('should emit media of type image with location', function () {
      const medias = [
        {
          type: 'image',
          location: {
            latitude: 0,
            longitude: 0,
          },
          id: '1',
          user: {
            username: 'someone',
          },
          images: {
            thumbnail: {
              url: 'localhost/i.png',
            },
          },
        },
      ];

      instagram.emitImageMedia(medias, this.io);
      assert.ok(this.emit.calledOnce);
    });

    it('should not emit media of type image without location', function () {
      const medias = [
        {
          type: 'image',
        },
      ];

      instagram.emitImageMedia(medias, this.io);
      assert.notOk(this.emit.called);
    });

    it('should not emit media of type non-image', function () {
      const medias = [
        {
          type: 'video',
          location: {
            latitude: 0,
            longitude: 0,
          },
        },
      ];

      instagram.emitImageMedia(medias, this.io);
      assert.notOk(this.emit.called);
    });
  });
});


<|start_filename|>test/app/actions/AppActions.js<|end_filename|>
'use strict';

import rewire from 'rewire';
import sinon from 'sinon';
import { assert } from 'chai';

const AppActions = rewire('../../../app/actions/AppActions');

describe('AppActions', function () {
  describe('updateFlow()', function () {
    beforeEach(function () {
      const socket = {
        emit() {},
      };

      this.emit = sinon.stub(socket, 'emit').yields();
      this.flowSuccessAction = sinon.stub(AppActions, 'updateFlowSuccess').returns(true);
      this.restoreSocket = AppActions.__set__('socket', socket);
    });

    afterEach(function () {
      this.emit.restore();
      this.flowSuccessAction.restore();
      this.restoreSocket();
    });

    it('should emit to correct event', function () {
      this.emit.withArgs('flow:pause');
      this.emit.withArgs('flow:start');

      AppActions.updateFlow('Pause');
      assert.ok(this.emit.withArgs('flow:pause').calledOnce);

      AppActions.updateFlow('Resume');
      assert.ok(this.emit.withArgs('flow:start').calledOnce);
    });

    it('should fire setFlow action', function () {
      AppActions.updateFlow('Pause');
      assert.ok(this.flowSuccessAction.called);
    });
  });
});


<|start_filename|>lib/instagram.js<|end_filename|>
'use strict';

import config from 'config';
import { instagram as instaApi } from 'instagram-node';

const api = instaApi();
const clientId = config.get('Instagram.client_id');
const clientSecret = config.get('Instagram.client_secret');
const redirectUri = config.get('Instagram.redirect_uri');
const subUri = config.get('Instagram.sub_uri');
const tag = config.get('Instagram.tag');
const verifyToken = config.get('Instagram.verify_token');

let apiTokens = {
  valid: [],
  expired: {},
};
let minTagId;
let busy = false;


export function setClientId() {
  api.use({
    /* eslint-disable */
    client_id: clientId,
    client_secret: clientSecret,
    /* eslint-enable */
  });
}

export function setUserToken() {
  /* eslint-disable */
  api.use({ access_token: apiTokens.valid[0] });
  /* eslint-enable */
}

export function getAuthUrl() {
  setClientId();
  return api.get_authorization_url(redirectUri);
}

export function handleAuth(code, next) {
  setClientId();
  api.authorize_user(code, redirectUri, (err, result) => {
    if (err) {
      return next(err);
    }

    return next(null, result);
  });
}

export function addSubscription(next) {
  setClientId();
  /* eslint-disable */
  api.add_tag_subscription(tag, subUri, {verify_token: verifyToken}, (err) => {
  /* eslint-enable */
    if (err) {
      return next(err);
    }

    return next();
  });
}

export function deleteSubscription(next) {
  setClientId();
  api.del_subscription({ all: true }, (err) => {
    if (err) {
      return next(err);
    }

    return next();
  });
}

export function getRecentMedia(next) {
  if (busy) {
    return next();
  }

  busy = true;

  setUserToken();
  /* eslint-disable */
  api.tag_media_recent(tag, { min_tag_id: minTagId }, (err, medias, pagination, remaining) => {
  /* eslint-enable */
    busy = false;

    if (err) {
      return next(err);
    }

    // Instagram pagination obj is inconsistent
    if (!pagination) {
      return next();
    }

    minTagId = pagination.min_tag_id;

    return next(null, medias, remaining);
  });
}

export function getValidTokens() {
  return apiTokens.valid;
}

export function setValidTokens(tokens) {
  apiTokens.valid = tokens;
}

export function addValidToken(token) {
  apiTokens.valid.push(token);
}

export function expireToken() {
  apiTokens.expired[Date.now() + 3600000] = apiTokens.valid.shift();
}

export function removeToken() {
  apiTokens.valid.shift();
}

export function recycleTokens(io, delay) {
  Object.keys(apiTokens.expired).forEach((timestamp) => {
    if (timestamp <= Date.now()) {
      apiTokens.valid.push(apiTokens.expired[timestamp]);
      delete apiTokens.expired[timestamp];
    }
  });

  if (apiTokens.valid.length > 0) {
    addSubscription(() => {
      io.emit('data:timeout', false, true);
    });
  } else {
    setTimeout(() => {
      recycleTokens(io, delay);
    }, delay);
  }
}

export function emitImageMedia(medias, io) {
  let data = [];

  medias.forEach((media) => {
    if (media.type === 'image' && media.location) {
      if ('latitude' in media.location && 'longitude' in media.location) {
        let caption = '';

        if (media.caption) {
          caption = media.caption.text;
        }

        data.unshift({
          caption,
          id: media.id,
          imgUrl: media.images.thumbnail.url,
          latLng: [media.location.latitude, media.location.longitude],
          profileUrl: `https://instagram.com/${media.user.username}`,
          user: media.user,
        });
      }
    }
  });

  if (data.length > 0) {
    io.to('realtime').emit('data:add', data, true);
  }
}


<|start_filename|>app/utils/prerender.js<|end_filename|>
'use strict';

import React from 'react';
import Iso from 'iso';
import Wrapper from '../components/Wrapper';

// webpack
import html from '../index.html';

export default (initData, scriptUrl, styleUrl, callback) => {
  const Component = React.createElement(Wrapper, { initData });

  // format the full page
  callback(null, html
    .replace('STYLE_URL', styleUrl)
    .replace('SCRIPT_URL', scriptUrl)
    .replace(
      'CONTENT',
      Iso.render(
        React.renderToStaticMarkup(Component),
        initData,
        { react: true }
      )
    )
  );
};


<|start_filename|>app/style/themes/default-theme.js<|end_filename|>
'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';

export default {
  spacing,
  contentFontFamily: 'Roboto, sans-serif',
  getPalette() {
    return {
      textColor: colors.red200,
      canvasColor: colors.red50,
      borderColor: colors.red100,
    };
  },
  getComponentThemes(palette) {
    let obj = {
      flatButton: {
        color: palette.canvasColor,
        hoverColor: colorManipulator.fade(palette.textColor, 0.2),
        textColor: palette.textColor,
      },
      githubButton: {
        color: 'rgba(0, 0, 0, .40)',
        hoverColor: colors.fullBlack,
        rippleColor: colorManipulator.fade(palette.textColor, 0.8),
      },
      toolbar: {
        backgroundColor: palette.canvasColor,
        borderColor: palette.borderColor,
        separatorColor: palette.borderColor,
        textColor: palette.textColor,
      },
      title: {
        color: palette.textColor,
        hoverColor: colors.red300,
      },
      listItem: {
        borderColor: '#EBEBEB',
        color: colorManipulator.fade('rgba(0, 0, 0, .035)', 0),
        hoverColor: 'rgba(0, 0, 0, .035)',
      },
    };

    return obj;
  },
};


<|start_filename|>app/stores/AppStore.js<|end_filename|>
'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;
  }

  onUpdateInstaData(data) {
    let imageData = data.concat(this.imageData);

    // limit imageData (used in list) to last 100
    while (imageData.length > 100) {
      imageData.pop();
    }

    this.focusMarker = {};
    this.newImageData = data;
    this.imageData = imageData;
  }

  onUpdateMarkers(markers) {
    this.newImageData = [];
    this.markers = markers;
  }

  onUpdateFocusedMarker(marker) {
    this.focusMarker = marker;
  }

  onUpdateFlowSuccess(newFlow) {
    this.flow = newFlow;
  }

  onUpdateTimeout(isActive) {
    this.timeout = isActive;
  }
}

export default alt.createStore(AppStore, 'AppStore');


<|start_filename|>config/webpack/devServer.config.js<|end_filename|>
'use strict';

module.exports = require('./makeConfig')({
  devServer: true,
  hotComponents: true,
  devtool: 'source-map',
  debug: true,
});


<|start_filename|>config/webpack/prerender.config.js<|end_filename|>
'use strict';

module.exports = require('./makeConfig')({
  prerender: true,
});


<|start_filename|>test/utils/compWithContext.js<|end_filename|>
'use strict';

import React from 'react';
import theme from '../../app/style/themes/default-theme';
import ThemeManager from 'material-ui/lib/styles/theme-manager';

const themeManager = new ThemeManager();
themeManager.setTheme(theme);

export default (Component) => {
  class CompWithContext extends React.Component {
    getChildContext() {
      return {
        muiTheme: themeManager.getCurrentTheme(),
      };
    }

    render() {
      return React.createElement(Component, this.props);
    }
  }

  CompWithContext.childContextTypes = {
    muiTheme: React.PropTypes.object,
  };

  return CompWithContext;
};


<|start_filename|>lib/services.js<|end_filename|>
'use strict';

import config from 'config';
import * as instagram from './instagram';
import { addUser, deleteUser } from './db';

// webpack
import renderApp from '../build/prerender/main.js';
import stats from '../build/stats.json';

const tag = config.get('Instagram.tag');
const verifyToken = config.get('Instagram.verify_token');
const repoUrl = config.get('Github.repoUrl');

let requestCnt = 0;
let timeout = true;

export function index(request, reply) {
  const options = request.server.app.options;
  const publicPath = stats.publicPath;
  const styleUrl = options.separateStylesheet && (`${publicPath}main.css?${stats.hash}`) || '';
  const scriptUrl = publicPath + [].concat(stats.assetsByChunkName.main)[0];
  const initData = {
    state: {
      AppStore: {
        timeout,
      },
    },
    ctx: {
      tag,
      repoUrl,
      loggedIn: request.auth.isAuthenticated,
    },
  };

  renderApp(initData, scriptUrl, styleUrl, (err, html) => {
    if (err) {
      console.log(err);
    }

    return reply(html);
  });
}

export function auth(request, reply) {
  reply.redirect(instagram.getAuthUrl());
}

export function handleAuth(request, reply) {
  const client = request.server.app.client;

  instagram.handleAuth(request.query.code, (err, result) => {
    if (err) {
      return reply(err);
    }

    addUser(client, result, (err, user) => {
      if (err) {
        return reply(err);
      }

      instagram.addValidToken(user.access_token);

      request.auth.session.set(user);
      reply.redirect('/');
    });
  });
}

export function logout(request, reply) {
  request.auth.session.clear();

  reply.redirect('/');
}

export function handleSubscription(request, reply) {
  const io = request.server.app.io;
  const client = request.server.app.client;

  if (request.method === 'get' && request.query.hub.mode === 'subscribe' && request.query.hub.verify_token === verifyToken) {
    return reply(request.query.hub.challenge);
  }

  // instagram provides 20 most recent
  if (++requestCnt === 20) {
    requestCnt = 0;

    const tokens = instagram.getValidTokens();
    if (tokens.length > 0) {
      timeout = false;

      instagram.getRecentMedia((err, medias, remaining) => {
        if (err) {
          if (err.error_type === 'OAuthAccessTokenException') {
            deleteUser(client, tokens[0], (err) => {
              if (err) {
                console.log(err);
              } else {
                instagram.removeToken();
              }
            });
          } else {
            console.log(err);
          }
        } else if (medias) {
          instagram.emitImageMedia(medias, io);

          console.log(`remaining: ${remaining}`);
        }
      });
    } else {
      timeout = true;

      instagram.deleteSubscription((err) => {
        if (err) {
          console.log(err);
        }

        io.emit('data:timeout', true, true);
        // check for available tokens every 10 seconds
        instagram.recycleTokens(io, 10000);
      });
    }
  }

  return reply();
}


<|start_filename|>config/webpack/makeConfig.js<|end_filename|>
'use strict';

var config = require('config');
var ExtractTextPlugin = require('extract-text-webpack-plugin');
var loadersByExtension = require('./loadersByExtension');
var path = require('path');
var StatsPlugin = require('stats-webpack-plugin');
var Webpack = require('webpack');

var host = config.get('Web.host');
var port = config.get('Web.webpack_port');

module.exports = function (options) {
  var entry = path.join(__dirname, '..', '..', 'app', options.prerender ? 'utils/prerender' : 'app');

  var loaders = {
    jsx: options.hotComponents ? ['react-hot-loader', 'babel-loader?stage=1'] : 'babel-loader?stage=1',
    png: 'url-loader?limit=10000',
    html: 'html-loader'
  };

  var stylesheetLoaders = {
    css: 'css-loader!autoprefixer-loader',
    less: 'css-loader!autoprefixer-loader!less-loader'
  };

  var additionalLoaders = [
    { test: /app\/.*\.js$/, loader: 'babel-loader' }
  ];

  var externals = [];
  var modulesDirectories = ['web_modules', 'node_modules'];
  var extensions = ['', '.js', '.jsx'];
  var root = path.join(__dirname, '..', '..', 'app', 'components');

  var publicPath = options.devServer ?
    'http://' + host + ':' + port + '/public/' :
    '/public/';

  var output = {
    path: path.join(__dirname, '..', '..', 'build', options.prerender ? 'prerender' : 'public'),
    publicPath: publicPath,
    filename: '[name].js' + (options.longTermCaching && !options.prerender ? '?[chunkhash]' : ''),
    chunkFilename: (options.devServer ? '[id].js' : '[name].js') + (options.longTermCaching && !options.prerender ? '?[chunkhash]' : ''),
    sourceMapFilename: 'debugging/[file].map',
    libraryTarget: options.prerender ? 'commonjs2' : undefined,
    pathinfo: options.debug || options.prerender,
  };

  var excludeFromStats = [
    /node_modules[\\\/]react[\\\/]/
  ];

  var plugins = [
    new Webpack.PrefetchPlugin('react'),
    new Webpack.PrefetchPlugin('react/lib/ReactComponentBrowserEnvironment')
  ];

  if (options.prerender) {
    plugins.push(new StatsPlugin(path.join(__dirname, '..', '..', 'build', 'stats.prerender.json'), {
      chunkModules: true,
      exclude: excludeFromStats
    }));

    externals.push(
      /^react(\/.*)?$/
    );

    plugins.push(new Webpack.optimize.LimitChunkCountPlugin({ maxChunks: 1 }));
  } else {
    plugins.push(new StatsPlugin(path.join(__dirname, '..', '..', 'build', 'stats.json'), {
      chunkModules: true,
      exclude: excludeFromStats
    }));
  }

  Object.keys(stylesheetLoaders).forEach(function (ext) {
    var stylesheetLoader = stylesheetLoaders[ext];

    if (Array.isArray(stylesheetLoader)) {
          stylesheetLoader = stylesheetLoader.join('!');
        }

    if (options.prerender) {
      stylesheetLoaders[ext] = stylesheetLoader.replace(/^css-loader/, 'css-loader/locals');
    } else if (options.separateStylesheet) {
      stylesheetLoaders[ext] = ExtractTextPlugin.extract('style-loader', stylesheetLoader);
    } else {
      stylesheetLoaders[ext] = 'style-loader!' + stylesheetLoader;
    }
  });

  if (options.separateStylesheet && !options.prerender) {
    plugins.push(new ExtractTextPlugin('[name].css' + (options.longTermCaching ? '?[contenthash]' : '')));
  }

  if (options.minimize && !options.prerender) {
    plugins.push(
      new Webpack.optimize.UglifyJsPlugin({
        compressor: {
          warnings: false
        }
      }),
      new Webpack.optimize.DedupePlugin()
    );
  }

  if (options.minimize) {
    plugins.push(
      new Webpack.DefinePlugin({
        'process.env': {
          NODE_ENV: '"production"'
        }
      }),
      new Webpack.NoErrorsPlugin()
    );
  }

  return {
    entry: entry,
    output: output,
    target: options.prerender ? 'node' : 'web',
    module: {
      loaders: loadersByExtension(loaders).concat(loadersByExtension(stylesheetLoaders)).concat(additionalLoaders)
    },
    devtool: options.devtool,
    debug: options.debug,
    resolveLoader: {
      root: path.join(__dirname, '..', '..', 'node_modules')
    },
    externals: externals,
    resolve: {
      root: root,
      modulesDirectories: modulesDirectories,
      extensions: extensions
    },
    plugins: plugins,
    devServer: {
      host: host,
      port: port,
      stats: {
        cached: false,
        exclude: excludeFromStats
      }
    }
  };
};


<|start_filename|>app/actions/AppActions.js<|end_filename|>
'use strict';

import alt from '../alt';
import io from 'socket.io-client';

let socket;
if (typeof window !== 'undefined') {
  socket = io();
}

class AppActions {
  constructor() {
    this.generateActions(
      'updateFocusedMarker',
      'updateFlowSuccess',
      'updateInstaData',
      'updateMarkers',
      'updateTimeout'
    );
  }

  updateFlow(curFlow) {
    if (curFlow === 'Pause') {
      socket.emit('flow:pause', () => this.actions.updateFlowSuccess('Resume'));
    } else {
      socket.emit('flow:start', () => this.actions.updateFlowSuccess('Pause'));
    }
  }
}

export default alt.createActions(AppActions);


<|start_filename|>app/app.js<|end_filename|>
'use strict';

import React from 'react';
import Iso from 'iso';
import Wrapper from './components/Wrapper';

require('babel/polyfill');

Iso.on('react', true, function (initData, _, node) {
  React.render(React.createElement(Wrapper, { initData }), node);
});


<|start_filename|>test/app/stores/AppStore.js<|end_filename|>
'use strict';

import alt from '../../../app/alt';
import AppActions from '../../../app/actions/AppActions';
import AppStore from '../../../app/stores/AppStore';
import { assert } from 'chai';

describe('AppStore', function () {
  before(function () {
    this.defaultState = AppStore.getState();
  });

  afterEach(function () {
    alt.recycle();
  });

  describe('onUpdateInstaData()', function () {
    it('should update relevant app state', function () {
      const data = ['foo', 'bar'];
      const action = AppActions.UPDATE_INSTA_DATA;

      alt.dispatcher.dispatch({ action, data });
      assert.deepEqual(AppStore.getState().newImageData, data);
      assert.deepEqual(AppStore.getState().imageData, data);
      assert.deepEqual(AppStore.getState().focusMarker, {});
      assert.equal(AppStore.getState().flow, this.defaultState.flow);
    });
  });

  describe('onUpdateFocusedMarker()', function () {
    it('should update focusMarker state', function () {
      const data = 'foo';
      const action = AppActions.UPDATE_FOCUSED_MARKER;

      alt.dispatcher.dispatch({ action, data });
      assert.equal(AppStore.getState().focusMarker, data);
      assert.deepEqual(AppStore.getState().newImageData, this.defaultState.newImageData);
      assert.deepEqual(AppStore.getState().imageData, this.defaultState.imageData);
      assert.equal(AppStore.getState().flow, this.defaultState.flow);
    });
  });

  describe('onUpdateFlowSuccess()', function () {
    it('should update flow state', function () {
      const data = 'Resume';
      const action = AppActions.UPDATE_FLOW_SUCCESS;

      alt.dispatcher.dispatch({ action, data });
      assert.equal(AppStore.getState().flow, data);
      assert.deepEqual(AppStore.getState().newImageData, this.defaultState.newImageData);
      assert.deepEqual(AppStore.getState().imageData, this.defaultState.imageData);
      assert.deepEqual(AppStore.getState().focusMarker, this.defaultState.focusMarker);
    });
  });
});


<|start_filename|>app/utils/stylePropable.js<|end_filename|>
'use strict';

import autoPrefix from 'material-ui/lib/styles/auto-prefix';
import extend from 'material-ui/lib/utils/extend';

export function mergeAndPrefix() {
  const args = Array.prototype.slice.call(arguments, 0);
  let base = args[0];
  let i;

  for (i = 1; i < args.length; i++) {
    if (args[i]) {
      base = extend(base, args[i]);
    }
  }

  return autoPrefix.all(base);
}
