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

'use strict';

/* global ga */

const FileUploader = require('./fileuploader');
const GithubAPI = require('./github');
const idb = require('idb-keyval');
const logger = require('./logger');
const ReportGenerator = require('../../../lighthouse-core/report/report-generator');

// TODO: We only need getFilenamePrefix from asset-saver. Tree shake!
const getFilenamePrefix = require('../../../lighthouse-core/lib/asset-saver').getFilenamePrefix;

const LH_CURRENT_VERSION = require('../../../package.json').version;
const APP_URL = `${location.origin}${location.pathname}`;

/**
 * Class to handle dynamic changes to the page when users view new reports.
 * @class
 */
class LighthouseViewerReport {

  constructor() {
    this.onShare = this.onShare.bind(this);
    this.onCopy = this.onCopy.bind(this);
    this.onCopyButtonClick = this.onCopyButtonClick.bind(this);
    this.onFileUpload = this.onFileUpload.bind(this);
    this.onPaste = this.onPaste.bind(this);
    this.onExportButtonClick = this.onExportButtonClick.bind(this);
    this.onExport = this.onExport.bind(this);
    this.onKeyDown = this.onKeyDown.bind(this);
    this.onInputChange = this.onInputChange.bind(this);

    this._copyAttempt = false;

    this.json = null;
    this.fileUploader = new FileUploader(this.onFileUpload);
    this.github = new GithubAPI();

    this._isNewReport = true;

    this.initUI();
    this.loadFromDeepLink();
  }

  initUI() {
    this.shareButton = document.querySelector('.js-share');
    if (this.shareButton) {
      this.shareButton.addEventListener('click', this.onShare);

      // Disable the share button after the user shares the gist or if we're loading
      // a gist from Github. In both cases, the gist is already shared :)
      if (this._isNewReport) {
        this.enableShareButton();
      } else {
        this.disableShareButton();
      }
    }

    const copyButton = document.querySelector('.js-copy');
    if (copyButton) {
      copyButton.addEventListener('click', this.onCopyButtonClick);
      document.addEventListener('copy', this.onCopy);
    }

    document.addEventListener('paste', this.onPaste);

    this.exportButton = document.querySelector('.js-export');
    if (this.exportButton) {
      this.exportButton.addEventListener('click', this.onExportButtonClick);
      const dropdown = document.querySelector('.export-dropdown');
      dropdown.addEventListener('click', this.onExport);
    }

    const gistURLInput = document.querySelector('.js-gist-url');
    if (gistURLInput) {
      gistURLInput.addEventListener('change', this.onInputChange);
    }
  }

  enableShareButton() {
    this.shareButton.classList.remove('disable');
    this.shareButton.disabled = false;
  }

  disableShareButton() {
    this.shareButton.classList.add('disable');
    this.shareButton.disabled = true;
  }

  closeExportDropdown() {
    this.exportButton.classList.remove('active');
  }

  loadFromDeepLink() {
    // Pull gist id from URL and render it.
    const params = new URLSearchParams(location.search);
    const gistId = params.get('gist');
    if (gistId) {
      logger.log('Fetching report from Github...', false);

      this.github.auth.ready.then(_ => {
        this.github.getGistFileContentAsJson(gistId).then(json => {
          logger.hide();

          this._isNewReport = false;
          this.replaceReportHTML(json.content);

          // Save fetched json and etag to IDB so we can use it later for 304
          // requests. This is done after replaceReportHTML, so we don't save
          // unrecognized JSON to IDB. replaceReportHTML will throw in that case.
          return idb.set(gistId, {etag: json.etag, content: json.content});
        }).catch(err => logger.error(err.message));
      });
    }
  }

  _validateReportJson(reportJson) {
    if (!reportJson.lighthouseVersion) {
      throw new Error('JSON file was not generated by Lighthouse');
    }

    // Leave off patch version in the comparison.
    const semverRe = new RegExp(/^(\d+)?\.(\d+)?\.(\d+)$/);
    const reportVersion = reportJson.lighthouseVersion.replace(semverRe, '$1.$2');
    const lhVersion = LH_CURRENT_VERSION.replace(semverRe, '$1.$2');

    if (reportVersion < lhVersion) {
      // TODO: figure out how to handler older reports. All permalinks to older
      // reports will start to throw this warning when the viewer rev's its
      // minor LH version.
      // See https://github.com/GoogleChrome/lighthouse/issues/1108
      logger.warn('Results may not display properly.\n' +
                  'Report was created with an earlier version of ' +
                  `Lighthouse (${reportJson.lighthouseVersion}). The latest ` +
                  `version is ${LH_CURRENT_VERSION}.`);
    }
  }

  replaceReportHTML(json) {
    this._validateReportJson(json);

    const reportGenerator = new ReportGenerator();

    let html;
    try {
      html = reportGenerator.generateHTML(json, 'viewer');
    } catch (err) {
      html = reportGenerator.renderException(err, json);
    }

    // Use only the results section of the full HTML page.
    const div = document.createElement('div');
    div.innerHTML = html;

    document.title = div.querySelector('title').textContent;

    html = div.querySelector('.js-report').outerHTML;

    this.json = json;

    // Remove the placeholder drop area UI once the user has interacted.
    this.fileUploader.removeDropzonePlaceholder();

    // Replace the HTML and hook up event listeners to the new DOM.
    document.querySelector('output').innerHTML = html;
    this.initUI();

    ga('send', 'event', 'report', 'view');
  }

  /**
   * Updates the page's HTML with contents of the JSON file passed in.
   * @param {!File} file
   * @return {!Promise<string>}
   * @throws file was not valid JSON generated by Lighthouse or an unknown file
   *     type of used.
   */
  onFileUpload(file) {
    return FileUploader.readFile(file).then(str => {
      let json;
      try {
        json = JSON.parse(str);
      } catch(e) {
        throw new Error('Could not parse JSON file.');
      }

      this._isNewReport = true;

      this.replaceReportHTML(json);
    }).catch(err => logger.error(err.message));
  }

  /**
   * Shares the current report by creating a gist on Github.
   * @return {!Promise<string>} id of the created gist.
   */
  onShare() {
    ga('send', 'event', 'report', 'share');

    // TODO: find and reuse existing json gist if one exists.
    return this.github.createGist(this.json).then(id => {
      ga('send', 'event', 'report', 'created');

      this.disableShareButton();
      history.pushState({}, null, `${APP_URL}?gist=${id}`);

      return id;
    }).catch(err => logger.log(err.message));
  }

  /**
   * Handler copy events.
   */
  onCopy(e) {
    // Only handle copy button presses (e.g. ignore the user copying page text).
    if (this._copyAttempt) {
      // We want to write our own data to the clipboard, not the user's text selection.
      e.preventDefault();
      e.clipboardData.setData('text/plain', JSON.stringify(this.json, null, 2));
      logger.log('Report copied to clipboard');
    }

    this._copyAttempt = false;
  }

  /**
   * Copies the report JSON to the clipboard (if supported by the browser).
   */
  onCopyButtonClick() {
    ga('send', 'event', 'report', 'copy');

    try {
      if (document.queryCommandSupported('copy')) {
        this._copyAttempt = true;

        // Note: In Safari 10.0.1, execCommand('copy') returns true if there's
        // a valid text selection on the page. See http://caniuse.com/#feat=clipboard.
        const successful = document.execCommand('copy');
        if (!successful) {
          this._copyAttempt = false; // Prevent event handler from seeing this as a copy attempt.
          logger.warn('Your browser does not support copy to clipboard.');
        }
      }
    } catch (err) {
      this._copyAttempt = false;
      logger.log(err.message);
    }
  }

  /**
   * Enables pasting a JSON report on the page.
   */
  onPaste(e) {
    e.preventDefault();

    // Try paste as gist URL.
    try {
      const url = new URL(e.clipboardData.getData('text'));
      this._loadFromGistURL(url);

      ga('send', 'event', 'report', 'paste-link');
    } catch (err) {
      // noop
    }

    // Try paste as json content.
    try {
      const json = JSON.parse(e.clipboardData.getData('text'));
      this.replaceReportHTML(json);

      ga('send', 'event', 'report', 'paste');
    } catch (err) {
      // noop
    }
  }

  /**
   * Click handler for export button.
   */
  onExportButtonClick(e) {
    e.target.classList.toggle('active');
    document.addEventListener('keydown', this.onKeyDown);
  }

  /**
   * Handles changes to the gist url input.
   */
  onInputChange(e) {
    e.stopPropagation();

    if (!e.target.value) {
      return;
    }

    try {
      this._loadFromGistURL(e.target.value);
    } catch (err) {
      logger.error('Invalid URL');
    }
  }

  /**
   * Updates URL with user's gist and loads from github.
   * @param {string} url Gist URL.
   */
  _loadFromGistURL(url) {
    try {
      url = new URL(url);

      if (url.origin !== 'https://gist.github.com') {
        logger.error('URL was not a gist');
        return;
      }

      const id = url.href.substring(url.href.lastIndexOf('/') + 1);
      history.pushState({}, null, `${APP_URL}?gist=${id}`);
      this.loadFromDeepLink();
    } catch (err) {
      logger.error('Invalid URL');
    }
  }

  /**
   * Downloads a file (blob) using a[download].
   * @param {Blob|File} The file to save.
   */
  _saveFile(blob) {
    const filename = getFilenamePrefix({
      url: this.json.url,
      date: new Date(this.json.generatedTime)
    });

    const ext = blob.type.match('json') ? '.json' : '.html';

    const a = document.createElement('a');
    a.download = `${filename}${ext}`;
    a.href = URL.createObjectURL(blob);
    a.click();

    setTimeout(_ => URL.revokeObjectURL(a.href), 500); // cleanup.
  }

  /**
   * Handler for "export as" button.
   */
  onExport(e) {
    if (!e.target.dataset.action) {
      return;
    }

    switch (e.target.dataset.action) {
      case 'print':
        window.print();
        break;
      case 'save-json':
        const jsonStr = JSON.stringify(this.json, null, 2);
        this._saveFile(new Blob([jsonStr], {type: 'application/json'}));
        break;
      case 'save-html':
        const reportGenerator = new ReportGenerator();
        try {
          const htmlStr = reportGenerator.generateHTML(this.json, 'cli');
          this._saveFile(new Blob([htmlStr], {type: 'text/html'}));
        } catch (err) {
          logger.error('Could not export as HTML.');
        }
        break;
    }

    this.closeExportDropdown();
    document.removeEventListener('keydown', this.onKeyDown);
  }

  /**
   * Keydown handler for the document.
   */
  onKeyDown(e) {
    if (e.keyCode === 27) { // ESC
      this.closeExportDropdown();
    }
  }
}

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

'use strict';

/* global ga */

