<commit_before>import React, { Component } from 'react';
import { StyleSheet, View, StatusBar } from 'react-native';
import { connect } from 'react-redux';
import PXHeader from '../../components/PXHeader';
import PXTabView from '../../components/PXTabView';
import PXPhotoView from '../../components/PXPhotoView';
import HeaderTextTitle from '../../components/HeaderTextTitle';
import HeaderSaveImageButton from '../../components/HeaderSaveImageButton';
import Loader from '../../components/Loader';
import { READING_DIRECTION_TYPES } from '../../common/constants';

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#000',
  },
  slide: {
    flex: 1,
  },
});

class ImagesViewer extends Component {
  constructor(props) {
    super(props);
    const { route, imageReadingDirection } = props;
    const { images, viewerIndex } = route.params;
    const imagesWithDirection =
      imageReadingDirection === READING_DIRECTION_TYPES.RIGHT_TO_LEFT
        ? images.reverse()
        : images;
    this.state = {
      index:
        imageReadingDirection === READING_DIRECTION_TYPES.RIGHT_TO_LEFT
          ? images.length - 1 - viewerIndex
          : viewerIndex,
      images: imagesWithDirection.map((image) => ({
        url: image,
        loading: true,
      })),
      routes: imagesWithDirection.map((image) => ({
        key: image.toString(),
      })),
      hideHeader: true,
    };
  }

  handleOnImageLoaded = (imageUrl) => {
    this.setState(({ images }) => ({
      images: images.map((image) =>
        image.url === imageUrl ? { ...image, loading: false } : image,
      ),
    }));
  };

  handleOnPressImage = () => {
    this.setState((prevState) => ({
      hideHeader: !prevState.hideHeader,
    }));
  };

  renderScene = ({ route }) => {
    const { routes, images } = this.state;
    const sceneIndex = routes.indexOf(route);
    const image = images[sceneIndex];
    return (
      <View key={image.url} style={styles.slide}>
        {image.loading && (
          <Loader absolutePosition style={styles.loader} color="#fff" />
        )}
        <PXPhotoView
          uri={image.url}
          onLoad={this.handleOnImageLoaded}
          onTap={this.handleOnPressImage}
          onViewTap={this.handleOnPressImage}
        />
      </View>
    );
  };

  handleChangeTab = (index) => {
    this.setState({ index });
  };

  getCurrentPageNumber = () => {
    const { route, imageReadingDirection } = this.props;
    const { images } = route.params;
    const { index } = this.state;
    if (imageReadingDirection === READING_DIRECTION_TYPES.RIGHT_TO_LEFT) {
      return images.length - index;
    }
    return index + 1;
  };

  renderTabBar = () => null;

  render() {
    const { route, imageReadingDirection } = this.props;
    const { images, item } = route.params;
    const { index, hideHeader } = this.state;
    const selectedImages = [images[index]];
    return (
      <View style={styles.container}>
        <StatusBar hidden={hideHeader} barStyle="light-content" animated />
        {!hideHeader && (
          <PXHeader
            darkTheme
            withShadow
            absolutePosition
            showBackButton
            headerTitle={
              <HeaderTextTitle>
                {images.length > 1
                  ? `${this.getCurrentPageNumber()}/${images.length}`
                  : null}
              </HeaderTextTitle>
            }
            headerRight={
              <HeaderSaveImageButton
                imageUrls={selectedImages}
                imageIndex={
                  imageReadingDirection ===
                  READING_DIRECTION_TYPES.RIGHT_TO_LEFT
                    ? images.length - index
                    : index
                }
                workId={item.id}
                workTitle={item.title}
                workType={item.type}
                userId={item.user.id}
                userName={item.user.name}
              />
            }
          />
        )}
        <PXTabView
          navigationState={this.state}
          renderTabBar={this.renderTabBar}
          renderScene={this.renderScene}
          onIndexChange={this.handleChangeTab}
          lazyPreloadDistance={3}
        />
      </View>
    );
  }
}

export default connect((state) => {
  const { imageReadingDirection } = state.readingSettings;
  return {
    imageReadingDirection,
  };
})(ImagesViewer);
<commit_msg>Fix loader color on image viewer<commit_after>import React, { Component } from 'react';
import { StyleSheet, View, StatusBar } from 'react-native';
import { connect } from 'react-redux';
import PXHeader from '../../components/PXHeader';
import PXTabView from '../../components/PXTabView';
import PXPhotoView from '../../components/PXPhotoView';
import HeaderTextTitle from '../../components/HeaderTextTitle';
import HeaderSaveImageButton from '../../components/HeaderSaveImageButton';
import Loader from '../../components/Loader';
import { READING_DIRECTION_TYPES } from '../../common/constants';

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#000',
  },
  slide: {
    flex: 1,
  },
});

class ImagesViewer extends Component {
  constructor(props) {
    super(props);
    const { route, imageReadingDirection } = props;
    const { images, viewerIndex } = route.params;
    const imagesWithDirection =
      imageReadingDirection === READING_DIRECTION_TYPES.RIGHT_TO_LEFT
        ? images.reverse()
        : images;
    this.state = {
      index:
        imageReadingDirection === READING_DIRECTION_TYPES.RIGHT_TO_LEFT
          ? images.length - 1 - viewerIndex
          : viewerIndex,
      images: imagesWithDirection.map((image) => ({
        url: image,
        loading: true,
      })),
      routes: imagesWithDirection.map((image) => ({
        key: image.toString(),
      })),
      hideHeader: true,
    };
  }

  handleOnImageLoaded = (imageUrl) => {
    this.setState(({ images }) => ({
      images: images.map((image) =>
        image.url === imageUrl ? { ...image, loading: false } : image,
      ),
    }));
  };

  handleOnPressImage = () => {
    this.setState((prevState) => ({
      hideHeader: !prevState.hideHeader,
    }));
  };

  renderScene = ({ route }) => {
    const { routes, images } = this.state;
    const sceneIndex = routes.indexOf(route);
    const image = images[sceneIndex];
    return (
      <View key={image.url} style={styles.slide}>
        {image.loading && <Loader absolutePosition style={styles.loader} />}
        <PXPhotoView
          uri={image.url}
          onLoad={this.handleOnImageLoaded}
          onTap={this.handleOnPressImage}
          onViewTap={this.handleOnPressImage}
        />
      </View>
    );
  };

  handleChangeTab = (index) => {
    this.setState({ index });
  };

  getCurrentPageNumber = () => {
    const { route, imageReadingDirection } = this.props;
    const { images } = route.params;
    const { index } = this.state;
    if (imageReadingDirection === READING_DIRECTION_TYPES.RIGHT_TO_LEFT) {
      return images.length - index;
    }
    return index + 1;
  };

  renderTabBar = () => null;

  render() {
    const { route, imageReadingDirection } = this.props;
    const { images, item } = route.params;
    const { index, hideHeader } = this.state;
    const selectedImages = [images[index]];
    return (
      <View style={styles.container}>
        <StatusBar hidden={hideHeader} barStyle="light-content" animated />
        {!hideHeader && (
          <PXHeader
            darkTheme
            withShadow
            absolutePosition
            showBackButton
            headerTitle={
              <HeaderTextTitle>
                {images.length > 1
                  ? `${this.getCurrentPageNumber()}/${images.length}`
                  : null}
              </HeaderTextTitle>
            }
            headerRight={
              <HeaderSaveImageButton
                imageUrls={selectedImages}
                imageIndex={
                  imageReadingDirection ===
                  READING_DIRECTION_TYPES.RIGHT_TO_LEFT
                    ? images.length - index
                    : index
                }
                workId={item.id}
                workTitle={item.title}
                workType={item.type}
                userId={item.user.id}
                userName={item.user.name}
              />
            }
          />
        )}
        <PXTabView
          navigationState={this.state}
          renderTabBar={this.renderTabBar}
          renderScene={this.renderScene}
          onIndexChange={this.handleChangeTab}
          lazyPreloadDistance={3}
        />
      </View>
    );
  }
}

export default connect((state) => {
  const { imageReadingDirection } = state.readingSettings;
  return {
    imageReadingDirection,
  };
})(ImagesViewer);
<|endoftext|>


<commit_before>import { createStore } from 'redux';
import { expect } from 'chai';
import rootReducer from './index';
import groups from './groups';
import choices from './choices';
import items from './items';

describe('reducers/rootReducer', () => {
  const store = createStore(rootReducer);

  it('returns expected reducers', () => {
    const state = store.getState();

    expect(state.groups).to.equal(groups(undefined, {}));
    expect(state.choices).to.equal(choices(undefined, {}));
    expect(state.items).to.equal(items(undefined, {}));
  });

  describe('CLEAR_ALL', () => {
    it('resets state', () => {
      const output = rootReducer(
        {
          items: [1, 2, 3],
          groups: [1, 2, 3],
          choices: [1, 2, 3],
        },
        {
          type: 'CLEAR_ALL',
        },
      );

      expect(output).to.eql({
        items: [],
        groups: [],
        choices: [],
        general: {
          loading: false,
        },
      });
    });
  });

  describe('RESET_TO', () => {
    it('replaces state with given state', () => {
      const output = rootReducer(
        {
          items: [1, 2, 3],
          groups: [1, 2, 3],
          choices: [1, 2, 3],
        },
        {
          type: 'RESET_TO',
          state: {},
        },
      );

      expect(output).to.eql({});
    });
  });
});
<commit_msg>Updated reference to choices<commit_after>import { createStore } from 'redux';
import { expect } from 'chai';
import rootReducer from './index';
import groups from './groups';
import { choices } from './choices';
import items from './items';

describe('reducers/rootReducer', () => {
  const store = createStore(rootReducer);

  it('returns expected reducers', () => {
    const state = store.getState();

    expect(state.groups).to.equal(groups(undefined, {}));
    expect(state.choices).to.equal(choices(undefined, {}));
    expect(state.items).to.equal(items(undefined, {}));
  });

  describe('CLEAR_ALL', () => {
    it('resets state', () => {
      const output = rootReducer(
        {
          items: [1, 2, 3],
          groups: [1, 2, 3],
          choices: [1, 2, 3],
        },
        {
          type: 'CLEAR_ALL',
        },
      );

      expect(output).to.eql({
        items: [],
        groups: [],
        choices: [],
        general: {
          loading: false,
        },
      });
    });
  });

  describe('RESET_TO', () => {
    it('replaces state with given state', () => {
      const output = rootReducer(
        {
          items: [1, 2, 3],
          groups: [1, 2, 3],
          choices: [1, 2, 3],
        },
        {
          type: 'RESET_TO',
          state: {},
        },
      );

      expect(output).to.eql({});
    });
  });
});
<|endoftext|>


