<commit_before>
module.exports = exports = search

var npm = require("../npm")
  , registry = require("./utils/npm-registry-client")
  , semver = require("semver")
  , output
  , log = require("./utils/log")

search.usage = "npm search [some search terms ...]"

search.completion = function (args, index, cb) {
  var compl = []
    , getCompletions = require("./utils/completion/get-completions")
    , name = (args.length + 1 === index) && args[args.length - 1] || ""
    , priors = name ? args.slice(0, args.length - 1) : args
  // get the batch of data that matches so far.
  // this is an example of using npm.commands.search programmatically
  // to fetch data that has been filtered by a set of arguments.
  search(priors, true, 3600, function (er, data) {
    if (er) return cb(er)
    Object.keys(data).forEach(function (name) {
      compl.push.apply(compl, [name].concat(data[name].words))
    })
    var last = null
    compl = compl.sort(strcmp).filter(function (c) {
      if (args.indexOf(c) !== -1) return false
      var r = c !== last
      last = c
      return r
    })
    var matches = getCompletions(name || "", compl)
    if (name && !name.match(/^=/)) {
      matches = matches.concat(getCompletions("="+name, compl))
    }
    cb(null, matches)
  })
}

function search (args, silent, staleness, cb_) {
  if (typeof cb_ !== "function") cb_ = staleness, staleness = 600
  if (typeof cb_ !== "function") cb_ = silent, silent = false
  var searchopts = npm.config.get("searchopts")
    , searchexclude = npm.config.get("searchexclude")
  if (typeof searchopts !== "string") searchopts = ""
  searchopts = searchopts.split(/\s+/)
  if (typeof searchexclude === "string") {
    searchexclude = searchexclude.split(/\s+/)
  } else searchexclude = []
  var opts = searchopts.concat(args).map(function (s) {
    return s.toLowerCase()
  }).filter(function (s) { return s })
  searchexclude = searchexclude.map(function (s) {
    return s.toLowerCase()
  })
  getFilteredData( staleness, opts, searchexclude, function (er, data) {
    // now data is the list of data that we want to show.
    // prettify and print it, and then provide the raw
    // data to the cb.
    if (er) return cb_(er)
    function cb (er) { return cb_(er, data) }
    if (silent) return cb()
    output = output || require("./utils/output")
    output.write(prettify(data, args), cb)
  })
}

function getFilteredData (staleness, args, notArgs, cb) {
  registry.get("/-/all", null, staleness, function (er, data) {
    if (er) return cb(er)
    return cb(null, filter(data, args, notArgs))
  })
}

function filter (data, args, notArgs) {
  // data={<name>:{package data}}
  return Object.keys(data).map(function (d) {
    return data[d]
  }).map(stripData).map(getWords).filter(function (data) {
    return filterWords(data, args, notArgs)
  }).reduce(function (l, r) {
    l[r.name] = r
    return l
  }, {})
}

function stripData (data) {
  return { name:data.name
         , description:npm.config.get("description") ? data.description : ""
         , maintainers:data.maintainers.map(function (m) {
             return "=" + m.name
           })
         , url:!Object.keys(data.versions).length ? data.url : null
         , keywords:data.keywords || []
         }
}

function getWords (data) {
  data.words = [ data.name ]
               .concat(data.description)
               .concat(data.maintainers)
               .concat(data.url && ("<" + data.url + ">"))
               .concat(data.keywords)
               .map(function (f) { return f && f.trim && f.trim() })
               .filter(function (f) { return f })
               .join(" ")
               .toLowerCase()
  return data
}

function filterWords (data, args, notArgs) {
  var words = data.words
  for (var i = 0, l = args.length; i < l; i ++) {
    if (words.indexOf(args[i]) === -1) {
      return false
    }
  }
  for (var i = 0, l = notArgs.length; i < l; i ++) {
    if (words.indexOf(notArgs[i]) !== -1) return false
  }
  return true
}

function prettify (data, args) {
  try {
    var stdio = process.binding("stdio")
      , cols = stdio.isatty(stdio.stdoutFD) ?
        ( stdio.getColumns ? stdio.getColumns()
        : stdio.getWindowSize ? stdio.getWindowSize()[1]
        : Infinity )
        : Infinity
  } catch (ex) { cols = Infinity }

  // name, desc, author, keywords
  var longest = []
    , spaces
    , maxLen = [20, 60, 20]
  return Object.keys(data).map(function (d) {
    return data[d]
  }).map(function (data) {
    // turn a pkg data into a string
    // [name,who,desc,targets,keywords] tuple
    // also set longest to the longest name
    if (typeof data.keywords === "string") {
      data.keywords = data.keywords.split(/[,\s]+/)
    }
    if (!Array.isArray(data.keywords)) data.keywords = []
    var l = [ data.name
            , data.description || ""
            , data.maintainers.join(" ")
            , (data.keywords || []).join(" ")
            ]
    l.forEach(function (s, i) {
      var len = s.length
      longest[i] = Math.min(maxLen[i] || Infinity
                           ,Math.max(longest[i] || 0, len))
      if (len > longest[i]) {
        l._undent = l._undent || []
        l._undent[i] = len - longest[i]
      }
      l[i] = l[i].replace(/\s+/g, " ")
    })
    return l
  }).map(function (line) {
    return line.map(function (s, i) {
      spaces = spaces || longest.map(function (n) {
        return new Array(n + 2).join(" ")
      })
      var len = s.length
      if (line._undent && line._undent[i - 1]) {
        len += line._undent[i - 1] - 1
      }
      return s + spaces[i].substr(len)
    }).join(" ").substr(0, cols)
  }).sort(function (a, b) {
    return a === b ? 0 : a > b ? 1 : -1
  }).map(function (line) {
    // colorize!
    args.forEach(function (arg, i) {
      line = addColorMarker(line, arg, i)
    })
    return colorize(line)
  }).join("\n")
}

var colors = [31, 33, 32, 36, 34, 35 ]
  , cl = colors.length
function addColorMarker (str, arg, i) {
  var m = i % cl + 1
    , markStart = String.fromCharCode(m)
    , markEnd = String.fromCharCode(0)
    , pieces = str.toLowerCase().split(arg)
    , p = 0
  return pieces.map(function (piece, i) {
    piece = str.substr(p, piece.length)
    var mark = markStart
             + str.substr(p+piece.length, arg.length)
             + markEnd
    p += piece.length + arg.length
    return piece + mark
  }).join("")
  return str.split(arg).join(mark)
}
function colorize (line) {
  for (var i = 0; i < cl; i ++) {
    var m = i + 1
    line = line.split(String.fromCharCode(m)).join("\033["+colors[i]+"m")
  }
  return line.split("\u0000").join("\033[0m")
}
<commit_msg>completion for search<commit_after>
module.exports = exports = search

var npm = require("../npm")
  , registry = require("./utils/npm-registry-client")
  , semver = require("semver")
  , output
  , log = require("./utils/log")

search.usage = "npm search [some search terms ...]"

search.completion = function (opts, cb) {
  var compl = {}
    , partial = opts.partialWord
    , ipartial = partial.toLowerCase()
    , plen = partial.length

  // get the batch of data that matches so far.
  // this is an example of using npm.commands.search programmatically
  // to fetch data that has been filtered by a set of arguments.
  search(opts.conf.argv.remain.slice(2), true, function (er, data) {
    if (er) return cb(er)
    Object.keys(data).forEach(function (name) {
      data[name].words.split(" ").forEach(function (w) {
        if (w.toLowerCase().indexOf(ipartial) === 0) {
          compl[partial + w.substr(plen)] = true
        }
      })
    })
    cb(null, Object.keys(compl))
  })
}

function search (args, silent, staleness, cb_) {
  if (typeof cb_ !== "function") cb_ = staleness, staleness = 600
  if (typeof cb_ !== "function") cb_ = silent, silent = false
  var searchopts = npm.config.get("searchopts")
    , searchexclude = npm.config.get("searchexclude")
  if (typeof searchopts !== "string") searchopts = ""
  searchopts = searchopts.split(/\s+/)
  if (typeof searchexclude === "string") {
    searchexclude = searchexclude.split(/\s+/)
  } else searchexclude = []
  var opts = searchopts.concat(args).map(function (s) {
    return s.toLowerCase()
  }).filter(function (s) { return s })
  searchexclude = searchexclude.map(function (s) {
    return s.toLowerCase()
  })
  getFilteredData( staleness, opts, searchexclude, function (er, data) {
    // now data is the list of data that we want to show.
    // prettify and print it, and then provide the raw
    // data to the cb.
    if (er || silent) return cb_(er, data)
    function cb (er) { return cb_(er, data) }
    output = output || require("./utils/output")
    output.write(prettify(data, args), cb)
  })
}

function getFilteredData (staleness, args, notArgs, cb) {
  registry.get("/-/all", null, staleness, function (er, data) {
    if (er) return cb(er)
    return cb(null, filter(data, args, notArgs))
  })
}

function filter (data, args, notArgs) {
  // data={<name>:{package data}}
  return Object.keys(data).map(function (d) {
    return data[d]
  }).map(stripData).map(getWords).filter(function (data) {
    return filterWords(data, args, notArgs)
  }).reduce(function (l, r) {
    l[r.name] = r
    return l
  }, {})
}

function stripData (data) {
  return { name:data.name
         , description:npm.config.get("description") ? data.description : ""
         , maintainers:data.maintainers.map(function (m) {
             return "=" + m.name
           })
         , url:!Object.keys(data.versions).length ? data.url : null
         , keywords:data.keywords || []
         }
}

function getWords (data) {
  data.words = [ data.name ]
               .concat(data.description)
               .concat(data.maintainers)
               .concat(data.url && ("<" + data.url + ">"))
               .concat(data.keywords)
               .map(function (f) { return f && f.trim && f.trim() })
               .filter(function (f) { return f })
               .join(" ")
               .toLowerCase()
  return data
}

function filterWords (data, args, notArgs) {
  var words = data.words
  for (var i = 0, l = args.length; i < l; i ++) {
    if (words.indexOf(args[i]) === -1) {
      return false
    }
  }
  for (var i = 0, l = notArgs.length; i < l; i ++) {
    if (words.indexOf(notArgs[i]) !== -1) return false
  }
  return true
}