const FileUploader = require('./fileuploader');
const GithubAPI = require('./github');
const idb = require('idb-keyval');
const logger = require('./logger');
const ReportGenerator = require('../../../lighthouse-core/report/report-generator');

// TODO: We only need getFilenamePrefix from asset-saver. Tree shake!
const getFilenamePrefix = require('../../../lighthouse-core/lib/asset-saver').getFilenamePrefix;

const LH_CURRENT_VERSION = require('../../../package.json').version;
const APP_URL = `${location.origin}${location.pathname}`;

/**
 * Class to handle dynamic changes to the page when users view new reports.
 * @class
 */
class LighthouseViewerReport {

  constructor() {
    this.onShare = this.onShare.bind(this);
    this.onCopy = this.onCopy.bind(this);
    this.onCopyButtonClick = this.onCopyButtonClick.bind(this);
    this.onFileUpload = this.onFileUpload.bind(this);
    this.onPaste = this.onPaste.bind(this);
    this.onExportButtonClick = this.onExportButtonClick.bind(this);
    this.onExport = this.onExport.bind(this);
    this.onKeyDown = this.onKeyDown.bind(this);
    this.onInputChange = this.onInputChange.bind(this);

    this._copyAttempt = false;

    this.json = null;
    this.fileUploader = new FileUploader(this.onFileUpload);
    this.github = new GithubAPI();

    this._isNewReport = true;

    this.initUI();
    this.loadFromDeepLink();
  }

  initUI() {
    this.shareButton = document.querySelector('.js-share');
    if (this.shareButton) {
      this.shareButton.addEventListener('click', this.onShare);

      // Disable the share button after the user shares the gist or if we're loading
      // a gist from Github. In both cases, the gist is already shared :)
      if (this._isNewReport) {
        this.enableShareButton();
      } else {
        this.disableShareButton();
      }
    }

    const copyButton = document.querySelector('.js-copy');
    if (copyButton) {
      copyButton.addEventListener('click', this.onCopyButtonClick);
      document.addEventListener('copy', this.onCopy);
    }

    document.addEventListener('paste', this.onPaste);

    this.exportButton = document.querySelector('.js-export');
    if (this.exportButton) {
      this.exportButton.addEventListener('click', this.onExportButtonClick);
      const dropdown = document.querySelector('.export-dropdown');
      dropdown.addEventListener('click', this.onExport);
    }

    const gistURLInput = document.querySelector('.js-gist-url');
    if (gistURLInput) {
      gistURLInput.addEventListener('change', this.onInputChange);
    }
  }

  enableShareButton() {
    this.shareButton.classList.remove('disable');
    this.shareButton.disabled = false;
  }

  disableShareButton() {
    this.shareButton.classList.add('disable');
    this.shareButton.disabled = true;
  }

  closeExportDropdown() {
    this.exportButton.classList.remove('active');
  }

  loadFromDeepLink() {
    // Pull gist id from URL and render it.
    const params = new URLSearchParams(location.search);
    const gistId = params.get('gist');
    if (gistId) {
      logger.log('Fetching report from Github...', false);

      this.github.auth.ready.then(_ => {
        this.github.getGistFileContentAsJson(gistId).then(json => {
          logger.hide();

          this._isNewReport = false;
          this.replaceReportHTML(json.content);

          // Save fetched json and etag to IDB so we can use it later for 304
          // requests. This is done after replaceReportHTML, so we don't save
          // unrecognized JSON to IDB. replaceReportHTML will throw in that case.
          return idb.set(gistId, {etag: json.etag, content: json.content});
        }).catch(err => logger.error(err.message));
      });
    }
  }

  _validateReportJson(reportJson) {
    if (!reportJson.lighthouseVersion) {
      throw new Error('JSON file was not generated by Lighthouse');
    }

    // Leave off patch version in the comparison.
    const semverRe = new RegExp(/^(\d+)?\.(\d+)?\.(\d+)$/);
    const reportVersion = reportJson.lighthouseVersion.replace(semverRe, '$1.$2');
    const lhVersion = LH_CURRENT_VERSION.replace(semverRe, '$1.$2');

    if (reportVersion < lhVersion) {
      // TODO: figure out how to handler older reports. All permalinks to older
      // reports will start to throw this warning when the viewer rev's its
      // minor LH version.
      // See https://github.com/GoogleChrome/lighthouse/issues/1108
      logger.warn('Results may not display properly.\n' +
                  'Report was created with an earlier version of ' +
                  `Lighthouse (${reportJson.lighthouseVersion}). The latest ` +
                  `version is ${LH_CURRENT_VERSION}.`);
    }
  }

  replaceReportHTML(json) {
    this._validateReportJson(json);

    const reportGenerator = new ReportGenerator();

    let html;
    try {
      html = reportGenerator.generateHTML(json, 'viewer');
    } catch (err) {
      html = reportGenerator.renderException(err, json);
    }

    // Use only the results section of the full HTML page.
    const div = document.createElement('div');
    div.innerHTML = html;

    document.title = div.querySelector('title').textContent;

    html = div.querySelector('.js-report').outerHTML;

    this.json = json;

    // Remove the placeholder drop area UI once the user has interacted.
    this.fileUploader.removeDropzonePlaceholder();

    // Replace the HTML and hook up event listeners to the new DOM.
    document.querySelector('output').innerHTML = html;
    this.initUI();

    ga('send', 'event', 'report', 'view');
  }

  /**
   * Updates the page's HTML with contents of the JSON file passed in.
   * @param {!File} file
   * @return {!Promise<string>}
   * @throws file was not valid JSON generated by Lighthouse or an unknown file
   *     type of used.
   */
  onFileUpload(file) {
    return FileUploader.readFile(file).then(str => {
      let json;
      try {
        json = JSON.parse(str);
      } catch(e) {
        throw new Error('Could not parse JSON file.');
      }

      this._isNewReport = true;

      this.replaceReportHTML(json);
    }).catch(err => logger.error(err.message));
  }

  /**
   * Shares the current report by creating a gist on Github.
   * @return {!Promise<string>} id of the created gist.
   */
  onShare() {
    ga('send', 'event', 'report', 'share');

    // TODO: find and reuse existing json gist if one exists.
    return this.github.createGist(this.json).then(id => {
      ga('send', 'event', 'report', 'created');

      this.disableShareButton();
      history.pushState({}, null, `${APP_URL}?gist=${id}`);

      return id;
    }).catch(err => logger.log(err.message));
  }

  /**
   * Handler copy events.
   */
  onCopy(e) {
    // Only handle copy button presses (e.g. ignore the user copying page text).
    if (this._copyAttempt) {
      // We want to write our own data to the clipboard, not the user's text selection.
      e.preventDefault();
      e.clipboardData.setData('text/plain', JSON.stringify(this.json, null, 2));
      logger.log('Report copied to clipboard');
    }

    this._copyAttempt = false;
  }

  /**
   * Copies the report JSON to the clipboard (if supported by the browser).
   */
  onCopyButtonClick() {
    ga('send', 'event', 'report', 'copy');

    try {
      if (document.queryCommandSupported('copy')) {
        this._copyAttempt = true;

        // Note: In Safari 10.0.1, execCommand('copy') returns true if there's
        // a valid text selection on the page. See http://caniuse.com/#feat=clipboard.
        const successful = document.execCommand('copy');
        if (!successful) {
          this._copyAttempt = false; // Prevent event handler from seeing this as a copy attempt.
          logger.warn('Your browser does not support copy to clipboard.');
        }
      }
    } catch (err) {
      this._copyAttempt = false;
      logger.log(err.message);
    }
  }

  /**
   * Enables pasting a JSON report on the page.
   */
  onPaste(e) {
    e.preventDefault();

    // Try paste as gist URL.
    try {
      const url = new URL(e.clipboardData.getData('text'));
      this._loadFromGistURL(url);

      ga('send', 'event', 'report', 'paste-link');
    } catch (err) {
      // noop
    }

    // Try paste as json content.
    try {
      const json = JSON.parse(e.clipboardData.getData('text'));
      this.replaceReportHTML(json);

      ga('send', 'event', 'report', 'paste');
    } catch (err) {
      // noop
    }
  }

  /**
   * Click handler for export button.
   */
  onExportButtonClick(e) {
    e.target.classList.toggle('active');
    document.addEventListener('keydown', this.onKeyDown);
  }

  /**
   * Handles changes to the gist url input.
   */
  onInputChange(e) {
    e.stopPropagation();

    if (!e.target.value) {
      return;
    }

    try {
      this._loadFromGistURL(e.target.value);
    } catch (err) {
      logger.error('Invalid URL');
    }
  }

  /**
   * Updates URL with user's gist and loads from github.
   * @param {string} url Gist URL.
   */
  _loadFromGistURL(url) {
    try {
      url = new URL(url);

      if (url.origin !== 'https://gist.github.com') {
        logger.error('URL was not a gist');
        return;
      }

      const match = url.pathname.match(/[a-f0-9]{5,}/);
      if (match) {
        history.pushState({}, null, `${APP_URL}?gist=${match[0]}`);
        this.loadFromDeepLink();
      }
    } catch (err) {
      logger.error('Invalid URL');
    }
  }

  /**
   * Downloads a file (blob) using a[download].
   * @param {Blob|File} The file to save.
   */
  _saveFile(blob) {
    const filename = getFilenamePrefix({
      url: this.json.url,
      date: new Date(this.json.generatedTime)
    });

    const ext = blob.type.match('json') ? '.json' : '.html';

    const a = document.createElement('a');
    a.download = `${filename}${ext}`;
    a.href = URL.createObjectURL(blob);
    a.click();

    setTimeout(_ => URL.revokeObjectURL(a.href), 500); // cleanup.
  }

  /**
   * Handler for "export as" button.
   */
  onExport(e) {
    if (!e.target.dataset.action) {
      return;
    }

    switch (e.target.dataset.action) {
      case 'print':
        window.print();
        break;
      case 'save-json':
        const jsonStr = JSON.stringify(this.json, null, 2);
        this._saveFile(new Blob([jsonStr], {type: 'application/json'}));
        break;
      case 'save-html':
        const reportGenerator = new ReportGenerator();
        try {
          const htmlStr = reportGenerator.generateHTML(this.json, 'cli');
          this._saveFile(new Blob([htmlStr], {type: 'text/html'}));
        } catch (err) {
          logger.error('Could not export as HTML.');
        }
        break;
    }

    this.closeExportDropdown();
    document.removeEventListener('keydown', this.onKeyDown);
  }

  /**
   * Keydown handler for the document.
   */
  onKeyDown(e) {
    if (e.keyCode === 27) { // ESC
      this.closeExportDropdown();
    }
  }
}

module.exports = LighthouseViewerReport;
<|endoftext|>