<commit_before>/*
 * Copyright 2014 WebFilings, LLC
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

define(function(require) {
    'use strict';

    var _ = require('lodash');
    var EventTypes = require('wf-js-uicomponents/awesome_map/EventTypes');
    var InterceptorMixin = require('wf-js-uicomponents/awesome_map/InterceptorMixin');

    /**
     * Creates a new PeekInterceptor.
     *
     * @classdesc
     *
     * The PeekInterceptor is used by the ScrollList to enable dragging
     * adjoining content into view and either snapping the content back into
     * place if peeking below a threshold distance or initiating a content jump
     * if peeking beyond the threshold.
     *
     * @name PeekInterceptor
     * @constructor
     * @mixes InterceptorMixin
     *
     * @param {ScrollList} scrollList
     *        The scroll list that owns this interceptor.
     */
    var PeekInterceptor = function(scrollList) {

        //---------------------------------------------------------
        // Private properties
        //---------------------------------------------------------

        /**
         * Flag to force a content jump along the direction of a peek on release.
         * Can be falsey to signify no forced jump is in effect, or can be +/- 1
         * to force jump to next or previous item.
         * @type {boolean|number}
         * @private
         */
        this._forceJump = false;

        /**
         * The current item index when touch event is detected. This allows some
         * sanity when dealing with transitions on release events since the
         * current item index will change as items move across the viewport
         * during drag events.
         * typ
         * @type {number}
         * @private
         */
        this._itemIndexAtTouch = null;

        /**
         * The pixel amount that is being peeked at.
         * @type {number}
         * @private
         */
        this._peekDelta = 0;

        /**
         * Whether or not adjoining content is peeked at.
         * @type {boolean}
         * @private
         */
        this._peeking = false;

        /**
         * The scroll list that owns this interceptor.
         * @type {ScrollList}
         * @private
         */
        this._scrollList = scrollList;
    };

    PeekInterceptor.prototype = {

        //---------------------------------------------------------
        // Public methods
        //---------------------------------------------------------

        /**
         * Intercepts drag events generated by direct user interaction
         * and peeks at adjoining content if contextually appropriate.
         * Note that transforms are applied DIRECTLY to to list map,
         * and are not sent through the normal handling process. This can lead
         * to some tricky behavior if you do not account for the bypass.
         * @param {AwesomeMap} source - The source map of the event.
         * @param {InteractionEvent} args.event - The interaction event.
         * @return {undefined|boolean}
         */
        handleInteraction: function(source, args) {
            var event = args.event;

            if (event.simulated || this._scrollList.isDisabled()) {
                return;
            }

            switch (event.type) {

            case EventTypes.TOUCH:

                this._handleTouchEvent();
                return;

            case EventTypes.DRAG:
            case EventTypes.DRAG_END:
            case EventTypes.DRAG_START:

                this._handleDragEvent(event);
                break;

            case EventTypes.SWIPE:

                this._handleSwipeEvent(event);
                break;

            case EventTypes.RELEASE:

                this._handleReleaseEvent();
                break;
            }

            return !this._peeking;
        },

        //---------------------------------------------------------
        // Private methods
        //---------------------------------------------------------

        /**
         * Handles drag events and peeks if appropriate.
         * @param {InteractionEvent} event
         */
        _handleDragEvent: function(event) {
            var itemMap = this._scrollList.getCurrentItemMap();
            var viewportHeight = itemMap.getViewportDimensions().height;
            var contentHeight = itemMap.getContentDimensions().height;
            var contentState = itemMap.getCurrentTransformState();
            var contentTop = contentState.translateY;
            var contentBottom = contentState.translateY +
                                Math.floor(contentHeight * contentState.scale);

            var listMap = this._scrollList.getListMap();
            var listState = listMap.getCurrentTransformState();
            var listHeight = listMap.getContentDimensions().height;
            var listTop = listState.translateY;
            var listBottom = listState.translateY + listHeight;
            var currentY = listState.translateY;

            this._setPeekDeltaByCurrentPosition();

            var peekDelta = this._peekDelta;
            var deltaY = event.iterativeGesture.deltaY;
            var newY;

            // Ignore event if:
            // - dragging down and list will be at top, or
            // - dragging up and list will be at bottom.
            if ((deltaY > 0 && listTop + contentTop + deltaY >= 0) ||
                (deltaY < 0 && listBottom + (contentBottom - viewportHeight) + deltaY <= viewportHeight)) {

                newY = deltaY > 0 ? 0 : viewportHeight - listHeight;
                this._resetPeekState();
            }
            // Start/continue peeking if:
            // - dragging down and top is visible, or
            // - dragging up and bottom is visible.
            else if (
                (deltaY > 0 && contentTop >= 0) ||
                (deltaY < 0 && contentBottom <= viewportHeight)) {

                newY = currentY + deltaY;
                this._peeking = true;
                this._peekDelta += deltaY;
            }
            // Undo peeking:
            else if (peekDelta !== 0) {

                // Reversing direction if:
                // - dragging down with peek into next content item still visible, or
                // - dragging up with peek into previous content item still visible.
                if ((deltaY > 0 && peekDelta + deltaY < 0) ||
                    (deltaY < 0 && peekDelta + deltaY > 0)) {

                    newY = currentY + deltaY;
                    this._peekDelta += deltaY;
                }
                // Stop peeking if:
                // - dragging down and deltaY will bring content to bottom of viewport, or
                // - dragging up and deltaY will bring content to top of viewport
                else if (deltaY !== 0) {

                    newY = currentY - this._peekDelta;
                    this._resetPeekState();
                }
            }
            else {
                // Modify event to impose boundary condition on item map if:
                // - dragging down and top will be visible, or
                // - dragging up and bottom will be visible.
                if (deltaY > 0 && contentTop + deltaY >= 0) {
                    event.iterativeGesture.deltaY = -contentTop;
                }
                else if (deltaY < 0 && contentBottom + deltaY <= viewportHeight) {
                    event.iterativeGesture.deltaY = viewportHeight - contentBottom;
                }
            }

            // If we have a new Y value, transform the list directly.
            if (newY !== undefined) {
                listMap.transform({
                    x: listState.translateX,
                    y: newY,
                    scale: listState.scale
                });
            }
        },

        /**
         * Force a jump to an adjoining item on swipe events. Detect whether the
         * jump is necessary by inspecting the current peek delta and direction
         * of swipe. For instance: if starting on item 1 and dragging up so that
         * the top of item 2 crosses the viewport center, then do not force a
         * jump on release, as the default behavior will pull item 2 to the top;
         * however, if item 1 is still the item at center when the swipe occurs,
         * then we need to jump to item 2.
         * @param {InteractionEvent} event
         */
        _handleSwipeEvent: function(event) {
            var peekDelta = this._peekDelta;
            if (this._peekDelta !== 0) {
                var direction = event.iterativeGesture.direction;
                if (direction === 'up' && peekDelta < 0) {
                    this._forceJump = 1;
                }
                else if (direction === 'down' && peekDelta > 0) {
                    this._forceJump = -1;
                }
            }
        },

        /**
         * Jump to an adjoining item if peeking beyond viewport center.
         * If forcing a jump due to handling swipes, then jump one item in the
         * direction specified.
         */
        _handleReleaseEvent: function() {
            if (!this._peeking) {
                return;
            }

            var scrollList = this._scrollList;
            var layout = scrollList.getLayout();
            var itemRange = layout.getRenderedItemRange();
            var itemIndex = layout.getCurrentItemIndex();

            if (this._forceJump) {
                itemIndex += this._forceJump;
            }
            else {
                var peekDelta = this._peekDelta;
                var viewportHeight = layout.getViewportSize().height;
                // If the item index has not changed during the drag and
                // have dragged beyond 1/3 of the viewport, then scroll to
                // adjacent item. If the item index _has_ changed, then the
                // current item shall be our scroll target.
                if (this._itemIndexAtTouch === itemIndex &&
                    Math.abs(peekDelta) > viewportHeight / 3
                ) {
                    itemIndex += peekDelta > 0 ? -1 : 1;
                }
            }

            // ninja scroll out-of-range prevention - If you were too fast, then
            // put the index back in the range.
            if (itemIndex > itemRange.endIndex) {
                itemIndex = itemRange.endIndex;
            }
            if (itemIndex < itemRange.startIndex) {
                itemIndex = itemRange.startIndex;
            }

            setTimeout(function() {
                scrollList.scrollToItem({ index: itemIndex, duration: 250});
            }, 0);

            this._resetPeekState();
        },

        _handleTouchEvent: function() {
            this._setPeekDeltaByCurrentPosition();
            var layout = this._scrollList.getLayout();
            this._itemIndexAtTouch = layout.getCurrentItemIndex();
        },

        /**
         * Resets the private peek state.
         */
        _resetPeekState: function() {
            this._peekDelta = 0;
            this._peeking = false;
            this._forceJump = false;
        },

        /**
         * Sets the peek delta by looking at the top position of the current
         * item and the current list/viewport position. This is used to handle
         * two cases: 1) interrupting an animated page change mid-stream and
         * 2) updating the peek delta when the current item changes due to
         * crossing the center of the viewport.
         */
        _setPeekDeltaByCurrentPosition: function() {
            var listMap = this._scrollList.getListMap();
            var currentState = listMap.getCurrentTransformState();
            var viewportTop = -currentState.translateY / currentState.scale;

            var layout = this._scrollList.getLayout();
            var currentItemIndex = layout.getCurrentItemIndex();
            var currentItemLayout = layout.getItemLayout(currentItemIndex);
            var currentItemTop = currentItemLayout.top;

            if (viewportTop !== currentItemTop) {
                this._peeking = true;
                this._peekDelta = currentItemTop - viewportTop;
            }
        }
    };

    _.assign(PeekInterceptor.prototype, InterceptorMixin);

    return PeekInterceptor;
});

