<commit_before>import {
  SELECT_PRODUCT,
  SELECT_PROMOTION_SET,
  INIT_TCP_CLIENT,
  RECEIVED_SENSOR_INFORMATION,
  RECEIVED_CASH,
  RECEIVED_CASH_COMPLETELY,
  PRODUCT_DROP_SUCCESS,
  PRODUCT_DROP_PROCESS_COMPLETELY,
  RESET_PAYMENT_REDUCER,
  CLEAR_PAYMENT_AMOUNT,
  SELECT_MOBILE_TOPUP_PROVIDER,
  CONFIRM_MOBILE_TOPUP_MSISDN,
  RECEIVED_CASH_REMAINING,
  SHOW_MODAL,
  HIDE_MODAL,
  HIDE_ALL_MODAL,
  READY_TO_DROP_PRODUCT,
  NOT_READY_TO_DROP_PRODUCT,
  CLEAR_ORDER,
  DROPPING_PRODUCT,
  SELECT_MOBILE_TOPUP_VALUE,
  SUBMIT_MOBILE_TOPUP_VALUE,
  CLEAR_MOBILE_TOPUP_VALUE,
  SET_CASH_CHANGE_AMOUNT,
  CLEAR_MOBILE_TOPUP_MSISDN,
  SELECT_EVENT,
  ADD_DISCOUNT,
  SET_CAN_CHANGE_CASH,
  RECEIVED_MASTERDATA
} from './actionTypes';

export const receivedSensorInformation = (data) => {
  return {
    type: RECEIVED_SENSOR_INFORMATION,
    data,
  };
};

export const receivedCash = (data) => {
  return {
    type: RECEIVED_CASH,
    data,
  };
};

export const receivedCashCompletely = () => {
  return {
    type: RECEIVED_CASH_COMPLETELY,
  };
};

export const productDropSuccess = (droppedProduct) => {
  return {
    type: PRODUCT_DROP_SUCCESS,
    product: droppedProduct
  };
};

export const productDropProcessCompletely = () => {
  return {
    type: PRODUCT_DROP_PROCESS_COMPLETELY,
  };
};

export const selectProduct = (item) => {
  return {
    type: SELECT_PRODUCT,
    item
  };
};

export const selectPromotionSet = (item) => {
  return {
    type: SELECT_PROMOTION_SET,
    item
  };
};

export const initTcpClient = (tcpClient) => {
  return {
    type: INIT_TCP_CLIENT,
    tcpClient
  };
};

export const resetPaymentReducer = () => {
  return {
    type: RESET_PAYMENT_REDUCER,
  };
};

export const clearPaymentAmount = () => {
  return {
    type: CLEAR_PAYMENT_AMOUNT,
  };
};

export const selectTopupProvider = (topupProvider) => {
  return {
    type: SELECT_MOBILE_TOPUP_PROVIDER,
    topupProvider
  };
};

export const confirmMobileTopupMSISDN = (MSISDN) => {
  return {
    type: CONFIRM_MOBILE_TOPUP_MSISDN,
    MSISDN,
  };
};

export const receivedCashRemaining = (data) => {
  return {
    type: RECEIVED_CASH_REMAINING,
    data,
  };
};

export const showModal = (modalName, data) => {
  return {
    type: SHOW_MODAL,
    name: modalName,
    data,
  };
};

export const hideModal = (modalName) => {
  return {
    type: HIDE_MODAL,
    name: modalName
  };
};

export const hideAllModal = () => {
  return {
    type: HIDE_ALL_MODAL
  };
};

export const readyToDropProduct = () => {
  return {
    type: READY_TO_DROP_PRODUCT
  };
};

export const notReadyToDropProduct = () => {
  return {
    type: NOT_READY_TO_DROP_PRODUCT
  };
};

export const clearOrder = () => {
  return {
    type: CLEAR_ORDER
  };
};

export const droppingProduct = (product) => {
  return {
    type: DROPPING_PRODUCT,
    product
  };
};

export const selectMobileTopupValue = (item) => {
  return {
    type: SELECT_MOBILE_TOPUP_VALUE,
    item,
  };
};

export const submitMobileTopupValue = (item) => {
  return {
    type: SUBMIT_MOBILE_TOPUP_VALUE,
    item,
  };
};

export const clearMobileTopupValue = () => {
  return {
    type: CLEAR_MOBILE_TOPUP_VALUE,
  };
};

export const setCashChangeAmount = (cashChangeAmount) => {
  return {
    type: SET_CASH_CHANGE_AMOUNT,
    cashChangeAmount
  };
};

export const clearMobileTopupMSISDN = () => {
  return {
    type: CLEAR_MOBILE_TOPUP_MSISDN,
  };
};

export const selectEvent = () => {
  return {
    type: SELECT_EVENT,
  };
};

export const addDiscount = (discount) => {
  return {
    type: ADD_DISCOUNT,
    discount,
  };
};

export const setCanChangeCash = (canChangeCash) => {
  return {
    type: SET_CAN_CHANGE_CASH,
    canChangeCash
  };
};

export const receivedMasterdata = (key, items) => {
  return {
    type: RECEIVED_MASTERDATA,
    key,
    value: items
  };
};
<commit_msg>Add Selected Event Item<commit_after>import {
  SELECT_PRODUCT,
  SELECT_PROMOTION_SET,
  INIT_TCP_CLIENT,
  RECEIVED_SENSOR_INFORMATION,
  RECEIVED_CASH,
  RECEIVED_CASH_COMPLETELY,
  PRODUCT_DROP_SUCCESS,
  PRODUCT_DROP_PROCESS_COMPLETELY,
  RESET_PAYMENT_REDUCER,
  CLEAR_PAYMENT_AMOUNT,
  SELECT_MOBILE_TOPUP_PROVIDER,
  CONFIRM_MOBILE_TOPUP_MSISDN,
  RECEIVED_CASH_REMAINING,
  SHOW_MODAL,
  HIDE_MODAL,
  HIDE_ALL_MODAL,
  READY_TO_DROP_PRODUCT,
  NOT_READY_TO_DROP_PRODUCT,
  CLEAR_ORDER,
  DROPPING_PRODUCT,
  SELECT_MOBILE_TOPUP_VALUE,
  SUBMIT_MOBILE_TOPUP_VALUE,
  CLEAR_MOBILE_TOPUP_VALUE,
  SET_CASH_CHANGE_AMOUNT,
  CLEAR_MOBILE_TOPUP_MSISDN,
  SELECT_EVENT,
  ADD_DISCOUNT,
  SET_CAN_CHANGE_CASH,
  RECEIVED_MASTERDATA
} from './actionTypes';

export const receivedSensorInformation = (data) => {
  return {
    type: RECEIVED_SENSOR_INFORMATION,
    data,
  };
};

export const receivedCash = (data) => {
  return {
    type: RECEIVED_CASH,
    data,
  };
};

export const receivedCashCompletely = () => {
  return {
    type: RECEIVED_CASH_COMPLETELY,
  };
};

export const productDropSuccess = (droppedProduct) => {
  return {
    type: PRODUCT_DROP_SUCCESS,
    product: droppedProduct
  };
};

export const productDropProcessCompletely = () => {
  return {
    type: PRODUCT_DROP_PROCESS_COMPLETELY,
  };
};

export const selectProduct = (item) => {
  return {
    type: SELECT_PRODUCT,
    item
  };
};

export const selectPromotionSet = (item) => {
  return {
    type: SELECT_PROMOTION_SET,
    item
  };
};

export const initTcpClient = (tcpClient) => {
  return {
    type: INIT_TCP_CLIENT,
    tcpClient
  };
};

export const resetPaymentReducer = () => {
  return {
    type: RESET_PAYMENT_REDUCER,
  };
};

export const clearPaymentAmount = () => {
  return {
    type: CLEAR_PAYMENT_AMOUNT,
  };
};

export const selectTopupProvider = (topupProvider) => {
  return {
    type: SELECT_MOBILE_TOPUP_PROVIDER,
    topupProvider
  };
};

export const confirmMobileTopupMSISDN = (MSISDN) => {
  return {
    type: CONFIRM_MOBILE_TOPUP_MSISDN,
    MSISDN,
  };
};

export const receivedCashRemaining = (data) => {
  return {
    type: RECEIVED_CASH_REMAINING,
    data,
  };
};

export const showModal = (modalName, data) => {
  return {
    type: SHOW_MODAL,
    name: modalName,
    data,
  };
};

export const hideModal = (modalName) => {
  return {
    type: HIDE_MODAL,
    name: modalName
  };
};

export const hideAllModal = () => {
  return {
    type: HIDE_ALL_MODAL
  };
};

export const readyToDropProduct = () => {
  return {
    type: READY_TO_DROP_PRODUCT
  };
};

export const notReadyToDropProduct = () => {
  return {
    type: NOT_READY_TO_DROP_PRODUCT
  };
};

export const clearOrder = () => {
  return {
    type: CLEAR_ORDER
  };
};

export const droppingProduct = (product) => {
  return {
    type: DROPPING_PRODUCT,
    product
  };
};

export const selectMobileTopupValue = (item) => {
  return {
    type: SELECT_MOBILE_TOPUP_VALUE,
    item,
  };
};

export const submitMobileTopupValue = (item) => {
  return {
    type: SUBMIT_MOBILE_TOPUP_VALUE,
    item,
  };
};

export const clearMobileTopupValue = () => {
  return {
    type: CLEAR_MOBILE_TOPUP_VALUE,
  };
};

export const setCashChangeAmount = (cashChangeAmount) => {
  return {
    type: SET_CASH_CHANGE_AMOUNT,
    cashChangeAmount
  };
};

export const clearMobileTopupMSISDN = () => {
  return {
    type: CLEAR_MOBILE_TOPUP_MSISDN,
  };
};

export const selectEvent = (item) => {
  return {
    type: SELECT_EVENT,
    event: item
  };
};

export const addDiscount = (discount) => {
  return {
    type: ADD_DISCOUNT,
    discount,
  };
};

export const setCanChangeCash = (canChangeCash) => {
  return {
    type: SET_CAN_CHANGE_CASH,
    canChangeCash
  };
};

export const receivedMasterdata = (key, items) => {
  return {
    type: RECEIVED_MASTERDATA,
    key,
    value: items
  };
};
<|endoftext|>


<commit_before>/**
 *  $Id$
 *
 *  Keyboard loader
 *
 *  This software is protected by patent No.2009611147 issued on 20.02.2009 by Russian Federal Service for Intellectual Property Patents and Trademarks.
 *
 *  @author Ilya Lebedev
 *  @copyright 2006-2009 Ilya Lebedev <ilya@lebedev.net>
 *  @version $Rev$
 *  @lastchange $Author$ $Date$
 */
VirtualKeyboard = new function () {
  var self = this, to = null;
  self.show = self.hide = self.toggle = self.attachInput = function () {
     window.status = 'VirtualKeyboard is not loaded yet.';
     if (!to) setTimeout(function(){window.status = ''},1000);
  }
  self.isOpen = function () {
      return false;
  }
};
(function () {
    var p = (function (sname){var sc=document.getElementsByTagName('script'),sr=new RegExp('^(.*/|)('+sname+')([#?]|$)');for (var i=0,scL=sc.length; i<scL; i++) {var m = String(sc[i].src).match(sr);if (m) {if (m[1].match(/^((https?|file)\:\/{2,}|\w:[\\])/)) return m[1];if (m[1].indexOf("/")==0) return m[1];b = document.getElementsByTagName('base');if (b[0] && b[0].href) return b[0].href+m[1];return (document.location.href.match(/(.*[\/\\])/)[0]+m[1]).replace(/^\/+/,"");}}return null;})
             ('vk_loader.js');

    var dpd = [ 'extensions/helpers.js'
               ,'extensions/dom.js'
               ,'extensions/ext/object.js'
               ,'extensions/ext/string.js'
               ,'extensions/ext/regexp.js'
               ,'extensions/ext/array.js'
               ,'extensions/eventmanager.js'
               ,'extensions/documentselection.js'
               ,'extensions/documentcookie.js'
/*
* not used by default
*
*               ,'layouts/unconverted.js'
*/
    ];

    for (var i=0,dL=dpd.length;i<dL;i++)
        dpd[i] = p+dpd[i];
    dpd[i++] = p+'virtualkeyboard.js';
    dpd[i] = p+'layouts/layouts.js';
    if (window.ScriptQueue) {
        ScriptQueue.queue(dpd);
    } else {
        if (!(window.ScriptQueueIncludes instanceof Array)) window.ScriptQueueIncludes = []
        window.ScriptQueueIncludes = window.ScriptQueueIncludes.concat(dpd);

        /*
        *  attach script loader
        */
        if (document.body) {
            s = document.createElement('script');
            s.type="text/javascript";
            s.src = p+'extensions/scriptqueue.js';
            head.appendChild(s);
        } else {
            document.write("<scr"+"ipt type=\"text/javascript\" src=\""+p+'extensions/scriptqueue.js'+"\"></scr"+"ipt>");
        }
    }
})();
<commit_msg>% fixed loading when document is ready<commit_after>/**
 *  $Id$
 *
 *  Keyboard loader
 *
 *  This software is protected by patent No.2009611147 issued on 20.02.2009 by Russian Federal Service for Intellectual Property Patents and Trademarks.
 *
 *  @author Ilya Lebedev
 *  @copyright 2006-2009 Ilya Lebedev <ilya@lebedev.net>
 *  @version $Rev$
 *  @lastchange $Author$ $Date$
 */