<commit_before>(function ($) {
  function SlickGridPager(dataView, grid, $container) {
    var $status;

    function init() {
      dataView.onPagingInfoChanged.subscribe(function (e, pagingInfo) {
        updatePager(pagingInfo);
      });

      constructPagerUI();
      updatePager(dataView.getPagingInfo());
    }

    function getNavState() {
      var cannotLeaveEditMode = !Slick.GlobalEditorLock.commitCurrentEdit();
      var pagingInfo = dataView.getPagingInfo();
      var lastPage = pagingInfo.totalPages - 1;

      return {
        canGotoFirst: !cannotLeaveEditMode && pagingInfo.pageSize != 0 && pagingInfo.pageNum > 0,
        canGotoLast: !cannotLeaveEditMode && pagingInfo.pageSize != 0 && pagingInfo.pageNum != lastPage,
        canGotoPrev: !cannotLeaveEditMode && pagingInfo.pageSize != 0 && pagingInfo.pageNum > 0,
        canGotoNext: !cannotLeaveEditMode && pagingInfo.pageSize != 0 && pagingInfo.pageNum < lastPage,
        pagingInfo: pagingInfo
      }
    }

    function setPageSize(n) {
      dataView.setRefreshHints({
        isFilterUnchanged: true
      });
      dataView.setPagingOptions({pageSize: n});
    }

    function gotoFirst() {
      if (getNavState().canGotoFirst) {
        dataView.setPagingOptions({pageNum: 0});
      }
    }

    function gotoLast() {
      var state = getNavState();
      if (state.canGotoLast) {
        dataView.setPagingOptions({pageNum: state.pagingInfo.totalPages - 1});
      }
    }

    function gotoPrev() {
      var state = getNavState();
      if (state.canGotoPrev) {
        dataView.setPagingOptions({pageNum: state.pagingInfo.pageNum - 1});
      }
    }

    function gotoNext() {
      var state = getNavState();
      if (state.canGotoNext) {
        dataView.setPagingOptions({pageNum: state.pagingInfo.pageNum + 1});
      }
    }

    function constructPagerUI() {
      $container.empty();

      var $nav = $("").appendTo($container);
      var $settings = $("").appendTo($container);
      $status = $("").appendTo($container);

      $settings
          .append("Show: AllAuto2550100");

      $settings.find("a[data]").click(function (e) {
        var pagesize = $(e.target).attr("data");
        if (pagesize != undefined) {
          if (pagesize == -1) {
            // TODO: produce the number of completely visible rows instead (height of the slickgrid viewport may be partially clipping the last row)
            var vp = grid.getViewport();
            setPageSize(vp.bottom - vp.top + 1);
          } else {
            setPageSize(parseInt(pagesize));
          }
        }
      });

      var icon_prefix = "";

      $(icon_prefix + "ui-icon-lightbulb" + icon_suffix)
          .click(function () {
            $(".slick-pager-settings-expanded").toggle()
          })
          .appendTo($settings);

      $(icon_prefix + "ui-icon-seek-first" + icon_suffix)
          .click(gotoFirst)
          .appendTo($nav);

      $(icon_prefix + "ui-icon-seek-prev" + icon_suffix)
          .click(gotoPrev)
          .appendTo($nav);

      $(icon_prefix + "ui-icon-seek-next" + icon_suffix)
          .click(gotoNext)
          .appendTo($nav);

      $(icon_prefix + "ui-icon-seek-end" + icon_suffix)
          .click(gotoLast)
          .appendTo($nav);

      $container.find(".ui-icon-container")
          .hover(function () {
            $(this).toggleClass("ui-state-hover");
          });

      $container.children().wrapAll("
");
    }

    function updatePager(pagingInfo) {
      var state = getNavState();

      $container.find(".slick-pager-nav span").removeClass("ui-state-disabled");
      if (!state.canGotoFirst) {
        $container.find(".ui-icon-seek-first").addClass("ui-state-disabled");
      }
      if (!state.canGotoLast) {
        $container.find(".ui-icon-seek-end").addClass("ui-state-disabled");
      }
      if (!state.canGotoNext) {
        $container.find(".ui-icon-seek-next").addClass("ui-state-disabled");
      }
      if (!state.canGotoPrev) {
        $container.find(".ui-icon-seek-prev").addClass("ui-state-disabled");
      }

      if (pagingInfo.pageSize == 0) {
        var totalRowsCount = dataView.getItems().length;
        var visibleRowsCount = pagingInfo.totalRows;
        if (visibleRowsCount < totalRowsCount) {
          $status.text("Showing " + visibleRowsCount + " of " + totalRowsCount + " rows");
        } else {
          $status.text("Showing all " + totalRowsCount + " rows");
        }
        $status.text("Showing all " + pagingInfo.totalRows + " rows");
      } else {
        $status.text("Showing page " + (pagingInfo.pageNum + 1) + " of " + pagingInfo.totalPages);
      }
    }

    init();
  }

  // Slick.Controls.Pager
  $.extend(true, window, { Slick:{ Controls:{ Pager:SlickGridPager }}});
})(jQuery);
<commit_msg>Fix latest merge: remove duplicate 'Show N items' message construction<commit_after>(function ($) {
  function SlickGridPager(dataView, grid, $container) {
    var $status;

    function init() {
      dataView.onPagingInfoChanged.subscribe(function (e, pagingInfo) {
        updatePager(pagingInfo);
      });

      constructPagerUI();
      updatePager(dataView.getPagingInfo());
    }

    function getNavState() {
      var cannotLeaveEditMode = !Slick.GlobalEditorLock.commitCurrentEdit();
      var pagingInfo = dataView.getPagingInfo();
      var lastPage = pagingInfo.totalPages - 1;

      return {
        canGotoFirst: !cannotLeaveEditMode && pagingInfo.pageSize != 0 && pagingInfo.pageNum > 0,
        canGotoLast: !cannotLeaveEditMode && pagingInfo.pageSize != 0 && pagingInfo.pageNum != lastPage,
        canGotoPrev: !cannotLeaveEditMode && pagingInfo.pageSize != 0 && pagingInfo.pageNum > 0,
        canGotoNext: !cannotLeaveEditMode && pagingInfo.pageSize != 0 && pagingInfo.pageNum < lastPage,
        pagingInfo: pagingInfo
      }
    }

    function setPageSize(n) {
      dataView.setRefreshHints({
        isFilterUnchanged: true
      });
      dataView.setPagingOptions({pageSize: n});
    }

    function gotoFirst() {
      if (getNavState().canGotoFirst) {
        dataView.setPagingOptions({pageNum: 0});
      }
    }

    function gotoLast() {
      var state = getNavState();
      if (state.canGotoLast) {
        dataView.setPagingOptions({pageNum: state.pagingInfo.totalPages - 1});
      }
    }

    function gotoPrev() {
      var state = getNavState();
      if (state.canGotoPrev) {
        dataView.setPagingOptions({pageNum: state.pagingInfo.pageNum - 1});
      }
    }

    function gotoNext() {
      var state = getNavState();
      if (state.canGotoNext) {
        dataView.setPagingOptions({pageNum: state.pagingInfo.pageNum + 1});
      }
    }

    function constructPagerUI() {
      $container.empty();

      var $nav = $("").appendTo($container);
      var $settings = $("").appendTo($container);
      $status = $("").appendTo($container);

      $settings
          .append("Show: AllAuto2550100");

      $settings.find("a[data]").click(function (e) {
        var pagesize = $(e.target).attr("data");
        if (pagesize != undefined) {
          if (pagesize == -1) {
            // TODO: produce the number of completely visible rows instead (height of the slickgrid viewport may be partially clipping the last row)
            var vp = grid.getViewport();
            setPageSize(vp.bottom - vp.top + 1);
          } else {
            setPageSize(parseInt(pagesize));
          }
        }
      });

      var icon_prefix = "";

      $(icon_prefix + "ui-icon-lightbulb" + icon_suffix)
          .click(function () {
            $(".slick-pager-settings-expanded").toggle()
          })
          .appendTo($settings);

      $(icon_prefix + "ui-icon-seek-first" + icon_suffix)
          .click(gotoFirst)
          .appendTo($nav);

      $(icon_prefix + "ui-icon-seek-prev" + icon_suffix)
          .click(gotoPrev)
          .appendTo($nav);

      $(icon_prefix + "ui-icon-seek-next" + icon_suffix)
          .click(gotoNext)
          .appendTo($nav);

      $(icon_prefix + "ui-icon-seek-end" + icon_suffix)
          .click(gotoLast)
          .appendTo($nav);

      $container.find(".ui-icon-container")
          .hover(function () {
            $(this).toggleClass("ui-state-hover");
          });

      $container.children().wrapAll("
");
    }

    function updatePager(pagingInfo) {
      var state = getNavState();

      $container.find(".slick-pager-nav span").removeClass("ui-state-disabled");
      if (!state.canGotoFirst) {
        $container.find(".ui-icon-seek-first").addClass("ui-state-disabled");
      }
      if (!state.canGotoLast) {
        $container.find(".ui-icon-seek-end").addClass("ui-state-disabled");
      }
      if (!state.canGotoNext) {
        $container.find(".ui-icon-seek-next").addClass("ui-state-disabled");
      }
      if (!state.canGotoPrev) {
        $container.find(".ui-icon-seek-prev").addClass("ui-state-disabled");
      }

      if (pagingInfo.pageSize == 0) {
        var totalRowsCount = dataView.getItems().length;
        var visibleRowsCount = pagingInfo.totalRows;
        if (visibleRowsCount < totalRowsCount) {
          $status.text("Showing " + visibleRowsCount + " of " + totalRowsCount + " rows");
        } else {
          $status.text("Showing all " + totalRowsCount + " rows");
        }
      } else {
        $status.text("Showing page " + (pagingInfo.pageNum + 1) + " of " + pagingInfo.totalPages);
      }
    }

    init();
  }

  // Slick.Controls.Pager
  $.extend(true, window, { Slick:{ Controls:{ Pager:SlickGridPager }}});
})(jQuery);
<|endoftext|>


<commit_before>/* eslint-disable react/no-danger */
import React, { PureComponent } from 'react'
import PropTypes from 'prop-types'
import moment from 'moment'
import { Link } from 'react-router'
import { before, css, hover, media, modifier, parent, select } from '../../styles/jss'
import * as s from '../../styles/jso'
import BackgroundImage from '../assets/BackgroundImage'
import ImageAsset from '../assets/ImageAsset'
import { ArrowIcon } from '../assets/Icons'
import { RoundedRect } from '../buttons/Buttons'
import ArtistInviteSubmissionsContainer from '../../containers/ArtistInviteSubmissionsContainer'

const gridContainerStyle = css(
  s.alignTop,
  s.bgcF2,
  s.fontSize14,
  s.fullWidth,
  s.mb10,
  s.sansRegular,
  s.transitionBgColor,
  media(
    s.minBreak2,
    s.mr20,
    s.mb20,
    // .1 is MS Edge not calc-ing rems correctly
    { width: 'calc(50% - 10.1px)' },
    select(':nth-child(2n)', s.mr0),
  ),
  media(
    s.minBreak4,
    s.mr40,
    s.mb40,
    { width: 'calc(50% - 20.2px)' },
  ),
  hover(s.bgcE5),
)

const imageContainerStyle = css(
  s.maxViewWidth,
  s.flex,
  s.justifyCenter,
  s.itemsCenter,
  s.relative,
  { height: 235, margin: '0 auto' },
  media(s.minBreak2, { height: 220 }, parent('.ArtistInvitesDetail', s.mb40, { height: 555 }, media(s.minBreak3, s.mb60))),
  parent('a:hover', select('> .BackgroundImage::before', { backgroundColor: 'rgba(0, 0, 0, 0.5)' })),
  parent('.ArtistInvitesDetail', s.mb20),
)

const logoImageStyle = css(
  { maxHeight: 155 },
  s.px20,
  s.zIndex1,
  media(s.minBreak2, { maxHeight: 140 }, parent('.ArtistInvitesDetail', { maxHeight: 190 })),
)

const contentContainerStyle = css(
  { height: 305 },
  s.p10,
  s.relative,
  media(s.minBreak3, s.py30, s.px20),
  media(s.minBreak4, { height: 250 }),
)

const titleStyle = css(
  s.sansBlack,
  s.fontSize24,
  media(s.minBreak3, s.mb20),
  parent(
    '.ArtistInvites',
    s.truncate,
  ),
  parent(
    '.ArtistInvitesDetail',
    media(s.minBreak2, { marginTop: -5 }),
    media(s.minBreak3, s.mb0, s.fontSize38, { marginTop: -10 }),
  ),
)

const inviteTypeStyle = css(
  s.colorA,
  s.my0,
  s.truncate,
  media(s.minBreak3, { maxWidth: 'calc(100% - 220px)' }),
  parent(
    '.ArtistInvitesDetail',
    s.fontSize24,
    s.sansLight,
    media(s.minBreak3, s.fontSize38, s.fit),
  ),
)

const textStatusStyle = css(
  s.fontSize24,
  s.mb0,
  s.mt20,
  s.truncate,
  modifier('.closed', { color: '#fe0404' }),
  modifier('.open', { color: '#00d100' }),
  modifier('.preview', { color: '#0409fe' }),
  modifier('.selecting', { color: '#ffb100' }),
  modifier('.upcoming', { color: '#c000ff' }),
  parent('.ArtistInvitesDetail', s.sansLight, media(s.minBreak3, s.fontSize38)),
)

const bulletStatusStyle = css(
  { ...textStatusStyle },
  s.fontSize14,
  s.mt10,
  before(
    {
      borderRadius: 5,
      content: '""',
      height: 10,
      width: 10,
    },
    s.inlineBlock,
    s.mr20,
  ),
  modifier('.closed', before({ backgroundColor: '#fe0404' })),
  modifier('.open', before(s.bgcGreen)),
  modifier('.preview', before({ backgroundColor: '#0409fe' })),
  modifier('.selecting', before({ backgroundColor: '#ffb100' })),
  modifier('.upcoming', before({ backgroundColor: '#c000ff' })),
  media(s.minBreak3, s.absolute, s.mt0, { left: 'calc(100% - 230px)', top: 85 }),
)

const dateRangeStyle = css(
  s.colorA,
  s.my0,
  s.truncate,
  media(s.minBreak3, s.absolute, { left: 'calc(100% - 200px)', top: 105 }),
  parent(
    '.ArtistInvitesDetail',
    s.fontSize24,
    s.sansLight,
    { left: 'auto', position: 'inherit', top: 'auto' },
    media(s.minBreak3, s.fontSize38),
  ),

)

const shortDescriptionStyle = css(
  media(s.minBreak3, s.mt40),
)

const getStatusText = (status) => {
  switch (status) {
    case 'closed':
      return 'Invite Closed'
    case 'open':
      return 'Open For Submissions'
    case 'preview':
      return 'Preview'
    case 'selecting':
      return 'Selections In Progress'
    case 'upcoming':
      return 'Upcoming'
    default:
      return null
  }
}

const renderTextStatus = status => (
  <p className={`${textStatusStyle} ${status}`}>
    {getStatusText(status)}

)

const renderBulletStatus = status => (
  <p className={`${bulletStatusStyle} ${status}`}>
    {getStatusText(status)}

)

function getSecondsRemaining(closedAt) {
  const remaining = moment(closedAt).unix() - moment().unix()
  return remaining < 0 ? 0 : remaining
}

class ArtistInviteCountDown extends PureComponent {
  static propTypes = {
    status: PropTypes.string.isRequired,
    openedAt: PropTypes.string.isRequired,
    closedAt: PropTypes.string.isRequired,
    className: PropTypes.object.isRequired,
  }

  constructor(props) {
    super(props)
    this.state = { secondsRemaining: getSecondsRemaining(props.closedAt) }
    this.timer = 0;
  }

  componentDidMount() {
    const { status } = this.props
    if (status === 'open') {
      this.interval = setInterval(this.tick, 1000)
    }
  }

  componentWillUnmount() {
    if (this.interval) {
      clearInterval(this.interval)
    }
  }

  tick = () => {
    const { closedAt } = this.props
    this.setState({ secondsRemaining: getSecondsRemaining(closedAt) })
  }

  countDown() {
    const { secondsRemaining } = this.state
    const pad = n => `${n}`.padStart(2, '0')
    const r = moment.duration(secondsRemaining, 'seconds')
    if (r.days() > 1) {
      return `${Math.floor(r.asDays())} Days Remaining`
    }
    return `${pad(r.hours())}:${pad(r.minutes())}:${pad(r.seconds())} Remaining`
  }

  renderByStatus() {
    const { status, openedAt } = this.props
    switch (status) {
      case 'preview':
      case 'upcoming':
        return ''
      case 'open':
        return this.countDown()
      case 'selecting':
        return 'Hold Tight'
      case 'closed':
        return moment(openedAt).format('MMMM YYYY')
      default:
        return ''
    }
  }

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

{this.renderByStatus()}

    )
  }

}

