<commit_before>var gm = require('gm');
var fs = require('fs');
var exec = require('child_process').exec;

function writeToFile(path, rawImageBuffer, done) {
  var imageBuffer = new Buffer(rawImageBuffer, 'base64');
  fs.writeFile(path, imageBuffer, done);
}

function compareAndSaveDiffOnMismatch(image1Buffer, image2Path, taskPath, done) {
  // in our use case, iamge1Buffer will always be a buffer of the temp image we
  // created
  var tempFileName = 'temp' + Math.random() + '.png';
  writeToFile(tempFileName, image1Buffer, function(err) {
    try {
      // pixel perfect, leaves no room for error (the 0)
      gm.compare(tempFileName, image2Path, 0, function(err, areSame) {
        // TODO: don't throw
        if (!areSame) {
          // waiting for gm to accept the pull on diff saving. Meanwhile...
          var diffPngPath = taskPath + '/diff.png';
          exec(
            'gm compare -file ' + diffPngPath + ' ' + tempFileName + ' ' + image2Path,
            function(err) {
              fs.unlink(tempFileName, function() {
                done(err, areSame);
              });
            }
          );
        } else {
          fs.unlink(tempFileName, function() {
            done(null, areSame);
          });
        }
      });
    } catch (e) {
      fs.unlinkSync(tempFileName);
      throw e;
    }
  });
}

module.exports = {
  writeToFile: writeToFile,
  compareAndSaveDiffOnMismatch: compareAndSaveDiffOnMismatch,
};
<commit_msg>remove gm dependency<commit_after>var fs = require('fs');
var exec = require('child_process').exec;

function writeToFile(path, rawImageBuffer, done) {
  var imageBuffer = new Buffer(rawImageBuffer, 'base64');
  fs.writeFile(path, imageBuffer, done);
}

function compareAndSaveDiffOnMismatch(image1Buffer, image2Path, taskPath, done) {
  // in our use case, iamge1Buffer will always be a buffer of the temp image we
  // created
  var tempFileName = 'temp' + Math.random() + '.png';
  writeToFile(tempFileName, image1Buffer, function(err) {
    try {
      _checkIfDifferent(tempFileName, image2Path, function(err, areSame) {
        if (!areSame) {
          var diffPath = taskPath + '/diff.png';
          _saveDiffImage(tempFileName, image2Path, diffPath, function(err) {
            fs.unlink(tempFileName, function(err2) {
              done(err || err2, areSame);
            });
          });
        } else {
          fs.unlink(tempFileName, function() {
            done(null, areSame);
          });
        }
      });
    } catch (e) {
      fs.unlinkSync(tempFileName);
      throw e;
    }
  });
}

function _checkIfDifferent(image1Path, image2Path, done) {
  exec('gm compare -metric mse ' + image1Path + ' ' + image2Path, function (err, stdout) {
    // TODO: throw?
    if (err) return done(err);

    // the output is an ascii table, with the last row like this:
    // Total: 0.0000607584        0.0
    //           ^ what we want
    var match = /Total: (\d+\.?\d*)/m.exec(stdout);
    if (!match) {
      // TODO: throw?
      err = new Error('Unable to parse output.\nGot ' + stdout);
      return done(err);
    }

    var equality = parseFloat(match[1]);
    done(err, equality === 0);
  });
}

function _saveDiffImage(image1Path, image2Path, diffPath, done) {
  exec(
    'gm compare -file ' + diffPath + ' ' + image1Path + ' ' + image2Path, done
  );
}

module.exports = {
  writeToFile: writeToFile,
  compareAndSaveDiffOnMismatch: compareAndSaveDiffOnMismatch,
};
<|endoftext|>


<commit_before>---
layout: null
---
$(document).ready(function () {
  $('a.blog-button').click(function (e) {
    if ($('.panel-cover').hasClass('panel-cover--collapsed')) return
    currentWidth = $('.panel-cover').width()
    if (currentWidth < 960) {
      $('.panel-cover').addClass('panel-cover--collapsed')
      $('.content-wrapper').addClass('animated slideInRight')
    } else {
      $('.panel-cover').css('max-width', currentWidth)
      $('.panel-cover').animate({'max-width': '530px', 'width': '40%'}, 400, swing = 'swing', function () {})
    }
  })

  if (window.location.hash && window.location.hash == '#blog') {
    $('.panel-cover').addClass('panel-cover--collapsed')
  }

  if (window.location.pathname !== '{{ site.baseurl }}' && window.location.pathname !== '{{ site.baseurl }}/index.html') {
    $('.panel-cover').addClass('panel-cover--collapsed')
  }

  $('.btn-mobile-menu').click(function () {
    $('.navigation-wrapper').toggleClass('visible animated bounceInDown')
    $('.btn-mobile-menu__icon').toggleClass('icon-list icon-x-circle animated fadeIn')
  })

  $('.navigation-wrapper .blog-button').click(function () {
    $('.navigation-wrapper').toggleClass('visible')
    $('.btn-mobile-menu__icon').toggleClass('icon-list icon-x-circle animated fadeIn')
  })

})
<commit_msg>Remove stuff from main.js<commit_after>$(document).ready(function () {
  $('a.blog-button').click(function (e) {
    if ($('.panel-cover').hasClass('panel-cover--collapsed')) return
    currentWidth = $('.panel-cover').width()
    if (currentWidth < 960) {
      $('.panel-cover').addClass('panel-cover--collapsed')
      $('.content-wrapper').addClass('animated slideInRight')
    } else {
      $('.panel-cover').css('max-width', currentWidth)
      $('.panel-cover').animate({'max-width': '530px', 'width': '40%'}, 400, swing = 'swing', function () {})
    }
  })

  if (window.location.hash && window.location.hash == '#blog') {
    $('.panel-cover').addClass('panel-cover--collapsed')
  }

  if (window.location.pathname !== '{{ site.baseurl }}' && window.location.pathname !== '{{ site.baseurl }}/index.html') {
    $('.panel-cover').addClass('panel-cover--collapsed')
  }

  $('.btn-mobile-menu').click(function () {
    $('.navigation-wrapper').toggleClass('visible animated bounceInDown')
    $('.btn-mobile-menu__icon').toggleClass('icon-list icon-x-circle animated fadeIn')
  })

  $('.navigation-wrapper .blog-button').click(function () {
    $('.navigation-wrapper').toggleClass('visible')
    $('.btn-mobile-menu__icon').toggleClass('icon-list icon-x-circle animated fadeIn')
  })

})
<|endoftext|>


<commit_before>import React, { Component } from 'react'
import PropTypes from 'prop-types'
import axios from 'axios'
import createBrowserHistory from 'history/createBrowserHistory'
import createMemoryHistory from 'history/createMemoryHistory'
import createHashHistory from 'history/createHashHistory'
import { Helmet } from 'react-helmet'
import { Router as ReactRouter, StaticRouter } from 'react-router-dom'
//
import { pathJoin } from './shared'

// Proxy React Router
export { Prompt, Redirect, Route, Switch, matchPath, withRouter } from 'react-router-dom'

export { Link, NavLink } from './links'

// Proxy Helmet as Head
export { Helmet as Head }

//

const propsByHash = {}
const pathProps = {}
const inflight = {}

let sitePropsPromise
let InitialLoading

let routesPromise

const getRouteInfo = async () => {
  if (typeof document !== 'undefined') {
    if (!routesPromise) {
      routesPromise = (async () => {
        let res
        if (process.env.REACT_STATIC_ENV === 'development') {
          res = await axios.get('/__react-static__/routeInfo')
        } else {
          res = await axios.get('/routeInfo.json')
        }
        return res.data
      })()
    }
    return routesPromise
  }
}

if (process.env.REACT_STATIC_ENV === 'development') {
  InitialLoading = () => (

      <svg
        style={{
          width: '50px',
          height: '50px',
        }}
      >
        <circle
          style={{
            transformOrigin: '50% 50% 0px',
            animation: 'react-static-loader 1s infinite',
            r: 20,
            stroke: 'rgba(0,0,0,0.4)',
            strokeWidth: 4,
            cx: 25,
            cy: 25,
            strokeDasharray: 10.4,
            strokeLinecap: 'round',
            fill: 'transparent',
          }}
        />
      </svg>

  )
}

function cleanPath (path) {
  // Resolve the local path
  if (!path) {
    return
  }
  // Only allow origin or absolute links
  const hasOrigin = path.startsWith(window.location.origin)
  const isAbsolute = path.startsWith('/')
  if (!hasOrigin && !isAbsolute) {
    return
  }
  let end = path.indexOf('#')
  end = end === -1 ? undefined : end
  return pathJoin(path.substring(hasOrigin ? window.location.origin.length : 0, end))
}

function isPrefetched (path) {
  path = cleanPath(path)
  if (!path) {
    return
  }

  if (pathProps[path]) {
    return pathProps[path]
  }
}

async function shouldPrefetch (path) {
  path = cleanPath(path)
  // Get route info so we can check if path has any data
  const routes = await getRouteInfo()
  // Returns true if data exists for path
  return routes[path] !== undefined
}

export async function prefetch (path) {
  path = cleanPath(path)

  if (!path) {
    return
  }

  // Defer to the cache first
  if (pathProps[path]) {
    return pathProps[path]
  }

  // Now, try and find the current route info (or lack of route)
  const routes = await getRouteInfo()

  // In development request all props in one go.
  if (process.env.REACT_STATIC_ENV === 'development') {
    const isStaticRoute = routes.indexOf(path) > -1

    // Not a static route? Bail out.
    if (!isStaticRoute) {
      return
    }

    // Then try for the embedded data first and return it
    if (window.__routeData && window.__routeData.path === path) {
      pathProps[path] = window.__routeData
      return pathProps[path]
    }
    // Reuse request for duplicate inflight requests
    try {
      if (!inflight[path]) {
        inflight[path] = axios.get(`/__react-static__/route${path}`)
      }
      const { data: initialProps } = await inflight[path]

      // Place it in the cache
      pathProps[path] = {
        initialProps,
      }
    } catch (err) {
      console.error('Error: There was an error retrieving props for this route! path:', path)
      throw err
    }
    delete inflight[path]
    return pathProps[path]
  }

  // for production, we'll need the full route
  const propsMap = routes[path]

  // Not a static route? Bail out.
  if (!propsMap) {
    return
  }

  // Request and build the props one by one
  let initialProps = {}

  // Loop over the propsMap, and request each prop
  await Promise.all(
    Object.keys(propsMap).map(async key => {
      const hash = propsMap[key]

      // Check the propsByHash first
      if (!propsByHash[hash]) {
        // Reuse request for duplicate inflight requests
        try {
          if (!inflight[hash]) {
            inflight[hash] = axios.get(`/staticData/${hash}.json`)
          }
          const { data: prop } = await inflight[hash]

          // Place it in the cache
          propsByHash[hash] = prop
        } catch (err) {
          console.error('Error: There was an error retrieving a prop for this route! hashID:', hash)
          console.error(err)
        }
        delete inflight[hash]
      }

      // If this prop was the local prop, spread it on the entire prop object
      if (key === '__local') {
        initialProps = {
          ...initialProps,
          ...propsByHash[hash],
        }
      } else {
        // Otherwise, just set it as the key
        initialProps[key] = propsByHash[hash]
      }
    }),
  )

  // Cache the entire props for the route
  pathProps[path] = {
    initialProps,
  }

  // Return the props
  return pathProps[path]
}