<commit_msg>readded comment<commit_after>/*
 * Copyright 2014 WebFilings, LLC
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

define(function(require) {
    'use strict';

    var _ = require('lodash');
    var EventTypes = require('wf-js-uicomponents/awesome_map/EventTypes');
    var InterceptorMixin = require('wf-js-uicomponents/awesome_map/InterceptorMixin');

    /**
     * Creates a new PeekInterceptor.
     *
     * @classdesc
     *
     * The PeekInterceptor is used by the ScrollList to enable dragging
     * adjoining content into view and either snapping the content back into
     * place if peeking below a threshold distance or initiating a content jump
     * if peeking beyond the threshold.
     *
     * @name PeekInterceptor
     * @constructor
     * @mixes InterceptorMixin
     *
     * @param {ScrollList} scrollList
     *        The scroll list that owns this interceptor.
     */
    var PeekInterceptor = function(scrollList) {

        //---------------------------------------------------------
        // Private properties
        //---------------------------------------------------------

        /**
         * Flag to force a content jump along the direction of a peek on release.
         * Can be falsey to signify no forced jump is in effect, or can be +/- 1
         * to force jump to next or previous item.
         * @type {boolean|number}
         * @private
         */
        this._forceJump = false;

        /**
         * The current item index when touch event is detected. This allows some
         * sanity when dealing with transitions on release events since the
         * current item index will change as items move across the viewport
         * during drag events.
         * typ
         * @type {number}
         * @private
         */
        this._itemIndexAtTouch = null;

        /**
         * The pixel amount that is being peeked at.
         * @type {number}
         * @private
         */
        this._peekDelta = 0;

        /**
         * Whether or not adjoining content is peeked at.
         * @type {boolean}
         * @private
         */
        this._peeking = false;

        /**
         * The scroll list that owns this interceptor.
         * @type {ScrollList}
         * @private
         */
        this._scrollList = scrollList;
    };

    PeekInterceptor.prototype = {

        //---------------------------------------------------------
        // Public methods
        //---------------------------------------------------------

        /**
         * Intercepts drag events generated by direct user interaction
         * and peeks at adjoining content if contextually appropriate.
         * Note that transforms are applied DIRECTLY to to list map,
         * and are not sent through the normal handling process. This can lead
         * to some tricky behavior if you do not account for the bypass.
         * @param {AwesomeMap} source - The source map of the event.
         * @param {InteractionEvent} args.event - The interaction event.
         * @return {undefined|boolean}
         */
        handleInteraction: function(source, args) {
            var event = args.event;

            if (event.simulated || this._scrollList.isDisabled()) {
                return;
            }

            switch (event.type) {

            case EventTypes.TOUCH:

                this._handleTouchEvent();
                return;

            case EventTypes.DRAG:
            case EventTypes.DRAG_END:
            case EventTypes.DRAG_START:

                this._handleDragEvent(event);
                break;

            case EventTypes.SWIPE:

                this._handleSwipeEvent(event);
                break;

            case EventTypes.RELEASE:

                this._handleReleaseEvent();
                break;
            }

            return !this._peeking;
        },

        //---------------------------------------------------------
        // Private methods
        //---------------------------------------------------------

        /**
         * Handles drag events and peeks if appropriate.
         * @param {InteractionEvent} event
         */
        _handleDragEvent: function(event) {
            var itemMap = this._scrollList.getCurrentItemMap();
            var viewportHeight = itemMap.getViewportDimensions().height;
            var contentHeight = itemMap.getContentDimensions().height;
            var contentState = itemMap.getCurrentTransformState();
            var contentTop = contentState.translateY;
            var contentBottom = contentState.translateY +
                                Math.floor(contentHeight * contentState.scale);

            var listMap = this._scrollList.getListMap();
            var listState = listMap.getCurrentTransformState();
            var listHeight = listMap.getContentDimensions().height;
            var listTop = listState.translateY;
            var listBottom = listState.translateY + listHeight;
            var currentY = listState.translateY;

            this._setPeekDeltaByCurrentPosition();

            var peekDelta = this._peekDelta;
            var deltaY = event.iterativeGesture.deltaY;
            var newY;

            // Ignore event if:
            // - dragging down and list will be at top, or
            // - dragging up and list will be at bottom.
            if ((deltaY > 0 && listTop + contentTop + deltaY >= 0) ||
                (deltaY < 0 && listBottom + (contentBottom - viewportHeight) + deltaY <= viewportHeight)) {

                newY = deltaY > 0 ? 0 : viewportHeight - listHeight;
                this._resetPeekState();
            }
            // Start/continue peeking if:
            // - dragging down and top is visible, or
            // - dragging up and bottom is visible.
            else if (
                (deltaY > 0 && contentTop >= 0) ||
                (deltaY < 0 && contentBottom <= viewportHeight)) {

                newY = currentY + deltaY;
                this._peeking = true;
                this._peekDelta += deltaY;
            }
            // Undo peeking:
            else if (peekDelta !== 0) {

                // Reversing direction if:
                // - dragging down with peek into next content item still visible, or
                // - dragging up with peek into previous content item still visible.
                if ((deltaY > 0 && peekDelta + deltaY < 0) ||
                    (deltaY < 0 && peekDelta + deltaY > 0)) {

                    newY = currentY + deltaY;
                    this._peekDelta += deltaY;
                }
                // Stop peeking if:
                // - dragging down and deltaY will bring content to bottom of viewport, or
                // - dragging up and deltaY will bring content to top of viewport
                else if (deltaY !== 0) {

                    newY = currentY - this._peekDelta;
                    this._resetPeekState();
                }
            }
            else {
                // Modify event to impose boundary condition on item map if:
                // - dragging down and top will be visible, or
                // - dragging up and bottom will be visible.
                if (deltaY > 0 && contentTop + deltaY >= 0) {
                    event.iterativeGesture.deltaY = -contentTop;
                }
                else if (deltaY < 0 && contentBottom + deltaY <= viewportHeight) {
                    event.iterativeGesture.deltaY = viewportHeight - contentBottom;
                }
            }

            // If we have a new Y value, transform the list directly.
            if (newY !== undefined) {
                listMap.transform({
                    x: listState.translateX,
                    y: newY,
                    scale: listState.scale
                });
            }
        },

        /**
         * Force a jump to an adjoining item on swipe events. Detect whether the
         * jump is necessary by inspecting the current peek delta and direction
         * of swipe. For instance: if starting on item 1 and dragging up so that
         * the top of item 2 crosses the viewport center, then do not force a
         * jump on release, as the default behavior will pull item 2 to the top;
         * however, if item 1 is still the item at center when the swipe occurs,
         * then we need to jump to item 2.
         * @param {InteractionEvent} event
         */
        _handleSwipeEvent: function(event) {
            var peekDelta = this._peekDelta;
            if (this._peekDelta !== 0) {
                var direction = event.iterativeGesture.direction;
                if (direction === 'up' && peekDelta < 0) {
                    this._forceJump = 1;
                }
                else if (direction === 'down' && peekDelta > 0) {
                    this._forceJump = -1;
                }
            }
        },

        /**
         * Jump to an adjoining item if peeking beyond viewport center.
         * If forcing a jump due to handling swipes, then jump one item in the
         * direction specified.
         */
        _handleReleaseEvent: function() {
            if (!this._peeking) {
                return;
            }

            var scrollList = this._scrollList;
            var layout = scrollList.getLayout();
            var itemRange = layout.getRenderedItemRange();
            var itemIndex = layout.getCurrentItemIndex();

            if (this._forceJump) {
                itemIndex += this._forceJump;
            }
            else {
                var peekDelta = this._peekDelta;
                var viewportHeight = layout.getViewportSize().height;
                // If the item index has not changed during the drag and
                // have dragged beyond 1/3 of the viewport, then scroll to
                // adjacent item. If the item index _has_ changed, then the
                // current item shall be our scroll target.
                if (this._itemIndexAtTouch === itemIndex &&
                    Math.abs(peekDelta) > viewportHeight / 3
                ) {
                    itemIndex += peekDelta > 0 ? -1 : 1;
                }
            }

            // ninja scroll out-of-range prevention - If you were too fast, then
            // put the index back in the range.
            if (itemIndex > itemRange.endIndex) {
                itemIndex = itemRange.endIndex;
            }
            if (itemIndex < itemRange.startIndex) {
                itemIndex = itemRange.startIndex;
            }

            // Let this release event play out before scrolling.
            setTimeout(function() {
                scrollList.scrollToItem({ index: itemIndex, duration: 250});
            }, 0);

            this._resetPeekState();
        },

        _handleTouchEvent: function() {
            this._setPeekDeltaByCurrentPosition();
            var layout = this._scrollList.getLayout();
            this._itemIndexAtTouch = layout.getCurrentItemIndex();
        },

        /**
         * Resets the private peek state.
         */
        _resetPeekState: function() {
            this._peekDelta = 0;
            this._peeking = false;
            this._forceJump = false;
        },

        /**
         * Sets the peek delta by looking at the top position of the current
         * item and the current list/viewport position. This is used to handle
         * two cases: 1) interrupting an animated page change mid-stream and
         * 2) updating the peek delta when the current item changes due to
         * crossing the center of the viewport.
         */
        _setPeekDeltaByCurrentPosition: function() {
            var listMap = this._scrollList.getListMap();
            var currentState = listMap.getCurrentTransformState();
            var viewportTop = -currentState.translateY / currentState.scale;

            var layout = this._scrollList.getLayout();
            var currentItemIndex = layout.getCurrentItemIndex();
            var currentItemLayout = layout.getItemLayout(currentItemIndex);
            var currentItemTop = currentItemLayout.top;

            if (viewportTop !== currentItemTop) {
                this._peeking = true;
                this._peekDelta = currentItemTop - viewportTop;
            }
        }
    };

    _.assign(PeekInterceptor.prototype, InterceptorMixin);

    return PeekInterceptor;
});

<|endoftext|>


<commit_before>'use strict';

// Setting up route
angular.module('core').config(['$stateProvider', '$urlRouterProvider',
  function ($stateProvider, $urlRouterProvider) {

    // Redirect to 404 when route not found
    $urlRouterProvider.otherwise(function ($injector, $location) {
      $injector.get('$state').transitionTo('not-found', null, {
        location: false
      });
    });

    // Home state routing
    $stateProvider
    .state('home', {
      url: '/',
      templateUrl: 'modules/core/client/views/home.client.view.html'
    })
    .state('about', {
      url: '/about',
      templateUrl: 'modules/core/client/views/about.client.view.html',
      data: {
        ignoreState: true
      }
    })
    .state('resources', {
      url: '/resources',
      templateUrl: 'modules/core/client/views/resources.client.view.html',
      data: {
        ignoreState: true
      }
    })
    .state('tests', {
      url: '/tests',
      templateUrl: 'modules/core/client/views/tests.client.view.html',
      data: {
        ignoreState: true
      }
    })
    .state('contact', {
      url: '/contact',
      templateUrl: 'modules/core/client/views/contact.client.view.html',
      data: {
        ignoreState: true
      }
    })
        .state('instructions', {
      url: '/instructions',
      templateUrl: 'modules/core/client/views/instructions.client.view.html',
      data: {
        ignoreState: true
      }
    })

    .state('access', {
      url: '/instructions/access',
      templateUrl: 'modules/core/client/views/access.client.view.html',
      data: {
        ignoreState: true
      }
    })
     .state('signIn', {
      url: '/instructions/signIn',
      templateUrl: 'modules/core/client/views/signIn.client.view.html',
      data: {
        ignoreState: true
      }
    })
    .state('features', {
      url: '/instructions/features',
      templateUrl: 'modules/core/client/views/features.client.view.html',
      data: {
        ignoreState: true
      }
    })
    .state('types', {
      url: '/instructions/types',
      templateUrl: 'modules/core/client/views/types.client.view.html',
      data: {
        ignoreState: true
      }
    })
    .state('end', {
      url: '/end',
      templateUrl: 'modules/test_portal/client/views/end.client.view.html',
      data: {
        ignoreState: true
      }
    })

    .state('bad-request', {
      url: '/bad-request',
      templateUrl: 'modules/core/client/views/400.client.view.html',
      data: {
        ignoreState: true
      }
    })
    .state('forbidden', {
      url: '/forbidden',
      templateUrl: 'modules/core/client/views/403.client.view.html',
      data: {
        ignoreState: true
      }
    });
  }
]);
<commit_msg>Fixed not-found error.<commit_after>'use strict';