function prettify (data, args) {
  try {
    var stdio = process.binding("stdio")
      , cols = stdio.isatty(stdio.stdoutFD) ?
        ( stdio.getColumns ? stdio.getColumns()
        : stdio.getWindowSize ? stdio.getWindowSize()[1]
        : Infinity )
        : Infinity
  } catch (ex) { cols = Infinity }

  // name, desc, author, keywords
  var longest = []
    , spaces
    , maxLen = [20, 60, 20]
  return Object.keys(data).map(function (d) {
    return data[d]
  }).map(function (data) {
    // turn a pkg data into a string
    // [name,who,desc,targets,keywords] tuple
    // also set longest to the longest name
    if (typeof data.keywords === "string") {
      data.keywords = data.keywords.split(/[,\s]+/)
    }
    if (!Array.isArray(data.keywords)) data.keywords = []
    var l = [ data.name
            , data.description || ""
            , data.maintainers.join(" ")
            , (data.keywords || []).join(" ")
            ]
    l.forEach(function (s, i) {
      var len = s.length
      longest[i] = Math.min(maxLen[i] || Infinity
                           ,Math.max(longest[i] || 0, len))
      if (len > longest[i]) {
        l._undent = l._undent || []
        l._undent[i] = len - longest[i]
      }
      l[i] = l[i].replace(/\s+/g, " ")
    })
    return l
  }).map(function (line) {
    return line.map(function (s, i) {
      spaces = spaces || longest.map(function (n) {
        return new Array(n + 2).join(" ")
      })
      var len = s.length
      if (line._undent && line._undent[i - 1]) {
        len += line._undent[i - 1] - 1
      }
      return s + spaces[i].substr(len)
    }).join(" ").substr(0, cols)
  }).sort(function (a, b) {
    return a === b ? 0 : a > b ? 1 : -1
  }).map(function (line) {
    // colorize!
    args.forEach(function (arg, i) {
      line = addColorMarker(line, arg, i)
    })
    return colorize(line)
  }).join("\n")
}

var colors = [31, 33, 32, 36, 34, 35 ]
  , cl = colors.length
function addColorMarker (str, arg, i) {
  var m = i % cl + 1
    , markStart = String.fromCharCode(m)
    , markEnd = String.fromCharCode(0)
    , pieces = str.toLowerCase().split(arg.toLowerCase())
    , p = 0
  return pieces.map(function (piece, i) {
    piece = str.substr(p, piece.length)
    var mark = markStart
             + str.substr(p+piece.length, arg.length)
             + markEnd
    p += piece.length + arg.length
    return piece + mark
  }).join("")
  return str.split(arg).join(mark)
}
function colorize (line) {
  for (var i = 0; i < cl; i ++) {
    var m = i + 1
    line = line.split(String.fromCharCode(m)).join("\033["+colors[i]+"m")
  }
  return line.split("\u0000").join("\033[0m")
}
<|endoftext|>


<commit_before>import React, { Component } from 'react';
import Dropdown from '../dropdown';
import './price-indicator.css';

const HOUSE_TYPE_PARAM = 'houseType';
const HOUSE_TYPE_APARTMENT = 'Apartment';
const HOUSE_TYPE_HOUSE = 'House';
const HOUSE_TYPE_LIST = [HOUSE_TYPE_APARTMENT, HOUSE_TYPE_HOUSE];

const BEDROOMS_TYPE_PARAM = 'bedrooms';
const STUDIO = 'Studio';
const BEDROOMS_1 = '1 Bedroom';
const BEDROOMS_2 = '2 Bedrooms';
const BEDROOMS_3 = '3 Bedrooms';
const BEDROOMS_4 = '4 Bedrooms';
const BEDROOM_TYPES_LIST = [STUDIO, BEDROOMS_1, BEDROOMS_2, BEDROOMS_3, BEDROOMS_4];

const BATHROOMS_TYPE_PARAM = 'bathrooms';
const BATHROOMS_1 = '1 Bathroom';
const BATHROOMS_2 = '2 Bathrooms';
const BATHROOMS_3 = '3 Bathrooms';
const BATHROOMS_4 = '4 Bathrooms';
const BATHROOMS_5 = '5 Bathrooms';
const BATHROOMS_TYPE_LIST = [BATHROOMS_1, BATHROOMS_2, BATHROOMS_3, BATHROOMS_4, BATHROOMS_5];

const CLEANING_TYPE_PARAM = 'cleaningType';
const REGULAR_CLEANING = 'Regular';
const DEEP_CLEANING = 'Deep clean';
const MOVING_CLEANING = 'Moving in/out';
const CLEANING_TYPE_LIST = [REGULAR_CLEANING, DEEP_CLEANING, MOVING_CLEANING];

class QuoteIndicator extends Component {

  constructor(props) {
    super(props);
    this.state = {
      [HOUSE_TYPE_PARAM]: HOUSE_TYPE_APARTMENT,
      [BEDROOMS_TYPE_PARAM]: BEDROOMS_2,
      [BATHROOMS_TYPE_PARAM]: BATHROOMS_2,
      [CLEANING_TYPE_PARAM]: REGULAR_CLEANING,
      indicativePrice: this.getIndicativePrice({[BEDROOMS_TYPE_PARAM]: BEDROOMS_2, [BATHROOMS_TYPE_PARAM]: BATHROOMS_2, [CLEANING_TYPE_PARAM]: REGULAR_CLEANING})
    }
  }

  housePropertyChangeHandler = (propertyName) => {
    return (value) => {
      this.setState({[propertyName]: value});
      this.setState({indicativePrice: this.getIndicativePrice(Object.assign({}, this.state, {[propertyName]: value}))});
      ga('send', 'event', 'price-indicator', 'property-changed', propertyName, value)
    };
  };

  bookButtonClickHandler = () => {
    ga('send', 'event', 'price-indicator', 'book-now-button', 'click', 'true')
  };

  getIndicativePrice = ({bedrooms, bathrooms, cleaningType}) => {
    const prices = {
      [STUDIO]: {
        [BATHROOMS_1]: {
          [REGULAR_CLEANING]: 55,
          [DEEP_CLEANING]: 100,
          [MOVING_CLEANING]: 300
        },
        [BATHROOMS_2]: {
          [REGULAR_CLEANING]: 65,
          [DEEP_CLEANING]: 120,
          [MOVING_CLEANING]: 350
        },
        [BATHROOMS_3]: {
          [REGULAR_CLEANING]: 75,
          [DEEP_CLEANING]: 140,
          [MOVING_CLEANING]: 400
        },
        [BATHROOMS_4]: {
          [REGULAR_CLEANING]: 85,
          [DEEP_CLEANING]: 150,
          [MOVING_CLEANING]: 450
        },
        [BATHROOMS_5]: {
          [REGULAR_CLEANING]: 95,
          [DEEP_CLEANING]: 160,
          [MOVING_CLEANING]: 500
        }
      },
      [BEDROOMS_1]: {
        [BATHROOMS_1]: {
          [REGULAR_CLEANING]: 60,
          [DEEP_CLEANING]: 120,
          [MOVING_CLEANING]: 400
        },
        [BATHROOMS_2]: {
          [REGULAR_CLEANING]: 70,
          [DEEP_CLEANING]: 140,
          [MOVING_CLEANING]: 450
        },
        [BATHROOMS_3]: {
          [REGULAR_CLEANING]: 80,
          [DEEP_CLEANING]: 160,
          [MOVING_CLEANING]: 500
        },
        [BATHROOMS_4]: {
          [REGULAR_CLEANING]: 90,
          [DEEP_CLEANING]: 180,
          [MOVING_CLEANING]: 550
        },
        [BATHROOMS_5]: {
          [REGULAR_CLEANING]: 100,
          [DEEP_CLEANING]: 200,
          [MOVING_CLEANING]: 600
        }
      },
      [BEDROOMS_2]: {
        [BATHROOMS_1]: {
          [REGULAR_CLEANING]: 70,
          [DEEP_CLEANING]: 150,
          [MOVING_CLEANING]: 450
        },
        [BATHROOMS_2]: {
          [REGULAR_CLEANING]: 85,
          [DEEP_CLEANING]: 180,
          [MOVING_CLEANING]: 500
        },
        [BATHROOMS_3]: {
          [REGULAR_CLEANING]: 100,
          [DEEP_CLEANING]: 210,
          [MOVING_CLEANING]: 550
        },
        [BATHROOMS_4]: {
          [REGULAR_CLEANING]: 115,
          [DEEP_CLEANING]: 240,
          [MOVING_CLEANING]: 600
        },
        [BATHROOMS_5]: {
          [REGULAR_CLEANING]: 130,
          [DEEP_CLEANING]: 270,
          [MOVING_CLEANING]: 650
        }
      },
      [BEDROOMS_3]: {
        [BATHROOMS_1]: {
          [REGULAR_CLEANING]: 70,
          [DEEP_CLEANING]: 160,
          [MOVING_CLEANING]: 500
        },
        [BATHROOMS_2]: {
          [REGULAR_CLEANING]: 90,
          [DEEP_CLEANING]: 190,
          [MOVING_CLEANING]: 550
        },
        [BATHROOMS_3]: {
          [REGULAR_CLEANING]: 110,
          [DEEP_CLEANING]: 220,
          [MOVING_CLEANING]: 600
        },
        [BATHROOMS_4]: {
          [REGULAR_CLEANING]: 130,
          [DEEP_CLEANING]: 250,
          [MOVING_CLEANING]: 650
        },
        [BATHROOMS_5]: {
          [REGULAR_CLEANING]: 150,
          [DEEP_CLEANING]: 280,
          [MOVING_CLEANING]: 700
        }
      },
      [BEDROOMS_4]: {
        [BATHROOMS_1]: {
          [REGULAR_CLEANING]: 95,
          [DEEP_CLEANING]: 190,
          [MOVING_CLEANING]: 550
        },
        [BATHROOMS_2]: {
          [REGULAR_CLEANING]: 110,
          [DEEP_CLEANING]: 220,
          [MOVING_CLEANING]: 600
        },
        [BATHROOMS_3]: {
          [REGULAR_CLEANING]: 130,
          [DEEP_CLEANING]: 250,
          [MOVING_CLEANING]: 650
        },
        [BATHROOMS_4]: {
          [REGULAR_CLEANING]: 150,
          [DEEP_CLEANING]: 280,
          [MOVING_CLEANING]: 700
        },
        [BATHROOMS_5]: {
          [REGULAR_CLEANING]: 170,
          [DEEP_CLEANING]: 310,
          [MOVING_CLEANING]: 750
        }
      },
    };

    return prices[bedrooms][bathrooms][cleaningType];
  };