VirtualKeyboard = new function () {
  var self = this, to = null;
  self.show = self.hide = self.toggle = self.attachInput = function () {
     window.status = 'VirtualKeyboard is not loaded yet.';
     if (!to) setTimeout(function(){window.status = ''},1000);
  }
  self.isOpen = function () {
      return false;
  }
};
(function () {
    var p = (function (sname){var sc=document.getElementsByTagName('script'),sr=new RegExp('^(.*/|)('+sname+')([#?]|$)');for (var i=0,scL=sc.length; i<scL; i++) {var m = String(sc[i].src).match(sr);if (m) {if (m[1].match(/^((https?|file)\:\/{2,}|\w:[\\])/)) return m[1];if (m[1].indexOf("/")==0) return m[1];b = document.getElementsByTagName('base');if (b[0] && b[0].href) return b[0].href+m[1];return (document.location.href.match(/(.*[\/\\])/)[0]+m[1]).replace(/^\/+/,"");}}return null;})
             ('vk_loader.js');

    var dpd = [ 'extensions/helpers.js'
               ,'extensions/dom.js'
               ,'extensions/ext/object.js'
               ,'extensions/ext/string.js'
               ,'extensions/ext/regexp.js'
               ,'extensions/ext/array.js'
               ,'extensions/eventmanager.js'
               ,'extensions/documentselection.js'
               ,'extensions/documentcookie.js'
/*
* not used by default
*
*               ,'layouts/unconverted.js'
*/
    ];

    for (var i=0,dL=dpd.length;i<dL;i++)
        dpd[i] = p+dpd[i];
    dpd[i++] = p+'virtualkeyboard.js';
    dpd[i] = p+'layouts/layouts.js';
    if (window.ScriptQueue) {
        ScriptQueue.queue(dpd);
    } else {
        if (!(window.ScriptQueueIncludes instanceof Array)) window.ScriptQueueIncludes = []
        window.ScriptQueueIncludes = window.ScriptQueueIncludes.concat(dpd);

        /*
        *  attach script loader
        */
        if (document.body) {
            s = document.createElement('script');
            s.type="text/javascript";
            s.src = p+'extensions/scriptqueue.js';
            var head = document.getElementsByTagName("head")[0];
            head.appendChild(s);
        } else {
            document.write("<scr"+"ipt type=\"text/javascript\" src=\""+p+'extensions/scriptqueue.js'+"\"></scr"+"ipt>");
        }
    }
})();
<|endoftext|>


<commit_before>import React, { Component, Fragment } from 'react'
import {
  Box,
  Container,
  Flex,
  Heading,
  Link as A,
  Text,
  Section,
  Icon,
  Button,
  Card
} from '@hackclub/design-system'
import Helmet from 'react-helmet'
import Link from 'gatsby-link'
import Nav from 'components/Nav'
import Footer from 'components/Footer'
import MarkdownBody from 'components/MarkdownBody'
import { lowerCase, camelCase, isEmpty } from 'lodash'
import { org } from 'data.json'

const Name = Heading.h1.extend`
  background-color: white;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.32);
  clip-path: polygon(4% 0%, 100% 0%, 96% 100%, 0% 100%);
  color: black;
  display: inline-block;
  mix-blend-mode: screen;
  padding-left: ${props => props.theme.space[4]}px;
  padding-right: ${props => props.theme.space[4]}px;
  width: max-content;
`

const Desc = Heading.h2.extend`
  text-shadow: 0 1px 2px rgba(0, 0, 0, 0.32);
`

const EditLink = A.extend.attrs({ color: 'white', f: 3 })`
  display: none;

  position: absolute;
  bottom: ${props => props.theme.space[3]}px;
  right: ${props => props.theme.space[4]}px;

  ${props => props.theme.mediaQueries.md} {
    display: inline-block;
  }
`

const Body = Container.withComponent(MarkdownBody)
A.link = A.withComponent(Link)
Section.h = Section.withComponent('header')

const generateSubtitle = (description, authorText) => {
  if (!authorText) {
    return description
  }

  // This iterates over each word in authorText, finds GitHub usernames (any
  // text that looks like "@orpheus", and turns them into links.
  const parsedAuthorText = authorText.split(' ').map((word, index, arr) => {
    const matches = word.match(/@(\w+)/)
    const processedWord = matches ? (
      <A
        href={`https://github.com/${matches[1]}`}
        target="_blank"
        color="white"
        children={word}
      />
    ) : (
      word
    )

    // This pads returned results with spaces, making our final array look the
    // following:
    //   [ 'Hack', ' ', 'Club', ' ', 'staff' ]
    // if last item in array, don't give an extra space
    return index === arr.length - 1 ? processedWord : [processedWord, ' ']
  })

  return (
    <Fragment>
      {description}. Created by {parsedAuthorText}.
    </Fragment>
  )
}

const BreadcrumbList = Flex.withComponent('ol').extend`
  list-style: none;
  padding-left: 0;
`
const Breadcrumbs = props => (
  <BreadcrumbList
    itemScope
    itemType="http://schema.org/BreadcrumbList"
    {...props}
  />
)
const Breadcrumb = ({ type = 'Thing', position, name, ...props }) => (

    <A.link
      itemScope
      itemType={`http://schema.org/${type}`}
      itemProp="item"
      color="white"
      f={3}
      bold
      caps
      {...props}
    >

    <meta itemProp="position" content={position} />

)
const BreadcrumbDivider = () => (
  <Text.span mx={2} color="snow" f={3} children="›" />
)

const githubEditUrl = slug =>
  `https://github.com/hackclub/hackclub/edit/master${slug}/README.md`
const twitterURL = (text, url) =>
  `https://twitter.com/intent/tweet?text=${text
    .split(' ')
    .join('%20')}&url=${url}`
const facebookURL = (text, url) =>
  `https://www.facebook.com/sharer/sharer.php?u=${url}`

const InlineButton = Button.extend`
  display: inline-flex;
  align-items: center;
  div {
    background-image: url(/social/${props => lowerCase(props.service)}-white.svg);
    background-repeat: no-repeat;
    background-size: 100%;
    width: 18px;
    height: 18px;
  }
`
const ShareButton = props => (
  <InlineButton target="_blank" title={`Share on ${props.service}`} {...props}>
    <Box mr={2} />
    {props.service}
  </InlineButton>
)