// Setting up route
angular.module('core').config(['$stateProvider', '$urlRouterProvider',
  function ($stateProvider, $urlRouterProvider) {

    // Redirect to 404 when route not found
    $urlRouterProvider.otherwise(function ($injector, $location) {
      /*$injector.get('$state').transitionTo('not-found', null, {
        location: false
      });*/
    });

    // Home state routing
    $stateProvider
    .state('home', {
      url: '/',
      templateUrl: 'modules/core/client/views/home.client.view.html'
    })
    .state('not-found', {
      url: '/not-found',
      templateUrl: 'modules/core/client/views/404.client.view.html',
      data: {
        ignoreState: true
      }
    })
    .state('about', {
      url: '/about',
      templateUrl: 'modules/core/client/views/about.client.view.html',
      data: {
        ignoreState: true
      }
    })
    .state('resources', {
      url: '/resources',
      templateUrl: 'modules/core/client/views/resources.client.view.html',
      data: {
        ignoreState: true
      }
    })
    .state('tests', {
      url: '/tests',
      templateUrl: 'modules/core/client/views/tests.client.view.html',
      data: {
        ignoreState: true
      }
    })
    .state('contact', {
      url: '/contact',
      templateUrl: 'modules/core/client/views/contact.client.view.html',
      data: {
        ignoreState: true
      }
    })
        .state('instructions', {
      url: '/instructions',
      templateUrl: 'modules/core/client/views/instructions.client.view.html',
      data: {
        ignoreState: true
      }
    })

    .state('access', {
      url: '/instructions/access',
      templateUrl: 'modules/core/client/views/access.client.view.html',
      data: {
        ignoreState: true
      }
    })
     .state('signIn', {
      url: '/instructions/signIn',
      templateUrl: 'modules/core/client/views/signIn.client.view.html',
      data: {
        ignoreState: true
      }
    })
    .state('features', {
      url: '/instructions/features',
      templateUrl: 'modules/core/client/views/features.client.view.html',
      data: {
        ignoreState: true
      }
    })
    .state('types', {
      url: '/instructions/types',
      templateUrl: 'modules/core/client/views/types.client.view.html',
      data: {
        ignoreState: true
      }
    })
    .state('end', {
      url: '/end',
      templateUrl: 'modules/test_portal/client/views/end.client.view.html',
      data: {
        ignoreState: true
      }
    })

    .state('bad-request', {
      url: '/bad-request',
      templateUrl: 'modules/core/client/views/400.client.view.html',
      data: {
        ignoreState: true
      }
    })
    .state('forbidden', {
      url: '/forbidden',
      templateUrl: 'modules/core/client/views/403.client.view.html',
      data: {
        ignoreState: true
      }
    });
  }
]);
<|endoftext|>


<commit_before>import Mongo from 'mongodb';
import connect from '../connect';
import permission from '../permission';

/**
 * Create a new challenge
 * @param {*} user
 * @param {String} key
 * @param {String} group
 * @param {String} type
 * @param {String} passphrase
 */
const createChallenge = async function(user, key, order, passphrase = null, title = '', description = '') {
	if (!user) return new Error('No user logged in');

	const authorized = await permission.checkPermission(user, ['admin:create-challenge']);
	if (authorized !== true) return authorized;

	// Validate parameters
	if (!key) return new Error('A challenge key is required.');
	if (order === null || order.length < 1) return new Error('A challenge order is required.');
	else if (isNaN(parseInt(order))) return new Error('Challenge order must be a valid integer');

	const db = await connect();

	// Ensure challenge key unique
	let challengeCheck = await db.collection('challenges').findOne({key});
	if (challengeCheck) return new Error(`A challenge with the key '${key}' already exists.`);

	// Create new challenge
	const challenge = {
		key: key,
		order: order,
		public: passphrase ? false : true,
		passphrase: passphrase,
		title: title,
		description: description,
		locked: false,
		items: [],
		teams: []
	};
	db.collection('challenges').insert(challenge);

	// Log action
	const action = {
		action: 'Create challenge',
		target: key,
		targetCollection: 'challenges',
		date: new Date(),
		who: user.username,
		infoJSONString: JSON.stringify({ key: key, order: order, public: challenge.public, passphrase: challenge.passphrase })
	};
	db.collection('actions').insert(action);

	return {
		ok: true,
		action: action
	}
}

/**
 * Delete a challenge
 * @param {*} user
 * @param {String} key
 */
const deleteChallenge = async function(user, key) {
	if (!user) return new Error('No user logged in');

	const authorized = await permission.checkPermission(user, ['admin:delete-challenge']);
	if (authorized !== true) return authorized;

	// Validate parameters
	if (!key) return new Error('A challenge key is required.');

	const db = await connect();

	// Ensure challenge key exists
	let challengeCheck = await db.collection('challenges').findOne({key});
	if (!challengeCheck) return new Error(`A challenge with the key '${key}' does not exist.`);

	//Remove the challenge
	db.collection('challenges').remove({key});

	// Log action
	const action = {
		action: 'Delete challenge',
		target: key,
		targetCollection: 'challenges',
		date: new Date(),
		who: user.username
	};
	db.collection('actions').insert(action);

	return {
		ok: true,
		action: action
	}
}

/**
 * Edit a challenge's property
 * @param {*} user
 * @param {*} key
 * @param {*} property
 * @param {*} value
 */
const _editChallengeProperty = async function(user, key, property, value) {
	if (!user) return new Error('No user logged in');

	const authorized = await permission.checkPermission(user, ['admin:edit-challenge']);
	if (authorized !== true) return authorized;

	if (!key) return new Error('A challenge key is required.');

	// Ensure property being edited is valid
	const editableProperties = [
		'type',
		'order',
		'public',
		'passphrase',
		'title',
		'description',
		'locked'
	];

	let valid = false;
	editableProperties.forEach((p) => { if (!valid) if (p === property) valid = true; });
	if (!valid) return new Error(`The property ${property} is not valid.`);

	const db = await connect();

	// Ensure challenge key exists
	let challengeCheck = await db.collection('challenges').findOne({key});
	if (!challengeCheck) return new Error(`A challenge with the key '${key}' does not exist.`);

	//Update the challenge property
	db.collection('challenges').update({key}, { $set: { [property]: value } });

	// Log action
	const action = {
		action: `Edit challenge property: ${property}`,
		target: key,
		targetCollection: 'challenges',
		date: new Date(),
		who: user.username,
		infoJSONString: JSON.stringify({property, value})
	};
	db.collection('actions').insert(action);

	return {
		ok: true,
		action: action
	}
}

/**
 * Get all challenges (admin only)
 * @param {*} user
 */
const getAllChallenges = async function(user) {
	if (!user) return new Error('No user logged in');

	const authorized = await permission.checkPermission(user, ['admin:view-allchallenges']);
	if (authorized !== true) return authorized;

	const db = await connect();
	return db.collection('challenges').find().sort({order: 1}).toArray();
}

/**
 * Get the challenges this user is permitted to see
 * @param {*} user
 */
const getChallenges = async function(user) {
	if (!user) return new Error('No user logged in');

	const authorized = await permission.checkPermission(user, ['user:view-challenges']);
	if (authorized !== true) return authorized;

	const db = await connect();

	const userProfile = await db.collection('users').findOne({username: user.username});
	return db.collection('challenges').find({
		$or: [{ teams: userProfile.teamId.toString() }, { public: true }]
	}).sort({order: 1}).toArray();
}

/**
 * Get a challenge (permitted users only)
 * @param {*} user
 * @param {*} key
 */
const getChallenge = async function(user, key) {
	if (!user) return new Error('No user logged in');

	let isAdmin = false;

	const adminAuthorized = await permission.checkPermission(user, ['admin:view-allchallenges']);
	if (adminAuthorized == true) isAdmin = true;

	if (!isAdmin) {
		const authorized = await permission.checkPermission(user, ['user:view-challenges']);
		if (authorized !== true) return authorized;
	}

	const db = await connect();

	const userProfile = await db.collection('users').findOne({username: user.username});

	if (isAdmin) {
		return db.collection('challenges').findOne({ key });
	}
	else {
		return db.collection('challenges').findOne({
			key,
			$or: [{ teams: userProfile.teamId.toString() }, { public: true }]
		});
	}
}

/**
 * Add a team to the list of unlocked teams
 * @param {*} user
 * @param {String} key
 * @param {String} teamId
 */
const addTeamToUnlocked = async function(user, key, teamId) {
	return _modifyTeams('add', user, key, teamId);
}

/**
 * Remove a team from the list of unlocked teams
 * @param {*} user
 * @param {String} key
 * @param {String} teamId
 */
const removeTeamFromUnlocked = async function(user, key, teamId) {
	return _modifyTeams('remove', user, key, teamId);
}

const _modifyTeams = async function(modifyAction, user, key, teamId) {
	if (!user) return new Error('No user logged in');

	const authorized = await permission.checkPermission(user, ['admin:modify-challenge-access']);
	if (authorized !== true) return authorized;

	// Ensure challenge exists
	const db = await connect();
	const challenge = await db.collection('challenges').findOne({key});
	if (!challenge) return new Error(`The challenge with the key '${key}' does not exist.`);

	// Ensure the team exists
	const teamCheck = await db.collection('teams').findOne({_id: Mongo.ObjectID(teamId)});
	if (!teamCheck) return new Error(`The team with the id '${teamId}' does not exist.`);

	let modifyActionDb;
	let found = false;

	// Find the teamId in the challenge list
	challenge.teams.forEach(id => {
		if (!found) { if (id.toString() === teamId) found = true; }
	});

	if (modifyAction === 'add') {
		// Ensure the team is not already in the list
		if (found) return new Error(`The team with the id '${teamId}' is already permitted to view this challenge.`);

		modifyActionDb = '$push';
	}
	else if (modifyAction === 'remove') {
		// Ensure that the team is in the list
		if (!found) return new Error(`The team with the id '${teamId}' is already not permitted to view this challenge.`);

		modifyActionDb = '$pull';
	}
	else return new Error(`Modification action ${action} not recognized.`);

	const result = await db.collection('challenges').update(
		// Selector
		{ key: key },
		// Update
		{ [modifyActionDb]: { teams: teamId } }
	);

	// Log action
	const action = {
		action: `Modify challenge access`,
		target: key,
		targetCollection: 'challenges',
		date: new Date(),
		who: user.username,
		infoJSONString: JSON.stringify({teamId, modifyAction})
	};
	db.collection('actions').insert(action);

	return {
		ok: true,
		action: action
	}
}