export const ArtistInviteGrid = ({
  closedAt,
  dpi,
  headerImage,
  inviteType,
  logoImage,
  openedAt,
  shortDescription,
  slug,
  status,
  title,
}, { onClickArtistInviteDetail }) => (
  <Link to={`/artist-invites/${slug}`} onClick={onClickArtistInviteDetail} className={gridContainerStyle}>

        <BackgroundImage className="hasOverlay3" dpi={dpi} sources={headerImage} />
        <ImageAsset className={logoImageStyle} src={logoImage.getIn(['optimized', 'url'])} />

{title}

{inviteType}

        {renderBulletStatus(status)}
        <ArtistInviteCountDown
          className={dateRangeStyle}
          status={status}
          openedAt={openedAt}
          closedAt={closedAt}
        />

  </Link>
)
ArtistInviteGrid.propTypes = {
  closedAt: PropTypes.string.isRequired,
  dpi: PropTypes.string.isRequired,
  headerImage: PropTypes.object.isRequired,
  inviteType: PropTypes.string.isRequired,
  logoImage: PropTypes.object.isRequired,
  openedAt: PropTypes.string.isRequired,
  shortDescription: PropTypes.string.isRequired,
  slug: PropTypes.string.isRequired,
  status: PropTypes.string.isRequired,
  title: PropTypes.string.isRequired,
}
ArtistInviteGrid.contextTypes = {
  onClickArtistInviteDetail: PropTypes.func.isRequired,
}

const detailContainerStyle = css(
  s.fullWidth,
  s.px10,
  s.mxAuto,
  media(s.minBreak2, s.px20),
  media(s.minBreak4, s.px0),
  select('& .ArrowIcon', { transform: 'rotate(90deg)' }),
  select(
    '& .RoundedRect',
    s.my20,
    media(
      s.minBreak2,
      modifier('.ScrollButton', s.my40),
      modifier('.SubmitButton', s.mt0, s.mb30),
    ),
    media(
      s.minBreak3,
      modifier('.ScrollButton', s.my60),
      modifier('.SubmitButton', { marginBottom: 50 }),
    ),
  ),
)

const detailContentContainerStyle = css(
  s.maxSiteWidthPadded, { margin: '0 auto' },
  media(s.maxBreak4, s.pr0, s.pl0),
)

const contentColumnStyle = css(
  s.alignTop,
  s.fit,
  s.inlineBlock,
  media(
    s.minBreak2,
    s.mr20,
    s.mb20,
    { width: 'calc(50% - 10px)' },
    select(':nth-child(2n)', s.mr0),
  ),
  media(
    s.minBreak3,
    s.mr40,
    s.mb40,
    { width: 'calc(50% - 20px)' },
  ),
)

const guideStyle = css(
  s.colorA,
  s.fontSize14,
  select('& .GuideHeader', s.fontSize24, s.mb20, s.sansBlack, s.truncate, media(s.minBreak3, s.fontSize38)),
  select('& .GuideContent', media(s.minBreak2, s.mb30), media(s.minBreak3, s.mb60)),
)

export const ArtistInviteDetail = ({
  closedAt,
  description,
  dpi,
  guide,
  hasSubmissions,
  headerImage,
  inviteType,
  isLoggedIn,
  links,
  logoImage,
  openedAt,
  slug,
  status,
  submissionBodyBlock,
  title,
}, {
  onClickScrollToContent,
  onClickSubmit,
}) => (

        <BackgroundImage className="hasOverlay3" dpi={dpi} sources={headerImage} />
        <ImageAsset className={logoImageStyle} src={logoImage.getIn(['optimized', 'url'])} />

{title}

{inviteType}

          {renderTextStatus(status)}
          <ArtistInviteCountDown
            className={dateRangeStyle}
            status={status}
            openedAt={openedAt}
            closedAt={closedAt}
          />
          {links.size !== 0 && hasSubmissions &&
            <RoundedRect className="ScrollButton GreenBorder" onClick={onClickScrollToContent}>
              <ArrowIcon />
              See Submissions
            </RoundedRect>
          }

          {status === 'open' && isLoggedIn &&
            <RoundedRect className="SubmitButton Green isXL" onClick={onClickSubmit}>
              SUBMIT
            </RoundedRect>
          }
          {guide.map(g => (
            <div className={guideStyle} key={`guide_${g.get('title')}`}>

{g.get('title')}

          ))}

    <ArtistInviteSubmissionsContainer links={links} slug={slug} status={status} />

)
ArtistInviteDetail.propTypes = {
  closedAt: PropTypes.string.isRequired,
  description: PropTypes.string.isRequired,
  dpi: PropTypes.string.isRequired,
  guide: PropTypes.object.isRequired,
  hasSubmissions: PropTypes.bool.isRequired,
  headerImage: PropTypes.object.isRequired,
  inviteType: PropTypes.string.isRequired,
  isLoggedIn: PropTypes.bool.isRequired,
  links: PropTypes.object.isRequired,
  logoImage: PropTypes.object.isRequired,
  openedAt: PropTypes.string.isRequired,
  slug: PropTypes.string.isRequired,
  status: PropTypes.string.isRequired,
  submissionBodyBlock: PropTypes.string.isRequired,
  title: PropTypes.string.isRequired,
}
ArtistInviteDetail.contextTypes = {
  onClickScrollToContent: PropTypes.func.isRequired,
  onClickSubmit: PropTypes.func.isRequired,
}
<commit_msg>Fix conditional to show countdown to use asDays.<commit_after>/* eslint-disable react/no-danger */
import React, { PureComponent } from 'react'
import PropTypes from 'prop-types'
import moment from 'moment'
import { Link } from 'react-router'
import { before, css, hover, media, modifier, parent, select } from '../../styles/jss'
import * as s from '../../styles/jso'
import BackgroundImage from '../assets/BackgroundImage'
import ImageAsset from '../assets/ImageAsset'
import { ArrowIcon } from '../assets/Icons'
import { RoundedRect } from '../buttons/Buttons'
import ArtistInviteSubmissionsContainer from '../../containers/ArtistInviteSubmissionsContainer'