  render() {

    return (

          {/*<Dropdown id="house-type-dropdown" value={this.state.houseType} options={HOUSE_TYPE_LIST} onClickHandler={this.housePropertyChangeHandler(HOUSE_TYPE_PARAM)}/>*/}
          <Dropdown id="number-of-bedrooms-dropdown" value={this.state.bedrooms} options={BEDROOM_TYPES_LIST} onClickHandler={this.housePropertyChangeHandler(BEDROOMS_TYPE_PARAM)}/>
          <Dropdown id="number-of-bathrooms-dropdown" value={this.state.bathrooms} options={BATHROOMS_TYPE_LIST} onClickHandler={this.housePropertyChangeHandler(BATHROOMS_TYPE_PARAM)}/>
          <Dropdown id="cleaning-type-dropdown" value={this.state.cleaningType} options={CLEANING_TYPE_LIST} onClickHandler={this.housePropertyChangeHandler(CLEANING_TYPE_PARAM)}/>

          Book from ${this.state.indicativePrice}

    );
  }
}

export default QuoteIndicator;
<commit_msg>Fixing google analytic events<commit_after>import React, { Component } from 'react';
import Dropdown from '../dropdown';
import './price-indicator.css';

const HOUSE_TYPE_PARAM = 'houseType';
const HOUSE_TYPE_APARTMENT = 'Apartment';
const HOUSE_TYPE_HOUSE = 'House';
const HOUSE_TYPE_LIST = [HOUSE_TYPE_APARTMENT, HOUSE_TYPE_HOUSE];

const BEDROOMS_TYPE_PARAM = 'bedrooms';
const STUDIO = 'Studio';
const BEDROOMS_1 = '1 Bedroom';
const BEDROOMS_2 = '2 Bedrooms';
const BEDROOMS_3 = '3 Bedrooms';
const BEDROOMS_4 = '4 Bedrooms';
const BEDROOM_TYPES_LIST = [STUDIO, BEDROOMS_1, BEDROOMS_2, BEDROOMS_3, BEDROOMS_4];

const BATHROOMS_TYPE_PARAM = 'bathrooms';
const BATHROOMS_1 = '1 Bathroom';
const BATHROOMS_2 = '2 Bathrooms';
const BATHROOMS_3 = '3 Bathrooms';
const BATHROOMS_4 = '4 Bathrooms';
const BATHROOMS_5 = '5 Bathrooms';
const BATHROOMS_TYPE_LIST = [BATHROOMS_1, BATHROOMS_2, BATHROOMS_3, BATHROOMS_4, BATHROOMS_5];

const CLEANING_TYPE_PARAM = 'cleaningType';
const REGULAR_CLEANING = 'Regular';
const DEEP_CLEANING = 'Deep clean';
const MOVING_CLEANING = 'Moving in/out';
const CLEANING_TYPE_LIST = [REGULAR_CLEANING, DEEP_CLEANING, MOVING_CLEANING];

class QuoteIndicator extends Component {

  constructor(props) {
    super(props);
    this.state = {
      [HOUSE_TYPE_PARAM]: HOUSE_TYPE_APARTMENT,
      [BEDROOMS_TYPE_PARAM]: BEDROOMS_2,
      [BATHROOMS_TYPE_PARAM]: BATHROOMS_2,
      [CLEANING_TYPE_PARAM]: REGULAR_CLEANING,
      indicativePrice: this.getIndicativePrice({[BEDROOMS_TYPE_PARAM]: BEDROOMS_2, [BATHROOMS_TYPE_PARAM]: BATHROOMS_2, [CLEANING_TYPE_PARAM]: REGULAR_CLEANING})
    }
  }

  housePropertyChangeHandler = (propertyName) => {
    return (value) => {
      this.setState({[propertyName]: value});
      this.setState({indicativePrice: this.getIndicativePrice(Object.assign({}, this.state, {[propertyName]: value}))});
      ga('send', 'event', 'price-indicator', propertyName, value)
    };
  };

  bookButtonClickHandler = () => {
    ga('send', 'event', 'price-indicator', 'book-now-button', 'click');
  };

  getIndicativePrice = ({bedrooms, bathrooms, cleaningType}) => {
    const prices = {
      [STUDIO]: {
        [BATHROOMS_1]: {
          [REGULAR_CLEANING]: 55,
          [DEEP_CLEANING]: 100,
          [MOVING_CLEANING]: 300
        },
        [BATHROOMS_2]: {
          [REGULAR_CLEANING]: 65,
          [DEEP_CLEANING]: 120,
          [MOVING_CLEANING]: 350
        },
        [BATHROOMS_3]: {
          [REGULAR_CLEANING]: 75,
          [DEEP_CLEANING]: 140,
          [MOVING_CLEANING]: 400
        },
        [BATHROOMS_4]: {
          [REGULAR_CLEANING]: 85,
          [DEEP_CLEANING]: 150,
          [MOVING_CLEANING]: 450
        },
        [BATHROOMS_5]: {
          [REGULAR_CLEANING]: 95,
          [DEEP_CLEANING]: 160,
          [MOVING_CLEANING]: 500
        }
      },
      [BEDROOMS_1]: {
        [BATHROOMS_1]: {
          [REGULAR_CLEANING]: 60,
          [DEEP_CLEANING]: 120,
          [MOVING_CLEANING]: 400
        },
        [BATHROOMS_2]: {
          [REGULAR_CLEANING]: 70,
          [DEEP_CLEANING]: 140,
          [MOVING_CLEANING]: 450
        },
        [BATHROOMS_3]: {
          [REGULAR_CLEANING]: 80,
          [DEEP_CLEANING]: 160,
          [MOVING_CLEANING]: 500
        },
        [BATHROOMS_4]: {
          [REGULAR_CLEANING]: 90,
          [DEEP_CLEANING]: 180,
          [MOVING_CLEANING]: 550
        },
        [BATHROOMS_5]: {
          [REGULAR_CLEANING]: 100,
          [DEEP_CLEANING]: 200,
          [MOVING_CLEANING]: 600
        }
      },
      [BEDROOMS_2]: {
        [BATHROOMS_1]: {
          [REGULAR_CLEANING]: 70,
          [DEEP_CLEANING]: 150,
          [MOVING_CLEANING]: 450
        },
        [BATHROOMS_2]: {
          [REGULAR_CLEANING]: 85,
          [DEEP_CLEANING]: 180,
          [MOVING_CLEANING]: 500
        },
        [BATHROOMS_3]: {
          [REGULAR_CLEANING]: 100,
          [DEEP_CLEANING]: 210,
          [MOVING_CLEANING]: 550
        },
        [BATHROOMS_4]: {
          [REGULAR_CLEANING]: 115,
          [DEEP_CLEANING]: 240,
          [MOVING_CLEANING]: 600
        },
        [BATHROOMS_5]: {
          [REGULAR_CLEANING]: 130,
          [DEEP_CLEANING]: 270,
          [MOVING_CLEANING]: 650
        }
      },
      [BEDROOMS_3]: {
        [BATHROOMS_1]: {
          [REGULAR_CLEANING]: 70,
          [DEEP_CLEANING]: 160,
          [MOVING_CLEANING]: 500
        },
        [BATHROOMS_2]: {
          [REGULAR_CLEANING]: 90,
          [DEEP_CLEANING]: 190,
          [MOVING_CLEANING]: 550
        },
        [BATHROOMS_3]: {
          [REGULAR_CLEANING]: 110,
          [DEEP_CLEANING]: 220,
          [MOVING_CLEANING]: 600
        },
        [BATHROOMS_4]: {
          [REGULAR_CLEANING]: 130,
          [DEEP_CLEANING]: 250,
          [MOVING_CLEANING]: 650
        },
        [BATHROOMS_5]: {
          [REGULAR_CLEANING]: 150,
          [DEEP_CLEANING]: 280,
          [MOVING_CLEANING]: 700
        }
      },
      [BEDROOMS_4]: {
        [BATHROOMS_1]: {
          [REGULAR_CLEANING]: 95,
          [DEEP_CLEANING]: 190,
          [MOVING_CLEANING]: 550
        },
        [BATHROOMS_2]: {
          [REGULAR_CLEANING]: 110,
          [DEEP_CLEANING]: 220,
          [MOVING_CLEANING]: 600
        },
        [BATHROOMS_3]: {
          [REGULAR_CLEANING]: 130,
          [DEEP_CLEANING]: 250,
          [MOVING_CLEANING]: 650
        },
        [BATHROOMS_4]: {
          [REGULAR_CLEANING]: 150,
          [DEEP_CLEANING]: 280,
          [MOVING_CLEANING]: 700
        },
        [BATHROOMS_5]: {
          [REGULAR_CLEANING]: 170,
          [DEEP_CLEANING]: 310,
          [MOVING_CLEANING]: 750
        }
      },
    };

    return prices[bedrooms][bathrooms][cleaningType];
  };

  render() {

    return (

          {/*<Dropdown id="house-type-dropdown" value={this.state.houseType} options={HOUSE_TYPE_LIST} onClickHandler={this.housePropertyChangeHandler(HOUSE_TYPE_PARAM)}/>*/}
          <Dropdown id="number-of-bedrooms-dropdown" value={this.state.bedrooms} options={BEDROOM_TYPES_LIST} onClickHandler={this.housePropertyChangeHandler(BEDROOMS_TYPE_PARAM)}/>
          <Dropdown id="number-of-bathrooms-dropdown" value={this.state.bathrooms} options={BATHROOMS_TYPE_LIST} onClickHandler={this.housePropertyChangeHandler(BATHROOMS_TYPE_PARAM)}/>
          <Dropdown id="cleaning-type-dropdown" value={this.state.cleaningType} options={CLEANING_TYPE_LIST} onClickHandler={this.housePropertyChangeHandler(CLEANING_TYPE_PARAM)}/>

          Book from ${this.state.indicativePrice}

    );
  }
}

