Commit 6d595c6c0f4c8738e7423ef61fc09b7a38f9096c:
    organize views

Branch: refs/heads/master
Author: Sam Ruby <[email protected]>
Committer: Sam Ruby <[email protected]>
Pusher: rubys <[email protected]>

------------------------------------------------------------
views/app.js.rb                                              | +++++ -----
views/elements/link.js.rb                                    |  
views/elements/modal-dialog.js.rb                            |  
views/forms/add-comment.js.rb                                |  
views/layout/footer.js.rb                                    |  
views/layout/header.js.rb                                    |  
views/layout/main.js.rb                                      |  
views/main.html.rb                                           |  
views/pages/comments.js.rb                                   |  
views/pages/index.js.rb                                      |  
views/pages/report.js.rb                                     |  
views/pages/search.js.rb                                     |  
------------------------------------------------------------
20 changes: 10 additions, 10 deletions.
------------------------------------------------------------


diff --git a/views/add-comment.js.rb b/views/add-comment.js.rb
deleted file mode 100644
index 3a0e5ff..0000000
--- a/views/add-comment.js.rb
+++ /dev/null
@@ -1,66 +0,0 @@
-class AddComment < React
-  def initialize
-    @save_disabled = true
-  end
-
-  def self.button
-    {
-      text: 'add comment',
-      class: 'btn_primary',
-      data_toggle: 'modal',
-      data_target: '#comment-form'
-    }
-  end
-
-  def render
-    _ModalDialog.comment_form! color: 'commented' do
-      # header
-      _h4 'Enter a comment'
-
-      #input field: initials
-      _div.form_group do
-        _label 'Initials', for: 'comment-initials'
-        _input.comment_initials!.form_control label: 'Initials',
-          placeholder: 'initials', 
-          defaultValue: @@server.pending.initials || @@server.initials
-      end
-
-      #input field: comment text
-      _div.form_group do
-        _label 'Comment', for: 'comment-text'
-        _textarea.comment_text!.form_control label: 'Comment',
-          placeholder: 'comment', rows: 5, onInput: self.input
-      end
-
-      # footer buttons
-      _button.btn_default 'Cancel', data_dismiss: 'modal'
-      _button.btn_primary 'Save', disabled: @save_disabled, onClick: self.save
-    end
-  end
-
-  # autofocus on comment form
-  def componentDidMount()
-    jQuery('#comment-form').on 'shown.bs.modal' do
-      ~'#comment-text'.focus()
-    end
-  end
-
-  # enable/disable save when input changes
-  def input(event)
-    @save_disabled = ( event.target.value.length == 0 )
-  end
-
-  def save(event)
-    data = {
-      agenda: Agenda.file,
-      attach: @@item.attach,
-      initials: ~'#comment_initials'.value,
-      comment: ~'#comment_text'.value
-    }
-
-    post 'comment', data do |pending|
-      Pending.load pending
-      jQuery('#comment-form').modal(:hide)
-    end
-  end
-end
diff --git a/views/app.js.rb b/views/app.js.rb
index ab6cc2c..f07efaa 100644
--- a/views/app.js.rb
+++ b/views/app.js.rb
@@ -1,20 +1,20 @@
 # General layout
-require_relative 'main'
-require_relative 'header'
-require_relative 'footer'
+require_relative 'layout/main'
+require_relative 'layout/header'
+require_relative 'layout/footer'
 
 # Individual pages
-require_relative 'index'
-require_relative 'report'
-require_relative 'search'
-require_relative 'comments'
+require_relative 'pages/index'
+require_relative 'pages/report'
+require_relative 'pages/search'
+require_relative 'pages/comments'
 
 # Button + forms
-require_relative 'add-comment'
+require_relative 'forms/add-comment'
 
 # Common elements
-require_relative 'link'
-require_relative 'modal-dialog'
+require_relative 'elements/link'
+require_relative 'elements/modal-dialog'
 
 # Model
 require_relative 'models/agenda'
