/* eslint-env node */
module.exports = {
  test_page: 'tests/index.html?hidepassed',
  disable_watching: true,
  launch_in_ci: [
    'Chromium'
  ],
  launch_in_dev: [
    'Chromium'
  ],
  browser_args: {
    Chromium: [
      '--disable-gpu',
      '--headless',
      '--no-sandbox',
      '--remote-debugging-port=9222',
      '--window-size=1440,900'
    ]
  }
};

/* eslint-env node */
module.exports = {
  test_page: 'tests/index.html?hidepassed',
  disable_watching: true,
  launch_in_ci: [
    'Chromium'
  ],
  launch_in_dev: [
    'Chromium'
  ],
  browser_args: {
    Chromium: [
      '--disable-gpu',
      '--headless',
      '--no-sandbox',
      '--remote-debugging-address=0.0.0.0',
      '--remote-debugging-port=9222',
      '--window-size=1440,900'
    ]
  }
};

Add remote debugging host setting


/* eslint-env node */

const FormData = require('form-data');
const got = require('got');

const BASE_URL = 'https://debuglogs.org';

// Workaround: Submitting `FormData` using native `FormData::submit` procedure
// as integration with `got` results in S3 error saying we haven’t set the
// `Content-Length` header:
const submitFormData = (form, url) =>
  new Promise((resolve, reject) => {
    form.submit(url, (error) => {
      if (error) {
        return reject(error);
      }

      return resolve();
    });
  });

//      upload :: String -> Promise URL
exports.upload = async (content) => {
  const signedForm = await got.get(BASE_URL, { json: true });
  const { fields, url } = signedForm.body;

  const form = new FormData();
  form.append('key', fields.key);

  Object.entries(fields)
    .filter(([key]) => key !== 'key')
    .forEach(([key, value]) => {
      form.append(key, value);
    });

  const contentBuffer = Buffer.from(content, 'utf8');
  const contentType = 'text/plain';
  form.append('Content-Type', contentType);
  form.append('file', contentBuffer, {
    contentType,
    filename: 'signal-desktop-debug-log.txt',
  });

  await submitFormData(form, url);

  return `${BASE_URL}/${fields.key}`;
};

/* eslint-env node */

const FormData = require('form-data');
const got = require('got');

const BASE_URL = 'https://debuglogs.org';

// Workaround: Submitting `FormData` using native `FormData::submit` procedure
// as integration with `got` results in S3 error saying we haven’t set the
// `Content-Length` header:
const submitFormData = (form, url) =>
  new Promise((resolve, reject) => {
    form.submit(url, (error) => {
      if (error) {
        return reject(error);
      }

      return resolve();
    });
  });

//      upload :: String -> Promise URL
exports.upload = async (content) => {
  const signedForm = await got.get(BASE_URL, { json: true });
  const { fields, url } = signedForm.body;

  const form = new FormData();
  // The API expects `key` to be the first field:
  form.append('key', fields.key);
  Object.entries(fields)
    .filter(([key]) => key !== 'key')
    .forEach(([key, value]) => {
      form.append(key, value);
    });

  const contentBuffer = Buffer.from(content, 'utf8');
  const contentType = 'text/plain';
  form.append('Content-Type', contentType);
  form.append('file', contentBuffer, {
    contentType,
    filename: 'signal-desktop-debug-log.txt',
  });

  await submitFormData(form, url);

  return `${BASE_URL}/${fields.key}`;
};

Document why `key` comes first


/*
    ___ usage ___ en_US ___
    usage: prolific udp <options>

        -u, --url <string>
            The URL of the logging destination.

        --help
            Display this message.

    ___ $ ___ en_US ___

    ___ . ___
*/

require('arguable')(module, require('cadence')(function (async, program) {
    program.helpIf(program.command.params.help)
    var response = {
        moduleName: 'prolific.udp/udp.processor',
        parameters: { params: program.command.param },
        argv: program.argv,
        terminal: program.command.terminal
    }
    if (process.mainModule == module) {
        console.log(response)
    }
    return response
}))

module.exports.isProlific = true

Add flag for new module loader.


import React from 'react';
import { storiesOf } from '@storybook/react';
import { text, boolean } from '@storybook/addon-knobs';
import notes from './notes.md';
import Heading from './heading';

storiesOf('Heading', module)
  .add('default', () => {
    const title = text('title', 'This is a heading');
    const children = text('children', 'This is content beneath a heading');
    const titleId = text('titleId', '');
    const subheader = text('subheader', 'This is a subheading');
    const subtitleId = text('subtitleId', '');
    const help = text('help', '');
    const helpLink = text('helpLink', '');
    const backLink = text('backLink', '');
    const divider = boolean('divider', Heading.defaultProps.divider);
    const separator = boolean('separator', Heading.defaultProps.separator);

    return (
      <Heading
        title={ title }
        titleId={ titleId }
        subheader={ subheader }
        subtitleId={ subtitleId }
        help={ help }
        helpLink={ helpLink }
        backLink={ backLink }
        divider={ divider }
        separator={ separator }
      >
        {children}
      </Heading>
    );
  }, {
    notes: { markdown: notes },
    knobs: { escapeHTML: false }
  });

import React from 'react';
import { storiesOf } from '@storybook/react';
import { text, boolean } from '@storybook/addon-knobs';
import notes from './notes.md';
import Heading from './heading';

storiesOf('Heading', module)
  .add('default', () => {
    const title = text('title', 'This is a heading');
    const children = text('children', 'This is content beneath a heading');
    const subheader = text('subheader', 'This is a subheading');
    const help = text('help', '');
    const helpLink = text('helpLink', '');
    const backLink = text('backLink', '');
    const divider = boolean('divider', Heading.defaultProps.divider);
    const separator = boolean('separator', Heading.defaultProps.separator);

    return (
      <Heading
        title={ title }
        subheader={ subheader }
        help={ help }
        helpLink={ helpLink }
        backLink={ backLink }
        divider={ divider }
        separator={ separator }
      >
        {children}
      </Heading>
    );
  }, {
    notes: { markdown: notes },
    knobs: { escapeHTML: false }
  });

Remove Unneeded Props from Heading Story


const test = require('ava')
const { tmpdir } = require('os')
const { join } = require('path')
const rollup = require('rollup')
const config = require('../rollup.config')

test.skip('build: creates a valid bundle', async assert => {
  const bundle = await rollup.rollup(config)
  const file = join(tmpdir(), 'bundle.js')
  await bundle.write({
    ...config.output.find(object => object.file === 'dist/index.js'),
    file
  })
  const AbstractSyntaxTree = require(file)
  const tree = new AbstractSyntaxTree('const foo = 42')
  const literal = tree.find('Literal')
  assert.truthy(literal)
})

const test = require('ava')
const { tmpdir } = require('os')
const { join } = require('path')
const rollup = require('rollup')
const config = require('../rollup.config')

test('build: creates a valid bundle', async assert => {
  const bundle = await rollup.rollup(config)
  const file = join(tmpdir(), 'bundle.js')
  await bundle.write({
    ...config.output.find(object => object.file === 'dist/index.js'),
    file
  })
  const AbstractSyntaxTree = require(file)
  const tree = new AbstractSyntaxTree('const foo = 42')
  const literal = tree.find('Literal')
  assert.truthy(literal)
})

Enable the skipped test case


import React from 'react';
import { graphql } from 'react-apollo';
import PropTypes from 'prop-types';

import GameShow from '../components/GameShow/GameShow';
import Loader from '../components/Loader';

import fullGameQuery from '../graphql/fullGame.graphql';

const withGame = graphql(fullGameQuery, {
  props: ({ data }) => {
    if (!data.game) return { loading: data.loading };
    if (data.error) return { hasErrors: true };
    return {
      game: data.game,
    };
  },
  options: (props) => ({ variables: { id: props.match.params.id } })
});

const GamePage = ({ game, loading }) => (
  loading
  ? <Loader />
  : <GameShow game={game} />
);

GamePage.propTypes = {
  loading: PropTypes.bool,
  game: PropTypes.object,
};

GamePage.defaultProps = {
  loading: false,
  game: {}
};

const GamePageWithData = withGame(GamePage);

export default GamePageWithData;

import React from 'react';
import { graphql } from 'react-apollo';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';

import GameShow from '../components/GameShow/GameShow';
import Loader from '../components/Loader';

import fullGameQuery from '../graphql/fullGame.graphql';

const mapStateToProps = ({ game }) => (
  {
    ...game
  }
);

const withGame = graphql(fullGameQuery, {
  props: ({ data }) => {
    if (!data.game) return { loading: data.loading };
    if (data.error) return { hasErrors: true };
    return {
      game: data.game,
    };
  },
  options: (props) => ({ variables: { id: props.match.params.id } })
});

const GamePage = ({ game, loading, isDownloading, isInstalling }) => (
  loading
  ? <Loader />
  : <GameShow
    game={game}
    isDownloading={isDownloading}
    isInstalling={isInstalling}
  />
);

GamePage.propTypes = {
  loading: PropTypes.bool,
  game: PropTypes.object,
  isDownloading: PropTypes.bool,
  isInstalling: PropTypes.bool
};

GamePage.defaultProps = {
  loading: false,
  game: {},
  isDownloading: false,
  isInstalling: false
};

const GamePageWithProps = connect(mapStateToProps, null)(GamePage);
const GamePageWithData = withGame(GamePageWithProps);

export default GamePageWithData;

Add state to game page


const fountain = require('fountain-generator');

module.exports = fountain.Base.extend({
  prompting() {
    this.fountainPrompting();
  },

  configuring() {
    this.mergeJson('package.json', {
      dependencies: {
        'axios': '^0.9.1',
        'es6-promise': '^3.1.2'
      }
    });

    if (this.props.js === 'typescript') {
      this.env.addToTsd = `"axios": "github:DefinitelyTyped/DefinitelyTyped/axios/axios.d.ts#bcd5761826eb567876c197ccc6a87c4d05731054",`;
    }
  },

  writing: {
    src() {
      [
        'src/index.js',
        'src/index.css',
        'src/app/footer.js',
        'src/app/header.js',
        'src/app/main.js',
        'src/app/title.js',
        'src/app/techs/tech.js',
        'src/app/techs/techs.js'
      ].map(file => this.copyTemplate(file, file));
    },

    techs() {
      this.prepareTechJson();
    }
  }
});

const fountain = require('fountain-generator');

module.exports = fountain.Base.extend({
  prompting() {
    this.fountainPrompting();
  },

  configuring() {
    this.mergeJson('package.json', {
      dependencies: {
        'axios': '^0.9.1',
        'es6-promise': '^3.1.2'
      }
    });

    if (this.props.js === 'typescript') {
      this.env.addToTsd = `"axios": "github:DefinitelyTyped/DefinitelyTyped/axios/axios.d.ts#bcd5761826eb567876c197ccc6a87c4d05731054",`;
    }
  },

  writing: {
    src() {
      [
        'src/index.js',
        'src/index.css',
        'src/app/footer.js',
        'src/app/footer.spec.js',
        'src/app/header.js',
        'src/app/main.js',
        'src/app/title.js',
        'src/app/techs/tech.js',
        'src/app/techs/techs.js'
      ].map(file => this.copyTemplate(file, file));
    },

    techs() {
      this.prepareTechJson();
    }
  }
});

Add spec file in yo write


var Fleur = require('..');

var Foo = Fleur.actions('foo', {
  synchronous: function(state) {
    return Fleur.assign(state, {
      status: 'pending'
    });
  },
  asynchronous: function(state, dispatch) {
    dispatch(Foo.synchronous());
    return Fleur.promise(function(resolve) {
      setTimeout(function() {
        resolve(Fleur.assign(state, {
          items: [
            { title: 'foo', id: 1 },
            { title: 'bar', id: 2 },
            { title: 'baz', id: 3 }
          ],
          status: 'success'
        }));
      }, 100);
    });
  }
});