export default QuoteIndicator;
<|endoftext|>


<commit_before>import {remote} from 'electron'
import {h, Component} from 'preact'
import classNames from 'classnames'
import i18n from '../../i18n.js'
import sabaki from '../../modules/sabaki.js'
import {noop} from '../../modules/helper.js'

const t = i18n.context('WinrateGraph')
const setting = remote.require('./setting')
const blunderThreshold = setting.get('view.winrategraph_blunderthreshold')

class WinrateStrip extends Component {
  render() {
    let {player, winrate, change} = this.props

    return h(
      'section',
      {class: 'winrate-strip'},

      h('img', {
        class: 'player',
        src: `./img/ui/${player > 0 ? 'black' : 'white'}.svg`,
        height: 14,
        alt: player > 0 ? t('Black') : t('White'),
        title: player > 0 ? t('Black') : t('White')
      }),

      h(
        'span',
        {class: 'main'},
        winrate == null ? '–' : `${i18n.formatNumber(winrate)}%`
      ),

      h(
        'span',
        {
          class: classNames('change', {
            positive: change != null && change > blunderThreshold,
            negative: change != null && change < -blunderThreshold
          })
        },

        h('span', {}, change == null ? '' : change >= 0 ? '+' : '-'),
        h(
          'span',
          {},
          change == null ? '–' : i18n.formatNumber(Math.abs(change))
        )
      )
    )
  }
}

export default class WinrateGraph extends Component {
  constructor() {
    super()

    this.state = {
      invert: setting.get('view.winrategraph_invert')
    }

    setting.events.on(sabaki.window.id, 'change', ({key, value}) => {
      if (key === 'view.winrategraph_invert') {
        this.setState({invert: value})
      }
    })

    this.handleMouseDown = evt => {
      this.mouseDown = true
      document.dispatchEvent(new MouseEvent('mousemove', evt))
    }
  }

  shouldComponentUpdate({lastPlayer, width, currentIndex, data}, {invert}) {
    return (
      lastPlayer !== this.props.lastPlayer ||
      width !== this.props.width ||
      currentIndex !== this.props.currentIndex ||
      data[currentIndex] !== this.props.data[currentIndex] ||
      invert !== this.state.invert
    )
  }

  componentDidMount() {
    document.addEventListener('mousemove', evt => {
      if (!this.mouseDown) return

      let rect = this.element.getBoundingClientRect()
      let percent = (evt.clientX - rect.left) / rect.width
      let {width, data, onCurrentIndexChange = noop} = this.props
      let index = Math.max(
        Math.min(Math.round(width * percent), data.length - 1),
        0
      )

      if (index !== this.props.currentIndex) onCurrentIndexChange({index})
    })

    document.addEventListener('mouseup', () => {
      this.mouseDown = false
    })
  }

  render() {
    let {lastPlayer, width, currentIndex, data} = this.props
    let {invert} = this.state

    let dataDiff = data.map((x, i) =>
      i === 0 || x == null || data[i - 1] == null ? null : x - data[i - 1]
    )
    let dataDiffMax = Math.max(...dataDiff.map(Math.abs), 25)

    let round2 = x => Math.round(x * 100) / 100
    let blackWinrate =
      data[currentIndex] == null ? null : round2(data[currentIndex])
    let blackWinrateDiff =
      dataDiff[currentIndex] == null ? null : round2(dataDiff[currentIndex])
    let whiteWinrate =
      data[currentIndex] == null ? null : round2(100 - data[currentIndex])
    let whiteWinrateDiff =
      dataDiff[currentIndex] == null ? null : -round2(dataDiff[currentIndex])

    let tooltip =
      data[currentIndex] == null
        ? ''
        : [
            [blackWinrate, blackWinrateDiff],
            [whiteWinrate, whiteWinrateDiff]
          ]
            .map(
              ([winrate, diff], i) =>
                `${
                  i === 0 ? t('Black Winrate:') : t('White Winrate:')
                } ${i18n.formatNumber(winrate)}%${
                  diff == null
                    ? ''
                    : ` (${diff >= 0 ? '+' : '-'}${i18n.formatNumber(
                        Math.abs(diff)
                      )})`
                }`
            )
            .join('\n')

    return h(
      'section',
      {
        ref: el => (this.element = el),
        id: 'winrategraph',
        style: {
          height: this.state.height + 'px'
        }
      },

      h(WinrateStrip, {
        player: lastPlayer,
        winrate: lastPlayer > 0 ? blackWinrate : whiteWinrate,
        change: lastPlayer > 0 ? blackWinrateDiff : whiteWinrateDiff
      }),

      h(
        'section',
        {
          class: 'graph',
          title: tooltip,
          onMouseDown: this.handleMouseDown
        },

        h(
          'svg',
          {
            viewBox: `0 0 ${width} 100`,
            preserveAspectRatio: 'none',
            style: {
              height: '100%',
              width: '100%',
              transform: !invert ? 'none' : 'scaleY(-1)'
            }
          },

          // Draw background

          h(
            'defs',
            {},
            h(
              'linearGradient',
              {
                id: 'bgGradient',
                x1: 0,
                y1: 0,
                x2: 0,
                y2: 1
              },
              h('stop', {
                offset: '0%',
                'stop-color': 'white',
                'stop-opacity': 0.7
              }),
              h('stop', {
                offset: '100%',
                'stop-color': 'white',
                'stop-opacity': 0.1
              })
            ),

            h(
              'clipPath',
              {id: 'clipGradient'},
              h('path', {
                fill: 'black',
                'stroke-width': 0,
                d: (() => {
                  let instructions = data
                    .map((x, i) => {
                      if (x == null) return i === 0 ? [i, 50] : null
                      return [i, x]
                    })
                    .filter(x => x != null)

                  if (instructions.length === 0) return ''

                  return (
                    `M ${instructions[0][0]},100 ` +
                    instructions.map(x => `L ${x.join(',')}`).join(' ') +
                    ` L ${instructions.slice(-1)[0][0]},100 Z`
                  )
                })()
              })
            )
          ),

          h('rect', {
            x: 0,
            y: 0,
            width,
            height: 100,
            fill: 'url(#bgGradient)',
            'clip-path': 'url(#clipGradient)'
          }),

          // Draw guiding lines

          h('line', {
            x1: 0,
            y1: 50,
            x2: width,
            y2: 50,
            stroke: '#aaa',
            'stroke-width': 1,
            'stroke-dasharray': 2,
            'vector-effect': 'non-scaling-stroke'
          }),

          [...Array(width)].map((_, i) => {
            if (i === 0 || i % 50 !== 0) return

            return h('line', {
              x1: i,
              y1: 0,
              x2: i,
              y2: 100,
              stroke: '#aaa',
              'stroke-width': 1,
              'stroke-dasharray': 2,
              'vector-effect': 'non-scaling-stroke'
            })
          }),

          // Current position marker

          h('line', {
            x1: currentIndex,
            y1: 0,
            x2: currentIndex,
            y2: 100,
            stroke: '#0082F0',
            'stroke-width': 2,
            'vector-effect': 'non-scaling-stroke'
          }),

          // Draw differential bar graph

          h('path', {
            fill: 'none',
            stroke: '#FF3B30',
            'stroke-width': 1,

            d: dataDiff
              .map((x, i) => {
                if (x == null || Math.abs(x) <= blunderThreshold) return ''

                return `M ${i},50 l 0,${(50 * x) / dataDiffMax}`
              })
              .join(' ')
          }),

          // Draw data lines

          h('path', {
            fill: 'none',
            stroke: '#eee',
            'stroke-width': 2,
            'vector-effect': 'non-scaling-stroke',

            d: data
              .map((x, i) => {
                if (x == null) return ''

                let command = i === 0 || data[i - 1] == null ? 'M' : 'L'
                return `${command} ${i},${x}`
              })
              .join(' ')
          }),

          h('path', {
            fill: 'none',
            stroke: '#ccc',
            'stroke-width': 2,
            'stroke-dasharray': 2,
            'vector-effect': 'non-scaling-stroke',

            d: data
              .map((x, i) => {
                if (i === 0) return 'M 0,50'

                if (x == null && data[i - 1] != null)
                  return `M ${i - 1},${data[i - 1]}`

                if (x != null && data[i - 1] == null) return `L ${i},${x}`

                return ''
              })
              .join(' ')
          })
        ),

        // Draw marker

        data[currentIndex] &&
          h('div', {
            class: 'marker',
            style: {
              left: `${(currentIndex * 100) / width}%`,
              top: `${!invert ? data[currentIndex] : 100 - data[currentIndex]}%`
            }
          })
      )
    )
  }
}
<commit_msg>Show winrate diff from two moves behind<commit_after>import {remote} from 'electron'
import {h, Component} from 'preact'
import classNames from 'classnames'
import i18n from '../../i18n.js'
import sabaki from '../../modules/sabaki.js'
import {noop} from '../../modules/helper.js'

const t = i18n.context('WinrateGraph')
const setting = remote.require('./setting')
const blunderThreshold = setting.get('view.winrategraph_blunderthreshold')

class WinrateStrip extends Component {
  render() {
    let {player, winrate, change} = this.props

    return h(
      'section',
      {class: 'winrate-strip'},

      h('img', {
        class: 'player',
        src: `./img/ui/${player > 0 ? 'black' : 'white'}.svg`,
        height: 14,
        alt: player > 0 ? t('Black') : t('White'),
        title: player > 0 ? t('Black') : t('White')
      }),

      h(
        'span',
        {class: 'main'},
        winrate == null ? '–' : `${i18n.formatNumber(winrate)}%`
      ),

      h(
        'span',
        {
          class: classNames('change', {
            positive: change != null && change > blunderThreshold,
            negative: change != null && change < -blunderThreshold
          })
        },

        h('span', {}, change == null ? '' : change >= 0 ? '+' : '-'),
        h(
          'span',
          {},
          change == null ? '–' : i18n.formatNumber(Math.abs(change))
        )
      )
    )
  }
}

