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