export function getRouteProps (Comp) {
  return class GetRouteProps extends Component {
    static contextTypes = {
      initialProps: PropTypes.object,
      router: PropTypes.object,
    }
    state = {
      loaded: false,
    }
    async componentWillMount () {
      if (process.env.REACT_STATIC_ENV === 'development') {
        const { pathname, search } = this.context.router.route.location
        const path = pathJoin(`${pathname}${search}`)
        await prefetch(path)
        if (this.unmounting) {
          return
        }
        this.setState({
          loaded: true,
        })
      }
    }
    componentWillUnmount () {
      this.unmounting = true
    }
    render () {
      const { pathname, search } = this.context.router.route.location
      const path = pathJoin(`${pathname}${search}`)

      let initialProps

      if (typeof window !== 'undefined') {
        if (window.__routeData && window.__routeData.path === path) {
          initialProps = window.__routeData.initialProps
        }
      }

      if (!initialProps && this.context.initialProps) {
        initialProps = this.context.initialProps
      } else {
        initialProps = pathProps[path] ? pathProps[path].initialProps : initialProps
      }

      if (!initialProps && this.state.loaded) {
        console.error(
          `Warning: getRouteProps could not find any props for route: ${path}. Either you are missing a getProps function for this route in your static.config.js or you are using the getRouteProps HOC when you don't need to.`,
        )
      }

      if (!initialProps) {
        if (process.env.REACT_STATIC_ENV === 'development') {
          return <InitialLoading />
        }
        return null
      }

      return <Comp {...this.props} {...initialProps} />
    }
  }
}

export function getSiteProps (Comp) {
  return class GetSiteProps extends Component {
    static contextTypes = {
      siteProps: PropTypes.object,
    }
    state = {
      siteProps: false,
    }
    async componentWillMount () {
      if (process.env.REACT_STATIC_ENV === 'development') {
        const { data: siteProps } = await (() => {
          if (sitePropsPromise) {
            return sitePropsPromise
          }
          sitePropsPromise = axios.get('/__react-static__/siteProps')
          return sitePropsPromise
        })()
        if (this.unmounting) {
          return
        }
        this.setState({
          siteProps,
        })
      }
    }
    componentWillUnmount () {
      this.unmounting = true
    }
    render () {
      let siteProps
      if (typeof window !== 'undefined') {
        if (window.__routeData) {
          siteProps = window.__routeData.siteProps
        }
      }

      if (!siteProps && this.context.siteProps) {
        siteProps = this.context.siteProps
      }

      if (!siteProps && this.state.siteProps) {
        siteProps = this.state.siteProps
      }

      if (!siteProps) {
        if (process.env.REACT_STATIC_ENV === 'development') {
          return <InitialLoading />
        }
        return null
      }

      return <Comp {...this.props} {...siteProps} />
    }
  }
}

export class Prefetch extends Component {
  static defaultProps = {
    children: null,
    path: null,
    onLoad: () => {},
  }
  async componentDidMount () {
    const { path, onLoad } = this.props

    const data = await prefetch(path)
    onLoad(data, path)
  }
  render () {
    return this.props.children
  }
}

const ioIsSupported = typeof window !== 'undefined' && 'IntersectionObserver' in window
const handleIntersection = (element, callback) => {
  const io = new window.IntersectionObserver(entries => {
    entries.forEach(entry => {
      // Edge doesn't support isIntersecting. intersectionRatio > 0 works as a fallback
      if (element === entry.target && (entry.isIntersecting || entry.intersectionRatio > 0)) {
        io.unobserve(element)
        io.disconnect()
        callback()
      }
    })
  })

  io.observe(element)
}

export class PrefetchWhenSeen extends Component {
  static defaultProps = {
    children: null,
    path: null,
    className: null,
    onLoad: () => {},
  }

  componentDidMount () {
    if (!ioIsSupported) {
      this.runPrefetch()
    }
  }

  runPrefetch = () => {
    const { path, onLoad } = this.props
    prefetch(path).then(data => {
      onLoad(data, path)
    })
  }

  handleRef = ref => {
    if (ioIsSupported && ref) {
      handleIntersection(ref, this.runPrefetch)
    }
  }

  render () {
    const { children, className } = this.props
    return (

        {children}

    )
  }
}

let loading = false
let subscribers = []
const setLoading = d => {
  loading = d
  subscribers.forEach(s => s())
}