export default class WinrateGraph extends Component {
  constructor() {
    super()

    this.state = {
      invert: setting.get('view.winrategraph_invert')
    }

    setting.events.on(sabaki.window.id, 'change', ({key, value}) => {
      if (key === 'view.winrategraph_invert') {
        this.setState({invert: value})
      }
    })

    this.handleMouseDown = evt => {
      this.mouseDown = true
      document.dispatchEvent(new MouseEvent('mousemove', evt))
    }
  }

  shouldComponentUpdate({lastPlayer, width, currentIndex, data}, {invert}) {
    return (
      lastPlayer !== this.props.lastPlayer ||
      width !== this.props.width ||
      currentIndex !== this.props.currentIndex ||
      data[currentIndex] !== this.props.data[currentIndex] ||
      invert !== this.state.invert
    )
  }

  componentDidMount() {
    document.addEventListener('mousemove', evt => {
      if (!this.mouseDown) return

      let rect = this.element.getBoundingClientRect()
      let percent = (evt.clientX - rect.left) / rect.width
      let {width, data, onCurrentIndexChange = noop} = this.props
      let index = Math.max(
        Math.min(Math.round(width * percent), data.length - 1),
        0
      )

      if (index !== this.props.currentIndex) onCurrentIndexChange({index})
    })

    document.addEventListener('mouseup', () => {
      this.mouseDown = false
    })
  }

  render() {
    let {lastPlayer, width, currentIndex, data} = this.props
    let {invert} = this.state

    let dataDiff = data.map((x, i) =>
      i === 0 || x == null || (data[i - 1] == null && data[i - 2] == null)
        ? null
        : x - data[data[i - 1] != null ? i - 1 : i - 2]
    )
    let dataDiffMax = Math.max(...dataDiff.map(Math.abs), 25)

    let round2 = x => Math.round(x * 100) / 100
    let blackWinrate =
      data[currentIndex] == null ? null : round2(data[currentIndex])
    let blackWinrateDiff =
      dataDiff[currentIndex] == null ? null : round2(dataDiff[currentIndex])
    let whiteWinrate =
      data[currentIndex] == null ? null : round2(100 - data[currentIndex])
    let whiteWinrateDiff =
      dataDiff[currentIndex] == null ? null : -round2(dataDiff[currentIndex])

    let tooltip =
      data[currentIndex] == null
        ? ''
        : [
            [blackWinrate, blackWinrateDiff],
            [whiteWinrate, whiteWinrateDiff]
          ]
            .map(
              ([winrate, diff], i) =>
                `${
                  i === 0 ? t('Black Winrate:') : t('White Winrate:')
                } ${i18n.formatNumber(winrate)}%${
                  diff == null
                    ? ''
                    : ` (${diff >= 0 ? '+' : '-'}${i18n.formatNumber(
                        Math.abs(diff)
                      )})`
                }`
            )
            .join('\n')

    return h(
      'section',
      {
        ref: el => (this.element = el),
        id: 'winrategraph',
        style: {
          height: this.state.height + 'px'
        }
      },

      h(WinrateStrip, {
        player: lastPlayer,
        winrate: lastPlayer > 0 ? blackWinrate : whiteWinrate,
        change: lastPlayer > 0 ? blackWinrateDiff : whiteWinrateDiff
      }),

      h(
        'section',
        {
          class: 'graph',
          title: tooltip,
          onMouseDown: this.handleMouseDown
        },

        h(
          'svg',
          {
            viewBox: `0 0 ${width} 100`,
            preserveAspectRatio: 'none',
            style: {
              height: '100%',
              width: '100%',
              transform: !invert ? 'none' : 'scaleY(-1)'
            }
          },

          // Draw background

          h(
            'defs',
            {},
            h(
              'linearGradient',
              {
                id: 'bgGradient',
                x1: 0,
                y1: 0,
                x2: 0,
                y2: 1
              },
              h('stop', {
                offset: '0%',
                'stop-color': 'white',
                'stop-opacity': 0.7
              }),
              h('stop', {
                offset: '100%',
                'stop-color': 'white',
                'stop-opacity': 0.1
              })
            ),

            h(
              'clipPath',
              {id: 'clipGradient'},
              h('path', {
                fill: 'black',
                'stroke-width': 0,
                d: (() => {
                  let instructions = data
                    .map((x, i) => {
                      if (x == null) return i === 0 ? [i, 50] : null
                      return [i, x]
                    })
                    .filter(x => x != null)

                  if (instructions.length === 0) return ''

                  return (
                    `M ${instructions[0][0]},100 ` +
                    instructions.map(x => `L ${x.join(',')}`).join(' ') +
                    ` L ${instructions.slice(-1)[0][0]},100 Z`
                  )
                })()
              })
            )
          ),

          h('rect', {
            x: 0,
            y: 0,
            width,
            height: 100,
            fill: 'url(#bgGradient)',
            'clip-path': 'url(#clipGradient)'
          }),

          // Draw guiding lines

          h('line', {
            x1: 0,
            y1: 50,
            x2: width,
            y2: 50,
            stroke: '#aaa',
            'stroke-width': 1,
            'stroke-dasharray': 2,
            'vector-effect': 'non-scaling-stroke'
          }),

          [...Array(width)].map((_, i) => {
            if (i === 0 || i % 50 !== 0) return

            return h('line', {
              x1: i,
              y1: 0,
              x2: i,
              y2: 100,
              stroke: '#aaa',
              'stroke-width': 1,
              'stroke-dasharray': 2,
              'vector-effect': 'non-scaling-stroke'
            })
          }),

          // Current position marker

          h('line', {
            x1: currentIndex,
            y1: 0,
            x2: currentIndex,
            y2: 100,
            stroke: '#0082F0',
            'stroke-width': 2,
            'vector-effect': 'non-scaling-stroke'
          }),

          // Draw differential bar graph

          h('path', {
            fill: 'none',
            stroke: '#FF3B30',
            'stroke-width': 1,

            d: dataDiff
              .map((x, i) => {
                if (x == null || Math.abs(x) <= blunderThreshold) return ''

                return `M ${i},50 l 0,${(50 * x) / dataDiffMax}`
              })
              .join(' ')
          }),

          // Draw data lines

          h('path', {
            fill: 'none',
            stroke: '#eee',
            'stroke-width': 2,
            'vector-effect': 'non-scaling-stroke',

            d: data
              .map((x, i) => {
                if (x == null) return ''

                let command = i === 0 || data[i - 1] == null ? 'M' : 'L'
                return `${command} ${i},${x}`
              })
              .join(' ')
          }),

          h('path', {
            fill: 'none',
            stroke: '#ccc',
            'stroke-width': 2,
            'stroke-dasharray': 2,
            'vector-effect': 'non-scaling-stroke',

            d: data
              .map((x, i) => {
                if (i === 0) return 'M 0,50'

                if (x == null && data[i - 1] != null)
                  return `M ${i - 1},${data[i - 1]}`

                if (x != null && data[i - 1] == null) return `L ${i},${x}`

                return ''
              })
              .join(' ')
          })
        ),

        // Draw marker

        data[currentIndex] &&
          h('div', {
            class: 'marker',
            style: {
              left: `${(currentIndex * 100) / width}%`,
              top: `${!invert ? data[currentIndex] : 100 - data[currentIndex]}%`
            }
          })
      )
    )
  }
}
<|endoftext|>


<commit_before>var $carousels = [];
var loadedCarousels = {};
var css = {
    'containers': '.carousel__js',
    'handles': '.carousel__link--next, .carousel__link--prev'
};

export function init() {
    $carousels = $(css.containers);

    // prevent flicker effect on page load
    $carousels.on('init', function () {
        $(css.containers).css("visibility", "visible");
    });

    // img carousels
    $.each($carousels, function (i, carousel) {
        var $carousel = $(carousel),
            id = $carousel.attr("id");
        loadedCarousels[id] = false;

        $carousel.slick({
            speed: 300,
            slidesToShow: 1,
            infinite: false,
            appendArrows: "#" + id + " .slick-list",
            prevArrow: '<button class="carousel__link--prev">Vorhergehendes Bild</button>',
            nextArrow: '<button class="carousel__link--next carousel__link--waggle">Nächstes Bild</button>',
            slide: ".carousel__item"
        });
        registerListener($carousel);
    });

    // video carousels
    $(".video_carousel__js").on("init", function (slick) {
        $(this).css("visibility", "visible");
        // triggering a "recalculation of dots" (via "setPosition" below)
        $(window).trigger('resize');
    });

    // video carousel:
    $.each($('.video_carousel__js'), function (i, carousel) {
        let $carousel = $(carousel),
            id = $carousel.attr("id");

        loadedCarousels[id] = false;

        $carousel.slick({
            infinite: false,
            slide: ".carousel__item",
            slidesToShow: 1, // we need all dots - initially
            slidesToScroll: 1,
            accessibility: false,
            focusOnSelect: true, // let's try this
            appendArrows: "#" + id + " .slick-list",
            dots: true,
            centerPadding: 0,
            variableWidth: true,
            prevArrow: '<button class="carousel__link--prev">Vorhergehender Slide</button>',
            nextArrow: '<button class="carousel__link--next">Nächster Slide</button>'
        });
    });

    // "position change" (resize page or "activate" slider in any way)
    $('.video_carousel__js').on('setPosition', function (slick) {
        let slidesToShow = getNumberOfSlidesPerScreen(1), // mobile : 1
            $carousel = $(this),
            currentSlide = $carousel.slick("slickCurrentSlide");
        // if previous num. of slides shown != the num. we'll see now
        if ($carousel.slick("slickGetOption", "slidesToShow") != slidesToShow) {
            // if slider not at initial pos. && arrows displayed
            if (currentSlide > 0 && slidesToShow != 1) {
                // move it to 0 - so the "arrows" don't get "confused"
                currentSlide = 0;
                $carousel.slick("slickGoTo", currentSlide, true);
            }

            if (slidesToShow > 1) {
                // the following option is terrible for screen sizes showing more than 1 elm
                $carousel.slick("slickSetOption", "focusOnSelect", false);
            }

            // and adjust num. of slides...
            $carousel.slick("slickSetOption", "slidesToShow", slidesToShow, false);
            $carousel.slick("slickSetOption", "slidesToScroll", slidesToShow, false);

            let screensToShow = Math.ceil($carousel.find(".carousel__item").length / slidesToShow);

            (slidesToShow > screensToShow) // are there more slides than elements?
                ? $(".slick-dots").removeClass(".h-element--hide")  // show dots
                : $(".slick-dots").addClass(".h-element--hide");    // else hide them

            // and adjust num. of dots
            rePaintDots($carousel, screensToShow);

            // if we're at the rightmost position within the carousel - we don't want the right arrow
            handleRightArrow($carousel, currentSlide, screensToShow);
        }

        if (slidesToShow > 1) {
            // unhide the following slidesToShow - 1 from screenreaders as well:
            let maxSlideVisible = currentSlide + slidesToShow - 1;
            $carousel.find(".carousel__item").each(function (i) {
                (i >= currentSlide && i <= maxSlideVisible)
                    ? $(this).attr("aria-hidden", false)
                    : $(this).attr("aria-hidden", true);
            });
        }
    });
}