var store = Fleur.store();
store.dispatch(Foo.asynchronous())
  .then(function() {
    console.log(store.getState());
  });

var Fleur = require('..');

var Foo = Fleur.actions('foo', {
  synchronous: function(state) {
    return Fleur.assign(state, {
      status: 'pending'
    });
  },
  asynchronous: function(state, dispatch) {
    dispatch(Foo.synchronous());
    return Fleur.promise(function(resolve) {
      setTimeout(function() {
        resolve(Fleur.assign(state, {
          items: [
            { title: 'foo', id: 1 },
            { title: 'bar', id: 2 },
            { title: 'baz', id: 3 }
          ],
          status: 'success'
        }));
      }, 100);
    });
  }
});

var log = function(object) {
  console.log(JSON.stringify(object, null, 2));
};

var store = Fleur.store();
store.dispatch(Foo.asynchronous())
  .then(function() {
    log(store.getState());
  });

Add a custom `log` to the example


/* RESTClient
 * Core interface that houses the REST adapter and revision of the API to target.
 * Setting a Client is optional and will automatically use a base client.
 *
 * You can define a custom client on your app like you would DS.Store in ember-data
 * Assign a revision number to be notified of breaking changes to the API
 */
(function() {

'use strict';

RESTless.RESTClient = Em.Object.extend({
  revision: RESTless.CURRENT_API_REVISION,
  adapter: RESTless.RESTAdapter.create()
});

// Set a default client
RESTless.set('client', RESTless.RESTClient.create());

// Detects 'RESTClient' on the Application implementaion namespace
// then sets that as the default client if found
// i.e. App.RESTClient = RESTless.RESTClient.create({ ... });
Ember.onLoad('Ember.Application', function(Application) {
  Application.initializer({
    name: 'RESTClient',
    initialize: function(container, application) {
      if(application.RESTClient) {
        RESTless.set('client', application.RESTClient);
      }
    }
  });
});

})();

/* RESTClient
 * Core interface that houses the REST adapter and revision of the API to target.
 * Setting a Client is optional and will automatically use a base client.
 *
 * You can define a custom client on your app like you would DS.Store in ember-data
 * Assign a revision number to be notified of breaking changes to the API
 */
(function() {

'use strict';

RESTless.RESTClient = Em.Object.extend({
  revision: RESTless.CURRENT_API_REVISION,
  adapter: function(key, value) {
    if (arguments.length === 1) {
      return RESTless.RESTAdapter.create();
    } else {
      //Allow the adapter to be set by Class or string
      if(typeof value === 'string') {
        var adapterClass = Em.get(window, value);
        Ember.assert('The adapter of type: "' + value + '" was not found.', adapterClass);
        return adapterClass;
      }
      return value;
    }
  }.property()
});

// Set a default client
RESTless.set('client', RESTless.RESTClient.create());

// Detects 'RESTClient' on the Application implementaion namespace
// then sets that as the default client if found
// i.e. App.RESTClient = RESTless.RESTClient.create({ ... });
Ember.onLoad('Ember.Application', function(Application) {
  Application.initializer({
    name: 'RESTClient',
    initialize: function(container, application) {
      if(application.RESTClient) {
        RESTless.set('client', application.RESTClient);
      }
    }
  });
});

})();

Allow adapter to be set by Class name string


$(function() {

  $("#student-searchbar").autocomplete({
    source: function(request, response) {
      $.ajax({
        url: "/students/names",
        data: {
          q: request.term
        },
        success: function(data) {
          response(data);
        }
      });
    },
    focus: function(e, ui) {
      e.preventDefault();
      $(this).val(ui.item.label);
    },
    select: function(e, ui) {
      e.preventDefault();
      window.location.pathname = '/students/' + ui.item.value;
    },
  });

});

$(function() {

  $("#student-searchbar").autocomplete({
    source: function(request, response) {
      $.ajax({
        url: "/students/names",
        data: {
          q: request.term
        },
        success: function(data) {

          if (data.length === 0) {
            data = [{ label: 'No matching students', value: null }];
          };

          response(data);
        }
      });
    },
    focus: function(e, ui) {
      e.preventDefault();
      $(this).val(ui.item.label);
    },
    select: function(e, ui) {
      e.preventDefault();

      if (ui.item.label === 'No matching students') {
        $(this).val('');
      } else {
        window.location.pathname = '/students/' + ui.item.value;
      }

    },
  });

});

Make student searchbar noisier when there are no results


'use strict';

module.exports = {
	defaultLangTag: 'en',
	primary: [
		'ar',
		'da',
		'de',
		'en',
		'es',
		'fi',
		'fr',
		'ja',
		'ko',
		'nb',
		'nl',
		'pt',
		'sv',
		'tr',
		'zh'
	],
	regions: [
		'ar-SA',
		'da-DK',
		'de-DE',
		'en-CA',
		'en-GB',
		'en-US',
		'es-MX',
		'fi-FI',
		'fr-CA',
		'fr-FR',
		'fr-ON',
		'ko-KR',
		'nb-NO',
		'nl-NL',
		'pt-BR',
		'sv-SE',
		'tr-TR',
		'zh-CN',
		'zh-TW'
	],
	aliases: {
		'en-AU': 'en-GB'
	}
};

'use strict';

module.exports = {
	defaultLangTag: 'en',
	primary: [
		'ar',
		'da',
		'de',
		'en',
		'es',
		'fi',
		'fr',
		'ja',
		'ko',
		'nb',
		'nl',
		'pt',
		'sv',
		'tr',
		'zh'
	],
	regions: [
		'ar-SA',
		'cy-GB',
		'da-DK',
		'de-DE',
		'en-CA',
		'en-GB',
		'en-US',
		'es-MX',
		'es-ES',
		'fi-FI',
		'fr-CA',
		'fr-FR',
		'fr-ON',
		'ko-KR',
		'nb-NO',
		'nl-NL',
		'pt-BR',
		'sv-SE',
		'tr-TR',
		'zh-CN',
		'zh-TW'
	],
	aliases: {
		'en-AU': 'en-GB'
	}
};

Add cy-GB and es-ES languages


/**
 * Main mixin for the Bolt buic module.
 *
 * @mixin
 * @namespace Bolt.buic
 *
 * @param {Object} bolt - The Bolt module.
 * @param {Object} $ - jQuery.
 */
(function (bolt, $) {
    'use strict';

    /**
     * Bolt.buic mixin container.
     *
     * @private
     * @type {Object}
     */
    var buic = {};

    /**
     * Initializes the fields, optionally based on a context.
     *
     * @function init
     * @memberof Bolt.buic
     * @param context
     */
    buic.init = function(context) {

        if (typeof context === 'undefined') {
            context = $(document.documentElement);
        }

        $('.buic-checkbox', context).each(function () {
            bolt.buic.checkbox.init(this);
        });
        $('.buic-listing', context).each(function () {
            bolt.buic.listing.init(this);
        });
        $('.buic-moment', context).each(function () {
            bolt.buic.moment.init(this);
        });
        $('.buic-progress', context).each(function () {
            bolt.buic.progress.init(this);
        });
        $('.buic-select', context).each(function () {
            bolt.buic.select.init(this);
        });
    };

    // Add placeholder for buic.
    bolt.buic = buic;

})(Bolt || {}, jQuery);

/**
 * Main mixin for the Bolt buic module.
 *
 * @mixin
 * @namespace Bolt.buic
 *
 * @param {Object} bolt - The Bolt module.
 * @param {Object} $ - jQuery.
 */
(function (bolt, $) {
    'use strict';

    /**
     * Bolt.buic mixin container.
     *
     * @private
     * @type {Object}
     */
    var buic = {};

    /**
     * Initializes the fields, optionally based on a context.
     *
     * @function init
     * @memberof Bolt.buic
     * @param context
     */
    buic.init = function(context) {

        if (typeof context === 'undefined') {
            context = $(document.documentElement);
        }

        $('.buic-checkbox', context).each(function () {
            bolt.buic.checkbox.init(this);
        });
        $('.buic-listing', context).each(function () {
            bolt.buic.listing.init(this);
        });
        $('.buic-moment', context).each(function () {
            bolt.buic.moment.init(this);
        });
        $('.buic-select', context).each(function () {
            bolt.buic.select.init(this);
        });
        // Widgets
        $('.buic-progress', context).progress();
    };

    // Add placeholder for buic.
    bolt.buic = buic;

})(Bolt || {}, jQuery);

Initialize bolt progress bar widget


describe('Praad App', function() {
  'use strict';

  describe('Offer list view', function() {
    var offers, clickPromise;

    beforeEach(function() {
      browser.manage().deleteAllCookies();
      browser.get('/');
      clickPromise = element(by.css('.popup')).element(by.css('label')).click();
      offers = element.all(by.repeater('offer in restaurant.offers'));
    });

    it('should initially have 3 offers', function() {
      clickPromise.then(function() {
        expect(offers.count()).toBe(13);
      });
    });

    it('should filter the offer list as user types into the search box', function() {
      clickPromise.then(function() {
        element(by.model('search.query')).sendKeys('kana');

        expect(offers.count()).toBe(1);
        var title = offers.first().element(by.binding('offer.title')).getText();
        expect(title).toBe('Pekingi kana');
      });
    });
  });
});

describe('Praad App', function() {
  'use strict';

  describe('Offer list view', function() {
    var offers, clickPromise;

    beforeEach(function() {
      browser.manage().deleteAllCookies();
      browser.get('/');
      clickPromise = element(by.css('.popup')).element(by.css('label')).click();
      offers = element.all(by.repeater('offer in restaurant.offers'));
    });

    it('should initially have 3 offers', function() {
      clickPromise.then(function() {
        expect(offers.count()).toBe(37);
      });
    });

    it('should filter the offer list as user types into the search box', function() {
      clickPromise.then(function() {
        element(by.model('search.query')).sendKeys('kana');

        expect(offers.count()).toBe(11);
        var title = offers.first().element(by.binding('offer.title')).getText();
        expect(title).toBe('Hiina menüü - Chicken eggplant');
      });
    });
  });
});

Fix e2e tests for mock data changes


// @flow
import * as React from 'react';

import HouseholdForm from './form/head-of-household';
import AddressForm from './form/address';
import PhoneNumbers from './form/phone-numbers';
import ChildForm from './form/child';
import { Row, Col, Button } from 'react-bootstrap';
import { Form } from 'neoform';
import { FormValidation } from 'neoform-validation';

const Household = ({ onSubmit, onInvalid, validate, data, addChild, removeChild, affiliations, saved }) => {
  return (
        <form
            onSubmit={e => {
              e.preventDefault();
              validate(onSubmit, onInvalid);
            }}
        >
            <HouseholdForm />
            <AddressForm />
            <PhoneNumbers />
            <ChildForm
                nominations={data.nominations}
                addChild={addChild}
                removeChild={removeChild}
                affiliations={affiliations}
            />
            <Row>
                <Col xs={12}>
                    <Button type="submit">Save Draft</Button>
                    {saved && <Button type="submit">Publish</Button>}
                </Col>
            </Row>
        </form>
  );
};

export default Form(FormValidation(Household));

// @flow
import * as React from 'react';

import HouseholdForm from './form/head-of-household';
import AddressForm from './form/address';
import PhoneNumbers from './form/phone-numbers';
import ChildForm from './form/child';
import { Row, Col, Button } from 'react-bootstrap';
import { Form } from 'neoform';
import { FormValidation } from 'neoform-validation';

const Household = ({ onSubmit, onInvalid, validate, data, addChild, removeChild, affiliations, saved }) => {
  return (
        <form
            onSubmit={e => {
              e.preventDefault();
              validate(onSubmit, onInvalid);
            }}
        >
            <HouseholdForm />
            <AddressForm />
            <PhoneNumbers />
            <ChildForm
                nominations={data.nominations}
                addChild={addChild}
                removeChild={removeChild}
                affiliations={affiliations}
            />
            <Row>
                <Col xs={12}>
                    <Button type="submit">Save Draft</Button>
                    {saved && <Button type="submit">Submit Nomination</Button>}
                </Col>
            </Row>
        </form>
  );
};