const gridContainerStyle = css(
  s.alignTop,
  s.bgcF2,
  s.fontSize14,
  s.fullWidth,
  s.mb10,
  s.sansRegular,
  s.transitionBgColor,
  media(
    s.minBreak2,
    s.mr20,
    s.mb20,
    // .1 is MS Edge not calc-ing rems correctly
    { width: 'calc(50% - 10.1px)' },
    select(':nth-child(2n)', s.mr0),
  ),
  media(
    s.minBreak4,
    s.mr40,
    s.mb40,
    { width: 'calc(50% - 20.2px)' },
  ),
  hover(s.bgcE5),
)

const imageContainerStyle = css(
  s.maxViewWidth,
  s.flex,
  s.justifyCenter,
  s.itemsCenter,
  s.relative,
  { height: 235, margin: '0 auto' },
  media(s.minBreak2, { height: 220 }, parent('.ArtistInvitesDetail', s.mb40, { height: 555 }, media(s.minBreak3, s.mb60))),
  parent('a:hover', select('> .BackgroundImage::before', { backgroundColor: 'rgba(0, 0, 0, 0.5)' })),
  parent('.ArtistInvitesDetail', s.mb20),
)

const logoImageStyle = css(
  { maxHeight: 155 },
  s.px20,
  s.zIndex1,
  media(s.minBreak2, { maxHeight: 140 }, parent('.ArtistInvitesDetail', { maxHeight: 190 })),
)

const contentContainerStyle = css(
  { height: 305 },
  s.p10,
  s.relative,
  media(s.minBreak3, s.py30, s.px20),
  media(s.minBreak4, { height: 250 }),
)

const titleStyle = css(
  s.sansBlack,
  s.fontSize24,
  media(s.minBreak3, s.mb20),
  parent(
    '.ArtistInvites',
    s.truncate,
  ),
  parent(
    '.ArtistInvitesDetail',
    media(s.minBreak2, { marginTop: -5 }),
    media(s.minBreak3, s.mb0, s.fontSize38, { marginTop: -10 }),
  ),
)

const inviteTypeStyle = css(
  s.colorA,
  s.my0,
  s.truncate,
  media(s.minBreak3, { maxWidth: 'calc(100% - 220px)' }),
  parent(
    '.ArtistInvitesDetail',
    s.fontSize24,
    s.sansLight,
    media(s.minBreak3, s.fontSize38, s.fit),
  ),
)

const textStatusStyle = css(
  s.fontSize24,
  s.mb0,
  s.mt20,
  s.truncate,
  modifier('.closed', { color: '#fe0404' }),
  modifier('.open', { color: '#00d100' }),
  modifier('.preview', { color: '#0409fe' }),
  modifier('.selecting', { color: '#ffb100' }),
  modifier('.upcoming', { color: '#c000ff' }),
  parent('.ArtistInvitesDetail', s.sansLight, media(s.minBreak3, s.fontSize38)),
)

const bulletStatusStyle = css(
  { ...textStatusStyle },
  s.fontSize14,
  s.mt10,
  before(
    {
      borderRadius: 5,
      content: '""',
      height: 10,
      width: 10,
    },
    s.inlineBlock,
    s.mr20,
  ),
  modifier('.closed', before({ backgroundColor: '#fe0404' })),
  modifier('.open', before(s.bgcGreen)),
  modifier('.preview', before({ backgroundColor: '#0409fe' })),
  modifier('.selecting', before({ backgroundColor: '#ffb100' })),
  modifier('.upcoming', before({ backgroundColor: '#c000ff' })),
  media(s.minBreak3, s.absolute, s.mt0, { left: 'calc(100% - 230px)', top: 85 }),
)

const dateRangeStyle = css(
  s.colorA,
  s.my0,
  s.truncate,
  media(s.minBreak3, s.absolute, { left: 'calc(100% - 200px)', top: 105 }),
  parent(
    '.ArtistInvitesDetail',
    s.fontSize24,
    s.sansLight,
    { left: 'auto', position: 'inherit', top: 'auto' },
    media(s.minBreak3, s.fontSize38),
  ),

)

const shortDescriptionStyle = css(
  media(s.minBreak3, s.mt40),
)

const getStatusText = (status) => {
  switch (status) {
    case 'closed':
      return 'Invite Closed'
    case 'open':
      return 'Open For Submissions'
    case 'preview':
      return 'Preview'
    case 'selecting':
      return 'Selections In Progress'
    case 'upcoming':
      return 'Upcoming'
    default:
      return null
  }
}

const renderTextStatus = status => (
  <p className={`${textStatusStyle} ${status}`}>
    {getStatusText(status)}

)

const renderBulletStatus = status => (
  <p className={`${bulletStatusStyle} ${status}`}>
    {getStatusText(status)}

)

function getSecondsRemaining(closedAt) {
  const remaining = moment(closedAt).unix() - moment().unix()
  return remaining < 0 ? 0 : remaining
}

class ArtistInviteCountDown extends PureComponent {
  static propTypes = {
    status: PropTypes.string.isRequired,
    openedAt: PropTypes.string.isRequired,
    closedAt: PropTypes.string.isRequired,
    className: PropTypes.object.isRequired,
  }

  constructor(props) {
    super(props)
    this.state = { secondsRemaining: getSecondsRemaining(props.closedAt) }
    this.timer = 0;
  }

  componentDidMount() {
    const { status } = this.props
    if (status === 'open') {
      this.interval = setInterval(this.tick, 1000)
    }
  }

  componentWillUnmount() {
    if (this.interval) {
      clearInterval(this.interval)
    }
  }

  tick = () => {
    const { closedAt } = this.props
    this.setState({ secondsRemaining: getSecondsRemaining(closedAt) })
  }

  countDown() {
    const { secondsRemaining } = this.state
    const pad = n => `${n}`.padStart(2, '0')
    const r = moment.duration(secondsRemaining, 'seconds')
    if (Math.floor(r.asDays()) > 1) {
      return `${Math.floor(r.asDays())} Days Remaining`
    }
    return `${pad(r.hours())}:${pad(r.minutes())}:${pad(r.seconds())} Remaining`
  }

  renderByStatus() {
    const { status, openedAt } = this.props
    switch (status) {
      case 'preview':
      case 'upcoming':
        return ''
      case 'open':
        return this.countDown()
      case 'selecting':
        return 'Hold Tight'
      case 'closed':
        return moment(openedAt).format('MMMM YYYY')
      default:
        return ''
    }
  }

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

{this.renderByStatus()}

    )
  }

}

export const ArtistInviteGrid = ({
  closedAt,
  dpi,
  headerImage,
  inviteType,
  logoImage,
  openedAt,
  shortDescription,
  slug,
  status,
  title,
}, { onClickArtistInviteDetail }) => (
  <Link to={`/artist-invites/${slug}`} onClick={onClickArtistInviteDetail} className={gridContainerStyle}>

        <BackgroundImage className="hasOverlay3" dpi={dpi} sources={headerImage} />
        <ImageAsset className={logoImageStyle} src={logoImage.getIn(['optimized', 'url'])} />

{title}

{inviteType}

        {renderBulletStatus(status)}
        <ArtistInviteCountDown
          className={dateRangeStyle}
          status={status}
          openedAt={openedAt}
          closedAt={closedAt}
        />

  </Link>
)
ArtistInviteGrid.propTypes = {
  closedAt: PropTypes.string.isRequired,
  dpi: PropTypes.string.isRequired,
  headerImage: PropTypes.object.isRequired,
  inviteType: PropTypes.string.isRequired,
  logoImage: PropTypes.object.isRequired,
  openedAt: PropTypes.string.isRequired,
  shortDescription: PropTypes.string.isRequired,
  slug: PropTypes.string.isRequired,
  status: PropTypes.string.isRequired,
  title: PropTypes.string.isRequired,
}
ArtistInviteGrid.contextTypes = {
  onClickArtistInviteDetail: PropTypes.func.isRequired,
}

const detailContainerStyle = css(
  s.fullWidth,
  s.px10,
  s.mxAuto,
  media(s.minBreak2, s.px20),
  media(s.minBreak4, s.px0),
  select('& .ArrowIcon', { transform: 'rotate(90deg)' }),
  select(
    '& .RoundedRect',
    s.my20,
    media(
      s.minBreak2,
      modifier('.ScrollButton', s.my40),
      modifier('.SubmitButton', s.mt0, s.mb30),
    ),
    media(
      s.minBreak3,
      modifier('.ScrollButton', s.my60),
      modifier('.SubmitButton', { marginBottom: 50 }),
    ),
  ),
)

const detailContentContainerStyle = css(
  s.maxSiteWidthPadded, { margin: '0 auto' },
  media(s.maxBreak4, s.pr0, s.pl0),
)

const contentColumnStyle = css(
  s.alignTop,
  s.fit,
  s.inlineBlock,
  media(
    s.minBreak2,
    s.mr20,
    s.mb20,
    { width: 'calc(50% - 10px)' },
    select(':nth-child(2n)', s.mr0),
  ),
  media(
    s.minBreak3,
    s.mr40,
    s.mb40,
    { width: 'calc(50% - 20px)' },
  ),
)

const guideStyle = css(
  s.colorA,
  s.fontSize14,
  select('& .GuideHeader', s.fontSize24, s.mb20, s.sansBlack, s.truncate, media(s.minBreak3, s.fontSize38)),
  select('& .GuideContent', media(s.minBreak2, s.mb30), media(s.minBreak3, s.mb60)),
)

