diff --git a/src/app.js b/src/app.js index 03ec910..25f337f 100644 --- a/src/app.js +++ b/src/app.js @@ -2,9 +2,21 @@ import 'foundation-sites/dist/foundation.css'; import 'css/app.css'; import $ from 'jquery'; - +import _ from 'underscore'; +import Backbone from 'backbone'; import Simulator from 'models/simulator'; + +// Models & Collections +import Quote from 'models/quote'; import QuoteList from 'collections/quote_list'; +import Order from 'models/order'; +import OrderList from 'collections/order_list'; + +// Views +import QuoteView from 'views/quote_view'; +import QuoteListView from 'views/quote_list_view'; +import OrderView from 'views/order_view'; +import OrderListView from 'views/order_list_view'; const quoteData = [ { @@ -27,9 +39,41 @@ const quoteData = [ $(document).ready(function() { const quotes = new QuoteList(quoteData); + const orders = new OrderList(); + + let bus = {}; + bus = _.extend(bus, Backbone.Events); + const simulator = new Simulator({ quotes: quotes, }); + const quoteListView = new QuoteListView({ + model: quotes, + template: _.template($('#quote-template').html()), + tradeTemplate: _.template($('#trade-template').html()), + bus: bus, + el: 'main' + }); + + const formSelect = function formSelect () { + quotes.each((quote) => { + const symbol = quote.get('symbol'); + $('select[name="symbol"]').append(``); + }); + }; + + const orderListView = new OrderListView({ + model: orders, + quotes: quotes, + template: _.template($('#order-template').html()), + bus: bus, + el: '#order-workspace' + }); + + + quoteListView.render(); + formSelect(); + orderListView.render(); simulator.start(); }); diff --git a/src/collections/order_list.js b/src/collections/order_list.js new file mode 100644 index 0000000..6b77957 --- /dev/null +++ b/src/collections/order_list.js @@ -0,0 +1,8 @@ +import Backbone from 'backbone'; +import Order from '../models/order'; + +const OrderList = Backbone.Collection.extend({ + model: Order, +}); + +export default OrderList; diff --git a/src/models/order.js b/src/models/order.js new file mode 100644 index 0000000..96ed9e9 --- /dev/null +++ b/src/models/order.js @@ -0,0 +1,27 @@ +import Backbone from 'backbone'; + +const Order = Backbone.Model.extend({ + validate(attributes) { + const errors = {}; + + if(!attributes.targetPrice || attributes.targetPrice <= 0) { + errors['price'] = ['Invalid target price']; + } + + if (this.get('buy') && attributes.targetPrice >= this.get('quote').get('price')) { + errors['price'] = ['Price higher than market price!']; + } + + if (!this.get('buy') && attributes.targetPrice <= this.get('quote').get('price')) { + errors['price'] = ['Price lower than market price!']; + } + + if ( Object.keys(errors).length > 0 ) { + return errors; + } else { + return false; + } + } +}); + +export default Order; diff --git a/src/models/quote.js b/src/models/quote.js index 4fbf466..263bb81 100644 --- a/src/models/quote.js +++ b/src/models/quote.js @@ -8,10 +8,12 @@ const Quote = Backbone.Model.extend({ buy() { // Implement this function to increase the price by $1.00 + this.set('price', this.get('price') + 1.00); }, sell() { // Implement this function to decrease the price by $1.00 + this.set('price', this.get('price') - 1.00); }, }); diff --git a/src/views/order_list_view.js b/src/views/order_list_view.js new file mode 100644 index 0000000..65dab22 --- /dev/null +++ b/src/views/order_list_view.js @@ -0,0 +1,75 @@ +import Backbone from 'backbone'; +// import _ from 'underscore'; +// import OrderList from '../collections/order_list'; +import $ from 'jquery'; +import OrderView from './order_view'; +import Order from '../models/order'; + +const OrderListView = Backbone.View.extend({ + initialize(params) { + this.template = params.template; + this.bus = params.bus; + this.quotes = params.quotes; + this.listenTo(this.model, 'update', this.render); + }, + render() { + this.$('#orders').empty(); + + this.model.each((order) => { + const orderView = new OrderView({ + model: order, + template: this.template, + bus: this.bus, + tagName: 'li', + quotes: this.quotes, + className: 'order', + }); + this.$('#orders').append(orderView.render().$el); + }); + return this; + }, + events: { + 'click .btn-buy': 'buyOrder', + 'click .btn-sell': 'sellOrder', + }, + createOrder(value) { + this.$('.form-errors').empty(); + + const orderData = { + buy: value.buy, + symbol: this.$('select[name=symbol]').val(), + quote: this.quotes.find({symbol: this.$('select[name=symbol]').val()}), + targetPrice: parseFloat(this.$('input[name=price-target]').val()), + bus: this.bus + }; + return new Order(orderData); + }, + buyOrder: function(event) { + event.preventDefault(); + const order = this.createOrder({buy: true}); + this.validate(order); + }, + sellOrder: function(event) { + event.preventDefault(); + const order = this.createOrder({buy: false}); + this.validate(order); + }, + validate(order) { + if (order.isValid()) { + this.model.add(order); + this.$el.find('form').trigger('reset'); + } else { + const errors = order.validationError; + const errorSection = this.$('.form-errors'); + + Object.keys(errors).forEach((field) => { + errors[field].forEach((error) => { + const html = `