/**
 * Create a new challenge item and add it to a challenge
 * @param {*} user
 * @param {String} key
 * @param {String} itemKey
 * @param {String} title
 * @param {Number} order
 */
const createChallengeItem = async function(user, key, itemKey, title, order, type) {
	if (!user) return new Error('No user logged in');

	const authorized = await permission.checkPermission(user, ['admin:edit-challenge']);
	if (authorized !== true) return authorized;

	// Validate parameters
	if (!key) return new Error('A challenge key is required.');
	if (!itemKey) return new Error('A challenge item key is required.');
	if (!title) return new Error('A challenge key is required.');
	if (order === null || order.length < 1) return new Error('A challenge order is required.');
	else if (isNaN(order)) return new Error('Challenge order must be a valid integer.');

	const typeValues = ['upload', 'phrase'];
	let typeValid = false;
	typeValues.forEach((typeValue) => {
		if (type === typeValue) typeValid = true;
	});
	if (!typeValid) return new Error(`The type '${type}' is not valid.`);

	// Check that challenge exists
	const db = await connect();
	const challenge = await db.collection('challenges').findOne({key});
	if (!challenge) return new Error(`The challenge with the key '${key}' does not exist.`);

	// Check that challenge key doesn't exists already
	const challengeItemCheck = await db.collection('challenges').findOne({key, 'items.key': itemKey});
	if (challengeItemCheck) return new Error(`A challenge item with the key '${itemKey}' already exists.`);

	// Create challenge item
	const item = {
		key: itemKey,
		title: title,
		description: '',
		type: type,
		order: order
	}

	db.collection('challenges').update(
		// Selector
		{ key: key },
		// Update
		{ $push: { items: item } }
	);

	// Log action
	const action = {
		action: `Add challenge item`,
		target: key,
		targetCollection: 'challenges',
		date: new Date(),
		who: user.username,
		infoJSONString: JSON.stringify({key: itemKey, title, type})
	};
	db.collection('actions').insert(action);

	return {
		ok: true,
		action: action
	}
}

/**
 * Deletes a challenge item from a challenge
 * @param {*} user
 * @param {String} key
 * @param {String} itemKey
 */
const deleteChallengeItem = async function(user, key, itemKey) {
	if (!user) return new Error('No user logged in');

	const authorized = await permission.checkPermission(user, ['admin:edit-challenge']);
	if (authorized !== true) return authorized;

	// Validate parameters
	if (!key) return new Error('A challenge key is required.');
	if (!itemKey) return new Error('A challenge item key is required.');

	// Check that challenge exists
	const db = await connect();
	const challenge = await db.collection('challenges').findOne({key});
	if (!challenge) return new Error(`The challenge with the key '${key}' does not exist.`);

	// Remove item
	const result = await db.collection('challenges').update(
		// Selector
		{ key: key },
		// Update
		{ $pull: { items: { key: itemKey } } }
	);

	if (result.result.nModified == 0)
		return new Error(`Challenge item with the key '${itemKey}' was not found in the challenge '${key}'`);

	// Log action
	const action = {
		action: `Delete challenge item`,
		target: key,
		targetCollection: 'challenges',
		date: new Date(),
		who: user.username,
		infoJSONString: JSON.stringify({itemKey})
	};
	db.collection('actions').insert(action);

	return {
		ok: true,
		action: action
	}
}

/**
 * Modify a challenge item property
 * @param {*} user
 * @param {String} key
 * @param {String} itemKey
 * @param {String} property
 * @param {String} value
 */
const _editChallengeItemProperty = async function(user, key, itemKey, property, value) {
	if (!user) return new Error('No user logged in');

	const authorized = await permission.checkPermission(user, ['admin:edit-challenge']);
	if (authorized !== true) return authorized;

	// Validate parameters
	if (!key) return new Error('A challenge key is required.');

	// Ensure property being edited is valid
	const editableProperties = [
		'order',
		'title',
		'description'
	];

	let valid = false;
	editableProperties.forEach((p) => { if (!valid) if (p === property) valid = true; });
	if (!valid) return new Error(`The property ${property} is not valid.`);

	// Check that challenge exists
	const db = await connect();
	const challenge = await db.collection('challenges').findOne({key});
	if (!challenge) return new Error(`The challenge with the key '${key}' does not exist.`);

	// Check that challenge with the item key exists
	const challengeItemCheck = await db.collection('challenges').findOne({key, 'items.key': itemKey });
	if (!challengeItemCheck) return new Error(`A challenge item with the key '${itemKey}' does not exist in the challenge '${key}'.`);

	// Update the challenge item
	const result = await db.collection('challenges').update(
		// Selector
		{ key: key, 'items.key': itemKey },
		// Update
		{ $set: { [`items.$.${property}`]: value } }
	);

	if (result.result.nModified == 0)
		return new Error(`Challenge item with the id '${itemKey}' already has the value '${value}' for the property '${property}'`);

	// Log action
	const action = {
		action: `Modify challenge item: ${property}`,
		target: key,
		targetCollection: 'challenges',
		date: new Date(),
		who: user.username,
		infoJSONString: JSON.stringify({itemKey, property, value})
	};
	db.collection('actions').insert(action);

	return {
		ok: true,
		action: action
	}
}

export default {
	createChallenge,
	deleteChallenge,
	_editChallengeProperty,
	getAllChallenges,
	getChallenges,
	getChallenge,
	addTeamToUnlocked,
	removeTeamFromUnlocked,
	createChallengeItem,
	deleteChallengeItem,
	_editChallengeItemProperty
}
<commit_msg>[FIX] Fix error message<commit_after>import Mongo from 'mongodb';
import connect from '../connect';
import permission from '../permission';

/**
 * Create a new challenge
 * @param {*} user
 * @param {String} key
 * @param {String} group
 * @param {String} type
 * @param {String} passphrase
 */
const createChallenge = async function(user, key, order, passphrase = null, title = '', description = '') {
	if (!user) return new Error('No user logged in');

	const authorized = await permission.checkPermission(user, ['admin:create-challenge']);
	if (authorized !== true) return authorized;

	// Validate parameters
	if (!key) return new Error('A challenge key is required.');
	if (order === null || order.length < 1) return new Error('A challenge order is required.');
	else if (isNaN(parseInt(order))) return new Error('Challenge order must be a valid integer');

	const db = await connect();

	// Ensure challenge key unique
	let challengeCheck = await db.collection('challenges').findOne({key});
	if (challengeCheck) return new Error(`A challenge with the key '${key}' already exists.`);

	// Create new challenge
	const challenge = {
		key: key,
		order: order,
		public: passphrase ? false : true,
		passphrase: passphrase,
		title: title,
		description: description,
		locked: false,
		items: [],
		teams: []
	};
	db.collection('challenges').insert(challenge);

	// Log action
	const action = {
		action: 'Create challenge',
		target: key,
		targetCollection: 'challenges',
		date: new Date(),
		who: user.username,
		infoJSONString: JSON.stringify({ key: key, order: order, public: challenge.public, passphrase: challenge.passphrase })
	};
	db.collection('actions').insert(action);

	return {
		ok: true,
		action: action
	}
}

/**
 * Delete a challenge
 * @param {*} user
 * @param {String} key
 */
const deleteChallenge = async function(user, key) {
	if (!user) return new Error('No user logged in');

	const authorized = await permission.checkPermission(user, ['admin:delete-challenge']);
	if (authorized !== true) return authorized;

	// Validate parameters
	if (!key) return new Error('A challenge key is required.');

	const db = await connect();

	// Ensure challenge key exists
	let challengeCheck = await db.collection('challenges').findOne({key});
	if (!challengeCheck) return new Error(`A challenge with the key '${key}' does not exist.`);

	//Remove the challenge
	db.collection('challenges').remove({key});

	// Log action
	const action = {
		action: 'Delete challenge',
		target: key,
		targetCollection: 'challenges',
		date: new Date(),
		who: user.username
	};
	db.collection('actions').insert(action);

	return {
		ok: true,
		action: action
	}
}

/**
 * Edit a challenge's property
 * @param {*} user
 * @param {*} key
 * @param {*} property
 * @param {*} value
 */
const _editChallengeProperty = async function(user, key, property, value) {
	if (!user) return new Error('No user logged in');

	const authorized = await permission.checkPermission(user, ['admin:edit-challenge']);
	if (authorized !== true) return authorized;

	if (!key) return new Error('A challenge key is required.');

	// Ensure property being edited is valid
	const editableProperties = [
		'type',
		'order',
		'public',
		'passphrase',
		'title',
		'description',
		'locked'
	];

	let valid = false;
	editableProperties.forEach((p) => { if (!valid) if (p === property) valid = true; });
	if (!valid) return new Error(`The property ${property} is not valid.`);

	const db = await connect();

	// Ensure challenge key exists
	let challengeCheck = await db.collection('challenges').findOne({key});
	if (!challengeCheck) return new Error(`A challenge with the key '${key}' does not exist.`);

	//Update the challenge property
	db.collection('challenges').update({key}, { $set: { [property]: value } });

	// Log action
	const action = {
		action: `Edit challenge property: ${property}`,
		target: key,
		targetCollection: 'challenges',
		date: new Date(),
		who: user.username,
		infoJSONString: JSON.stringify({property, value})
	};
	db.collection('actions').insert(action);

	return {
		ok: true,
		action: action
	}
}

/**
 * Get all challenges (admin only)
 * @param {*} user
 */
const getAllChallenges = async function(user) {
	if (!user) return new Error('No user logged in');

	const authorized = await permission.checkPermission(user, ['admin:view-allchallenges']);
	if (authorized !== true) return authorized;

	const db = await connect();
	return db.collection('challenges').find().sort({order: 1}).toArray();
}

/**
 * Get the challenges this user is permitted to see
 * @param {*} user
 */