export class Router extends Component {
  static defaultProps = {
    type: 'browser',
  }
  static subscribe = cb => {
    const ccb = () => cb(loading)
    subscribers.push(ccb)
    return () => {
      subscribers = subscribers.filter(d => d !== ccb)
    }
  }
  static contextTypes = {
    staticURL: PropTypes.string,
  }
  state = {
    error: null,
    errorInfo: null,
  }
  componentWillMount () {
    getRouteInfo()
  }
  componentDidMount () {
    if (typeof window !== 'undefined') {
      const { href, origin } = window.location
      const path = pathJoin(href.replace(origin, ''))
      if (window.__routeData && window.__routeData.path === path) {
        const initialProps = window.__routeData.initialProps
        Object.keys(initialProps).forEach(key => {
          propsByHash[window.__routeData.propsMap[key]] = initialProps[key]
        })
      }
    }
  }
  componentDidCatch (error, errorInfo) {
    // Catch errors in any child components and re-renders with an error message
    this.setState({
      error,
      errorInfo,
    })
  }
  render () {
    const { history, type, ...rest } = this.props
    const { staticURL } = this.context
    const context = staticURL ? {} : undefined

    let ResolvedRouter
    let resolvedHistory

    if (this.state.error) {
      // Fallback UI if an error occurs
      return (

Oh-no! Something's gone wrong!

            {this.state.error && this.state.error.toString()}

This error occurred here:

            {this.state.errorInfo.componentStack}

For more information, please see the console.

      )
    }

    // If statically rendering, use the static router
    if (staticURL) {
      ResolvedRouter = StaticRouter
      resolvedHistory = undefined
    } else {
      ResolvedRouter = ReactRouter
      resolvedHistory = history || global.__reactStaticRouterHistory
      if (!resolvedHistory) {
        if (type === 'memory') {
          resolvedHistory = createMemoryHistory()
        } else if (type === 'hash') {
          resolvedHistory = createHashHistory()
        } else {
          resolvedHistory = createBrowserHistory()
        }
      }
      global.__reactStaticRouterHistory = resolvedHistory
      ;['push', 'replace'].forEach(method => {
        const originalMethod = resolvedHistory[method]
        resolvedHistory[method] = async (...args) => {
          const path = typeof args[0] === 'string' ? args[0] : args[0].path + args[0].search
          if (await shouldPrefetch(path) && !isPrefetched(path)) {
            setLoading(true)
            await prefetch(path)
            setLoading(false)
          }
          originalMethod.apply(resolvedHistory, args)
        }
      })
    }

    return (
      <ResolvedRouter history={resolvedHistory} location={staticURL} context={context} {...rest} />
    )
  }
}
<commit_msg>Prefetch when route for getRouteProps component change (#255)<commit_after>import React, { Component } from 'react'
import PropTypes from 'prop-types'
import axios from 'axios'
import createBrowserHistory from 'history/createBrowserHistory'
import createMemoryHistory from 'history/createMemoryHistory'
import createHashHistory from 'history/createHashHistory'
import { Helmet } from 'react-helmet'
import { Router as ReactRouter, StaticRouter } from 'react-router-dom'
//
import { pathJoin } from './shared'

// Proxy React Router
export { Prompt, Redirect, Route, Switch, matchPath, withRouter } from 'react-router-dom'

export { Link, NavLink } from './links'

// Proxy Helmet as Head
export { Helmet as Head }

//

const propsByHash = {}
const pathProps = {}
const inflight = {}

let sitePropsPromise
let InitialLoading

let routesPromise

const getRouteInfo = async () => {
  if (typeof document !== 'undefined') {
    if (!routesPromise) {
      routesPromise = (async () => {
        let res
        if (process.env.REACT_STATIC_ENV === 'development') {
          res = await axios.get('/__react-static__/routeInfo')
        } else {
          res = await axios.get('/routeInfo.json')
        }
        return res.data
      })()
    }
    return routesPromise
  }
}

if (process.env.REACT_STATIC_ENV === 'development') {
  InitialLoading = () => (

      <svg
        style={{
          width: '50px',
          height: '50px',
        }}
      >
        <circle
          style={{
            transformOrigin: '50% 50% 0px',
            animation: 'react-static-loader 1s infinite',
            r: 20,
            stroke: 'rgba(0,0,0,0.4)',
            strokeWidth: 4,
            cx: 25,
            cy: 25,
            strokeDasharray: 10.4,
            strokeLinecap: 'round',
            fill: 'transparent',
          }}
        />
      </svg>

  )
}

function cleanPath (path) {
  // Resolve the local path
  if (!path) {
    return
  }
  // Only allow origin or absolute links
  const hasOrigin = path.startsWith(window.location.origin)
  const isAbsolute = path.startsWith('/')
  if (!hasOrigin && !isAbsolute) {
    return
  }
  let end = path.indexOf('#')
  end = end === -1 ? undefined : end
  return pathJoin(path.substring(hasOrigin ? window.location.origin.length : 0, end))
}

function isPrefetched (path) {
  path = cleanPath(path)
  if (!path) {
    return
  }

  if (pathProps[path]) {
    return pathProps[path]
  }
}

async function shouldPrefetch (path) {
  path = cleanPath(path)
  // Get route info so we can check if path has any data
  const routes = await getRouteInfo()
  // Returns true if data exists for path
  return routes[path] !== undefined
}

export async function prefetch (path) {
  path = cleanPath(path)

  if (!path) {
    return
  }

  // Defer to the cache first
  if (pathProps[path]) {
    return pathProps[path]
  }

  // Now, try and find the current route info (or lack of route)
  const routes = await getRouteInfo()

  // In development request all props in one go.
  if (process.env.REACT_STATIC_ENV === 'development') {
    const isStaticRoute = routes.indexOf(path) > -1

    // Not a static route? Bail out.
    if (!isStaticRoute) {
      return
    }

    // Then try for the embedded data first and return it
    if (window.__routeData && window.__routeData.path === path) {
      pathProps[path] = window.__routeData
      return pathProps[path]
    }
    // Reuse request for duplicate inflight requests
    try {
      if (!inflight[path]) {
        inflight[path] = axios.get(`/__react-static__/route${path}`)
      }
      const { data: initialProps } = await inflight[path]

      // Place it in the cache
      pathProps[path] = {
        initialProps,
      }
    } catch (err) {
      console.error('Error: There was an error retrieving props for this route! path:', path)
      throw err
    }
    delete inflight[path]
    return pathProps[path]
  }

  // for production, we'll need the full route
  const propsMap = routes[path]

  // Not a static route? Bail out.
  if (!propsMap) {
    return
  }

  // Request and build the props one by one
  let initialProps = {}

  // Loop over the propsMap, and request each prop
  await Promise.all(
    Object.keys(propsMap).map(async key => {
      const hash = propsMap[key]

      // Check the propsByHash first
      if (!propsByHash[hash]) {
        // Reuse request for duplicate inflight requests
        try {
          if (!inflight[hash]) {
            inflight[hash] = axios.get(`/staticData/${hash}.json`)
          }
          const { data: prop } = await inflight[hash]

          // Place it in the cache
          propsByHash[hash] = prop
        } catch (err) {
          console.error('Error: There was an error retrieving a prop for this route! hashID:', hash)
          console.error(err)
        }
        delete inflight[hash]
      }

      // If this prop was the local prop, spread it on the entire prop object
      if (key === '__local') {
        initialProps = {
          ...initialProps,
          ...propsByHash[hash],
        }
      } else {
        // Otherwise, just set it as the key
        initialProps[key] = propsByHash[hash]
      }
    }),
  )

  // Cache the entire props for the route
  pathProps[path] = {
    initialProps,
  }

  // Return the props
  return pathProps[path]
}

export function getRouteProps (Comp) {
  return class GetRouteProps extends Component {
    static contextTypes = {
      initialProps: PropTypes.object,
      router: PropTypes.object,
    }
    state = {
      loaded: false,
    }
    componentWillMount () {
      if (process.env.REACT_STATIC_ENV === 'development') {
        this.loadRouteProps()
      }
    }
    componentWillReceiveProps (nextProps) {
      if (process.env.REACT_STATIC_ENV === 'development') {
        if (this.props.match.url !== nextProps.match.url) {
          this.setState({ loaded: false }, this.loadRouteProps)
        }
      }
    }
    componentWillUnmount () {
      this.unmounting = true
    }
    loadRouteProps = async () => {
      const { pathname, search } = this.context.router.route.location
      const path = pathJoin(`${pathname}${search}`)
      await prefetch(path)
      if (this.unmounting) {
        return
      }
      this.setState({
        loaded: true,
      })
    }
    render () {
      const { pathname, search } = this.context.router.route.location
      const path = pathJoin(`${pathname}${search}`)

      let initialProps

      if (typeof window !== 'undefined') {
        if (window.__routeData && window.__routeData.path === path) {
          initialProps = window.__routeData.initialProps
        }
      }

      if (!initialProps && this.context.initialProps) {
        initialProps = this.context.initialProps
      } else {
        initialProps = pathProps[path] ? pathProps[path].initialProps : initialProps
      }

      if (!initialProps && this.state.loaded) {
        console.error(
          `Warning: getRouteProps could not find any props for route: ${path}. Either you are missing a getProps function for this route in your static.config.js or you are using the getRouteProps HOC when you don't need to.`,
        )
      }

      if (!initialProps) {
        if (process.env.REACT_STATIC_ENV === 'development') {
          return <InitialLoading />
        }
        return null
      }

      return <Comp {...this.props} {...initialProps} />
    }
  }
}

export function getSiteProps (Comp) {
  return class GetSiteProps extends Component {
    static contextTypes = {
      siteProps: PropTypes.object,
    }
    state = {
      siteProps: false,
    }
    async componentWillMount () {
      if (process.env.REACT_STATIC_ENV === 'development') {
        const { data: siteProps } = await (() => {
          if (sitePropsPromise) {
            return sitePropsPromise
          }
          sitePropsPromise = axios.get('/__react-static__/siteProps')
          return sitePropsPromise
        })()
        if (this.unmounting) {
          return
        }
        this.setState({
          siteProps,
        })
      }
    }
    componentWillUnmount () {
      this.unmounting = true
    }
    render () {
      let siteProps
      if (typeof window !== 'undefined') {
        if (window.__routeData) {
          siteProps = window.__routeData.siteProps
        }
      }

      if (!siteProps && this.context.siteProps) {
        siteProps = this.context.siteProps
      }

      if (!siteProps && this.state.siteProps) {
        siteProps = this.state.siteProps
      }

      if (!siteProps) {
        if (process.env.REACT_STATIC_ENV === 'development') {
          return <InitialLoading />
        }
        return null
      }

      return <Comp {...this.props} {...siteProps} />
    }
  }
}

export class Prefetch extends Component {
  static defaultProps = {
    children: null,
    path: null,
    onLoad: () => {},
  }
  async componentDidMount () {
    const { path, onLoad } = this.props

    const data = await prefetch(path)
    onLoad(data, path)
  }
  render () {
    return this.props.children
  }
}

const ioIsSupported = typeof window !== 'undefined' && 'IntersectionObserver' in window
const handleIntersection = (element, callback) => {
  const io = new window.IntersectionObserver(entries => {
    entries.forEach(entry => {
      // Edge doesn't support isIntersecting. intersectionRatio > 0 works as a fallback
      if (element === entry.target && (entry.isIntersecting || entry.intersectionRatio > 0)) {
        io.unobserve(element)
        io.disconnect()
        callback()
      }
    })
  })

  io.observe(element)
}

export class PrefetchWhenSeen extends Component {
  static defaultProps = {
    children: null,
    path: null,
    className: null,
    onLoad: () => {},
  }

  componentDidMount () {
    if (!ioIsSupported) {
      this.runPrefetch()
    }
  }

  runPrefetch = () => {
    const { path, onLoad } = this.props
    prefetch(path).then(data => {
      onLoad(data, path)
    })
  }

  handleRef = ref => {
    if (ioIsSupported && ref) {
      handleIntersection(ref, this.runPrefetch)
    }
  }

  render () {
    const { children, className } = this.props
    return (

        {children}

    )
  }
}

let loading = false
let subscribers = []
const setLoading = d => {
  loading = d
  subscribers.forEach(s => s())
}

export class Router extends Component {
  static defaultProps = {
    type: 'browser',
  }
  static subscribe = cb => {
    const ccb = () => cb(loading)
    subscribers.push(ccb)
    return () => {
      subscribers = subscribers.filter(d => d !== ccb)
    }
  }
  static contextTypes = {
    staticURL: PropTypes.string,
  }
  state = {
    error: null,
    errorInfo: null,
  }
  componentWillMount () {
    getRouteInfo()
  }
  componentDidMount () {
    if (typeof window !== 'undefined') {
      const { href, origin } = window.location
      const path = pathJoin(href.replace(origin, ''))
      if (window.__routeData && window.__routeData.path === path) {
        const initialProps = window.__routeData.initialProps
        Object.keys(initialProps).forEach(key => {
          propsByHash[window.__routeData.propsMap[key]] = initialProps[key]
        })
      }
    }
  }
  componentDidCatch (error, errorInfo) {
    // Catch errors in any child components and re-renders with an error message
    this.setState({
      error,
      errorInfo,
    })
  }
  render () {
    const { history, type, ...rest } = this.props
    const { staticURL } = this.context
    const context = staticURL ? {} : undefined

    let ResolvedRouter
    let resolvedHistory

    if (this.state.error) {
      // Fallback UI if an error occurs
      return (

Oh-no! Something's gone wrong!

            {this.state.error && this.state.error.toString()}

This error occurred here:

            {this.state.errorInfo.componentStack}

For more information, please see the console.

      )
    }

    // If statically rendering, use the static router
    if (staticURL) {
      ResolvedRouter = StaticRouter
      resolvedHistory = undefined
    } else {
      ResolvedRouter = ReactRouter
      resolvedHistory = history || global.__reactStaticRouterHistory
      if (!resolvedHistory) {
        if (type === 'memory') {
          resolvedHistory = createMemoryHistory()
        } else if (type === 'hash') {
          resolvedHistory = createHashHistory()
        } else {
          resolvedHistory = createBrowserHistory()
        }
      }
      global.__reactStaticRouterHistory = resolvedHistory
      ;['push', 'replace'].forEach(method => {
        const originalMethod = resolvedHistory[method]
        resolvedHistory[method] = async (...args) => {
          const path = typeof args[0] === 'string' ? args[0] : args[0].path + args[0].search
          if (await shouldPrefetch(path) && !isPrefetched(path)) {
            setLoading(true)
            await prefetch(path)
            setLoading(false)
          }
          originalMethod.apply(resolvedHistory, args)
        }
      })
    }

    return (
      <ResolvedRouter history={resolvedHistory} location={staticURL} context={context} {...rest} />
    )
  }
}
<|endoftext|>


<commit_before>var id = null;

//Thanks, stack overflow, for providing me with a much faster version of what I was going to do.
function clamp(num, min, max) {
  return num <= min ? min : num >= max ? max : num;
}

function randEverySec(button) {
    id = window.setInterval(function() {
      var random1 = 10 * (5 - Math.floor(Math.random() * 1));
      var random2 = 10 * (5 - Math.floor(Math.random() * 10));

      //the following works, but it doesn't
      button.style.width = clamp(button.style.width + random1, 20, 100) + "px";
      button.style.height = clamp(button.style.height + random2, 20, 100) + "px";

   } , 500);
  }
function killRandChange(button) {
  window.clearInterval(id);
  button.style.width = 20 +"px";
  button.style.height = 20 +"px";
}

function toggleDiv(id) {
    var div = document.getElementById(id);
    div.style.display = div.style.display == "none" ? "block" : "none";
}
<commit_msg>Update main.js<commit_after>var id = null;

//Thanks, stack overflow, for providing me with a much faster version of what I was going to do.
function clamp(num, min, max) {
  return num <= min ? min : num >= max ? max : num;
}

function randEverySec(button) {
    id = window.setInterval(function() {
      var random1 = 10 * (5 - Math.floor(Math.random() * 1));
      var random2 = 10 * (5 - Math.floor(Math.random() * 10));

      button.style.width = clamp(button.style.width + random1, 20, 100) + "px";
      button.style.height = clamp(button.style.height + random2, 20, 100) + "px";

   } , 500);
  }
function killRandChange(button) {
  window.clearInterval(id);
  button.style.width = 20 +"px";
  button.style.height = 20 +"px";
}

function toggleDiv(id) {
    var div = document.getElementById(id);
    div.style.display = div.style.display == "none" ? "block" : "none";
}
<|endoftext|>


<commit_before>import React, {Component} from 'react';
import promisify from 'es6-promisify';

// constants
const CLOSED = 'closed';
const CONNECTING = 'connecting';
const OPEN = 'open';
const AUTHENTICATED = 'authenticated';
const PENDING = 'pending';
const UNAUTHENTICATED = 'unauthenticated';

const CONNECT_REQUEST = '@@socketCluster/CONNECT_REQUEST';
const CONNECT_SUCCESS = '@@socketCluster/CONNECT_SUCCESS';
const CONNECT_ERROR = '@@socketCluster/CONNECT_ERROR';
const AUTH_REQUEST = '@@socketCluster/AUTH_REQUEST';
const AUTH_SUCCESS = '@@socketCluster/AUTH_SUCCESS';
const AUTH_ERROR = '@@socketCluster/AUTH_ERROR';
const SUBSCRIBE_REQUEST = '@@socketCluster/SUBSCRIBE_REQUEST';
const SUBSCRIBE_SUCCESS = '@@socketCluster/SUBSCRIBE_SUCCESS';
const UNSUBSCRIBE = '@@socketCluster/UNSUBSCRIBE';
const SUBSCRIBE_ERROR = '@@socketCluster/SUBSCRIBE_ERROR';
const KICKOUT = '@@socketCluster/KICKOUT';
const DISCONNECT = '@@socketCluster/DISCONNECT';
const DEAUTHENTICATE = '@@socketCluster/DEAUTHENTICATE';

// Reducer
const initialState = {
  id: null,
  socketState: CLOSED,
  authState: PENDING,
  authToken: null,
  authError: null,
  error: null,
  pendingSubs: [],
  subs: []
};

export const socketClusterReducer = function(state = initialState, action) {
  switch (action.type) {
    case DEAUTHENTICATE:
      return {
        ...state,
        authState: UNAUTHENTICATED,
        authToken: null
      };
    case DISCONNECT:
      return initialState;
    case CONNECT_REQUEST:
      return {
        ...state,
        socketState: CONNECTING
      };
    case CONNECT_ERROR:
      return {
        ...state,
        error: action.error
      };
    case CONNECT_SUCCESS:
      return {
        ...state,
        id: action.payload.id,
        socketState: action.payload.socketState,
        authState: action.payload.authState,
        error: action.error
      };
    case AUTH_REQUEST:
      return {
        ...state,
        authState: PENDING
      };
    case AUTH_SUCCESS:
      return {
        ...state,
        authState: AUTHENTICATED,
        authToken: action.payload.authToken
      };
    case AUTH_ERROR:
      return {
        ...state,
        authState: UNAUTHENTICATED,
        authError: action.error
      };
    case SUBSCRIBE_REQUEST:
      return {
        ...state,
        pendingSubs: state.pendingSubs.concat(action.payload.channelName)
      };
    case SUBSCRIBE_SUCCESS:
      return {
        ...state,
        pendingSubs: state.pendingSubs.filter(sub => sub !== action.payload.channelName),
        subs: state.subs.concat(action.payload.channelName)
      };
    case SUBSCRIBE_ERROR:
      return {
        ...state,
        pendingSubs: state.pendingSubs.filter(sub => sub !== action.payload.channelName),
        error: action.error
      };
    case UNSUBSCRIBE:
      return {
        ...state,
        subs: state.subs.filter(sub => sub !== action.payload.channelName),
        error: action.error
      };
    case KICKOUT:
      return {
        ...state,
        error: action.error
      };
    default:
      return state;
  }
};

// keep in outer context so we can check if it exists in the maintainSocket HOC
let initialized = false;
let instances = 0;
let destructionId;
let destroyer;
let options;
let hocOptions;
let socket;
const hocOptionsDefaults = {keepAlive: 15000};

export const reduxSocket = (scOptions = {}, _hocOptions) => ComposedComponent =>
  class SocketClustered extends Component {
    static contextTypes = {
      store: React.PropTypes.object.isRequired
    };

    constructor(props, context) {
      super(props, context);
      const {AuthEngine, onDisconnect, socketCluster} = _hocOptions;
      options = AuthEngine ? {...scOptions, authEngine: new AuthEngine(context.store)} : scOptions;
      hocOptions = {...hocOptionsDefaults, ..._hocOptions};
      socket = socketCluster.connect(options);
      destroyer = () => {
        socket.disconnect();
        socketCluster.destroy(scOptions);
        if (onDisconnect) {
          onDisconnect(true, scOptions, _hocOptions, socket);
        }
        initialized = false;
      }
    }

    componentWillMount() {
      if (!initialized) {
        // apply callback here so it happens on the same tick
        const {onConnect} = hocOptions;
        if (onConnect) {
          onConnect(options, hocOptions, socket);
        }
        this.handleConnection();
        this.handleError();
        this.handleSubs();
        this.handleAuth();
        initialized = true;
      } else if (destructionId) {
        // a second instance of the HOC was used or the first is revisited
        window.clearTimeout(destructionId);
        destructionId = undefined;
      }
      instances++;
    }

    componentWillUnmount() {
      // if this is the last place the socket was used, try to destroy it
      if (--instances === 0) {
        const {keepAlive} = hocOptions;
        if (Number.isFinite(keepAlive)) {
          destructionId = window.setTimeout(destroyer, keepAlive)
        }
      }
    }

    render() {
      return <ComposedComponent {...this.props}/>;
    }

    handleSubs() {
      const {dispatch} = this.context.store;
      socket.on('subscribeStateChange', (channelName, oldState, newState) => {
        if (newState === PENDING) {
          dispatch({type: SUBSCRIBE_REQUEST, payload: {channelName}});
        }
      });
      socket.on('subscribeRequest', channelName => {
        dispatch({type: SUBSCRIBE_REQUEST, payload: {channelName}});
      });
      socket.on('subscribe', channelName => {
        dispatch({type: SUBSCRIBE_SUCCESS, payload: {channelName}});
      });
      socket.on('subscribeFail', (error, channelName) => {
        dispatch({type: SUBSCRIBE_ERROR, payload: {channelName}, error});
      });
      // only sends a messsage to error, unsub does the rest, takes in (error, channelName)
      socket.on('kickOut', error => {
        dispatch({type: KICKOUT, error});
      });
      socket.on('unsubscribe', channelName => {
        dispatch({type: UNSUBSCRIBE, payload: {channelName}});
      });
    }

    handleConnection() {
      const {dispatch} = this.context.store;
      // handle case where socket was opened before the HOC
      if (socket.state === OPEN) {
        if (!socket.id || socket.authState !== AUTHENTICATED) {
          dispatch({
            type: CONNECT_SUCCESS,
            payload: {
              id: socket.id,
              authState: socket.authState,
              socketState: socket.state
            }
          })
        }
      } else {
        dispatch({type: CONNECT_REQUEST, payload: {socketState: socket.state}});
      }

      socket.on('connect', status => {
        const {onConnect} = hocOptions;
        if (onConnect) {
          onConnect(options, hocOptions, socket);
        }
        dispatch({
          type: CONNECT_SUCCESS,
          payload: {
            id: status.id,
            authState: socket.authState,
            socketState: socket.state
          },
          error: status.authError
        });
      });
      socket.on('disconnect', () => {
        dispatch({type: DISCONNECT});
        const {onDisconnect} = hocOptions;
        if (onDisconnect) {
          // did not time out, so first param is false
          onDisconnect(false, options, hocOptions, socket);
        }
      });
      // triggers while in connecting state
      socket.on('connectAbort', () => {
        dispatch({type: DISCONNECT});
      });
    }

    handleError() {
      const {dispatch} = this.context.store;
      socket.on('error', error => {
        dispatch({type: CONNECT_ERROR, error: error.message});
      });
    }

    async handleAuth() {
      const {dispatch} = this.context.store;
      const {authTokenName} = options;
      socket.on('authenticate', authToken => {
        dispatch({type: AUTH_SUCCESS, payload: {authToken}});
      });
      socket.on('deauthenticate', () => {
        dispatch({type: DEAUTHENTICATE});
      });
      if (authTokenName && socket.authState !== AUTHENTICATED) {
        dispatch({type: AUTH_REQUEST});
        const loadToken = promisify(socket.auth.loadToken.bind(socket.auth));
        const authenticate = promisify(socket.authenticate.bind(socket));
        const authToken = await loadToken(authTokenName);
        const authStatus = await authenticate(authToken);
        if (authStatus.authError) {
          dispatch({type: AUTH_ERROR, error: authStatus.authError.message});
        }
      }
    }
  };

export const maintainSocket = ComposedComponent => {
  return class MaintainSocket extends Component {
    componentWillMount() {
      window.clearTimeout(destructionId);
      instances++;
    }

    componentWillUnmount() {
      if (--instances === 0) {
        const {keepAlive} = options;
        if (Number.isFinite(keepAlive)) {
          destructionId = window.setTimeout(destroyer, keepAlive)
        }
      }
    }

    render() {
      return <ComposedComponent {...this.props}/>;
    }
  }
};

<commit_msg>add default options if maintain is called before the reduxSocket hoc<commit_after>import React, {Component} from 'react';
import promisify from 'es6-promisify';

// constants
const CLOSED = 'closed';
const CONNECTING = 'connecting';
const OPEN = 'open';
const AUTHENTICATED = 'authenticated';
const PENDING = 'pending';
const UNAUTHENTICATED = 'unauthenticated';

const CONNECT_REQUEST = '@@socketCluster/CONNECT_REQUEST';
const CONNECT_SUCCESS = '@@socketCluster/CONNECT_SUCCESS';
const CONNECT_ERROR = '@@socketCluster/CONNECT_ERROR';
const AUTH_REQUEST = '@@socketCluster/AUTH_REQUEST';
const AUTH_SUCCESS = '@@socketCluster/AUTH_SUCCESS';
const AUTH_ERROR = '@@socketCluster/AUTH_ERROR';
const SUBSCRIBE_REQUEST = '@@socketCluster/SUBSCRIBE_REQUEST';
const SUBSCRIBE_SUCCESS = '@@socketCluster/SUBSCRIBE_SUCCESS';
const UNSUBSCRIBE = '@@socketCluster/UNSUBSCRIBE';
const SUBSCRIBE_ERROR = '@@socketCluster/SUBSCRIBE_ERROR';
const KICKOUT = '@@socketCluster/KICKOUT';
const DISCONNECT = '@@socketCluster/DISCONNECT';
const DEAUTHENTICATE = '@@socketCluster/DEAUTHENTICATE';

// Reducer
const initialState = {
  id: null,
  socketState: CLOSED,
  authState: PENDING,
  authToken: null,
  authError: null,
  error: null,
  pendingSubs: [],
  subs: []
};

export const socketClusterReducer = function(state = initialState, action) {
  switch (action.type) {
    case DEAUTHENTICATE:
      return {
        ...state,
        authState: UNAUTHENTICATED,
        authToken: null
      };
    case DISCONNECT:
      return initialState;
    case CONNECT_REQUEST:
      return {
        ...state,
        socketState: CONNECTING
      };
    case CONNECT_ERROR:
      return {
        ...state,
        error: action.error
      };
    case CONNECT_SUCCESS:
      return {
        ...state,
        id: action.payload.id,
        socketState: action.payload.socketState,
        authState: action.payload.authState,
        error: action.error
      };
    case AUTH_REQUEST:
      return {
        ...state,
        authState: PENDING
      };
    case AUTH_SUCCESS:
      return {
        ...state,
        authState: AUTHENTICATED,
        authToken: action.payload.authToken
      };
    case AUTH_ERROR:
      return {
        ...state,
        authState: UNAUTHENTICATED,
        authError: action.error
      };
    case SUBSCRIBE_REQUEST:
      return {
        ...state,
        pendingSubs: state.pendingSubs.concat(action.payload.channelName)
      };
    case SUBSCRIBE_SUCCESS:
      return {
        ...state,
        pendingSubs: state.pendingSubs.filter(sub => sub !== action.payload.channelName),
        subs: state.subs.concat(action.payload.channelName)
      };
    case SUBSCRIBE_ERROR:
      return {
        ...state,
        pendingSubs: state.pendingSubs.filter(sub => sub !== action.payload.channelName),
        error: action.error
      };
    case UNSUBSCRIBE:
      return {
        ...state,
        subs: state.subs.filter(sub => sub !== action.payload.channelName),
        error: action.error
      };
    case KICKOUT:
      return {
        ...state,
        error: action.error
      };
    default:
      return state;
  }
};

// keep in outer context so we can check if it exists in the maintainSocket HOC
let initialized = false;
let instances = 0;
let destructionId;
let destroyer;
let options = {};
let hocOptions = {};
let socket;
const hocOptionsDefaults = {keepAlive: 15000};

export const reduxSocket = (scOptions = {}, _hocOptions = {}) => ComposedComponent =>
  class SocketClustered extends Component {
    static contextTypes = {
      store: React.PropTypes.object.isRequired
    };

    constructor(props, context) {
      super(props, context);
      const {AuthEngine, onDisconnect, socketCluster} = _hocOptions;
      options = AuthEngine ? {...scOptions, authEngine: new AuthEngine(context.store)} : scOptions;
      hocOptions = {...hocOptionsDefaults, ..._hocOptions};
      socket = socketCluster.connect(options);
      destroyer = () => {
        socket.disconnect();
        socketCluster.destroy(scOptions);
        if (onDisconnect) {
          onDisconnect(true, scOptions, _hocOptions, socket);
        }
        initialized = false;
      }
    }

    componentWillMount() {
      if (!initialized) {
        // apply callback here so it happens on the same tick
        const {onConnect} = hocOptions;
        if (onConnect) {
          onConnect(options, hocOptions, socket);
        }
        this.handleConnection();
        this.handleError();
        this.handleSubs();
        this.handleAuth();
        initialized = true;
      } else if (destructionId) {
        // a second instance of the HOC was used or the first is revisited
        window.clearTimeout(destructionId);
        destructionId = undefined;
      }
      instances++;
    }

    componentWillUnmount() {
      // if this is the last place the socket was used, try to destroy it
      if (--instances === 0) {
        const {keepAlive} = hocOptions;
        if (Number.isFinite(keepAlive)) {
          destructionId = window.setTimeout(destroyer, keepAlive)
        }
      }
    }

    render() {
      return <ComposedComponent {...this.props}/>;
    }

    handleSubs() {
      const {dispatch} = this.context.store;
      socket.on('subscribeStateChange', (channelName, oldState, newState) => {
        if (newState === PENDING) {
          dispatch({type: SUBSCRIBE_REQUEST, payload: {channelName}});
        }
      });
      socket.on('subscribeRequest', channelName => {
        dispatch({type: SUBSCRIBE_REQUEST, payload: {channelName}});
      });
      socket.on('subscribe', channelName => {
        dispatch({type: SUBSCRIBE_SUCCESS, payload: {channelName}});
      });
      socket.on('subscribeFail', (error, channelName) => {
        dispatch({type: SUBSCRIBE_ERROR, payload: {channelName}, error});
      });
      // only sends a messsage to error, unsub does the rest, takes in (error, channelName)
      socket.on('kickOut', error => {
        dispatch({type: KICKOUT, error});
      });
      socket.on('unsubscribe', channelName => {
        dispatch({type: UNSUBSCRIBE, payload: {channelName}});
      });
    }

    handleConnection() {
      const {dispatch} = this.context.store;
      // handle case where socket was opened before the HOC
      if (socket.state === OPEN) {
        if (!socket.id || socket.authState !== AUTHENTICATED) {
          dispatch({
            type: CONNECT_SUCCESS,
            payload: {
              id: socket.id,
              authState: socket.authState,
              socketState: socket.state
            }
          })
        }
      } else {
        dispatch({type: CONNECT_REQUEST, payload: {socketState: socket.state}});
      }

      socket.on('connect', status => {
        const {onConnect} = hocOptions;
        if (onConnect) {
          onConnect(options, hocOptions, socket);
        }
        dispatch({
          type: CONNECT_SUCCESS,
          payload: {
            id: status.id,
            authState: socket.authState,
            socketState: socket.state
          },
          error: status.authError
        });
      });
      socket.on('disconnect', () => {
        dispatch({type: DISCONNECT});
        const {onDisconnect} = hocOptions;
        if (onDisconnect) {
          // did not time out, so first param is false
          onDisconnect(false, options, hocOptions, socket);
        }
      });
      // triggers while in connecting state
      socket.on('connectAbort', () => {
        dispatch({type: DISCONNECT});
      });
    }

    handleError() {
      const {dispatch} = this.context.store;
      socket.on('error', error => {
        dispatch({type: CONNECT_ERROR, error: error.message});
      });
    }

    async handleAuth() {
      const {dispatch} = this.context.store;
      const {authTokenName} = options;
      socket.on('authenticate', authToken => {
        dispatch({type: AUTH_SUCCESS, payload: {authToken}});
      });
      socket.on('deauthenticate', () => {
        dispatch({type: DEAUTHENTICATE});
      });
      if (authTokenName && socket.authState !== AUTHENTICATED) {
        dispatch({type: AUTH_REQUEST});
        const loadToken = promisify(socket.auth.loadToken.bind(socket.auth));
        const authenticate = promisify(socket.authenticate.bind(socket));
        const authToken = await loadToken(authTokenName);
        const authStatus = await authenticate(authToken);
        if (authStatus.authError) {
          dispatch({type: AUTH_ERROR, error: authStatus.authError.message});
        }
      }
    }
  };

export const maintainSocket = ComposedComponent => {
  return class MaintainSocket extends Component {
    componentWillMount() {
      window.clearTimeout(destructionId);
      instances++;
    }

    componentWillUnmount() {
      if (--instances === 0) {
        const {keepAlive} = options;
        if (Number.isFinite(keepAlive)) {
          destructionId = window.setTimeout(destroyer, keepAlive)
        }
      }
    }

    render() {
      return <ComposedComponent {...this.props}/>;
    }
  }
};

<|endoftext|>


<commit_before>(function() {

    var btn = document.getElementById('calc');

    btn.onclick = function(){

        var startHour = parseInt(document.getElementById('start-hour').value),
            startMinute = parseInt(document.getElementById('start-minute').value),
            startAmPm = document.querySelector('input[name = start-am-pm]:checked').value,
            endHour = parseInt(document.getElementById('end-hour').value),
            endMinute = parseInt(document.getElementById('end-minute').value),
            endAmPm = document.querySelector('input[name = end-am-pm]:checked').value,
            lunchTaken = document.querySelector('input[name = lunch]:checked').value,

            startToMinutes = 0,
            endToMinutes = 0,
            minutesDiff = 0,
            hourFinal = 0,
            minuteFinal = 0,
            hourOutput = "",
            minuteOutput = "";

            // Allows for AM/PM calculation
            if (startAmPm === "pm") {
                startHour += 12;
            }

            // Allows for AM/PM calculation
            if (endAmPm === "pm") {
                endHour += 12;
            }

            // Convert to total minutes
            startToMinutes = ((startHour * 60) + startMinute);
            endToMinutes = ((endHour * 60) + endMinute);

            minutesDiff = (endToMinutes - startToMinutes);

            // Removes 30 minutes if lunch was taken
            if (lunchTaken === "yes") {
                minutesDiff -= 30;
            }

            hourFinal = Math.floor(minutesDiff / 60);
            minuteFinal = (minutesDiff % 60);

            // Determines proper output of hour/hours and minute/minutes
            if (hourFinal === 1) {
                hourOutput = "hour";
            } else {
                hourOutput = "hours";
            }

            if (minuteFinal === 1) {
                minuteOutput = "minute";
            } else {
                minuteOutput = "minutes";
            }

            var ans = document.getElementById("answer");
            ans.innerHTML = "
Total time is " + hourFinal + " " + hourOutput +
            " and " + minuteFinal + " " + minuteOutput + "
";

        return false;
    };

    var clr = document.getElementById('clear');

    clr.onclick = function(){

        //remove empty field warnings
        window.location.reload();
    };

})();<commit_msg>Corrected PM calculation when using 12 as the hour<commit_after>(function() {

    var btn = document.getElementById('calc');

    btn.onclick = function(){

        var startHour = parseInt(document.getElementById('start-hour').value),
            startMinute = parseInt(document.getElementById('start-minute').value),
            startAmPm = document.querySelector('input[name = start-am-pm]:checked').value,
            endHour = parseInt(document.getElementById('end-hour').value),
            endMinute = parseInt(document.getElementById('end-minute').value),
            endAmPm = document.querySelector('input[name = end-am-pm]:checked').value,
            lunchTaken = document.querySelector('input[name = lunch]:checked').value,

            startToMinutes = 0,
            endToMinutes = 0,
            minutesDiff = 0,
            hourFinal = 0,
            minuteFinal = 0,
            hourOutput = "",
            minuteOutput = "";

            // Allows for AM/PM calculation
            if (startAmPm === "pm" && startHour != 12) {
                startHour += 12;
            }

            // Allows for AM/PM calculation
            if (endAmPm === "pm" && endHour != 12) {
                endHour += 12;
            }

            // Convert to total minutes
            startToMinutes = ((startHour * 60) + startMinute);
            endToMinutes = ((endHour * 60) + endMinute);

            minutesDiff = (endToMinutes - startToMinutes);

            // Removes 30 minutes if lunch was taken
            if (lunchTaken === "yes") {
                minutesDiff -= 30;
            }

            hourFinal = Math.floor(minutesDiff / 60);
            minuteFinal = (minutesDiff % 60);

            // Determines proper output of hour/hours and minute/minutes
            if (hourFinal === 1) {
                hourOutput = "hour";
            } else {
                hourOutput = "hours";
            }

            if (minuteFinal === 1) {
                minuteOutput = "minute";
            } else {
                minuteOutput = "minutes";
            }

            var ans = document.getElementById("answer");
            ans.innerHTML = "
Total time is " + hourFinal + " " + hourOutput +
            " and " + minuteFinal + " " + minuteOutput + "
";

        return false;
    };

    var clr = document.getElementById('clear');

    clr.onclick = function(){

        //remove empty field warnings
        window.location.reload();
    };

})();<|endoftext|>


<commit_before>var q = require('q');
/**
 * A tool for sending emails
 * @class EmailService
 * @param {EmailProvider} provider the email provider which this service delegates to
 * @param {StorageService} storage an instance of le-storage-service that is used to create records
 * @param {string} from the email address of the sender
 * @returns {service}
 */
var EmailService = function (provider, storage, from) {
  if (!provider) { throw new Error('Email provider required'); }
  if (!storage) { throw new Error('Storage service required'); }
  if (!from) { throw new Error('From email address required'); }
  var _provider = provider;
  var _storage = storage;
  var _from = from;
  /**
   * Sends an email with html content
   * @function sendHtml
   * @memberof EmailService
   * @instance
   * @param {string} to the email address of the recipient
   * @param {string} subject the subject line of the email
   * @param {string} html the html content
   * @returns {record}
   */
  this.sendHtml = function (to, subject, html) {
    if (!to) { return q.reject(new Error('To email required')); }
    if (!subject) { return q.reject(new Error('Subject required')); }
    if (!html) { return q.reject(new Error('HTML required')); }
    return _provider.sendHtml(_from, to, subject, html)
    .then(function () {
      var record = _storage.createRecord('Email');
      return record.update({
        to: to,
        subject: subject,
        html: html
      }).then(function () { return record });
    });
  };
  /**
   * Sends a template email
   * @function sendTemplate
   * @memberof EmailService
   * @instance
   * @param {string} to the email address of the recipient
   * @param {string} id the unique identifier of the template
   * @param {Object} data the key/value pairs to inject
   * @returns {record}
   */
  this.sendTemplate = function (to, id, data) {
    if (!to) { return q.reject(new Error('To email required')); }
    if (!id) { return q.reject(new Error('Template ID required')); }
    return _provider.sendTemplate(_from, to, id, data)
    .then(function () {
      var record = _storage.createRecord('Email');
      return record.update({
        to: to,
        template: id
      }).then(function () { return record });
    });
  };
}

module.exports = EmailService;
<commit_msg>docs(EmailService): methods return promises, not records<commit_after>var q = require('q');
/**
 * A tool for sending emails
 * @class EmailService
 * @param {EmailProvider} provider the email provider which this service delegates to
 * @param {StorageService} storage an instance of le-storage-service that is used to create records
 * @param {string} from the email address of the sender
 * @returns {service}
 */
var EmailService = function (provider, storage, from) {
  if (!provider) { throw new Error('Email provider required'); }
  if (!storage) { throw new Error('Storage service required'); }
  if (!from) { throw new Error('From email address required'); }
  var _provider = provider;
  var _storage = storage;
  var _from = from;
  /**
   * Sends an email with html content
   * @function sendHtml
   * @memberof EmailService
   * @instance
   * @param {string} to the email address of the recipient
   * @param {string} subject the subject line of the email
   * @param {string} html the html content
   * @returns {promise} resolves with the newly created email record
   */
  this.sendHtml = function (to, subject, html) {
    if (!to) { return q.reject(new Error('To email required')); }
    if (!subject) { return q.reject(new Error('Subject required')); }
    if (!html) { return q.reject(new Error('HTML required')); }
    return _provider.sendHtml(_from, to, subject, html)
    .then(function () {
      var record = _storage.createRecord('Email');
      return record.update({
        to: to,
        subject: subject,
        html: html
      }).then(function () { return record });
    });
  };
  /**
   * Sends a template email
   * @function sendTemplate
   * @memberof EmailService
   * @instance
   * @param {string} to the email address of the recipient
   * @param {string} id the unique identifier of the template
   * @param {Object} data the key/value pairs to inject
   * @returns {promise} resolves with the newly created email record
   */
  this.sendTemplate = function (to, id, data) {
    if (!to) { return q.reject(new Error('To email required')); }
    if (!id) { return q.reject(new Error('Template ID required')); }
    return _provider.sendTemplate(_from, to, id, data)
    .then(function () {
      var record = _storage.createRecord('Email');
      return record.update({
        to: to,
        template: id
      }).then(function () { return record });
    });
  };
}

module.exports = EmailService;
<|endoftext|>


<commit_before>$(document).ready(function () {
  populateTable();
});

function populateTable(){
	var metabugs = [];
	var table = $("#compattable");
	for(var i = 0; i < data.length; i++){
		metabugs.push(data[i].bug);
		var row = $("<tr>");
		table.append(row);
		row.append(createTableCell(data[i].name));
		row.append(createTableCell(data[i].url));
		row.append(createTableCell(createBugDiv(data[i].bug)));
		row.append(createTableCell(createDependsDiv(data[i].bug)));
		row.append(createTableCell(data[i].info));
	}
	getMetabugs(metabugs);

}

function createTableCell(value){
	var td = $("<td>");
	td.append(value);
	return td;
}

function createDependsDiv(value){
	var div = $("<div>");
	div.attr("id", "bug" + value + "-depends");
	return div;
}

function createBugDiv(id, name, resolved){
	var div = $("<div>");
	div.attr("id", "bug"+ id);
	var link = $("<a>");
	link.attr("href", "https://bugzilla.mozilla.org/show_bug.cgi?id="+id);
	link.attr("title", name);
	if(resolved){
		link.attr("style", "text-decoration: line-through;");
	}
	link.append(id);
	div.append(link);
	return div;
}

function getMetabugs(metabugs){
	var url = "https://api-dev.bugzilla.mozilla.org/latest/bug?include_fields=id,depends_on,status,summary&id=";
	var ids ="";
	for(var i = 0; i < metabugs.length; i++){
		if(i == metabugs.length-1){
			ids += metabugs[i];
		}
		else{
			ids += metabugs[i] + ",";
		}
	}
	$.ajax({
	  url: url + ids,
	  crossDomain:true,
	  dataType: 'json',
	  success: function(data){
	    processMetabugs(data.bugs);
	  },
	  error: function(data){
	    alert('fail.');
	  }
	});
}

function processMetabugs(bugs){
	var depends = "";
	for(var i = 0; i < bugs.length; i++){
		var id = bugs[i].id;
		var bugdiv = $("#bug"+id);
		var newbugdiv = createBugDiv(id, bugs[i].summary, isResolved(bugs[i].status));
		bugdiv.replaceWith(newbugdiv);
		var localDepends = bugs[i].depends_on;
		if(localDepends && localDepends != ""){
			if(depends != ""){
				depends += ",";
			}
			depends += localDepends;
		}
		var dependsdiv = $("#bug"+ id + "-depends");
		var dependsArray = depends.split(',');
		for(var j = 0; j < dependsArray.length; j++){
			dependsdiv.append(createBugDiv(dependsArray[j]));
			if(j+1 < dependsArray.length){
				dependsdiv.append(",");
			}
		}
	}
	getDependentBugs(depends);
}

function isResolved(status){
	if(status == "RESOLVED" || status == "VERIFIED" || status == "CLOSED"){
		return true;
	}
	return false;
}

function getDependentBugs(dependentBugs){
	var url = "https://api-dev.bugzilla.mozilla.org/latest/bug?include_fields=id,status,summary&id=";
	var ids =dependentBugs;
	$.ajax({
	  url: url + ids,
	  crossDomain:true,
	  dataType: 'json',
	  success: function(data){
	    processDependentbugs(data.bugs);
	  },
	  error: function(data){
	    alert('fail.');
	  }
	});
}

function processDependentbugs(bugs){
	for(var i = 0; i < bugs.length; i++){
		var id = bugs[i].id;
		var bugdiv = $("#bug"+id);
		var newbugdiv = createBugDiv(id, bugs[i].summary, isResolved(bugs[i].status));
		bugdiv.replaceWith(newbugdiv);
	}
}<commit_msg>Added support for bug alias.<commit_after>$(document).ready(function () {
  populateTable();
});

function populateTable(){
	var metabugs = [];
	var table = $("#compattable");
	for(var i = 0; i < data.length; i++){
		metabugs.push(data[i].bug);
		var row = $("<tr>");
		table.append(row);
		row.append(createTableCell(data[i].name));
		row.append(createTableCell(data[i].url));
		row.append(createTableCell(createBugDiv(data[i].bug)));
		row.append(createTableCell(createDependsDiv(data[i].bug)));
		row.append(createTableCell(data[i].info));
	}
	getMetabugs(metabugs);

}

function createTableCell(value){
	var td = $("<td>");
	td.append(value);
	return td;
}

function createDependsDiv(value){
	var div = $("<div>");
	div.attr("id", "bug" + value + "-depends");
	return div;
}

function createBugDiv(id, name, alias, resolved){
	var div = $("<div>");
	div.attr("id", "bug"+ id);
	var link = $("<a>");
	link.attr("href", "https://bugzilla.mozilla.org/show_bug.cgi?id="+id);
	link.attr("title", name);
	if(resolved){
		link.attr("style", "text-decoration: line-through;");
	}
	if(alias){
		link.append(alias);
	}
	else{
		link.append(id);
	}
	div.append(link);
	return div;
}

function getMetabugs(metabugs){
	var url = "https://api-dev.bugzilla.mozilla.org/latest/bug?include_fields=alias,id,depends_on,status,summary&id=";
	var ids ="";
	for(var i = 0; i < metabugs.length; i++){
		if(i == metabugs.length-1){
			ids += metabugs[i];
		}
		else{
			ids += metabugs[i] + ",";
		}
	}
	$.ajax({
	  url: url + ids,
	  crossDomain:true,
	  dataType: 'json',
	  success: function(data){
	    processMetabugs(data.bugs);
	  },
	  error: function(data){
	    alert('fail.');
	  }
	});
}

function processMetabugs(bugs){
	var depends = "";
	for(var i = 0; i < bugs.length; i++){
		var id = bugs[i].id;
		var bugdiv = $("#bug"+id);
		var newbugdiv = createBugDiv(id, bugs[i].summary, bugs[i].alias, isResolved(bugs[i].status));
		bugdiv.replaceWith(newbugdiv);
		var localDepends = bugs[i].depends_on;
		if(localDepends && localDepends != ""){
			if(depends != ""){
				depends += ",";
			}
			depends += localDepends;
		}
		var dependsdiv = $("#bug"+ id + "-depends");
		var dependsArray = depends.split(',');
		for(var j = 0; j < dependsArray.length; j++){
			dependsdiv.append(createBugDiv(dependsArray[j]));
			if(j+1 < dependsArray.length){
				dependsdiv.append(",");
			}
		}
	}
	getDependentBugs(depends);
}

function isResolved(status){
	if(status == "RESOLVED" || status == "VERIFIED" || status == "CLOSED"){
		return true;
	}
	return false;
}

function getDependentBugs(dependentBugs){
	var url = "https://api-dev.bugzilla.mozilla.org/latest/bug?include_fields=alias,id,status,summary&id=";
	var ids =dependentBugs;
	$.ajax({
	  url: url + ids,
	  crossDomain:true,
	  dataType: 'json',
	  success: function(data){
	    processDependentbugs(data.bugs);
	  },
	  error: function(data){
	    alert('fail.');
	  }
	});
}

function processDependentbugs(bugs){
	for(var i = 0; i < bugs.length; i++){
		var id = bugs[i].id;
		var bugdiv = $("#bug"+id);
		var newbugdiv = createBugDiv(id, bugs[i].summary, bugs[i].alias, isResolved(bugs[i].status));
		bugdiv.replaceWith(newbugdiv);
	}
}<|endoftext|>


<commit_before>import { Meteor } from 'meteor/meteor';
import { MinutesCollection } from './collections/minutes_private';
import { MeetingSeries } from './meetingseries';
import { Topic } from './topic';
import { ActionItem } from './actionitem';
import { emailAddressRegExpMatch, formatDateISO8601Time } from '/imports/helpers/date';
import { subElementsHelper } from '/imports/helpers/subElements';
import { _ } from 'meteor/underscore';
import './helpers/promisedMethods';
import './collections/workflow_private';

export class Minutes {
    constructor(source) {   // constructs obj from Mongo ID or Mongo document
        if (! source)
            throw new Meteor.Error('invalid-argument', 'Mongo ID or Mongo document required');

        if (typeof source === 'string') {   // we may have an ID here.
            source = Minutes.findOne(source);
        }
        if (typeof source === 'object') { // inject class methods in plain collection document
            _.extend(this, source);
        }
    }

    // ################### static methods
    static find(...args) {
        return MinutesCollection.find(...args);
    }

    static findOne(...args) {
        return MinutesCollection.findOne(...args);
    }

    static findAllIn(MinutesIDArray, limit, lastMintuesFirst = true) {
        if (!MinutesIDArray || MinutesIDArray.length === 0) {
            return [];
        }

        let sort = (lastMintuesFirst) ? -1 : 1;
        let options = {sort: {date: sort}};
        if (limit) {
            options["limit"] = limit;
        }
        return MinutesCollection.find(
            {_id: {$in: MinutesIDArray}},
            options);
    }

    static remove(id) {
        return Meteor.callPromise("workflow.removeMinute", id);
    }

    static async syncVisibility(parentSeriesID, visibleForArray) {
        return Meteor.callPromise("minutes.syncVisibility", parentSeriesID, visibleForArray);
    }

    // ################### object methods

    async update (docPart, callback) {
        console.log("Minutes.update()");
        _.extend(docPart, {_id: this._id});
        await Meteor.callPromise ("minutes.update", docPart, callback);

        // merge new doc fragment into this document
        _.extend(this, docPart);

        if (docPart.hasOwnProperty('date')) {
            return this.parentMeetingSeries().updateLastMinutesDateAsync();
        }
    }

    save (optimisticUICallback, serverCallback) {
        console.log("Minutes.save()");
        if (this.createdAt === undefined) {
            this.createdAt = new Date();
        }
        if (this._id && this._id !== "") {
            Meteor.call("minutes.update", this);
        } else {
            if (this.topics === undefined) {
                this.topics = [];
            }
            Meteor.call("workflow.addMinutes", this, optimisticUICallback, serverCallback);
        }
        this.parentMeetingSeries().updateLastMinutesDate(serverCallback);
    }

    nextMinutes() {
        return this._getNeighborMintues(1);
    }

    previousMinutes() {
        return this._getNeighborMintues(-1);
    }

    _getNeighborMintues(offset) {
        let parentSeries = this.parentMeetingSeries();
        let myPosition = parentSeries.minutes.indexOf(this._id);
        let neighborPosition = myPosition + offset;
        if (neighborPosition > -1 && neighborPosition < parentSeries.minutes.length) {
            let neighborMinutesId = parentSeries.minutes[neighborPosition];
            return new Minutes(neighborMinutesId);
        }
        return false;
    }

    toString () {
        return "Minutes: "+JSON.stringify(this, null, 4);
    }

    log () {
        console.log(this.toString());
    }

    parentMeetingSeries () {
        return new MeetingSeries(this.meetingSeries_id);
    }

    parentMeetingSeriesID () {
        return this.meetingSeries_id;
    }

    // This also does a minimal update of collection!
    async removeTopic(id) {
        let i = this._findTopicIndex(id);
        if (i !== undefined) {
            this.topics.splice(i, 1);
            return Meteor.callPromise('minutes.removeTopic', id);
        }
    }

    findTopic(id) {
        let i = this._findTopicIndex(id);
        if (i !== undefined) {
            return this.topics[i];
        }
        return undefined;
    }

    /**
     * Returns all topics which are created
     * within this meeting.
     */
    getNewTopics() {
        return this.topics.filter((topic) => {
            return topic.isNew;
        });
    }

    /**
     * Returns all old topics which were closed
     * within this topic.
     */
    getOldClosedTopics() {
        return this.topics.filter((topic) => {
            return ( !topic.isNew && !topic.isOpen && !Topic.hasOpenActionItem(topic) );
        });
    }

    /**
     * Checks whether this minute has at least one
     * open AI.
     *
     * @returns {boolean}
     */
    hasOpenActionItems() {
        for (let i = this.topics.length; i-- > 0;) {
            let topic = new Topic(this, this.topics[i]);
            if (topic.hasOpenActionItem()) {
                return true;
            }
        }
        return false;
    }

    /**
     * Returns tailored topics which
     * does only contain info items.
     */
    getTopicsWithOnlyInfoItems() {
        return this.topics.map((topicDoc) => {
            let topic = new Topic(this, topicDoc);
            topicDoc.infoItems = topic.getOnlyInfoItems();
            return topicDoc;
        })
    }

    getTopicsWithoutItems() {
        return this.topics.map((topicDoc) => {
            topicDoc.infoItems = [];
            return topicDoc;
        })
    }

    async upsertTopic(topicDoc, insertPlacementTop = true) {
        let i = undefined;

        if (! topicDoc._id) {             // brand-new topic
            topicDoc._id = Random.id();   // create our own local _id here!
        } else {
            i = this._findTopicIndex(topicDoc._id); // try to find it
        }

        if (i === undefined) {                      // topic not in array
            return Meteor.callPromise('minutes.addTopic', this._id, topicDoc, insertPlacementTop);
        } else {
            this.topics[i] = topicDoc;      // overwrite in place
            return Meteor.callPromise('minutes.updateTopic', topicDoc._id, topicDoc);
        }
    }

    /**
     *
     * @returns ActionItem[]
     */
    getOpenActionItems() {
        return this.topics.reduce((acc, topicDoc) => {
            let topic = new Topic(this, topicDoc);
            let actionItemDocs = topic.getOpenActionItems();
            return acc.concat(
                actionItemDocs.map(doc => {
                    return new ActionItem(topic, doc);
                })
            );
        }, /* initial value */[]);
    }

    /**
     * Finalizes this minute by calling
     * the workflow-server-method.
     *
     * @param sendActionItems default: true
     * @param sendInfoItems default: true
     */
    finalize(sendActionItems, sendInfoItems) {
        return Meteor.callPromise('workflow.finalizeMinute', this._id, sendActionItems, sendInfoItems);
    }

    /**
     * Unfinalizes this minutes by calling
     * the workflow-server-method.
     */
    unfinalize() {
        return Meteor.callPromise('workflow.unfinalizeMinute', this._id);
    }

    sendAgenda() {
        return Meteor.callPromise('minutes.sendAgenda', this._id);
    }

    getAgendaSentAt() {
        if (!this.agendaSentAt) {
            return false;
        }
        return this.agendaSentAt;
    }

    isCurrentUserModerator() {
        return this.parentMeetingSeries().isCurrentUserModerator();
    }

    /**
     * Gets all persons who want to be
     * informed about this minute:
     * (visibleFor + informedUsers)
     *
     * @returns {string[]} of user ids
     */
    getPersonsInformed() {
        let informed = this.visibleFor;
        if (this.informedUsers) {
            informed = informed.concat(this.informedUsers);
        }
       return informed;
    }

    /**
     * Returns all informed persons with name and
     * email address.
     * Skips all persons with no email address.
     *
     * @param userCollection
     * @returns {Array}
     */
    getPersonsInformedWithEmail(userCollection) {
        let recipientResult = this.getPersonsInformed().reduce((recipients, userId) => {
            let user = userCollection.findOne(userId);
            if (user.emails && user.emails.length > 0) {
                recipients.push({
                    userId: userId,
                    name: user.username,
                    address: user.emails[0].address
                });
            }
            return recipients;
        }, /* initial value */ []);

        // search for mail addresses in additional participants and add them to recipients
        if (this.participantsAdditional) {
            let addMails = this.participantsAdditional.match(emailAddressRegExpMatch);
            if (addMails) { // addMails is null if there is no substring matching the email regular expression
                addMails.forEach(additionalMail => {
                    recipientResult.push(
                        {
                            userId: "additionalRecipient",
                            name: additionalMail,
                            address: additionalMail
                        }
                    )
                });
            }
        }

        return recipientResult;
    }

    /**
     * Sync all users of .visibleFor into .participants
     * This method adds and removes users from the .participants list.
     * But it does not change attributes (e.g. .present) of untouched users
     * Throws an exception if this minutes are finalized
     * @param saveToDB internal saving can be skipped
     */
    async refreshParticipants (saveToDB) {
        if (this.isFinalized) {
            throw new Error("updateParticipants () must not be called on finalized minutes");
        }
        let changed = false;

        // ********************** PHASE-1
        // Add new, not-yet known entries from .visibleFor to .participants
        // construct lookup dict
        if (!this.participants) {
            this.participants = [];
        }
        let participantDict = {};
        this.participants.forEach(participant => {
            participantDict[participant.userId] = true;
        });

        this.visibleFor.forEach(userId => {
            if (!participantDict[userId]) {
                this.participants.push({
                    userId: userId,
                    present: false,
                    minuteKeeper: false
                });
                changed = true;
            }
        });

        // ********************** PHASE-2
        // Remove entries from .participants that are not in .visibleFor anymore
        // construct lookup dict
        if (!this.visibleFor) {
            this.visibleFor = [];
        }
        let visibleForDict = {};
        this.visibleFor.forEach(visUserId => {
            visibleForDict[visUserId] = true;
        });

        let newParticipants = [];
        this.participants.forEach(participant => {
            if (visibleForDict[participant.userId]) {
                newParticipants.push(participant);
            } else {
                // here a former participant is removed
                changed = true;
            }
        });

        // only save if desired and we did change something
        if (saveToDB && changed) {
            return this.update({participants: newParticipants}); // update only participants array!
        }
    }

    /**
     * Change presence of a single participant. Immediately updates .participants array
     * TODO Reactive performance may be better if we only update one array element in DB
     * @param index of the participant in the participant array
     * @param isPresent new state of presence
     */
    async updateParticipantPresent(index, isPresent) {
        this.participants[index].present = isPresent;
        return this.update({participants: this.participants});
    }

    /**
     * Returns the list of participants and adds the name of
     * each participants if a userCollection is given.
     * @param userCollection to query for the participants name.
     * @returns {Array}
     */
    getParticipants(userCollection) {
        if (userCollection) {
            return this.participants.map(participant => {
                let user = userCollection.findOne(participant.userId);
                participant.name = user.username;
                return participant;
            });
        }

        return this.participants;
    }

    /**
     * Returns the list of informed users and adds the name of
     * each informed if a userCollection is given.
     * @param userCollection to query for the participants name.
     * @returns {Array}
     */
    getInformed(userCollection) {
        if (this.informedUsers)
        {
            if (userCollection) {
                return this.informedUsers.map(informed => {
                    let user = userCollection.findOne(informed);
                    informed = {id: informed, name: user.username};
                    return informed;
                });
            } else {
                return this.informedUsers;
            }
        }

        return [];
    }

    /**
     * Returns a human readable list of present participants of the meeting
     * @param maxChars truncate and add ellipsis if necessary
     * @returns {String} with comma separated list of names
     */
    getPresentParticipantNames(maxChars) {
        let names = "";

        this.participants = this.participants || [];

        this.participants.forEach(part => {
            if (part.present) {
                let name = Meteor.users.findOne(part.userId).username;
                names = names + name + ", ";
            }
        });
        if (this.participantsAdditional) {
            names = names + this.participantsAdditional;
        } else {
            names = names .slice(0, -2);    // delete last ", "
        }
        if (maxChars && names.length > maxChars) {
            return names.substr(0, maxChars)+"...";
        }
        if (names === "") {
            names = "None.";
        }
        return names;
    }

    checkParent() {
        let parent = this.parentMeetingSeries();
        if (!parent.hasMinute(this._id)) {
            throw new Meteor.Error('runtime-error', 'Minute is an orphan!');
        }
    }

    getFinalizedString() {
        if (this.finalizedAt) {
            let finalizedTimestamp = formatDateISO8601Time(this.finalizedAt);
            let finalizedString = this.isFinalized? "Finalized" : "Unfinalized";
            let version = this.finalizedVersion ? "Version "+this.finalizedVersion+". " : "";
            return (`${version}${finalizedString} on ${finalizedTimestamp} by ${this.finalizedBy}`);
        } else {
            return "Never finalized."
        }
    }

    // ################### private methods
    _findTopicIndex(id) {
        return subElementsHelper.findIndexById(id, this.topics);
    }
}
<commit_msg>Fixed import of emailAddressRegExpMatch<commit_after>import { Meteor } from 'meteor/meteor';
import { MinutesCollection } from './collections/minutes_private';
import { MeetingSeries } from './meetingseries';
import { Topic } from './topic';
import { ActionItem } from './actionitem';
import { formatDateISO8601Time } from '/imports/helpers/date';
import { emailAddressRegExpMatch } from '/imports/helpers/email';
import { subElementsHelper } from '/imports/helpers/subElements';
import { _ } from 'meteor/underscore';
import './helpers/promisedMethods';
import './collections/workflow_private';

export class Minutes {
    constructor(source) {   // constructs obj from Mongo ID or Mongo document
        if (! source)
            throw new Meteor.Error('invalid-argument', 'Mongo ID or Mongo document required');

        if (typeof source === 'string') {   // we may have an ID here.
            source = Minutes.findOne(source);
        }
        if (typeof source === 'object') { // inject class methods in plain collection document
            _.extend(this, source);
        }
    }

    // ################### static methods
    static find(...args) {
        return MinutesCollection.find(...args);
    }

    static findOne(...args) {
        return MinutesCollection.findOne(...args);
    }

    static findAllIn(MinutesIDArray, limit, lastMintuesFirst = true) {
        if (!MinutesIDArray || MinutesIDArray.length === 0) {
            return [];
        }

        let sort = (lastMintuesFirst) ? -1 : 1;
        let options = {sort: {date: sort}};
        if (limit) {
            options["limit"] = limit;
        }
        return MinutesCollection.find(
            {_id: {$in: MinutesIDArray}},
            options);
    }

    static remove(id) {
        return Meteor.callPromise("workflow.removeMinute", id);
    }

    static async syncVisibility(parentSeriesID, visibleForArray) {
        return Meteor.callPromise("minutes.syncVisibility", parentSeriesID, visibleForArray);
    }

    // ################### object methods

    async update (docPart, callback) {
        console.log("Minutes.update()");
        _.extend(docPart, {_id: this._id});
        await Meteor.callPromise ("minutes.update", docPart, callback);

        // merge new doc fragment into this document
        _.extend(this, docPart);

        if (docPart.hasOwnProperty('date')) {
            return this.parentMeetingSeries().updateLastMinutesDateAsync();
        }
    }

    save (optimisticUICallback, serverCallback) {
        console.log("Minutes.save()");
        if (this.createdAt === undefined) {
            this.createdAt = new Date();
        }
        if (this._id && this._id !== "") {
            Meteor.call("minutes.update", this);
        } else {
            if (this.topics === undefined) {
                this.topics = [];
            }
            Meteor.call("workflow.addMinutes", this, optimisticUICallback, serverCallback);
        }
        this.parentMeetingSeries().updateLastMinutesDate(serverCallback);
    }

    nextMinutes() {
        return this._getNeighborMintues(1);
    }

    previousMinutes() {
        return this._getNeighborMintues(-1);
    }

    _getNeighborMintues(offset) {
        let parentSeries = this.parentMeetingSeries();
        let myPosition = parentSeries.minutes.indexOf(this._id);
        let neighborPosition = myPosition + offset;
        if (neighborPosition > -1 && neighborPosition < parentSeries.minutes.length) {
            let neighborMinutesId = parentSeries.minutes[neighborPosition];
            return new Minutes(neighborMinutesId);
        }
        return false;
    }

    toString () {
        return "Minutes: "+JSON.stringify(this, null, 4);
    }

    log () {
        console.log(this.toString());
    }

    parentMeetingSeries () {
        return new MeetingSeries(this.meetingSeries_id);
    }

    parentMeetingSeriesID () {
        return this.meetingSeries_id;
    }

    // This also does a minimal update of collection!
    async removeTopic(id) {
        let i = this._findTopicIndex(id);
        if (i !== undefined) {
            this.topics.splice(i, 1);
            return Meteor.callPromise('minutes.removeTopic', id);
        }
    }

    findTopic(id) {
        let i = this._findTopicIndex(id);
        if (i !== undefined) {
            return this.topics[i];
        }
        return undefined;
    }

    /**
     * Returns all topics which are created
     * within this meeting.
     */
    getNewTopics() {
        return this.topics.filter((topic) => {
            return topic.isNew;
        });
    }

    /**
     * Returns all old topics which were closed
     * within this topic.
     */
    getOldClosedTopics() {
        return this.topics.filter((topic) => {
            return ( !topic.isNew && !topic.isOpen && !Topic.hasOpenActionItem(topic) );
        });
    }

    /**
     * Checks whether this minute has at least one
     * open AI.
     *
     * @returns {boolean}
     */
    hasOpenActionItems() {
        for (let i = this.topics.length; i-- > 0;) {
            let topic = new Topic(this, this.topics[i]);
            if (topic.hasOpenActionItem()) {
                return true;
            }
        }
        return false;
    }

    /**
     * Returns tailored topics which
     * does only contain info items.
     */
    getTopicsWithOnlyInfoItems() {
        return this.topics.map((topicDoc) => {
            let topic = new Topic(this, topicDoc);
            topicDoc.infoItems = topic.getOnlyInfoItems();
            return topicDoc;
        })
    }

    getTopicsWithoutItems() {
        return this.topics.map((topicDoc) => {
            topicDoc.infoItems = [];
            return topicDoc;
        })
    }

    async upsertTopic(topicDoc, insertPlacementTop = true) {
        let i = undefined;

        if (! topicDoc._id) {             // brand-new topic
            topicDoc._id = Random.id();   // create our own local _id here!
        } else {
            i = this._findTopicIndex(topicDoc._id); // try to find it
        }

        if (i === undefined) {                      // topic not in array
            return Meteor.callPromise('minutes.addTopic', this._id, topicDoc, insertPlacementTop);
        } else {
            this.topics[i] = topicDoc;      // overwrite in place
            return Meteor.callPromise('minutes.updateTopic', topicDoc._id, topicDoc);
        }
    }

    /**
     *
     * @returns ActionItem[]
     */
    getOpenActionItems() {
        return this.topics.reduce((acc, topicDoc) => {
            let topic = new Topic(this, topicDoc);
            let actionItemDocs = topic.getOpenActionItems();
            return acc.concat(
                actionItemDocs.map(doc => {
                    return new ActionItem(topic, doc);
                })
            );
        }, /* initial value */[]);
    }

    /**
     * Finalizes this minute by calling
     * the workflow-server-method.
     *
     * @param sendActionItems default: true
     * @param sendInfoItems default: true
     */
    finalize(sendActionItems, sendInfoItems) {
        return Meteor.callPromise('workflow.finalizeMinute', this._id, sendActionItems, sendInfoItems);
    }

    /**
     * Unfinalizes this minutes by calling
     * the workflow-server-method.
     */
    unfinalize() {
        return Meteor.callPromise('workflow.unfinalizeMinute', this._id);
    }

    sendAgenda() {
        return Meteor.callPromise('minutes.sendAgenda', this._id);
    }

    getAgendaSentAt() {
        if (!this.agendaSentAt) {
            return false;
        }
        return this.agendaSentAt;
    }

    isCurrentUserModerator() {
        return this.parentMeetingSeries().isCurrentUserModerator();
    }

    /**
     * Gets all persons who want to be
     * informed about this minute:
     * (visibleFor + informedUsers)
     *
     * @returns {string[]} of user ids
     */
    getPersonsInformed() {
        let informed = this.visibleFor;
        if (this.informedUsers) {
            informed = informed.concat(this.informedUsers);
        }
       return informed;
    }

    /**
     * Returns all informed persons with name and
     * email address.
     * Skips all persons with no email address.
     *
     * @param userCollection
     * @returns {Array}
     */
    getPersonsInformedWithEmail(userCollection) {
        let recipientResult = this.getPersonsInformed().reduce((recipients, userId) => {
            let user = userCollection.findOne(userId);
            if (user.emails && user.emails.length > 0) {
                recipients.push({
                    userId: userId,
                    name: user.username,
                    address: user.emails[0].address
                });
            }
            return recipients;
        }, /* initial value */ []);

        // search for mail addresses in additional participants and add them to recipients
        if (this.participantsAdditional) {
            let addMails = this.participantsAdditional.match(emailAddressRegExpMatch);
            if (addMails) { // addMails is null if there is no substring matching the email regular expression
                addMails.forEach(additionalMail => {
                    recipientResult.push(
                        {
                            userId: "additionalRecipient",
                            name: additionalMail,
                            address: additionalMail
                        }
                    )
                });
            }
        }

        return recipientResult;
    }

    /**
     * Sync all users of .visibleFor into .participants
     * This method adds and removes users from the .participants list.
     * But it does not change attributes (e.g. .present) of untouched users
     * Throws an exception if this minutes are finalized
     * @param saveToDB internal saving can be skipped
     */
    async refreshParticipants (saveToDB) {
        if (this.isFinalized) {
            throw new Error("updateParticipants () must not be called on finalized minutes");
        }
        let changed = false;

        // ********************** PHASE-1
        // Add new, not-yet known entries from .visibleFor to .participants
        // construct lookup dict
        if (!this.participants) {
            this.participants = [];
        }
        let participantDict = {};
        this.participants.forEach(participant => {
            participantDict[participant.userId] = true;
        });

        this.visibleFor.forEach(userId => {
            if (!participantDict[userId]) {
                this.participants.push({
                    userId: userId,
                    present: false,
                    minuteKeeper: false
                });
                changed = true;
            }
        });

        // ********************** PHASE-2
        // Remove entries from .participants that are not in .visibleFor anymore
        // construct lookup dict
        if (!this.visibleFor) {
            this.visibleFor = [];
        }
        let visibleForDict = {};
        this.visibleFor.forEach(visUserId => {
            visibleForDict[visUserId] = true;
        });

        let newParticipants = [];
        this.participants.forEach(participant => {
            if (visibleForDict[participant.userId]) {
                newParticipants.push(participant);
            } else {
                // here a former participant is removed
                changed = true;
            }
        });

        // only save if desired and we did change something
        if (saveToDB && changed) {
            return this.update({participants: newParticipants}); // update only participants array!
        }
    }

    /**
     * Change presence of a single participant. Immediately updates .participants array
     * TODO Reactive performance may be better if we only update one array element in DB
     * @param index of the participant in the participant array
     * @param isPresent new state of presence
     */
    async updateParticipantPresent(index, isPresent) {
        this.participants[index].present = isPresent;
        return this.update({participants: this.participants});
    }

    /**
     * Returns the list of participants and adds the name of
     * each participants if a userCollection is given.
     * @param userCollection to query for the participants name.
     * @returns {Array}
     */
    getParticipants(userCollection) {
        if (userCollection) {
            return this.participants.map(participant => {
                let user = userCollection.findOne(participant.userId);
                participant.name = user.username;
                return participant;
            });
        }

        return this.participants;
    }

    /**
     * Returns the list of informed users and adds the name of
     * each informed if a userCollection is given.
     * @param userCollection to query for the participants name.
     * @returns {Array}
     */
    getInformed(userCollection) {
        if (this.informedUsers)
        {
            if (userCollection) {
                return this.informedUsers.map(informed => {
                    let user = userCollection.findOne(informed);
                    informed = {id: informed, name: user.username};
                    return informed;
                });
            } else {
                return this.informedUsers;
            }
        }

        return [];
    }

    /**
     * Returns a human readable list of present participants of the meeting
     * @param maxChars truncate and add ellipsis if necessary
     * @returns {String} with comma separated list of names
     */
    getPresentParticipantNames(maxChars) {
        let names = "";

        this.participants = this.participants || [];

        this.participants.forEach(part => {
            if (part.present) {
                let name = Meteor.users.findOne(part.userId).username;
                names = names + name + ", ";
            }
        });
        if (this.participantsAdditional) {
            names = names + this.participantsAdditional;
        } else {
            names = names .slice(0, -2);    // delete last ", "
        }
        if (maxChars && names.length > maxChars) {
            return names.substr(0, maxChars)+"...";
        }
        if (names === "") {
            names = "None.";
        }
        return names;
    }

    checkParent() {
        let parent = this.parentMeetingSeries();
        if (!parent.hasMinute(this._id)) {
            throw new Meteor.Error('runtime-error', 'Minute is an orphan!');
        }
    }

    getFinalizedString() {
        if (this.finalizedAt) {
            let finalizedTimestamp = formatDateISO8601Time(this.finalizedAt);
            let finalizedString = this.isFinalized? "Finalized" : "Unfinalized";
            let version = this.finalizedVersion ? "Version "+this.finalizedVersion+". " : "";
            return (`${version}${finalizedString} on ${finalizedTimestamp} by ${this.finalizedBy}`);
        } else {
            return "Never finalized."
        }
    }

    // ################### private methods
    _findTopicIndex(id) {
        return subElementsHelper.findIndexById(id, this.topics);
    }
}
<|endoftext|>