function registerListener($carousel) {
    // load all images after user starts interacting with carousel
    $carousel.on('beforeChange', function (event, slick, currentSlide, nextSlide) {
        if (loadedCarousels[$carousel.attr("id")] === false) {
            loadedCarousels[$carousel.attr("id")] = true;
            loadLazyImages(slick.$slides.find(".image-figure__js"));
        }
    });

    $carousel.on('swipe', function (event, slick, direction) {
        // on "interacting" with the carousel => no more animation
        $(this).find(".carousel__link--next").removeClass("carousel__link--waggle");
    });

    $carousel.find(css.handles).on("touchstart mousedown mouseenter", function () {
        $(this).removeClass("untouched");
        // if the handles are clicked / touched: stop the animation
        $(this).removeClass("carousel__link--waggle");
    }).on("touchend touchcancel", function () {
        $(this).addClass("untouched");
    });
}

function loadLazyImages(images) {
    images.each(function (i, image) {
        var $image = $(image);
        if ($image.data("src")) {
            $image.attr("srcset", $image.data("srcset"));
            $image.attr("src", $image.data("src"));
        }
    });
}

function rePaintDots($carousel, screensToShow) {
    let x = screensToShow + 1;
    $carousel.find(".slick-dots li").removeClass("h-element--hide");
    $carousel.find(".slick-dots li:nth-child(1n + " + x + ")").addClass("h-element--hide");
}

function handleRightArrow($carousel, currentSlide, screensToShow) {
    if ((currentSlide + 1) == screensToShow) {
        $carousel.find(".carousel__link--next").addClass("h-element--hide").attr("aria-disabled", true);
    } else {
        $carousel.find(".carousel__link--next").removeClass("h-element--hide").attr("aria-disabled", false);
    }
}

function getNumberOfSlidesPerScreen(slidesToShow = 1) {
    // let slidesToShow = 1; // mobile

    if (matchMedia('screen and (min-width: 720px) and (max-width: 1023px)').matches) {
        slidesToShow = 2; // tablet
    } else if (matchMedia('screen and (min-width: 1024px)').matches) {
        slidesToShow = 3; // larger
    }

    return slidesToShow
}
<commit_msg>NOW!<commit_after>var $carousels = [];
var loadedCarousels = {};
var css = {
    'containers': '.carousel__js',
    'handles': '.carousel__link--next, .carousel__link--prev'
};

export function init() {
    $carousels = $(css.containers);

    // prevent flicker effect on page load
    $carousels.on('init', function () {
        $(css.containers).css("visibility", "visible");
    });

    // img carousels
    $.each($carousels, function (i, carousel) {
        var $carousel = $(carousel),
            id = $carousel.attr("id");
        loadedCarousels[id] = false;

        $carousel.slick({
            speed: 300,
            slidesToShow: 1,
            infinite: false,
            appendArrows: "#" + id + " .slick-list",
            prevArrow: '<button class="carousel__link--prev">Vorhergehendes Bild</button>',
            nextArrow: '<button class="carousel__link--next carousel__link--waggle">Nächstes Bild</button>',
            slide: ".carousel__item"
        });
        registerListener($carousel);
    });

    // video carousels
    $(".video_carousel__js").on("init", function (slick) {
        $(this).css("visibility", "visible");
        // triggering a "recalculation of dots" (via "setPosition" below)
        $(window).trigger('resize');
    });

    // video carousel:
    $.each($('.video_carousel__js'), function (i, carousel) {
        let $carousel = $(carousel),
            id = $carousel.attr("id");

        loadedCarousels[id] = false;

        $carousel.slick({
            infinite: false,
            slide: ".carousel__item",
            slidesToShow: 1, // we need all dots - initially
            slidesToScroll: 1,
            accessibility: false,
            focusOnSelect: true, // let's try this
            appendArrows: "#" + id + " .slick-list",
            dots: true,
            centerPadding: 0,
            variableWidth: true,
            prevArrow: '<button class="carousel__link--prev">Vorhergehender Slide</button>',
            nextArrow: '<button class="carousel__link--next">Nächster Slide</button>'
        });
    });

    // "position change" (resize page or "activate" slider in any way)
    $('.video_carousel__js').on('setPosition', function (slick) {
        let slidesToShow = getNumberOfSlidesPerScreen(1), // mobile : 1
            $carousel = $(this),
            currentSlide = $carousel.slick("slickCurrentSlide");
        // if previous num. of slides shown != the num. we'll see now
        if ($carousel.slick("slickGetOption", "slidesToShow") != slidesToShow) {
            // if slider not at initial pos. && arrows displayed
            if (currentSlide > 0 && slidesToShow != 1) {
                // move it to 0 - so the "arrows" don't get "confused"
                currentSlide = 0;
                $carousel.slick("slickGoTo", currentSlide, true);
            }

            if (slidesToShow > 1) {
                // the following option is terrible for screen sizes showing more than 1 elm
                $carousel.slick("slickSetOption", "focusOnSelect", false);
            }

            // and adjust num. of slides...
            $carousel.slick("slickSetOption", "slidesToShow", slidesToShow, false);
            $carousel.slick("slickSetOption", "slidesToScroll", slidesToShow, false);

            let screensToShow = Math.ceil($carousel.find(".carousel__item").length / slidesToShow);

            (screensToShow > 1) // are there more slides than one?
                ? $(".slick-dots").removeClass("h-element--hide")  // show dots
                : $(".slick-dots").addClass("h-element--hide");    // else hide the one :)

            // and adjust num. of dots
            rePaintDots($carousel, screensToShow);

            // if we're at the rightmost position within the carousel - we don't want the right arrow
            handleRightArrow($carousel, currentSlide, screensToShow);
        }

        if (slidesToShow > 1) {
            // unhide the following slidesToShow - 1 from screenreaders as well:
            let maxSlideVisible = currentSlide + slidesToShow - 1;
            $carousel.find(".carousel__item").each(function (i) {
                (i >= currentSlide && i <= maxSlideVisible)
                    ? $(this).attr("aria-hidden", false)
                    : $(this).attr("aria-hidden", true);
            });
        }
    });
}

function registerListener($carousel) {
    // load all images after user starts interacting with carousel
    $carousel.on('beforeChange', function (event, slick, currentSlide, nextSlide) {
        if (loadedCarousels[$carousel.attr("id")] === false) {
            loadedCarousels[$carousel.attr("id")] = true;
            loadLazyImages(slick.$slides.find(".image-figure__js"));
        }
    });

    $carousel.on('swipe', function (event, slick, direction) {
        // on "interacting" with the carousel => no more animation
        $(this).find(".carousel__link--next").removeClass("carousel__link--waggle");
    });

    $carousel.find(css.handles).on("touchstart mousedown mouseenter", function () {
        $(this).removeClass("untouched");
        // if the handles are clicked / touched: stop the animation
        $(this).removeClass("carousel__link--waggle");
    }).on("touchend touchcancel", function () {
        $(this).addClass("untouched");
    });
}

function loadLazyImages(images) {
    images.each(function (i, image) {
        var $image = $(image);
        if ($image.data("src")) {
            $image.attr("srcset", $image.data("srcset"));
            $image.attr("src", $image.data("src"));
        }
    });
}

function rePaintDots($carousel, screensToShow) {
    let x = screensToShow + 1;
    $carousel.find(".slick-dots li").removeClass("h-element--hide");
    $carousel.find(".slick-dots li:nth-child(1n + " + x + ")").addClass("h-element--hide");
}

function handleRightArrow($carousel, currentSlide, screensToShow) {
    if ((currentSlide + 1) == screensToShow) {
        $carousel.find(".carousel__link--next").addClass("h-element--hide").attr("aria-disabled", true);
    } else {
        $carousel.find(".carousel__link--next").removeClass("h-element--hide").attr("aria-disabled", false);
    }
}

function getNumberOfSlidesPerScreen(slidesToShow = 1) {
    // let slidesToShow = 1; // mobile

    if (matchMedia('screen and (min-width: 720px) and (max-width: 1023px)').matches) {
        slidesToShow = 2; // tablet
    } else if (matchMedia('screen and (min-width: 1024px)').matches) {
        slidesToShow = 3; // larger
    }

    return slidesToShow
}
<|endoftext|>


<commit_before>import PropTypes from 'prop-types';
import React from 'react';
import { FormattedMessage, injectIntl } from 'react-intl';
import ArrowDropDownIcon from '@material-ui/icons/ArrowDropDown';
import messages from '../../resources/messages';
import withHelp from '../common/hocs/HelpfulContainer';
import LinkWithFilters from './LinkWithFilters';
import { storyPubDateToTimestamp, STORY_PUB_DATE_UNDATEABLE } from '../../lib/dateUtil';
import { googleFavIconUrl, storyDomainName } from '../../lib/urlUtil';
import { ReadItNowButton } from '../common/IconButton';
import SafelyFormattedNumber from '../common/SafelyFormattedNumber';