const getChallenges = async function(user) {
	if (!user) return new Error('No user logged in');

	const authorized = await permission.checkPermission(user, ['user:view-challenges']);
	if (authorized !== true) return authorized;

	const db = await connect();

	const userProfile = await db.collection('users').findOne({username: user.username});
	return db.collection('challenges').find({
		$or: [{ teams: userProfile.teamId.toString() }, { public: true }]
	}).sort({order: 1}).toArray();
}

/**
 * Get a challenge (permitted users only)
 * @param {*} user
 * @param {*} key
 */
const getChallenge = async function(user, key) {
	if (!user) return new Error('No user logged in');

	let isAdmin = false;

	const adminAuthorized = await permission.checkPermission(user, ['admin:view-allchallenges']);
	if (adminAuthorized == true) isAdmin = true;

	if (!isAdmin) {
		const authorized = await permission.checkPermission(user, ['user:view-challenges']);
		if (authorized !== true) return authorized;
	}

	const db = await connect();

	const userProfile = await db.collection('users').findOne({username: user.username});

	if (isAdmin) {
		return db.collection('challenges').findOne({ key });
	}
	else {
		return db.collection('challenges').findOne({
			key,
			$or: [{ teams: userProfile.teamId.toString() }, { public: true }]
		});
	}
}

/**
 * Add a team to the list of unlocked teams
 * @param {*} user
 * @param {String} key
 * @param {String} teamId
 */
const addTeamToUnlocked = async function(user, key, teamId) {
	return _modifyTeams('add', user, key, teamId);
}

/**
 * Remove a team from the list of unlocked teams
 * @param {*} user
 * @param {String} key
 * @param {String} teamId
 */
const removeTeamFromUnlocked = async function(user, key, teamId) {
	return _modifyTeams('remove', user, key, teamId);
}

const _modifyTeams = async function(modifyAction, user, key, teamId) {
	if (!user) return new Error('No user logged in');

	const authorized = await permission.checkPermission(user, ['admin:modify-challenge-access']);
	if (authorized !== true) return authorized;

	// Ensure challenge exists
	const db = await connect();
	const challenge = await db.collection('challenges').findOne({key});
	if (!challenge) return new Error(`The challenge with the key '${key}' does not exist.`);

	// Ensure the team exists
	const teamCheck = await db.collection('teams').findOne({_id: Mongo.ObjectID(teamId)});
	if (!teamCheck) return new Error(`The team with the id '${teamId}' does not exist.`);

	let modifyActionDb;
	let found = false;

	// Find the teamId in the challenge list
	challenge.teams.forEach(id => {
		if (!found) { if (id.toString() === teamId) found = true; }
	});

	if (modifyAction === 'add') {
		// Ensure the team is not already in the list
		if (found) return new Error(`The team with the id '${teamId}' is already permitted to view this challenge.`);

		modifyActionDb = '$push';
	}
	else if (modifyAction === 'remove') {
		// Ensure that the team is in the list
		if (!found) return new Error(`The team with the id '${teamId}' is already not permitted to view this challenge.`);

		modifyActionDb = '$pull';
	}
	else return new Error(`Modification action ${action} not recognized.`);

	const result = await db.collection('challenges').update(
		// Selector
		{ key: key },
		// Update
		{ [modifyActionDb]: { teams: teamId } }
	);

	// Log action
	const action = {
		action: `Modify challenge access`,
		target: key,
		targetCollection: 'challenges',
		date: new Date(),
		who: user.username,
		infoJSONString: JSON.stringify({teamId, modifyAction})
	};
	db.collection('actions').insert(action);

	return {
		ok: true,
		action: action
	}
}

/**
 * Create a new challenge item and add it to a challenge
 * @param {*} user
 * @param {String} key
 * @param {String} itemKey
 * @param {String} title
 * @param {Number} order
 */
const createChallengeItem = async function(user, key, itemKey, title, order, type) {
	if (!user) return new Error('No user logged in');

	const authorized = await permission.checkPermission(user, ['admin:edit-challenge']);
	if (authorized !== true) return authorized;

	// Validate parameters
	if (!key) return new Error('A challenge key is required.');
	if (!itemKey) return new Error('A challenge item key is required.');
	if (!title) return new Error('A challenge item title is required.');
	if (order === null || order.length < 1) return new Error('A challenge order is required.');
	else if (isNaN(order)) return new Error('Challenge order must be a valid integer.');

	const typeValues = ['upload', 'phrase'];
	let typeValid = false;
	typeValues.forEach((typeValue) => {
		if (type === typeValue) typeValid = true;
	});
	if (!typeValid) return new Error(`The type '${type}' is not valid.`);

	// Check that challenge exists
	const db = await connect();
	const challenge = await db.collection('challenges').findOne({key});
	if (!challenge) return new Error(`The challenge with the key '${key}' does not exist.`);

	// Check that challenge key doesn't exists already
	const challengeItemCheck = await db.collection('challenges').findOne({key, 'items.key': itemKey});
	if (challengeItemCheck) return new Error(`A challenge item with the key '${itemKey}' already exists.`);

	// Create challenge item
	const item = {
		key: itemKey,
		title: title,
		description: '',
		type: type,
		order: order
	}

	db.collection('challenges').update(
		// Selector
		{ key: key },
		// Update
		{ $push: { items: item } }
	);

	// Log action
	const action = {
		action: `Add challenge item`,
		target: key,
		targetCollection: 'challenges',
		date: new Date(),
		who: user.username,
		infoJSONString: JSON.stringify({key: itemKey, title, type})
	};
	db.collection('actions').insert(action);

	return {
		ok: true,
		action: action
	}
}

/**
 * Deletes a challenge item from a challenge
 * @param {*} user
 * @param {String} key
 * @param {String} itemKey
 */
const deleteChallengeItem = async function(user, key, itemKey) {
	if (!user) return new Error('No user logged in');

	const authorized = await permission.checkPermission(user, ['admin:edit-challenge']);
	if (authorized !== true) return authorized;

	// Validate parameters
	if (!key) return new Error('A challenge key is required.');
	if (!itemKey) return new Error('A challenge item key is required.');

	// Check that challenge exists
	const db = await connect();
	const challenge = await db.collection('challenges').findOne({key});
	if (!challenge) return new Error(`The challenge with the key '${key}' does not exist.`);

	// Remove item
	const result = await db.collection('challenges').update(
		// Selector
		{ key: key },
		// Update
		{ $pull: { items: { key: itemKey } } }
	);

	if (result.result.nModified == 0)
		return new Error(`Challenge item with the key '${itemKey}' was not found in the challenge '${key}'`);

	// Log action
	const action = {
		action: `Delete challenge item`,
		target: key,
		targetCollection: 'challenges',
		date: new Date(),
		who: user.username,
		infoJSONString: JSON.stringify({itemKey})
	};
	db.collection('actions').insert(action);

	return {
		ok: true,
		action: action
	}
}

/**
 * Modify a challenge item property
 * @param {*} user
 * @param {String} key
 * @param {String} itemKey
 * @param {String} property
 * @param {String} value
 */
const _editChallengeItemProperty = async function(user, key, itemKey, property, value) {
	if (!user) return new Error('No user logged in');

	const authorized = await permission.checkPermission(user, ['admin:edit-challenge']);
	if (authorized !== true) return authorized;

	// Validate parameters
	if (!key) return new Error('A challenge key is required.');

	// Ensure property being edited is valid
	const editableProperties = [
		'order',
		'title',
		'description'
	];

	let valid = false;
	editableProperties.forEach((p) => { if (!valid) if (p === property) valid = true; });
	if (!valid) return new Error(`The property ${property} is not valid.`);

	// Check that challenge exists
	const db = await connect();
	const challenge = await db.collection('challenges').findOne({key});
	if (!challenge) return new Error(`The challenge with the key '${key}' does not exist.`);

	// Check that challenge with the item key exists
	const challengeItemCheck = await db.collection('challenges').findOne({key, 'items.key': itemKey });
	if (!challengeItemCheck) return new Error(`A challenge item with the key '${itemKey}' does not exist in the challenge '${key}'.`);

	// Update the challenge item
	const result = await db.collection('challenges').update(
		// Selector
		{ key: key, 'items.key': itemKey },
		// Update
		{ $set: { [`items.$.${property}`]: value } }
	);

	if (result.result.nModified == 0)
		return new Error(`Challenge item with the id '${itemKey}' already has the value '${value}' for the property '${property}'`);

	// Log action
	const action = {
		action: `Modify challenge item: ${property}`,
		target: key,
		targetCollection: 'challenges',
		date: new Date(),
		who: user.username,
		infoJSONString: JSON.stringify({itemKey, property, value})
	};
	db.collection('actions').insert(action);

	return {
		ok: true,
		action: action
	}
}

export default {
	createChallenge,
	deleteChallenge,
	_editChallengeProperty,
	getAllChallenges,
	getChallenges,
	getChallenge,
	addTeamToUnlocked,
	removeTeamFromUnlocked,
	createChallengeItem,
	deleteChallengeItem,
	_editChallengeItemProperty
}
<|endoftext|>


<commit_before>/**
 * Bonus Manager
 *
 * @param {Game} game
 */
function BonusManager(game)
{
    BaseBonusManager.call(this, game);

    this.world         = new World(this.game.size, 1);
    this.popingTimeout = null;
    this.timeouts      = [];

    this.popBonus = this.popBonus.bind(this);
}

BonusManager.prototype = Object.create(BaseBonusManager.prototype);

/**
 * Start
 */
BonusManager.prototype.start = function()
{
    BaseBonusManager.prototype.start.call(this);

    this.popingTimeout = setTimeout(this.popBonus, this.getRandomPopingTime());
};

/**
 * Stop
 */
BonusManager.prototype.stop = function()
{
    BaseBonusManager.prototype.start.call(this);

    clearTimeout(this.popingTimeout);
    this.popingTimeout = null;

    this.clearTimeouts();
};

/**
 * Make a bonus 'pop'
 */
BonusManager.prototype.popBonus = function ()
{
    if (this.bonuses.count() < this.bonusCap) {
        var bonus;

        if (this.percentChance(50)) {
            bonus = new RabbitBonus('test');
        } else {
            bonus = new TurtleBonus('test');
        }

        bonus.setPosition(this.game.world.getRandomPosition(bonus.radius, 0.1));
        bonus.pop();

        this.world.addCircle([
            bonus.position[0],
            bonus.position[1],
            bonus.radius,
            1,
            bonus
        ]);

        this.bonuses.add(bonus);

        this.emit('bonus:pop', { game: this.game, bonus: bonus });
    }

    this.popingTimeout = setTimeout(this.popBonus, this.getRandomPopingTime());
};

/**
 * Test if an avatar catches a bonus
 *
 * @param {Avaatr} avatar
 */
