commit
a39f032ef9
|
@ -32,6 +32,7 @@ rerun.txt
|
|||
pickle-email-*.html
|
||||
/public/assets/
|
||||
/public/build/*
|
||||
/public/js/*
|
||||
|
||||
|
||||
## Environment normalisation:
|
||||
|
|
3
Gemfile
3
Gemfile
|
@ -19,9 +19,10 @@ gem 'devise', github: 'plataformatec/devise'
|
|||
gem 'mutations'
|
||||
gem 'active_model_serializers', '~> 0.8.3'
|
||||
gem 'ice_cube'
|
||||
gem 'gulp_rails', '~> 1.0'
|
||||
|
||||
source 'https://rails-assets.org' do
|
||||
gem 'rails-assets-ng-sortable'
|
||||
gem 'rails-assets-ng-sortable', '~> 1.2.2'
|
||||
gem 'rails-assets-ng-pickadate'
|
||||
gem 'rails-assets-js-data'
|
||||
gem 'rails-assets-js-data-angular'
|
||||
|
|
|
@ -145,6 +145,7 @@ GEM
|
|||
sexp_processor (~> 4.4)
|
||||
font-awesome-rails (4.3.0.0)
|
||||
railties (>= 3.2, < 5.0)
|
||||
gulp_rails (1.0)
|
||||
haml (4.0.6)
|
||||
tilt
|
||||
high_voltage (2.1.0)
|
||||
|
@ -242,7 +243,7 @@ GEM
|
|||
rails-assets-ng-pickadate (0.2.2)
|
||||
rails-assets-angular (~> 1.4.5)
|
||||
rails-assets-pickadate (~> 3.5.6)
|
||||
rails-assets-ng-sortable (1.3.0)
|
||||
rails-assets-ng-sortable (1.2.3)
|
||||
rails-assets-angular (>= 1.3.0)
|
||||
rails-assets-pickadate (3.5.6)
|
||||
rails-assets-jquery (>= 1.7)
|
||||
|
@ -370,6 +371,7 @@ DEPENDENCIES
|
|||
factory_girl_rails
|
||||
faker
|
||||
font-awesome-rails
|
||||
gulp_rails (~> 1.0)
|
||||
haml
|
||||
high_voltage (~> 2.1.0)
|
||||
ice_cube
|
||||
|
@ -387,7 +389,7 @@ DEPENDENCIES
|
|||
rails-assets-js-data-angular!
|
||||
rails-assets-lodash!
|
||||
rails-assets-ng-pickadate!
|
||||
rails-assets-ng-sortable!
|
||||
rails-assets-ng-sortable (~> 1.2.2)!
|
||||
rails-assets-pickadate!
|
||||
rails-assets-sio-client!
|
||||
rails_12factor
|
||||
|
|
20
README.md
20
README.md
|
@ -14,14 +14,18 @@ This Repo is the Web based side of FarmBot. It allows users to control the devic
|
|||
|
||||
# Developer setup
|
||||
|
||||
1. `git clone git@github.com:FarmBot/farmbot-web-app.git`
|
||||
2. `cd farmbot-web-app`
|
||||
3. [Install MongoDB](http://docs.mongodb.org/manual/tutorial/install-mongodb-on-os-x/)
|
||||
4. Start Mongo if you have not already done so. (typically via the `mongod` command)
|
||||
3. `bundle install`
|
||||
4. `npm install`
|
||||
5. `rails s`
|
||||
6. Go to `http://localhost:3000`
|
||||
0. `git clone git@github.com:FarmBot/farmbot-web-app.git`
|
||||
0. `cd farmbot-web-app`
|
||||
0. [Install MongoDB](http://docs.mongodb.org/manual/tutorial/install-mongodb-on-os-x/)
|
||||
0. Start Mongo if you have not already done so. (typically via the `mongod` command)
|
||||
0. `bundle install`
|
||||
0. [Install node](https://nodejs.org/en/download/package-manager/)
|
||||
0. `sudo npm install gulp -g` if you don't have gulp installed already.
|
||||
0. `npm install`
|
||||
0. `rails s`
|
||||
0. Go to `http://localhost:3000`
|
||||
|
||||
The frontend (and asset management) are very much in a transitional state. We're experimenting with Gulp as an alternative
|
||||
|
||||
**We can't fix issues we don't know about.** Please submit an issue if you are having trouble installing on your local machine.
|
||||
|
||||
|
|
|
@ -0,0 +1,117 @@
|
|||
function NullSequence(){
|
||||
this._id = null;
|
||||
this.steps = [];
|
||||
}
|
||||
|
||||
var controller = function($scope, Data, Devices) {
|
||||
$scope.sequence = new NullSequence;
|
||||
$scope.operators = ['==', '>', '<', '!='];
|
||||
$scope.variables = ["x", "y", "z", "s", "busy", "last", "pin0", "pin1",
|
||||
"pin2", "pin3", "pin4", "pin5", "pin6", "pin7", "pin8",
|
||||
"pin9", "pin10", "pin11", "pin12", "pin13"];
|
||||
|
||||
var nope = function(e) {
|
||||
alert('Doh!');
|
||||
return console.error(e);
|
||||
};
|
||||
|
||||
Data.findAll('sequence', {}).catch(nope);
|
||||
Data.bindAll('sequence', {}, $scope, 'storedSequences');
|
||||
$scope.dragControlListeners = {
|
||||
orderChanged: function(event) {
|
||||
var position, step;
|
||||
position = event.dest.index;
|
||||
step = event.source.itemScope.step;
|
||||
return Data.update('step', step._id, {
|
||||
position: position
|
||||
}).catch(nope).then(function(step) {
|
||||
return $scope.load($scope.sequence);
|
||||
});
|
||||
}
|
||||
};
|
||||
var hasSequence = function() {
|
||||
var whoah = function() {
|
||||
return alert('Select or create a sequence first.');
|
||||
};
|
||||
if (!!$scope.sequence._id) {
|
||||
return true;
|
||||
} else {
|
||||
whoah();
|
||||
return false;
|
||||
}
|
||||
};
|
||||
$scope.addStep = function(message_type) {
|
||||
if (!hasSequence()) {
|
||||
return;
|
||||
}
|
||||
return Data.create('step', {
|
||||
message_type: message_type,
|
||||
sequence_id: $scope.sequence._id
|
||||
}).catch(nope);
|
||||
};
|
||||
$scope.load = function(seq) {
|
||||
return Data.loadRelations('sequence', seq._id, ['step'], {
|
||||
bypassCache: true
|
||||
}).catch(nope).then(function(sequence) {
|
||||
return $scope.sequence = sequence;
|
||||
});
|
||||
};
|
||||
$scope.addSequence = function(params) {
|
||||
if (params == null) {
|
||||
params = {};
|
||||
}
|
||||
if (params.name == null) {
|
||||
params.name = 'Untitled Sequence';
|
||||
}
|
||||
return Data.create('sequence', params).catch(nope).then(function(seq) {
|
||||
return $scope.load(seq);
|
||||
});
|
||||
};
|
||||
$scope.deleteSequence = function(seq) {
|
||||
if (!hasSequence()) {
|
||||
return;
|
||||
}
|
||||
return Data.destroy('sequence', seq._id).catch(nope).then(function() {
|
||||
return $scope.sequence = new NullSequence;
|
||||
});
|
||||
};
|
||||
$scope.saveSequence = function(seq) {
|
||||
return Data.save('sequence', seq._id).catch(nope).then(function(sequence) {
|
||||
var i, inx, len, ref, results, step;
|
||||
ref = sequence.steps;
|
||||
results = [];
|
||||
for (inx = i = 0, len = ref.length; i < len; inx = ++i) {
|
||||
step = ref[inx];
|
||||
results.push(Data.update('step', step._id, {
|
||||
command: step.command
|
||||
}).catch(function(e) {
|
||||
alert("Error saving step " + (inx + 1) + ". See console for details.");
|
||||
return console.error(e);
|
||||
}));
|
||||
}
|
||||
return results;
|
||||
});
|
||||
};
|
||||
$scope.copy = function(obj, index) {
|
||||
if (!hasSequence()) {
|
||||
return;
|
||||
}
|
||||
return Data.create('step', {
|
||||
sequence_id: $scope.sequence._id,
|
||||
message_type: obj.message_type,
|
||||
command: obj.command || {},
|
||||
position: index
|
||||
}).catch(nope);
|
||||
};
|
||||
$scope.deleteStep = function(index) {
|
||||
return Data.destroy('step', $scope.sequence.steps[index]._id).catch(nope);
|
||||
};
|
||||
return $scope.execute = function(seq) {
|
||||
var sequence;
|
||||
sequence = Data.utils.removeCircular(seq);
|
||||
return Devices.send("exec_sequence", sequence);
|
||||
};
|
||||
};
|
||||
|
||||
angular.module('FarmBot').controller("SequenceController",
|
||||
['$scope', 'Data', 'Devices', controller])
|
|
@ -0,0 +1,26 @@
|
|||
module Api
|
||||
class PlantingAreasController < Api::AbstractController
|
||||
|
||||
def index
|
||||
render json: PlantingArea.where(device: current_device)
|
||||
end
|
||||
|
||||
def create
|
||||
mutate PlantingAreas::Create.run(params, device: current_device)
|
||||
end
|
||||
|
||||
def destroy
|
||||
if (planting_area.device == current_device) && planting_area.destroy
|
||||
render nothing: true
|
||||
else
|
||||
raise Errors::Forbidden, "Not your Planting Area."
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def planting_area
|
||||
@planting_area ||= PlantingArea.find(params[:id])
|
||||
end
|
||||
end
|
||||
end
|
|
@ -10,7 +10,7 @@ module Api
|
|||
end
|
||||
|
||||
def destroy
|
||||
if (crop.device == current_device) && crop.destroy
|
||||
if (plant.device == current_device) && plant.destroy
|
||||
render nothing: true
|
||||
else
|
||||
raise Errors::Forbidden, "Not your Plant object."
|
||||
|
@ -19,8 +19,8 @@ module Api
|
|||
|
||||
private
|
||||
|
||||
def crop
|
||||
@crop ||= Plant.find(params[:id])
|
||||
def plant
|
||||
@plant ||= Plant.find(params[:id])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -7,6 +7,7 @@ class Device
|
|||
has_many :schedules, dependent: :destroy
|
||||
has_many :sequences
|
||||
has_many :plants, dependent: :destroy
|
||||
has_one :planting_area
|
||||
|
||||
|
||||
# The SkyNet UUID of the device
|
||||
|
|
|
@ -3,6 +3,7 @@ class Plant
|
|||
include Mongoid::Document
|
||||
|
||||
belongs_to :device
|
||||
belongs_to :planting_area
|
||||
|
||||
field :x, type: Integer
|
||||
field :y, type: Integer
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
# The area inside of the FarmBot's tracks.
|
||||
class PlantingArea
|
||||
include Mongoid::Document
|
||||
|
||||
field :width, type: Integer, default: 600
|
||||
field :length, type: Integer, default: 300
|
||||
|
||||
has_many :plants
|
||||
belongs_to :device
|
||||
end
|
|
@ -1,3 +1,4 @@
|
|||
= render partial: "pages/navbar"
|
||||
%div
|
||||
%script{:id => "devices.html", :type => "text/ng-template"}
|
||||
= render partial: "dashboard/ng-partials/devices"
|
||||
|
|
|
@ -14,57 +14,5 @@
|
|||
= javascript_include_tag "//maxcdn.bootstrapcdn.com/bootstrap/3.3.5/js/bootstrap.min.js"
|
||||
= javascript_include_tag "application"
|
||||
%body{'ng-app' => 'FarmBot'}
|
||||
.row
|
||||
%nav.navbar.navbar-default.drop-shadow{ng_app: 'farmbot', ng_controller: 'nav', role: "navigation"}
|
||||
.container-fluid
|
||||
/ Brand and toggle get grouped for better mobile display
|
||||
.navbar-header
|
||||
%button.navbar-toggle{"data-target" => "#navbar", "data-toggle" => "collapse", :type => "button"}
|
||||
%span.glyphicon.glyphicon-menu-hamburger
|
||||
/ Collect the nav links, forms, and other content for toggling
|
||||
#navbar.collapse.navbar-collapse
|
||||
%ul.nav.navbar-nav
|
||||
- if current_user
|
||||
%li
|
||||
%a{:href => "/pages/farm_designer"} Farm Designer
|
||||
%li
|
||||
%a{:href => "/dashboard#/movement"} Controls
|
||||
%li
|
||||
%a{:href => "/dashboard#/devices"} Devices
|
||||
%li
|
||||
%a{:href => "/dashboard#/sequence"} Sequences
|
||||
%li
|
||||
%a{:href => "/dashboard#/schedule"} Schedules
|
||||
%ul.nav.navbar-nav.navbar-right
|
||||
- if current_user && controller_name == "dashboard"
|
||||
%li
|
||||
%syncbutton.nav-status-buttons{schedules: "schedules"}
|
||||
%li
|
||||
%stopbutton.nav-status-buttons
|
||||
- if current_user
|
||||
%li
|
||||
%a{:href => destroy_user_session_path} Sign out
|
||||
%li
|
||||
%a{:href => edit_user_registration_path} My Account
|
||||
- else
|
||||
%li
|
||||
%a{:href => new_user_session_path} Log In
|
||||
%li
|
||||
%a{:href => new_user_registration_path} Register
|
||||
%li
|
||||
%a{:href => page_path('help')} Help
|
||||
/ /.navbar-collapse
|
||||
/ /.container-fluid
|
||||
.row
|
||||
.container.col-xs-12.col-sm-6.col-md-4.col-xs-centered
|
||||
- if notice
|
||||
.alert-box.notice.round{"onClick" => "hidden = true"}
|
||||
= notice
|
||||
%a.close{style: 'margin-right: 10px; color: white; opacity: 1.0;'} ×
|
||||
- if alert
|
||||
.alert-box.alert.round{"onClick" => "hidden = true"}
|
||||
= alert
|
||||
%a.close{style: 'margin-right: 10px; color: white; opacity: 1.0;'} ×
|
||||
.container.col-lg-12
|
||||
.content
|
||||
= yield
|
||||
.content
|
||||
= yield
|
||||
|
|
|
@ -0,0 +1,83 @@
|
|||
<div class="row"></div>
|
||||
<nav class="navbar navbar-default drop-shadow" ng_app="farmbot" ng_controller="nav" role="navigation">
|
||||
<div class="container-fluid">
|
||||
<!-- Brand and toggle get grouped for better mobile display
|
||||
-->
|
||||
<div class="navbar-header">
|
||||
<button class="navbar-toggle" data-target="#navbar" data-toggle="collapse" type="button">
|
||||
<span class="glyphicon glyphicon-menu-hamburger"></span>
|
||||
</button>
|
||||
</div>
|
||||
<!-- Collect the nav links, forms, and other content for toggling
|
||||
-->
|
||||
<div class="collapse navbar-collapse" id="navbar">
|
||||
<ul class="nav navbar-nav">
|
||||
<% if current_user %>
|
||||
<li>
|
||||
<a href="/pages/farm_designer">Farm Designer</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/dashboard#/movement">Controls</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/dashboard#/devices">Devices</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/dashboard#/sequence">Sequences</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/dashboard#/schedule">Schedules</a>
|
||||
</li>
|
||||
<% end %>
|
||||
</ul>
|
||||
<ul class="nav navbar-nav navbar-right">
|
||||
<% if current_user && controller_name == "dashboard" %>
|
||||
<li>
|
||||
<syncbutton class="nav-status-buttons" schedules="schedules"></syncbutton>
|
||||
</li>
|
||||
<li>
|
||||
<stopbutton class="nav-status-buttons"></stopbutton>
|
||||
</li>
|
||||
<% end %>
|
||||
<% if current_user %>
|
||||
<li>
|
||||
<a href="<%= destroy_user_session_path %>">Sign out</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="<%= edit_user_registration_path %>">My Account</a>
|
||||
</li>
|
||||
<% else %>
|
||||
<li>
|
||||
<a href="<%= new_user_session_path %>">Log In</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="<%= new_user_registration_path %>">Register</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="<%= page_path('help') %>">Help</a>
|
||||
</li>
|
||||
<% end %>
|
||||
</ul>
|
||||
</div>
|
||||
<!-- /.navbar-collapse
|
||||
-->
|
||||
</div>
|
||||
<!-- /.container-fluid
|
||||
-->
|
||||
</nav>
|
||||
<div class="row">
|
||||
<div class="container col-xs-12 col-sm-6 col-md-4 col-xs-centered">
|
||||
<% if notice %>
|
||||
<div class="alert-box notice round" onClick="hidden = true">
|
||||
<%= notice %>
|
||||
<a class="close" style="margin-right: 10px; color: white; opacity: 1.0;">×</a>
|
||||
</div>
|
||||
<% end %>
|
||||
<% if alert %>
|
||||
<div class="alert-box alert round" onClick="hidden = true">
|
||||
<%= alert %>
|
||||
<a class="close" style="margin-right: 10px; color: white; opacity: 1.0;">×</a>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
|
@ -1,4 +1,4 @@
|
|||
<div class="farm-designer" id="root">
|
||||
<div id="root">
|
||||
</div>
|
||||
|
||||
<script type="text/javascript">
|
||||
|
@ -9,6 +9,7 @@
|
|||
},
|
||||
global: {
|
||||
plants: <%= raw(current_user.device.plants.to_json) %>,
|
||||
planting_area: <%= raw PlantingArea.find_or_create_by(device: current_user.device).to_json %>,
|
||||
selectedPlant: {}
|
||||
}
|
||||
};
|
||||
|
|
|
@ -7,3 +7,9 @@ FarmBot::Application.configure do
|
|||
config.consider_all_requests_local = true
|
||||
config.eager_load = false
|
||||
end
|
||||
# Whether or not compilation should take place
|
||||
GulpRails.options[:enabled] = true
|
||||
# The command to run
|
||||
GulpRails.options[:command] = 'gulp default'
|
||||
# The directory in which your command should be executed
|
||||
GulpRails.options[:directory] = Rails.root
|
|
@ -3,6 +3,7 @@ FarmBot::Application.routes.draw do
|
|||
namespace :api, defaults: {format: :json} do
|
||||
resource :device, only: [:show, :destroy, :create, :update]
|
||||
resources :plants, only: [:create, :destroy, :index]
|
||||
resources :planting_area, only: [:create, :destroy]
|
||||
resources :sequences, only: [:create, :update, :destroy, :index, :show] do
|
||||
resources :steps, only: [:show, :create, :index, :update, :destroy]
|
||||
end
|
||||
|
|
19
gulpfile.js
19
gulpfile.js
|
@ -3,30 +3,27 @@ var gulp = require('gulp'),
|
|||
concat = require('gulp-concat'),
|
||||
browserify = require('browserify'),
|
||||
source = require('vinyl-source-stream'),
|
||||
exec = require('child_process').exec;
|
||||
exec = require('child_process').exec,
|
||||
babelify = require('babelify');
|
||||
|
||||
var paths = {
|
||||
js: './javascripts/**/**/*.js'
|
||||
};
|
||||
|
||||
function oops (s) {
|
||||
exec("espeak 'build Error.'");
|
||||
exec( 'notify-send "' + (s.message || s) + '"' );
|
||||
exec( 'notify-send "' + (s.message || s || "Gulp Error") + '"' );
|
||||
gutil.log(s.message);
|
||||
}
|
||||
|
||||
gulp.task('default', function () {
|
||||
gulp.watch(paths.js, ['build']);
|
||||
gulp.task('watch', function () {
|
||||
gulp.watch(paths.js, ['default']);
|
||||
});
|
||||
|
||||
gulp.task('build', function () {
|
||||
browserify({
|
||||
entries: ['javascripts/farm_designer.js'],
|
||||
extensions: ['.js']
|
||||
})
|
||||
gulp.task('default', function () {
|
||||
browserify('javascripts/farm_designer.js',{debug:true})
|
||||
.transform(babelify)
|
||||
.bundle()
|
||||
.on('error', oops)
|
||||
.pipe(source('farm-designer.js'))
|
||||
.pipe(gulp.dest('public/build/'));
|
||||
exec("espeak 'Saved.'");
|
||||
})
|
||||
|
|
|
@ -1,17 +1,20 @@
|
|||
import React from 'react/addons';
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { Provider } from 'react-redux';
|
||||
import { store } from './redux/store';
|
||||
import { connect } from 'react-redux';
|
||||
import { DesignerMain } from './menus/designer_main';
|
||||
|
||||
function wow (d) {
|
||||
return {dispatch: d};
|
||||
function mapDispatchToProps(d) {
|
||||
return {
|
||||
dispatch: d
|
||||
};
|
||||
}
|
||||
var App = connect(s => s, wow)(DesignerMain);
|
||||
var App = connect(state => state, mapDispatchToProps)(DesignerMain);
|
||||
|
||||
React.render(
|
||||
<Provider store={store}>
|
||||
{() => <App />}
|
||||
{ () => <App />}
|
||||
</Provider>,
|
||||
document.getElementById('root')
|
||||
);
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
/*
|
||||
Translates page x/y coordinates to garden x/y.
|
||||
TODO: A type checker would be pretty sweet here.
|
||||
*/
|
||||
export function fromScreenToGarden(mouseX, mouseY, boxX, boxY) {
|
||||
var rawX = mouseX - boxX;
|
||||
var rawY = boxY - mouseY;
|
||||
|
||||
return {x: rawX, y: rawY};
|
||||
};
|
|
@ -4,6 +4,7 @@ import { Calendar } from './calendar';
|
|||
import { PlantInfo } from './plant_info';
|
||||
import { CropInfo } from './crop_info';
|
||||
import { GardenMap } from './garden_map';
|
||||
import { Navbar } from './navbar';
|
||||
|
||||
const LEFT_MENU_CHOICES = {PlantInventory, PlantCatalog, PlantInfo, CropInfo}
|
||||
|
||||
|
@ -33,23 +34,26 @@ export class DesignerMain extends React.Component {
|
|||
|
||||
render(){
|
||||
return (
|
||||
<div className="farm-designer-body">
|
||||
<div className="farm-designer-left">
|
||||
<div id="designer-left">
|
||||
{ this.renderLeft() }
|
||||
<div className="farm-designer">
|
||||
<Navbar/>
|
||||
<div className="farm-designer-body">
|
||||
<div className="farm-designer-left">
|
||||
<div id="designer-left">
|
||||
{ this.renderLeft() }
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="farm-designer-middle">
|
||||
{ this.renderMiddle() }
|
||||
</div>
|
||||
|
||||
<div className="farm-designer-right">
|
||||
<div id="designer-right">
|
||||
<Calendar />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="farm-designer-middle">
|
||||
{ this.renderMiddle() }
|
||||
</div>
|
||||
|
||||
<div className="farm-designer-right">
|
||||
<div id="designer-right">
|
||||
<Calendar />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,35 +1,51 @@
|
|||
export class MapPointView extends React.Component {
|
||||
export class MapPoint extends React.Component {
|
||||
select() {
|
||||
this.props.dispatch({type: "CROP_SELECT", payload: this.props.plant});
|
||||
}
|
||||
|
||||
selected() {
|
||||
return (!!this.props.selected);
|
||||
}
|
||||
|
||||
render() {
|
||||
var style = {
|
||||
position: 'absolute',
|
||||
left: (this.props.plant.x - 20),
|
||||
top: (this.props.plant.y - 40)
|
||||
};
|
||||
if (!!this.props.selected) { style.border = "1px solid green"; };
|
||||
return <div onClick={ this.select.bind(this) }>
|
||||
<img style={style} src="/designer_icons/pin.png"></img>
|
||||
</div>
|
||||
var { length } = this.props.planting_area;
|
||||
var fill = this.selected() ? "red" : "black";
|
||||
return <circle cx={ this.props.plant.x }
|
||||
cy={ (-1 * this.props.plant.y) + length }
|
||||
onClick={ this.select.bind(this) }
|
||||
fill={ fill }
|
||||
r="5" />;
|
||||
}
|
||||
};
|
||||
|
||||
export class GardenMap extends React.Component {
|
||||
plants() {
|
||||
return this.props.plants.map(
|
||||
(p, k) => <MapPointView plant={ p }
|
||||
(p, k) => <MapPoint plant={ p }
|
||||
key={ k }
|
||||
planting_area={ this.props.planting_area }
|
||||
selected={ (this.props.selectedPlant._id === p._id) }
|
||||
dispatch={ this.props.dispatch }/>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
var style = {
|
||||
fill: 'rgb(136, 119, 102)',
|
||||
strokeWidth: 1,
|
||||
stroke: 'rgb(0,0,0)'
|
||||
}
|
||||
var {width, length} = this.props.planting_area;
|
||||
|
||||
return <div>
|
||||
<div id="drop-area">
|
||||
{ this.plants() }
|
||||
<div id="drop-area" style={ {marginLeft: '10px', marginTop: '10px'} }>
|
||||
<svg width={ width }
|
||||
height={ length } >
|
||||
<rect width={ width }
|
||||
height={ length }
|
||||
style={ style } />
|
||||
{ this.plants() }
|
||||
</svg>
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
export class Navbar extends React.Component {
|
||||
render() {
|
||||
return <div className="row">
|
||||
<nav className="drop-shadow">
|
||||
<div className="small-menu-title">MENU</div>
|
||||
<a href="/">Home</a>
|
||||
<a href="/pages/farm_designer">Farm Designer</a>
|
||||
<a href="/dashboard#/movement">Controls</a>
|
||||
<a href="/dashboard#/devices">Devices</a>
|
||||
<a href="/dashboard#/sequence">Sequences</a>
|
||||
<a href="/dashboard#/schedule">Schedules</a>
|
||||
<a className="large-menu-right" href="/users/sign_out">Sign out</a>
|
||||
<a className="large-menu-right" href="/users/edit">My Account</a>
|
||||
<button className="red button-like" type="button">Stop*</button>
|
||||
<button className="yellow button-like" type="button">
|
||||
Sync <i className="fa fa-upload"></i>*
|
||||
</button>
|
||||
LAST SYNC: Never
|
||||
</nav>
|
||||
</div>
|
||||
}
|
||||
}
|
|
@ -1,8 +1,13 @@
|
|||
import { Plant } from '../plant';
|
||||
import { fromScreenToGarden } from '../geometry/coordinates';
|
||||
|
||||
export class PlantInfo extends React.Component {
|
||||
drop (e) {
|
||||
var plant = new Plant({x: e.clientX, y: e.clientY});
|
||||
var box = document
|
||||
.querySelector('#drop-area > svg > rect')
|
||||
.getBoundingClientRect();
|
||||
var coords = fromScreenToGarden(e.pageX, e.pageY, box.left, box.bottom)
|
||||
var plant = new Plant(coords);
|
||||
this.props.dispatch({type: "CROP_ADD_REQUEST", payload: plant});
|
||||
}
|
||||
|
||||
|
|
|
@ -33,6 +33,9 @@ export class Item extends React.Component {
|
|||
};
|
||||
|
||||
export class Plants extends React.Component {
|
||||
wow() {
|
||||
this.props.dispatch({type: "EXPERIMENTAL"});
|
||||
}
|
||||
render() {
|
||||
var d = this.props.dispatch;
|
||||
return(
|
||||
|
|
|
@ -9,7 +9,6 @@ export class Plant {
|
|||
}
|
||||
};
|
||||
|
||||
|
||||
Plant.fakePlants = [
|
||||
new Plant({name: "Blueberry", imgUrl: "/designer_icons/blueberry.svg"}),
|
||||
new Plant({name: "Cabbage", imgUrl: "/designer_icons/cabbage.svg"}),
|
||||
|
|
|
@ -1,45 +1,48 @@
|
|||
//actually, these are 'action creators'.
|
||||
import { store } from './store';
|
||||
import { addons } from 'react/addons';
|
||||
|
||||
let actions = {};
|
||||
|
||||
actions['@@redux/INIT'] = empty;
|
||||
|
||||
actions.DEFAULT = function (s, a) {
|
||||
actions.DEFAULT = function(s, a) {
|
||||
console.warn("Unknown action fired.");
|
||||
console.trace();
|
||||
return s;
|
||||
};
|
||||
|
||||
actions.CROP_SELECT = function(s, a) {
|
||||
var select_crop = update(s, {global: {selectedPlant: a.payload}});
|
||||
var select_crop = update(s, {
|
||||
global: {
|
||||
selectedPlant: a.payload
|
||||
}
|
||||
});
|
||||
var change_menu = actions.CROP_INFO_SHOW(select_crop, a);
|
||||
return _.merge({}, select_crop, change_menu);
|
||||
};
|
||||
|
||||
actions.CROP_ADD_REQUEST = function (s, a) {
|
||||
actions.CROP_ADD_REQUEST = function(s, a) {
|
||||
// TODO: Add some sort of Redux Async handler.
|
||||
$.ajax({method: "POST", url: "/api/plants", data: a.payload})
|
||||
.fail(function (a, b, c) {
|
||||
alert("Failed to add crop. Refresh page.");
|
||||
});
|
||||
$.ajax({
|
||||
method: "POST",
|
||||
url: "/api/plants",
|
||||
data: a.payload
|
||||
})
|
||||
.fail((a, b, c) => alert("Failed to add crop. Refresh page."))
|
||||
.then((aa,bb,cc,dd) => store.dispatch({type: "CROP_ADD_FINISH"}));
|
||||
var plants = _.cloneDeep(s.global.plants);
|
||||
var selectedPlant = _.cloneDeep(a.payload);
|
||||
var selectedPlant = _.cloneDeep(a.payload);
|
||||
plants.push(selectedPlant);
|
||||
return update(s, { global: { plants, selectedPlant } });
|
||||
return update(s, {
|
||||
global: {
|
||||
plants,
|
||||
selectedPlant
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// function incrementAsync() {
|
||||
// return dispatch => {
|
||||
// setTimeout(() => {
|
||||
// // Yay! Can invoke sync or async actions with `dispatch`
|
||||
// dispatch(increment());
|
||||
// }, 1000);
|
||||
// };
|
||||
// }
|
||||
actions.CROP_ADD_FINISH = function(s, a) { return s; };
|
||||
|
||||
actions.CROP_REMOVE_REQUEST = function (s, a) {
|
||||
actions.CROP_REMOVE_REQUEST = function(s, a) {
|
||||
var s = _.cloneDeep(s);
|
||||
var id = a.payload._id;
|
||||
_.remove(s.global.plants, a.payload)
|
||||
|
@ -66,36 +69,47 @@ actions.PLANT_INFO_SHOW = function(s, a) {
|
|||
actions.CROP_INFO_SHOW = function(s, a) {
|
||||
// TODO: add type system to check for presence of `crop` Object?
|
||||
return update(s, {
|
||||
leftMenu: {
|
||||
component: 'CropInfo',
|
||||
plant: a.payload
|
||||
}
|
||||
});
|
||||
leftMenu: {
|
||||
component: 'CropInfo',
|
||||
plant: a.payload
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
actions.CATALOG_SHOW = function(s, a){
|
||||
actions.CATALOG_SHOW = function(s, a) {
|
||||
return changeLeftComponent(s, 'PlantCatalog');
|
||||
};
|
||||
|
||||
actions.INVENTORY_SHOW = function(s, a){
|
||||
actions.INVENTORY_SHOW = function(s, a) {
|
||||
return changeLeftComponent(s, 'PlantInventory');
|
||||
};
|
||||
|
||||
actions.INVENTORY_SHOW_TAB = function(s, a) {
|
||||
return update(s, {leftMenu: {tab: a.payload}});
|
||||
return update(s, {
|
||||
leftMenu: {
|
||||
tab: a.payload
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function empty(s, a) {
|
||||
return s;
|
||||
};
|
||||
}
|
||||
;
|
||||
|
||||
function changeLeftComponent(state, name) {
|
||||
return update(state, {leftMenu: {component: name}});
|
||||
};
|
||||
return update(state, {
|
||||
leftMenu: {
|
||||
component: name
|
||||
}
|
||||
});
|
||||
}
|
||||
;
|
||||
|
||||
function update(old_state, new_state) {
|
||||
return _.merge({}, old_state, new_state);
|
||||
};
|
||||
}
|
||||
;
|
||||
|
||||
export { actions };
|
||||
|
|
|
@ -2,11 +2,13 @@ import { actions } from './actions';
|
|||
import { isFSA } from 'flux-standard-action';
|
||||
|
||||
export function reducer(state, action) {
|
||||
if (isFSA(action)){
|
||||
if (isFSA(action)) {
|
||||
console.log(action.type);
|
||||
console.dir(state);
|
||||
return (actions[action.type] || actions.DEFAULT)(state, action);
|
||||
} else {
|
||||
console.error("Action does not conform to 'flux-standard-action", action);
|
||||
};
|
||||
};
|
||||
}
|
||||
;
|
||||
}
|
||||
;
|
||||
|
|
|
@ -2,8 +2,7 @@ import { createStore, applyMiddleware } from 'redux';
|
|||
import { reducer } from './reducer';
|
||||
import thunk from 'redux-thunk';
|
||||
|
||||
// var store = createStore(reducer, window.initialState);
|
||||
var store = applyMiddleware(thunk)
|
||||
(createStore)
|
||||
(reducer, window.initialState);
|
||||
var wrappedCreatedStore = applyMiddleware(thunk)(createStore);
|
||||
var store = wrappedCreatedStore(reducer, window.initialState);
|
||||
|
||||
export { store };
|
||||
|
|
|
@ -31,11 +31,13 @@
|
|||
"gulp": "^3.9.0",
|
||||
"gulp-concat": "^2.6.0",
|
||||
"gulp-util": "^3.0.6",
|
||||
"react": "^0.13.3",
|
||||
"react": "^0.13.0",
|
||||
"react-dom": "^0.14.0",
|
||||
"react-redux": "^2.1.2",
|
||||
"reactify": "^1.1.1",
|
||||
"redux": "^3.0.0",
|
||||
"redux-thunk": "^1.0.0",
|
||||
"vinyl-source-stream": "^1.1.0"
|
||||
"vinyl-source-stream": "^1.1.0",
|
||||
"whatwg-fetch": "^0.9.0"
|
||||
}
|
||||
}
|
||||
|
|
Binary file not shown.
After Width: | Height: | Size: 7.3 KiB |
Loading…
Reference in New Issue