const localMessages = {
  undateable: { id: 'story.publishDate.undateable', defaultMessage: 'Undateable' },
  foci: { id: 'story.foci.list', defaultMessage: 'List of Subtopics {list}' },
};

const ICON_STYLE = { margin: 0, padding: 0, width: 12, height: 12 };

class TopicStoryTable extends React.Component {
  sortableHeader = (sortKey, textMsg) => {
    const { onChangeSort, sortedBy } = this.props;
    const { formatMessage } = this.props.intl;
    let content;
    if (onChangeSort) {
      if (sortedBy === sortKey) {
        // currently sorted by this key
        content = (

            <FormattedMessage {...textMsg} />
            <ArrowDropDownIcon style={ICON_STYLE} />

        );
      } else {
        // link to sort by this key
        content = (
          <a
            href={`#${formatMessage(textMsg)}`}
            onClick={(e) => { e.preventDefault(); onChangeSort(sortKey); }}
            title={formatMessage(textMsg)}
          >
            <FormattedMessage {...textMsg} />

        );
      }
    } else {
      // not sortable
      content = <FormattedMessage {...textMsg} />;
    }
    return content;
  }

  render() {
    const { stories, showTweetCounts, onChangeFocusSelection, topicId, maxTitleLength, helpButton } = this.props;
    const { formatMessage, formatDate } = this.props.intl;
    const tweetHeader = showTweetCounts ? {this.sortableHeader('twitter', messages.tweetCounts)}	 : null;
    return (

              <FormattedMessage {...messages.storyTitle} />
              <FormattedMessage {...messages.media} />
              <FormattedMessage {...messages.storyDate} />{helpButton}
              {this.sortableHeader('inlink', messages.mediaInlinks)}
              <FormattedMessage {...messages.outlinks} />
              {this.sortableHeader('facebook', messages.facebookShares)}
              {tweetHeader}
              {}
              <FormattedMessage {...messages.focusHeader} />

            {stories.map((story, idx) => {
              const domain = storyDomainName(story);
              let dateToShow = null; // need to handle undateable stories
              let dateStyle = '';
              const title = maxTitleLength !== undefined ? `${story.title.substr(0, maxTitleLength)}...` : story.title;
              const tweetInfo = showTweetCounts ? <SafelyFormattedNumber value={story.simple_tweet_count} />	 : null;
              if (story.publish_date === STORY_PUB_DATE_UNDATEABLE) {
                dateToShow = formatMessage(localMessages.undateable);
                dateStyle = 'story-date-undateable';
              } else {
                dateToShow = formatDate(storyPubDateToTimestamp(story.publish_date));
                dateStyle = (story.date_is_reliable === 0) ? 'story-date-unreliable' : 'story-date-reliable';
                if (story.date_is_reliable === 0) {
                  dateToShow += '?';
                }
              }
              let listOfFoci = 'none';
              if (story.foci && story.foci.length > 0) {
                listOfFoci = (
                  story.foci.map((foci, i) => (

                      {!!i && ', '}
                      <LinkWithFilters
                        to={`/topics/${topicId}/summary`}
                        filters={{ focusId: foci.foci_id, timespanId: null }}
                        onClick={() => onChangeFocusSelection(foci.foci_id)}
                      >
                        { foci.name }
                      </LinkWithFilters>

                  )));
                // listOfFoci = intersperse(listOfFoci, ', ');
              }
              return (

                    <LinkWithFilters to={`/topics/${topicId}/stories/${story.stories_id}`}>
                      {title}
                    </LinkWithFilters>

                    <LinkWithFilters to={`/topics/${topicId}/media/${story.media_id}`}>
                      {story.media_name}
                    </LinkWithFilters>

                  <span className={`story-date ${dateStyle}`}>{dateToShow}
                  <SafelyFormattedNumber value={story.media_inlink_count} />
                  <SafelyFormattedNumber value={story.outlink_count} />
                  <SafelyFormattedNumber value={story.facebook_share_count} />
                  {tweetInfo}

                      <ReadItNowButton />

                  {listOfFoci}

              );
            })}

    );
  }
}

TopicStoryTable.propTypes = {
  stories: PropTypes.array.isRequired,
  showTweetCounts: PropTypes.bool,
  intl: PropTypes.object.isRequired,
  helpButton: PropTypes.node.isRequired,
  topicId: PropTypes.number, // not required as this table is now also used by query routine
  onChangeSort: PropTypes.func,
  onChangeFocusSelection: PropTypes.func,
  sortedBy: PropTypes.string,
  maxTitleLength: PropTypes.number,
};

export default injectIntl(
  withHelp(messages.pubDateTableHelpTitle, messages.pubDateTableHelpText)(
    TopicStoryTable
  )
);
<commit_msg>Show post_count for now #1740<commit_after>import PropTypes from 'prop-types';
import React from 'react';
import { FormattedMessage, injectIntl } from 'react-intl';
import ArrowDropDownIcon from '@material-ui/icons/ArrowDropDown';
import messages from '../../resources/messages';
import withHelp from '../common/hocs/HelpfulContainer';
import LinkWithFilters from './LinkWithFilters';
import { storyPubDateToTimestamp, STORY_PUB_DATE_UNDATEABLE } from '../../lib/dateUtil';
import { googleFavIconUrl, storyDomainName } from '../../lib/urlUtil';
import { ReadItNowButton } from '../common/IconButton';
import SafelyFormattedNumber from '../common/SafelyFormattedNumber';

const localMessages = {
  undateable: { id: 'story.publishDate.undateable', defaultMessage: 'Undateable' },
  foci: { id: 'story.foci.list', defaultMessage: 'List of Subtopics {list}' },
  postCount: { id: 'story.postCount', defaultMessage: 'Posts' },
};

const ICON_STYLE = { margin: 0, padding: 0, width: 12, height: 12 };

class TopicStoryTable extends React.Component {
  sortableHeader = (sortKey, textMsg) => {
    const { onChangeSort, sortedBy } = this.props;
    const { formatMessage } = this.props.intl;
    let content;
    if (onChangeSort) {
      if (sortedBy === sortKey) {
        // currently sorted by this key
        content = (

            <FormattedMessage {...textMsg} />
            <ArrowDropDownIcon style={ICON_STYLE} />

        );
      } else {
        // link to sort by this key
        content = (
          <a
            href={`#${formatMessage(textMsg)}`}
            onClick={(e) => { e.preventDefault(); onChangeSort(sortKey); }}
            title={formatMessage(textMsg)}
          >
            <FormattedMessage {...textMsg} />

        );
      }
    } else {
      // not sortable
      content = <FormattedMessage {...textMsg} />;
    }
    return content;
  }

  render() {
    const { stories, showTweetCounts, onChangeFocusSelection, topicId, maxTitleLength, helpButton } = this.props;
    const { formatMessage, formatDate } = this.props.intl;
    const tweetHeader = showTweetCounts ? {this.sortableHeader('twitter', messages.tweetCounts)}	 : null;
    return (

              <FormattedMessage {...messages.storyTitle} />
              <FormattedMessage {...messages.media} />
              <FormattedMessage {...messages.storyDate} />{helpButton}
              {this.sortableHeader('inlink', messages.mediaInlinks)}
              <FormattedMessage {...messages.outlinks} />
              {this.sortableHeader('facebook', messages.facebookShares)}
              <FormattedMessage {...localMessages.postCount} />
              {tweetHeader}
              {}
              <FormattedMessage {...messages.focusHeader} />

            {stories.map((story, idx) => {
              const domain = storyDomainName(story);
              let dateToShow = null; // need to handle undateable stories
              let dateStyle = '';
              const title = maxTitleLength !== undefined ? `${story.title.substr(0, maxTitleLength)}...` : story.title;
              const tweetInfo = showTweetCounts ? <SafelyFormattedNumber value={story.simple_tweet_count} />	 : null;
              if (story.publish_date === STORY_PUB_DATE_UNDATEABLE) {
                dateToShow = formatMessage(localMessages.undateable);
                dateStyle = 'story-date-undateable';
              } else {
                dateToShow = formatDate(storyPubDateToTimestamp(story.publish_date));
                dateStyle = (story.date_is_reliable === 0) ? 'story-date-unreliable' : 'story-date-reliable';
                if (story.date_is_reliable === 0) {
                  dateToShow += '?';
                }
              }
              let listOfFoci = 'none';
              if (story.foci && story.foci.length > 0) {
                listOfFoci = (
                  story.foci.map((foci, i) => (

                      {!!i && ', '}
                      <LinkWithFilters
                        to={`/topics/${topicId}/summary`}
                        filters={{ focusId: foci.foci_id, timespanId: null }}
                        onClick={() => onChangeFocusSelection(foci.foci_id)}
                      >
                        { foci.name }
                      </LinkWithFilters>

                  )));
                // listOfFoci = intersperse(listOfFoci, ', ');
              }
              return (

                    <LinkWithFilters to={`/topics/${topicId}/stories/${story.stories_id}`}>
                      {title}
                    </LinkWithFilters>

                    <LinkWithFilters to={`/topics/${topicId}/media/${story.media_id}`}>
                      {story.media_name}
                    </LinkWithFilters>

                  <span className={`story-date ${dateStyle}`}>{dateToShow}
                  <SafelyFormattedNumber value={story.media_inlink_count} />
                  <SafelyFormattedNumber value={story.outlink_count} />
                  <SafelyFormattedNumber value={story.facebook_share_count} />
                  <SafelyFormattedNumber value={story.post_count} />
                  {tweetInfo}

                      <ReadItNowButton />

                  {listOfFoci}

              );
            })}

    );
  }
}

TopicStoryTable.propTypes = {
  stories: PropTypes.array.isRequired,
  showTweetCounts: PropTypes.bool,
  intl: PropTypes.object.isRequired,
  helpButton: PropTypes.node.isRequired,
  topicId: PropTypes.number, // not required as this table is now also used by query routine
  onChangeSort: PropTypes.func,
  onChangeFocusSelection: PropTypes.func,
  sortedBy: PropTypes.string,
  maxTitleLength: PropTypes.number,
};

