<commit_before>/**
 * Views for the story viewer Backbone application
 */
Namespace('storybase.viewer');

// Translations for interface text
Globalize.addCultureInfo('en', {
  messages: {
    "Topic Map": "Topic Map",
    "How Can You Help?": "How Can You Help?"
  }
});

Globalize.addCultureInfo('es', {
  messages: {
    "Topic Map": "Mapa Temático",
    "How Can You Help?": "How Can You Help?"
  }
});

// Container view for the viewer application.
// It delegates rendering and events for the entire app to views
// rendering more specific "widgets"
storybase.viewer.views.ViewerApp = Backbone.View.extend({
  // Initialize the view
  initialize: function() {
    this.sections = this.options.sections;
    this.story = this.options.story;
    this.navigationView = new storybase.viewer.views.StoryNavigation({
      sections: this.options.sections
    });
    this.headerView = new storybase.viewer.views.StoryHeader();
    this.setSection(this.sections.at(0), {showActiveSection: false});
    _.bindAll(this, 'handleScroll');
    $(window).scroll(this.handleScroll);
  },

  // Add the view's container element to the DOM and render the sub-views
  render: function() {
    this.$el.addClass(this.elClass);
    this.$('footer').append(this.navigationView.el);
    this.navigationView.render();
    this.$('.summary').show();
    this.$('.section').show();
    return this;
  },

  // Update the active story section in the sub-views
  updateSubviewSections: function() {
    this.navigationView.setSection(this.activeSection);
    this.headerView.setSection(this.activeSection);
  },

  // Show the active section
  showActiveSection: function() {
    throw "showActiveSection() is not implemented";
  },

  // Set the active story section
  setSection: function(section, options) {
    options = typeof options !== 'undefined' ? options : {};
    var showActiveSection = options.hasOwnProperty('showActiveSection') ? options.showActiveSection : true;
    this.activeSection = section;
    if (showActiveSection) {
      this.showActiveSection();
    }
    this.updateSubviewSections();
  },

  // Like setSection(), but takes a section ID as an argument instead of
  // a Section model instance object
  setSectionById: function(id) {
    this.setSection(this.sections.get(id));
  },

  // Convenience method to get the element for the active section
  activeSectionEl: function() {
      return this.$('#' + this.activeSection.id);
  },

  // Event handler for scroll event
  handleScroll: function(e) {
    // Do nothing. Subclasses might want to implement this to do some work
  }
});