BonusManager.prototype.testCatch = function(avatar)
{
    var bonus = this.world.getCircle([
        avatar.head[0],
        avatar.head[1],
        avatar.radius,
        1,
    ]);

    if (bonus) {
        console.log('bonus', bonus);
        bonus.clear();
        this.emit('bonus:clear', {game: this.game, bonus: bonus});
        this.timeouts.push(
            bonus.applyTo(avatar)
        );
    }
};

/**
 * Clear timeouts
 */
BaseBonusManager.prototype.clearTimeouts = function()
{
    for (var i = this.timeouts.length - 1; i >= 0; i--) {
        clearTimeout(this.timeouts[i]);
    }
};

/**
 * Has a percent of chance to return true
 *
 * @param percentTrue
 * @returns {boolean}
 */
BaseBonusManager.prototype.percentChance = function (percentTrue)
{
    percentTrue = percentTrue || 100;

    return (Math.floor(Math.random()*101) <= percentTrue);
};

/**
 * Get random printing time
 *
 * @return {Number}
 */
BaseBonusManager.prototype.getRandomPopingTime  = function()
{
    return this.bonusPopingTime * (1 +  Math.random() * 2);
};
<commit_msg>add mask to circle<commit_after>/**
 * Bonus Manager
 *
 * @param {Game} game
 */
function BonusManager(game)
{
    BaseBonusManager.call(this, game);

    this.world         = new World(this.game.size, 1);
    this.popingTimeout = null;
    this.timeouts      = [];

    this.popBonus = this.popBonus.bind(this);
}

BonusManager.prototype = Object.create(BaseBonusManager.prototype);

/**
 * Start
 */
BonusManager.prototype.start = function()
{
    BaseBonusManager.prototype.start.call(this);

    this.popingTimeout = setTimeout(this.popBonus, this.getRandomPopingTime());
};

/**
 * Stop
 */
BonusManager.prototype.stop = function()
{
    BaseBonusManager.prototype.start.call(this);

    clearTimeout(this.popingTimeout);
    this.popingTimeout = null;

    this.clearTimeouts();
};

/**
 * Make a bonus 'pop'
 */
BonusManager.prototype.popBonus = function ()
{
    if (this.bonuses.count() < this.bonusCap) {
        var bonus;

        if (this.percentChance(50)) {
            bonus = new RabbitBonus('test');
        } else {
            bonus = new TurtleBonus('test');
        }

        bonus.setPosition(this.game.world.getRandomPosition(bonus.radius, 0.1));
        bonus.pop();

        this.world.addCircle([
            bonus.position[0],
            bonus.position[1],
            bonus.radius,
            0,
            bonus
        ]);

        this.bonuses.add(bonus);

        this.emit('bonus:pop', { game: this.game, bonus: bonus });
    }

    this.popingTimeout = setTimeout(this.popBonus, this.getRandomPopingTime());
};

/**
 * Test if an avatar catches a bonus
 *
 * @param {Avaatr} avatar
 */
BonusManager.prototype.testCatch = function(avatar)
{
    var circle = this.world.getCircle([
        avatar.head[0],
        avatar.head[1],
        avatar.radius,
        0,
    ]);

    if (circle) {
        var bonus = circle[4];
        console.log('bonus', bonus);
        bonus.clear();
        this.emit('bonus:clear', {game: this.game, bonus: bonus});
        this.timeouts.push(
            bonus.applyTo(avatar)
        );
    }
};

/**
 * Clear timeouts
 */
BaseBonusManager.prototype.clearTimeouts = function()
{
    for (var i = this.timeouts.length - 1; i >= 0; i--) {
        clearTimeout(this.timeouts[i]);
    }
};

/**
 * Has a percent of chance to return true
 *
 * @param percentTrue
 * @returns {boolean}
 */
BaseBonusManager.prototype.percentChance = function (percentTrue)
{
    percentTrue = percentTrue || 100;

    return (Math.floor(Math.random()*101) <= percentTrue);
};

/**
 * Get random printing time
 *
 * @return {Number}
 */
BaseBonusManager.prototype.getRandomPopingTime  = function()
{
    return this.bonusPopingTime * (1 +  Math.random() * 2);
};
<|endoftext|>


<commit_before>import React, { Component } from 'react';
import PropTypes from 'prop-types';
import moment from 'moment';
import { momentObj } from 'react-moment-proptypes';
import Day from './Day';
import { range } from './utils';

const propTypes = {
  year: PropTypes.number.isRequired,
  month: PropTypes.number.isRequired,
  forceFullWeeks: PropTypes.bool.isRequired,
  showWeekSeparators: PropTypes.bool.isRequired,
  selectedDay: momentObj.isRequired,
  firstDayOfWeek: PropTypes.number.isRequired,
  selectingRange: PropTypes.arrayOf(momentObj),
  selectRange: PropTypes.bool.isRequired,
  selectedRange: PropTypes.arrayOf(momentObj),
  customClasses: PropTypes.oneOfType([PropTypes.object, PropTypes.func]),
  dayClicked: PropTypes.func.isRequired,
  dayHovered: PropTypes.func.isRequired
};

const defaultProps = {
  selectingRange: undefined,
  selectedRange: undefined,
  customClasses: undefined
};

class Month extends Component {
  constructor(props) {
    super(props);

    this.state = {};
  }

  componentWillReceiveProps(nextProps) {
    if (nextProps.selectingRange !== undefined) {
      this.setState({
        selectingRangeStart: nextProps.selectingRange[0].month(),
        selectingRangeEnd: nextProps.selectingRange[1].month()
      });
    }
  }

  shouldComponentUpdate(nextProps) {
    const { month, selectingRange, selectedRange } = this.props;
    const { selectingRangeStart, selectingRangeEnd } = this.state;

    // full repaint for some global-affecting rendering props
    if (
      this.props.year !== nextProps.year ||
      this.props.forceFullWeeks !== nextProps.forceFullWeeks ||
      this.props.showWeekSeparators !== nextProps.showWeekSeparators ||
      this.props.firstDayOfWeek !== nextProps.firstDayOfWeek ||
      this.props.selectRange !== nextProps.selectRange ||
      this.props.customClasses !== nextProps.customClasses
    ) {
      return true;
    }

    // if we get to this point and we are in 'selectRange' mode then it's likely that we have a change in selectingRange
    if (this.props.selectRange) {
      if (selectingRange === undefined) {
        let oldRangeStart = selectedRange[0].month();
        let oldRangeEnd = selectedRange[1].month();
        if (oldRangeStart > oldRangeEnd) [oldRangeStart, oldRangeEnd] = [oldRangeEnd, oldRangeStart];

        let newRangeStart = nextProps.selectingRange[0].month();
        let newRangeEnd = nextProps.selectingRange[1].month();
        if (newRangeStart > newRangeEnd) [newRangeStart, newRangeEnd] = [newRangeEnd, newRangeStart];

        // first time it's called, repaint months in old selectedRange and next selectingRange
        return (oldRangeStart <= month && month <= oldRangeEnd) || (newRangeStart <= month && month <= newRangeEnd);
      } else if (nextProps.selectingRange === undefined) {
        // last time it's called, repaint months in previous selectingRange
        let oldRangeStart = selectingRangeStart;
        let oldRangeEnd = selectingRangeEnd;
        if (oldRangeStart > oldRangeEnd) [oldRangeStart, oldRangeEnd] = [oldRangeEnd, oldRangeStart];

        let newRangeStart = nextProps.selectedRange[0].month();
        let newRangeEnd = nextProps.selectedRange[1].month();
        if (newRangeStart > newRangeEnd) [newRangeStart, newRangeEnd] = [newRangeEnd, newRangeStart];

        // called on day hovering changed
        return (oldRangeStart <= month && month <= oldRangeEnd) || (newRangeStart <= month && month <= newRangeEnd);
      }
      // called on day hovering changed
      let oldRangeStart = selectingRangeStart;
      let oldRangeEnd = selectingRangeEnd;
      if (oldRangeStart > oldRangeEnd) [oldRangeStart, oldRangeEnd] = [oldRangeEnd, oldRangeStart];

      let newRangeStart = nextProps.selectingRange[0].month();
      let newRangeEnd = nextProps.selectingRange[1].month();
      if (newRangeStart > newRangeEnd) [newRangeStart, newRangeEnd] = [newRangeEnd, newRangeStart];

      return (oldRangeStart <= month && month <= oldRangeEnd) || (newRangeStart <= month && month <= newRangeEnd);
    } else if (this.props.selectedDay.month() === month || nextProps.selectedDay.month() === month) {
      // single selectedDay changed: repaint months where selectedDay was and where will be
      return true;
    }

    return false;
  }

  dayClicked(day, classes) {
    const { dayClicked } = this.props;
    dayClicked(day, classes);
  }

  dayHovered(day) {
    const { selectRange, dayHovered } = this.props;
    if (selectRange) {
      dayHovered(day);
    }
  }

  renderMonthDays() {
    const {
      year,
      month,
      forceFullWeeks,
      showWeekSeparators,
      selectedDay,
      firstDayOfWeek,
      selectingRange,
      selectRange,
      selectedRange,
      customClasses
    } = this.props;
    const monthStart = moment([year, month, 1]); // current day

    // number of days to insert before the first of the month to correctly align the weekdays
    let prevMonthDaysCount = monthStart.weekday();
    while (prevMonthDaysCount < firstDayOfWeek) {
      prevMonthDaysCount += 7;
    }
    // days in month
    const numberOfDays = monthStart.daysInMonth();
    // insert days at the end to match up 37 (max number of days in a month + 6)
    // or 42 (if user prefers seeing the week closing with Sunday)
    const totalDays = forceFullWeeks ? 42 : 37;

    // day-generating loop
    const days = [];
    range(firstDayOfWeek + 1, totalDays + firstDayOfWeek + 1).forEach(i => {
      const day = moment([year, month, i - prevMonthDaysCount]);

      // pick appropriate classes
      const classes = [];
      if (i <= prevMonthDaysCount) {
        classes.push('prev-month');
      } else if (i > numberOfDays + prevMonthDaysCount) {
        classes.push('next-month');
      } else {
        if (selectRange) {
          // selectingRange is used while user is selecting a range
          // (has clicked on start day, and is hovering end day - but not yet clicked)
          let start = (selectingRange || selectedRange)[0];
          let end = (selectingRange || selectedRange)[1];

          // validate range
          if (end.isBefore(start)) {
            [end, start] = selectingRange || selectedRange;
          }

          if (day.isBetween(start, end, 'day', '[]')) {
            classes.push('range');
          }

          if (day.isSame(start, 'day')) {
            classes.push('range-left');
          }

          if (day.isSame(end, 'day')) {
            classes.push('range-right');
          }
        } else if (day.isSame(selectedDay, 'day')) {
          classes.push('selected');
        }

        // call here customClasses function to avoid giving improper classes to prev/next month
        if (customClasses instanceof Function) {
          classes.push(customClasses(day));
        }
      }

      if ((i - 1) % 7 === 0) {
        // sunday
        classes.push('bolder');
      }

      if (customClasses) {
        Object.keys(customClasses).forEach(k => {
          const obj = customClasses[k];
          // Order here is important! Everything is instance of Object in js
          if (typeof obj === 'string') {
            if (obj.indexOf(day.format('ddd')) > -1) {
              classes.push(k);
            }
          } else if (obj instanceof Array) {
            obj.forEach(d => {
              if (day.format('YYYY-MM-DD') === d) classes.push(k);
            });
          } else if (obj instanceof Function) {
            if (obj(day)) {
              classes.push(k);
            }
          } else if (obj.start && obj.end) {
            const startDate = moment(obj.start, 'YYYY-MM-DD').add(-1, 'days');
            const endDate = moment(obj.end, 'YYYY-MM-DD').add(1, 'days');
            if (day.isBetween(startDate, endDate)) {
              classes.push(k);
            }
          }
        });
      }

      if (showWeekSeparators) {
        if ((i - 1) % 7 === firstDayOfWeek && days.length) {
          // push week separator
          days.push(<td className="week-separator" key={`seperator-${i}`} />);
        }
      }
      days.push(
        <Day
          key={`day-${i}`}
          day={day}
          classes={classes.join(' ')}
          dayClicked={d => this.dayClicked(d, classes.join(' '))}
          dayHovered={d => this.dayHovered(d)}
        />
      );
    });

    return days;
  }