export default injectIntl(
  withHelp(messages.pubDateTableHelpTitle, messages.pubDateTableHelpText)(
    TopicStoryTable
  )
);
<|endoftext|>


<commit_before>/*!
 * node-gcm
 * Copyright(c) 2013 Marcus Farkas <toothlessgear@finitebox.com>
 * MIT Licensed
 */
var Constants = require('./constants');
var req = require('request');
var debug = require('debug')('node-gcm');

function Sender(key , options) {
    if (!(this instanceof Sender)) {
        return new Sender(key, options);
    }

    this.key = key;
    this.options = options;
}

Sender.prototype.sendNoRetry = function(message, registrationIds, callback) {
    if(!callback) {
        callback = function() {};
    }

    var body = {},
        requestBody,
        post_options,
        post_req,
        timeout;

    body[Constants.JSON_REGISTRATION_IDS] = registrationIds;

    if (message.delayWhileIdle !== undefined) {
        body[Constants.PARAM_DELAY_WHILE_IDLE] = message.delayWhileIdle;
    }
    if (message.collapseKey !== undefined) {
        body[Constants.PARAM_COLLAPSE_KEY] = message.collapseKey;
    }
    if (message.timeToLive !== undefined) {
        body[Constants.PARAM_TIME_TO_LIVE] = message.timeToLive;
    }
    if (message.dryRun !== undefined) {
        body[Constants.PARAM_DRY_RUN] = message.dryRun;
    }
    if (Object.keys(message.data)) {
        body[Constants.PARAM_PAYLOAD_KEY] = message.data;
    }

    requestBody = JSON.stringify(body);

    post_options = {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json',
            'Content-length': Buffer.byteLength(requestBody, 'utf8'),
            'Authorization': 'key=' + this.key
        },
        uri: Constants.GCM_SEND_URI,
        body: requestBody
    };

    if (this.options && this.options.proxy) {
        post_options.proxy = this.options.proxy;
    }

    if (this.options && this.options.maxSockets) {
        post_options.maxSockets = this.options.maxSockets;
    }

    timeout = Constants.SOCKET_TIMEOUT;

    if (this.options && this.options.timeout) {
        timeout =  this.options.timeout;
    }

    post_options.timeout = timeout;

    post_req = req(post_options, function (err, res, resBody) {

        if (err) {
            return callback(err, null);

        }
        if (!res) {
            return callback('response is null', null);

        }
        if (res.statusCode >= 500) {
            debug('GCM service is unavailable (500)');
            return callback(res.statusCode, null);
        } else if (res.statusCode === 401) {
            debug('Unauthorized (401). Check that your API token is correct.');
            return callback(res.statusCode, null);
        } else if (res.statusCode !== 200) {
            debug('Invalid request (' + res.statusCode + '): ' + resBody);
            return callback(res.statusCode, null);
        }

        try {
            callback(null, JSON.parse(resBody));
        } catch (e) {
            debug("Error handling GCM response " + e);
            callback("error", null);
        }
    });
};

Sender.prototype.send = function(message, registrationIds, retries, backoff, callback) {
    if(typeof backoff == "undefined" || typeof backoff == "function") {
        callback = backoff;
        backoff = Constants.BACKOFF_INITIAL_DELAY;
    }
    if(typeof retries == "undefined" || typeof retries == "function") {
        callback = retries;
        retries = 5;
    }
    if(!callback) {
        callback = function() {};
    }

    if (!registrationIds.length) {
        debug('No RegistrationIds given!');
        callback('No RegistrationIds given!', null);
        return;
    }

    if(retries == 0) {
        return this.sendNoRetry(message, registrationIds, callback);
    }
    if (backoff > Constants.MAX_BACKOFF_DELAY) {
        backoff = Constants.MAX_BACKOFF_DELAY;
    }

    var self = this;

    this.sendNoRetry(message, registrationIds, function(err, result) {
        // if we err'd resend them all
        if (err) {
            return self.send(message, registrationIds, retries - 1, backoff * 2, callback);
        }

        // check for bad ids
        var unsentRegIds = [], regIdPositionMap = [];
        for (var i = 0; i < registrationIds.length; i++) {
            if (result.results[i].error === 'Unavailable') {
                regIdPositionMap.push(i);
                unsentRegIds.push(registrationIds[i]);
            }
        }

        if (!unsentRegIds.length) {
            return callback(err, result);
        }

        debug("Results received but not all successful", result);

        setTimeout(function () {
            debug("Retrying unsent registration ids");
            self.send(message, unsentRegIds, retries - 1, backoff * 2, function(err, retriedResult) {
                //If the recursive call err'd completely, our current result is the best available
                if(err) {
                    return callback(null, result);
                }

                //Map retried results onto their previous positions in the full array
                var retriedResults = retriedResult.results;
                for(var i = 0; i < result.results.length; i++) {
                    var oldIndex = regIdPositionMap[i];
                    result.results[oldIndex] = retriedResults[i];
                }
                callback(null, result);
            });
        }, backoff);
    });
};

module.exports = Sender;<commit_msg>Change interface of function to take `options` instead of `retries` and `backoff` This requries changing the defaulting of arguments<commit_after>/*!
 * node-gcm
 * Copyright(c) 2013 Marcus Farkas <toothlessgear@finitebox.com>
 * MIT Licensed
 */
var Constants = require('./constants');
var req = require('request');
var debug = require('debug')('node-gcm');

function Sender(key , options) {
    if (!(this instanceof Sender)) {
        return new Sender(key, options);
    }

    this.key = key;
    this.options = options;
}

Sender.prototype.sendNoRetry = function(message, registrationIds, callback) {
    if(!callback) {
        callback = function() {};
    }

    var body = {},
        requestBody,
        post_options,
        post_req,
        timeout;

    body[Constants.JSON_REGISTRATION_IDS] = registrationIds;

    if (message.delayWhileIdle !== undefined) {
        body[Constants.PARAM_DELAY_WHILE_IDLE] = message.delayWhileIdle;
    }
    if (message.collapseKey !== undefined) {
        body[Constants.PARAM_COLLAPSE_KEY] = message.collapseKey;
    }
    if (message.timeToLive !== undefined) {
        body[Constants.PARAM_TIME_TO_LIVE] = message.timeToLive;
    }
    if (message.dryRun !== undefined) {
        body[Constants.PARAM_DRY_RUN] = message.dryRun;
    }
    if (Object.keys(message.data)) {
        body[Constants.PARAM_PAYLOAD_KEY] = message.data;
    }

    requestBody = JSON.stringify(body);

    post_options = {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json',
            'Content-length': Buffer.byteLength(requestBody, 'utf8'),
            'Authorization': 'key=' + this.key
        },
        uri: Constants.GCM_SEND_URI,
        body: requestBody
    };

    if (this.options && this.options.proxy) {
        post_options.proxy = this.options.proxy;
    }

    if (this.options && this.options.maxSockets) {
        post_options.maxSockets = this.options.maxSockets;
    }

    timeout = Constants.SOCKET_TIMEOUT;

    if (this.options && this.options.timeout) {
        timeout =  this.options.timeout;
    }

    post_options.timeout = timeout;

    post_req = req(post_options, function (err, res, resBody) {

        if (err) {
            return callback(err, null);

        }
        if (!res) {
            return callback('response is null', null);

        }
        if (res.statusCode >= 500) {
            debug('GCM service is unavailable (500)');
            return callback(res.statusCode, null);
        } else if (res.statusCode === 401) {
            debug('Unauthorized (401). Check that your API token is correct.');
            return callback(res.statusCode, null);
        } else if (res.statusCode !== 200) {
            debug('Invalid request (' + res.statusCode + '): ' + resBody);
            return callback(res.statusCode, null);
        }

        try {
            callback(null, JSON.parse(resBody));
        } catch (e) {
            debug("Error handling GCM response " + e);
            callback("error", null);
        }
    });
};

Sender.prototype.send = function(message, registrationIds, options, callback) {
    var backoff, retries;

    if(typeof options == "object") {
        retries = options.retries || 5;
        backoff = options.backoff || Constants.BACKOFF_INITIAL_DELAY;
    }
    else if(typeof options == "function") {
        retries = 5;
        backoff = Constants.BACKOFF_INITIAL_DELAY;
        callback = options;
    }
    else if(typeof options == "number") {
        retries = options;
        backoff = Constants.BACKOFF_INITIAL_DELAY;
    }

    if(!callback) {
        callback = function() {};
    }

    if (!registrationIds.length) {
        debug('No RegistrationIds given!');
        callback('No RegistrationIds given!', null);
        return;
    }

    if(retries == 0) {
        return this.sendNoRetry(message, registrationIds, callback);
    }
    if (backoff > Constants.MAX_BACKOFF_DELAY) {
        backoff = Constants.MAX_BACKOFF_DELAY;
    }

    var self = this;

    this.sendNoRetry(message, registrationIds, function(err, result) {
        // if we err'd resend them all
        if (err) {
            return self.send(message, registrationIds, retries - 1, backoff * 2, callback);
        }

        // check for bad ids
        var unsentRegIds = [], regIdPositionMap = [];
        for (var i = 0; i < registrationIds.length; i++) {
            if (result.results[i].error === 'Unavailable') {
                regIdPositionMap.push(i);
                unsentRegIds.push(registrationIds[i]);
            }
        }

        if (!unsentRegIds.length) {
            return callback(err, result);
        }

        debug("Results received but not all successful", result);

        setTimeout(function () {
            debug("Retrying unsent registration ids");
            self.send(message, unsentRegIds, retries - 1, backoff * 2, function(err, retriedResult) {
                //If the recursive call err'd completely, our current result is the best available
                if(err) {
                    return callback(null, result);
                }

                //Map retried results onto their previous positions in the full array
                var retriedResults = retriedResult.results;
                for(var i = 0; i < result.results.length; i++) {
                    var oldIndex = regIdPositionMap[i];
                    result.results[oldIndex] = retriedResults[i];
                }
                callback(null, result);
            });
        }, backoff);
    });
};

module.exports = Sender;<|endoftext|>