export default Form(FormValidation(Household));

Change wording of nomination submit


var express = require('express');
var User = require('../schemas/userSchema.js');

var app = express();

app.get('/users', function (req, res, next) {
  res.json({
    "users": [req.user]
  });
});

app.put('/users/:id', function (req, res, next) {
  User.findById(req.params.id).exec().then(function (user) {
    user.projects = req.body.user.projects;
    user.save();

    res.json({
      "user": user
    });
  }, next);
});

module.exports = app;

var express = require('express');
var User = require('../schemas/userSchema.js');

var app = express();

app.get('/users', function (req, res, next) {
  if (req.isAuthenticated()) {
    res.json({
      "users": [req.user]
    });
  } else {
    res.json({
      "users": [{
        _id: "anonymous user"
      }]
    })
  }
});

app.put('/users/:id', function (req, res, next) {
  if (!req.isAuthenticated()) {
    res.send(401, 'You must be logged in to join a project');
  }

  User.findById(req.params.id).exec().then(function (user) {
    user.projects = req.body.user.projects;
    user.save();

    res.json({
      "user": user
    });
  }, next);
});

module.exports = app;

Create the anonymous user server side instead


var expect = require('chai').expect;
var thirteen = require('../index.js');

describe('thirteen', function () {
  it('should be a function', function () {
    expect(thirteen).to.be.a("function");
  });

  it('should return its argument multiplied by thirteen', function () {
    expect(thirteen(10)).to.eql(130);
  });

  it('should NOT return its argument multiplied by twelve', function () {
    expect(thirteen(10)).to.not.eql(120);
  });

  it('should return its arguments number-like value multiplied by thirteen', function () {
    var wannabeString = {};
    var wannabeValue = {};

    wannabeString.toString = function () { return '13'; };
    wannabeValue.valueOf = function () { return '13'; };

    expect(thirteen(wannabeString)).to.eql(169);
    expect(thirteen(wannabeValue)).to.eql(169);
  });
});

var expect = require('chai').expect;
var thirteen = require('../index.js');

describe('thirteen', function () {
  it('should be a function', function () {
    expect(thirteen).to.be.a("function");
  });

  it('should return its argument multiplied by thirteen', function () {
    // Cast to number from string for test comparison. Test will pass even if value is returned with more force (i.e. "130!!!!!!!!").
    var returnedAsNumber = parseFloat(thirteen(10).replace(/\!/g, ""));
    expect(returnedAsNumber).to.eql(130);
  });

  it('should NOT return its argument multiplied by twelve', function () {
    var returnedAsNumber = parseFloat(thirteen(10).replace(/\!/g, ""));
    expect(returnedAsNumber).to.not.eql(120);
  });

  it('should return its arguments number-like value multiplied by thirteen', function () {
    var wannabeString = {};
    var wannabeValue = {};

    wannabeString.toString = function () { return '13'; };
    wannabeValue.valueOf = function () { return '13'; };

    expect(thirteen(wannabeString)).to.eql(169);
    expect(thirteen(wannabeValue)).to.eql(169);
  });
});

Fix test for returning correct value. String trimmed and parsed.


'use strict'

import React from 'react'
import {
	View,
	TouchableHighlight,
  Image,
	Text,
  Modal
} from 'react-native';

var css = require('../../styles/css');

// Modal with text information content and a large button (usually for dismissing the modal)
export default class InfoModal extends React.Component {
  _onPress = () => {
    if (this.props.onPress) {
      this.props.onPress(); //callback when modal is pressed
    }
  }
  render() {
    return(
      <Modal animationType={'none'} transparent={true} visible={this.props.modalVisible}>
        <View style={css.modal_container}>
          <Text style={css.modal_text_intro}>{this.props.title}</Text>
          <Text style={css.modal_text}>
            {this.props.children}
          </Text>

          <TouchableHighlight underlayColor={'rgba(200,200,200,.5)'} onPress={this._onPress}>
            <View style={css.modal_button}>
              <Text style={css.modal_button_text}>{this.props.buttonText}</Text>
            </View>
          </TouchableHighlight>
        </View>
      </Modal>
    );
  }
}

'use strict'

import React from 'react'
import {
	View,
	TouchableHighlight,
  Image,
	Text,
  Modal
} from 'react-native';

var css = require('../../styles/css');

// Modal with text information content and a large button (usually for dismissing the modal)
export default class InfoModal extends React.Component {
  _onPress = () => {
    if (this.props.onPress) {
      this.props.onPress(); //callback when modal is pressed
    }
  }
  render() {
    return(
      <Modal animationType={'none'} transparent={true} visible={this.props.modalVisible} onRequestClose={() => {this.props.onRequestClose}}>
        <View style={css.modal_container}>
          <Text style={css.modal_text_intro}>{this.props.title}</Text>
          <Text style={css.modal_text}>
            {this.props.children}
          </Text>

          <TouchableHighlight underlayColor={'rgba(200,200,200,.5)'} onPress={this._onPress}>
            <View style={css.modal_button}>
              <Text style={css.modal_button_text}>{this.props.buttonText}</Text>
            </View>
          </TouchableHighlight>
        </View>
      </Modal>
    );
  }
}

Fix required parameter for Modal popup


'use babel';

import { CompositeDisposable } from 'atom';

export default {

  subscriptions: null,

  activate(state) {
    // Events subscribed to in atom's system can be easily cleaned up with a CompositeDisposable
    this.subscriptions = new CompositeDisposable();

    // Register command that toggles this view
    this.subscriptions.add(atom.commands.add('atom-workspace', {
      'elixir-alphabetize:beautify': () => this.beautify()
    }));
  },

  deactivate() {
    this.subscriptions.dispose();
  },

  beautify() {
    let editor
    if (editor = atom.workspace.getActiveTextEditor()) {
      all_text = editor.getText().split('\n')
      any = false
      start = 0
      end = 0
      for (var i = 0, len = all_text.length; i < len; i++) {
        line = all_text[i]
        if (!any) {
          if (line.includes("alias") || line.includes("require") || line.includes("use")) {
            start = i
            any = true
          }
        } else {
          if (!line.includes("alias") && !line.includes("require") && !line.includes("use")) {
            end = i
            break
          }
        }
      }

      relevant_lines = all_text.slice(start, end).sort()
      Array.prototype.splice.apply(all_text, [1, relevant_lines.length].concat(relevant_lines))
      editor.setText(all_text.join('\n'))
    }
  }
};

'use babel';

import { CompositeDisposable } from 'atom';

export default {

  subscriptions: null,

  activate(state) {
    // Events subscribed to in atom's system can be easily cleaned up with a CompositeDisposable
    this.subscriptions = new CompositeDisposable();

    // Register command that toggles this view
    this.subscriptions.add(atom.commands.add('atom-workspace', {
      'elixir-alphabetize:beautify': () => this.beautify()
    }));
  },

  deactivate() {
    this.subscriptions.dispose();
  },

  beautify() {
    let editor
    if (editor = atom.workspace.getActiveTextEditor()) {
      all_text = editor.getText().split('\n')
      any_found = false
      start = 0
      end = 0
      for (var i = 0, len = all_text.length; i < len; i++) {
        line = all_text[i]
        if (!any_found) {
          if (line.includes("alias") || line.includes("require") || line.includes("use") || line.includes("import")) {
            start = i
            any_found = true
          }
        } else {
          if (!line.includes("alias") && !line.includes("require") && !line.includes("use") && !line.includes("import")) {
            end = i
            break
          }
        }
      }

      relevant_lines = all_text.slice(start, end).sort()
      Array.prototype.splice.apply(all_text, [1, relevant_lines.length].concat(relevant_lines))
      editor.setText(all_text.join('\n'))
    }
  }
};

Add support for import statements


var ws = new WebSocket("ws://localhost:8000/websocket");
ws.onmessage = function(evt){
	myOutput.value = evt.data;
}

function sendRain(){
  var msg = {
    type: "message",
    value:  document.getElementById("RainNumber").value,
    id:   "Rain",
    date: Date.now()
  };

  // Send the msg object as a JSON-formatted string.
  ws.send(JSON.stringify(msg));
}
function updateRainRange(){
//get elements
var myRange = document.getElementById("RainRange");
var myNumber = document.getElementById("RainNumber");
//copy the value over
myNumber.value = myRange.value;
sendRain();
} // end function

function updateRainNumber(){
//get elements
var myRange = document.getElementById("RainRange");
var myNumber = document.getElementById("RainNumber");
//copy the value over
myRange.value = myNumber.value;
sendRain();
} // end function

ws.onmessage = function(evt){
	var myOutput = document.getElementById("serverFeedback");
	myOutput.value = evt.data;
}

var ws = new WebSocket("ws://"+window.location.host+":8000/websocket");
ws.onmessage = function(evt){
	myOutput.value = evt.data;
}

function sendRain(){
  var msg = {
    type: "message",
    value:  document.getElementById("RainNumber").value,
    id:   "Rain",
    date: Date.now()
  };

  // Send the msg object as a JSON-formatted string.
  ws.send(JSON.stringify(msg));
}
function updateRainRange(){
//get elements
var myRange = document.getElementById("RainRange");
var myNumber = document.getElementById("RainNumber");
//copy the value over
myNumber.value = myRange.value;
sendRain();
} // end function

function updateRainNumber(){
//get elements
var myRange = document.getElementById("RainRange");
var myNumber = document.getElementById("RainNumber");
//copy the value over
myRange.value = myNumber.value;
sendRain();
} // end function

ws.onmessage = function(evt){
	var myOutput = document.getElementById("serverFeedback");
	myOutput.value = evt.data;
}

Change not to be reduced to localhost access


"use strict";

const getActivities = wrapper => {
    return new Promise(resolve => {
        const observer = new MutationObserver(mutations => {
            for (let mutation of mutations) {
                for (let addedNode of mutation.addedNodes) {
                    if (addedNode.classList && addedNode.classList.contains("activities")) {
                        observer.disconnect();
                        resolve(addedNode);
                        return;
                    }
                }
            }
        });
        observer.observe(wrapper, { childList: true, subtree: true });
    });
};

const infiniteScroll = activities => {
    let htmlHeight = document.documentElement.scrollHeight;

    const observer = new MutationObserver(() => {
        htmlHeight = document.documentElement.scrollHeight;
    });
    observer.observe(activities, {
        childList: true,
        attributes: true,
        characterData: false,
        subtree: true
    });

    document.addEventListener("scroll", e => {
        if (e.pageY + window.innerHeight === htmlHeight) {
            const moreButton = activities.parentNode.querySelector(".more-button");
            if (moreButton === null) { return; }
            moreButton.click();
        }
    });
};

const init = () => {
    const activities = document.querySelector(".activities");
    if (activities !== null) {
        infiniteScroll(activities);
        return;
    }
    const wrapper = document.querySelector(".feedActivities");
    if (wrapper === null) {
        return;
    }
    getActivities(wrapper).then(activities => infiniteScroll(activities));
};
document.addEventListener("DOMContentLoaded", init);

"use strict";

const getActivities = wrapper => {
    return new Promise(resolve => {
        const observer = new MutationObserver(mutations => {
            for (let mutation of mutations) {
                for (let addedNode of mutation.addedNodes) {
                    if (addedNode.classList && addedNode.classList.contains("activities")) {
                        observer.disconnect();
                        resolve(addedNode);
                        return;
                    }
                }
            }
        });
        observer.observe(wrapper, { childList: true, subtree: true });
    });
};

const infiniteScroll = activities => {
    let htmlHeight = document.documentElement.scrollHeight;

    const observer = new MutationObserver(() => {
        htmlHeight = document.documentElement.scrollHeight;
    });
    observer.observe(activities, {
        childList: true,
        attributes: true,
        characterData: false,
        subtree: true
    });

    document.addEventListener("scroll", e => {
        if (e.pageY + window.innerHeight === htmlHeight) {
            const moreButton = activities.parentNode.querySelector(".more-button");
            if (moreButton === null) { return; }
            moreButton.click();
        }
    }, { passive: true });
};