// View for the story viewer header.
// Updates the section heading title when the active section changes
storybase.viewer.views.StoryHeader = Backbone.View.extend({
  el: 'header',

  render: function() {
    var $titleEl = this.$el.find('.section-title').first();
    if ($titleEl.length == 0) {
      $titleEl = $('
');
      this.$el.append($titleEl);
    }
    $titleEl.text(this.section.get('title'));
    return this;
  },

  setSection: function(section) {
    this.section = section;
    this.render();
  }
});

// View to provide previous/next buttons to navigate between sections
storybase.viewer.views.StoryNavigation = Backbone.View.extend({
  tagName: 'nav',

  className: 'story-nav',

  initialize: function() {
    this.section = null;
    this.sections = this.options.sections;
    if (this.options.hasOwnProperty('addlLinks')) {
      this.addlLinks = this.options.addlLinks.map(function(link) {
        return {
         text: link.text,
         id: link.id,
         href: link.hasOwnProperty('href') ? link.href: '#'
       }
      });
    }
    else {
      this.addlLinks = [];
    }
  },

  render: function() {
    var context = {};
    if (this.section) {
      context.next_section = this.sections.get(
	this.section.get('next_section_id'));
      context.previous_section = this.sections.get(
	this.section.get('previous_section_id'));
    }
    context.addl_links = this.addlLinks;

    this.$el.html(ich.navigationTemplate(context));
    return this;
  },

  setSection: function(section) {
    this.section = section;
    this.render();
  }
});

// Interative visualization of a spider story structure
storybase.viewer.views.Spider = Backbone.View.extend({
  events: {
    "hover g.node": "hoverSectionNode"
  },

  initialize: function() {
    this.sections = this.options.sections;
    this.insertBefore = this.options.insertBefore;
    this.subtractHeight = this.options.subtractHeight;
    this.subtractWidth = this.options.subtractWidth;
    this.activeSection = null;
    // The id for the visualization's wrapper element
    this.visId = 'spider-vis';
    this.maxDepth = null;
    this.sectionsAtDepth = [];
    this.maxSectionsAtDepth = 0;
    this.walkSectionHierarchy(0, this.sections.at(0));
  },

  // Walk the section hierarchy to build a sense of its "shape"
  walkSectionHierarchy: function(depth, section) {
    if (this.maxDepth == null ||
        depth > this.maxDepth) {
      this.maxDepth = depth;
    }

    if (this.sectionsAtDepth[depth] === undefined) {
      this.sectionsAtDepth[depth] = 1;
    }
    else {
      this.sectionsAtDepth[depth]++;
    }

    if (this.sectionsAtDepth[depth] > this.maxSectionsAtDepth) {
      this.maxSectionsAtDepth = this.sectionsAtDepth[depth];
    }

    var $this = this;
    _.each(section.get('children'), function(childId) {
      $this.walkSectionHierarchy(depth + 1, $this.sections.get(childId));
    });
  },

  // Return the visualization wrapper element
  visEl: function() {
    return this.$('#' + this.visId).first();
  },

  setSection: function(section) {
    this.activeSection = section;
    this.highlightSectionNode(this.activeSection.id);
  },

  highlightSectionNode: function(sectionId) {
    d3.selectAll('g.node').classed('active', false);
    d3.select('g.node.section-' + sectionId).classed('active', true);
  },

  // Get the dimensions of the visualization's wrapper element
  getVisDimensions: function() {
    // We have to calculate the dimensions relative to the window rather than
    // the parent element because the parent element is really tall as it
    // also contains the (hidden) section content.  It seems there should be
    // a better way to do this, but I don't know it.
    width = $(window).width();
    height = $(window).height();
    _.each(this.subtractWidth, function(selector) {
      width -= $(selector).outerWidth();
    });
    _.each(this.subtractHeight, function(selector) {
      height -= $(selector).outerHeight();
    });
    return {
      width: width,
      height: height
    };
  },

  // Get the radius of the tree
  getTreeRadius: function(width, height) {
    return (this.maxSectionsAtDepth == 1 ? _.max([width, height]) : _.min([width, height])) * .66;
  },

  render: function() {
    var $this = this;
    var elId = this.$el.attr('id');
    var dimensions = this.getVisDimensions();
    var treeRadius = this.getTreeRadius(dimensions.width, dimensions.height);
    var vis = d3.select("#" + elId).insert("svg", this.insertBefore)
        .attr("id", this.visId)
        .attr("width", dimensions.width)
        .attr("height", dimensions.height)
	.attr("style", "float:left")
      .append("g");
    var rootSection = this.sections.at(0).populateChildren();
    var tree = d3.layout.tree()
      .size([360, treeRadius - 120])
      .separation(function(a, b) { return (a.parent == b.parent ? 1 : 2) / a.depth; });
    var diagonal = d3.svg.diagonal.radial()
      .projection(function(d) { return [d.y, d.x / 180 * Math.PI]; });
    var nodes = tree.nodes(rootSection);
    // Fix x coordinate (angle) when each level has only one child
    // In this case d.x = NaN which breakins things
    nodes = _.map(nodes, function(node) {
      node.x = $this.maxSectionsAtDepth == 1 ? 90 : node.x;
      return node;
    });
    var links = tree.links(nodes);

    var link = vis.selectAll("path.link")
        .data(links)
      .enter().append("path")
        .attr("class", "link")
        .attr("d", diagonal);

    var node = vis.selectAll("g.node")
      .data(nodes)
      .enter().append("g")
      .attr('class', function(d) {
        return "node section-" + d.id;
      })
      .attr("transform", function(d) {
	var transform = "";
	if (d.depth > 0) {
	  transform += "rotate(" + (d.x - 90) + ")";
	}
        transform += "translate(" + d.y + ")";
	return transform;
      });

    node.append("circle")
        .attr("r", 15);

    node.append("text")
      .attr("x", function(d) {
	if (d.depth == 0) { return 20; }
        return d.x < 180 ? 20 : -20;
      })
      .attr("y", ".31em")
      .attr("text-anchor", function(d) {
	if (d.depth == 0) { return "start"; }
        return d.x < 180 ? "start" : "end"; })
      .attr("transform", function(d) {
	var rotation = 0;
	if ($this.maxSectionsAtDepth == 1) {
	  rotation = 315;
	}
	else if (d.depth > 0) {
          rotation = 90 - d.x;
	}
	return "rotate(" + rotation + ")";
      })
      .text(function(d) { return d.get('title'); });

    // Center the tree within the viewport
    var treeBBox = vis[0][0].getBBox();
    var translateX = (0 - treeBBox.x) + ((dimensions.width - treeBBox.width) / 2);
    var translateY = (0 - treeBBox.y) + ((dimensions.height - treeBBox.height) / 2);
    vis.attr("transform", "translate(" + translateX + ", " + translateY + ")");
  },

  // Event handler for hovering over a section node
  // Changes the cursor to a "pointer" style icon to indicate that
  // the nodes are clickable.
  hoverSectionNode: function(e) {
    e.currentTarget.style.cursor = 'pointer';
  },
});

// Master view that shows a story in a linear fashion
storybase.viewer.views.LinearViewerApp = storybase.viewer.views.ViewerApp.extend({
  elClass: 'linear',

  // Get the position, from the top of the winder, of the lower edge of the
  // header
  headerBottom: function() {
    return headerHeight = this.$('header').outerHeight()
  },

  footerTop: function() {
    return this.$('footer').offset().top;
  },

  // Show the active section
  showActiveSection: function() {
    var sectionTop = this.$('#' + this.activeSection.id).offset().top;
    this._preventScrollEvent = true;
    var headerBottom = this.headerBottom();
    var scrollBy = Math.ceil(sectionTop - this.headerBottom());
    $(window).scrollTop(scrollBy);
  },

  // Return the element of the last visible story section
  getLastVisibleSectionEl: function() {
    var visibleSections = [];
    var numSections = this.$('section').length;
    for (var i = 0; i < numSections; i++) {
      var $section = this.$('section').eq(i);
      var sectionTop = $section.offset().top;
      var sectionBottom = sectionTop + $section.outerHeight();
      if (sectionTop >= this.headerBottom() &&
          sectionBottom <= this.footerTop()) {
        visibleSections.push($section);
      }
    }
    return _.last(visibleSections);
  },

  // Event handler for scroll event
  handleScroll: function(e) {
    var newSection = this.activeSection;
    if (this._preventScrollEvent !== true) {
      if ($(window).scrollTop() == 0) {
	// At the top of the window.  Set the active section to the
	// first section
	newSection = this.sections.first();
      }
      else {
	var $lastVisibleSectionEl = this.getLastVisibleSectionEl();
	if ($lastVisibleSectionEl) {
	  var lastVisibleSection = this.sections.get($lastVisibleSectionEl.attr('id'));
	  if (lastVisibleSection != this.activeSection) {
	    newSection = lastVisibleSection;
	  }
	}
      }
      this.setSection(newSection, {showActiveSection: false});
      storybase.viewer.router.navigate("sections/" + newSection.id,
                                     {trigger: false});
    }
    this._preventScrollEvent = false;
  }
});

// Master view that shows the story structure visualization initially
storybase.viewer.views.SpiderViewerApp = storybase.viewer.views.ViewerApp.extend({
  elClass: 'spider',

  events: {
    "click #topic-map": "clickTopicMapLink",
    "click g.node": "clickSectionNode"
  },

  initialize: function() {
    this.sections = this.options.sections;
    this.story = this.options.story;
    this.navigationView = new storybase.viewer.views.StoryNavigation({
      sections: this.options.sections,
      addlLinks: [{text: Globalize.localize("Topic Map"), id: 'topic-map'}]
    });
    this.headerView = new storybase.viewer.views.StoryHeader();
    this.initialView = new storybase.viewer.views.Spider({
      el: this.$('#body'),
      sections: this.options.sections,
      insertBefore: '.section',
      subtractWidth: ['.summary'],
      subtractHeight: ['header', 'footer']
    });
  },

  render: function() {
    this.$el.addClass(this.elClass);
    this.$('footer').append(this.navigationView.el);
    this.navigationView.render();
    this.initialView.render();
    this.$('.summary').show();
    // Hide all the section content initially
    this.$('.section').hide();
    return this;
  },

  // Show the active section
  showActiveSection: function() {
    // Hide the summary
    this.$('.summary').hide();
    // Hide all sections other than the active one
    this.$('.section').hide();
    this.$('#' + this.activeSection.id).show();
    // Hide the visualization
    this.initialView.$('svg').hide();
  },

  // Update the active story section in the sub-views
  updateSubviewSections: function() {
    this.navigationView.setSection(this.activeSection);
    this.headerView.setSection(this.activeSection);
    this.initialView.setSection(this.activeSection);
  },

  // Event handler for clicks on a section node
  clickSectionNode: function(e) {
    var node = d3.select(e.currentTarget);
    var sectionId = node.data()[0].id;
    this.initialView.visEl().hide();
    // TODO: I'm not sure if this is the best way to access the router
    // I don't like global variables, but passing it as an argument
    // makes for a weird circular dependency between the master view/
    // router.
    storybase.viewer.router.navigate("sections/" + sectionId,
                                     {trigger: true});
  },

  // Event handler for clicking the "Topic Map" link
  clickTopicMapLink: function(e) {
    var activeSectionEl = this.activeSectionEl();
    if (activeSectionEl !== null) {
      var visEl = this.initialView.visEl();
      if (visEl.css('display') == 'none') {
	this.initialView.visEl().show();
      }
      else {
	this.initialView.visEl().hide();
      }
      this.$('.summary').toggle();
    }
  }

});

// Get the appropriate master view based on the story structure type
storybase.viewer.views.getViewerApp = function(structureType, options) {
  if (structureType == 'linear') {
    return new storybase.viewer.views.LinearViewerApp(options);
  }
  else if (structureType == 'spider') {
    return new storybase.viewer.views.SpiderViewerApp(options);
  }
  else {
    throw "Unknown story structure type '" + structureType + "'";
  }
};
<commit_msg>Toggle the visibility of the active section when the user displays the topic map.<commit_after>/**
 * Views for the story viewer Backbone application
 */
Namespace('storybase.viewer');

// Translations for interface text
Globalize.addCultureInfo('en', {
  messages: {
    "Topic Map": "Topic Map",
    "How Can You Help?": "How Can You Help?"
  }
});

Globalize.addCultureInfo('es', {
  messages: {
    "Topic Map": "Mapa Temático",
    "How Can You Help?": "How Can You Help?"
  }
});

// Container view for the viewer application.
// It delegates rendering and events for the entire app to views
// rendering more specific "widgets"
storybase.viewer.views.ViewerApp = Backbone.View.extend({
  // Initialize the view
  initialize: function() {
    this.sections = this.options.sections;
    this.story = this.options.story;
    this.navigationView = new storybase.viewer.views.StoryNavigation({
      sections: this.options.sections
    });
    this.headerView = new storybase.viewer.views.StoryHeader();
    this.setSection(this.sections.at(0), {showActiveSection: false});
    _.bindAll(this, 'handleScroll');
    $(window).scroll(this.handleScroll);
  },

  // Add the view's container element to the DOM and render the sub-views
  render: function() {
    this.$el.addClass(this.elClass);
    this.$('footer').append(this.navigationView.el);
    this.navigationView.render();
    this.$('.summary').show();
    this.$('.section').show();
    return this;
  },

  // Update the active story section in the sub-views
  updateSubviewSections: function() {
    this.navigationView.setSection(this.activeSection);
    this.headerView.setSection(this.activeSection);
  },

  // Show the active section
  showActiveSection: function() {
    throw "showActiveSection() is not implemented";
  },

  // Set the active story section
  setSection: function(section, options) {
    options = typeof options !== 'undefined' ? options : {};
    var showActiveSection = options.hasOwnProperty('showActiveSection') ? options.showActiveSection : true;
    this.activeSection = section;
    if (showActiveSection) {
      this.showActiveSection();
    }
    this.updateSubviewSections();
  },

  // Like setSection(), but takes a section ID as an argument instead of
  // a Section model instance object
  setSectionById: function(id) {
    this.setSection(this.sections.get(id));
  },

  // Convenience method to get the element for the active section
  activeSectionEl: function() {
      return this.$('#' + this.activeSection.id);
  },

  // Event handler for scroll event
  handleScroll: function(e) {
    // Do nothing. Subclasses might want to implement this to do some work
  }
});

// View for the story viewer header.
// Updates the section heading title when the active section changes
storybase.viewer.views.StoryHeader = Backbone.View.extend({
  el: 'header',

  render: function() {
    var $titleEl = this.$el.find('.section-title').first();
    if ($titleEl.length == 0) {
      $titleEl = $('
');
      this.$el.append($titleEl);
    }
    $titleEl.text(this.section.get('title'));
    return this;
  },

  setSection: function(section) {
    this.section = section;
    this.render();
  }
});

// View to provide previous/next buttons to navigate between sections
storybase.viewer.views.StoryNavigation = Backbone.View.extend({
  tagName: 'nav',

  className: 'story-nav',

  initialize: function() {
    this.section = null;
    this.sections = this.options.sections;
    if (this.options.hasOwnProperty('addlLinks')) {
      this.addlLinks = this.options.addlLinks.map(function(link) {
        return {
         text: link.text,
         id: link.id,
         href: link.hasOwnProperty('href') ? link.href: '#'
       }
      });
    }
    else {
      this.addlLinks = [];
    }
  },

  render: function() {
    var context = {};
    if (this.section) {
      context.next_section = this.sections.get(
	this.section.get('next_section_id'));
      context.previous_section = this.sections.get(
	this.section.get('previous_section_id'));
    }
    context.addl_links = this.addlLinks;

    this.$el.html(ich.navigationTemplate(context));
    return this;
  },

  setSection: function(section) {
    this.section = section;
    this.render();
  }
});

// Interative visualization of a spider story structure
storybase.viewer.views.Spider = Backbone.View.extend({
  events: {
    "hover g.node": "hoverSectionNode"
  },

  initialize: function() {
    this.sections = this.options.sections;
    this.insertBefore = this.options.insertBefore;
    this.subtractHeight = this.options.subtractHeight;
    this.subtractWidth = this.options.subtractWidth;
    this.activeSection = null;
    // The id for the visualization's wrapper element
    this.visId = 'spider-vis';
    this.maxDepth = null;
    this.sectionsAtDepth = [];
    this.maxSectionsAtDepth = 0;
    this.walkSectionHierarchy(0, this.sections.at(0));
  },

  // Walk the section hierarchy to build a sense of its "shape"
  walkSectionHierarchy: function(depth, section) {
    if (this.maxDepth == null ||
        depth > this.maxDepth) {
      this.maxDepth = depth;
    }

    if (this.sectionsAtDepth[depth] === undefined) {
      this.sectionsAtDepth[depth] = 1;
    }
    else {
      this.sectionsAtDepth[depth]++;
    }

    if (this.sectionsAtDepth[depth] > this.maxSectionsAtDepth) {
      this.maxSectionsAtDepth = this.sectionsAtDepth[depth];
    }

    var $this = this;
    _.each(section.get('children'), function(childId) {
      $this.walkSectionHierarchy(depth + 1, $this.sections.get(childId));
    });
  },

  // Return the visualization wrapper element
  visEl: function() {
    return this.$('#' + this.visId).first();
  },

  setSection: function(section) {
    this.activeSection = section;
    this.highlightSectionNode(this.activeSection.id);
  },

  highlightSectionNode: function(sectionId) {
    d3.selectAll('g.node').classed('active', false);
    d3.select('g.node.section-' + sectionId).classed('active', true);
  },

  // Get the dimensions of the visualization's wrapper element
  getVisDimensions: function() {
    // We have to calculate the dimensions relative to the window rather than
    // the parent element because the parent element is really tall as it
    // also contains the (hidden) section content.  It seems there should be
    // a better way to do this, but I don't know it.
    width = $(window).width();
    height = $(window).height();
    _.each(this.subtractWidth, function(selector) {
      width -= $(selector).outerWidth();
    });
    _.each(this.subtractHeight, function(selector) {
      height -= $(selector).outerHeight();
    });
    return {
      width: width,
      height: height
    };
  },

  // Get the radius of the tree
  getTreeRadius: function(width, height) {
    return (this.maxSectionsAtDepth == 1 ? _.max([width, height]) : _.min([width, height])) * .66;
  },

  render: function() {
    var $this = this;
    var elId = this.$el.attr('id');
    var dimensions = this.getVisDimensions();
    var treeRadius = this.getTreeRadius(dimensions.width, dimensions.height);
    var vis = d3.select("#" + elId).insert("svg", this.insertBefore)
        .attr("id", this.visId)
        .attr("width", dimensions.width)
        .attr("height", dimensions.height)
	.attr("style", "float:left")
      .append("g");
    var rootSection = this.sections.at(0).populateChildren();
    var tree = d3.layout.tree()
      .size([360, treeRadius - 120])
      .separation(function(a, b) { return (a.parent == b.parent ? 1 : 2) / a.depth; });
    var diagonal = d3.svg.diagonal.radial()
      .projection(function(d) { return [d.y, d.x / 180 * Math.PI]; });
    var nodes = tree.nodes(rootSection);
    // Fix x coordinate (angle) when each level has only one child
    // In this case d.x = NaN which breakins things
    nodes = _.map(nodes, function(node) {
      node.x = $this.maxSectionsAtDepth == 1 ? 90 : node.x;
      return node;
    });
    var links = tree.links(nodes);

    var link = vis.selectAll("path.link")
        .data(links)
      .enter().append("path")
        .attr("class", "link")
        .attr("d", diagonal);

    var node = vis.selectAll("g.node")
      .data(nodes)
      .enter().append("g")
      .attr('class', function(d) {
        return "node section-" + d.id;
      })
      .attr("transform", function(d) {
	var transform = "";
	if (d.depth > 0) {
	  transform += "rotate(" + (d.x - 90) + ")";
	}
        transform += "translate(" + d.y + ")";
	return transform;
      });

    node.append("circle")
        .attr("r", 15);

    node.append("text")
      .attr("x", function(d) {
	if (d.depth == 0) { return 20; }
        return d.x < 180 ? 20 : -20;
      })
      .attr("y", ".31em")
      .attr("text-anchor", function(d) {
	if (d.depth == 0) { return "start"; }
        return d.x < 180 ? "start" : "end"; })
      .attr("transform", function(d) {
	var rotation = 0;
	if ($this.maxSectionsAtDepth == 1) {
	  rotation = 315;
	}
	else if (d.depth > 0) {
          rotation = 90 - d.x;
	}
	return "rotate(" + rotation + ")";
      })
      .text(function(d) { return d.get('title'); });

    // Center the tree within the viewport
    var treeBBox = vis[0][0].getBBox();
    var translateX = (0 - treeBBox.x) + ((dimensions.width - treeBBox.width) / 2);
    var translateY = (0 - treeBBox.y) + ((dimensions.height - treeBBox.height) / 2);
    vis.attr("transform", "translate(" + translateX + ", " + translateY + ")");
  },

  // Event handler for hovering over a section node
  // Changes the cursor to a "pointer" style icon to indicate that
  // the nodes are clickable.
  hoverSectionNode: function(e) {
    e.currentTarget.style.cursor = 'pointer';
  },
});

// Master view that shows a story in a linear fashion
storybase.viewer.views.LinearViewerApp = storybase.viewer.views.ViewerApp.extend({
  elClass: 'linear',

  // Get the position, from the top of the winder, of the lower edge of the
  // header
  headerBottom: function() {
    return headerHeight = this.$('header').outerHeight()
  },

  footerTop: function() {
    return this.$('footer').offset().top;
  },

  // Show the active section
  showActiveSection: function() {
    var sectionTop = this.$('#' + this.activeSection.id).offset().top;
    this._preventScrollEvent = true;
    var headerBottom = this.headerBottom();
    var scrollBy = Math.ceil(sectionTop - this.headerBottom());
    $(window).scrollTop(scrollBy);
  },

  // Return the element of the last visible story section
  getLastVisibleSectionEl: function() {
    var visibleSections = [];
    var numSections = this.$('section').length;
    for (var i = 0; i < numSections; i++) {
      var $section = this.$('section').eq(i);
      var sectionTop = $section.offset().top;
      var sectionBottom = sectionTop + $section.outerHeight();
      if (sectionTop >= this.headerBottom() &&
          sectionBottom <= this.footerTop()) {
        visibleSections.push($section);
      }
    }
    return _.last(visibleSections);
  },

  // Event handler for scroll event
  handleScroll: function(e) {
    var newSection = this.activeSection;
    if (this._preventScrollEvent !== true) {
      if ($(window).scrollTop() == 0) {
	// At the top of the window.  Set the active section to the
	// first section
	newSection = this.sections.first();
      }
      else {
	var $lastVisibleSectionEl = this.getLastVisibleSectionEl();
	if ($lastVisibleSectionEl) {
	  var lastVisibleSection = this.sections.get($lastVisibleSectionEl.attr('id'));
	  if (lastVisibleSection != this.activeSection) {
	    newSection = lastVisibleSection;
	  }
	}
      }
      this.setSection(newSection, {showActiveSection: false});
      storybase.viewer.router.navigate("sections/" + newSection.id,
                                     {trigger: false});
    }
    this._preventScrollEvent = false;
  }
});

// Master view that shows the story structure visualization initially
storybase.viewer.views.SpiderViewerApp = storybase.viewer.views.ViewerApp.extend({
  elClass: 'spider',

  events: {
    "click #topic-map": "clickTopicMapLink",
    "click g.node": "clickSectionNode"
  },

  initialize: function() {
    this.sections = this.options.sections;
    this.story = this.options.story;
    this.navigationView = new storybase.viewer.views.StoryNavigation({
      sections: this.options.sections,
      addlLinks: [{text: Globalize.localize("Topic Map"), id: 'topic-map'}]
    });
    this.headerView = new storybase.viewer.views.StoryHeader();
    this.initialView = new storybase.viewer.views.Spider({
      el: this.$('#body'),
      sections: this.options.sections,
      insertBefore: '.section',
      subtractWidth: ['.summary'],
      subtractHeight: ['header', 'footer']
    });
  },

  render: function() {
    this.$el.addClass(this.elClass);
    this.$('footer').append(this.navigationView.el);
    this.navigationView.render();
    this.initialView.render();
    this.$('.summary').show();
    // Hide all the section content initially
    this.$('.section').hide();
    return this;
  },

  // Show the active section
  showActiveSection: function() {
    // Hide the summary
    this.$('.summary').hide();
    // Hide all sections other than the active one
    this.$('.section').hide();
    this.$('#' + this.activeSection.id).show();
    // Hide the visualization
    this.initialView.$('svg').hide();
  },

  // Update the active story section in the sub-views
  updateSubviewSections: function() {
    this.navigationView.setSection(this.activeSection);
    this.headerView.setSection(this.activeSection);
    this.initialView.setSection(this.activeSection);
  },

  // Event handler for clicks on a section node
  clickSectionNode: function(e) {
    var node = d3.select(e.currentTarget);
    var sectionId = node.data()[0].id;
    this.initialView.visEl().hide();
    // TODO: I'm not sure if this is the best way to access the router
    // I don't like global variables, but passing it as an argument
    // makes for a weird circular dependency between the master view/
    // router.
    storybase.viewer.router.navigate("sections/" + sectionId,
                                     {trigger: true});
  },

  // Event handler for clicking the "Topic Map" link
  clickTopicMapLink: function(e) {
    var activeSectionEl = this.activeSectionEl();
    if (activeSectionEl !== null) {
      // Only toggle the topic map if an active section has been set
      // The only time there should not be one set is when the viewer
      // first loads
      activeSectionEl.toggle();
      var visEl = this.initialView.visEl();
      if (visEl.css('display') == 'none') {
	this.initialView.visEl().show();
      }
      else {
	this.initialView.visEl().hide();
      }
      this.$('.summary').toggle();
    }
  }

});

// Get the appropriate master view based on the story structure type
storybase.viewer.views.getViewerApp = function(structureType, options) {
  if (structureType == 'linear') {
    return new storybase.viewer.views.LinearViewerApp(options);
  }
  else if (structureType == 'spider') {
    return new storybase.viewer.views.SpiderViewerApp(options);
  }
  else {
    throw "Unknown story structure type '" + structureType + "'";
  }
};
<|endoftext|>


<commit_before>import { ERROR_MESSAGES as errorMessagesBBB, STATES as videoconferenceStates } from './videoconference';

/* eslint-disable max-len */
$(document).ready(() => {
	$('.js-course-name-input').change(function courseNameInput() {
		$(this).val($(this).val().trim());
	});

	$('.btn-hidden-toggle').click(function hiddenToggle(e) {
		e.stopPropagation();
		e.preventDefault();
		const $hiddenToggleBtn = $(this);
		const $hiddenToggleIcon = $(this).find('.fa');
		const $card = $(this).closest('.card');
		const href = $(this).attr('href');
		$.ajax({
			method: 'PATCH',
			url: `${href}?json=true`,
			data: { hidden: !$hiddenToggleIcon.hasClass('fa-eye-slash') },
			success(result) {
				if (result.hidden) {
					$hiddenToggleIcon.addClass('fa-eye-slash');
					$hiddenToggleIcon.removeClass('fa-eye');
					$hiddenToggleBtn.attr('data-original-title', 'Thema sichtbar machen');
					$card.addClass('card-transparent');
				} else {
					$hiddenToggleIcon.removeClass('fa-eye-slash');
					$hiddenToggleIcon.addClass('fa-eye');
					$hiddenToggleBtn.attr('data-original-title', 'Thema verstecken');
					$card.removeClass('card-transparent');
				}
			},
		});
	});

	$('.btn-create-invitation').click(function createInvitation(e) {
		e.stopPropagation();
		e.preventDefault();
		const target = `${$(this).attr('data-href')}addStudent`;
		const $invitationModal = $('.invitation-modal');
		$.ajax({
			type: 'POST',
			url: '/link/',
			beforeSend(xhr) {
				// eslint-disable-next-line no-undef
				xhr.setRequestHeader('Csrf-Token', csrftoken);
			},
			data: {
				target,
			},
			success(data) {
				populateModalForm($invitationModal, {
					title: 'Einladungslink generiert!',
					closeLabel: 'Abbrechen',
					submitLabel: 'Speichern',
					fields: { invitation: data.newUrl },
				});
				$invitationModal.find('.btn-submit').remove();
				$invitationModal.find("input[name='invitation']").click(function inputNameInvitation() {
					$(this).select();
				});

				$invitationModal.appendTo('body').modal('show');
			},
		});
	});

	$('.btn-import-topic').click(function importTopic(e) {
		e.stopPropagation();
		e.preventDefault();
		const courseId = $(this).attr('data-courseId');
		const $importModal = $('.import-modal');
		populateModalForm($importModal, {
			title: 'Thema importieren',
			closeLabel: 'Abbrechen',
			submitLabel: 'Speichern',
			fields: { courseId },
		});

		const $modalForm = $importModal.find('.modal-form');
		$modalForm.attr('action', `/courses/${courseId}/importTopic`);
		$importModal.appendTo('body').modal('show');
	});

	$('.move-handle').click((e) => {
		e.stopPropagation();
	});

	if ($('#topic-list').length) {
		$('#topic-list').sortable({
			placeholder: 'ui-state-highlight',
			handle: '.move-handle',
			update() {
				const positions = {};
				$('#topic-list .card-topic').each(function topicListCardTopic(i) {
					positions[($(this).attr('data-topicId'))] = i;
				});
				const courseId = $(this).attr('data-courseId');
				$.ajax({
					type: 'PATCH',
					url: `/courses/${courseId}/positions`,
					data: positions,
				});
			},
		});

		$('#topic-list').disableSelection();
	}

	$('.btn-create-share-course').click(function createShareCourse(e) {
		e.stopPropagation();
		e.preventDefault();
		const courseId = $(this).attr('data-courseId');
		const $shareModal = $('.share-modal');
		$.ajax({
			type: 'GET',
			url: `/courses/${courseId}/share/`,
			success(data) {
				populateModalForm($shareModal, {
					title: 'Kopiercode generiert!',
					closeLabel: 'Schließen',
					fields: { shareToken: data.shareToken },
				});
				$shareModal.find('.btn-submit').remove();
				$shareModal.find("input[name='shareToken']").click(function inputNameShareToken() {
					$(this).select();
				});

				$shareModal.appendTo('body').modal('show');

				// eslint-disable-next-line max-len
				$("label[for='shareToken']").text('Verteile folgenden Code an einen Lehrer-Kollegen, um den Kurs mit diesem zu teilen. Die Funktion befindet sich auf der Übersichtsseite für Kurse.');
				// eslint-disable-next-line no-undef
				const image = kjua({
					text: `${$('meta[name=baseUrl]').attr('content')}/courses?import=${data.shareToken}`,
					render: 'image',
				});
				const $shareqrbox = $('.course-qr');
				$shareqrbox.empty();
				// eslint-disable-next-line max-len
				$shareqrbox.append('
Alternativ kannst du deinen Lehrer-Kollegen auch folgenden QR-Code zeigen.
');
				$shareqrbox.append(image);
			},
		});
	});

	let activeBbbCard = false;

	if ($('.bbbTool').length > 0) {
		const courseId = $('.bbbTool').parent().attr('data-courseId');

		const videoconferenceResponse = (data) => {
			activeBbbCard = true;
			const { permission, state } = data;

			if (!permission || permission.length < 0) {
				$.showNotification(errorMessagesBBB.NO_PERMISSION, 'danger');
			}

			const guestInactiveState = {
				condition: videoconferenceStates.GuestInactiveState.condition,
				updateUi: () => {
					const reloadIcon = $('.bbbTool-reload-icon');

					$('.bbbTool').off('click').css({
						cursor: 'auto',
						backgroundColor: 'white',
					});

					$('.bbb-state').hide();
					$('.bbb-guest-inactive-state').show();

					reloadIcon.off('click').on('click', (e) => {
						e.stopPropagation();
						e.preventDefault();

						reloadIcon.addClass('reload-animation');

						setTimeout(() => {
							reloadIcon.removeClass('reload-animation');
						}, 700);

						$.ajax({
							type: 'GET',
							url: `/videoconference/course/${courseId}`,
							success: videoconferenceResponse,
						}).done((res) => {
							if (res.state === 'RUNNING') {
								$('.bbb-state').hide();
								$('.bbb-running-videoconference-state').show();
							}
						});
					});
				},
			};

			const modInactiveState = {
				condition: videoconferenceStates.ModeratorInactiveState.condition,
				updateUi: () => {
					$('.bbb-state').hide();
					$('.bbb-moderator-inactive-state').show();
				},
			};

			const runningState = {
				condition: videoconferenceStates.RunningState.condition,
				updateUi: () => {
					$('.bbb-state').hide();
					$('.bbb-running-videoconference-state').show();

					$('.bbbTool').off('click').css({ cursor: 'pointer' }).on('click', () => {
						$.ajax({
							method: 'POST',
							url: '/videoconference/',
							contentType: 'application/json',
							dataType: 'json',
							data: JSON.stringify({
								scopeId: courseId,
								scopeName: 'course',
								options: {},
							}),
						}).done((response) => {
							if (!response.url || response.url.length < 0) {
								$.showNotification(errorMessagesBBB.NOT_STARTED_OR_FINISHED, 'danger');
								$('.bbb-state').hide();
								$('.bbb-guest-inactive-state').show();
							}
							window.open(response.url, '_blank');
						}).fail((error) => {
							if (error && error.status !== 'SUCCESS') {
								$.showNotification(errorMessagesBBB.NOT_STARTED_OR_FINISHED, 'danger');
								$('.bbb-state').hide();
								$('.bbb-guest-inactive-state').show();
							}
						});
					});
				},
			};

			// eslint-disable-next-line func-names
			$('.bbbTool').each(() => {
				[guestInactiveState, modInactiveState, runningState].forEach((bbbState) => {
					if (bbbState.condition(permission, state)) bbbState.updateUi();
				});
			});
		};

		$.ajax({
			type: 'GET',
			url: `/videoconference/course/${courseId}`,
			success: videoconferenceResponse,
			error: (error) => {
				if (error && error.status !== 'SUCCESS') {
					$.showNotification(errorMessagesBBB.GENERAL_ERROR, 'danger');
				}
			},
		});
	}

	// eslint-disable-next-line func-names
	$('.bbbTool').click(function (e) {
		if (!activeBbbCard) {
			return;
		}

		e.stopPropagation();
		e.preventDefault();
		const courseId = $(this).parent().attr('data-courseId');
		const $createVideoconferenceModal = $('.create-videoconference-modal');

		$.ajax({
			type: 'GET',
			url: `/courses/${courseId}/usersJson`,
			success(data) {
				populateModalForm($createVideoconferenceModal, {
					title: `Videokonferenzraum "${data.course.name}" erstellen`,
					closeLabel: 'Abbrechen',
					submitLabel: 'Erstellen',
				});
			},
		});
		$createVideoconferenceModal.appendTo('body').modal('show');
		$createVideoconferenceModal.off('submit').on('submit', (event) => {
			event.preventDefault();

			const everyAttendeJoinsMuted = $createVideoconferenceModal.find('[name=startMuted]').is(':checked');
			const moderatorMustApproveJoinRequests = $createVideoconferenceModal.find('[name=requestModerator]').is(':checked');
			const everybodyJoinsAsModerator = $createVideoconferenceModal.find('[name=everyoneIsModerator]').is(':checked');

			$.ajax({
				type: 'POST',
				url: '/videoconference/',
				contentType: 'application/json',
				dataType: 'json',
				data: JSON.stringify({
					scopeId: courseId,
					scopeName: 'course',
					options: {
						everyAttendeJoinsMuted,
						moderatorMustApproveJoinRequests,
						everybodyJoinsAsModerator,
					},
				}),
			}).done((response) => {
				// todo, the browser may block popups...
				window.open(response.url, '_blank');
				$('.bbb-state').hide();
				$('.bbb-running-videoconference-state').show();
				$('.bbbTool').off('click').css({ cursor: 'pointer' }).on('click', () => window.open(response.url, '_blank'));
			}).fail((error) => {
				console.log(error);
				if (error && error.status !== 'SUCCESS') {
					return $.showNotification(errorMessagesBBB.GENERAL_ERROR, 'danger');
				}
				return $.showNotification(errorMessagesBBB.GENERAL_ERROR, 'danger');
			});
			$createVideoconferenceModal.modal('hide');
		});
	});

	$('.bbbTool-info-icon').click((e) => {
		e.stopPropagation();
		e.preventDefault();

		const $bbbReloadInfoModal = $('.reload-info-modal');

		populateModalForm($bbbReloadInfoModal, {
			title: '',
			closeLabel: 'OK',
		});

		$bbbReloadInfoModal.appendTo('body').modal('show');
	});
});
<commit_msg>Ignore console warning<commit_after>import { ERROR_MESSAGES as errorMessagesBBB, STATES as videoconferenceStates } from './videoconference';

/* eslint-disable max-len */
$(document).ready(() => {
	$('.js-course-name-input').change(function courseNameInput() {
		$(this).val($(this).val().trim());
	});

	$('.btn-hidden-toggle').click(function hiddenToggle(e) {
		e.stopPropagation();
		e.preventDefault();
		const $hiddenToggleBtn = $(this);
		const $hiddenToggleIcon = $(this).find('.fa');
		const $card = $(this).closest('.card');
		const href = $(this).attr('href');
		$.ajax({
			method: 'PATCH',
			url: `${href}?json=true`,
			data: { hidden: !$hiddenToggleIcon.hasClass('fa-eye-slash') },
			success(result) {
				if (result.hidden) {
					$hiddenToggleIcon.addClass('fa-eye-slash');
					$hiddenToggleIcon.removeClass('fa-eye');
					$hiddenToggleBtn.attr('data-original-title', 'Thema sichtbar machen');
					$card.addClass('card-transparent');
				} else {
					$hiddenToggleIcon.removeClass('fa-eye-slash');
					$hiddenToggleIcon.addClass('fa-eye');
					$hiddenToggleBtn.attr('data-original-title', 'Thema verstecken');
					$card.removeClass('card-transparent');
				}
			},
		});
	});

	$('.btn-create-invitation').click(function createInvitation(e) {
		e.stopPropagation();
		e.preventDefault();
		const target = `${$(this).attr('data-href')}addStudent`;
		const $invitationModal = $('.invitation-modal');
		$.ajax({
			type: 'POST',
			url: '/link/',
			beforeSend(xhr) {
				// eslint-disable-next-line no-undef
				xhr.setRequestHeader('Csrf-Token', csrftoken);
			},
			data: {
				target,
			},
			success(data) {
				populateModalForm($invitationModal, {
					title: 'Einladungslink generiert!',
					closeLabel: 'Abbrechen',
					submitLabel: 'Speichern',
					fields: { invitation: data.newUrl },
				});
				$invitationModal.find('.btn-submit').remove();
				$invitationModal.find("input[name='invitation']").click(function inputNameInvitation() {
					$(this).select();
				});

				$invitationModal.appendTo('body').modal('show');
			},
		});
	});

	$('.btn-import-topic').click(function importTopic(e) {
		e.stopPropagation();
		e.preventDefault();
		const courseId = $(this).attr('data-courseId');
		const $importModal = $('.import-modal');
		populateModalForm($importModal, {
			title: 'Thema importieren',
			closeLabel: 'Abbrechen',
			submitLabel: 'Speichern',
			fields: { courseId },
		});

		const $modalForm = $importModal.find('.modal-form');
		$modalForm.attr('action', `/courses/${courseId}/importTopic`);
		$importModal.appendTo('body').modal('show');
	});

	$('.move-handle').click((e) => {
		e.stopPropagation();
	});

	if ($('#topic-list').length) {
		$('#topic-list').sortable({
			placeholder: 'ui-state-highlight',
			handle: '.move-handle',
			update() {
				const positions = {};
				$('#topic-list .card-topic').each(function topicListCardTopic(i) {
					positions[($(this).attr('data-topicId'))] = i;
				});
				const courseId = $(this).attr('data-courseId');
				$.ajax({
					type: 'PATCH',
					url: `/courses/${courseId}/positions`,
					data: positions,
				});
			},
		});

		$('#topic-list').disableSelection();
	}

	$('.btn-create-share-course').click(function createShareCourse(e) {
		e.stopPropagation();
		e.preventDefault();
		const courseId = $(this).attr('data-courseId');
		const $shareModal = $('.share-modal');
		$.ajax({
			type: 'GET',
			url: `/courses/${courseId}/share/`,
			success(data) {
				populateModalForm($shareModal, {
					title: 'Kopiercode generiert!',
					closeLabel: 'Schließen',
					fields: { shareToken: data.shareToken },
				});
				$shareModal.find('.btn-submit').remove();
				$shareModal.find("input[name='shareToken']").click(function inputNameShareToken() {
					$(this).select();
				});

				$shareModal.appendTo('body').modal('show');

				// eslint-disable-next-line max-len
				$("label[for='shareToken']").text('Verteile folgenden Code an einen Lehrer-Kollegen, um den Kurs mit diesem zu teilen. Die Funktion befindet sich auf der Übersichtsseite für Kurse.');
				// eslint-disable-next-line no-undef
				const image = kjua({
					text: `${$('meta[name=baseUrl]').attr('content')}/courses?import=${data.shareToken}`,
					render: 'image',
				});
				const $shareqrbox = $('.course-qr');
				$shareqrbox.empty();
				// eslint-disable-next-line max-len
				$shareqrbox.append('
Alternativ kannst du deinen Lehrer-Kollegen auch folgenden QR-Code zeigen.
');
				$shareqrbox.append(image);
			},
		});
	});

	let activeBbbCard = false;

	if ($('.bbbTool').length > 0) {
		const courseId = $('.bbbTool').parent().attr('data-courseId');

		const videoconferenceResponse = (data) => {
			activeBbbCard = true;
			const { permission, state } = data;

			if (!permission || permission.length < 0) {
				$.showNotification(errorMessagesBBB.NO_PERMISSION, 'danger');
			}

			const guestInactiveState = {
				condition: videoconferenceStates.GuestInactiveState.condition,
				updateUi: () => {
					const reloadIcon = $('.bbbTool-reload-icon');

					$('.bbbTool').off('click').css({
						cursor: 'auto',
						backgroundColor: 'white',
					});

					$('.bbb-state').hide();
					$('.bbb-guest-inactive-state').show();

					reloadIcon.off('click').on('click', (e) => {
						e.stopPropagation();
						e.preventDefault();

						reloadIcon.addClass('reload-animation');

						setTimeout(() => {
							reloadIcon.removeClass('reload-animation');
						}, 700);

						$.ajax({
							type: 'GET',
							url: `/videoconference/course/${courseId}`,
							success: videoconferenceResponse,
						}).done((res) => {
							if (res.state === 'RUNNING') {
								$('.bbb-state').hide();
								$('.bbb-running-videoconference-state').show();
							}
						});
					});
				},
			};

			const modInactiveState = {
				condition: videoconferenceStates.ModeratorInactiveState.condition,
				updateUi: () => {
					$('.bbb-state').hide();
					$('.bbb-moderator-inactive-state').show();
				},
			};

			const runningState = {
				condition: videoconferenceStates.RunningState.condition,
				updateUi: () => {
					$('.bbb-state').hide();
					$('.bbb-running-videoconference-state').show();

					$('.bbbTool').off('click').css({ cursor: 'pointer' }).on('click', () => {
						$.ajax({
							method: 'POST',
							url: '/videoconference/',
							contentType: 'application/json',
							dataType: 'json',
							data: JSON.stringify({
								scopeId: courseId,
								scopeName: 'course',
								options: {},
							}),
						}).done((response) => {
							if (!response.url || response.url.length < 0) {
								$.showNotification(errorMessagesBBB.NOT_STARTED_OR_FINISHED, 'danger');
								$('.bbb-state').hide();
								$('.bbb-guest-inactive-state').show();
							}
							window.open(response.url, '_blank');
						}).fail((error) => {
							if (error && error.status !== 'SUCCESS') {
								$.showNotification(errorMessagesBBB.NOT_STARTED_OR_FINISHED, 'danger');
								$('.bbb-state').hide();
								$('.bbb-guest-inactive-state').show();
							}
						});
					});
				},
			};

			// eslint-disable-next-line func-names
			$('.bbbTool').each(() => {
				[guestInactiveState, modInactiveState, runningState].forEach((bbbState) => {
					if (bbbState.condition(permission, state)) bbbState.updateUi();
				});
			});
		};

		$.ajax({
			type: 'GET',
			url: `/videoconference/course/${courseId}`,
			success: videoconferenceResponse,
			error: (error) => {
				if (error && error.status !== 'SUCCESS') {
					$.showNotification(errorMessagesBBB.GENERAL_ERROR, 'danger');
				}
			},
		});
	}

	// eslint-disable-next-line func-names
	$('.bbbTool').click(function (e) {
		if (!activeBbbCard) {
			return;
		}

		e.stopPropagation();
		e.preventDefault();
		const courseId = $(this).parent().attr('data-courseId');
		const $createVideoconferenceModal = $('.create-videoconference-modal');

		$.ajax({
			type: 'GET',
			url: `/courses/${courseId}/usersJson`,
			success(data) {
				populateModalForm($createVideoconferenceModal, {
					title: `Videokonferenzraum "${data.course.name}" erstellen`,
					closeLabel: 'Abbrechen',
					submitLabel: 'Erstellen',
				});
			},
		});
		$createVideoconferenceModal.appendTo('body').modal('show');
		$createVideoconferenceModal.off('submit').on('submit', (event) => {
			event.preventDefault();

			const everyAttendeJoinsMuted = $createVideoconferenceModal.find('[name=startMuted]').is(':checked');
			const moderatorMustApproveJoinRequests = $createVideoconferenceModal.find('[name=requestModerator]').is(':checked');
			const everybodyJoinsAsModerator = $createVideoconferenceModal.find('[name=everyoneIsModerator]').is(':checked');

			$.ajax({
				type: 'POST',
				url: '/videoconference/',
				contentType: 'application/json',
				dataType: 'json',
				data: JSON.stringify({
					scopeId: courseId,
					scopeName: 'course',
					options: {
						everyAttendeJoinsMuted,
						moderatorMustApproveJoinRequests,
						everybodyJoinsAsModerator,
					},
				}),
			}).done((response) => {
				// todo, the browser may block popups...
				window.open(response.url, '_blank');
				$('.bbb-state').hide();
				$('.bbb-running-videoconference-state').show();
				$('.bbbTool').off('click').css({ cursor: 'pointer' }).on('click', () => window.open(response.url, '_blank'));
			}).fail((error) => {
				// eslint-disable-next-line no-console
				console.log(error);
				if (error && error.status !== 'SUCCESS') {
					return $.showNotification(errorMessagesBBB.GENERAL_ERROR, 'danger');
				}
				return $.showNotification(errorMessagesBBB.GENERAL_ERROR, 'danger');
			});
			$createVideoconferenceModal.modal('hide');
		});
	});

	$('.bbbTool-info-icon').click((e) => {
		e.stopPropagation();
		e.preventDefault();

		const $bbbReloadInfoModal = $('.reload-info-modal');

		populateModalForm($bbbReloadInfoModal, {
			title: '',
			closeLabel: 'OK',
		});

		$bbbReloadInfoModal.appendTo('body').modal('show');
	});
});
<|endoftext|>


<commit_before>import React from 'react'

import {API_URL} from 'lib/constants'
import fetch from 'lib/utils/fetch'

export default function Status() {
  const [serverOk, setServerOk] = React.useState(false)
  const [serverStatus, setServerStatus] = React.useState('Unknown')

  // On component mount
  React.useEffect(() => {
    let timeoutId = null

    function pingApi() {
      const startTime = new Date()
      fetch(API_URL, {method: 'options'})
        .then(res => {
          if (res.ok) {
            setServerOk(true)
            setServerStatus((new Date() - startTime) / 1000 + 's')
          } else {
            setServerOk(false)
            setServerStatus(res.status)
          }
        })
        .catch(e => {
          setServerOk(false)
          setServerStatus(String(e))
        })
        .finally(() => {
          timeoutId = setTimeout(pingApi, 10000)
        })
    }

    pingApi()

    return () => clearTimeout(timeoutId)
  }, [])

  return (

Conveyal Status

              Client: OK

              Server:

                {serverStatus}

  )
}
<commit_msg>Show server version information<commit_after>import React from 'react'

import fetch from 'lib/utils/fetch'

const API_URL = process.env.API_URL

export default function Status() {
  const [serverOk, setServerOk] = React.useState(false)
  const [serverStatus, setServerStatus] = React.useState('Unknown')

  // On component mount
  React.useEffect(() => {
    let timeoutId = null

    function pingApi() {
      const startTime = new Date()
      fetch(`${API_URL}/version`)
        .then(async res => {
          if (res.ok) {
            const body = await res.json()
            setServerOk(true)
            setServerStatus({
              ...body,
              responseTime: (new Date() - startTime) / 1000 + 's'
            })
          } else {
            setServerOk(false)
            setServerStatus(res.status)
          }
        })
        .catch(e => {
          setServerOk(false)
          setServerStatus(String(e))
        })
        .finally(() => {
          timeoutId = setTimeout(pingApi, 10000)
        })
    }

    pingApi()

    return () => clearTimeout(timeoutId)
  }, [])

  return (

Conveyal Status

              Client: OK

              Server:

                {JSON.stringify(serverStatus, null, '\t')}

  )
}
<|endoftext|>


<commit_before>/**
 * Created by GUY on 2015/6/26.
 */

BI.Tab = BI.inherit(BI.Widget, {
    _defaultConfig: function () {
        return BI.extend(BI.Tab.superclass._defaultConfig.apply(this, arguments), {
            baseCls: "bi-tab",
            direction: "top", // top, bottom, left, right, custom
            single: false, // 是不是单页面
            logic: {
                dynamic: false
            },
            showIndex: false,
            tab: false,
            cardCreator: function (v) {
                return BI.createWidget();
            },
            keepAlives: []
        });
    },

    render: function () {
        var self = this, o = this.options;
        if (BI.isObject(o.tab)) {
            this.tab = BI.createWidget(this.options.tab, {type: "bi.button_group"});
            this.tab.on(BI.Controller.EVENT_CHANGE, function (type, value, obj) {
                self.fireEvent(BI.Controller.EVENT_CHANGE, arguments);
            });
        }
        this.cardMap = {};
        this.layout = BI.createWidget({
            type: "bi.card"
        });

        BI.createWidget(BI.extend({
            element: this
        }, BI.LogicFactory.createLogic(BI.LogicFactory.createLogicTypeByDirection(o.direction), BI.extend({}, o.logic, {
            items: BI.LogicFactory.createLogicItemsByDirection(o.direction, this.tab, this.layout)
        }))));

        var listener = new BI.ShowListener({
            eventObj: this.tab,
            cardLayout: this.layout,
            cardCreator: function (v) {
                var card = o.cardCreator.apply(self, arguments);
                self.cardMap[v] = card;
                return card;
            },
            afterCardShow: function (v) {
                self._deleteOtherCards(v);
                self.curr = v;
            }
        });
        listener.on(BI.ShowListener.EVENT_CHANGE, function (value) {
            self.fireEvent(BI.Tab.EVENT_CHANGE, value, self);
        });

        if (BI.isFunction(o.showIndex)) {
            this.__watch(o.showIndex, function (context, newValue) {
                self.setSelect(newValue);
            })
        }
    },

    _deleteOtherCards: function (currCardName) {
        var self = this, o = this.options;
        if (o.single === true) {
            BI.each(this.cardMap, function (name, card) {
                if (name !== (currCardName + "") && self._keepAlive(name) !== true) {
                    self.layout.deleteCardByName(name);
                    delete self.cardMap[name];
                }
            });
        }
    },

    _assertCard: function (v) {
        if (!this.layout.isCardExisted(v)) {
            var card = this.options.cardCreator(v);
            this.cardMap[v] = card;
            this.layout.addCardByName(v, card);
        }
    },

    _keepAlive: function (v) {
        var o = this.options;
        return BI.isFunction(o.keepAlives) ? o.keepAlives(v) : BI.contains(o.keepAlives, v);

    },

    created: function () {
        var o = this.options;
        if (o.showIndex !== false) {
            this.setSelect(BI.isFunction(o.showIndex) ? o.showIndex() : o.showIndex);
        }
    },

    setSelect: function (v, action, callback) {
        this.tab && this.tab.setValue(v);
        this._assertCard(v);
        this.layout.showCardByName(v, action, callback);
        this._deleteOtherCards(v);
        if (this.curr !== v) {
            this.curr = v;
        }
    },

    removeTab: function (cardname) {
        var self = this, o = this.options;
        BI.any(this.cardMap, function (name, card) {
            if (BI.isEqual(name, (cardname + ""))) {
                self.layout.deleteCardByName(name);
                delete self.cardMap[name];
                return true;
            }
        });
    },

    isCardExisted: function (cardName) {
        return this.layout.isCardExisted(cardName);
    },

    getSelect: function () {
        return this.curr;
    },

    getSelectedTab: function () {
        return this.layout.getShowingCard();
    },

    getTab: function (v) {
        this._assertCard(v);
        return this.layout.getCardByName(v);
    },

    setValue: function (v) {
        var card = this.layout.getShowingCard();
        if (card) {
            card.setValue(v);
        }
    },

    getValue: function () {
        var card = this.layout.getShowingCard();
        if (card) {
            return card.getValue();
        }
    },

    populate: function () {
        var card = this.layout.getShowingCard();
        if (card) {
            return card.populate && card.populate.apply(card, arguments);
        }
    },

    empty: function () {
        this.layout.deleteAllCard();
        this.cardMap = {};
    },

    destroy: function () {
        this.cardMap = {};
        BI.Tab.superclass.destroy.apply(this, arguments);
    }
});
BI.Tab.EVENT_CHANGE = "EVENT_CHANGE";

BI.shortcut("bi.tab", BI.Tab);
<commit_msg>无jira任务 tab showIndex function v1<commit_after>/**
 * Created by GUY on 2015/6/26.
 */

BI.Tab = BI.inherit(BI.Widget, {
    _defaultConfig: function () {
        return BI.extend(BI.Tab.superclass._defaultConfig.apply(this, arguments), {
            baseCls: "bi-tab",
            direction: "top", // top, bottom, left, right, custom
            single: false, // 是不是单页面
            logic: {
                dynamic: false
            },
            showIndex: false,
            tab: false,
            cardCreator: function (v) {
                return BI.createWidget();
            },
            keepAlives: []
        });
    },

    render: function () {
        var self = this, o = this.options;
        if (BI.isObject(o.tab)) {
            this.tab = BI.createWidget(this.options.tab, {type: "bi.button_group"});
            this.tab.on(BI.Controller.EVENT_CHANGE, function (type, value, obj) {
                self.fireEvent(BI.Controller.EVENT_CHANGE, arguments);
            });
        }
        this.cardMap = {};
        this.layout = BI.createWidget({
            type: "bi.card"
        });

        BI.createWidget(BI.extend({
            element: this
        }, BI.LogicFactory.createLogic(BI.LogicFactory.createLogicTypeByDirection(o.direction), BI.extend({}, o.logic, {
            items: BI.LogicFactory.createLogicItemsByDirection(o.direction, this.tab, this.layout)
        }))));

        var listener = new BI.ShowListener({
            eventObj: this.tab,
            cardLayout: this.layout,
            cardCreator: function (v) {
                var card = o.cardCreator.apply(self, arguments);
                self.cardMap[v] = card;
                return card;
            },
            afterCardShow: function (v) {
                self._deleteOtherCards(v);
                self.curr = v;
            }
        });
        listener.on(BI.ShowListener.EVENT_CHANGE, function (value) {
            self.fireEvent(BI.Tab.EVENT_CHANGE, value, self);
        });
    },

    _deleteOtherCards: function (currCardName) {
        var self = this, o = this.options;
        if (o.single === true) {
            BI.each(this.cardMap, function (name, card) {
                if (name !== (currCardName + "") && self._keepAlive(name) !== true) {
                    self.layout.deleteCardByName(name);
                    delete self.cardMap[name];
                }
            });
        }
    },

    _assertCard: function (v) {
        if (!this.layout.isCardExisted(v)) {
            var card = this.options.cardCreator(v);
            this.cardMap[v] = card;
            this.layout.addCardByName(v, card);
        }
    },

    _keepAlive: function (v) {
        var o = this.options;

        return BI.isFunction(o.keepAlives) ? o.keepAlives(v) : BI.contains(o.keepAlives, v);
    },

    created: function () {
        var self = this, o = this.options;
        if (o.showIndex !== false) {
            if (BI.isFunction(o.showIndex)) {
                var v = this.__watch(o.showIndex, function (context, newValue) {
                    self.setSelect(newValue);
                });
                this.setSelect(v);
            }
            this.setSelect(o.showIndex);
        }
    },

    setSelect: function (v, action, callback) {
        this.tab && this.tab.setValue(v);
        this._assertCard(v);
        this.layout.showCardByName(v, action, callback);
        this._deleteOtherCards(v);
        if (this.curr !== v) {
            this.curr = v;
        }
    },

    removeTab: function (cardname) {
        var self = this, o = this.options;
        BI.any(this.cardMap, function (name, card) {
            if (BI.isEqual(name, (cardname + ""))) {
                self.layout.deleteCardByName(name);
                delete self.cardMap[name];
                return true;
            }
        });
    },

    isCardExisted: function (cardName) {
        return this.layout.isCardExisted(cardName);
    },

    getSelect: function () {
        return this.curr;
    },

    getSelectedTab: function () {
        return this.layout.getShowingCard();
    },

    getTab: function (v) {
        this._assertCard(v);
        return this.layout.getCardByName(v);
    },

    setValue: function (v) {
        var card = this.layout.getShowingCard();
        if (card) {
            card.setValue(v);
        }
    },

    getValue: function () {
        var card = this.layout.getShowingCard();
        if (card) {
            return card.getValue();
        }
    },

    populate: function () {
        var card = this.layout.getShowingCard();
        if (card) {
            return card.populate && card.populate.apply(card, arguments);
        }
    },

    empty: function () {
        this.layout.deleteAllCard();
        this.cardMap = {};
    },

    destroy: function () {
        this.cardMap = {};
        BI.Tab.superclass.destroy.apply(this, arguments);
    }
});
BI.Tab.EVENT_CHANGE = "EVENT_CHANGE";

BI.shortcut("bi.tab", BI.Tab);
<|endoftext|>


<commit_before>/**
 * ALttP Discord Bot
  */

// Import modules
const request = require('request'),
  irc = require('irc'),
  fs = require('fs'),
  path = require('path'),
  moment = require('moment'),
  Discord = require('discord.js'),
  staticCommands = require('./lib/static-commands.js'),
  cooldowns = require('./lib/cooldowns.js'),
  StreamAlerts = require('./lib/stream-alerts.js'),
  RaceAlerts = require('./lib/race-alerts.js'),
  src = require('./lib/src.js'),
  timers = require('./lib/timers.js');

// Read in bot configuration
let config = require('./config.json');

// Set up Twitch stream watcher
const streamWatcher = new StreamAlerts(config.streamAlerts);

// Set up SRL Race watcher
const raceWatcher = new RaceAlerts(config.srl);

// Set up the commands the bot will natively handle
const commands = {
  // Allow members to request role additions/removals for allowed roles
  'role': (msg, guildConfig) => {
    // make sure there are allowed roles defined
    if (typeof guildConfig.allowedRolesForRequest === undefined || guildConfig.allowedRolesForRequest.length === 0) {
      return msg.reply('No roles are currently allowed to be added/removed by members.');
    }

    let validRoles = guildConfig.allowedRolesForRequest.split('|');

    if (msg.content === guildConfig.cmdPrefix+'role') {
      return dmUserFromMsg(msg, `Useage: ${guildConfig.cmdPrefix}role {add|remove} {${guildConfig.allowedRolesForRequest}}`);
    }

    // parse+validate action+role (use original case from message because roles are case-sensitive)
    let roleName = msg.originalContent.match(/role\s(add|remove)\s([a-z0-9\-]+)/i);
    if (!roleName) {
      return dmUserFromMsg(msg, `You must include a role name! *e.g. ${guildConfig.cmdPrefix}role ${roleName[1]} ${validRoles[0]}*`);
    } else {
      let tester = new RegExp(guildConfig.allowedRolesForRequest, 'i');
      if (tester.test(roleName[2])) {
        // make sure this message is in a guild channel they're a member of
        if (!msg.guild) return;

        // find the role in the member's guild
        let role = msg.guild.roles.find('name', roleName[2]);

        if (!role) {
          return dmUserFromMsg(msg, `${roleName[2]} is not a role on this server!`);
        }

        // add/remove the role and DM the user the results
        if (roleName[1] === 'add') {
          msg.member.addRole(role)
            .then(requestingMember => {
              requestingMember.createDM()
                .then(channel => {
                  channel.send(`You have successfully been added to the ${roleName[2]} group!`)
                })
                .catch(console.error)
            })
            .catch(console.log);
        } else if (roleName[1] === 'remove') {
          msg.member.removeRole(role)
            .then(requestingMember => {
              requestingMember.createDM()
                .then(channel => {
                  channel.send(`You have successfully been removed from the ${roleName[2]} group!`)
                })
                .catch(console.error)
            })
            .catch(console.error);
        } else {
          return dmUserFromMsg(msg, `You must use add/remove after the role command! *e.g. ${guildConfig.cmdPrefix}role add ${validRoles[0]}*`);
        }
      } else {
        dmUserFromMsg(msg, `${roleName[1]} is not a valid role name! The roles allowed for request are: ${validRoles.join(',')}`);
      }
    }
  },
  // Speedrun.com API Integration (leaderboard lookups)
  'wr': (msg, guildConfig) => {
    if (msg.content === guildConfig.cmdPrefix+'wr') {
      return dmUserFromMsg(msg, `Useage: ${guildConfig.cmdPrefix}wr {nmg/mg} {subcategory-code}`);
    }

    let [command, majorCat, minorCat] = msg.content.split(' ');
    if (!command || !majorCat || !minorCat || (majorCat !== 'nmg' && majorCat !== 'mg')) {
      return dmUserFromMsg(msg, `Useage: ${guildConfig.cmdPrefix}wr {nmg/mg} {subcategory-code}`);
    }

    let cooldownKey = msg.content + msg.channel.id;
    src.findWR(config.src.gameSlug, majorCat, minorCat)
      .then(result => {
        msg.reply(result).then(sentMsg => cooldowns.set(cooldownKey, guildConfig.srcCmdCooldown));
      })
      .catch(console.error);
  },
  'pb': (msg, guildConfig) => {
    if (msg.content === guildConfig.cmdPrefix+'pb') {
      return dmUserFromMsg(msg, `Useage: ${guildConfig.cmdPrefix}pb {speedrun.com-username} {nmg/mg} {subcategory-code}`);
    }

    let [command, username, majorCat, minorCat] = msg.content.split(' ');
    if (!command || !username || !majorCat || !minorCat || (majorCat !== 'nmg' && majorCat !== 'mg')) {
      return dmUserFromMsg(msg, `Useage: ${guildConfig.cmdPrefix}pb {speedrun.com-username} {nmg/mg} {subcategory-code}`);
    }

    let cooldownKey = msg.content + msg.channel.id;
    src.findPB(username, majorCat, minorCat)
      .then(result => {
        msg.reply(result).then(sentMsg => cooldowns.set(cooldownKey, guildConfig.srcCmdCooldown));
      })
      .catch(console.error);
  },
  // @todo implement pulling in category rules from SRC
  'rules': (msg, guildConfig) => {
  }
};

// Set up Discord client
const client = new Discord.Client();
// Wait for bot to be ready before watching streams/races
client.on('ready', () => {
  console.log(config.botName + ' Online');

  // Set up alerts for each guild we're a member of
  client.guilds.forEach((guild) => {
    if (guild.id !== "default") {
      initGuild(guild, config);
    }
  });
// Listen for commands for the bot to respond to across all channels
}).on('message', msg => {
  msg.originalContent = msg.content;
  msg.content = msg.content.toLowerCase();

  // Find the guild config for this msg, use default if no guild (DM)
  let guildConfig = (msg.guild) ? config.discord.guilds[msg.guild.id] : config.discord.guilds.default;

  // Make sure it starts with the configured prefix
  if (!msg.content.startsWith(guildConfig.cmdPrefix)) return;

  // And that it's not on cooldown
  let cooldownKey = msg.content + msg.channel.id;
  cooldowns.get(cooldownKey, guildConfig.textCmdCooldown)
    .then(onCooldown => {
      if (onCooldown === false) {
        // Not on CD, check for native or static command
        let commandNoPrefix = msg.content.slice(guildConfig.cmdPrefix.length).split(' ')[0];
        console.log(`'${commandNoPrefix}' received in ${guildConfig.internalName}#${msg.channel.name} from @${msg.author.username}`);
        if (commands.hasOwnProperty(commandNoPrefix)) {
          commands[commandNoPrefix](msg, guildConfig);
        } else if (staticCommands.exists(commandNoPrefix)) {
          let result = staticCommands.get(commandNoPrefix);
          msg.channel.send({embed: {
            "title": commandNoPrefix,
            "color": 0xff9f25,
            "description": result
          }}).then(sentMessage => cooldowns.set(cooldownKey, guildConfig.textCmdCooldown))
          .catch(console.error);
        } else {
          // Not a command we recognize, ignore
        }
      } else {
        // DM the user that it's on CD
        dmUserFromMsg(msg, `**${msg.content}** is currently on cooldown for another *${onCooldown} seconds!*`);
      }
    })
    .catch(console.error);
// Create an event listener for new guild members
}).on('guildMemberAdd', member => {
  console.log(`A new member has joined '${member.guild.name}': ${member.displayName}`);
  // Check to see if this guild has welcome DM's enabled
  let guildConfig = (member.guild) ? config.discord.guilds[member.guild.id] : config.discord.guilds.default;
  if (guildConfig.enableWelcomeDM === true && guildConfig.welcomeDMFile) {
    console.log('Guild has welcome DMs enabled, fetching message...');
    // Send the member a DM using the configured message for the guild
    let filePath = path.join(__dirname, 'conf', guildConfig.welcomeDMFile);
    let welcomeMessage = fs.readFileSync(filePath, 'utf-8').toString();
    if (welcomeMessage) {
      console.log('Sending welcome DM to user...');
      dmUser(member, welcomeMessage);
    }
  }
// Log the bot in
}).login(config.discord.token);

function initGuild(guild, config)
{
  // @TODO add some validation to guild config
  // - require alerts channel if any alerts are enabled
  // - require weekly race role if weekly race alert is enabled
  let guildConfig = config.discord.guilds[guild.id];

  // Find the text channel(s) where we'll be posting alerts
  let alertsChannel = guild.channels.find('name', guildConfig.alertsChannelName);
  if (guildConfig.alertOnConnect === true) alertsChannel.send(config.botName + ' has connected. :white_check_mark:');

  // Watch + alert for Twitch streams
  if (guildConfig.enableLivestreamAlerts) {
    let embed = new Discord.RichEmbed();
    streamWatcher.on('live', stream => {
      embed.setStreamAlertDefaults(stream)
        .setTitle(`Now live at ${stream.channel.url}!`)
        .setColor('#339e31')
        .setImage(`${stream.preview.medium}?r=${moment().valueOf()}`);
      alertsChannel.send({embed});
    }).on('title', stream => {
      embed.setStreamAlertDefaults(stream)
        .setTitle(`Changed title:`)
        .setColor('#dedede')
        .setImage(null)
      alertsChannel.send({embed});
    }).watch();
  }

  // Watch + alert for SRL races
  if (guildConfig.enableRaceAlerts) {
    let embed = new Discord.RichEmbed();
    raceWatcher.on('init', (raceChannel, srlUrl) => {
      embed.setRaceAlertDefaults(raceChannel, srlUrl).setDescription(`A race was just started for *${config.srl.gameName}*!`);
      alertsChannel.send({embed});
    }).on('goal', (raceChannel, goal, srlUrl) => {
      embed.setRaceAlertDefaults(raceChannel, srlUrl).setDescription(`Goal was set to: **${goal}**`);
      alertsChannel.send({embed});
    }).on('done', (raceChannel, goal, srlUrl) => {
      embed.setRaceAlertDefaults(raceChannel, srlUrl).setDescription('Race finished!');
      alertsChannel.send({embed});
    }).on('rematch', (raceChannel, goal, srlUrl) => {
      embed.setRaceAlertDefaults(raceChannel, srlUrl).setDescription('Rematch initiated!');
      alertsChannel.send({embed});
    }).on('error', console.error)
    .watch();
  }

  // Schedule timers for some special messages / commands
  //
  // Weekly NMG Race Alert: Every Sunday at 11 AM Pacific /
  if (guildConfig.enableWeeklyRaceAlert) {
    let weeklyAlertTimestamp = moment().day(7).hour(11).minute(0).second(0).valueOf();
    let weeklyRaceAlertRole = guild.roles.find('name', guildConfig.weeklyRaceAlertRole);
    timers.onceAndRepeat(weeklyAlertTimestamp, 604800, 'weekly-alert')
      .on('weekly-alert', () => {
        let randomEmoji = guild.emojis.random();
        alertsChannel.send([
          weeklyRaceAlertRole,
          `The weekly Any% NMG Race is starting in 1 Hour! ${randomEmoji} Information on joining SRL can be found here: http://www.speedrunslive.com/faq/#join`
        ]);
        console.log('received weekly alert event at' + moment().format('MMMM Do YYYY, h:mm:ss a'));
      });
  }
}

function dmUserFromMsg(originalMessage, newMessage)
{
  // check that this isn't already a DM before sending
  if (originalMessage.channel.type === 'dm') {
    originalMessage.channel.send(newMessage);
  } else {
    originalMessage.member.createDM()
      .then(channel => {
        channel.send(newMessage);
      })
      .catch(console.log);
  }
}

function dmUser(user, message)
{
  user.createDM()
    .then(channel => {
      channel.send(message);
    })
    .catch(console.log);
}

Discord.RichEmbed.prototype.setStreamAlertDefaults = function (stream) {
  return this.setAuthor(stream.channel.display_name, stream.channel.logo)
    .setURL(stream.channel.url)
    .setDescription(stream.channel.status)
    .setTimestamp();
};

Discord.RichEmbed.prototype.setRaceAlertDefaults = function (raceChannel, srlUrl) {
  return this.setTitle(`SRL Race #${raceChannel}`)
    .setURL(srlUrl)
    .setThumbnail('http://i.imgur.com/8nqgDcI.png')
    .setColor('#f8e47f')
    .setFooter(`#${raceChannel}`)
    .setTimestamp();
};

// Converts seconds to human-readable time
String.prototype.toHHMMSS = function () {
  let sec_num = parseInt(this, 10); // don't forget the second param
  let hours   = Math.floor(sec_num / 3600);
  let minutes = Math.floor((sec_num - (hours * 3600)) / 60);
  let seconds = sec_num - (hours * 3600) - (minutes * 60);

  if (hours   < 10) {hours   = "0"+hours;}
  if (minutes < 10) {minutes = "0"+minutes;}
  if (seconds < 10) {seconds = "0"+seconds;}
  return hours+':'+minutes+':'+seconds;
};

// catch Promise errors
process.on('unhandledRejection', console.error);<commit_msg>make sure a valid alerts channel exists before enabling watches<commit_after>/**
 * ALttP Discord Bot
  */

// Import modules
const request = require('request'),
  irc = require('irc'),
  fs = require('fs'),
  path = require('path'),
  moment = require('moment'),
  Discord = require('discord.js'),
  staticCommands = require('./lib/static-commands.js'),
  cooldowns = require('./lib/cooldowns.js'),
  StreamAlerts = require('./lib/stream-alerts.js'),
  RaceAlerts = require('./lib/race-alerts.js'),
  src = require('./lib/src.js'),
  timers = require('./lib/timers.js');

// Read in bot configuration
let config = require('./config.json');

// Set up Twitch stream watcher
const streamWatcher = new StreamAlerts(config.streamAlerts);

// Set up SRL Race watcher
const raceWatcher = new RaceAlerts(config.srl);

// Set up the commands the bot will natively handle
const commands = {
  // Allow members to request role additions/removals for allowed roles
  'role': (msg, guildConfig) => {
    // make sure there are allowed roles defined
    if (typeof guildConfig.allowedRolesForRequest === undefined || guildConfig.allowedRolesForRequest.length === 0) {
      return msg.reply('No roles are currently allowed to be added/removed by members.');
    }

    let validRoles = guildConfig.allowedRolesForRequest.split('|');

    if (msg.content === guildConfig.cmdPrefix+'role') {
      return dmUserFromMsg(msg, `Useage: ${guildConfig.cmdPrefix}role {add|remove} {${guildConfig.allowedRolesForRequest}}`);
    }

    // parse+validate action+role (use original case from message because roles are case-sensitive)
    let roleName = msg.originalContent.match(/role\s(add|remove)\s([a-z0-9\-]+)/i);
    if (!roleName) {
      return dmUserFromMsg(msg, `You must include a role name! *e.g. ${guildConfig.cmdPrefix}role ${roleName[1]} ${validRoles[0]}*`);
    } else {
      let tester = new RegExp(guildConfig.allowedRolesForRequest, 'i');
      if (tester.test(roleName[2])) {
        // make sure this message is in a guild channel they're a member of
        if (!msg.guild) return;

        // find the role in the member's guild
        let role = msg.guild.roles.find('name', roleName[2]);

        if (!role) {
          return dmUserFromMsg(msg, `${roleName[2]} is not a role on this server!`);
        }

        // add/remove the role and DM the user the results
        if (roleName[1] === 'add') {
          msg.member.addRole(role)
            .then(requestingMember => {
              requestingMember.createDM()
                .then(channel => {
                  channel.send(`You have successfully been added to the ${roleName[2]} group!`)
                })
                .catch(console.error)
            })
            .catch(console.log);
        } else if (roleName[1] === 'remove') {
          msg.member.removeRole(role)
            .then(requestingMember => {
              requestingMember.createDM()
                .then(channel => {
                  channel.send(`You have successfully been removed from the ${roleName[2]} group!`)
                })
                .catch(console.error)
            })
            .catch(console.error);
        } else {
          return dmUserFromMsg(msg, `You must use add/remove after the role command! *e.g. ${guildConfig.cmdPrefix}role add ${validRoles[0]}*`);
        }
      } else {
        dmUserFromMsg(msg, `${roleName[1]} is not a valid role name! The roles allowed for request are: ${validRoles.join(',')}`);
      }
    }
  },
  // Speedrun.com API Integration (leaderboard lookups)
  'wr': (msg, guildConfig) => {
    if (msg.content === guildConfig.cmdPrefix+'wr') {
      return dmUserFromMsg(msg, `Useage: ${guildConfig.cmdPrefix}wr {nmg/mg} {subcategory-code}`);
    }

    let [command, majorCat, minorCat] = msg.content.split(' ');
    if (!command || !majorCat || !minorCat || (majorCat !== 'nmg' && majorCat !== 'mg')) {
      return dmUserFromMsg(msg, `Useage: ${guildConfig.cmdPrefix}wr {nmg/mg} {subcategory-code}`);
    }

    let cooldownKey = msg.content + msg.channel.id;
    src.findWR(config.src.gameSlug, majorCat, minorCat)
      .then(result => {
        msg.reply(result).then(sentMsg => cooldowns.set(cooldownKey, guildConfig.srcCmdCooldown));
      })
      .catch(console.error);
  },
  'pb': (msg, guildConfig) => {
    if (msg.content === guildConfig.cmdPrefix+'pb') {
      return dmUserFromMsg(msg, `Useage: ${guildConfig.cmdPrefix}pb {speedrun.com-username} {nmg/mg} {subcategory-code}`);
    }

    let [command, username, majorCat, minorCat] = msg.content.split(' ');
    if (!command || !username || !majorCat || !minorCat || (majorCat !== 'nmg' && majorCat !== 'mg')) {
      return dmUserFromMsg(msg, `Useage: ${guildConfig.cmdPrefix}pb {speedrun.com-username} {nmg/mg} {subcategory-code}`);
    }

    let cooldownKey = msg.content + msg.channel.id;
    src.findPB(username, majorCat, minorCat)
      .then(result => {
        msg.reply(result).then(sentMsg => cooldowns.set(cooldownKey, guildConfig.srcCmdCooldown));
      })
      .catch(console.error);
  },
  // @todo implement pulling in category rules from SRC
  'rules': (msg, guildConfig) => {
  }
};

// Set up Discord client
const client = new Discord.Client();
// Wait for bot to be ready before watching streams/races
client.on('ready', () => {
  console.log(config.botName + ' Online');

  // Set up alerts for each guild we're a member of
  client.guilds.forEach((guild) => {
    if (guild.id !== "default") {
      initGuild(guild, config);
    }
  });
// Listen for commands for the bot to respond to across all channels
}).on('message', msg => {
  msg.originalContent = msg.content;
  msg.content = msg.content.toLowerCase();

  // Find the guild config for this msg, use default if no guild (DM)
  let guildConfig = (msg.guild) ? config.discord.guilds[msg.guild.id] : config.discord.guilds.default;

  // Make sure it starts with the configured prefix
  if (!msg.content.startsWith(guildConfig.cmdPrefix)) return;

  // And that it's not on cooldown
  let cooldownKey = msg.content + msg.channel.id;
  cooldowns.get(cooldownKey, guildConfig.textCmdCooldown)
    .then(onCooldown => {
      if (onCooldown === false) {
        // Not on CD, check for native or static command
        let commandNoPrefix = msg.content.slice(guildConfig.cmdPrefix.length).split(' ')[0];
        console.log(`'${commandNoPrefix}' received in ${guildConfig.internalName}#${msg.channel.name} from @${msg.author.username}`);
        if (commands.hasOwnProperty(commandNoPrefix)) {
          commands[commandNoPrefix](msg, guildConfig);
        } else if (staticCommands.exists(commandNoPrefix)) {
          let result = staticCommands.get(commandNoPrefix);
          msg.channel.send({embed: {
            "title": commandNoPrefix,
            "color": 0xff9f25,
            "description": result
          }}).then(sentMessage => cooldowns.set(cooldownKey, guildConfig.textCmdCooldown))
          .catch(console.error);
        } else {
          // Not a command we recognize, ignore
        }
      } else {
        // DM the user that it's on CD
        dmUserFromMsg(msg, `**${msg.content}** is currently on cooldown for another *${onCooldown} seconds!*`);
      }
    })
    .catch(console.error);
// Create an event listener for new guild members
}).on('guildMemberAdd', member => {
  console.log(`A new member has joined '${member.guild.name}': ${member.displayName}`);
  // Check to see if this guild has welcome DM's enabled
  let guildConfig = (member.guild) ? config.discord.guilds[member.guild.id] : config.discord.guilds.default;
  if (guildConfig.enableWelcomeDM === true && guildConfig.welcomeDMFile) {
    console.log('Guild has welcome DMs enabled, fetching message...');
    // Send the member a DM using the configured message for the guild
    let filePath = path.join(__dirname, 'conf', guildConfig.welcomeDMFile);
    let welcomeMessage = fs.readFileSync(filePath, 'utf-8').toString();
    if (welcomeMessage) {
      console.log('Sending welcome DM to user...');
      dmUser(member, welcomeMessage);
    }
  }
// Log the bot in
}).login(config.discord.token);

function initGuild(guild, config)
{
  let guildConfig = config.discord.guilds[guild.id];

  // Find the text channel(s) where we'll be posting alerts
  let alertsChannel = guild.channels.find('name', guildConfig.alertsChannelName);
  if (guildConfig.alertOnConnect === true) alertsChannel.send(config.botName + ' has connected. :white_check_mark:');

  // Watch + alert for Twitch streams
  if (alertsChannel && guildConfig.enableLivestreamAlerts) {
    let embed = new Discord.RichEmbed();
    streamWatcher.on('live', stream => {
      embed.setStreamAlertDefaults(stream)
        .setTitle(`Now live at ${stream.channel.url}!`)
        .setColor('#339e31')
        .setImage(`${stream.preview.medium}?r=${moment().valueOf()}`);
      alertsChannel.send({embed});
    }).on('title', stream => {
      embed.setStreamAlertDefaults(stream)
        .setTitle(`Changed title:`)
        .setColor('#dedede')
        .setImage(null)
      alertsChannel.send({embed});
    }).watch();
  }

  // Watch + alert for SRL races
  if (alertsChannel && guildConfig.enableRaceAlerts) {
    let embed = new Discord.RichEmbed();
    raceWatcher.on('init', (raceChannel, srlUrl) => {
      embed.setRaceAlertDefaults(raceChannel, srlUrl).setDescription(`A race was just started for *${config.srl.gameName}*!`);
      alertsChannel.send({embed});
    }).on('goal', (raceChannel, goal, srlUrl) => {
      embed.setRaceAlertDefaults(raceChannel, srlUrl).setDescription(`Goal was set to: **${goal}**`);
      alertsChannel.send({embed});
    }).on('done', (raceChannel, goal, srlUrl) => {
      embed.setRaceAlertDefaults(raceChannel, srlUrl).setDescription('Race finished!');
      alertsChannel.send({embed});
    }).on('rematch', (raceChannel, goal, srlUrl) => {
      embed.setRaceAlertDefaults(raceChannel, srlUrl).setDescription('Rematch initiated!');
      alertsChannel.send({embed});
    }).on('error', console.error)
    .watch();
  }

  // Schedule timers for some special messages / commands
  //
  // Weekly NMG Race Alert: Every Sunday at 11 AM Pacific /
  if (alertsChannel && guildConfig.enableWeeklyRaceAlert) {
    let weeklyAlertTimestamp = moment().day(7).hour(11).minute(0).second(0).valueOf();
    let weeklyRaceAlertRole = guild.roles.find('name', guildConfig.weeklyRaceAlertRole);
    timers.onceAndRepeat(weeklyAlertTimestamp, 604800, 'weekly-alert')
      .on('weekly-alert', () => {
        let randomEmoji = guild.emojis.random();
        alertsChannel.send([
          weeklyRaceAlertRole,
          `The weekly Any% NMG Race is starting in 1 Hour! ${randomEmoji} Information on joining SRL can be found here: http://www.speedrunslive.com/faq/#join`
        ]);
        console.log('received weekly alert event at' + moment().format('MMMM Do YYYY, h:mm:ss a'));
      });
  }
}

function dmUserFromMsg(originalMessage, newMessage)
{
  // check that this isn't already a DM before sending
  if (originalMessage.channel.type === 'dm') {
    originalMessage.channel.send(newMessage);
  } else {
    originalMessage.member.createDM()
      .then(channel => {
        channel.send(newMessage);
      })
      .catch(console.log);
  }
}

function dmUser(user, message)
{
  user.createDM()
    .then(channel => {
      channel.send(message);
    })
    .catch(console.log);
}

Discord.RichEmbed.prototype.setStreamAlertDefaults = function (stream) {
  return this.setAuthor(stream.channel.display_name, stream.channel.logo)
    .setURL(stream.channel.url)
    .setDescription(stream.channel.status)
    .setTimestamp();
};

Discord.RichEmbed.prototype.setRaceAlertDefaults = function (raceChannel, srlUrl) {
  return this.setTitle(`SRL Race #${raceChannel}`)
    .setURL(srlUrl)
    .setThumbnail('http://i.imgur.com/8nqgDcI.png')
    .setColor('#f8e47f')
    .setFooter(`#${raceChannel}`)
    .setTimestamp();
};

// Converts seconds to human-readable time
String.prototype.toHHMMSS = function () {
  let sec_num = parseInt(this, 10); // don't forget the second param
  let hours   = Math.floor(sec_num / 3600);
  let minutes = Math.floor((sec_num - (hours * 3600)) / 60);
  let seconds = sec_num - (hours * 3600) - (minutes * 60);

  if (hours   < 10) {hours   = "0"+hours;}
  if (minutes < 10) {minutes = "0"+minutes;}
  if (seconds < 10) {seconds = "0"+seconds;}
  return hours+':'+minutes+':'+seconds;
};

// catch Promise errors
process.on('unhandledRejection', console.error);<|endoftext|>


<commit_before>import React, { Component } from 'react';
import Head from "next/head"
import Router from "next/router"
import { graphql, gql, compose } from 'react-apollo'

import Page from "../layouts/main"
import withData from '../lib/withData'

class VerifyPage extends Component {

  static async getInitialProps({ query }) {
    return { verificationCode: query.verificationCode }
  }

  constructor(props) {
    super(props)
    this.state = {
      isVerifying: false,
      error: null,
    }
  }

  componentDidMount() {
    this.verifyUser()
  }

  verifyUser = async () => {
    if (!this.props.verificationCode) {
      this.setState({
        error: "No verification code found."
      })
      Router.push('/')
      return;
    }
    if (this.state.isVerifying) { return; }
    this.setState({
      isVerifying: true,
      error: null,
    })
    try {
      const verifyUser = await this.props.verifyUserMutation({
        variables: {
          verificationCode: this.props.verificationCode,
        }
      })
      if (verifyUser.username) {
        alert(`Successfully verify ${verifyUser.username}.`)
        Router.push('/join')
      } else {
        throw 'Internal Error: no username found.'
      }
      this.setState({
        isVerifying: false,
        error: null,
      })
    } catch (err) {
      this.setState({
        isVerifying: false,
        error: (err.graphQLErrors
          && err.graphQLErrors.length >= 1
          && (err.graphQLErrors[0].functionError || err.graphQLErrors[0].message)
        || err),
      })
      alert('Oops: ' + this.state.error)
      Router.push('/')
    }
  }

  render() {
    const { isVerifying, error } = this.state
    return (
      <Page>

          <title>Platonos</title>

{isVerifying && "Verifying user..."}

{error && "" + error}

      </Page>
    );
  }
}

const VERIFY_USER_MUTATION = gql`
  mutation verifyUser($verificationCode: String!) {
    verifyUser(verificationCode: $verificationCode) {
      username
    }
  }
`

export default withData(graphql(VERIFY_USER_MUTATION, { name: "verifyUserMutation" })(VerifyPage));
<commit_msg>Update message<commit_after>import React, { Component } from 'react';
import Head from "next/head"
import Router from "next/router"
import { graphql, gql, compose } from 'react-apollo'

import Page from "../layouts/main"
import withData from '../lib/withData'

class VerifyPage extends Component {

  static async getInitialProps({ query }) {
    return { verificationCode: query.verificationCode }
  }

  constructor(props) {
    super(props)
    this.state = {
      isVerifying: false,
      error: null,
    }
  }

  componentDidMount() {
    this.verifyUser()
  }

  verifyUser = async () => {
    if (!this.props.verificationCode) {
      this.setState({
        error: "No verification code found."
      })
      Router.push('/')
      return;
    }
    if (this.state.isVerifying) { return; }
    this.setState({
      isVerifying: true,
      error: null,
    })
    try {
      const verifyUser = await this.props.verifyUserMutation({
        variables: {
          verificationCode: this.props.verificationCode,
        }
      })
      if (verifyUser.verifyUser.username) {
        alert(`Successfully verify ${verifyUser.username}. Proceed to login.`)
        Router.push('/join')
      } else {
        throw 'Internal Error: no username found.'
      }
      this.setState({
        isVerifying: false,
        error: null,
      })
    } catch (err) {
      this.setState({
        isVerifying: false,
        error: (err.graphQLErrors
          && err.graphQLErrors.length >= 1
          && (err.graphQLErrors[0].functionError || err.graphQLErrors[0].message)
        || err),
      })
      alert('Oops: ' + this.state.error)
      Router.push('/')
    }
  }

  render() {
    const { isVerifying, error } = this.state
    return (
      <Page>

          <title>Platonos</title>

{isVerifying && "Verifying user..."}

{error && "" + error}

      </Page>
    );
  }
}

const VERIFY_USER_MUTATION = gql`
  mutation verifyUser($verificationCode: String!) {
    verifyUser(verificationCode: $verificationCode) {
      username
    }
  }
`

export default withData(graphql(VERIFY_USER_MUTATION, { name: "verifyUserMutation" })(VerifyPage));
<|endoftext|>


<commit_before>import React from 'react'
import { browserHistory } from 'react-router'
import { FormattedMessage } from 'react-intl'

import { SettingsForm } from '~client/ux/components'
import { FormDomain } from '~client/mobilizations/components'
import * as paths from '~client/paths'

const MobilizationsSettingsDomainPage = ({ location, mobilization, ...formProps }) => {
  return (
    <FormDomain
      {...formProps}
      mobilization={mobilization}
      formComponent={SettingsForm}
      redirectToCreateDNS={() => browserHistory.push(paths.communityDomainCreate())}
      successMessage={
        <FormattedMessage
          id='page--mobilizations-domain.form-domain.success-message'
          defaultMessage='Dados de domínio salvos com sucesso'
        />
      }
    />
  )
}

export default MobilizationsSettingsDomainPage
<commit_msg>chore(routes-v1): fix redirect mobilization form domain<commit_after>import React from 'react'
import { FormattedMessage } from 'react-intl'

import { SettingsForm } from '~client/ux/components'
import { FormDomain } from '~client/mobilizations/components'
import * as paths from '~client/paths'

const MobilizationsSettingsDomainPage = ({ location, history, mobilization, ...formProps }) => {
  return (
    <FormDomain
      {...formProps}
      mobilization={mobilization}
      formComponent={SettingsForm}
      redirectToCreateDNS={() => history.push(paths.communityDomainCreate())}
      successMessage={
        <FormattedMessage
          id='page--mobilizations-domain.form-domain.success-message'
          defaultMessage='Dados de domínio salvos com sucesso'
        />
      }
    />
  )
}

export default MobilizationsSettingsDomainPage
<|endoftext|>