From 8fa730ccc17ac8d6fe80c4cb35b4f44f24ce9215 Mon Sep 17 00:00:00 2001 From: Jamie Gaskins Date: Fri, 17 Jun 2016 12:06:41 -0400 Subject: [PATCH] Initial UI prototype --- Gemfile | 19 +-- Gemfile.lock | 81 +++++++---- app/assets/javascripts/actions.rb | 8 ++ app/assets/javascripts/application.js | 16 --- app/assets/javascripts/application.rb | 181 +++++++++++++++++++++++++ app/assets/javascripts/store.rb | 181 +++++++++++++++++++++++++ app/views/landing/index.html.erb | 5 - app/views/layouts/application.html.erb | 1 + config/routes.rb | 1 + 9 files changed, 428 insertions(+), 65 deletions(-) create mode 100644 app/assets/javascripts/actions.rb delete mode 100644 app/assets/javascripts/application.js create mode 100644 app/assets/javascripts/application.rb create mode 100644 app/assets/javascripts/store.rb diff --git a/Gemfile b/Gemfile index 642e4c4..98ec2b8 100644 --- a/Gemfile +++ b/Gemfile @@ -9,19 +9,11 @@ gem 'pg', '~> 0.15' gem 'sass-rails', '~> 5.0' # Use Uglifier as compressor for JavaScript assets gem 'uglifier', '>= 1.3.0' -# Use CoffeeScript for .coffee assets and views -gem 'coffee-rails', '~> 4.1.0' -# See https://github.com/rails/execjs#readme for more supported runtimes -# gem 'therubyracer', platforms: :ruby -# Use jquery as the JavaScript library -gem 'jquery-rails' -# Turbolinks makes following links in your web application faster. Read more: https://github.com/rails/turbolinks -gem 'turbolinks' -# Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder -gem 'jbuilder', '~> 2.0' -# bundle exec rake doc:rails generates the API under doc/api. -gem 'sdoc', '~> 0.4.0', group: :doc +gem 'clearwater', '~> 1.0.0.rc1' +gem 'opal', github: 'opal/opal' +gem 'opal-rails', github: 'opal/opal-rails' +gem 'grand_central', github: 'clearwater-rb/grand_central' # Use ActiveModel has_secure_password # gem 'bcrypt', '~> 3.1.7' @@ -37,9 +29,6 @@ end group :development do # Access an IRB console on exception pages or by using <%= console %> in views gem 'web-console', '~> 2.0' - - # Spring speeds up development by keeping your application running in the background. Read more: https://github.com/rails/spring - gem 'spring' end gem 'rails_12factor', group: :production diff --git a/Gemfile.lock b/Gemfile.lock index 5fcff28..29b53cb 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,3 +1,33 @@ +GIT + remote: git://github.com/clearwater-rb/grand_central.git + revision: 773bcf55dd43eb65dd3ab397164e8533693f0fa3 + specs: + grand_central (0.3.0) + +GIT + remote: git://github.com/opal/opal-rails.git + revision: 5f1f1f681bcbfecd01f601bedc76e4ecf31898ed + specs: + opal-rails (0.9.0) + jquery-rails + opal (>= 0.8.0, < 0.11) + opal-activesupport (>= 0.0.5) + opal-jquery (~> 0.4.0) + rails (>= 4.0, < 6.0) + sprockets-rails (< 3.0) + +GIT + remote: git://github.com/opal/opal.git + revision: 5bac8d786cd233e9982aaed713a0862d21403f69 + specs: + opal (0.10.0.rc1) + ast (>= 2.3.0) + hike (~> 1.2) + parser (>= 2.3.1.2) + sourcemap (~> 0.1.0) + sprockets (~> 3.1) + tilt (>= 1.4) + GEM remote: https://rubygems.org/ specs: @@ -37,17 +67,16 @@ GEM thread_safe (~> 0.3, >= 0.3.4) tzinfo (~> 1.1) arel (6.0.3) + ast (2.3.0) binding_of_caller (0.7.2) debug_inspector (>= 0.0.1) + bowser (0.1.6) + opal (>= 0.8.0, < 0.11.0) builder (3.2.2) byebug (9.0.5) - coffee-rails (4.1.1) - coffee-script (>= 2.2.0) - railties (>= 4.0.0, < 5.1.x) - coffee-script (2.4.1) - coffee-script-source - execjs - coffee-script-source (1.10.0) + clearwater (1.0.0.rc1) + bowser (~> 0.1.5) + opal (~> 0.7) concurrent-ruby (1.0.2) debug_inspector (0.0.2) diff-lcs (1.2.5) @@ -55,10 +84,8 @@ GEM execjs (2.7.0) globalid (0.3.6) activesupport (>= 4.1.0) + hike (1.2.3) i18n (0.7.0) - jbuilder (2.5.0) - activesupport (>= 3.0.0, < 5.1) - multi_json (~> 1.2) jquery-rails (4.1.1) rails-dom-testing (>= 1, < 3) railties (>= 4.2.0) @@ -73,10 +100,15 @@ GEM mime-types-data (3.2016.0521) mini_portile2 (2.1.0) minitest (5.9.0) - multi_json (1.12.1) nokogiri (1.6.8) mini_portile2 (~> 2.1.0) pkg-config (~> 1.1.7) + opal-activesupport (0.3.0) + opal (>= 0.5.0, < 1.0.0) + opal-jquery (0.4.1) + opal (>= 0.7.0, < 0.10.0) + parser (2.3.1.2) + ast (~> 2.2) pg (0.18.4) pkg-config (1.1.7) puma (3.4.0) @@ -113,8 +145,6 @@ GEM rake (>= 0.8.7) thor (>= 0.18.1, < 2.0) rake (11.2.2) - rdoc (4.2.2) - json (~> 1.4) rspec-core (3.4.4) rspec-support (~> 3.4.0) rspec-expectations (3.4.0) @@ -139,22 +169,17 @@ GEM sprockets (>= 2.8, < 4.0) sprockets-rails (>= 2.0, < 4.0) tilt (>= 1.1, < 3) - sdoc (0.4.1) - json (~> 1.7, >= 1.7.7) - rdoc (~> 4.0) - spring (1.7.1) + sourcemap (0.1.1) sprockets (3.6.0) concurrent-ruby (~> 1.0) rack (> 1, < 3) - sprockets-rails (3.0.4) - actionpack (>= 4.0) - activesupport (>= 4.0) - sprockets (>= 3.0.0) + sprockets-rails (2.3.3) + actionpack (>= 3.0) + activesupport (>= 3.0) + sprockets (>= 2.8, < 4.0) thor (0.19.1) thread_safe (0.3.5) tilt (2.0.5) - turbolinks (2.5.3) - coffee-rails tzinfo (1.2.2) thread_safe (~> 0.1) uglifier (3.0.0) @@ -170,18 +195,16 @@ PLATFORMS DEPENDENCIES byebug - coffee-rails (~> 4.1.0) - jbuilder (~> 2.0) - jquery-rails + clearwater (~> 1.0.0.rc1) + grand_central! + opal! + opal-rails! pg (~> 0.15) puma (~> 3.4) rails (= 4.2.6) rails_12factor rspec-rails (~> 3.4) sass-rails (~> 5.0) - sdoc (~> 0.4.0) - spring - turbolinks uglifier (>= 1.3.0) web-console (~> 2.0) diff --git a/app/assets/javascripts/actions.rb b/app/assets/javascripts/actions.rb new file mode 100644 index 0000000..cd2eda0 --- /dev/null +++ b/app/assets/javascripts/actions.rb @@ -0,0 +1,8 @@ +require 'grand_central/action' + +ObserveBehavior = GrandCentral::Action.with_attributes(:behavior, :actor) +SetCurrentActor = GrandCentral::Action.with_attributes(:actor_id) +SetCurrentTarget = GrandCentral::Action.with_attributes(:target) +AddModifier = GrandCentral::Action.with_attributes(:observation, :modifier) +CompleteObservation = GrandCentral::Action.create + diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js deleted file mode 100644 index e07c5a8..0000000 --- a/app/assets/javascripts/application.js +++ /dev/null @@ -1,16 +0,0 @@ -// This is a manifest file that'll be compiled into application.js, which will include all the files -// listed below. -// -// Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts, -// or any plugin's vendor/assets/javascripts directory can be referenced here using a relative path. -// -// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the -// compiled file. -// -// Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details -// about supported directives. -// -//= require jquery -//= require jquery_ujs -//= require turbolinks -//= require_tree . diff --git a/app/assets/javascripts/application.rb b/app/assets/javascripts/application.rb new file mode 100644 index 0000000..ff55bf5 --- /dev/null +++ b/app/assets/javascripts/application.rb @@ -0,0 +1,181 @@ +require 'opal' +require 'clearwater' + +require 'store' + +class Layout + include Clearwater::Component + + def render + div([ + h1(NavLink.new({ href: '/' }, 'Red Pandas')), + header([ + nav([ + NavLink.new({ href: '/focus/animal' }, 'Focus Animal'), + NavLink.new({ href: '/focus/scan' }, 'Scan'), + NavLink.new({ href: '/focus/behavior' }, 'Focus Behavior'), + ]) + ]), + + outlet || Data.new, + ]) + end +end + +class NavLink + include Clearwater::Component + + def initialize(properties, label) + @properties = properties + @label = label + end + + def render + Link.new(defaults.merge(@properties), @label) + end + + def defaults + { + style: { + color: :blue, + display: 'inline-block', + padding: '5px 8px', + }, + } + end +end + +class Data + include Clearwater::Component +end + +class NewObservation + include Clearwater::Component + + def render + div([ + + ]) + end + + def handle_submit(event) + event.prevent + + Store.dispatch UpdateObservation.new + end +end + +class Focus + include Clearwater::Component + + def render + div([ + h2('Focus'), + outlet, + ]) + end +end + +class FocusAnimal + include Clearwater::Component + + def render + div([ + h3('Focus animal'), + select({ + onchange: method(:set_actor) + }, [option({ value: nil }, 'Select an actor')] + animals.map { |animal| + option({ value: animal.id, selected: current_actor_id == animal.id }, animal.name) + }), + content, + ]) + end + + def content + return unless current_actor + + if observation.behavior.nil? + div([ + header([ + h2("Select behavior"), + ]), + Store.state.behaviors.map { |behavior| + button({ onclick: proc { set_behavior behavior } }, behavior.name) + } + ]) + elsif observation.behavior.targetable? && !observation.target + div([ + h2('Select target'), + animals.map { |animal| + button({ onclick: proc { set_target animal } }, animal.name) + } + ]) + else + div([ + available_modifiers.map { |modifier| + button({ onclick: proc { add_modifier modifier } }, modifier) + }, + div(button({ onclick: proc { next_behavior } }, 'Next Observation')), + ]) + end + end + + def available_modifiers + observation.available_modifiers - observation.modifiers + end + + def add_modifier modifier + Store.dispatch AddModifier.new(observation, modifier) + end + + def next_behavior + Store.dispatch CompleteObservation.new + end + + def current_actor + Store.state.current_actor + end + + def current_actor_id + current_actor && current_actor.id + end + + def observation + Store.state.new_observation + end + + def animals + Store.state.subjects + end + + def set_behavior behavior + Store.dispatch ObserveBehavior.new(behavior) + end + + def set_actor event + Store.dispatch SetCurrentActor.new(event.target.value) + end + + def set_target target + Store.dispatch SetCurrentTarget.new(target) + end +end + +router = Clearwater::Router.new do + route 'new_observation' => NewObservation.new + + route 'focus' => Focus.new do + route 'animal' => FocusAnimal.new + end +end + +app = Clearwater::Application.new( + component: Layout.new, + router: router, +) + +app.call + +Store.on_dispatch do + app.render +end diff --git a/app/assets/javascripts/store.rb b/app/assets/javascripts/store.rb new file mode 100644 index 0000000..0d25450 --- /dev/null +++ b/app/assets/javascripts/store.rb @@ -0,0 +1,181 @@ +require 'grand_central/store' +require 'grand_central/model' + +require 'actions' + +class AppState < GrandCentral::Model + attributes( + :subjects, + :behaviors, + :modifiers, + :observations, + :sessions, + :current_actor, + :current_session, + :new_observation, + :unsaved_observations, + ) +end + +class Subject < GrandCentral::Model + attributes( + :id, + :name, + ) +end + +class Behavior < GrandCentral::Model + attributes( + :name, + :type, # :state or :event + :mutually_exclusive, # can only be true for :state types + :target, # :self, :target, nil + :parent_behavior, + :available_modifiers, + ) + + def initialize *args + super + + @available_modifiers ||= [] + end + + def targetable? + target == :target + end +end + +class Modifier < GrandCentral::Model + attributes( + :name, + ) + + def initialize name + @name = name + end +end + +class Observation < GrandCentral::Model + attributes( + :session_id, + :time, + :duration, + :actor, + :behavior, + :modifiers, + :target, + :note, + ) + + def initialize *args + super + + @modifiers ||= [] + end + + def available_modifiers + if behavior + behavior.available_modifiers.to_a + else + [] + end + end +end + +class Session < GrandCentral::Model + attributes( + :id, + :name, + ) +end + +initial_state = AppState.new( + subjects: [ + Subject.new(id: 1, name: 'Jamie'), + Subject.new(id: 2, name: 'Jim'), + Subject.new(id: 3, name: 'Thomas'), + Subject.new(id: 4, name: 'Julie'), + Subject.new(id: 5, name: 'Betsy'), + Subject.new(id: 6, name: 'Christina'), + Subject.new(id: 7, name: 'Andrew'), + Subject.new(id: 8, name: 'Ryan'), + ], + behaviors: [ + Behavior.new( + id: 1, + name: 'Running', + type: :state, + mutually_exclusive: true, + target: nil, + available_modifiers: %w(fast slow) + ), + Behavior.new( + id: 2, + name: 'Grooming', + type: :state, + mutually_exclusive: true, + target: :target, + ), + Behavior.new( + id: 3, + name: 'Eating', + type: :state, + mutually_exclusive: true, + target: nil, + ) + ], + modifiers: [ + ], + observations: [], + sessions: [], + current_session: Session.new, + new_observation: Observation.new, + unsaved_observations: [], +) + +Store = GrandCentral::Store.new(initial_state) do |state, action| + case action + when ObserveBehavior + state.update( + new_observation: state.new_observation.update( + behavior: action.behavior, + ), + ) + + when SetCurrentActor + new_actor = state.subjects.find { |subject| + subject.id.to_s == action.actor_id.to_s + } + + state.update( + current_actor: new_actor, + new_observation: state.new_observation.update( + actor: new_actor, + ), + ) + + when SetCurrentTarget + state.update( + new_observation: state.new_observation.update( + target: action.target, + ) + ) + + when AddModifier + observation = state.new_observation + state.update( + new_observation: observation.update( + modifiers: observation.modifiers + [action.modifier], + ), + ) + + when CompleteObservation + state.update( + unsaved_observations: state.unsaved_observations + [state.new_observation], + new_observation: Observation.new, + ) + + else + state + end +end diff --git a/app/views/landing/index.html.erb b/app/views/landing/index.html.erb index 8aa17e4..e69de29 100644 --- a/app/views/landing/index.html.erb +++ b/app/views/landing/index.html.erb @@ -1,5 +0,0 @@ -

Hello Ruby for Good

-

Deployed with TravisCI

-

- The time is now: <%= Time.now %> -

diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index b5e6118..3604899 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -5,6 +5,7 @@ <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track' => true %> <%= javascript_include_tag 'application', 'data-turbolinks-track' => true %> <%= csrf_meta_tags %> + diff --git a/config/routes.rb b/config/routes.rb index 7fea0cc..8b48b31 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,5 +1,6 @@ Rails.application.routes.draw do root 'landing#index' + get '*all' => 'landing#index' # The priority is based upon order of creation: first created -> highest priority. # See how all your routes lay out with "rake routes".