const init = () => {
    const activities = document.querySelector(".activities");
    if (activities !== null) {
        infiniteScroll(activities);
        return;
    }
    const wrapper = document.querySelector(".feedActivities");
    if (wrapper === null) {
        return;
    }
    getActivities(wrapper).then(activities => infiniteScroll(activities));
};
document.addEventListener("DOMContentLoaded", init);

Use passive flag on addEventListener


function annotatedTimeLineChart(chart_element, url, dataString) {
    var chart_element = $('#line_chart');

    chart_element.text('Loading...');

    $.ajax({
	    url: url,
        //dataType: 'json',
        //timeout: 30000,
        data: dataString,
        success: function (data_perl) {

            //var data_json = eval (data_perl);
            var data = new google.visualization.DataTable(data_perl, 0.6);

            //var chart_placement =  document.getElementById('line_chart');
            var chart = new google.visualization.AnnotatedTimeLine(chart_element.get(0));

            var json = data.toJSON();

            //alert(json);
            chart.draw(data, {
                displayAnnotations: true
            });

            //alert(data.toJSON() );

        },
        error: function (jqXHR, textStatus, errorThrownsuccess) {
            alert(textStatus);
        }
    });

}

 google.load('visualization', '1', {'packages':['annotatedtimeline']});

function annotatedTimeLineChart(chart_element, url, dataString) {

    chart_element.text('Loading...');

    $.ajax({
	    url: url,
        //dataType: 'json',
        //timeout: 30000,
        data: dataString,
        success: function (data_perl) {

            //var data_json = eval (data_perl);
            var data = new google.visualization.DataTable(data_perl, 0.6);

            //var chart_placement =  document.getElementById('line_chart');
            var chart = new google.visualization.AnnotatedTimeLine(chart_element.get(0));

            var json = data.toJSON();

            //alert(json);
            chart.draw(data, {
                displayAnnotations: true
            });

            //alert(data.toJSON() );

        },
        error: function (jqXHR, textStatus, errorThrownsuccess) {
            alert(textStatus);
        }
    });

}

Load Google visualization lbirary here so that we don't have to load it in hte inlcuding html.


module.exports = {
  'p1': require('../tutorial/p1').test,
  'p2': require('../tutorial/p2').test,
  'p3': require('../tutorial/p3').test,
  'p4': require('../tutorial/p4').test
}

module.exports = {
  'p1': require('../tutorial/p1').test,
  'p2': require('../tutorial/p2').test,
  'p3': require('../tutorial/p3').test
}

Remove unfinished chapter from tests


// Number of object properties
function propCount(obj) {
    var ctr = 0;
    for (var prop in obj)
        ++ctr;
    return ctr;
}

function deepEqual(obj1, obj2) {
    if (typeof obj1 == "object" && obj1 != null &&
        typeof obj2 == "object" && obj2 != null)
    {
        if (propCount(obj1) !== propCount(obj2))
            return false;
        for (var prop in obj1) {
            if (! prop in obj2)
                return false;
            if (deepEqual(obj1[prop], obj2[prop]) === false)
                return false;
        }
        return true;
    }
    else
        return obj1 === obj2;
}

function deepEqual(obj1, obj2) {
    if (typeof obj1 != "object" || obj1 == null ||
        typeof obj2 != "object" || obj2 == null)
    {
        return obj1 === obj2;
    } else {
        if (Object.keys(obj1).length != Object.keys(obj2).length) {
            return false;
        }
        for (let key of Object.keys(obj1)) {
            if (!(obj2[key] && deepEqual(obj1[key], obj2[key]))) return false;
        }
        return true;
    }
}

Add Chapter 04, exercise 4


import {
  createFragmentContainer,
  graphql
} from 'react-relay'

import Authentication from 'js/components/Authentication/Authentication'

export default createFragmentContainer(Authentication, {
  user: graphql`
    fragment AuthenticationContainer_user on User {
      id
      email
      username
    }
  `
})

import {
  createFragmentContainer,
  graphql
} from 'react-relay'

import Authentication from 'js/components/Authentication/Authentication'

export default createFragmentContainer(Authentication, {
  user: graphql`
    fragment AuthenticationContainer_user on User {
      id
      email
      username
      ...AssignExperimentGroupsContainer_user
    }
  `
})

Fix unrelated bug with data fetching


(function () {
  'use strict';

  angular
    .module('drinks', ['ngAnimate', 'toastr'])
    .controller('DrinksListController', DrinksListController);

  DrinksListController.$inject = ['DrinksService', '$state' , '$scope', 'toastr'];

  function DrinksListController(DrinksService, $state, $scope, toastr) {
    var vm = this;
    vm.AddToMenu = AddToMenu;
    vm.mvOnMenu  = mvOnMenu;
    vm.mvOffMenu = mvOffMenu;

    vm.drinks = DrinksService.query();

    function AddToMenu(drink) {
      drink.$update(successCallback, errorCallback);

      function successCallback(res) {
        $state.go('drinks.list', {
          drinkId: res._id
        });
      }

      function errorCallback(res) {
        vm.error = res.data.message;
      }
    }

    function mvOnMenu(drink) {
      toastr.success(drink.drinkName + ' was added to tap!');
    }

    function mvOffMenu(drink) {
      toastr.success(drink.drinkName + ' was removed from tap!');
    }
  }
})();

(function () {
  'use strict';

  angular
    .module('drinks', ['ngAnimate', 'toastr'])
    .controller('DrinksListController', DrinksListController);

  DrinksListController.$inject = ['DrinksService', '$state' , '$scope', 'toastr'];

  function DrinksListController(DrinksService, $state, $scope, toastr) {
    var vm = this;
    vm.AddToMenu = AddToMenu;
    vm.mvOnMenu  = mvOnMenu;
    vm.mvOffMenu = mvOffMenu;

    vm.drinks = DrinksService.query();

    function AddToMenu(drink) {
      drink.$update(successCallback, errorCallback);

      function successCallback(res) {
        $state.go('drinks.list', {
          drinkId: res._id
        });
      }

      function errorCallback(res) {
        vm.error = res.data.message;
      }
    }

    //Menu drink toggle notification via toastr
    function mvOnMenu(drink) {
      toastr.success(
          drink.drinkName + ' was added to tap!',
          {
            closeHtml: '<button></button>'
          }
      );
    }
    function mvOffMenu(drink) {
      toastr.success(
          drink.drinkName + ' was removed from tap!'
      );
    }

  }
})();

Comment and toast delete tested


(function() {
  'use strict';

  function init() {
    u.addEventListener('change', storeID);
    var userid = localStorage.getItem('userid');
    if (userid !== null && typeof userid !== 'undefined' && userid !== '' && userid !== ' ') { // localStorage key exists and is not empty or a single space
      fillID();
    } else { // Does not exist or is empty or a single space
      u.focus();
    }
  }

  function fillID() {
    u.value = localStorage.userid;
    p.focus();
  }

  function storeID() {
    localStorage.setItem('userid', u.value);
  }

  if (document.getElementById('userid') && document.getElementById('password')) { // Username and password inputs exist
    var p = document.getElementById('password');
    var u = document.getElementById('userid');
    init();
  } else {
    console.log('Chrome extension "Wells Fargo - Remember username" is out of date and is no longer functioning. Developer must update.'); // Wells Fargo homepage has been updated, breaking extension functionality
  }

}());

(function() {
  'use strict';

  function init() {
    if (u.value !== 'undefined' && u.value !== '') { // Username is not empty
      console.log('Username remembered');
      p.focus();
    } else { // Is empty
      console.log('Enter your username');
      u.focus();
    }
  }

  if (document.getElementById('userid') && document.getElementById('password')) { // Username and password inputs exist
    var p = document.getElementById('password');
    var u = document.getElementById('userid');
    init();
  } else {
    console.log('Chrome extension "Wells Fargo - Remember username" is out of date and is no longer functioning. Developer must update.'); // Wells Fargo homepage has been updated, breaking extension functionality
  }

}());

Update to only focus login fields


const syrup = require('../../');

syrup.scenario('array', `${__dirname}/test-array`);
syrup.scenario('object', `${__dirname}/test-object`);
syrup.scenario('save', `${__dirname}/test-save`);
syrup.scenario('get', `${__dirname}/test-get`, ['save']);

syrup.pour();

const syrup = require('../../');

syrup.enableDebug()
    .scenario('array', `${__dirname}/test-array`)
    .scenario('object', `${__dirname}/test-object`)
    .scenario('save', `${__dirname}/test-save`)
    .scenario('get', `${__dirname}/test-get`, ['save'])
    .pour();

Move to using chainable calls on syrup in console snippet


'use strict';

module.exports = (config, { strapi }) => async (ctx, next) => {
  const ratelimit = require('koa2-ratelimit').RateLimit;

  const message = [
    {
      messages: [
        {
          id: 'Auth.form.error.ratelimit',
          message: 'Too many attempts, please try again in a minute.',
        },
      ],
    },
  ];

  return ratelimit.middleware(
    Object.assign(
      {},
      {
        interval: 1 * 60 * 1000,
        max: 5,
        prefixKey: `${ctx.request.path}:${ctx.request.ip}`,
        message,
      },
      strapi.config.get('plugin.users-permissions.ratelimit')
    )
  )(ctx, next);
};

'use strict';

module.exports = (config, { strapi }) => async (ctx, next) => {
  const ratelimit = require('koa2-ratelimit').RateLimit;

  const message = [
    {
      messages: [
        {
          id: 'Auth.form.error.ratelimit',
          message: 'Too many attempts, please try again in a minute.',
        },
      ],
    },
  ];

  return ratelimit.middleware(
    Object.assign(
      {},
      {
        interval: 1 * 60 * 1000,
        max: 5,
        prefixKey: `${ctx.request.path}:${ctx.request.ip}`,
        message,
      },
      strapi.config.get('plugin.users-permissions.ratelimit'),
      config
    )
  )(ctx, next);
};

Allow rate limiter's config to be overridden for specific routes


'use strict';

var path = require('path');

var serveTask = function (gulp, plugins, config, helpers, generator_config) {
  <% if(projectType == 'wp-with-fe') { %>
  var startTasks = ['styles-watch', 'assets-watch'];
  <% } else { %>
  var startTasks = ['styles-watch', 'templates-watch', 'assets-watch'];
  <% } %>
  gulp.task('serve', startTasks, function() {
    <% if(projectType == 'wp-with-fe') { %>
    var name = generator_config.nameSlug;
    var browserSyncConfig = {
      proxy: {
        target: generator_config.proxyTarget || name+'.dev',
        reqHeaders: {
          'x-chisel-proxy': '1'
        }
      },
      online: true
    }
    <% } else { %>
    var browserSyncConfig = {
      server: './',
      online: true
    }
    <% } %>
    plugins.browserSync.init(browserSyncConfig);

    gulp.watch(path.join(config.src.base, config.src.styles), ['styles-watch']);
    <% if(projectType == 'fe') { %>
    gulp.watch(config.src.templatesWatch, ['templates-watch']);
    <% } %>
    gulp.watch(path.join(config.src.base, config.src.assets), ['assets-watch']);
    <% if(projectType == 'wp-with-fe') { %>
    gulp.watch('**/*.{php,twig}').on('change', plugins.browserSync.reload);
    <% } %>
  });
};

module.exports = serveTask;

'use strict';

var path = require('path');