export default ({ data }) => {
  if (isEmpty(data)) return null

  const {
    fields: { slug, bg },
    frontmatter: { name, description, author, group, order },
    html
  } = data.markdownRemark

  const subtitle = generateSubtitle(description, author)

  const title = `${name} – Hack Club Workshops`
  const desc = `${description}. Read the tutorial on Hack Club Workshops.`
  const url = `https://hackclub.com${slug}`

  const schema = {
    '@context': 'http://schema.org',
    '@type': 'HowTo',
    headline: name,
    image: [`https://hackclub.com${bg}`],
    author: {
      '@type': 'Person',
      name: author,
      url: `https://github.com/${
        author.match(/@(\S+)/) ? author.replace('@', '') : 'hackclub'
      }`
    },
    publisher: org,
    url,
    description,
    genre: group,
    position: order,
    educationalUse: 'assignment',
    isAccessibleForFree: true,
    potentialAction: {
      '@type': 'CreateAction',
      result: {
        '@type': 'CreativeWork',
        name
      }
    }
  }

  return (
    <Fragment>
      <Helmet
        title={title}
        meta={[
          { name: 'description', content: desc },
          { property: 'og:title', content: title },
          { name: 'twitter:title', content: title },
          { property: 'og:description', content: desc },
          { name: 'twitter:description', content: desc },
          { property: 'og:site_name', content: 'Hack Club Workshops' },
          { property: 'og:url', content: url }
        ]}
        children={


<commit_before>import React from 'react';
import { ProgressBar, Button, Modal, Collapse, FormGroup, Radio, ControlLabel, Tabs, Tab } from 'react-bootstrap';
import { object } from 'prop-types';
import ReactInterval from 'react-interval';
import { Queue } from 'taskcluster-client-web';
import config, { SHIPIT_API_URL } from '../../config';

const statusStyles = {
  // TC statuses
  unscheduled: 'info',
  pending: 'info',
  running: 'info',
  completed: 'success',
  failed: 'danger',
  exception: 'warning',
  // Additional statuses
  ready: 'info',
  blocked: 'info',
};

const taskStatus = async (taskId) => {
  const status = await (new Queue()).status(taskId);
  return status;
};

export default class ListReleases extends React.Component {
  constructor(...args) {
    super(...args);
    this.state = {
      loaded: false,
      message: '',
      releases: [],
      shippedReleases: [],
      shippedReleasesMessage: '',
    };
  }

  async componentDidMount() {
    await this.getReleases();
  }

  getReleases = async () => {
    try {
      const req = await fetch(`${SHIPIT_API_URL}/releases`);
      const releases = await req.json();
      let message = '';
      if (releases.length === 0) {
        message =
No pending releases!
;
      }
      this.setState({
        releases,
        message,
        loaded: true,
      });
    } catch (e) {
      const message =
Failed to fetch releases!
;
      this.setState({
        loaded: true,
        message,
        releases: [],
      });
      throw e;
    }
  };

  getRecentReleases = async () => {
    // this is an expensive call, let's not repeat it
    if (this.state.shippedReleases.length > 0) {
      return;
    }
    try {
      const req = await fetch(`${SHIPIT_API_URL}/releases?status=shipped`);
      const shippedReleases = await req.json();
      let shippedReleasesMessage = '';
      if (shippedReleases.length === 0) {
        shippedReleasesMessage =
No recent releases!
;
      }
      this.setState({
        shippedReleasesMessage,
        shippedReleases: shippedReleases.reverse().slice(0, 8),
      });
    } catch (e) {
      const shippedReleasesMessage =
Failed to fetch releases!
;
      this.setState({
        shippedReleasesMessage,
        shippedReleases: [],
      });
      throw e;
    }
  };

  handleTabSelect = (key) => {
    if (key === 'recentReleases') {
      this.getRecentReleases();
    }
  };

  renderRecentReleases = () => {
    const { shippedReleases, shippedReleasesMessage } = this.state;
    return (

Recent releases

          {shippedReleasesMessage}
          {shippedReleases.length > 0 && shippedReleases.map(release => (
            <Release
              release={release}
              key={release.name}
              showCancel={false}
            />))}

    );
  };

  renderReleases = () => {
    const { releases, loaded, message } = this.state;
    return (

Releases in progress

          {loaded || loading...}
          {message}
          {releases.length > 0 && releases.map(release => (
            <Release
              release={release}
              key={release.name}
              showCancel
            />))}
          <ReactInterval
            enabled
            timeout={2 * 60 * 1000}
            callback={() => this.getReleases()}
          />

    );
  };

  render() {
    return (
      <Tabs defaultActiveKey="releases" id="releases" onSelect={this.handleTabSelect}>
        <Tab eventKey="releases" title="In progress">
          {this.renderReleases()}
        </Tab>
        <Tab eventKey="recentReleases" title="Recent">
          {this.renderRecentReleases()}
        </Tab>
      </Tabs>
    );
  }
}

class Release extends React.Component {
  static contextTypes = {
    authController: object.isRequired,
  };
  constructor(props) {
    super(props);
    this.state = {
      showModal: false,
      submitted: props.submitted,
      errorMsg: null,
    };
  }

  open = () => {
    this.setState({ showModal: true });
  };

  close = () => {
    this.setState({
      showModal: false,
      errorMsg: null,
    });
  };

  abortRelease = async (release) => {
    const url = `${SHIPIT_API_URL}/releases/${release.name}`;
    if (!this.context.authController.userSession) {
      this.setState({ errorMsg: 'Login required!' });
      return;
    }
    const { accessToken } = this.context.authController.getUserSession();
    const headers = { Authorization: `Bearer ${accessToken}` };
    try {
      const response = await fetch(url, { method: 'DELETE', headers });
      if (!response.ok) {
        this.setState({ errorMsg: 'Auth failure!' });
        return;
      }
      this.setState({ submitted: true });
      window.location.reload();
    } catch (e) {
      this.setState({ errorMsg: 'Server issues!' });
      throw e;
    }
  };

  renderBody = () => {
    const { submitted, errorMsg } = this.state;
    if (errorMsg) {
      return (

{errorMsg}

      );
    }
    if (!submitted) {
      return (

Are you sure?

            The release will be cancelled!

      );
    }
    return (

Done.

    );
  };

  render() {
    const { release, showCancel } = this.props;
    return (

            <a href={`${config.TREEHERDER_URL}/#/jobs?repo=${release.project}&revision=${release.revision}`}>
              {release.product} {release.version} build{release.build_number}

            {showCancel &&
              <Button
                onClick={this.open}
                bsStyle="danger"
                bsSize="xsmall"
                style={{ margin: '10px' }}
                disabled={!this.context.authController.userSession}
              >
                Cancel release
              </Button>
            }

        <Modal show={this.state.showModal} onHide={this.close}>
          <Modal.Header closeButton>
            <Modal.Title>Cancel release</Modal.Title>
          </Modal.Header>
          <Modal.Body>
            {this.renderBody()}
          </Modal.Body>
          <Modal.Footer>
            <Button
              onClick={() => this.abortRelease(release)}
              bsStyle="danger"
              disabled={!this.context.authController.userSession}
            >
              Stop release
            </Button>
            <Button onClick={this.close} bsStyle="primary">Close</Button>
          </Modal.Footer>
        </Modal>

          <TaskProgress phases={release.phases} releaseName={release.name} />

    );
  }
}

const phaseStatus = async (phase, idx, phases) => {
  // Use TC status if task is submitted
  const previousPhase = phases[idx - 1];
  if (phase.submitted) {
    const status = await taskStatus(phase.actionTaskId);
    return status.status.state;
  }
  // First phase, ready any time
  if (idx === 0) {
    return 'ready';
  }
  // Phase is ready only when the previous one is submitted and the task is completed
  if (previousPhase.submitted) {
    // TODO: cache previous phase status
    const status = await taskStatus(previousPhase.actionTaskId);
    if (status.status.state === 'completed') {
      return 'ready';
    }
  }
  // Special case for Firefox RC.
  // push_firefox can be scheduled even if ship_firefox_rc (the previous phase)
  // is not ready. We still need to be sure that promote_firefox_rc is ready
  if (phase.name === 'push_firefox' && previousPhase.name === 'ship_firefox_rc') {
    const promoteFirefoxRCPhase = phases[0];
    if (promoteFirefoxRCPhase.submitted) {
      const status = await taskStatus(promoteFirefoxRCPhase.actionTaskId);
      if (status.status.state === 'completed') {
        return 'ready';
      }
    }
  }
  return 'blocked';
};

const phaseSignOffs = async (releaseName, phaseName) => {
  const url = `${SHIPIT_API_URL}/signoff/${releaseName}/${phaseName}`;
  const response = await fetch(url);
  if (!response.ok) {
    return [];
  }
  const signoffs = await response.json();
  return signoffs;
};

class TaskProgress extends React.Component {
  constructor(...args) {
    super(...args);
    this.state = {
      phasesWithStatus: [],
    };
  }

  async componentDidMount() {
    await this.syncPhases();
  }

  syncPhases = async () => {
    const { releaseName, phases } = this.props;
    const phasesWithStatus = await Promise.all(phases.map(async (phase, idx, arr) => {
      const status = await phaseStatus(phase, idx, arr);
      const signoffs = await phaseSignOffs(releaseName, phase.name);
      return { ...phase, status, signoffs };
    }));
    this.setState({ phasesWithStatus });
  };

  render() {
    const { phasesWithStatus } = this.state;
    const { releaseName } = this.props;
    const width = 100 / phasesWithStatus.length;
    return (
      <ProgressBar style={{ height: '40px', padding: '3px' }}>
        {phasesWithStatus.map(({
          name, submitted, actionTaskId, status, signoffs,
        }) => (
          <ProgressBar
            key={name}
            bsStyle={statusStyles[status] || 'info'}
            now={width}
            active={submitted && status === 'running'}
            label={<TaskLabel
              key={actionTaskId}
              name={name}
              submitted={submitted}
              status={status}
              signoffs={signoffs}
              releaseName={releaseName}
              taskGroupUrl={`${config.TASKCLUSTER_TOOLS_URL}/groups/${actionTaskId}`}
            />}
          />
        ))}
      </ProgressBar>
    );
  }
}

class TaskLabel extends React.PureComponent {
  static contextTypes = {
    authController: object.isRequired,
  };

  constructor(props) {
    super(props);
    this.state = {
      showModal: false,
      submitted: props.submitted,
      errorMsg: null,
      selectedSignoff: null,
    };
  }

  open = () => {
    this.setState({ showModal: true });
  };

  close = () => {
    this.setState({
      showModal: false,
      errorMsg: null,
      selectedSignoff: null,
    });
  };

  signOff = async () => {
    const { accessToken } = this.context.authController.getUserSession();
    const { releaseName, name } = this.props;
    const { selectedSignoff } = this.state;
    const url = `${SHIPIT_API_URL}/signoff/${releaseName}/${name}`;
    const headers = {
      Authorization: `Bearer ${accessToken}`,
      'Content-Type': 'application/json',
    };
    const body = JSON.stringify(selectedSignoff);
    try {
      const response = await fetch(url, { method: 'PUT', headers, body });
      if (!response.ok) {
        const err = await response.json();
        this.setState({ errorMsg: `Error: ${err.detail}` });
        return;
      }
      this.setState({ submitted: true });
    } catch (e) {
      this.setState({ errorMsg: 'Server issues!' });
      throw e;
    }
  };

  schedulePhase = async () => {
    const { accessToken } = this.context.authController.getUserSession();
    const { releaseName, name } = this.props;
    const headers = { Authorization: `Bearer ${accessToken}` };
    const url = `${SHIPIT_API_URL}/releases/${releaseName}/${name}`;
    try {
      const response = await fetch(url, { method: 'PUT', headers });
      if (!response.ok) {
        const err = await response.json();
        this.setState({ errorMsg: `Error: ${err.detail}` });
        return;
      }
      this.setState({ submitted: true });
    } catch (e) {
      this.setState({ errorMsg: 'Server issues!' });
      throw e;
    }
  };

  doEet = async () => {
    if (!this.context.authController.userSession) {
      this.setState({ errorMsg: 'Login required!' });
      return;
    }
    const { selectedSignoff } = this.state;
    if (selectedSignoff) {
      await this.signOff();
    } else {
      await this.schedulePhase();
    }
  };

  renderSignoffs = () => {
    const { signoffs } = this.props.signoffs;
    if (signoffs.length === 0) {
      return
No signoffs required
;
    }
    return (

        <ControlLabel>Sign off as</ControlLabel>
        <FormGroup>
          {signoffs.map(s => (
            <Radio
              key={s.uid}
              name="signoff"
              disabled={s.signed}
              onClick={() => this.setState({ selectedSignoff: s.uid })}
            >
              {s.name} - {s.description}
            </Radio>
          ))}
        </FormGroup>

    );
  };

  renderBody = () => {
    const { submitted, errorMsg } = this.state;
    if (errorMsg) {
      return (

{errorMsg}

      );
    }
    if (!submitted) {
      return (

Are you sure?

Action will be scheduled

          {this.renderSignoffs()}

      );
    }
    return (

        Action task has been submitted.

    );
  };

  render() {
    const { status, name, taskGroupUrl } = this.props;
    if (status === 'blocked') {
      return (

          <Button disabled bsStyle={statusStyles[status]}>{name}</Button>

      );
    }
    if (status === 'ready') {
      return (

          <Button bsStyle="primary" onClick={this.open}>{name}</Button>
          <Modal show={this.state.showModal} onHide={this.close}>
            <Modal.Header closeButton>
              <Modal.Title>Do eet</Modal.Title>
            </Modal.Header>
            <Modal.Body>
              {this.renderBody()}
            </Modal.Body>
            <Modal.Footer>
              <Collapse in={!this.state.submitted}>

                  <Button
                    onClick={this.doEet}
                    bsStyle="danger"
                    disabled={!this.context.authController.userSession && !this.state.submitted}
                  >
                    Do eet!
                  </Button>
                  <Button onClick={this.close} bsStyle="primary">Close</Button>

              </Collapse>
            </Modal.Footer>
          </Modal>

      );
    }
    return (

        <Button bsStyle={statusStyles[status]} href={taskGroupUrl}>{name}</Button>

    );
  }
}
<commit_msg>shipit/frontend: recent releases for all branches (#1949)<commit_after>import React from 'react';
import { ProgressBar, Button, Modal, Collapse, FormGroup, Radio, ControlLabel, Tabs, Tab } from 'react-bootstrap';
import { object } from 'prop-types';
import ReactInterval from 'react-interval';
import { Queue } from 'taskcluster-client-web';
import config, { SHIPIT_API_URL } from '../../config';
import { getShippedReleases } from '../../components/api';

const statusStyles = {
  // TC statuses
  unscheduled: 'info',
  pending: 'info',
  running: 'info',
  completed: 'success',
  failed: 'danger',
  exception: 'warning',
  // Additional statuses
  ready: 'info',
  blocked: 'info',
};

const taskStatus = async (taskId) => {
  const status = await (new Queue()).status(taskId);
  return status;
};

export default class ListReleases extends React.Component {
  constructor(...args) {
    super(...args);
    this.state = {
      loaded: false,
      message: '',
      releases: [],
      shippedReleases: [],
      shippedReleasesMessage: '',
    };
  }

  async componentDidMount() {
    await this.getReleases();
  }

  getReleases = async () => {
    try {
      const req = await fetch(`${SHIPIT_API_URL}/releases`);
      const releases = await req.json();
      let message = '';
      if (releases.length === 0) {
        message =
No pending releases!
;
      }
      this.setState({
        releases,
        message,
        loaded: true,
      });
    } catch (e) {
      const message =
Failed to fetch releases!
;
      this.setState({
        loaded: true,
        message,
        releases: [],
      });
      throw e;
    }
  };

  getRecentReleases = async (product, branch) => {
    try {
      const shippedReleases = await getShippedReleases(product, branch);
      let shippedReleasesMessage = '';
      if (shippedReleases.length === 0) {
        shippedReleasesMessage =
No recent releases!
;
      }
      this.setState(state => ({
        shippedReleasesMessage,
        shippedReleases: state.shippedReleases.concat(shippedReleases.slice(0, 4)),
      }));
    } catch (e) {
      const shippedReleasesMessage =
Failed to fetch releases!
;
      this.setState({
        shippedReleasesMessage,
        shippedReleases: [],
      });
      throw e;
    }
  };

  handleTabSelect = (key) => {
    if (key === 'recentReleases') {
      // this is an expensive call, let's not repeat it
      if (this.state.shippedReleases.length > 0) {
        return;
      }
      config.PRODUCTS.forEach((product) => {
        product.branches.forEach((branch) => {
          this.getRecentReleases(product.product, branch.branch);
        });
      });
    }
  };

  renderRecentReleases = () => {
    const { shippedReleases, shippedReleasesMessage } = this.state;
    // Sort the releases by date, reversed
    const sortedShippedReleases = shippedReleases.sort((a, b) => a.created  0 && sortedShippedReleases.map(release => (
            <Release
              release={release}
              key={release.name}
              showCancel={false}
            />))}

    );
  };

  renderReleases = () => {
    const { releases, loaded, message } = this.state;
    return (

Releases in progress

          {loaded || loading...}
          {message}
          {releases.length > 0 && releases.map(release => (
            <Release
              release={release}
              key={release.name}
              showCancel
            />))}
          <ReactInterval
            enabled
            timeout={2 * 60 * 1000}
            callback={() => this.getReleases()}
          />

    );
  };

  render() {
    return (
      <Tabs defaultActiveKey="releases" id="releases" onSelect={this.handleTabSelect}>
        <Tab eventKey="releases" title="In progress">
          {this.renderReleases()}
        </Tab>
        <Tab eventKey="recentReleases" title="Recent">
          {this.renderRecentReleases()}
        </Tab>
      </Tabs>
    );
  }
}

class Release extends React.Component {
  static contextTypes = {
    authController: object.isRequired,
  };
  constructor(props) {
    super(props);
    this.state = {
      showModal: false,
      submitted: props.submitted,
      errorMsg: null,
    };
  }

  open = () => {
    this.setState({ showModal: true });
  };

  close = () => {
    this.setState({
      showModal: false,
      errorMsg: null,
    });
  };

  abortRelease = async (release) => {
    const url = `${SHIPIT_API_URL}/releases/${release.name}`;
    if (!this.context.authController.userSession) {
      this.setState({ errorMsg: 'Login required!' });
      return;
    }
    const { accessToken } = this.context.authController.getUserSession();
    const headers = { Authorization: `Bearer ${accessToken}` };
    try {
      const response = await fetch(url, { method: 'DELETE', headers });
      if (!response.ok) {
        this.setState({ errorMsg: 'Auth failure!' });
        return;
      }
      this.setState({ submitted: true });
      window.location.reload();
    } catch (e) {
      this.setState({ errorMsg: 'Server issues!' });
      throw e;
    }
  };

  renderBody = () => {
    const { submitted, errorMsg } = this.state;
    if (errorMsg) {
      return (

{errorMsg}

      );
    }
    if (!submitted) {
      return (

Are you sure?

            The release will be cancelled!

      );
    }
    return (

Done.

    );
  };

  render() {
    const { release, showCancel } = this.props;
    return (

            <a href={`${config.TREEHERDER_URL}/#/jobs?repo=${release.project}&revision=${release.revision}`}>
              {release.product} {release.version} build{release.build_number}

            {showCancel &&
              <Button
                onClick={this.open}
                bsStyle="danger"
                bsSize="xsmall"
                style={{ margin: '10px' }}
                disabled={!this.context.authController.userSession}
              >
                Cancel release
              </Button>
            }

        <Modal show={this.state.showModal} onHide={this.close}>
          <Modal.Header closeButton>
            <Modal.Title>Cancel release</Modal.Title>
          </Modal.Header>
          <Modal.Body>
            {this.renderBody()}
          </Modal.Body>
          <Modal.Footer>
            <Button
              onClick={() => this.abortRelease(release)}
              bsStyle="danger"
              disabled={!this.context.authController.userSession}
            >
              Stop release
            </Button>
            <Button onClick={this.close} bsStyle="primary">Close</Button>
          </Modal.Footer>
        </Modal>

          <TaskProgress phases={release.phases} releaseName={release.name} />

    );
  }
}

const phaseStatus = async (phase, idx, phases) => {
  // Use TC status if task is submitted
  const previousPhase = phases[idx - 1];
  if (phase.submitted) {
    const status = await taskStatus(phase.actionTaskId);
    return status.status.state;
  }
  // First phase, ready any time
  if (idx === 0) {
    return 'ready';
  }
  // Phase is ready only when the previous one is submitted and the task is completed
  if (previousPhase.submitted) {
    // TODO: cache previous phase status
    const status = await taskStatus(previousPhase.actionTaskId);
    if (status.status.state === 'completed') {
      return 'ready';
    }
  }
  // Special case for Firefox RC.
  // push_firefox can be scheduled even if ship_firefox_rc (the previous phase)
  // is not ready. We still need to be sure that promote_firefox_rc is ready
  if (phase.name === 'push_firefox' && previousPhase.name === 'ship_firefox_rc') {
    const promoteFirefoxRCPhase = phases[0];
    if (promoteFirefoxRCPhase.submitted) {
      const status = await taskStatus(promoteFirefoxRCPhase.actionTaskId);
      if (status.status.state === 'completed') {
        return 'ready';
      }
    }
  }
  return 'blocked';
};

const phaseSignOffs = async (releaseName, phaseName) => {
  const url = `${SHIPIT_API_URL}/signoff/${releaseName}/${phaseName}`;
  const response = await fetch(url);
  if (!response.ok) {
    return [];
  }
  const signoffs = await response.json();
  return signoffs;
};

class TaskProgress extends React.Component {
  constructor(...args) {
    super(...args);
    this.state = {
      phasesWithStatus: [],
    };
  }

  async componentDidMount() {
    await this.syncPhases();
  }

  syncPhases = async () => {
    const { releaseName, phases } = this.props;
    const phasesWithStatus = await Promise.all(phases.map(async (phase, idx, arr) => {
      const status = await phaseStatus(phase, idx, arr);
      const signoffs = await phaseSignOffs(releaseName, phase.name);
      return { ...phase, status, signoffs };
    }));
    this.setState({ phasesWithStatus });
  };

  render() {
    const { phasesWithStatus } = this.state;
    const { releaseName } = this.props;
    const width = 100 / phasesWithStatus.length;
    return (
      <ProgressBar style={{ height: '40px', padding: '3px' }}>
        {phasesWithStatus.map(({
          name, submitted, actionTaskId, status, signoffs,
        }) => (
          <ProgressBar
            key={name}
            bsStyle={statusStyles[status] || 'info'}
            now={width}
            active={submitted && status === 'running'}
            label={<TaskLabel
              key={actionTaskId}
              name={name}
              submitted={submitted}
              status={status}
              signoffs={signoffs}
              releaseName={releaseName}
              taskGroupUrl={`${config.TASKCLUSTER_TOOLS_URL}/groups/${actionTaskId}`}
            />}
          />
        ))}
      </ProgressBar>
    );
  }
}

class TaskLabel extends React.PureComponent {
  static contextTypes = {
    authController: object.isRequired,
  };

  constructor(props) {
    super(props);
    this.state = {
      showModal: false,
      submitted: props.submitted,
      errorMsg: null,
      selectedSignoff: null,
    };
  }

  open = () => {
    this.setState({ showModal: true });
  };

  close = () => {
    this.setState({
      showModal: false,
      errorMsg: null,
      selectedSignoff: null,
    });
  };

  signOff = async () => {
    const { accessToken } = this.context.authController.getUserSession();
    const { releaseName, name } = this.props;
    const { selectedSignoff } = this.state;
    const url = `${SHIPIT_API_URL}/signoff/${releaseName}/${name}`;
    const headers = {
      Authorization: `Bearer ${accessToken}`,
      'Content-Type': 'application/json',
    };
    const body = JSON.stringify(selectedSignoff);
    try {
      const response = await fetch(url, { method: 'PUT', headers, body });
      if (!response.ok) {
        const err = await response.json();
        this.setState({ errorMsg: `Error: ${err.detail}` });
        return;
      }
      this.setState({ submitted: true });
    } catch (e) {
      this.setState({ errorMsg: 'Server issues!' });
      throw e;
    }
  };

  schedulePhase = async () => {
    const { accessToken } = this.context.authController.getUserSession();
    const { releaseName, name } = this.props;
    const headers = { Authorization: `Bearer ${accessToken}` };
    const url = `${SHIPIT_API_URL}/releases/${releaseName}/${name}`;
    try {
      const response = await fetch(url, { method: 'PUT', headers });
      if (!response.ok) {
        const err = await response.json();
        this.setState({ errorMsg: `Error: ${err.detail}` });
        return;
      }
      this.setState({ submitted: true });
    } catch (e) {
      this.setState({ errorMsg: 'Server issues!' });
      throw e;
    }
  };

  doEet = async () => {
    if (!this.context.authController.userSession) {
      this.setState({ errorMsg: 'Login required!' });
      return;
    }
    const { selectedSignoff } = this.state;
    if (selectedSignoff) {
      await this.signOff();
    } else {
      await this.schedulePhase();
    }
  };

  renderSignoffs = () => {
    const { signoffs } = this.props.signoffs;
    if (signoffs.length === 0) {
      return
No signoffs required
;
    }
    return (

        <ControlLabel>Sign off as</ControlLabel>
        <FormGroup>
          {signoffs.map(s => (
            <Radio
              key={s.uid}
              name="signoff"
              disabled={s.signed}
              onClick={() => this.setState({ selectedSignoff: s.uid })}
            >
              {s.name} - {s.description}
            </Radio>
          ))}
        </FormGroup>

    );
  };

  renderBody = () => {
    const { submitted, errorMsg } = this.state;
    if (errorMsg) {
      return (

{errorMsg}

      );
    }
    if (!submitted) {
      return (

Are you sure?

Action will be scheduled

          {this.renderSignoffs()}

      );
    }
    return (

        Action task has been submitted.

    );
  };

  render() {
    const { status, name, taskGroupUrl } = this.props;
    if (status === 'blocked') {
      return (

          <Button disabled bsStyle={statusStyles[status]}>{name}</Button>

      );
    }
    if (status === 'ready') {
      return (

          <Button bsStyle="primary" onClick={this.open}>{name}</Button>
          <Modal show={this.state.showModal} onHide={this.close}>
            <Modal.Header closeButton>
              <Modal.Title>Do eet</Modal.Title>
            </Modal.Header>
            <Modal.Body>
              {this.renderBody()}
            </Modal.Body>
            <Modal.Footer>
              <Collapse in={!this.state.submitted}>

                  <Button
                    onClick={this.doEet}
                    bsStyle="danger"
                    disabled={!this.context.authController.userSession && !this.state.submitted}
                  >
                    Do eet!
                  </Button>
                  <Button onClick={this.close} bsStyle="primary">Close</Button>

              </Collapse>
            </Modal.Footer>
          </Modal>

      );
    }
    return (

        <Button bsStyle={statusStyles[status]} href={taskGroupUrl}>{name}</Button>

    );
  }
}
<|endoftext|>


<commit_before>// disabling eslint for now, going to rewrite this in v5
/* eslint-disable */

import { TEXT_GRADIENT } from '../const';
import { hex2string } from '../utils';

const defaultStyle = {
    align: 'left',
    breakWords: false,
    dropShadow: false,
    dropShadowAlpha: 1,
    dropShadowAngle: Math.PI / 6,
    dropShadowBlur: 0,
    dropShadowColor: '#000000',
    dropShadowDistance: 5,
    fill: 'black',
    fillGradientType: TEXT_GRADIENT.LINEAR_VERTICAL,
    fillGradientStops: [],
    fontFamily: 'Arial',
    fontSize: 26,
    fontStyle: 'normal',
    fontVariant: 'normal',
    fontWeight: 'normal',
    letterSpacing: 0,
    lineHeight: 0,
    lineJoin: 'miter',
    miterLimit: 10,
    padding: 0,
    stroke: 'black',
    strokeThickness: 0,
    textBaseline: 'alphabetic',
    trim: false,
    wordWrap: false,
    wordWrapWidth: 100,
};

/**
 * A TextStyle Object decorates a Text Object. It can be shared between
 * multiple Text objects. Changing the style will update all text objects using it.
 *
 * @class
 * @memberof PIXI
 */
export default class TextStyle
{
    /**
     * @param {object} [style] - The style parameters
     * @param {string} [style.align='left'] - Alignment for multiline text ('left', 'center' or 'right'),
     *  does not affect single line text
     * @param {boolean} [style.breakWords=false] - Indicates if lines can be wrapped within words, it
     *  needs wordWrap to be set to true
     * @param {boolean} [style.dropShadow=false] - Set a drop shadow for the text
     * @param {number} [style.dropShadowAlpha=1] - Set alpha for the drop shadow
     * @param {number} [style.dropShadowAngle=Math.PI/6] - Set a angle of the drop shadow
     * @param {number} [style.dropShadowBlur=0] - Set a shadow blur radius
     * @param {string} [style.dropShadowColor='#000000'] - A fill style to be used on the dropshadow e.g 'red', '#00FF00'
     * @param {number} [style.dropShadowDistance=5] - Set a distance of the drop shadow
     * @param {string|string[]|number|number[]|CanvasGradient|CanvasPattern} [style.fill='black'] - A canvas
     *  fillstyle that will be used on the text e.g 'red', '#00FF00'. Can be an array to create a gradient
     *  eg ['#000000','#FFFFFF']
     * {@link https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/fillStyle|MDN}
     * @param {number} [style.fillGradientType=PIXI.TEXT_GRADIENT.LINEAR_VERTICAL] - If fill is an array of colours
     *  to create a gradient, this can change the type/direction of the gradient. See {@link PIXI.TEXT_GRADIENT}
     * @param {number[]} [style.fillGradientStops] - If fill is an array of colours to create a gradient, this array can set
     * the stop points (numbers between 0 and 1) for the color, overriding the default behaviour of evenly spacing them.
     * @param {string|string[]} [style.fontFamily='Arial'] - The font family
     * @param {number|string} [style.fontSize=26] - The font size (as a number it converts to px, but as a string,
     *  equivalents are '26px','20pt','160%' or '1.6em')
     * @param {string} [style.fontStyle='normal'] - The font style ('normal', 'italic' or 'oblique')
     * @param {string} [style.fontVariant='normal'] - The font variant ('normal' or 'small-caps')
     * @param {string} [style.fontWeight='normal'] - The font weight ('normal', 'bold', 'bolder', 'lighter' and '100',
     *  '200', '300', '400', '500', '600', '700', 800' or '900')
     * @param {number} [style.letterSpacing=0] - The amount of spacing between letters, default is 0
     * @param {number} [style.lineHeight] - The line height, a number that represents the vertical space that a letter uses
     * @param {string} [style.lineJoin='miter'] - The lineJoin property sets the type of corner created, it can resolve
     *      spiked text issues. Default is 'miter' (creates a sharp corner).
     * @param {number} [style.miterLimit=10] - The miter limit to use when using the 'miter' lineJoin mode. This can reduce
     *      or increase the spikiness of rendered text.
     * @param {number} [style.padding=0] - Occasionally some fonts are cropped. Adding some padding will prevent this from
     *     happening by adding padding to all sides of the text.
     * @param {string|number} [style.stroke='black'] - A canvas fillstyle that will be used on the text stroke
     *  e.g 'blue', '#FCFF00'
     * @param {number} [style.strokeThickness=0] - A number that represents the thickness of the stroke.
     *  Default is 0 (no stroke)
     * @param {boolean} [style.trim=false] - Trim transparent borders
     * @param {string} [style.textBaseline='alphabetic'] - The baseline of the text that is rendered.
     * @param {boolean} [style.wordWrap=false] - Indicates if word wrap should be used
     * @param {number} [style.wordWrapWidth=100] - The width at which text will wrap, it needs wordWrap to be set to true
     */
    constructor(style)
    {
        this.styleID = 0;

        Object.assign(this, defaultStyle, style);
    }

    /**
     * Creates a new TextStyle object with the same values as this one.
     * Note that the only the properties of the object are cloned.
     *
     * @return {PIXI.TextStyle} New cloned TextStyle object
     */
    clone()
    {
        const clonedProperties = {};

        for (const key in defaultStyle)
        {
            clonedProperties[key] = this[key];
        }

        return new TextStyle(clonedProperties);
    }

    /**
     * Resets all properties to the defaults specified in TextStyle.prototype._default
     */
    reset()
    {
        Object.assign(this, defaultStyle);
    }

    get align()
    {
        return this._align;
    }
    set align(align)
    {
        if (this._align !== align)
        {
            this._align = align;
            this.styleID++;
        }
    }

    get breakWords()
    {
        return this._breakWords;
    }
    set breakWords(breakWords)
    {
        if (this._breakWords !== breakWords)
        {
            this._breakWords = breakWords;
            this.styleID++;
        }
    }

    get dropShadow()
    {
        return this._dropShadow;
    }
    set dropShadow(dropShadow)
    {
        if (this._dropShadow !== dropShadow)
        {
            this._dropShadow = dropShadow;
            this.styleID++;
        }
    }

    get dropShadowAlpha()
    {
        return this._dropShadowAlpha;
    }
    set dropShadowAlpha(dropShadowAlpha)
    {
        if (this._dropShadowAlpha !== dropShadowAlpha)
        {
            this._dropShadowAlpha = dropShadowAlpha;
            this.styleID++;
        }
    }

    get dropShadowAngle()
    {
        return this._dropShadowAngle;
    }
    set dropShadowAngle(dropShadowAngle)
    {
        if (this._dropShadowAngle !== dropShadowAngle)
        {
            this._dropShadowAngle = dropShadowAngle;
            this.styleID++;
        }
    }

    get dropShadowBlur()
    {
        return this._dropShadowBlur;
    }
    set dropShadowBlur(dropShadowBlur)
    {
        if (this._dropShadowBlur !== dropShadowBlur)
        {
            this._dropShadowBlur = dropShadowBlur;
            this.styleID++;
        }
    }

    get dropShadowColor()
    {
        return this._dropShadowColor;
    }
    set dropShadowColor(dropShadowColor)
    {
        const outputColor = getColor(dropShadowColor);
        if (this._dropShadowColor !== outputColor)
        {
            this._dropShadowColor = outputColor;
            this.styleID++;
        }
    }

    get dropShadowDistance()
    {
        return this._dropShadowDistance;
    }
    set dropShadowDistance(dropShadowDistance)
    {
        if (this._dropShadowDistance !== dropShadowDistance)
        {
            this._dropShadowDistance = dropShadowDistance;
            this.styleID++;
        }
    }

    get fill()
    {
        return this._fill;
    }
    set fill(fill)
    {
        const outputColor = getColor(fill);
        if (this._fill !== outputColor)
        {
            this._fill = outputColor;
            this.styleID++;
        }
    }

    get fillGradientType()
    {
        return this._fillGradientType;
    }
    set fillGradientType(fillGradientType)
    {
        if (this._fillGradientType !== fillGradientType)
        {
            this._fillGradientType = fillGradientType;
            this.styleID++;
        }
    }

    get fillGradientStops()
    {
        return this._fillGradientStops;
    }
    set fillGradientStops(fillGradientStops)
    {
        if (!areArraysEqual(this._fillGradientStops,fillGradientStops))
        {
            this._fillGradientStops = fillGradientStops;
            this.styleID++;
        }
    }

    get fontFamily()
    {
        return this._fontFamily;
    }
    set fontFamily(fontFamily)
    {
        if (this.fontFamily !== fontFamily)
        {
            this._fontFamily = fontFamily;
            this.styleID++;
        }
    }

    get fontSize()
    {
        return this._fontSize;
    }
    set fontSize(fontSize)
    {
        if (this._fontSize !== fontSize)
        {
            this._fontSize = fontSize;
            this.styleID++;
        }
    }

    get fontStyle()
    {
        return this._fontStyle;
    }
    set fontStyle(fontStyle)
    {
        if (this._fontStyle !== fontStyle)
        {
            this._fontStyle = fontStyle;
            this.styleID++;
        }
    }

    get fontVariant()
    {
        return this._fontVariant;
    }
    set fontVariant(fontVariant)
    {
        if (this._fontVariant !== fontVariant)
        {
            this._fontVariant = fontVariant;
            this.styleID++;
        }
    }

    get fontWeight()
    {
        return this._fontWeight;
    }
    set fontWeight(fontWeight)
    {
        if (this._fontWeight !== fontWeight)
        {
            this._fontWeight = fontWeight;
            this.styleID++;
        }
    }

    get letterSpacing()
    {
        return this._letterSpacing;
    }
    set letterSpacing(letterSpacing)
    {
        if (this._letterSpacing !== letterSpacing)
        {
            this._letterSpacing = letterSpacing;
            this.styleID++;
        }
    }

    get lineHeight()
    {
        return this._lineHeight;
    }
    set lineHeight(lineHeight)
    {
        if (this._lineHeight !== lineHeight)
        {
            this._lineHeight = lineHeight;
            this.styleID++;
        }
    }

    get lineJoin()
    {
        return this._lineJoin;
    }
    set lineJoin(lineJoin)
    {
        if (this._lineJoin !== lineJoin)
        {
            this._lineJoin = lineJoin;
            this.styleID++;
        }
    }

    get miterLimit()
    {
        return this._miterLimit;
    }
    set miterLimit(miterLimit)
    {
        if (this._miterLimit !== miterLimit)
        {
            this._miterLimit = miterLimit;
            this.styleID++;
        }
    }

    get padding()
    {
        return this._padding;
    }
    set padding(padding)
    {
        if (this._padding !== padding)
        {
            this._padding = padding;
            this.styleID++;
        }
    }

    get stroke()
    {
        return this._stroke;
    }
    set stroke(stroke)
    {
        const outputColor = getColor(stroke);
        if (this._stroke !== outputColor)
        {
            this._stroke = outputColor;
            this.styleID++;
        }
    }

    get strokeThickness()
    {
        return this._strokeThickness;
    }
    set strokeThickness(strokeThickness)
    {
        if (this._strokeThickness !== strokeThickness)
        {
            this._strokeThickness = strokeThickness;
            this.styleID++;
        }
    }

    get textBaseline()
    {
        return this._textBaseline;
    }
    set textBaseline(textBaseline)
    {
        if (this._textBaseline !== textBaseline)
        {
            this._textBaseline = textBaseline;
            this.styleID++;
        }
    }

    get trim()
    {
        return this._trim;
    }
    set trim(trim)
    {
        if (this._trim !== trim)
        {
            this._trim = trim;
            this.styleID++;
        }
    }

    get wordWrap()
    {
        return this._wordWrap;
    }
    set wordWrap(wordWrap)
    {
        if (this._wordWrap !== wordWrap)
        {
            this._wordWrap = wordWrap;
            this.styleID++;
        }
    }

    get wordWrapWidth()
    {
        return this._wordWrapWidth;
    }
    set wordWrapWidth(wordWrapWidth)
    {
        if (this._wordWrapWidth !== wordWrapWidth)
        {
            this._wordWrapWidth = wordWrapWidth;
            this.styleID++;
        }
    }

    /**
     * Generates a font style string to use for `TextMetrics.measureFont()`.
     *
     * @return {string} Font style string, for passing to `TextMetrics.measureFont()`
     */
    toFontString()
    {
        // build canvas api font setting from individual components. Convert a numeric this.fontSize to px
        const fontSizeString = (typeof this.fontSize === 'number') ? `${this.fontSize}px` : this.fontSize;

        // Clean-up fontFamily property by quoting each font name
        // this will support font names with spaces
        let fontFamilies = this.fontFamily;

        if (!Array.isArray(this.fontFamily))
        {
            fontFamilies = this.fontFamily.split(',');
        }

        for (let i = fontFamilies.length - 1; i >= 0; i--)
        {
            // Trim any extra white-space
            let fontFamily = fontFamilies[i].trim();

            // Check if font already contains strings
            if (!(/([\"\'])[^\'\"]+\1/).test(fontFamily))
            {
                fontFamily = `"${fontFamily}"`;
            }
            fontFamilies[i] = fontFamily;
        }

        return `${this.fontStyle} ${this.fontVariant} ${this.fontWeight} ${fontSizeString} ${fontFamilies.join(',')}`;
    }
}

/**
 * Utility function to convert hexadecimal colors to strings, and simply return the color if it's a string.
 *
 * @param {number|number[]} color
 * @return {string} The color as a string.
 */
function getSingleColor(color)
{
    if (typeof color === 'number')
    {
        return hex2string(color);
    }
    else if ( typeof color === 'string' )
    {
        if ( color.indexOf('0x') === 0 )
        {
            color = color.replace('0x', '#');
        }
    }

    return color;
}

/**
 * Utility function to convert hexadecimal colors to strings, and simply return the color if it's a string.
 * This version can also convert array of colors
 *
 * @param {number|number[]} color
 * @return {string} The color as a string.
 */
function getColor(color)
{
    if (!Array.isArray(color))
    {
        return getSingleColor(color);
    }
    else
    {
        for (let i = 0; i < color.length; ++i)
        {
            color[i] = getSingleColor(color[i]);
        }

        return color;
    }
}

/**
 * Utility function to convert hexadecimal colors to strings, and simply return the color if it's a string.
 * This version can also convert array of colors
 *
 * @param {Array} array1 First array to compare
 * @param {Array} array2 Second array to compare
 * @return {boolean} Do the arrays contain the same values in the same order
 */
function areArraysEqual(array1, array2)
{
    if (!Array.isArray(array1) || !Array.isArray(array2))
    {
        return false;
    }

    if (array1.length !== array2.length)
    {
        return false;
    }

    for (let i = 0; i < array1.length; ++i)
    {
        if (array1[i] !== array2[i])
        {
            return false;
        }
    }

    return true;
}
<commit_msg>Changes the default dropShadowColor in TextStyle (#4083)<commit_after>// disabling eslint for now, going to rewrite this in v5
/* eslint-disable */

import { TEXT_GRADIENT } from '../const';
import { hex2string } from '../utils';

const defaultStyle = {
    align: 'left',
    breakWords: false,
    dropShadow: false,
    dropShadowAlpha: 1,
    dropShadowAngle: Math.PI / 6,
    dropShadowBlur: 0,
    dropShadowColor: 'black',
    dropShadowDistance: 5,
    fill: 'black',
    fillGradientType: TEXT_GRADIENT.LINEAR_VERTICAL,
    fillGradientStops: [],
    fontFamily: 'Arial',
    fontSize: 26,
    fontStyle: 'normal',
    fontVariant: 'normal',
    fontWeight: 'normal',
    letterSpacing: 0,
    lineHeight: 0,
    lineJoin: 'miter',
    miterLimit: 10,
    padding: 0,
    stroke: 'black',
    strokeThickness: 0,
    textBaseline: 'alphabetic',
    trim: false,
    wordWrap: false,
    wordWrapWidth: 100,
};

/**
 * A TextStyle Object decorates a Text Object. It can be shared between
 * multiple Text objects. Changing the style will update all text objects using it.
 *
 * @class
 * @memberof PIXI
 */
export default class TextStyle
{
    /**
     * @param {object} [style] - The style parameters
     * @param {string} [style.align='left'] - Alignment for multiline text ('left', 'center' or 'right'),
     *  does not affect single line text
     * @param {boolean} [style.breakWords=false] - Indicates if lines can be wrapped within words, it
     *  needs wordWrap to be set to true
     * @param {boolean} [style.dropShadow=false] - Set a drop shadow for the text
     * @param {number} [style.dropShadowAlpha=1] - Set alpha for the drop shadow
     * @param {number} [style.dropShadowAngle=Math.PI/6] - Set a angle of the drop shadow
     * @param {number} [style.dropShadowBlur=0] - Set a shadow blur radius
     * @param {string} [style.dropShadowColor='black'] - A fill style to be used on the dropshadow e.g 'red', '#00FF00'
     * @param {number} [style.dropShadowDistance=5] - Set a distance of the drop shadow
     * @param {string|string[]|number|number[]|CanvasGradient|CanvasPattern} [style.fill='black'] - A canvas
     *  fillstyle that will be used on the text e.g 'red', '#00FF00'. Can be an array to create a gradient
     *  eg ['#000000','#FFFFFF']
     * {@link https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/fillStyle|MDN}
     * @param {number} [style.fillGradientType=PIXI.TEXT_GRADIENT.LINEAR_VERTICAL] - If fill is an array of colours
     *  to create a gradient, this can change the type/direction of the gradient. See {@link PIXI.TEXT_GRADIENT}
     * @param {number[]} [style.fillGradientStops] - If fill is an array of colours to create a gradient, this array can set
     * the stop points (numbers between 0 and 1) for the color, overriding the default behaviour of evenly spacing them.
     * @param {string|string[]} [style.fontFamily='Arial'] - The font family
     * @param {number|string} [style.fontSize=26] - The font size (as a number it converts to px, but as a string,
     *  equivalents are '26px','20pt','160%' or '1.6em')
     * @param {string} [style.fontStyle='normal'] - The font style ('normal', 'italic' or 'oblique')
     * @param {string} [style.fontVariant='normal'] - The font variant ('normal' or 'small-caps')
     * @param {string} [style.fontWeight='normal'] - The font weight ('normal', 'bold', 'bolder', 'lighter' and '100',
     *  '200', '300', '400', '500', '600', '700', 800' or '900')
     * @param {number} [style.letterSpacing=0] - The amount of spacing between letters, default is 0
     * @param {number} [style.lineHeight] - The line height, a number that represents the vertical space that a letter uses
     * @param {string} [style.lineJoin='miter'] - The lineJoin property sets the type of corner created, it can resolve
     *      spiked text issues. Default is 'miter' (creates a sharp corner).
     * @param {number} [style.miterLimit=10] - The miter limit to use when using the 'miter' lineJoin mode. This can reduce
     *      or increase the spikiness of rendered text.
     * @param {number} [style.padding=0] - Occasionally some fonts are cropped. Adding some padding will prevent this from
     *     happening by adding padding to all sides of the text.
     * @param {string|number} [style.stroke='black'] - A canvas fillstyle that will be used on the text stroke
     *  e.g 'blue', '#FCFF00'
     * @param {number} [style.strokeThickness=0] - A number that represents the thickness of the stroke.
     *  Default is 0 (no stroke)
     * @param {boolean} [style.trim=false] - Trim transparent borders
     * @param {string} [style.textBaseline='alphabetic'] - The baseline of the text that is rendered.
     * @param {boolean} [style.wordWrap=false] - Indicates if word wrap should be used
     * @param {number} [style.wordWrapWidth=100] - The width at which text will wrap, it needs wordWrap to be set to true
     */
    constructor(style)
    {
        this.styleID = 0;

        Object.assign(this, defaultStyle, style);
    }

    /**
     * Creates a new TextStyle object with the same values as this one.
     * Note that the only the properties of the object are cloned.
     *
     * @return {PIXI.TextStyle} New cloned TextStyle object
     */
    clone()
    {
        const clonedProperties = {};

        for (const key in defaultStyle)
        {
            clonedProperties[key] = this[key];
        }

        return new TextStyle(clonedProperties);
    }

    /**
     * Resets all properties to the defaults specified in TextStyle.prototype._default
     */
    reset()
    {
        Object.assign(this, defaultStyle);
    }

    get align()
    {
        return this._align;
    }
    set align(align)
    {
        if (this._align !== align)
        {
            this._align = align;
            this.styleID++;
        }
    }

    get breakWords()
    {
        return this._breakWords;
    }
    set breakWords(breakWords)
    {
        if (this._breakWords !== breakWords)
        {
            this._breakWords = breakWords;
            this.styleID++;
        }
    }

    get dropShadow()
    {
        return this._dropShadow;
    }
    set dropShadow(dropShadow)
    {
        if (this._dropShadow !== dropShadow)
        {
            this._dropShadow = dropShadow;
            this.styleID++;
        }
    }

    get dropShadowAlpha()
    {
        return this._dropShadowAlpha;
    }
    set dropShadowAlpha(dropShadowAlpha)
    {
        if (this._dropShadowAlpha !== dropShadowAlpha)
        {
            this._dropShadowAlpha = dropShadowAlpha;
            this.styleID++;
        }
    }

    get dropShadowAngle()
    {
        return this._dropShadowAngle;
    }
    set dropShadowAngle(dropShadowAngle)
    {
        if (this._dropShadowAngle !== dropShadowAngle)
        {
            this._dropShadowAngle = dropShadowAngle;
            this.styleID++;
        }
    }

    get dropShadowBlur()
    {
        return this._dropShadowBlur;
    }
    set dropShadowBlur(dropShadowBlur)
    {
        if (this._dropShadowBlur !== dropShadowBlur)
        {
            this._dropShadowBlur = dropShadowBlur;
            this.styleID++;
        }
    }

    get dropShadowColor()
    {
        return this._dropShadowColor;
    }
    set dropShadowColor(dropShadowColor)
    {
        const outputColor = getColor(dropShadowColor);
        if (this._dropShadowColor !== outputColor)
        {
            this._dropShadowColor = outputColor;
            this.styleID++;
        }
    }

    get dropShadowDistance()
    {
        return this._dropShadowDistance;
    }
    set dropShadowDistance(dropShadowDistance)
    {
        if (this._dropShadowDistance !== dropShadowDistance)
        {
            this._dropShadowDistance = dropShadowDistance;
            this.styleID++;
        }
    }

    get fill()
    {
        return this._fill;
    }
    set fill(fill)
    {
        const outputColor = getColor(fill);
        if (this._fill !== outputColor)
        {
            this._fill = outputColor;
            this.styleID++;
        }
    }

    get fillGradientType()
    {
        return this._fillGradientType;
    }
    set fillGradientType(fillGradientType)
    {
        if (this._fillGradientType !== fillGradientType)
        {
            this._fillGradientType = fillGradientType;
            this.styleID++;
        }
    }

    get fillGradientStops()
    {
        return this._fillGradientStops;
    }
    set fillGradientStops(fillGradientStops)
    {
        if (!areArraysEqual(this._fillGradientStops,fillGradientStops))
        {
            this._fillGradientStops = fillGradientStops;
            this.styleID++;
        }
    }

    get fontFamily()
    {
        return this._fontFamily;
    }
    set fontFamily(fontFamily)
    {
        if (this.fontFamily !== fontFamily)
        {
            this._fontFamily = fontFamily;
            this.styleID++;
        }
    }

    get fontSize()
    {
        return this._fontSize;
    }
    set fontSize(fontSize)
    {
        if (this._fontSize !== fontSize)
        {
            this._fontSize = fontSize;
            this.styleID++;
        }
    }

    get fontStyle()
    {
        return this._fontStyle;
    }
    set fontStyle(fontStyle)
    {
        if (this._fontStyle !== fontStyle)
        {
            this._fontStyle = fontStyle;
            this.styleID++;
        }
    }

    get fontVariant()
    {
        return this._fontVariant;
    }
    set fontVariant(fontVariant)
    {
        if (this._fontVariant !== fontVariant)
        {
            this._fontVariant = fontVariant;
            this.styleID++;
        }
    }

    get fontWeight()
    {
        return this._fontWeight;
    }
    set fontWeight(fontWeight)
    {
        if (this._fontWeight !== fontWeight)
        {
            this._fontWeight = fontWeight;
            this.styleID++;
        }
    }

    get letterSpacing()
    {
        return this._letterSpacing;
    }
    set letterSpacing(letterSpacing)
    {
        if (this._letterSpacing !== letterSpacing)
        {
            this._letterSpacing = letterSpacing;
            this.styleID++;
        }
    }

    get lineHeight()
    {
        return this._lineHeight;
    }
    set lineHeight(lineHeight)
    {
        if (this._lineHeight !== lineHeight)
        {
            this._lineHeight = lineHeight;
            this.styleID++;
        }
    }

    get lineJoin()
    {
        return this._lineJoin;
    }
    set lineJoin(lineJoin)
    {
        if (this._lineJoin !== lineJoin)
        {
            this._lineJoin = lineJoin;
            this.styleID++;
        }
    }

    get miterLimit()
    {
        return this._miterLimit;
    }
    set miterLimit(miterLimit)
    {
        if (this._miterLimit !== miterLimit)
        {
            this._miterLimit = miterLimit;
            this.styleID++;
        }
    }

    get padding()
    {
        return this._padding;
    }
    set padding(padding)
    {
        if (this._padding !== padding)
        {
            this._padding = padding;
            this.styleID++;
        }
    }

    get stroke()
    {
        return this._stroke;
    }
    set stroke(stroke)
    {
        const outputColor = getColor(stroke);
        if (this._stroke !== outputColor)
        {
            this._stroke = outputColor;
            this.styleID++;
        }
    }

    get strokeThickness()
    {
        return this._strokeThickness;
    }
    set strokeThickness(strokeThickness)
    {
        if (this._strokeThickness !== strokeThickness)
        {
            this._strokeThickness = strokeThickness;
            this.styleID++;
        }
    }

    get textBaseline()
    {
        return this._textBaseline;
    }
    set textBaseline(textBaseline)
    {
        if (this._textBaseline !== textBaseline)
        {
            this._textBaseline = textBaseline;
            this.styleID++;
        }
    }

    get trim()
    {
        return this._trim;
    }
    set trim(trim)
    {
        if (this._trim !== trim)
        {
            this._trim = trim;
            this.styleID++;
        }
    }

    get wordWrap()
    {
        return this._wordWrap;
    }
    set wordWrap(wordWrap)
    {
        if (this._wordWrap !== wordWrap)
        {
            this._wordWrap = wordWrap;
            this.styleID++;
        }
    }

    get wordWrapWidth()
    {
        return this._wordWrapWidth;
    }
    set wordWrapWidth(wordWrapWidth)
    {
        if (this._wordWrapWidth !== wordWrapWidth)
        {
            this._wordWrapWidth = wordWrapWidth;
            this.styleID++;
        }
    }

    /**
     * Generates a font style string to use for `TextMetrics.measureFont()`.
     *
     * @return {string} Font style string, for passing to `TextMetrics.measureFont()`
     */
    toFontString()
    {
        // build canvas api font setting from individual components. Convert a numeric this.fontSize to px
        const fontSizeString = (typeof this.fontSize === 'number') ? `${this.fontSize}px` : this.fontSize;

        // Clean-up fontFamily property by quoting each font name
        // this will support font names with spaces
        let fontFamilies = this.fontFamily;

        if (!Array.isArray(this.fontFamily))
        {
            fontFamilies = this.fontFamily.split(',');
        }

        for (let i = fontFamilies.length - 1; i >= 0; i--)
        {
            // Trim any extra white-space
            let fontFamily = fontFamilies[i].trim();

            // Check if font already contains strings
            if (!(/([\"\'])[^\'\"]+\1/).test(fontFamily))
            {
                fontFamily = `"${fontFamily}"`;
            }
            fontFamilies[i] = fontFamily;
        }

        return `${this.fontStyle} ${this.fontVariant} ${this.fontWeight} ${fontSizeString} ${fontFamilies.join(',')}`;
    }
}

/**
 * Utility function to convert hexadecimal colors to strings, and simply return the color if it's a string.
 *
 * @param {number|number[]} color
 * @return {string} The color as a string.
 */
function getSingleColor(color)
{
    if (typeof color === 'number')
    {
        return hex2string(color);
    }
    else if ( typeof color === 'string' )
    {
        if ( color.indexOf('0x') === 0 )
        {
            color = color.replace('0x', '#');
        }
    }

    return color;
}

/**
 * Utility function to convert hexadecimal colors to strings, and simply return the color if it's a string.
 * This version can also convert array of colors
 *
 * @param {number|number[]} color
 * @return {string} The color as a string.
 */
function getColor(color)
{
    if (!Array.isArray(color))
    {
        return getSingleColor(color);
    }
    else
    {
        for (let i = 0; i < color.length; ++i)
        {
            color[i] = getSingleColor(color[i]);
        }

        return color;
    }
}

/**
 * Utility function to convert hexadecimal colors to strings, and simply return the color if it's a string.
 * This version can also convert array of colors
 *
 * @param {Array} array1 First array to compare
 * @param {Array} array2 Second array to compare
 * @return {boolean} Do the arrays contain the same values in the same order
 */
function areArraysEqual(array1, array2)
{
    if (!Array.isArray(array1) || !Array.isArray(array2))
    {
        return false;
    }

    if (array1.length !== array2.length)
    {
        return false;
    }

    for (let i = 0; i < array1.length; ++i)
    {
        if (array1[i] !== array2[i])
        {
            return false;
        }
    }

    return true;
}
<|endoftext|>


<commit_before>export default {};
<commit_msg>add keyboard<commit_after>export default {
  Enter: '\r',
  Left: '\x1b[D',
  Right: '\x1b[C',
  Up: '\x1b[A',
  Down: '\x1b[B',
  PgUp: '\x1b[5~',
  PgDown: '\x1b[6~',
  Home: '\x1b[1~',
  End: '\x1b[4~',
  CtrlZ: '\x1a',
  CtrlP: '\x10',
};
<|endoftext|>


<commit_before>var date = require('./date.js');
var api = require('./api.js');

module.exports = {
  getTrainID: function(trainNumber) {
    var response = api.callApi("/searchTrains?trainno=" + trainNumber);
    if(response.length != 0)
    return response[0].id;
    else
    return null;
  },
  getTrainStatus: function(trainID, day) {
    var msg;
    if(trainID) {
      var response = api.callApi("/getRunningStatus/trainno/" + trainID + "/date/" + date.getDate(day));
      if (response.errorMsg) {
        return response.errorMsg;
      }
      let currentStation = response.markers.filter(station => station.status === 'current');
      let stationCode, stationDisplayed, delay;
      let trainName = response.details.name;
      let trainNo = response.details.number;
      console.log(trainNo);
      let alertMessage = response.train.alertMsg;
      if(currentStation.length != 0) {
        stationDisplayed = currentStation[0].title.split('(')[0];
        stationCode = currentStation[0].title.split('(')[1].slice(0, -1);
        delay = response.stations.filter(station => station.source_code === stationCode)[0].delay_arrival;
        console.log(stationCode);
      }
      else {
        stationDisplayed = response.stations[response.stations.length-1].source_name;
        delay = response.stations[response.stations.length-1].delay_arrival;
      }
      if(delay < 0 && Math.abs(delay)>=60) {
        delay = 24*60 - Math.abs(delay);
      }
      if(delay <= 0){
        msg = 'Train ' +  trainName + '        is currently at ' + stationDisplayed + ' station, and is right on time.';
      } else if(delay < 60) {
        msg = 'Train ' +  trainName + '        is currently at ' + stationDisplayed + ' station, and is late by ' + delay + ' minutes';
      } else {
        msg = 'Train ' +  trainName + '        is currently at ' + stationDisplayed + ' station, and is late by ' + parseInt(delay/60) + ' hours and ' + delay%60 + ' minutes';
      }
      if(alertMessage)
        msg = alertMessage;
    }
    else {
      msg = 'Sorry, no train found by this train number. Please try again.';
    }
    return msg;
  }
};
<commit_msg>Updating train status message (#12)<commit_after>var date = require('./date.js');
var api = require('./api.js');

module.exports = {
  getTrainID: function(trainNumber) {
    var response = api.callApi("/searchTrains?trainno=" + trainNumber);
    if(response.length != 0)
    return response[0].id;
    else
    return null;
  },
  getTrainStatus: function(trainID, day) {
    var msg;
    if(trainID) {
      var response = api.callApi("/getRunningStatus/trainno/" + trainID + "/date/" + date.getDate(day));
      if (response.errorMsg) {
        return response.errorMsg;
      }
      let currentStation = response.markers.filter(station => station.status === 'current');
      let stationCode, stationDisplayed, delay;
      let trainName = response.details.name;
      let trainNo = response.details.number;
      console.log(trainNo);
      let alertMessage = response.train.alertMsg;
      if(currentStation.length != 0) {
        stationDisplayed = currentStation[0].title.split('(')[0];
        stationCode = currentStation[0].title.split('(')[1].slice(0, -1);
        delay = response.stations.filter(station => station.source_code === stationCode)[0].delay_arrival;
        console.log(stationCode);
      }
      else {
        stationDisplayed = response.stations[response.stations.length-1].source_name;
        delay = response.stations[response.stations.length-1].delay_arrival;
      }
      if(delay < 0 && Math.abs(delay)>=60) {
        delay = 24*60 - Math.abs(delay);
      }
      if(delay <= 0){
        msg = 'Train ' +  trainName + ' is currently at ' + stationDisplayed + ' station, and is right on time.';
      } else if(delay < 60) {
        msg = 'Train ' +  trainName + ' is currently at ' + stationDisplayed + ' station, and is late by ' + delay + ' minutes';
      } else {
        let hours = parseInt(delay/60);
        let minutes = delay%60;
        if(hours == 1)
          msg = 'Train ' +  trainName + ' is currently at ' + stationDisplayed + ' station, and is late by 1 hour and ' + minutes + ' minutes';
        else
          msg = 'Train ' +  trainName + ' is currently at ' + stationDisplayed + ' station, and is late by ' + hours + ' hours and ' + minutes + ' minutes';
      }
      if(alertMessage)
        msg = alertMessage;
    }
    else {
      msg = 'Sorry, no train found by this train number. Please try again.';
    }
    return msg;
  }
};
<|endoftext|>


<commit_before>const xmlbuilder = require('xmlbuilder')

const { addBackground, byteArray, int2ByteString, polar, reverse } = require('./util')

const generate = ({ hash, size = 4, width = 128, segments = Infinity, symmetricAxisAngle, background }) => {
  let bytes = byteArray(hash)
  bytes = reverse(bytes)

  let color = '#' + bytes.slice(bytes.length - 3, bytes.length).map(int2ByteString).join('')
  let segmentWidth = Math.floor(width / ((size * 2) + 1))

  let svg = xmlbuilder.create('svg')
  svg.att('width', width)
  svg.att('height', width)
  svg.att('xmlns', 'http://www.w3.org/2000/svg')

  if (background) {
    addBackground(svg, {
      color: '#EEEEEE',
      width: width,
      rx: 0,
      ...background
    })
  }

  svg.ele('circle', {
    cx: width / 2,
    cy: width / 2,
    r: segmentWidth + 1,
    fill: color
  })

  for (let i = 1; i < size; ++i) {
    let theta1 = floorToCongruent(360 * (bytes[(i * 2) + 0] / 0xFF), 360 / segments)
    let theta2 = floorToCongruent(360 * (bytes[(i * 2) + 1] / 0xFF), 360 / segments)

    if(theta2 < theta1){
      let temp = theta1
      theta1 = theta2
      theta2 = temp
    }

    svg.ele('path', {
      d: buildArc(width / 2, width / 2, segmentWidth * i, segmentWidth * (i + 1) + 1, theta1, theta2),
      fill: color
    })

    if (symmetricAxisAngle !== undefined) {
      svg.ele('path', {
        d: buildArc(width / 2, width / 2, segmentWidth * i, segmentWidth * (i + 1) + 1, (symmetricAxisAngle * 2) - theta2, (symmetricAxisAngle * 2) - theta1),
        fill: color
      })
    }
  }

  return svg.end({ pretty: false })
}

/**
 * Generates the path attribute for an arc given a shell and two angles.
 *
 * @param {number} shell - The shell to draw the arc for (affects radii).
 * @param {number} theta1 - The starting angle for drawing the arc (0, 360) (degrees).
 * @param {number} theta2 - The end angle for drawing the arc, theta2 > theta1 (0, 360) (degrees).
 * @returns {string} Value of the d attribute for the path element.
 */
const buildArc = (cx, cy, r1, r2, theta1, theta2) => {
  let largeArcFlag = (theta2 - theta1) < 180 ? 0 : 1

  let points = [polar(r2, theta1), polar(r2, theta2), polar(r1, theta2), polar(r1, theta1)]

  return `M ${cx + points[0].x} ${cy + points[0].y} ` +
         `A ${r2} ${r2} 0 ${largeArcFlag} 1 ${cx + points[1].x} ${cy + points[1].y} ` +
         `L ${cx + points[2].x} ${cy + points[2].y} ` +
         `A ${r1} ${r1} 0 ${largeArcFlag} 0 ${cx + points[3].x} ${cy + points[3].y} ` +
         'Z'
}

/**
 * Reduces a number to a multiple of some other number. Like generalized integer division.
 *
 * @private
 * @param {number} n - The number to floor.
 * @param {number} m - The number to floor to a multiple of.
 * @returns {number} Multiple of m closest to but not greater than n.
 */
const floorToCongruent = (n, m) => {
  if (!m) {
    return n
  }

  n /= m
  n = Math.floor(n)
  return n * m
}

module.exports = generate
<commit_msg>Update circle.svg<commit_after>const xmlbuilder = require('xmlbuilder')

const { addBackground, byteArray, int2ByteString, polar, reverse } = require('./util')

const generate = ({ hash, size = 4, width = 128, segments = Infinity, symmetricAxisAngle, background }) => {
  let bytes = byteArray(hash)
  bytes = reverse(bytes)

  let color = '#' + bytes.slice(bytes.length - 3, bytes.length).map(int2ByteString).join('')
  let segmentWidth = Math.floor(width / ((size * 2) + 1))

  let svg = xmlbuilder.create('svg')
  svg.att('width', width)
  svg.att('height', width)
  svg.att('viewBox', `0 0 ${width} ${width}`)
  svg.att('preserveAspectRatio', 'xMinYMin')
  svg.att('xmlns', 'http://www.w3.org/2000/svg')

  if (background) {
    addBackground(svg, {
      color: '#EEEEEE',
      width: width,
      rx: 0,
      ...background
    })
  }

  svg.ele('circle', {
    cx: width / 2,
    cy: width / 2,
    r: segmentWidth + 1,
    fill: color
  })

  for (let i = 1; i < size; ++i) {
    let theta1 = floorToCongruent(360 * (bytes[(i * 2) + 0] / 0xFF), 360 / segments)
    let theta2 = floorToCongruent(360 * (bytes[(i * 2) + 1] / 0xFF), 360 / segments)

    if(theta2 < theta1){
      let temp = theta1
      theta1 = theta2
      theta2 = temp
    }

    svg.ele('path', {
      d: buildArc(width / 2, width / 2, segmentWidth * i, segmentWidth * (i + 1) + 1, theta1, theta2),
      fill: color
    })

    if (symmetricAxisAngle !== undefined) {
      svg.ele('path', {
        d: buildArc(width / 2, width / 2, segmentWidth * i, segmentWidth * (i + 1) + 1, (symmetricAxisAngle * 2) - theta2, (symmetricAxisAngle * 2) - theta1),
        fill: color
      })
    }
  }

  return svg.end({ pretty: false })
}

/**
 * Generates the path attribute for an arc given a shell and two angles.
 *
 * @param {number} shell - The shell to draw the arc for (affects radii).
 * @param {number} theta1 - The starting angle for drawing the arc (0, 360) (degrees).
 * @param {number} theta2 - The end angle for drawing the arc, theta2 > theta1 (0, 360) (degrees).
 * @returns {string} Value of the d attribute for the path element.
 */
const buildArc = (cx, cy, r1, r2, theta1, theta2) => {
  let largeArcFlag = (theta2 - theta1) < 180 ? 0 : 1

  let points = [polar(r2, theta1), polar(r2, theta2), polar(r1, theta2), polar(r1, theta1)]

  return `M ${cx + points[0].x} ${cy + points[0].y} ` +
         `A ${r2} ${r2} 0 ${largeArcFlag} 1 ${cx + points[1].x} ${cy + points[1].y} ` +
         `L ${cx + points[2].x} ${cy + points[2].y} ` +
         `A ${r1} ${r1} 0 ${largeArcFlag} 0 ${cx + points[3].x} ${cy + points[3].y} ` +
         'Z'
}

/**
 * Reduces a number to a multiple of some other number. Like generalized integer division.
 *
 * @private
 * @param {number} n - The number to floor.
 * @param {number} m - The number to floor to a multiple of.
 * @returns {number} Multiple of m closest to but not greater than n.
 */
const floorToCongruent = (n, m) => {
  if (!m) {
    return n
  }

  n /= m
  n = Math.floor(n)
  return n * m
}

module.exports = generate
<|endoftext|>


<commit_before>// Unit test for a.state.protocol (plugin)
module('plugin/state.protocol.js');

// The protocol tester does work as expected
test('a.state.protocol-tester', function() {
    var simple    = 'superhash',
        prefix    = 'url://anotherone',
        uri       = 'uri://someuri',
        something = 'something://some';

    // They must all response that url is the good choice
    strictEqual(a.state.protocol.tester(simple), 'url');
    strictEqual(a.state.protocol.tester(prefix), 'url');
    strictEqual(a.state.protocol.tester(uri), 'uri');
    strictEqual(a.state.protocol.tester(something), null);
});

// Simple test url does work
test('a.state.protocol-url', function() {
    var simple = {
        hash: [
            'superhash'
        ]
    };

    var prefix = {
        hash: [
            'url://anotherone'
        ]
    };

    var none = {

    };

    // They must all response that url is the good choice
    var url = a.state.protocol.get('url');

    strictEqual(url.fn(simple, 0), 'superhash');
    strictEqual(url.fn(prefix, 0), 'anotherone');
    strictEqual(url.fn(none, 0), null);
});

// Test the specific uri protocol
test('a.state.protocol-uri', function() {

    // First test
    var simple = {
        hash: [
            'uri://superuri'
        ],

        parent: {
            hash: [
                'uri://ok'
            ]
        }
    };

    var uri = a.state.protocol.get('uri');

    strictEqual(uri.fn(simple, 0), 'ok/superuri');

    // Second test
    var lessSimple = {
        hash: [
            'uri://superuri'
        ],

        parent: {
            hash: [
                'uri://ok'
            ],

            parent: {
                hash: [
                    'uri://another'
                ]
            }
        }
    };

    strictEqual(uri.fn(lessSimple, 0), 'another/ok/superuri');

    // Third test
    var complex = {
        hash: [
            'uri://superuri'
        ],

        parent: {
            // This one is in url, it should stop here
            hash: [
                'superb'
            ],

            parent: {
                hash: [
                    'uri://if-you-see-it-its-problem'
                ]
            }
        }
    };

    strictEqual(uri.fn(complex, 0), 'superb/superuri');
});<commit_msg>Adding protocol unit test with multiple input.<commit_after>// Unit test for a.state.protocol (plugin)
module('plugin/state.protocol.js');

// The protocol tester does work as expected
test('a.state.protocol-tester', function() {
    var simple    = 'superhash',
        prefix    = 'url://anotherone',
        uri       = 'uri://someuri',
        something = 'something://some';

    // They must all response that url is the good choice
    strictEqual(a.state.protocol.tester(simple), 'url');
    strictEqual(a.state.protocol.tester(prefix), 'url');
    strictEqual(a.state.protocol.tester(uri), 'uri');
    strictEqual(a.state.protocol.tester(something), null);
});

// Simple test url does work
test('a.state.protocol-url', function() {
    var simple = {
        hash: [
            'superhash'
        ]
    };

    var prefix = {
        hash: [
            'url://anotherone'
        ]
    };

    var none = {

    };

    // They must all response that url is the good choice
    var url = a.state.protocol.get('url');

    strictEqual(url.fn(simple, 0), 'superhash');
    strictEqual(url.fn(prefix, 0), 'anotherone');
    strictEqual(url.fn(none, 0), null);
});

// Simple test url does work
test('a.state.protocol-url-multi', function() {
    var simple = {
        hash: [
            'superhash',
            'second'
        ]
    };

    var prefix = {
        hash: [
            'url://anotherone',
            'url://anotherone2'
        ]
    };

    var none = {

    };

    // They must all response that url is the good choice
    var url = a.state.protocol.get('url');

    strictEqual(url.fn(simple, 0), 'superhash');
    strictEqual(url.fn(simple, 1), 'second');
    strictEqual(url.fn(prefix, 0), 'anotherone');
    strictEqual(url.fn(prefix, 1), 'anotherone2');
    strictEqual(url.fn(none, 0), null);
});

// Test the specific uri protocol
test('a.state.protocol-uri', function() {

    // First test
    var simple = {
        hash: [
            'uri://superuri'
        ],

        parent: {
            hash: [
                'uri://ok'
            ]
        }
    };

    var uri = a.state.protocol.get('uri');

    strictEqual(uri.fn(simple, 0), 'ok/superuri');

    // Second test
    var lessSimple = {
        hash: [
            'uri://superuri'
        ],

        parent: {
            hash: [
                'uri://ok'
            ],

            parent: {
                hash: [
                    'uri://another'
                ]
            }
        }
    };

    strictEqual(uri.fn(lessSimple, 0), 'another/ok/superuri');

    // Third test
    var complex = {
        hash: [
            'uri://superuri'
        ],

        parent: {
            // This one is in url, it should stop here
            hash: [
                'superb'
            ],

            parent: {
                hash: [
                    'uri://if-you-see-it-its-problem'
                ]
            }
        }
    };

    strictEqual(uri.fn(complex, 0), 'superb/superuri');
});

// Test the specific uri protocol
test('a.state.protocol-uri-multiple', function() {

    // First test
    var simple = {
        hash: [
            'uri://superuri',
            'uri://superuri2'
        ],

        parent: {
            hash: [
                'uri://ok',
                'uri://ok2'
            ]
        }
    };

    var uri = a.state.protocol.get('uri');

    strictEqual(uri.fn(simple, 0), 'ok/superuri');
    strictEqual(uri.fn(simple, 1), 'ok/superuri2');

    // Second test
    var lessSimple = {
        hash: [
            'uri://superuri',
            'uri://superuri2'
        ],

        parent: {
            hash: [
                'uri://ok',
                'uri://ok2'
            ],

            parent: {
                hash: [
                    'uri://another',
                    'uri://another2'
                ]
            }
        }
    };

    strictEqual(uri.fn(lessSimple, 0), 'another/ok/superuri');
    strictEqual(uri.fn(lessSimple, 1), 'another/ok/superuri2');

    // Third test
    var complex = {
        hash: [
            'uri://superuri',
            'uri://superuri2'
        ],

        parent: {
            // This one is in url, it should stop here
            hash: [
                'superb',
                'superb2'
            ],

            parent: {
                hash: [
                    'uri://if-you-see-it-its-problem',
                    'uri://if-you-see-it-its-problem2'
                ]
            }
        }
    };

    strictEqual(uri.fn(complex, 0), 'superb/superuri');
    strictEqual(uri.fn(complex, 1), 'superb/superuri2');
});<|endoftext|>


<commit_before>(function() {
	var getNSpaces = function(n) {
		var sArr = [];
		for (; n > 0; n--) {
			sArr[n] = "";
		}
		return sArr.join(" ");
	};

	window.benjbeautify = {
		bean : function(textToFormat) {
			var out = '';
			var deep = 0;
			for(var i = 0; i < textToFormat.length; i++) {
				var letter = textToFormat[i];
				if(letter === '(') {
					deep++;
					out += '(\n' + getNSpaces(deep);
				}
				else if(letter === ')') {
					deep--;
					out += '\n' + getNSpaces(deep) + ')';
				} else if(letter === ','){
					out += ',\n' + getNSpaces(deep);
				} else {
					out += letter;
				}
			}
			return out;
		}
	};
})();
<commit_msg>fix : bean formatting<commit_after>(function() {
	var getNSpaces = function(n) {
		var sArr = [];
		for (; n > 0; n--) {
			sArr[n] = "";
		}
		return sArr.join(" ");
	};

	window.benjbeautify = {
		bean : function(textToFormat) {
			var out = '';
			var deep = 0;
			for(var i = 0; i < textToFormat.length; i++) {
				var letter = textToFormat[i];
				if(letter === '(') {
					deep++;
					out += '(\n' + getNSpaces(deep);
				} else if(letter === ')') {
					deep--;
					out += '\n' + getNSpaces(deep) + ')';
				} else if(letter === ','){
					out += ',\n' + getNSpaces(deep);
				} else if(!letter.match(/\s/)) {
					out += letter;
				}
			}
			return out;
		}
	};
})();
<|endoftext|>


<commit_before>module.exports = function (sequelize, DataTypes) {
  const attributes = {
    githubId: DataTypes.STRING,
    email: {
      type: DataTypes.STRING,
      validate: {
        isEmail: true,
      },
    },
    avatar: DataTypes.STRING,

    lastLogin: DataTypes.DATE,
    lastIp: {
      type: DataTypes.STRING,
      validate: {
        isIP: true,
      },
    },
  };

  const options = {};
  options.classMethods = {
    associate: function (models) {
      User.hasMany(models.Failure);
    }
  };

  const User = sequelize.define('User', attributes, options);

  return User;
};
<commit_msg>add username and oauth tokens on user model<commit_after>module.exports = function (sequelize, DataTypes) {
  const attributes = {
    githubId: DataTypes.STRING,
    username: DataTypes.STRING,
    accessToken: DataTypes.STRING,
    refreshToken: DataTypes.STRING,
    email: {
      type: DataTypes.STRING,
      validate: {
        isEmail: true,
      },
    },
    avatar: DataTypes.STRING,

    lastLogin: DataTypes.DATE,
    lastIp: {
      type: DataTypes.STRING,
      validate: {
        isIP: true,
      },
    },
  };

  const options = {};
  options.classMethods = {
    associate: function (models) {
      User.hasMany(models.Failure);
    }
  };

  const User = sequelize.define('User', attributes, options);

  return User;
};
<|endoftext|>