  render() {
    const { month, year } = this.props;

    return (

        {moment([year, month, 1]).format('MMM')}
        {this.renderMonthDays()}

    );
  }
}

Month.propTypes = propTypes;
Month.defaultProps = defaultProps;

export default Month;
<commit_msg>Fixed #21: Uncaught TypeError: Cannot read property '0' of undefined<commit_after>import React, { Component } from 'react';
import PropTypes from 'prop-types';
import moment from 'moment';
import { momentObj } from 'react-moment-proptypes';
import Day from './Day';
import { range } from './utils';

const propTypes = {
  year: PropTypes.number.isRequired,
  month: PropTypes.number.isRequired,
  forceFullWeeks: PropTypes.bool.isRequired,
  showWeekSeparators: PropTypes.bool.isRequired,
  selectedDay: momentObj.isRequired,
  firstDayOfWeek: PropTypes.number.isRequired,
  selectingRange: PropTypes.arrayOf(momentObj),
  selectRange: PropTypes.bool.isRequired,
  selectedRange: PropTypes.arrayOf(momentObj),
  customClasses: PropTypes.oneOfType([PropTypes.object, PropTypes.func]),
  dayClicked: PropTypes.func.isRequired,
  dayHovered: PropTypes.func.isRequired
};

const defaultProps = {
  selectingRange: undefined,
  selectedRange: undefined,
  customClasses: undefined
};

class Month extends Component {
  constructor(props) {
    super(props);

    this.state = {};
  }

  componentWillReceiveProps(nextProps) {
    if (nextProps.selectingRange !== undefined) {
      this.setState({
        selectingRangeStart: nextProps.selectingRange[0].month(),
        selectingRangeEnd: nextProps.selectingRange[1].month()
      });
    }
  }

  shouldComponentUpdate(nextProps) {
    const { month, selectingRange, selectedRange } = this.props;
    const { selectingRangeStart, selectingRangeEnd } = this.state;

    // full repaint for some global-affecting rendering props
    if (
      this.props.year !== nextProps.year ||
      this.props.forceFullWeeks !== nextProps.forceFullWeeks ||
      this.props.showWeekSeparators !== nextProps.showWeekSeparators ||
      this.props.firstDayOfWeek !== nextProps.firstDayOfWeek ||
      this.props.selectRange !== nextProps.selectRange ||
      this.props.customClasses !== nextProps.customClasses ||
      (this.props.selectRange && selectingRange === undefined && nextProps.selectingRange === undefined)
    ) {
      return true;
    }

    // if we get to this point and we are in 'selectRange' mode then it's likely that we have a change in selectingRange
    if (this.props.selectRange) {
      if (selectingRange === undefined) {
        let oldRangeStart = selectedRange[0].month();
        let oldRangeEnd = selectedRange[1].month();
        if (oldRangeStart > oldRangeEnd) {
          [oldRangeStart, oldRangeEnd] = [oldRangeEnd, oldRangeStart];
        }

        let newRangeStart = nextProps.selectingRange[0].month();
        let newRangeEnd = nextProps.selectingRange[1].month();
        if (newRangeStart > newRangeEnd) {
          [newRangeStart, newRangeEnd] = [newRangeEnd, newRangeStart];
        }

        // first time it's called, repaint months in old selectedRange and next selectingRange
        return (oldRangeStart <= month && month <= oldRangeEnd) || (newRangeStart <= month && month <= newRangeEnd);
      } else if (nextProps.selectingRange === undefined) {
        // last time it's called, repaint months in previous selectingRange
        let oldRangeStart = selectingRangeStart;
        let oldRangeEnd = selectingRangeEnd;
        if (oldRangeStart > oldRangeEnd) {
          [oldRangeStart, oldRangeEnd] = [oldRangeEnd, oldRangeStart];
        }

        let newRangeStart = nextProps.selectedRange[0].month();
        let newRangeEnd = nextProps.selectedRange[1].month();
        if (newRangeStart > newRangeEnd) {
          [newRangeStart, newRangeEnd] = [newRangeEnd, newRangeStart];
        }

        // called on day hovering changed
        return (oldRangeStart <= month && month <= oldRangeEnd) || (newRangeStart <= month && month <= newRangeEnd);
      }
      // called on day hovering changed
      let oldRangeStart = selectingRangeStart;
      let oldRangeEnd = selectingRangeEnd;
      if (oldRangeStart > oldRangeEnd) [oldRangeStart, oldRangeEnd] = [oldRangeEnd, oldRangeStart];

      let newRangeStart = nextProps.selectingRange[0].month();
      let newRangeEnd = nextProps.selectingRange[1].month();
      if (newRangeStart > newRangeEnd) {
        [newRangeStart, newRangeEnd] = [newRangeEnd, newRangeStart];
      }

      return (oldRangeStart <= month && month <= oldRangeEnd) || (newRangeStart <= month && month <= newRangeEnd);
    } else if (this.props.selectedDay.month() === month || nextProps.selectedDay.month() === month) {
      // single selectedDay changed: repaint months where selectedDay was and where will be
      return true;
    }

    return false;
  }

  dayClicked(day, classes) {
    const { dayClicked } = this.props;
    dayClicked(day, classes);
  }

  dayHovered(day) {
    const { selectRange, dayHovered } = this.props;
    if (selectRange) {
      dayHovered(day);
    }
  }

  renderMonthDays() {
    const {
      year,
      month,
      forceFullWeeks,
      showWeekSeparators,
      selectedDay,
      firstDayOfWeek,
      selectingRange,
      selectRange,
      selectedRange,
      customClasses
    } = this.props;
    const monthStart = moment([year, month, 1]); // current day

    // number of days to insert before the first of the month to correctly align the weekdays
    let prevMonthDaysCount = monthStart.weekday();
    while (prevMonthDaysCount < firstDayOfWeek) {
      prevMonthDaysCount += 7;
    }
    // days in month
    const numberOfDays = monthStart.daysInMonth();
    // insert days at the end to match up 37 (max number of days in a month + 6)
    // or 42 (if user prefers seeing the week closing with Sunday)
    const totalDays = forceFullWeeks ? 42 : 37;

    // day-generating loop
    const days = [];
    range(firstDayOfWeek + 1, totalDays + firstDayOfWeek + 1).forEach(i => {
      const day = moment([year, month, i - prevMonthDaysCount]);

      // pick appropriate classes
      const classes = [];
      if (i <= prevMonthDaysCount) {
        classes.push('prev-month');
      } else if (i > numberOfDays + prevMonthDaysCount) {
        classes.push('next-month');
      } else {
        if (selectRange) {
          // selectingRange is used while user is selecting a range
          // (has clicked on start day, and is hovering end day - but not yet clicked)
          let start = (selectingRange || selectedRange)[0];
          let end = (selectingRange || selectedRange)[1];

          // validate range
          if (end.isBefore(start)) {
            [end, start] = selectingRange || selectedRange;
          }

          if (day.isBetween(start, end, 'day', '[]')) {
            classes.push('range');
          }

          if (day.isSame(start, 'day')) {
            classes.push('range-left');
          }

          if (day.isSame(end, 'day')) {
            classes.push('range-right');
          }
        } else if (day.isSame(selectedDay, 'day')) {
          classes.push('selected');
        }

        // call here customClasses function to avoid giving improper classes to prev/next month
        if (customClasses instanceof Function) {
          classes.push(customClasses(day));
        }
      }

      if ((i - 1) % 7 === 0) {
        // sunday
        classes.push('bolder');
      }

      if (customClasses) {
        Object.keys(customClasses).forEach(k => {
          const obj = customClasses[k];
          // Order here is important! Everything is instance of Object in js
          if (typeof obj === 'string') {
            if (obj.indexOf(day.format('ddd')) > -1) {
              classes.push(k);
            }
          } else if (obj instanceof Array) {
            obj.forEach(d => {
              if (day.format('YYYY-MM-DD') === d) classes.push(k);
            });
          } else if (obj instanceof Function) {
            if (obj(day)) {
              classes.push(k);
            }
          } else if (obj.start && obj.end) {
            const startDate = moment(obj.start, 'YYYY-MM-DD').add(-1, 'days');
            const endDate = moment(obj.end, 'YYYY-MM-DD').add(1, 'days');
            if (day.isBetween(startDate, endDate)) {
              classes.push(k);
            }
          }
        });
      }

      if (showWeekSeparators) {
        if ((i - 1) % 7 === firstDayOfWeek && days.length) {
          // push week separator
          days.push(<td className="week-separator" key={`seperator-${i}`} />);
        }
      }
      days.push(
        <Day
          key={`day-${i}`}
          day={day}
          classes={classes.join(' ')}
          dayClicked={d => this.dayClicked(d, classes.join(' '))}
          dayHovered={d => this.dayHovered(d)}
        />
      );
    });

    return days;
  }

  render() {
    const { month, year } = this.props;

    return (

        {moment([year, month, 1]).format('MMM')}
        {this.renderMonthDays()}

    );
  }
}

Month.propTypes = propTypes;
Month.defaultProps = defaultProps;

export default Month;
<|endoftext|>