var serveTask = function (gulp, plugins, config, helpers, generator_config) {
  <% if(projectType == 'wp-with-fe') { %>
  var startTasks = ['styles-watch', 'assets-watch'];
  <% } else { %>
  var startTasks = ['styles-watch', 'templates-watch', 'assets-watch'];
  <% } %>
  gulp.task('serve', startTasks, function() {
    <% if(projectType == 'wp-with-fe') { %>
    var name = generator_config.nameSlug;
    var browserSyncConfig = {
      proxy: {
        target: generator_config.proxyTarget || name+'.dev',
        reqHeaders: {
          'x-chisel-proxy': '1'
        }
      },
      ghostMode: false,
      online: true
    }
    <% } else { %>
    var browserSyncConfig = {
      server: './',
      ghostMode: false,
      online: true
    }
    <% } %>
    plugins.browserSync.init(browserSyncConfig);

    gulp.watch(path.join(config.src.base, config.src.styles), ['styles-watch']);
    <% if(projectType == 'fe') { %>
    gulp.watch(config.src.templatesWatch, ['templates-watch']);
    <% } %>
    gulp.watch(path.join(config.src.base, config.src.assets), ['assets-watch']);
    <% if(projectType == 'wp-with-fe') { %>
    gulp.watch('**/*.{php,twig}').on('change', plugins.browserSync.reload);
    <% } %>
  });
};

module.exports = serveTask;

Disable BrowserSync synchronisation of scroll and clicks


import Injector from './injector';

function injectServices(obj) {
  console.log('Injecting...');
  for (var prop in obj) {
    var val = obj[prop];
    if (val instanceof Injector) {
      console.log('Injecting ' + val);
      obj[prop] = val.service();
    }
  }
}

export default function jedify(klass) {
  if (klass._jedi) {
    return klass;
  } else {
    var JediClass = class JediClass extends klass {
      constructor(...args) {
        super(...args);
        injectServices(this);
      }
    };

    JediClass._jedi = true;
    return JediClass;
  }
}

import Injector from './injector';
import Container from './container';

function injectServices(obj) {
  var container = Container.containerFor(obj);

  for (var prop in obj) {
    var val = obj[prop];
    if (val instanceof Injector) {
      obj[prop] = val.service(container);
    }
  }
}

function injectConstructor(klass) {
  return class JediClass extends klass {
    constructor(...args) {
      super(...args);
      injectServices(this);
    }
  };
}

export default function jedify(klass) {
  if (klass._jedi) {
    return klass;
  } else {
    var JediClass = injectConstructor(klass);

    JediClass._jedi = true;
    return JediClass;
  }
}

Initialize object w/ stored container.


import evalModule from "eval"
import yaml from "js-yaml"
import stripJsonComments from "strip-json-comments"

export default function parser(content, file) {
  try {
    return yaml.safeLoad(stripJsonComments(content))
  }
  catch(e) {
    // `(module.)exports` = JS ?
    if (/exports/.test(content)) {
      return evalModule(content)
    }

    throw new Error(
      "Cannot parse runtime configuration as YAML, JSON or JS" +
      (file ? `from '${file}'` : "")
    )
  }
}

import evalModule from "eval"
import yaml from "js-yaml"
import stripJsonComments from "strip-json-comments"

export default function parser(content, file) {
  try {
    return yaml.safeLoad(stripJsonComments(content))
  }
  catch (e) {
    // `(module.)exports` = JS ?
    if (/exports/.test(content)) {
      return evalModule(content)
    }

    throw new Error(
      "Cannot parse runtime configuration as YAML, JSON or JS" +
      (file ? `from '${file}'` : "")
    )
  }
}

Fix the lack of whitespace after the 'catch' keyword


'use strict';

/**
 * Contains all of the functions necessary to deal with user
 * signup and authentication.
 *
 * 1. decodeToken: decodes a token into user data using.
 * 2. getToken middleware that sets up req.user if a token is passed in REST.
 * 3. Login
 */
var token = require('./token');

/* * * Custom REST routes. * * */
module.exports = function(app, config){

	// Secret is required.
	if (!config.local.secret) {
		throw new Error('A config.local.secret is required');
	}

	// TODO: Decouple Mandrill & pluginify. Offer other options.
	// Mandrill Key is required.
	if (!config.local.mandrill.key) {
		throw new Error('A config.local.mandrill.key is required');
	}

	// All REST routes will set up req.feathers.user if a token is passed.
	app.post('*', token.get);

	// POST signup route / create user.
	require('./route.signup')(app, config);

	// POST login.
	require('./route.login')(app, config);

	// POST token login
	require('./route.token-login')(app, config);

	// Verify a user's account by passing a secret.
	require('./route.verify')(app, config);

	// Send a password-reset email.
	require('./route.pw-reset-request')(app, config);

	// Pass in a secret and password to change a user's password.
	require('./route.pw-change')(app, config);
};

'use strict';

/**
 * Contains all of the functions necessary to deal with user
 * signup and authentication.
 *
 * 1. decodeToken: decodes a token into user data using.
 * 2. getToken middleware that sets up req.user if a token is passed in REST.
 * 3. Login
 */
var token = require('./token');

/* * * Custom REST routes. * * */
module.exports = function(app, config){

	// Secret is required.
	if (!config.local.secret) {
		throw new Error('A config.local.secret is required');
	}

	// TODO: Decouple Mandrill & pluginify. Offer other options.
	// Mandrill Key is required.
	if (!config.local.mandrill.key) {
		throw new Error('A config.local.mandrill.key is required');
	}

	// All REST routes will set up req.feathers.user if a token is passed.
	app.post('*', token.get);

	// POST signup route / create user.
	require('./route.signup')(app, config);

	// Verify a user's account by passing a secret.
	require('./route.verify')(app, config);

	// POST login.
	require('./route.login')(app, config);

	// POST token login
	require('./route.token-login')(app, config);

	// Send a password-reset email.
	require('./route.pw-reset-request')(app, config);

	// Pass in a secret and password to change a user's password.
	require('./route.pw-change')(app, config);
};

Put routes in order of use.


/**
 * Otagai.js
 * Based on https://github.com/olafurnielsen/form5-node-express-mongoose-coffeescript
 */

var express = require('express'),
    http = require('http'),
    fs = require('fs'),
    passport = require('passport'),
    mongoose = require('mongoose'),
    coffee = require('coffee-script')

var env = process.env.NODE_ENV || 'development',
    config = require('./config/environment')[env],
    auth = require('./config/middlewares/authorization')

// Bootstrap database
console.log('Connecting to database at ' + config.db)
mongoose.connect(config.db)

// Bootstrap models
var models_path = __dirname + '/app/models'
fs.readdirSync(models_path).forEach(function (file) {
  require(models_path+'/'+file)
});

// bootstrap passport config
require('./config/passport')(passport, config)

var app = express()
// express settings
require('./config/express')(app, config, passport)

// Bootstrap routes
require('./config/routes')(app, config, passport, auth)

// Helper funtions
require('./app/helpers/general')(app)

// Start the app by listening on <port>
var port = process.env.PORT || 3000
http.createServer(app).listen(port, function(){
  console.log("\u001b[36mApp running on port " + port + "\u001b[0m")
});

/**
 * Otagai.js
 * Based on https://github.com/olafurnielsen/form5-node-express-mongoose-coffeescript
 */

var express = require('express'),
    http = require('http'),
    fs = require('fs'),
    passport = require('passport'),
    mongoose = require('mongoose'),
    coffee = require('coffee-script')

var env = process.env.NODE_ENV || 'development',
    config = require('./config/environment')[env],
    auth = require('./config/middlewares/authorization')

// Bootstrap database
console.log('Connecting to database at ' + config.db)
mongoose.connect(config.db)

// Bootstrap models
var models_path = __dirname + '/app/models'
fs.readdirSync(models_path).forEach(function (file) {
  require(models_path+'/'+file)
});

// bootstrap passport config
require('./config/passport')(passport, config)

var app = express()
// express settings
require('./config/express')(app, config, passport)

// Bootstrap routes
require('./config/routes')(app, config, passport, auth)

// Helper funtions
require('./app/helpers/general')(app)

// Start the app by listening on <port>
var host = process.argv[2] || '0.0.0.0';
var port = process.argv[3] || process.env.PORT || 3000
http.createServer(app).listen(port, host, function(){
  console.log("\u001b[36mApp running on port " + port + "\u001b[0m")
});

Add passive supervizer module support


#!/usr/bin/env node

import http from 'http';
import app from "./app";

const httpServer = http.createServer(app);

const port = process.env.PORT || 3000;

httpServer.listen(port, () => {
    console.log('port %s, env=%s', port, process.env.NODE_ENV);
});

Set script shebang and make it runnable


'use strict';

var EOL         = require('os').EOL;
var chalk       = require('chalk');

module.exports = {
  normalizeEntityName: function() {
    // this prevents an error when the entityName is
    // not specified (since that doesn't actually matter
    // to us
  },

  availableOptions: [
    { name: 'url', type: String }
  ],

  afterInstall: function(options) {
    var firebaseUrl = options.url || 'https://YOUR-FIREBASE-NAME.firebaseio.com/';
    return this.addBowerPackagesToProject([
      {name: 'emberfire', target: "~0.0.0"}
    ]).then(function() {
      return this.insertIntoFile(
        'config/environment.js',
        '    firebase: \'' + firebaseUrl + '\',',
        {after: '    locationType: \'auto\',' + EOL}
      );
    }.bind(this)).then(function () {
      var output = EOL;
      output += chalk.yellow('EmberFire') + ' has been installed. Please configure your firebase URL in ' + chalk.green('config/environment.js') + EOL;
      console.log(output);
    });
  }
};

'use strict';

var EOL         = require('os').EOL;
var chalk       = require('chalk');

module.exports = {
  normalizeEntityName: function() {
    // this prevents an error when the entityName is
    // not specified (since that doesn't actually matter
    // to us
  },

  availableOptions: [
    { name: 'url', type: String }
  ],

  afterInstall: function(options) {
    var firebaseUrl = options.url || 'https://YOUR-FIREBASE-NAME.firebaseio.com/';
    return this.addBowerPackagesToProject([
      {name: 'firebase', target: "~2.1.0"}
    ]).then(function() {
      return this.insertIntoFile(
        'config/environment.js',
        '    firebase: \'' + firebaseUrl + '\',',
        {after: '    locationType: \'auto\',' + EOL}
      );
    }.bind(this)).then(function () {
      var output = EOL;
      output += chalk.yellow('EmberFire') + ' has been installed. Please configure your firebase URL in ' + chalk.green('config/environment.js') + EOL;
      console.log(output);
    });
  }
};

Fix regression, install firebase bower dep, not emberfire


'use strict';

// MODULES //

var constantFunction = require( '@stdlib/utils/constant-function' );

// MAIN //

/**
* Polyfill for returning a numeric user identity for environments which do not provide native support.
*
* @private
* @name getuid
* @type {Function}
* @returns {null} null
*
* @example
* var uid = getuid();
* // returns null
*/
var getuid = constantFunction( null );

// EXPORTS //

module.exports = getuid;

'use strict';

// MAIN //

/**
* Polyfill for returning a numeric user identity for environments which do not provide native support.
*
* @private
* @name getuid
* @type {Function}
* @returns {null} null
*
* @example
* var uid = getuid();
* // returns null
*/
function getuid() {
	return null;
}

// EXPORTS //

module.exports = getuid;

Refactor to avoid pkg dependency


/* ---------------------------------------------------- +/

## Handlebars Helpers ##

Custom Handlebars helpers.

/+ ---------------------------------------------------- */

Handlebars.registerHelper('thisYear', function(myArgument){
    return new Date().getFullYear();
});

/**
 * Turn titles into CSS classes
 *
 * Remove puncuation
 * Spaces to undescores
 * Lowercase text
 */