${error}

`; + errorSection.append(html); + }); + }); + } + }, +}); + +export default OrderListView; diff --git a/src/views/order_view.js b/src/views/order_view.js new file mode 100644 index 0000000..e40654f --- /dev/null +++ b/src/views/order_view.js @@ -0,0 +1,37 @@ +import Backbone from 'backbone'; + +const OrderView = Backbone.View.extend({ + initialize(params) { + this.template = params.template; + this.bus = params.bus; + this.listenTo(this.model, 'change', this.render); + this.listenTo(this.model.get('quote'), 'change', this.tradeOrder); + }, + render() { + const compiledTemplate = this.template(this.model.toJSON()); + this.$el.html(compiledTemplate); + return this; + }, + events: { + 'click .btn-cancel': 'cancelOrder', + }, + cancelOrder() { + this.model.destroy(); + this.remove(); + }, + tradeOrder() { + if (this.model.get('buy')) { + if (this.model.get('quote').get('price') <= this.model.get('targetPrice')) { + this.bus.trigger('buyOrder', this); + this.cancelOrder(); + } + } else { + if (this.model.get('quote').get('price') >= this.model.get('targetPrice')) { + this.bus.trigger('sellOrder', this); + this.cancelOrder(); + } + } + }, +}); + +export default OrderView; diff --git a/src/views/quote_list_view.js b/src/views/quote_list_view.js new file mode 100644 index 0000000..91b3b95 --- /dev/null +++ b/src/views/quote_list_view.js @@ -0,0 +1,34 @@ +import Backbone from 'backbone'; +// import _ from 'underscore'; +// import Quote from '../models/quote'; +import QuoteView from './quote_view'; + + +const QuoteListView = Backbone.View.extend({ + initialize(params) { + this.template = params.template; + this.tradeTemplate = params.tradeTemplate; + this.bus = params.bus; + this.listenTo(this.model, 'update', this.render); + }, + render() { + this.model.each( (quote) => { + const quoteView = new QuoteView({ + model: quote, + template: this.template, + bus: this.bus, + tagName: 'li', + className: 'quote', + }); + this.listenTo(quoteView, 'quoteAction', this.trade); + this.$('#quotes').append(quoteView.render().$el); + }); + return this; + }, + trade: function(quoteView) { + const tradeTemplate = this.tradeTemplate(quoteView.model.toJSON()); + this.$('#trades').prepend(tradeTemplate); + }, +}); + +export default QuoteListView; diff --git a/src/views/quote_view.js b/src/views/quote_view.js new file mode 100644 index 0000000..633a59f --- /dev/null +++ b/src/views/quote_view.js @@ -0,0 +1,42 @@ +import Backbone from 'backbone'; + +const QuoteView = Backbone.View.extend({ + initialize(params) { + this.template = params.template; + this.bus = params.bus; + this.listenTo(this.model, 'change', this.render); + this.listenTo(this.bus, 'buyOrder', this.buyOrder); + this.listenTo(this.bus, 'sellOrder', this.sellOrder); + }, + render() { + const compiledTemplate = this.template(this.model.toJSON()); + this.$el.html(compiledTemplate); + return this; + }, + events: { + 'click button.btn-buy': 'buyQuote', + 'click button.btn-sell': 'sellQuote', + }, + buyQuote: function(event) { + this.model.set('buy', true); + this.trigger('quoteAction', this); + this.model.buy(); + }, + sellQuote: function(event) { + this.model.set('buy', false); + this.trigger('quoteAction', this); + this.model.sell(); + }, + buyOrder: function(orderQuote) { + if(orderQuote.model.get('symbol') === this.model.get('symbol')){ + this.buyQuote(); + } + }, + sellOrder: function(orderQuote) { + if(orderQuote.model.get('symbol') === this.model.get('symbol')){ + this.sellQuote(); + } + } +}); + +export default QuoteView;