export const ArtistInviteDetail = ({
  closedAt,
  description,
  dpi,
  guide,
  hasSubmissions,
  headerImage,
  inviteType,
  isLoggedIn,
  links,
  logoImage,
  openedAt,
  slug,
  status,
  submissionBodyBlock,
  title,
}, {
  onClickScrollToContent,
  onClickSubmit,
}) => (

        <BackgroundImage className="hasOverlay3" dpi={dpi} sources={headerImage} />
        <ImageAsset className={logoImageStyle} src={logoImage.getIn(['optimized', 'url'])} />

{title}

{inviteType}

          {renderTextStatus(status)}
          <ArtistInviteCountDown
            className={dateRangeStyle}
            status={status}
            openedAt={openedAt}
            closedAt={closedAt}
          />
          {links.size !== 0 && hasSubmissions &&
            <RoundedRect className="ScrollButton GreenBorder" onClick={onClickScrollToContent}>
              <ArrowIcon />
              See Submissions
            </RoundedRect>
          }

          {status === 'open' && isLoggedIn &&
            <RoundedRect className="SubmitButton Green isXL" onClick={onClickSubmit}>
              SUBMIT
            </RoundedRect>
          }
          {guide.map(g => (
            <div className={guideStyle} key={`guide_${g.get('title')}`}>

{g.get('title')}

          ))}

    <ArtistInviteSubmissionsContainer links={links} slug={slug} status={status} />

)
ArtistInviteDetail.propTypes = {
  closedAt: PropTypes.string.isRequired,
  description: PropTypes.string.isRequired,
  dpi: PropTypes.string.isRequired,
  guide: PropTypes.object.isRequired,
  hasSubmissions: PropTypes.bool.isRequired,
  headerImage: PropTypes.object.isRequired,
  inviteType: PropTypes.string.isRequired,
  isLoggedIn: PropTypes.bool.isRequired,
  links: PropTypes.object.isRequired,
  logoImage: PropTypes.object.isRequired,
  openedAt: PropTypes.string.isRequired,
  slug: PropTypes.string.isRequired,
  status: PropTypes.string.isRequired,
  submissionBodyBlock: PropTypes.string.isRequired,
  title: PropTypes.string.isRequired,
}
ArtistInviteDetail.contextTypes = {
  onClickScrollToContent: PropTypes.func.isRequired,
  onClickSubmit: PropTypes.func.isRequired,
}
<|endoftext|>


<commit_before>import React from 'react';
import { FormattedMessage, injectIntl } from 'react-intl';
import { connect } from 'react-redux';
import { Grid, Row, Col } from 'react-flexbox-grid/lib';
import Link from 'react-router/lib/Link';
import { push } from 'react-router-redux';
import Title from 'react-title-component';
import { fetchSourceFeeds, scrapeSourceFeeds } from '../../../actions/sourceActions';
import composeAsyncContainer from '../../common/AsyncContainer';
import MediaSourceIcon from '../../common/icons/MediaSourceIcon';
import SourceFeedTable from '../SourceFeedTable';
import messages from '../../../resources/messages';
import { DownloadButton } from '../../common/IconButton';
import AppButton from '../../common/AppButton';
import Permissioned from '../../common/Permissioned';
import { PERMISSION_MEDIA_EDIT } from '../../../lib/auth';
import { updateFeedback } from '../../../actions/appActions';
import { SOURCE_SCRAPE_STATE_QUEUED, SOURCE_SCRAPE_STATE_RUNNING } from '../../../reducers/sources/sources/selected/sourceDetails';

const localMessages = {
  sourceFeedsTitle: { id: 'source.details.feeds.title', defaultMessage: '{name}: Feeds' },
  scrapeFeeds: { id: 'source.details.feeds.scrape', defaultMessage: 'Scrape for New Feeds' },
  scraping: { id: 'source.deatils.feeds.scraping', defaultMessage: 'We\'ve started to scrape this source' },
  scrapeFailed: { id: 'source.deatils.feeds.failed', defaultMessage: 'Sorry, for some reason we couldn\'t start the scraping job' },
};

class SourceFeedContainer extends React.Component {

  componentWillReceiveProps(nextProps) {
    const { sourceId, fetchData } = this.props;
    if ((nextProps.sourceId !== sourceId)) {
      fetchData(nextProps.sourceId);
    }
  }

  downloadCsv = () => {
    const { sourceId } = this.props;
    const url = `/api/sources/${sourceId}/feeds/feeds.csv`;
    window.location = url;
  }

  render() {
    const { sourceId, sourceName, feeds, scrapeFeeds } = this.props;
    const { formatMessage } = this.props.intl;
    const titleHandler = parentTitle => `${sourceName} | ${parentTitle}`;
    const content = null;
    if (feeds === undefined) {
      return (

          { content }

      );
    }
    return (
      <Grid className="details source-details">
        <Title render={titleHandler} />
        <Row>
          <Col lg={11} xs={11}>

              <MediaSourceIcon height={32} />
              <Link to={`/sources/${sourceId}`} >
                <FormattedMessage {...localMessages.sourceFeedsTitle} values={{ name: sourceName }} />
              </Link>

            <Permissioned onlyRole={PERMISSION_MEDIA_EDIT}>
              <AppButton
                className="source-scrape-feeds-button"
                label={formatMessage(localMessages.scrapeFeeds)}
                primary
                onClick={scrapeFeeds}
              />
            </Permissioned>
          </Col>
          <Col lg={1} xs={1}>

              <DownloadButton tooltip={formatMessage(messages.download)} onClick={this.downloadCsv} />

          </Col>
        </Row>
        <Row>
          <Col lg={6} xs={12}>
            <SourceFeedTable feeds={feeds} />
          </Col>
        </Row>
      </Grid>
    );
  }

}

SourceFeedContainer.propTypes = {
  intl: React.PropTypes.object.isRequired,
  // from dispatch
  fetchData: React.PropTypes.func.isRequired,
  asyncFetch: React.PropTypes.func.isRequired,
  scrapeFeeds: React.PropTypes.func.isRequired,
  // from context
  params: React.PropTypes.object.isRequired,       // params from router
  sourceId: React.PropTypes.number.isRequired,
  sourceName: React.PropTypes.string.isRequired,
  // from state
  fetchStatus: React.PropTypes.string.isRequired,
  source: React.PropTypes.object,
  feeds: React.PropTypes.array,
  feedcount: React.PropTypes.number,
};

const mapStateToProps = (state, ownProps) => ({
  sourceId: parseInt(ownProps.params.sourceId, 10),
  sourceName: state.sources.sources.selected.sourceDetails.name,
  fetchStatus: state.sources.sources.selected.feed.fetchStatus,
  feeds: state.sources.sources.selected.feed.list,
  feedcount: state.sources.sources.selected.feed.count,
});

const mapDispatchToProps = (dispatch, ownProps) => ({
  fetchData: (sourceId) => {
    dispatch(fetchSourceFeeds(sourceId));
  },
  asyncFetch: () => {
    dispatch(fetchSourceFeeds(ownProps.params.sourceId));
  },
  scrapeFeeds: () => {
    dispatch(scrapeSourceFeeds(ownProps.params.sourceId))
      .then((results) => {
        if ((results.job_state.state === SOURCE_SCRAPE_STATE_QUEUED) ||
          (results.job_state.state === SOURCE_SCRAPE_STATE_RUNNING)) {
          dispatch(updateFeedback({ open: true, message: ownProps.intl.formatMessage(localMessages.scraping) }));
          dispatch(push(`/sources/${ownProps.params.sourceId}`));
        } else {
          dispatch(updateFeedback({ open: true, message: ownProps.intl.formatMessage(localMessages.scrapeFailed) }));
        }
      });
  },
});

export default
  injectIntl(
    connect(mapStateToProps, mapDispatchToProps)(
      composeAsyncContainer(
        SourceFeedContainer
      )
    )
  );