Handlebars.registerHelper('lowerSpacesToDashes', function(input) {
    if (input) {
        return input.replace(/[\.,'-\/#!$%\^&\*;:{}=\-_`~()@\+\?><\[\]\+]/g, '').replace(/\s+/g, '-').toLowerCase();
    }
});

Add a helper for css classes


const requestsController = require('../../controllers/requests.js');
const requestHelper = require('../../helpers/requestHelper.js');

module.exports = {
  '/:request/top10': {
    get: function get(req, res, cb) {
    	requestsController.getRowRequest(req.params.request, req, res);
    },
  },
};

const requestsController = require('../../controllers/requests.js');
const requestHelper = require('../../helpers/requestHelper.js');

module.exports = {
  '/:request/top10': {
    get: function get(req, res, cb) {
    	res.setHeader('Access-Control-Allow-Origin', '*');
    	requestsController.getRowRequest(req.params.request, req, res);
    },
  },
};

Add access control header that allows origin


"use strict";
var Main = module.parent.exports;

Main.ActionSet = function(Child, Request, Message){
  Main.ValidateArguments(Main.ARGS_EVEN, Request.length);
  for(let Number = 0; Number < Request.length; Number += 2){
    let Value = Main.NormalizeType(Request[Number + 1]);
    this.Database.set(Request[Number], Value);
  }
  return {Type: 'OK'};
};

Main.ActionGET = function(Child, Request, Message){
  if(Request.length === 1){
    return this.Database.get(Request[0]) || null;
  }
  return Request.map(function(Name){
    return this.Database.get(Name) || null;
  }.bind(this));
};

Main.ActionINCR = function(Child, Request, Message){
  if(Request.length === 1){
    let Value = this.Database.get(Request[0]) || 0;
    Main.Validate(Main.VAL_NUMERIC, 'INCR', Value);
    this.Database.set(Request[0], ++Value);
    return Value;
  }
  return Request.map(function(Name){
    let Value = this.Database.get(Name) || 0;
    Main.Validate(Main.VAL_NUMERIC, 'INCR', Value);
    this.Database.set(Request[0], ++Value);
    return Value;
  }.bind(this));
};

"use strict";
var Main = module.parent.exports;

Main.ActionSet = function(Child, Request){
  Main.ValidateArguments(Main.ARGS_EVEN, Request.length);
  for(let Number = 0; Number < Request.length; Number += 2){
    let Value = Main.NormalizeType(Request[Number + 1]);
    this.Database.set(Request[Number], Value);
  }
  return {Type: 'OK'};
};

Main.ActionGET = function(Child, Request){
  if(Request.length === 1){
    return this.Database.get(Request[0]) || null;
  }
  return Request.map(function(Name){
    return this.Database.get(Name) || null;
  }.bind(this));
};

Main.ActionINCR = function(Child, Request){
  if(Request.length === 1){
    let Value = this.Database.get(Request[0]) || 0;
    Main.Validate(Main.VAL_NUMERIC, 'INCR', Value);
    this.Database.set(Request[0], ++Value);
    return Value;
  }
  return Request.map(function(Name){
    let Value = this.Database.get(Name) || 0;
    Main.Validate(Main.VAL_NUMERIC, 'INCR', Value);
    this.Database.set(Request[0], ++Value);
    return Value;
  }.bind(this));
};

Remove un-used argument in Main::Action


TodoMVC = Space.Application.extend('TodoMVC', {

  requiredModules: ['Space.flux'],

  stores: ['TodoMVC.TodosStore'],

  components: [
    'TodoMVC.Input',
    'TodoMVC.Footer',
    'TodoMVC.TodoList'
  ],

  controllers: [
    'TodoMVC.RouteController',
    'TodoMVC.LayoutController'
  ],

  singletons: ['TodoMVC.TodosTracker'],

  onInitialize() {
    this.injector.map('TodoMVC.Todos').asStaticValue();
  }

});

TodoMVC = Space.Application.extend('TodoMVC', {

  requiredModules: ['Space.flux'],

  stores: ['TodoMVC.TodosStore'],

  components: [
    'TodoMVC.Input',
    'TodoMVC.Footer',
    'TodoMVC.TodoList'
  ],

  controllers: [
    'TodoMVC.RouteController',
    'TodoMVC.LayoutController'
  ],

  singletons: ['TodoMVC.TodosTracker'],

  onInitialize() {
    this.injector.map('TodoMVC.Todos').asStaticValue();
    this.injector.map('BlazeLayout').to(BlazeLayout);
  }

});

Add blaze layout injection to client app instance


/* eslint-env mocha */
const {expect} = chai;
import React from './React';
import TestUtils from './TestUtils';

describe('React components', () => {
  it('should find valid xpath in react component', () => {
    const component = TestUtils.renderIntoDocument(<blink>hi</blink>);
    expect(component).to.have.xpath('//blink');
  });

  describe('when it does not find valid xpath in react component', () => {
    it('should throw', () => {
      const component = TestUtils.renderIntoDocument(<blink>hi</blink>);
      expect(() => {
        expect(component).to.have.xpath('//h1');
      }).to.throw('to have xpath \'//h1\'');
    });

    it('should throw with outerHTML of the component', () => {
      const component = TestUtils.renderIntoDocument(<blink>hi</blink>);
      expect(() => {
        expect(component).to.have.xpath('//h1');
      }).to.throw('hi</blink>');
    });
  });
});

/* eslint-env mocha */
const {expect} = chai;
import React from './React';
import TestUtils from './TestUtils';

describe('React components', () => {
  it('should find valid xpath in react component', () => {
    const component = TestUtils.renderIntoDocument(<blink>hi</blink>);
    expect(component).to.have.xpath('//blink');
  });

  it('should find valid xpath in react component twice', () => {
    const component = TestUtils.renderIntoDocument(<blink>hi</blink>);
    expect(component).to.have.xpath('//blink');
    expect(component).to.have.xpath('//blink');
  });

  describe('when it does not find valid xpath in react component', () => {
    it('should throw', () => {
      const component = TestUtils.renderIntoDocument(<blink>hi</blink>);
      expect(() => {
        expect(component).to.have.xpath('//h1');
      }).to.throw('to have xpath \'//h1\'');
    });

    it('should throw with outerHTML of the component', () => {
      const component = TestUtils.renderIntoDocument(<blink>hi</blink>);
      expect(() => {
        expect(component).to.have.xpath('//h1');
      }).to.throw('hi</blink>');
    });
  });
});

Add test for double assertion


$(document).ready(function() {

  const $form = $('.edit_user');
  $form.on('submit', function(event) {

    let url = $form.attr('action');
    let method = $form.attr('method');
    let data = $form.serialize();

    $.ajax({
      url: url,
      method: method,
      data: data,
      dataType: "json"
    })
    .success(function(response) {
      console.log(response);
    })
    .fail(function(error) {
      console.log(error);
    })
  });

  $("#profile").on("click", ".edit-profile", function() {
    const $modal = $('#userEditModal')[0];
    console.log($modal);
    $modal.style.display = "block";

    $('#closeEditModal').on("click", function() {
      $modal.style.display = "none";
    });
  });
});

$(document).ready(function() {

  $("#profile").on('submit', '.edit-user', function(event) {
    const $form = $('.edit-user');
    let url = $form.attr('action');
    let method = $form.attr('method');
    let data = $form.serialize();

    $.ajax({
      url: url,
      method: method,
      data: data,
      dataType: "json"
    })
    .success(function(response) {
      console.log(response);
    })
    .fail(function(error) {
      console.log(error);
    })
  });

  $("#profile").on("click", ".edit-profile", function() {
    const $modal = $('#userEditModal')[0];
    console.log($modal);
    $modal.style.display = "block";

    $('#closeEditModal').on("click", function() {
      $modal.style.display = "none";
    });
  });
});

Fix ajax for user update modal


import React from 'react';
import PropTypes from 'prop-types';
import css from './FilterControlGroup.css';

const propTypes = {
  children: PropTypes.oneOfType([
    PropTypes.arrayOf(PropTypes.node),
    PropTypes.node,
  ]),
  label: PropTypes.oneOfType([
    PropTypes.string,
    PropTypes.node,
  ]),
};

const FilterControlGroup = props => (

    {React.Children.map(props.children, child => (

{child}

    ))}

);

FilterControlGroup.propTypes = propTypes;

export default FilterControlGroup;

import React from 'react';
import PropTypes from 'prop-types';
import css from './FilterControlGroup.css';

const propTypes = {
  children: PropTypes.oneOfType([
    PropTypes.arrayOf(PropTypes.node),
    PropTypes.node,
  ]),
  label: PropTypes.oneOfType([
    PropTypes.string,
    PropTypes.node,
  ]),
  style: PropTypes.object,
};

const FilterControlGroup = props => (

    {React.Children.map(props.children, child => (

{child}

    ))}

);

FilterControlGroup.propTypes = propTypes;

export default FilterControlGroup;

Add `style` to PropTypes for ESlint *sigh*


import React from 'react';

import IntroBand from '../components/IntroBand';
import AboutBand from '../components/AboutBand';

const AboutPage = () => (
  <div className="about-page">
    <IntroBand />
    <AboutBand />
  </div>
);

export default AboutPage;

import React from 'react';

import AboutBand from '../components/AboutBand';
import IntroBand from '../components/IntroBand';
import ProjectsBand from '../components/ProjectsBand';

class AboutPage extends React.Component {
  shouldComponentUpdate() {
    return false;
  }

  render() {
    return (
      <div className="about-page">
        <IntroBand />
        <AboutBand />
        <ProjectsBand />
      </div>
    );
  }
}

export default AboutPage;

Add projects band to about page


import { TagRels } from './collection.js';
import { ensureIndex } from '../../collectionUtils';

TagRels.addDefaultView(terms => {
  return {
    selector: {
      deleted: false,
    },
  };
});

TagRels.addView('postsWithTag', terms => {
  return {
    selector: {
      tagId: terms.tagId,
      baseScore: {$gt: 0},
    },
    sort: {baseScore: -1},
  }
});

TagRels.addView('tagsOnPost', terms => {
  return {
    selector: {
      postId: terms.postId,
      baseScore: {$gt: 0},
    },
    sort: {baseScore: -1},
  }
});

ensureIndex(TagRels, {postId:1});
ensureIndex(TagRels, {tagId:1});

import { TagRels } from './collection.js';
import { ensureIndex } from '../../collectionUtils';

TagRels.addDefaultView(terms => {
  return {
    selector: {
      deleted: false,
    },
    options: {
      sort: {baseScore: -1},
    },
  };
});

TagRels.addView('postsWithTag', terms => {
  return {
    selector: {
      tagId: terms.tagId,
      baseScore: {$gt: 0},
    },
  }
});

TagRels.addView('tagsOnPost', terms => {
  return {
    selector: {
      postId: terms.postId,
      baseScore: {$gt: 0},
    },
  }
});

ensureIndex(TagRels, {postId:1});
ensureIndex(TagRels, {tagId:1});

Fix relevance-sorting on tag page


const lit = require('fountain-generator').lit;

module.exports = function browsersyncConf(props) {
  const conf = {
    server: {
      baseDir: []
    }
  };

  if (props.dist) {
    conf.server.baseDir.push(lit`conf.paths.dist`);
  } else {
    conf.server.baseDir.push(lit`conf.paths.tmp`);
    if (props.modules === 'systemjs') {
      conf.server.baseDir.push('.');
    } else {
      conf.server.baseDir.push(lit`conf.paths.src`);
    }
    if (props.modules === 'inject') {
      conf.server.routes = {
        '/bower_components': 'bower_components'
      };
    }
    if (props.modules === 'systemjs') {
      conf.server.routes = {
        '/index.html': 'src/index.html'
      };
      conf.server.index = 'src/index.html';
    }
    if (props.webpackHotReload) {
      conf.server.middleware = [
        lit`webpackDevMiddleware(webpackBundler, {
        // IMPORTANT: dev middleware can't access config, so we should
        // provide publicPath by ourselves
        publicPath: webpackConf.output.publicPath,

        // Quiet verbose output in console
        quiet: true
      }),

      // bundler should be the same as above
      webpackHotMiddleware(webpackBundler)`
      ];
    }
  }

  return conf;
};

const lit = require('fountain-generator').lit;

module.exports = function browsersyncConf(props) {
  const conf = {
    server: {
      baseDir: [],
      browser: []
    }
  };

  if (props.dist) {
    conf.server.baseDir.push(lit`conf.paths.dist`);
  } else {
    conf.server.baseDir.push(lit`conf.paths.tmp`);
    if (props.modules === 'systemjs') {
      conf.server.baseDir.push('.');
    } else {
      conf.server.baseDir.push(lit`conf.paths.src`);
    }
    if (props.modules === 'inject') {
      conf.server.routes = {
        '/bower_components': 'bower_components'
      };
    }
    if (props.modules === 'systemjs') {
      conf.server.routes = {
        '/index.html': 'src/index.html'
      };
      conf.server.index = 'src/index.html';
    }
    if (props.webpackHotReload) {
      conf.server.middleware = [
        lit`webpackDevMiddleware(webpackBundler, {
        // IMPORTANT: dev middleware can't access config, so we should
        // provide publicPath by ourselves
        publicPath: webpackConf.output.publicPath,

        // Quiet verbose output in console
        quiet: true
      }),

      // bundler should be the same as above
      webpackHotMiddleware(webpackBundler)`
      ];
    }
  }

  return conf;
};

Remove auto open of the browser on gulp serve


$(function() {

    $("a.officelaunchlink").click(function() {
	var ua = window.navigator.userAgent;
	window.console.log("Checking UA %s", ua);
	if (~ua.indexOf('MSIE ') || ~ua.indexOf('Trident/')) {
	    // http://stackoverflow.com/questions/19999388/jquery-check-if-user-is-using-ie
	    window.console.log("Detected IE");
	    window.console.log("ActiveXObject is %s", window.ActiveXObject);
	    ed = new ActiveXObject('SharePoint.OpenDocuments.4');
	    window.console.log("ActiveXObject: %s", ed);
	    if (ed) {
		ed.EditDocument(this.href);
	    }
	} else {
	    alert("This feature requires Windows, Internet Explorer, and Microsoft Office.")
	}
	return false;
    });

});

$(function() {

    $("a.officelaunchlink").click(function() {
	var ua = window.navigator.userAgent;
	if (~ua.indexOf('MSIE ') || ~ua.indexOf('Trident/')) {
	    // http://stackoverflow.com/questions/19999388/jquery-check-if-user-is-using-ie
	    ed = new ActiveXObject('SharePoint.OpenDocuments.4');
	    if (ed) {
		ed.EditDocument(this.href);
	    }
	} else {
	    alert("This feature requires Windows, Internet Explorer, and Microsoft Office.")
	}
	return false;
    });

});

Remove left over console logging


import React from 'react'
import {
  Card,
  Box,
  Text,
  Icon,
  Container,
  Flex,
  Link
} from '@hackclub/design-system'

export default () => (
  <Container maxWidth={38}>
    <Link href="challenge">
      <Card bg="blue.1" p={[2, 3]} mt={-2} mb={4}>
        <Flex justify="flex-start">
          <Icon name="open_in_new" size={48} mr={[2, 3]} color="blue.6" />
          <Box color="blue.6">
            <Text f={3} bold>
              Check out the winning challenge submissions
            </Text>
            <Text f={2}>
              Our latest challenge just wrapped up. See the winning projects.
            </Text>
          </Box>
        </Flex>
      </Card>
    </Link>
  </Container>
)

import React from 'react'
import {
  Card,
  Box,
  Text,
  Icon,
  Container,
  Flex,
  Link
} from '@hackclub/design-system'

export default () => (
  <Container maxWidth={24} my={-3}>
    <Link href="challenge">
      <Card bg="blue.1" p={[2, 3]}>
        <Flex justify="flex-start" align="center">
          <Icon name="open_in_new" size={24} mr={[2, 3]} color="blue.6" />
          <Box color="blue.6">
            <Text f={3} bold>
              Check out student-built projects
            </Text>
          </Box>
        </Flex>
      </Card>
    </Link>
  </Container>
)

Simplify project banner on home page


import React, {
    PropTypes
} from 'react';
import '../../css/SearchGifView.css'

class SearchGifView extends React.Component {
    constructor(props) {
        super(props);

        this.handleClick = this.handleClick.bind(this);
        this.handleMouseOver = this.handleMouseOver.bind(this);
        this.handleMouseOut = this.handleMouseOut.bind(this);

        this.state = {
            url: props.gif.images.original_still.url
        };
    }
    handleClick() {
        this.props.click(this.props.gif);
    }
    handleMouseOver() {
        let gif = this.props.gif;

        this.setState({
            url: gif.images.downsized.url
        });
    }
    handleMouseOut() {
        let gif = this.props.gif;

        this.setState({
            url: gif.images.original_still.url
        });
    }
    render() {
        return (

        );
    }
};

SearchGifView.propTypes = {
    gif: PropTypes.object.isRequired,
    click: PropTypes.func
};

export default SearchGifView;

Move mouse events to thumbnail


/*
 * Adapted from: http://mikejolley.com/2012/12/using-the-new-wordpress-3-5-media-uploader-in-plugins/
 */
jQuery(document).ready(function ($) {
// Uploading files
    var file_frame;

    $('.custom-profile-picture-remove').on('click', function (event) {
        $('#user_meta_image').val('');
        $('#submit').click();
    });

    $('.custom-profile-picture').on('click', function (event) {

        event.preventDefault();

        // If the media frame already exists, reopen it.
        if (file_frame) {
            file_frame.open();
            return;
        }

        // Create the media frame.
        file_frame = wp.media.frames.file_frame = wp.media({
            title: $(this).data('uploader_title'),
            button: {
                text: $(this).data('uploader_button_text')
            },
            multiple: false  // Set to true to allow multiple files to be selected
        });

        // When an image is selected, run a callback.
        file_frame.on('select', function () {
            // We set multiple to false so only get one image from the uploader
            attachment = file_frame.state().get('selection').first().toJSON();

            // Do something with attachment.id and/or attachment.url here
            $('#user_meta_image').val(attachment.url);
            $('.current-profile-picture').attr('src', attachment.url);
        });

        // Finally, open the modal
        file_frame.open();
    });

});

/*
 * Adapted from: http://mikejolley.com/2012/12/using-the-new-wordpress-3-5-media-uploader-in-plugins/
 */
jQuery(document).ready(function ($) {
// Uploading files
    var file_frame;

    $('.custom-profile-picture-remove').on('click', function (event) {
        $('#user_meta_image').val('');
        $('#submit').click();
    });

    $('.custom-profile-picture').on('click', function (event) {

        event.preventDefault();

        // If the media frame already exists, reopen it.
        if (file_frame) {
            file_frame.open();
            return;
        }

        // Create the media frame.
        file_frame = wp.media.frames.file_frame = wp.media({
            title: $(this).data('uploader_title'),
            button: {
                text: $(this).data('uploader_button_text')
            },
            multiple: false  // Set to true to allow multiple files to be selected
        });

        // When an image is selected, run a callback.
        file_frame.on('select', function () {
            // We set multiple to false so only get one image from the uploader
            attachment = file_frame.state().get('selection').first().toJSON();

            // Do something with attachment.id and/or attachment.url here

            $('#user_meta_image').val(attachment.id);
            $('.current-profile-picture').attr('src', attachment.url);
        });

        // Finally, open the modal
        file_frame.open();
    });

});

Return attachment ID instead of URL


const path = require('path');
const favicon = require('serve-favicon');
const compress = require('compression');
const cors = require('cors');
const helmet = require('helmet');
const bodyParser = require('body-parser');

const feathers = require('feathers');
const configuration = require('feathers-configuration');
const hooks = require('feathers-hooks');
const rest = require('feathers-rest');

const middleware = require('./middleware');
const services = require('./services');
const appHooks = require('./app.hooks');

const mongodb = require('./mongodb');

const app = feathers();

// Load app configuration
app.configure(configuration(path.join(__dirname, '..')));
// Enable CORS, security, compression, favicon and body parsing
app.use(cors());
app.use(helmet());
app.use(compress());
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));
app.use(favicon(path.join(app.get('public'), 'favicon.ico')));
// Host the public folder
app.use('/', feathers.static(app.get('public')));