diff --git a/views/comments.js.rb b/views/comments.js.rb
deleted file mode 100644
index d0e8b41..0000000
--- a/views/comments.js.rb
+++ /dev/null
@@ -1,20 +0,0 @@
-class Comments < React
-  def render
-    found = false
-
-    Agenda.index.each do |item|
-      next if item.comments.empty?
-      found = true
-
-      _section do
-       _h4 {_Link text: item.title, href: item.href}
-
-        item.comments.each do |comment|
-          _pre.comment comment
-        end
-      end
-    end
-
-    _p {_em 'No comments found'} unless found
-  end
-end
diff --git a/views/elements/link.js.rb b/views/elements/link.js.rb
new file mode 100644
index 0000000..6da510c
--- /dev/null
+++ b/views/elements/link.js.rb
@@ -0,0 +1,34 @@
+#
+# Replacement for 'a' element which handles clicks events by calling
+# Main.navigate.
+#
+
+class Link < React
+  def initialize
+    @attrs = {}
+  end
+
+  def componentWillMount()
+    self.componentWillReceiveProps(self.props)
+    @attrs.onClick = self.click
+  end
+
+  def componentWillReceiveProps(props)
+    @text = props.text
+    for attr in props
+      @attrs[attr] = props[attr] unless attr == 'text'
+    end
+  end
+
+  def render
+    React.createElement('a', @attrs, @text)
+  end
+
+  def click(event)
+    href = event.target.getAttribute('href')
+    if href =~ /^(\.|[-\w]+)$/
+      Main.navigate href
+      return false
+    end
+  end
+end
diff --git a/views/elements/modal-dialog.js.rb 
b/views/elements/modal-dialog.js.rb
new file mode 100644
index 0000000..b9c3a61
--- /dev/null
+++ b/views/elements/modal-dialog.js.rb
@@ -0,0 +1,63 @@
+class ModalDialog < React
+  def initialize
+    @header = []
+    @body = []
+    @footer = []
+  end
+
+  def componentWillMount()
+    self.componentWillReceiveProps(self.props)
+  end
+
+  def componentWillReceiveProps(props)
+    @header.clear()
+    @body.clear()
+    @footer.clear()
+
+    props.children.each do |child|
+      if child.type == 'h4'
+        if not child.props.className
+          child.props.className = 'modal-title'
+        elsif not child.props.className.split(' ').include? 'modal-title'
+          child.props.className += ' modal-title'
+        end
+
+        @header << child
+        ModalDialog.h4 = child
+
+      elsif child.type == 'button'
+        if not child.props.className
+          child.props.className = 'btn'
+        elsif not child.props.className.split(' ').include? 'btn'
+          child.props.className += ' btn'
+        end
+
+        @footer << child
+
+      else
+        @body << child
+      end
+    end
+  end
+
+  def render
+    _div.modal.fade id: @@id, class: @@className do
+      _div.modal_dialog do
+        _div.modal_content do
+          _div.modal_header class: @@color do
+            _button.close "\u00d7", type: 'button', data_dismiss: 'modal'
+            _[*@header]
+          end
+
+          _div.modal_body do
+            _[*@body]
+          end
+
+          _div.modal_footer class: @@color do
+            _[*@footer]
+          end
+        end
+      end
+    end
+  end
+end
diff --git a/views/footer.js.rb b/views/footer.js.rb
deleted file mode 100644
index 67c916f..0000000
--- a/views/footer.js.rb
+++ /dev/null
@@ -1,23 +0,0 @@
-class Footer < React
-  def render
-    _footer.navbar.navbar_fixed_bottom class: @@item.color do
-      if @@item.prev
-        _a.backlink.navbar_brand @@item.prev.title, rel: 'prev', 
-         href: @@item.prev.href
-      end
-
-      if @@buttons
-        _span do
-          @@buttons.each do |button|
-            React.createElement('button', button.attrs, button.text) if button
-          end
-        end
-      end
-
-      if @@item.next
-        _Link.nextlink.navbar_brand text: @@item.next.title, rel: 'next', 
-         href: @@item.next.href
-      end
-    end
-  end
-end
diff --git a/views/forms/add-comment.js.rb b/views/forms/add-comment.js.rb
new file mode 100644
index 0000000..3a0e5ff
--- /dev/null
+++ b/views/forms/add-comment.js.rb
@@ -0,0 +1,66 @@
+class AddComment < React
+  def initialize
+    @save_disabled = true
+  end
+
+  def self.button
+    {
+      text: 'add comment',
+      class: 'btn_primary',
+      data_toggle: 'modal',
+      data_target: '#comment-form'
+    }
+  end
+
+  def render
+    _ModalDialog.comment_form! color: 'commented' do
+      # header
+      _h4 'Enter a comment'
+
+      #input field: initials
+      _div.form_group do
+        _label 'Initials', for: 'comment-initials'
+        _input.comment_initials!.form_control label: 'Initials',
+          placeholder: 'initials', 
+          defaultValue: @@server.pending.initials || @@server.initials
+      end
+
+      #input field: comment text
+      _div.form_group do
+        _label 'Comment', for: 'comment-text'
+        _textarea.comment_text!.form_control label: 'Comment',
+          placeholder: 'comment', rows: 5, onInput: self.input
+      end
+
+      # footer buttons
+      _button.btn_default 'Cancel', data_dismiss: 'modal'
+      _button.btn_primary 'Save', disabled: @save_disabled, onClick: self.save
+    end
+  end
+
+  # autofocus on comment form
+  def componentDidMount()
+    jQuery('#comment-form').on 'shown.bs.modal' do
+      ~'#comment-text'.focus()
+    end
+  end
+
+  # enable/disable save when input changes
+  def input(event)
+    @save_disabled = ( event.target.value.length == 0 )
+  end
+
+  def save(event)
+    data = {
+      agenda: Agenda.file,
+      attach: @@item.attach,
+      initials: ~'#comment_initials'.value,
+      comment: ~'#comment_text'.value
+    }
+
+    post 'comment', data do |pending|
+      Pending.load pending
+      jQuery('#comment-form').modal(:hide)
+    end
+  end
+end
diff --git a/views/header.js.rb b/views/header.js.rb
deleted file mode 100644
index b080c66..0000000
--- a/views/header.js.rb
+++ /dev/null
@@ -1,98 +0,0 @@
-#
-# Header: title on the left, dropdowns on the right
-#
-# Also keeps the window/tab title in sync with the header title
-
-class Header < React
-  def render
-    _header.navbar.navbar_fixed_top class: @@item.color do
-      _div.navbar_brand @@item.title
-
-      _ul.nav.nav_pills.navbar_right do
-
-        # 'info' dropdown
-        #
-        if @@item.attach
-          _li.dropdown do
-            _a.dropdown_toggle.nav! data_toggle: "dropdown" do
-              _ 'info'
-              _b.caret
-            end
-
-            _dl.dropdown_menu.dl_horizontal do
-              _dt 'Attach'
-              _dd @@item.attach
-
-              if @@item.owner
-                _dt 'Author'
-                _dd @@item.owner
-              end
-
-              if @@item.shepherd
-                _dt 'Shepherd'
-                _dd @@item.shepherd
-              end
-
-              if @@item.approved and not @@item.approved.empty?
-                _dt 'Approved'
-                _dd @@item.approved.join(', ')
-              end
-
-              if @@item.roster or @@item.prior_reports or @@item.stats
-                _dt 'Links'
-
-                if @@item.roster
-                  _dd { _a 'Roster', href: @@item.roster }
-                end
-
-                if @@item.prior_reports
-                  _dd { _a 'Prior Reports', href: @@item.prior_reports }
-                end
-
-                if @@item.stats
-                  _dd { _a 'Statistics', href: @@item.stats }
-                end
-              end
-            end
-          end
-        end
-
-        # 'navigation' dropdown
-        #
-        _li.dropdown do
-          _a.dropdown_toggle.nav! data_toggle: "dropdown" do
-            _ 'navigation'
-            _b.caret
-          end
-
-          _ul.dropdown_menu do
-            _li { _Link text: 'Agenda', href: '.' }
-
-            Agenda.index.each do |item|
-              _li { _Link text: item.index, href: item.href } if item.index
-            end
-
-            _li.divider
-
-            _li { _Link text: 'Search', href: 'search' }
-            _li { _Link text: 'Comments', href: 'comments' }
-          end
-        end
-
-      end
-    end
-  end
-
-  # set title on initial rendering
-  def componentDidMount()
-    self.componentDidUpdate()
-  end
-
-  # update title to match the item title whenever page changes
-  def componentDidUpdate()
-    title = ~'title'
-    if title.textContent != @@item.title
-      title.textContent = @@item.title
-    end
-  end
-end
diff --git a/views/index.js.rb b/views/index.js.rb
deleted file mode 100644
index 0c65f7e..0000000
--- a/views/index.js.rb
+++ /dev/null
@@ -1,29 +0,0 @@
-#
-# Overall Agenda page: simple table with one row for each item in the index
-#
-
-class Index < React
-  def render
-    _header do
-      _h1 'ASF Board Agenda'
-    end
-
-    _table.table_bordered do
-      _thead do
-             _th 'Attach'
-             _th 'Title'
-             _th 'Owner'
-             _th 'Shepherd'
-      end
-
-      _tbody Agenda.index do |row|
-             _tr class: row.color do
-               _td row.attach
-               _td { _Link text: row.title, href: row.href }
-               _td row.owner
-               _td row.shepherd
-             end
-      end
-    end
-  end
-end
diff --git a/views/layout/footer.js.rb b/views/layout/footer.js.rb
new file mode 100644
index 0000000..67c916f
--- /dev/null
+++ b/views/layout/footer.js.rb
@@ -0,0 +1,23 @@
+class Footer < React
+  def render
+    _footer.navbar.navbar_fixed_bottom class: @@item.color do
+      if @@item.prev
+        _a.backlink.navbar_brand @@item.prev.title, rel: 'prev', 
+         href: @@item.prev.href
+      end
+
+      if @@buttons
+        _span do
+          @@buttons.each do |button|
+            React.createElement('button', button.attrs, button.text) if button
+          end
+        end
+      end
+
+      if @@item.next
+        _Link.nextlink.navbar_brand text: @@item.next.title, rel: 'next', 
+         href: @@item.next.href
+      end
+    end
+  end
+end
diff --git a/views/layout/header.js.rb b/views/layout/header.js.rb
new file mode 100644
index 0000000..b080c66
--- /dev/null
+++ b/views/layout/header.js.rb
@@ -0,0 +1,98 @@
+#
+# Header: title on the left, dropdowns on the right
+#
+# Also keeps the window/tab title in sync with the header title
+
+class Header < React
+  def render
+    _header.navbar.navbar_fixed_top class: @@item.color do
+      _div.navbar_brand @@item.title
+
+      _ul.nav.nav_pills.navbar_right do
+
+        # 'info' dropdown
+        #
+        if @@item.attach
+          _li.dropdown do
+            _a.dropdown_toggle.nav! data_toggle: "dropdown" do
+              _ 'info'
+              _b.caret
+            end
+
+            _dl.dropdown_menu.dl_horizontal do
+              _dt 'Attach'
+              _dd @@item.attach
+
+              if @@item.owner
+                _dt 'Author'
+                _dd @@item.owner
+              end
+
+              if @@item.shepherd
+                _dt 'Shepherd'
+                _dd @@item.shepherd
+              end
+
+              if @@item.approved and not @@item.approved.empty?
+                _dt 'Approved'
+                _dd @@item.approved.join(', ')
+              end
+
+              if @@item.roster or @@item.prior_reports or @@item.stats
+                _dt 'Links'
+
+                if @@item.roster
+                  _dd { _a 'Roster', href: @@item.roster }
+                end
+
+                if @@item.prior_reports
+                  _dd { _a 'Prior Reports', href: @@item.prior_reports }
+                end
+
+                if @@item.stats
+                  _dd { _a 'Statistics', href: @@item.stats }
+                end
+              end
+            end
+          end
+        end
+
+        # 'navigation' dropdown
+        #
+        _li.dropdown do
+          _a.dropdown_toggle.nav! data_toggle: "dropdown" do
+            _ 'navigation'
+            _b.caret
+          end
+
+          _ul.dropdown_menu do
+            _li { _Link text: 'Agenda', href: '.' }
+
+            Agenda.index.each do |item|
+              _li { _Link text: item.index, href: item.href } if item.index
+            end
+
+            _li.divider
+
+            _li { _Link text: 'Search', href: 'search' }
+            _li { _Link text: 'Comments', href: 'comments' }
+          end
+        end
+
+      end
+    end
+  end
+
+  # set title on initial rendering
+  def componentDidMount()
+    self.componentDidUpdate()
+  end
+
+  # update title to match the item title whenever page changes
+  def componentDidUpdate()
+    title = ~'title'
+    if title.textContent != @@item.title
+      title.textContent = @@item.title
+    end
+  end
+end
diff --git a/views/layout/main.js.rb b/views/layout/main.js.rb
new file mode 100644
index 0000000..b28f489
--- /dev/null
+++ b/views/layout/main.js.rb
@@ -0,0 +1,175 @@
+#
+# Main component, responsible for:
+#
+#  * Initial loading and polling of the agenda
+#
+#  * Routing based on path and query information in the URL
+#
+#  * Rendering a Header, a item view, and a Footer
+#
+#  * Resizing view to leave room for the Header and Footer
+#
+
+class Main < React
+
+  # initialize polling state
+  def initialize
+    @poll = {
+      link: "../#{@@page.date}.json",
+      etag: @@page.etag,
+      interval: 10_000
+    }
+  end
+
+  # route request based on path and query from the window location (URL)
+  def route(path, query)
+    if path == 'search'
+      item = {view: Search, query: query}
+    elsif path == 'comments'
+      item = {view: Comments}
+    elsif path and path != '.'
+      item = Agenda.find(path)
+    else
+      item = Agenda
+    end
+
+    # provide defaults for required properties
+    item.color ||= 'blank'
+    item.title ||= item.view.displayName
+
+    # determine what buttons are required
+    if item.forms
+      @buttons = item.forms.map do |form|
+        if form and form.button
+          props = {text: 'button', attrs: {className: 'btn'}}
+          for name in form.button
+            if name == 'text'
+              props.text = form.button.text
+            elsif name == 'class' or name == 'classname'
+              props.attrs.className = "btn #{form.button[name].gsub('_', '-')}"
+            else
+              props.attrs[name.gsub('_', '-')] = form.button[name]
+            end
+          end
+          return props
+        else
+          return nil
+        end
+      end
+    else
+      @buttons = []
+    end
+
+    @item = Main.item = item
+  end
+
+  # common layout for all pages: header, main, footer, and forms
+  def render
+    _Header item: @item
+
+    _main do
+      React.createElement(@item.view, data: @item)
+    end
+
+    _Footer item: @item, buttons: @buttons
+
+    if @item.forms
+      @item.forms.each do |form|
+        React.createElement(form, item: @item, server: @@server) if form
+      end
+    end
+  end
+
+  # initial load of the agenda, and route first request
+  def componentWillMount()
+    Agenda.load(@@page.parsed)
+    Agenda.date = @@page.date
+    self.route(@@page.path, @@page.query)
+
+    # copy server info for later use
+    for prop in @@server
+      Server[prop] = @@server[prop]
+    end
+
+    # free memory
+    @@page.parsed = nil
+  end
+
+  # navigation method that updates history (back button) information
+  def navigate(path, query)
+    self.route(path, query)
+    history.pushState({path: path, query: query}, nil, path)
+  end
+
+  # refresh the current page
+  def refresh()
+    self.route(history.state.path, history.state.query)
+  end
+
+  # additional client side initialization
+  def componentDidMount()
+    # export navigate and refresh methods
+    Main.navigate = self.navigate
+    Main.refresh  = self.refresh
+
+    # store initial state in history, taking care not to overwrite
+    # history set by the Search component.
+    if not history.state or not history.state.query
+      history.replaceState({path: @@page.path}, nil, @@page.path)
+    end
+
+    # listen for back button, and re-route/re-render when it occcurs
+    window.addEventListener :popstate do |event|
+      if event.state and defined? event.state.path
+        self.route(event.state.path, event.state.query)
+      end
+    end
+
+    # keyboard navigation (unless on the search screen)
+    def (document.body).onkeyup(event)
+      return if ~'#search-text' or ~'.modal-open'
+
+      if event.keyCode == 37
+        self.navigate ~"a[rel=prev]".getAttribute('href')
+      elsif event.keyCode == 39
+        self.navigate ~"a[rel=next]".getAttribute('href')
+      end
+    end
+
+    # whenever the window is resized, adjust margins of the main area to
+    # avoid overlapping the header and footer areas
+    def window.onresize()
+      main = ~'main'
+      main.style.marginTop = "#{~'header.navbar'.clientHeight}px"
+      main.style.marginBottom = "#{~'footer.navbar'.clientHeight}px"
+    end
+
+    # do an initial resize
+    window.onresize()
+
+    # if agenda is stale, fetch immediately; start polling agenda
+    self.fetchAgenda() unless @poll.etag
+    setInterval self.fetchAgenda, @poll.interval
+  end
+
+  # after each subsequent re-rendering, resize main window
+  def componentDidUpdate()
+    window.onresize()
+  end
+
+  # fetch agenda
+  def fetchAgenda()
+    xhr = XMLHttpRequest.new()
+    xhr.open('GET', @poll.link, true)
+    xhr.setRequestHeader('If-None-Match', @poll.etag) if @poll.etag
+    xhr.responseType = 'text'
+    def xhr.onreadystatechange()
+      if xhr.readyState == 4 and xhr.status == 200 and xhr.responseText != ''
+        @poll.etag = xhr.getResponseHeader('ETag')
+        Agenda.load(JSON.parse(xhr.responseText))
+        self.route(history.state.path, history.state.query)
+      end
+    end
+    xhr.send()
+  end
+end
diff --git a/views/link.js.rb b/views/link.js.rb
deleted file mode 100644
index 6da510c..0000000
--- a/views/link.js.rb
+++ /dev/null
@@ -1,34 +0,0 @@
-#
-# Replacement for 'a' element which handles clicks events by calling
-# Main.navigate.
-#
-
-class Link < React
-  def initialize
-    @attrs = {}
-  end
-
-  def componentWillMount()
-    self.componentWillReceiveProps(self.props)
-    @attrs.onClick = self.click
-  end
-
-  def componentWillReceiveProps(props)
-    @text = props.text
-    for attr in props
-      @attrs[attr] = props[attr] unless attr == 'text'
-    end
-  end
-
-  def render
-    React.createElement('a', @attrs, @text)
-  end
-
-  def click(event)
-    href = event.target.getAttribute('href')
-    if href =~ /^(\.|[-\w]+)$/
-      Main.navigate href
-      return false
-    end
-  end
-end
diff --git a/views/main.html.rb b/views/main.html.rb
old mode 100755
new mode 100644
diff --git a/views/main.js.rb b/views/main.js.rb
deleted file mode 100644
index b28f489..0000000
--- a/views/main.js.rb
+++ /dev/null
@@ -1,175 +0,0 @@
-#
-# Main component, responsible for:
-#
-#  * Initial loading and polling of the agenda
-#
-#  * Routing based on path and query information in the URL
-#
-#  * Rendering a Header, a item view, and a Footer
-#
-#  * Resizing view to leave room for the Header and Footer
-#
-
-class Main < React
-
-  # initialize polling state
-  def initialize
-    @poll = {
-      link: "../#{@@page.date}.json",
-      etag: @@page.etag,
-      interval: 10_000
-    }
-  end
-
-  # route request based on path and query from the window location (URL)
-  def route(path, query)
-    if path == 'search'
-      item = {view: Search, query: query}
-    elsif path == 'comments'
-      item = {view: Comments}
-    elsif path and path != '.'
-      item = Agenda.find(path)
-    else
-      item = Agenda
-    end
-
-    # provide defaults for required properties
-    item.color ||= 'blank'
-    item.title ||= item.view.displayName
-
-    # determine what buttons are required
-    if item.forms
-      @buttons = item.forms.map do |form|
-        if form and form.button
-          props = {text: 'button', attrs: {className: 'btn'}}
-          for name in form.button
-            if name == 'text'
-              props.text = form.button.text
-            elsif name == 'class' or name == 'classname'
-              props.attrs.className = "btn #{form.button[name].gsub('_', '-')}"
-            else
-              props.attrs[name.gsub('_', '-')] = form.button[name]
-            end
-          end
-          return props
-        else
-          return nil
-        end
-      end
-    else
-      @buttons = []
-    end
-
-    @item = Main.item = item
-  end
-
-  # common layout for all pages: header, main, footer, and forms
-  def render
-    _Header item: @item
-
-    _main do
-      React.createElement(@item.view, data: @item)
-    end
-
-    _Footer item: @item, buttons: @buttons
-
-    if @item.forms
-      @item.forms.each do |form|
-        React.createElement(form, item: @item, server: @@server) if form
-      end
-    end
-  end
-
-  # initial load of the agenda, and route first request
-  def componentWillMount()
-    Agenda.load(@@page.parsed)
-    Agenda.date = @@page.date
-    self.route(@@page.path, @@page.query)
-
-    # copy server info for later use
-    for prop in @@server
-      Server[prop] = @@server[prop]
-    end
-
-    # free memory
-    @@page.parsed = nil
-  end
-
-  # navigation method that updates history (back button) information
-  def navigate(path, query)
-    self.route(path, query)
-    history.pushState({path: path, query: query}, nil, path)
-  end
-
-  # refresh the current page
-  def refresh()
-    self.route(history.state.path, history.state.query)
-  end
-
-  # additional client side initialization
-  def componentDidMount()
-    # export navigate and refresh methods
-    Main.navigate = self.navigate
-    Main.refresh  = self.refresh
-
-    # store initial state in history, taking care not to overwrite
-    # history set by the Search component.
-    if not history.state or not history.state.query
-      history.replaceState({path: @@page.path}, nil, @@page.path)
-    end
-
-    # listen for back button, and re-route/re-render when it occcurs
-    window.addEventListener :popstate do |event|
-      if event.state and defined? event.state.path
-        self.route(event.state.path, event.state.query)
-      end
-    end
-
-    # keyboard navigation (unless on the search screen)
-    def (document.body).onkeyup(event)
-      return if ~'#search-text' or ~'.modal-open'
-
-      if event.keyCode == 37
-        self.navigate ~"a[rel=prev]".getAttribute('href')
-      elsif event.keyCode == 39
-        self.navigate ~"a[rel=next]".getAttribute('href')
-      end
-    end
-
-    # whenever the window is resized, adjust margins of the main area to
-    # avoid overlapping the header and footer areas
-    def window.onresize()
-      main = ~'main'
-      main.style.marginTop = "#{~'header.navbar'.clientHeight}px"
-      main.style.marginBottom = "#{~'footer.navbar'.clientHeight}px"
-    end
-
-    # do an initial resize
-    window.onresize()
-
-    # if agenda is stale, fetch immediately; start polling agenda
-    self.fetchAgenda() unless @poll.etag
-    setInterval self.fetchAgenda, @poll.interval
-  end
-
-  # after each subsequent re-rendering, resize main window
-  def componentDidUpdate()
-    window.onresize()
-  end
-
-  # fetch agenda
-  def fetchAgenda()
-    xhr = XMLHttpRequest.new()
-    xhr.open('GET', @poll.link, true)
-    xhr.setRequestHeader('If-None-Match', @poll.etag) if @poll.etag
-    xhr.responseType = 'text'
-    def xhr.onreadystatechange()
-      if xhr.readyState == 4 and xhr.status == 200 and xhr.responseText != ''
-        @poll.etag = xhr.getResponseHeader('ETag')
-        Agenda.load(JSON.parse(xhr.responseText))
-        self.route(history.state.path, history.state.query)
-      end
-    end
-    xhr.send()
-  end
-end
diff --git a/views/modal-dialog.js.rb b/views/modal-dialog.js.rb
deleted file mode 100644
index b9c3a61..0000000
--- a/views/modal-dialog.js.rb
+++ /dev/null
@@ -1,63 +0,0 @@
-class ModalDialog < React
-  def initialize
-    @header = []
-    @body = []
-    @footer = []
-  end
-
-  def componentWillMount()
-    self.componentWillReceiveProps(self.props)
-  end
-
-  def componentWillReceiveProps(props)
-    @header.clear()
-    @body.clear()
-    @footer.clear()
-
-    props.children.each do |child|
-      if child.type == 'h4'
-        if not child.props.className
-          child.props.className = 'modal-title'
-        elsif not child.props.className.split(' ').include? 'modal-title'
-          child.props.className += ' modal-title'
-        end
-
-        @header << child
-        ModalDialog.h4 = child
-
-      elsif child.type == 'button'
-        if not child.props.className
-          child.props.className = 'btn'
-        elsif not child.props.className.split(' ').include? 'btn'
-          child.props.className += ' btn'
-        end
-
-        @footer << child
-
-      else
-        @body << child
-      end
-    end
-  end
-
-  def render
-    _div.modal.fade id: @@id, class: @@className do
-      _div.modal_dialog do
-        _div.modal_content do
-          _div.modal_header class: @@color do
-            _button.close "\u00d7", type: 'button', data_dismiss: 'modal'
-            _[*@header]
-          end
-
-          _div.modal_body do
-            _[*@body]
-          end
-
-          _div.modal_footer class: @@color do
-            _[*@footer]
-          end
-        end
-      end
-    end
-  end
-end
diff --git a/views/pages/comments.js.rb b/views/pages/comments.js.rb
new file mode 100644
index 0000000..d0e8b41
--- /dev/null
+++ b/views/pages/comments.js.rb
@@ -0,0 +1,20 @@
+class Comments < React
+  def render
+    found = false
+
+    Agenda.index.each do |item|
+      next if item.comments.empty?
+      found = true
+
+      _section do
+       _h4 {_Link text: item.title, href: item.href}
+
+        item.comments.each do |comment|
+          _pre.comment comment
+        end
+      end
+    end
+
+    _p {_em 'No comments found'} unless found
+  end
+end
diff --git a/views/pages/index.js.rb b/views/pages/index.js.rb
new file mode 100644
index 0000000..0c65f7e
--- /dev/null
+++ b/views/pages/index.js.rb
@@ -0,0 +1,29 @@
+#
+# Overall Agenda page: simple table with one row for each item in the index
+#
+
+class Index < React
+  def render
+    _header do
+      _h1 'ASF Board Agenda'
+    end
+
+    _table.table_bordered do
+      _thead do
+             _th 'Attach'
+             _th 'Title'
+             _th 'Owner'
+             _th 'Shepherd'
+      end
+
+      _tbody Agenda.index do |row|
+             _tr class: row.color do
+               _td row.attach
+               _td { _Link text: row.title, href: row.href }
+               _td row.owner
+               _td row.shepherd
+             end
+      end
+    end
+  end
+end
diff --git a/views/pages/report.js.rb b/views/pages/report.js.rb
new file mode 100644
index 0000000..74641d0
--- /dev/null
+++ b/views/pages/report.js.rb
@@ -0,0 +1,21 @@
+class Report < React
+  def render
+    _section.flexbox do
+      _section do
+        _pre.report do
+          _p {_em 'Missing'} if @@data.missing
+          _ @@data.text
+        end
+      end
+
+      unless @@data.comments.empty?
+        _section do
+          _h3.comments! 'Comments'
+          @@data.comments.each do |comment|
+            _pre.comment comment
+          end
+        end
+      end
+    end
+  end
+end
diff --git a/views/pages/search.js.rb b/views/pages/search.js.rb
new file mode 100644
index 0000000..8bebdc7
--- /dev/null
+++ b/views/pages/search.js.rb
@@ -0,0 +1,75 @@
+#
+# Search component: 
+#  * prompt for search 
+#  * display matching paragraphs from agenda, highlighting search strings
+#  * keep query string in window location URL in synch
+#
+
+class Search < React
+  # initialize query text based on data passed to the component
+  def initialize
+    @text = @@data.query || ''
+  end
+
+  def render
+    # search input field
+    _div.search do
+      _label 'Search:', for: 'search_text'
+      _input.search_text! autofocus: 'autofocus', value: @text, 
+        onInput: self.input
+    end
+
+    if @text.length > 2
+      matches = false
+      text = @text.downcase()
+
+      Agenda.index.each do |item|
+        next unless item.text and item.text.downcase().include? text
+        matches = true
+
+        _section do
+          _h4 {_Link text: item.title, href: item.href}
+
+          # highlight matching strings in paragraph
+          item.text.split(/\n\s*\n/).each do |paragraph|
+            if paragraph.downcase().include? text
+              _pre.report dangerouslySetInnerHTML: {
+                __html: htmlEscape(paragraph).gsub(/(#{text})/i,
+                 "<span class='hilite'>$1</span>")
+              }
+            end
+          end
+        end
+      end
+
+      # if no sections were output, indicate 'no matches'
+      _p {_em 'No matches'} unless matches
+    else
+
+      # start producing query results when input string has three characters
+      _p 'Please enter at least three characters'
+
+    end
+  end
+
+  # update text whenever input changes
+  def input(event)
+    @text = event.target.value
+  end
+
+  # set history on initial rendering
+  def componentDidMount()
+    self.componentDidUpdate()
+  end
+
+  # replace history state on subsequent renderings
+  def componentDidUpdate()
+    state = {path: 'search', query: @text}
+
+    if state.query
+      history.replaceState(state, nil, "search?q=#{encodeURIComponent(@text)}")
+    else
+      history.replaceState(state, nil, 'search')
+    end
+  end
+end
diff --git a/views/report.js.rb b/views/report.js.rb
deleted file mode 100644
index 74641d0..0000000
--- a/views/report.js.rb
+++ /dev/null
@@ -1,21 +0,0 @@
-class Report < React
-  def render
-    _section.flexbox do
-      _section do
-        _pre.report do
-          _p {_em 'Missing'} if @@data.missing
-          _ @@data.text
-        end
-      end
-
-      unless @@data.comments.empty?
-        _section do
-          _h3.comments! 'Comments'
-          @@data.comments.each do |comment|
-            _pre.comment comment
-          end
-        end
-      end
-    end
-  end
-end
diff --git a/views/search.js.rb b/views/search.js.rb
deleted file mode 100644
index 8bebdc7..0000000
--- a/views/search.js.rb
+++ /dev/null
@@ -1,75 +0,0 @@
-#
-# Search component: 
-#  * prompt for search 
-#  * display matching paragraphs from agenda, highlighting search strings
-#  * keep query string in window location URL in synch
-#
-
-class Search < React
-  # initialize query text based on data passed to the component
-  def initialize
-    @text = @@data.query || ''
-  end
-
-  def render
-    # search input field
-    _div.search do
-      _label 'Search:', for: 'search_text'
-      _input.search_text! autofocus: 'autofocus', value: @text, 
-        onInput: self.input
-    end
-
-    if @text.length > 2
-      matches = false
-      text = @text.downcase()
-
-      Agenda.index.each do |item|
-        next unless item.text and item.text.downcase().include? text
-        matches = true
-
-        _section do
-          _h4 {_Link text: item.title, href: item.href}
-
-          # highlight matching strings in paragraph
-          item.text.split(/\n\s*\n/).each do |paragraph|
-            if paragraph.downcase().include? text
-              _pre.report dangerouslySetInnerHTML: {
-                __html: htmlEscape(paragraph).gsub(/(#{text})/i,
-                 "<span class='hilite'>$1</span>")
-              }
-            end
-          end
-        end
-      end
-
-      # if no sections were output, indicate 'no matches'
-      _p {_em 'No matches'} unless matches
-    else
-
-      # start producing query results when input string has three characters
-      _p 'Please enter at least three characters'
-
-    end
-  end
-
-  # update text whenever input changes
-  def input(event)
-    @text = event.target.value
-  end
-
-  # set history on initial rendering
-  def componentDidMount()
-    self.componentDidUpdate()
-  end
-
-  # replace history state on subsequent renderings
-  def componentDidUpdate()
-    state = {path: 'search', query: @text}
-
-    if state.query
-      history.replaceState(state, nil, "search?q=#{encodeURIComponent(@text)}")
-    else
-      history.replaceState(state, nil, 'search')
-    end
-  end
-end

Reply via email to