<commit_msg>better feedback after requesting scrape job (#256)<commit_after>import React from 'react';
import { FormattedMessage, injectIntl } from 'react-intl';
import { connect } from 'react-redux';
import { Grid, Row, Col } from 'react-flexbox-grid/lib';
import Link from 'react-router/lib/Link';
import { push } from 'react-router-redux';
import Title from 'react-title-component';
import { fetchSourceFeeds, scrapeSourceFeeds, fetchSourceDetails } from '../../../actions/sourceActions';
import composeAsyncContainer from '../../common/AsyncContainer';
import MediaSourceIcon from '../../common/icons/MediaSourceIcon';
import SourceFeedTable from '../SourceFeedTable';
import messages from '../../../resources/messages';
import { DownloadButton } from '../../common/IconButton';
import AppButton from '../../common/AppButton';
import Permissioned from '../../common/Permissioned';
import { PERMISSION_MEDIA_EDIT } from '../../../lib/auth';
import { updateFeedback } from '../../../actions/appActions';
import { SOURCE_SCRAPE_STATE_QUEUED, SOURCE_SCRAPE_STATE_RUNNING } from '../../../reducers/sources/sources/selected/sourceDetails';

const localMessages = {
  sourceFeedsTitle: { id: 'source.details.feeds.title', defaultMessage: '{name}: Feeds' },
  scrapeFeeds: { id: 'source.details.feeds.scrape', defaultMessage: 'Scrape for New Feeds' },
  scraping: { id: 'source.deatils.feeds.scraping', defaultMessage: 'We\'ve started to scrape this source' },
  scrapeFailed: { id: 'source.deatils.feeds.failed', defaultMessage: 'Sorry, for some reason we couldn\'t start the scraping job' },
};

class SourceFeedContainer extends React.Component {

  componentWillReceiveProps(nextProps) {
    const { sourceId, fetchData } = this.props;
    if ((nextProps.sourceId !== sourceId)) {
      fetchData(nextProps.sourceId);
    }
  }

  downloadCsv = () => {
    const { sourceId } = this.props;
    const url = `/api/sources/${sourceId}/feeds/feeds.csv`;
    window.location = url;
  }

  render() {
    const { sourceId, sourceName, feeds, scrapeFeeds } = this.props;
    const { formatMessage } = this.props.intl;
    const titleHandler = parentTitle => `${sourceName} | ${parentTitle}`;
    const content = null;
    if (feeds === undefined) {
      return (

          { content }

      );
    }
    return (
      <Grid className="details source-details">
        <Title render={titleHandler} />
        <Row>
          <Col lg={11} xs={11}>

              <MediaSourceIcon height={32} />
              <Link to={`/sources/${sourceId}`} >
                <FormattedMessage {...localMessages.sourceFeedsTitle} values={{ name: sourceName }} />
              </Link>

            <Permissioned onlyRole={PERMISSION_MEDIA_EDIT}>
              <AppButton
                className="source-scrape-feeds-button"
                label={formatMessage(localMessages.scrapeFeeds)}
                primary
                onClick={scrapeFeeds}
              />
            </Permissioned>
          </Col>
          <Col lg={1} xs={1}>

              <DownloadButton tooltip={formatMessage(messages.download)} onClick={this.downloadCsv} />

          </Col>
        </Row>
        <Row>
          <Col lg={6} xs={12}>
            <SourceFeedTable feeds={feeds} />
          </Col>
        </Row>
      </Grid>
    );
  }

}

SourceFeedContainer.propTypes = {
  intl: React.PropTypes.object.isRequired,
  // from dispatch
  fetchData: React.PropTypes.func.isRequired,
  asyncFetch: React.PropTypes.func.isRequired,
  scrapeFeeds: React.PropTypes.func.isRequired,
  // from context
  params: React.PropTypes.object.isRequired,       // params from router
  sourceId: React.PropTypes.number.isRequired,
  sourceName: React.PropTypes.string.isRequired,
  // from state
  fetchStatus: React.PropTypes.string.isRequired,
  source: React.PropTypes.object,
  feeds: React.PropTypes.array,
  feedcount: React.PropTypes.number,
};

const mapStateToProps = (state, ownProps) => ({
  sourceId: parseInt(ownProps.params.sourceId, 10),
  sourceName: state.sources.sources.selected.sourceDetails.name,
  fetchStatus: state.sources.sources.selected.feed.fetchStatus,
  feeds: state.sources.sources.selected.feed.list,
  feedcount: state.sources.sources.selected.feed.count,
});

const mapDispatchToProps = (dispatch, ownProps) => ({
  fetchData: (sourceId) => {
    dispatch(fetchSourceFeeds(sourceId));
  },
  asyncFetch: () => {
    dispatch(fetchSourceFeeds(ownProps.params.sourceId));
  },
  scrapeFeeds: () => {
    dispatch(scrapeSourceFeeds(ownProps.params.sourceId))
      .then((results) => {
        if ((results.job_state.state === SOURCE_SCRAPE_STATE_QUEUED) ||
          (results.job_state.state === SOURCE_SCRAPE_STATE_RUNNING)) {
          dispatch(updateFeedback({ open: true, message: ownProps.intl.formatMessage(localMessages.scraping) }));
          // update the source so the user sees the new scrape status
          dispatch(fetchSourceDetails(ownProps.params.sourceId))
            .then(() => dispatch(push(`/sources/${ownProps.params.sourceId}`)));
        } else {
          dispatch(updateFeedback({ open: true, message: ownProps.intl.formatMessage(localMessages.scrapeFailed) }));
        }
      });
  },
});

export default
  injectIntl(
    connect(mapStateToProps, mapDispatchToProps)(
      composeAsyncContainer(
        SourceFeedContainer
      )
    )
  );
<|endoftext|>


<commit_before>// Hides elements if the result of the call is the expected
// Html use:
//  Define an a element with:
//      data-hideable: css selector indicating what are you going to hide
//      data-callable-url: url that ajax will call using post
//      expected-result: a string representing the value to hide
// Example:
//  Code: ~/Views/Shared/_LegalInfo.cshtml

$(function () {
    $('a[data-hideable]').click(function () {
        var tohide = $(this).data('hideable');
        var expected = $(this).data('expected-result');
        $.ajax({
            type: 'POST',
            contentType: 'application/json; charset=utf-8',
            url: $(this).data('callable-url'),
            success: function (response) {
                if (response == expected) $(tohide).fadeOut('fast');
            }
        });
    });
});

/// Defines a fixeable menu that toggles elements
/// Html use:
///  1. For the menu:
///      Define two divs one with id="fixeable-menu" and another
///      with id="fixed-menu". The first one contains the menu.
///      The other one is empty.
///      The links must have a data-toggle tag indicating what divs
///      they will toggle.
///  2. For the contents:
///      Inside a div with a property data-showable="true" put all
///      the contents with hidden (display:none) divs and a
///      data-hide-me="true" to allow toggling after each click.
/// Example:
///  Live: http://thescienceofcode.azurewebsites.net/Courses (and open a course)
///  Code: ~/Views/Courses/Show.cshtml

// This part controls the fixable menu
var fixed_menu = -1;
var fixed_status = false;
$(function () {
    // No fixeable menu?
    if ($('#fixeable-menu').offset() == undefined) return;

    // Set position
    fixed_menu = $('#fixeable-menu').offset().top - 45;
    // Copy menu
    $('#fixed-menu').html($('#fixeable-menu').html());

    // Hide and show fixed menu!
    $(window).scroll(function () {
        if ($(window).scrollTop() > fixed_menu) {
            if (fixed_status) return;
            fixed_status = true;
            $('#fixed-menu').slideDown('fast');
            $('a[data-toggle]').removeClass('btn-primary');
            $('a[data-toggle]').addClass('btn-default');
        } else {
            if (!fixed_status) return;
            fixed_status = false;
            $('#fixed-menu').fadeOut('fast');
            $('a[data-toggle]').removeClass('btn-default');
            $('a[data-toggle]').addClass('btn-primary');
        }
    });
});

// This part controls the toggleable elements
var called = false;
$(function () {
    $('a[data-toggle]').click(function () {
        // Sets a min-h for the first time that content is showed
        if (!called) {
            called = true;
            $('[data-showable="true"]').addClass('min-h-content');
        }
        var current = $('a[data-toggle=' + $(this).data('toggle') + ']');

        // Update menu
        var show = true;
        if (current.hasClass('active')) {
            current.removeClass('active');
            show = false;
        }
        $('a[data-toggle]').removeClass('active');

        // Go to top
        $('html, body').animate({
            scrollTop: fixed_menu - 40
        });
        // And toggle contents
        $.when($('[data-hide-me="true"]').fadeOut('fast')).then(function () {
            if (!show) return;

            var element = current.attr('data-toggle');
            current.addClass('active');
            $(element).fadeIn('fast');
        });
    });
});

/// Sets on click for all a[data-url]
///
/// options.load: determines if a gif is showed when loading (by default is a nested <img>).
/// options.push: determines if url must be pushed into history.
/// options.top: position to scroll to.
/// options.div: div to replace result.
/// options.func: handler.
/// options.precall: a function to call before ajax.
/// options.postcall: a function to call after ajax.
/// options.method: GET or POST for ajax call
/// options.contentType: for ajax result
/// options.getCall: default URL getter.

function setOnClickDataUrl(o) {
    var options = o || {};
    options.load = options.load || false;
    options.push = options.push || false;
    options.top = options.top || 0;
    options.div = options.div || '';
    options.func = options.func || defaultLoad;
    options.precall = options.precall || undefined;
    options.postcall = options.postcall || undefined;
    options.method = options.method || 'POST';
    options.contentType = options.contentType || 'text/html; charset=utf-8';
    options.getCall = options.getCall || function (id) { return $(id).data('url'); };

    $('a[data-url]').on('click', function () {
        // History
        if (options.push) window.history.pushState("", "", $(this).data('url'));
        // Precall
        if (options.precall) options.precall();

        // Scroll
        $('html, body').animate({
            scrollTop: top
        });

        // Call
        options.func($(this), options);

        // Postcall
        if (options.postcall) options.postcall;
    });
}

window.onpopstate = function (event) {
    // Trigger url load
    document.location = document.location;
};

/// Default a[data-url] handler
/// If push is on, calls a default metadata update, that takes info from #desc and #keyword
var reqs = [];
function defaultLoad(e, options) {
    // Abort pending reqs
    for (i = 0; i < reqs.length; i++) {
        reqs[i].abort();
    }
    reqs.length = 0;

    // Show loading gif
    if (options.load) jQuery(e).find("img").addClass('visible');

    // Ajax call
    var r = $.ajax({
        // Config
        type: options.method,
        contentType: options.contentType,
        url: options.getCall(e),
        success: function (response) {
            // Show
            $(options.div).html(response);

            // Reconfigure buttons
            setOnClickDataUrl(options);

            // History
            if (options.push) updateMetadata();
            // Hide loading gif
            if (options.load) jQuery(e).find("img").removeClass('visible');

            // Force FB & TW
            try {
                FB.XFBML.parse();
                twttr.widgets.load();
            } catch (ex) { }
        }
    });
    reqs.push(r);
}

function updateMetadata() {
    $('meta[name=description]').remove();
    $('head').append('<meta name="description" content="' + $('#desc').html() + '" />');
    $('meta[name=keywords]').remove();
    $('head').append('<meta name="keywords" content="' + $('#keywords').html() + '" />');
}

<commit_msg>JS metadata options<commit_after>// Hides elements if the result of the call is the expected
// Html use:
//  Define an a element with:
//      data-hideable: css selector indicating what are you going to hide
//      data-callable-url: url that ajax will call using post
//      expected-result: a string representing the value to hide
// Example:
//  Code: ~/Views/Shared/_LegalInfo.cshtml

$(function () {
    $('a[data-hideable]').click(function () {
        var tohide = $(this).data('hideable');
        var expected = $(this).data('expected-result');
        $.ajax({
            type: 'POST',
            contentType: 'application/json; charset=utf-8',
            url: $(this).data('callable-url'),
            success: function (response) {
                if (response == expected) $(tohide).fadeOut('fast');
            }
        });
    });
});

/// Defines a fixeable menu that toggles elements
/// Html use:
///  1. For the menu:
///      Define two divs one with id="fixeable-menu" and another
///      with id="fixed-menu". The first one contains the menu.
///      The other one is empty.
///      The links must have a data-toggle tag indicating what divs
///      they will toggle.
///  2. For the contents:
///      Inside a div with a property data-showable="true" put all
///      the contents with hidden (display:none) divs and a
///      data-hide-me="true" to allow toggling after each click.
/// Example:
///  Live: http://thescienceofcode.azurewebsites.net/Courses (and open a course)
///  Code: ~/Views/Courses/Show.cshtml

// This part controls the fixable menu
var fixed_menu = -1;
var fixed_status = false;
$(function () {
    // No fixeable menu?
    if ($('#fixeable-menu').offset() == undefined) return;

    // Set position
    fixed_menu = $('#fixeable-menu').offset().top - 45;
    // Copy menu
    $('#fixed-menu').html($('#fixeable-menu').html());

    // Hide and show fixed menu!
    $(window).scroll(function () {
        if ($(window).scrollTop() > fixed_menu) {
            if (fixed_status) return;
            fixed_status = true;
            $('#fixed-menu').slideDown('fast');
            $('a[data-toggle]').removeClass('btn-primary');
            $('a[data-toggle]').addClass('btn-default');
        } else {
            if (!fixed_status) return;
            fixed_status = false;
            $('#fixed-menu').fadeOut('fast');
            $('a[data-toggle]').removeClass('btn-default');
            $('a[data-toggle]').addClass('btn-primary');
        }
    });
});

// This part controls the toggleable elements
var called = false;
$(function () {
    $('a[data-toggle]').click(function () {
        // Sets a min-h for the first time that content is showed
        if (!called) {
            called = true;
            $('[data-showable="true"]').addClass('min-h-content');
        }
        var current = $('a[data-toggle=' + $(this).data('toggle') + ']');

        // Update menu
        var show = true;
        if (current.hasClass('active')) {
            current.removeClass('active');
            show = false;
        }
        $('a[data-toggle]').removeClass('active');

        // Go to top
        $('html, body').animate({
            scrollTop: fixed_menu - 40
        });
        // And toggle contents
        $.when($('[data-hide-me="true"]').fadeOut('fast')).then(function () {
            if (!show) return;

            var element = current.attr('data-toggle');
            current.addClass('active');
            $(element).fadeIn('fast');
        });
    });
});

/// Sets on click for all a[data-url]
///
/// options.load: determines if a gif is showed when loading (by default is a nested <img>).
/// options.push: determines if url must be pushed into history.
/// options.top: position to scroll to.
/// options.div: div to replace result.
/// options.func: handler.
/// options.precall: a function to call before ajax.
/// options.postcall: a function to call after ajax.
/// options.method: GET or POST for ajax call.
/// options.contentType: for ajax result.
/// options.getCall: default URL getter.
/// options.updateMetadata: a function to update metadata.
/// options.metadataDesc: id that contains new desc info.
/// options.metadataKeywords: id that contains new keywords info.

function setOnClickDataUrl(o) {
    var options = o || {};
    options.load = options.load || false;
    options.push = options.push || false;
    options.top = options.top || 0;
    options.div = options.div || '';
    options.func = options.func || defaultLoad;
    options.precall = options.precall || undefined;
    options.postcall = options.postcall || undefined;
    options.method = options.method || 'POST';
    options.contentType = options.contentType || 'text/html; charset=utf-8';
    options.getCall = options.getCall || function (id) { return $(id).data('url'); };
    options.updateMetadata = options.updateMetadata || updateMetadata;
    options.metadataDesc = options.metadataDesc || '#desc';
    options.metadataKeywords = options.metadataKeywords || '#keywords';

    $('a[data-url]').on('click', function () {
        // History
        if (options.push) window.history.pushState("", "", $(this).data('url'));
        // Precall
        if (options.precall) options.precall();

        // Scroll
        $('html, body').animate({
            scrollTop: top
        });

        // Call
        options.func($(this), options);

        // Postcall
        if (options.postcall) options.postcall;
    });
}

window.onpopstate = function (event) {
    // Trigger url load
    document.location = document.location;
};

/// Default a[data-url] handler
/// If push is on, calls a default metadata update, that takes info from #desc and #keyword
var reqs = [];
function defaultLoad(e, options) {
    // Abort pending reqs
    for (i = 0; i < reqs.length; i++) {
        reqs[i].abort();
    }
    reqs.length = 0;

    // Show loading gif
    if (options.load) jQuery(e).find("img").addClass('visible');

    // Ajax call
    var r = $.ajax({
        // Config
        type: options.method,
        contentType: options.contentType,
        url: options.getCall(e),
        success: function (response) {
            // Show
            $(options.div).html(response);

            // Reconfigure buttons
            setOnClickDataUrl(options);

            // History
            if (options.push) options.updateMetadata(options.metadataDesc, options.metadataKeywords);
            // Hide loading gif
            if (options.load) jQuery(e).find("img").removeClass('visible');

            // Force FB & TW
            try {
                FB.XFBML.parse();
                twttr.widgets.load();
            } catch (ex) { }
        }
    });
    reqs.push(r);
}

function updateMetadata(desc, keywords) {
    $('meta[name=description]').remove();
    $('head').append('<meta name="description" content="' + $(desc).html() + '" />');
    $('meta[name=keywords]').remove();
    $('head').append('<meta name="keywords" content="' + $(keywords).html() + '" />');
}

<|endoftext|>


<commit_before>var fs = require("fs");
var path = require("path");

var log4js = require('log4js'),
    logger = log4js.getLogger();

var dateFormat = function (date, fmt) {
    var o = {
        "M+": date.getMonth() + 1, //月份
        "d+": date.getDate(), //日
        "h+": date.getHours(), //小时
        "m+": date.getMinutes(), //分
        "s+": date.getSeconds(), //秒
        "q+": Math.floor((date.getMonth() + 3) / 3), //季度
        "S": date.getMilliseconds() //毫秒
    };
    if (/(y+)/.test(fmt)) fmt = fmt.replace(RegExp.$1, (date.getFullYear() + "").substr(4 - RegExp.$1.length));
    for (var k in o)
        if (new RegExp("(" + k + ")").test(fmt)) fmt = fmt.replace(RegExp.$1, (RegExp.$1.length == 1) ? (o[k]) : (("00" + o[k]).substr(("" + o[k]).length)));
    return fmt;
}

var key = dateFormat(new Date, "yyyy-MM-dd");
var saveData = {};
GLOBAL.total = {};
GLOBAL.total[key] = saveData;

var existFile = path.join(".", "cache", "cacheTotal", key);

if (fs.existsSync(existFile)) {
    logger.info("load exist file  = " + existFile);
    try {
        saveData = JSON.parse(fs.readFileSync(existFile));
    } catch (e) {
        saveData = {};
    }

}

setInterval(function () {
    var filePath = path.join(".", "cache", "cacheTotal", key);
    fs.writeFileSync(filePath, JSON.stringify(saveData));

    logger.debug("save into disk , key = " + key);

    // clear old data
    var newKey = dateFormat(new Date, "yyyy-MM-dd");
    if (newKey != key) {
        logger.debug("new day  and clear old data , newkey = " + key);
        key = newKey;
        GLOBAL.total = {};
        GLOBAL.total[key] = saveData = {};
    }
}, 300000);

module.exports = {
    increase: function (data) {
        var count = saveData[data.id];
        if (count >= 1) {
            count++;
        } else {
            count = 1;
        }
        saveData[data.id] = count;
    },
    getTotal: function (data, callback) {
        if (!GLOBAL.total[data.key] || "{}" == JSON.stringify(GLOBAL.total[data.key])) {
            var filePath = path.join(".", "cache", "cacheTotal", data.key);
            fs.readFile(filePath, function (err, json) {
                if (err) {
                    logger.error("load cacheTotal fail :" + JSON.stringify(data));
                    callback&&callback(err);
                }
                GLOBAL.total[data.key] = json;
                var count = GLOBAL.total[data.key][data.id];
                if (count > 0) {
                    callback&&callback(null, json);
                } else {
                    callback&&callback({err: 'the count is 0'});
                }
            });

        }

    }
    /*getTotal : function (data){
     if(!GLOBAL.total[data.key] ||  "{}" == JSON.stringify(GLOBAL.total[data.key])){
     var filePath = path.join("." , "cache" , "cacheTotal" , data.key);
     try{
     var json = JSON.parse(fs.readFileSync(filePath));
     GLOBAL.total[data.key] = json;
     }catch(e){
     logger.error("load cacheTotal fail :" + JSON.stringify(data));
     return 0;
     }
     }

     var count = GLOBAL.total[data.key][data.id];
     if( count >0 ){
     return count;
     }else {
     return 0;
     }
     }*/
}<commit_msg>change the params<commit_after>var fs = require("fs");
var path = require("path");

var log4js = require('log4js'),
    logger = log4js.getLogger();

var dateFormat = function (date, fmt) {
    var o = {
        "M+": date.getMonth() + 1, //月份
        "d+": date.getDate(), //日
        "h+": date.getHours(), //小时
        "m+": date.getMinutes(), //分
        "s+": date.getSeconds(), //秒
        "q+": Math.floor((date.getMonth() + 3) / 3), //季度
        "S": date.getMilliseconds() //毫秒
    };
    if (/(y+)/.test(fmt)) fmt = fmt.replace(RegExp.$1, (date.getFullYear() + "").substr(4 - RegExp.$1.length));
    for (var k in o)
        if (new RegExp("(" + k + ")").test(fmt)) fmt = fmt.replace(RegExp.$1, (RegExp.$1.length == 1) ? (o[k]) : (("00" + o[k]).substr(("" + o[k]).length)));
    return fmt;
}

var key = dateFormat(new Date, "yyyy-MM-dd");
var saveData = {};
GLOBAL.total = {};
GLOBAL.total[key] = saveData;

var existFile = path.join(".", "cache", "cacheTotal", key);

if (fs.existsSync(existFile)) {
    logger.info("load exist file  = " + existFile);
    try {
        saveData = JSON.parse(fs.readFileSync(existFile));
    } catch (e) {
        saveData = {};
    }

}

setInterval(function () {
    var filePath = path.join(".", "cache", "cacheTotal", key);
    fs.writeFileSync(filePath, JSON.stringify(saveData));

    logger.debug("save into disk , key = " + key);

    // clear old data
    var newKey = dateFormat(new Date, "yyyy-MM-dd");
    if (newKey != key) {
        logger.debug("new day  and clear old data , newkey = " + key);
        key = newKey;
        GLOBAL.total = {};
        GLOBAL.total[key] = saveData = {};
    }
}, 300000);

module.exports = {
    increase: function (data) {
        var count = saveData[data.id];
        if (count >= 1) {
            count++;
        } else {
            count = 1;
        }
        saveData[data.id] = count;
    },
    getTotal: function (data, callback) {
        if (!GLOBAL.total[data.key] || "{}" == JSON.stringify(GLOBAL.total[data.key])) {
            var filePath = path.join(".", "cache", "cacheTotal", data.key);
            fs.readFile(filePath, function (err, json) {
                if (err) {
                    logger.error("load cacheTotal fail :" + JSON.stringify(data));
                    callback && callback(err);
                }
                GLOBAL.total[data.key] = json;
                var count = GLOBAL.total[data.key][data.id];
                if (count > 0) {
                    callback && callback(null, count);
                } else {
                    callback && callback({err: 'the count is 0'});
                }
            });

        }

    }
    /*getTotal : function (data){
     if(!GLOBAL.total[data.key] ||  "{}" == JSON.stringify(GLOBAL.total[data.key])){
     var filePath = path.join("." , "cache" , "cacheTotal" , data.key);
     try{
     var json = JSON.parse(fs.readFileSync(filePath));
     GLOBAL.total[data.key] = json;
     }catch(e){
     logger.error("load cacheTotal fail :" + JSON.stringify(data));
     return 0;
     }
     }

     var count = GLOBAL.total[data.key][data.id];
     if( count >0 ){
     return count;
     }else {
     return 0;
     }
     }*/
}<|endoftext|>