// Set up Plugins and providers
app.configure(hooks());
app.configure(mongodb);
app.configure(rest());

// Set up our services (see `services/index.js`)
app.configure(services);
// Configure middleware (see `middleware/index.js`) - always has to be last
app.configure(middleware);
app.hooks(appHooks);

module.exports = app;

const path = require('path');
const favicon = require('serve-favicon');
const compress = require('compression');
const cors = require('cors');
const helmet = require('helmet');
const bodyParser = require('body-parser');

const feathers = require('feathers');
const configuration = require('feathers-configuration');
const hooks = require('feathers-hooks');
const rest = require('feathers-rest');
const socketio = require('feathers-socketio');

const middleware = require('./middleware');
const services = require('./services');
const appHooks = require('./app.hooks');

const mongodb = require('./mongodb');

const app = feathers();

// Load app configuration
app.configure(configuration(path.join(__dirname, '..')));
// Enable CORS, security, compression, favicon and body parsing
app.use(cors());
app.use(helmet());
app.use(compress());
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));
app.use(favicon(path.join(app.get('public'), 'favicon.ico')));
// Host the public folder
app.use('/', feathers.static(app.get('public')));

// Set up Plugins and providers
app.configure(hooks());
app.configure(mongodb);
app.configure(rest());
app.configure(socketio());

// Set up our services (see `services/index.js`)
app.configure(services);
// Configure middleware (see `middleware/index.js`) - always has to be last
app.configure(middleware);
app.hooks(appHooks);

module.exports = app;

Add socket.io to handle client real-time requests


const EventEmitter = require("events");
class DemoClient extends EventEmitter {}

const Client = new DemoClient();
exports.client = Client;

Client.connect = () => {
  Client.emit("ready");
};

Client.on("ready", () => {
  console.log("Demo client ready");
});
Client.emit("ready");

Client.exec = (command, callback) => {
  var response = "";
  if(command === "gettime") {
    response = "Day 1, 07:00\n6 days to next horde.";
  }
  else if(command === "version") {
    response = "Game version: Alpha 16.4 (b8) Compatibility Version: Alpha 16.4 (Simulated)";
  }
  else if(command === "lp") {
    response = "Total of 0 in the game";
  }

  callback(void 0, response);
};

const EventEmitter = require("events");
class DemoClient extends EventEmitter {}

const Client = new DemoClient();
exports.client = Client;

Client.connect = () => {
  Client.emit("ready");
};

Client.on("ready", () => {
  console.log("Demo client ready");
});
Client.emit("ready");

Client.exec = (command, callback) => {
  var response = "";
  if(command === "gettime") {
    response = "Day 1, 07:00\n6 days to next horde.";
  }
  else if(command === "version") {
    response = "Game version: Alpha 17 (b240) Compatibility Version: Alpha 17 (Simulated)";
  }
  else if(command === "lp") {
    response = "Total of 0 in the game";
  }

  callback(void 0, response);
};

Update demo server version response


import React, { Component } from 'react';
import { Route, Router } from 'react-router';
import ReactGA from 'react-ga';
import createHistory from 'history/createBrowserHistory';
import ScrollToTop from 'shared/components/scrollToTop/scrollToTop';
import Home from './scenes/home/home';

const history = createHistory();
ReactGA.initialize('UA-75642413-1');

class App extends Component {
  componentDidMount() {
    if (process.env.NODE_ENV === 'production') {
      history.listen((location) => {
        ReactGA.set({ page: location.pathname });
        ReactGA.pageview(location.pathname);
      });
    }
  }

  render() {
    return (
      <Router history={history}>
        <ScrollToTop>
          <Route path="/" component={Home} />
        </ScrollToTop>
      </Router>
    );
  }
}

export default App;

import React, { Component } from 'react';
import { Route, Router } from 'react-router';
import ReactGA from 'react-ga';
import createHistory from 'history/createBrowserHistory';
import ScrollToTop from 'shared/components/scrollToTop/scrollToTop';
import Home from './scenes/home/home';

const history = createHistory();
ReactGA.initialize('UA-75642413-1');

class App extends Component {
  componentDidMount() {
    // History listening doesn't catch first page load
    ReactGA.set({ page: history.location.pathname });
    ReactGA.pageview(history.location.pathname);

    if (process.env.NODE_ENV === 'production') {
      history.listen((location) => {
        ReactGA.set({ page: location.pathname });
        ReactGA.pageview(location.pathname);
      });
    }
  }

  render() {
    return (
      <Router history={history}>
        <ScrollToTop>
          <Route path="/" component={Home} />
        </ScrollToTop>
      </Router>
    );
  }
}

export default App;

Send pageview on app load and THEN listen


class BaseChecker {
  constructor(reporter, ruleId, meta) {
    this.reporter = reporter
    this.ruleId = ruleId
    this.meta = meta
  }

  error(ctx, message) {
    this.reporter.error(ctx, this.ruleId, message)
  }

  errorAt(line, column, message) {
    this.reporter.errorAt(line, column, this.ruleId, message)
  }

  warn(ctx, message) {
    this.reporter.warn(ctx, this.ruleId, message)
  }
}

module.exports = BaseChecker

class BaseChecker {
  constructor(reporter, ruleId, meta) {
    this.reporter = reporter
    this.ruleId = ruleId
    this.meta = meta
  }

  error(ctx, message, fix) {
    this.addReport('error', ctx, message, fix)
  }

  errorAt(line, column, message, fix) {
    this.error({ loc: { start: { line, column } } }, message, fix)
  }

  warn(ctx, message, fix) {
    this.addReport('warn', ctx, message, fix)
  }

  addReport(type, ctx, message, fix) {
    this.reporter[type](ctx, this.ruleId, message, this.meta.fixable ? fix : null)
  }
}

module.exports = BaseChecker

Extend base checker to use fixer


'use strict';

let util = require('util'),
  urlParser = require('url'),
  moment = require('moment');

module.exports.fileNamer = function() {
  // ensure all files for a given name gets the same timestamp
  let timestamp = moment().toISOString();

  return {
      getNameFromUrl(url, extension) {
        let host = urlParser.parse(url).host;
        host = host.split('.').join('-');
        return util.format('%s-%s.%s', host, timestamp, extension);
      }
  };
};

'use strict';

let util = require('util'),
  urlParser = require('url'),
  moment = require('moment');

module.exports.fileNamer = function() {
  // ensure all files for a given name gets the same timestamp
  // Removing ':' makes for better file names, and is also valid ISO8601 dates.
  let timestamp = moment().format().replace(/:/g, '');

  return {
      getNameFromUrl(url, extension) {
        let host = urlParser.parse(url).host;
        host = host.split('.').join('-');
        return util.format('%s-%s.%s', host, timestamp, extension);
      }
  };
};

Make file names use local timestamps without :


const webpack = require('webpack'),
      webpackMerge = require('webpack-merge'),
      ExtractTextPlugin = require('extract-text-webpack-plugin'),
      commonConfig = require('./webpack.common.js'),
      path = require('path'),
      rootDir = path.resolve(__dirname, '..');

const env = process.env.NODE_ENV;

module.exports = webpackMerge(commonConfig, {
  devtool: 'source-map',
  output: {
    path: path.resolve(rootDir, 'src/devDist'),
    sourceMapFilename: '[name].map',
    publicPath: '/devDist/',
    filename: '[name].js',
    chunkFilename: '[id].chunk.js'
  },
  module: {
    loaders: [
      {
        test: /\.ts$/,
        exclude: /node_modules/,
        loaders: ['awesome-typescript-loader', 'angular-router-loader', 'angular2-template-loader']
      }]
  }
  //Example of a dev server. Not needed in this app since it uses Node/Express for the server.
  // devServer: {
  //   contentBase: './src',
  //   historyApiFallback: true,
  //   watchOptions: { aggregateTimeout: 300, poll: 1000 },
  //   quiet: true,
  //   stats: 'minimal'
  // }
});

const webpack = require('webpack'),
      webpackMerge = require('webpack-merge'),
      ExtractTextPlugin = require('extract-text-webpack-plugin'),
      commonConfig = require('./webpack.common.js'),
      path = require('path'),
      rootDir = path.resolve(__dirname, '..');

const env = process.env.NODE_ENV;

module.exports = webpackMerge(commonConfig, {
  devtool: 'source-map',
  output: {
    path: path.resolve(rootDir, 'src/devDist'),
    sourceMapFilename: '[name].map',
    publicPath: '/devDist/',
    filename: '[name].js',
    chunkFilename: '[id].chunk.js'
  },
  module: {
    loaders: [
      {
        test: /\.ts$/,
        exclude: /node_modules/,
        loaders: ['awesome-typescript-loader', 'angular-router-loader', 'angular2-template-loader']
      }]
  }
});

Remove commented out dev server


// Note: You must restart bin/webpack-watcher for changes to take effect

const merge = require('webpack-merge')
const common = require('./common.js')

const { resolve } = require('path')
const { devServer, publicPath, paths } = require('./configuration.js')

module.exports = merge(common, {
  devtool: 'sourcemap',

  stats: {
    errorDetails: true
  },

  output: {
    pathinfo: true
  },

  devServer: {
    host: devServer.host,
    port: devServer.port,
    contentBase: resolve(paths.output, paths.entry),
    proxy: {'/': 'http://localhost:9001'},
    publicPath
  }
})

// Note: You must restart bin/webpack-watcher for changes to take effect

const merge = require('webpack-merge')
const common = require('./common.js')

const { resolve } = require('path')
const { devServer, publicPath, paths } = require('./configuration.js')

module.exports = merge(common, {
  devtool: 'sourcemap',

  stats: {
    errorDetails: true
  },

  output: {
    pathinfo: true
  },

  devServer: {
    host: devServer.host,
    port: devServer.port,
    disableHostCheck: true,
    contentBase: resolve(paths.output, paths.entry),
    proxy: {'/': 'http://localhost:9001'},
    publicPath
  }
})

Disable host check in dev-server for mobile testing


'use strict';

module.exports = function checkRights(usernames, rights='', defaultValue=false) {
    if (! rights) return defaultValue;
    if (! usernames) return false;
    if (! Array.isArray(usernames)) usernames=[usernames];
    var alloweds=rights.split(/[ ,;\r\n]+/).filter(a => a);

    for (let username of usernames) {
        for (let allowed of alloweds) {
            if (username.endsWith(allowed)) return true;
        }
    }
    return false;
}

'use strict';

/**
Checks if one of the usernames is part of the rights list
 A right may be a regular expression.

 */

module.exports = function checkRights(usernames, rights='', defaultValue=false) {
    if (! rights) return defaultValue;
    if (! usernames) return false;
    if (! Array.isArray(usernames)) usernames=[usernames];
    var alloweds=rights.split(/[ ,;\r\n]+/).filter(a => a);

    for (let allowed of alloweds) {
        let isRegExp=false;
        if (allowed.startsWith('/') && allowed.endsWith('/')) {
            isRegExp=true;
            var regexp=new RegExp(allowed.substring(1, allowed.length-1));
        }

        for (let username of usernames) {
            if (isRegExp) {
                if (username.match(regexp)) return true;
            } else {
                if (username.toLowerCase()===allowed.toLowerCase()) return true;
            }
        }
    }
    return false;
}

Update check rights to deal with regexp


/**
 * Adds an admin pointer that describes new features in 1.0.
 */

/* exported ampAdminPointer */
/* global ajaxurl, jQuery */
var ampAdminPointer = ( function( $ ) { // eslint-disable-line no-unused-vars
	'use strict';

	return {

		/**
		 * Loads the pointer.
		 *
		 * @param {Object} data - Module data.
		 * @return {void}
		 */
		load: function load( data ) {
			var options = $.extend(
				data.pointer.options,
				{
					/**
					 * Makes a POST request to store the pointer ID as dismissed for this user.
					 */
					close: function() {
						$.post( ajaxurl, {
							pointer: data.pointer.pointer_id,
							action: 'dismiss-wp-pointer'
						} );
					}
				}
			);

			$( data.pointer.target ).pointer( options ).pointer( 'open' );
		}
	};
}( jQuery ) );

/**
 * Adds an admin pointer that describes new features in 1.0.
 */

/* exported ampAdminPointer */
/* global ajaxurl, jQuery */
var ampAdminPointer = ( function( $ ) { // eslint-disable-line no-unused-vars
	'use strict';

	return {

		/**
		 * Loads the pointer.
		 *
		 * @param {Object} data - Module data.
		 * @return {void}
		 */
		load: function load( data ) {
			var options = $.extend(
				data.pointer.options,
				{
					/**
					 * Makes a POST request to store the pointer ID as dismissed for this user.
					 */
					close: function() {
						$.post( ajaxurl, {
							pointer: data.pointer.pointer_id,
							action: 'dismiss-wp-pointer'
						} );
					},

					/**
					 * Adds styling to the pointer, to ensure it appears alongside the AMP menu.
					 *
					 * @param {Object} event The pointer event.
					 * @param {Object} t Pointer element and state.
					 */
					show: function( event, t ) {
						t.pointer.css( 'position', 'fixed' );
					}
				}
			);

			$( data.pointer.target ).pointer( options ).pointer( 'open' );
		}
	};
}( jQuery ) );

Apply Tonya's suggestion for pointer styling.


module.exports = {
	string: require('./string'),
	object: require('./object')
};

module.exports = {
	string: require('./string'),
	object: require('./object'),
	url: require('./url')
};

Add url builder and preprocessor


/* eslint-disable no-underscore-dangle */

import append from './append';
import keys from './keys';
import prop from './prop';
import reduce from './reduce';

const _values = obj => {
  const props = keys(obj);
  return reduce((acc, key) => append(prop(key, obj), acc), [], props);
};

const values = obj => {
  if (obj == null || Object(obj) !== obj) return [];

  if (Object.values) return Object.values(obj);
  return _values(obj);
};

export { values as default, _values };

Remove line break between comment and import statements


import React from 'react';
import Chartist from 'chartist';

export default class Chart extends React.Component {
  componentDidMount() {
    this.updateChart(this.props);
  }

  componentWillReceiveProps(newProps) {
    this.updateChart(newProps);
  }

  componentWillUnmount() {
    this.chartist.detach();
  }

  render() {
    return

;
  }

  updateChart(props) {
    const {type, data, options = {}, responsiveOptions = []} = props;

    this.chartist ? this.chartist.update(data, options, true) :
    data.series ? this.chartist = new Chartist[type](React.findDOMNode(this), data, options, responsiveOptions) :
    null;
  }
}

Chart.propTypes = {
  type: React.PropTypes.string.isRequired,
  ratio: React.PropTypes.string,
  data: React.PropTypes.shape({
    labels: React.PropTypes.array.isRequired,
    series: React.PropTypes.array.isRequired
  }),
  options: React.PropTypes.object,
  responsiveOptions: React.PropTypes.array
};

import React from 'react';
import Chartist from 'chartist';

export default class Chart extends React.Component {
  componentDidMount() {
    this.updateChart(this.props);
  }

  componentWillReceiveProps(newProps) {
    this.updateChart(newProps);
  }

  componentWillUnmount() {
    this.chartist.detach();
  }

  render() {
    return

;
  }

  updateChart(props) {
    const {type, data, options = {}, responsiveOptions = []} = props;

    this.chartist ? this.chartist.update(data, options, true) :
    data.series ? this.chartist = new Chartist[type](React.findDOMNode(this), data, options, responsiveOptions) :
    null;
  }
}

Chart.propTypes = {
  type: React.PropTypes.string.isRequired,
  ratio: React.PropTypes.string,
  data: React.PropTypes.shape({
    labels: React.PropTypes.array,
    series: React.PropTypes.array
  }),
  options: React.PropTypes.object,
  responsiveOptions: React.PropTypes.array
};

Make data.labels and data.series optional