diff --git a/dist/index.html b/dist/index.html index 559b18ecd..7d40f8fda 100644 --- a/dist/index.html +++ b/dist/index.html @@ -1,15 +1,135 @@ - - - Backbone Baseline - - -
- -
- + + + Backbone Baseline + + +
+
+

Movie Store

+
+
+ +
+ +
+ + + +
+
+
+

Find a movie in the store:

+
+ + + + +
+
+
+
+

+

+

+

+ + + + + + + + + +
ImageTitleRelease DateOverview
+
+
+
+
+ + + + + + diff --git a/package-lock.json b/package-lock.json index 2d3371d51..a232553f5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -373,7 +373,6 @@ "version": "2.6.0", "resolved": "https://registry.npmjs.org/async/-/async-2.6.0.tgz", "integrity": "sha512-xAfGg1/NTLBBKlHFmnd7PlmUW9KhVQIUuSrYem9xzFUZy13ScvtyGGejaae9iAVRiRq9+Cx7DPFaAAhCpyxyPw==", - "dev": true, "requires": { "lodash": "4.17.4" } @@ -2216,6 +2215,11 @@ "domelementtype": "1.3.0" } }, + "duplexer": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.1.tgz", + "integrity": "sha1-rOb/gIwc5mtX0ev5eXessCM0z8E=" + }, "ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -2725,6 +2729,20 @@ "es5-ext": "0.10.37" } }, + "event-stream": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/event-stream/-/event-stream-3.3.4.tgz", + "integrity": "sha1-SrTJoPWlTbkzi0w02Gv86PSzVXE=", + "requires": { + "duplexer": "0.1.1", + "from": "0.1.7", + "map-stream": "0.1.0", + "pause-stream": "0.0.11", + "split": "0.3.3", + "stream-combiner": "0.0.4", + "through": "2.3.8" + } + }, "eventemitter3": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-1.2.0.tgz", @@ -3045,9 +3063,9 @@ "dev": true }, "foundation-sites": { - "version": "6.4.4-rc1", - "resolved": "https://registry.npmjs.org/foundation-sites/-/foundation-sites-6.4.4-rc1.tgz", - "integrity": "sha512-26cL66QFNqMVwM7bmIEqq4jiW+6CkIeW719ci1pchdJ4UK0Om+3Jl7MhkX/lzdzRHB75f2m1IK9lxk3JGOwApA==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/foundation-sites/-/foundation-sites-6.3.1.tgz", + "integrity": "sha1-I4Uzct65SAwTtCZJnq8Mj5DNvh0=", "requires": { "jquery": "3.2.1", "what-input": "4.3.1" @@ -3059,6 +3077,11 @@ "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=", "dev": true }, + "from": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/from/-/from-0.1.7.tgz", + "integrity": "sha1-g8YK/Fi5xWmXAH7Rp2izqzA6RP4=" + }, "fs-readdir-recursive": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/fs-readdir-recursive/-/fs-readdir-recursive-1.1.0.tgz", @@ -5162,8 +5185,12 @@ "lodash": { "version": "4.17.4", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.4.tgz", - "integrity": "sha1-eCA6TRwyiuHYbcpkYONptX9AVa4=", - "dev": true + "integrity": "sha1-eCA6TRwyiuHYbcpkYONptX9AVa4=" + }, + "lodash.assign": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.assign/-/lodash.assign-4.2.0.tgz", + "integrity": "sha1-DZnzzNem0mHRm9rrkkUAXShYCOc=" }, "lodash.camelcase": { "version": "4.3.0", @@ -5257,6 +5284,11 @@ "integrity": "sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0=", "dev": true }, + "map-stream": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/map-stream/-/map-stream-0.1.0.tgz", + "integrity": "sha1-5WqpTEyAVaFkBKBnS3jyFffI4ZQ=" + }, "math-expression-evaluator": { "version": "1.2.17", "resolved": "https://registry.npmjs.org/math-expression-evaluator/-/math-expression-evaluator-1.2.17.tgz", @@ -5485,6 +5517,11 @@ "integrity": "sha1-5md4PZLonb00KBi1IwudYqZyrRg=", "dev": true }, + "mingo": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/mingo/-/mingo-1.3.3.tgz", + "integrity": "sha1-aSLE0Ufvx3GgFCWixMj3eER4xUY=" + }, "minimalistic-assert": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.0.tgz", @@ -5984,6 +6021,14 @@ } } }, + "pause-stream": { + "version": "0.0.11", + "resolved": "https://registry.npmjs.org/pause-stream/-/pause-stream-0.0.11.tgz", + "integrity": "sha1-/lo0sMvOErWqaitAPuLnO2AvFEU=", + "requires": { + "through": "2.3.8" + } + }, "pbkdf2": { "version": "3.0.14", "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.0.14.tgz", @@ -7196,6 +7241,17 @@ "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==", "dev": true }, + "save": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/save/-/save-2.3.2.tgz", + "integrity": "sha1-hZJnS1VlzE4SvG3dnLCfgo4+z30=", + "requires": { + "async": "2.6.0", + "event-stream": "3.3.4", + "lodash.assign": "4.2.0", + "mingo": "1.3.3" + } + }, "sax": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", @@ -7465,6 +7521,14 @@ "wbuf": "1.7.2" } }, + "split": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/split/-/split-0.3.3.tgz", + "integrity": "sha1-zQ7qXmOiEd//frDwkcQTPi0N0o8=", + "requires": { + "through": "2.3.8" + } + }, "sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", @@ -7487,6 +7551,14 @@ "readable-stream": "2.3.3" } }, + "stream-combiner": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/stream-combiner/-/stream-combiner-0.0.4.tgz", + "integrity": "sha1-TV5DPBhSYd3mI8o/RMWGvPXErRQ=", + "requires": { + "duplexer": "0.1.1" + } + }, "stream-http": { "version": "2.7.2", "resolved": "https://registry.npmjs.org/stream-http/-/stream-http-2.7.2.tgz", @@ -7669,8 +7741,7 @@ "through": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", - "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", - "dev": true + "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=" }, "thunky": { "version": "0.1.0", diff --git a/package.json b/package.json index 97144b128..acb88d1df 100644 --- a/package.json +++ b/package.json @@ -57,8 +57,9 @@ }, "dependencies": { "backbone": "^1.3.3", - "foundation-sites": "^6.4.4-rc1", + "foundation-sites": "^6.3.1", "jquery": "^3.2.1", + "save": "^2.3.2", "underscore": "^1.8.3" } } diff --git a/spec/models/movie_spec.js b/spec/models/movie_spec.js new file mode 100644 index 000000000..2fa9ef74e --- /dev/null +++ b/spec/models/movie_spec.js @@ -0,0 +1,28 @@ +import Movie from 'models/movie'; + +describe('Movie spec', () => { + + describe('validate', () => { + it ('requires a release_date', () => { + const invalidMovieNoReleaseDate = new Movie({ + title: 'Jaws', + }) + expect(invalidMovieNoReleaseDate.isValid()).toBeFalsy(); + }); + + it ('requires a title', () => { + const invalidMovieNoTitle = new Movie({ + release_date: '1975-06-18', + }) + expect(invalidMovieNoTitle.isValid()).toBeFalsy(); + }); + + it ('creates a movie if title and release date are provided', () => { + const validMovie = new Movie({ + title: 'Jaws', + release_date: '1975-06-18', + }); + expect(validMovie.isValid()).toBeTruthy(); + }); + }); +}); diff --git a/spec/models/search_spec.js b/spec/models/search_spec.js new file mode 100644 index 000000000..60b0afc6b --- /dev/null +++ b/spec/models/search_spec.js @@ -0,0 +1,28 @@ +import Search from 'models/search'; + +describe('Search spec', () => { + + describe('validate', () => { + it ('requires a release_date', () => { + const invalidSearchNoReleaseDate = new Search({ + title: 'Jaws', + }) + expect(invalidSearchNoReleaseDate.isValid()).toBeFalsy(); + }); + + it ('requires a title', () => { + const invalidSearchNoTitle = new Search({ + release_date: '1975-06-18', + }) + expect(invalidSearchNoTitle.isValid()).toBeFalsy(); + }); + + it ('creates a movie if title and release date are provided', () => { + const validSearch = new Search({ + title: 'Jaws', + release_date: '1975-06-18', + }); + expect(validSearch.isValid()).toBeTruthy(); + }); + }); +}); diff --git a/spec/test_spec.js b/spec/test_spec.js index 9252883f2..68b22b4af 100644 --- a/spec/test_spec.js +++ b/spec/test_spec.js @@ -1,13 +1,13 @@ - -describe('Sample spec', () => { - let a; - - it('and so is a spec', () => { - a = true; - - expect(a).toBe(true); - }); - xit('will not work', () => { - expect(false).toBe(true); - }); -}); +// +// describe('Sample spec', () => { +// let a; +// +// it('and so is a spec', () => { +// a = true; +// +// expect(a).toBe(true); +// }); +// xit('will not work', () => { +// expect(false).toBe(true); +// }); +// }); diff --git a/src/app.js b/src/app.js index 30c00d594..7fb5cb74f 100644 --- a/src/app.js +++ b/src/app.js @@ -6,9 +6,88 @@ import './css/styles.css'; import $ from 'jquery'; import _ from 'underscore'; +import Movie from './models/movie'; +import MovieView from './views/movie_view'; + +import MovieListView from './views/movie_list_view'; +import MovieList from './collections/movie_list'; + +import Search from './models/search'; +import SearchView from './views/search_view'; + +import SearchListView from './views/search_list_view'; +import SearchList from './collections/search_list'; + +const TABLE_HEADERS = [ 'title', 'release_date'] + +let movieTemplate; +let searchTemplate; + + // ready to go $(document).ready(function() { - $('#main-content').append('

Hello World!

'); + let bus = {}; + bus = _.extend(bus, Backbone.Events); + + movieTemplate = _.template($('#movie-template').html()); + searchTemplate = _.template($('#search-template').html()); + + // orderTemplate = _.template($('#order-template').html()); + + + const movies = new MovieList(); + const searches = new SearchList({ + // query: "", + }); + + + const movieListView = new MovieListView({ + el: '#movie-list', + model: movies, + template: movieTemplate, + bus: bus, + }); + + const searchListView = new SearchListView({ + el: '#movie-search', + model: searches, + template: searchTemplate, + bus: bus, + }); + + + const modalOpener= function modalOpener() { + // console.log(event) + console.log('opening modal') + $('.modal').removeClass('hide'); + $('#close').on('click', modalCloser) + } + + const modalCloser= function modalCloser() { + // console.log(event) + console.log('closing modal') + $('.modal').addClass('hide'); + $('.modal').addClass('hide'); + } + + $('#modalBtn').on('click', modalOpener); + + // const movieList = new MovieList() + // + // TABLE_HEADERS.forEach((field) => { + // const headerElement = $(`th.sort.${ field }`); + // headerElement.on('click', (event) => { + // console.log(`Sorting table by ${ field }`); + // movieList.comparator = field; + // movieList.sort(); + // }); + // }); + + // TODO: check fetch and rerendering + movies.fetch(); + // movieListView.render(); + + // $('#main-content').append('

Hello World!

'); }); diff --git a/src/collections/movie_list.js b/src/collections/movie_list.js new file mode 100644 index 000000000..951446602 --- /dev/null +++ b/src/collections/movie_list.js @@ -0,0 +1,21 @@ +import Backbone from 'backbone'; + +import Movie from '../models/movie'; + +const MovieList = Backbone.Collection.extend({ + model: Movie, + url: 'http://localhost:3000/movies', + comparator: 'title', + + validate(attributes) { + }, + + myWhere : function( key, val ){ + return this.filter( function( item ){ + return item.get( key ).toLowerCase() === val.toLowerCase(); + }); + }, +}); + + +export default MovieList; diff --git a/src/collections/search_list.js b/src/collections/search_list.js new file mode 100644 index 000000000..3d814f86a --- /dev/null +++ b/src/collections/search_list.js @@ -0,0 +1,34 @@ +import Backbone from 'backbone'; + +import Search from '../models/search'; + +const SearchList = Backbone.Collection.extend({ + model: Search, + url: 'http://localhost:3000/movies?query=', + comparator: 'title', + addTitle(title) { + return this.url += title + }, + resetUrl() { + this.url = 'http://localhost:3000/movies?query='; + }, + // if (this.get('query')) { + // `http://localhost:3000/movies?query=${this.get('query')}` + // else { + // 'http://localhost:3000/movies?query=' + // } + // }, + + // }') `http://localhost:3000/movies?query=${this.get('query')}`, + // // url: 'http://localhost:3000/movies', + // // urlR: function() { + // // return `http://localhost:3000/movies?query=${this.get('query')}` + // // }, + + validate(attributes) { + }, + +}); + + +export default SearchList; diff --git a/src/css/styles.css b/src/css/styles.css index 68a79a569..c588248dc 100644 --- a/src/css/styles.css +++ b/src/css/styles.css @@ -22,6 +22,33 @@ button.success { display: inline; } +button.black{ + background-color: black; +} + +label{ + font-size: 1.5em; +} + +.small{ + font-size: .75em; +} +img { + align-items: center; +} + +h4, p { + text-align: center; +} +td { + max-width: 200px; + +} + +button.black:hover { + background-color: grey; +} + aside.create-tasklist { background-color: navy; color: #FFFFFF; @@ -37,6 +64,51 @@ aside label { div { display: inline; } + +.red{ + color: red; +} +.white { + background-color: white +} +.green{ + color: green; +} + +#close { +margin-top: 10px; +background-color: black; +font-size: 2em +} + +#close.black:hover { + background-color: grey; +} + + +.modal { + position: fixed; + z-index: 100; + top: 0; + width: 100%; + height: 100%; + overflow: auto; + background-color: rgb(0,0,0); + background-color: rgba(0,0,0,0.4); + } + +.modal-content { + background-color: white; + margin: 15% auto; + padding: 20px; + border: 1px solid #888; + width: 80%; +}; + +/*button { + background-color: black; + font-size: 2em; +}*/ /* * { border-style: solid; diff --git a/src/models/movie.js b/src/models/movie.js new file mode 100644 index 000000000..9d26ee6da --- /dev/null +++ b/src/models/movie.js @@ -0,0 +1,30 @@ +import Backbone from 'backbone'; + +const Movie = Backbone.Model.extend({ + // urlRoot: function() { + // return `http://localhost:3000/movies?query=${this.get('query')}` + // }, + + validate(attributes) { + const errors = {}; + + if (!attributes.title) { + errors['title'] = ["Movie Title is required"]; + console.log(errors) + } + if (!attributes.release_date) { + errors['release date'] = ["Movie release date is required"]; + console.log(errors) + } + if ( Object.keys(errors).length > 0 ) { + return errors; + } else { + return false; + } + }, + + +}); + + +export default Movie; diff --git a/src/models/search.js b/src/models/search.js new file mode 100644 index 000000000..83c530545 --- /dev/null +++ b/src/models/search.js @@ -0,0 +1,30 @@ +import Backbone from 'backbone'; + +const Search = Backbone.Model.extend({ + // urlRoot: function() { + // return `http://localhost:3000/movies?query=${this.get('query')}` + // }, + + validate(attributes) { + const errors = {}; + + if (!attributes.title) { + errors['title'] = ["Movie Title is required"]; + console.log(errors) + } + if (!attributes.release_date) { + errors['release date'] = ["Movie release date is required"]; + console.log(errors) + } + + if ( Object.keys(errors).length > 0 ) { + return errors; + } else { + return false; + } + }, + +}); + + +export default Search; diff --git a/src/views/movie_list_view.js b/src/views/movie_list_view.js new file mode 100644 index 000000000..d6121f599 --- /dev/null +++ b/src/views/movie_list_view.js @@ -0,0 +1,111 @@ +import Backbone from 'backbone'; +import MovieView from '../views/movie_view' + +import Movie from '../models/movie'; + +const MovieListView = Backbone.View.extend({ + initialize(params) { + this.template = params.template; + this.bus = params.bus; + this.listenTo(this.model, 'update', this.render); + this.listenTo(this.bus, 'addMovieDB', this.addMovieDB) + }, + render(searchResults) { + this.$('#movie-list').empty(); + if (searchResults) { + searchResults.forEach((movie) => { + console.log('in Movie List View render'); + const movieView = new MovieView({ + model: movie, + template: this.template, + tagName: 'tr', + className: 'movie', + bus: this.bus, + }); + this.$('#movie-list').append(movieView.render().$el); + }); + } else { + this.model.sort(); + this.model.each((movie) => { + console.log('in Movie List View render'); + const movieView = new MovieView({ + model: movie, + template: this.template, + tagName: 'tr', + className: 'movie', + bus: this.bus, + }); + this.$('#movie-list').append(movieView.render().$el); + }); + // } + return this; + } + }, + + events: { + 'click button.btn-search': 'searchMovies', + 'click button.btn-showAll': 'render', + }, + + addMovieDB(movie_hash){ + const newMovie = new Movie(movie_hash) + this.model.add(newMovie); + + if (!newMovie.isValid()) { + // handleValidationFailuresTrip(trip.validationError); + return; + } + this.clearMessages('#movie-success-messages'); + this.clearMessages('#movie-fail-messages'); + + newMovie.save({}, { + success: (model, response) => { + console.log(this.model.attributes) + console.log(`Successfully added new movie: ${newMovie.get('title')}`); + let successMessage = `Successfully added new movie: ${newMovie.get('title')}`; + this.$('#movie-success-messages').append(successMessage); + this.$('#movie-success-messages').show(); + }, + error: (model, response) => { + console.log('Failed to save movie! Server response:'); + console.log(response); + this.model.remove(model); + }, + }); + }, + + clearMessages(tag){ + this.$(tag).html(''); + }, + + getFormData() { + console.log("I am reading the movie rental form") + const title = this.$('.movie-search-form input[name=title]').val(); + this.$('.movie-search-form input[name=title]').val('') + return title; + }, + + searchMovies() { + event.preventDefault(); + const query = this.getFormData() + const searchResults = this.model.myWhere('title', query); + console.log(searchResults.length == 0); + this.clearMessages('#movie-fail-messages') + this.clearMessages('#movie-success-messages'); + if (searchResults.length == 0){ + this.render() + // this.$('#movie-fail-messages').html(''); + if (query == ""){ + let failMessage = 'Search requires an inputed title' + this.$('#movie-fail-messages').append(failMessage); + }else { + let failMessage = `No movies found for title "${query}"`; + this.$('#movie-fail-messages').append(failMessage); + } + } else { + this.render(searchResults); + } + }, +}); + +export default MovieListView; diff --git a/src/views/movie_view.js b/src/views/movie_view.js new file mode 100644 index 000000000..d6a0ce92f --- /dev/null +++ b/src/views/movie_view.js @@ -0,0 +1,33 @@ +import Backbone from 'backbone'; +import Movie from '../models/movie'; + +const MovieView = Backbone.View.extend({ + + initialize(params) { + this.template = params.template; + // this.bus = params.bus; + this.listenTo(this.model, 'change', this.render); + }, + events: { + }, + + render() { + const compiledTemplate = this.template(this.model.toJSON()); + this.$el.html(compiledTemplate); + return this; + }, + + // events: { + // 'click td': 'showModal', + // }, + // + // showModal() { + // console.log(this.model) + // this.$('.modal').removeClass('hide') + // } + + + +}); + +export default MovieView; diff --git a/src/views/search_list_view.js b/src/views/search_list_view.js new file mode 100644 index 000000000..01787ccff --- /dev/null +++ b/src/views/search_list_view.js @@ -0,0 +1,87 @@ +import Backbone from 'backbone'; +import SearchView from '../views/search_view' +import SearchList from '../collections/search_list' + + +import Search from '../models/search'; + +const SearchListView = Backbone.View.extend({ + initialize(params) { + this.template = params.template; + this.listenTo(this.model, 'update', this.render); + this.bus = params.bus + }, + render() { + this.$('#search-list').empty(); + this.model.sort(); + this.model.each((search) => { + console.log('in Search List View render'); + const searchView = new SearchView({ + model: search, + template: this.template, + tagName: 'tr', + className: 'search', + bus: this.bus, + }); + this.$('#search-list').append(searchView.render().$el); + }); + return this; + }, + + events: { + 'click button.btn-search-api': 'searchApi', + }, + + getFormData() { + console.log("I am reading the form") + const formData = {}; + const title = this.$('.movie-entry-form input[name=title]').val(); + this.$('.movie-entry-form input[name=title]').val('') + // formData['title'] = title; + return title; + }, + + + searchApi() { + //this is an ugly way to reset the URL. See if we can fix it + this.clearMessages('#search-error-message'); + event.preventDefault(); + const title = this.getFormData() + if (title === ""){ + console.log('Error: No title') + this.$('#search-error-message').append('Error: No search terms') + return + } else { + // this.model.url += title; + this.model.addTitle(title); + // console.log(this.model.url); + let that = this + this.model.fetch().done(function () { + that.reportNoResults(title); + }); + + + //reset URL + this.model.resetUrl(); + // this.model.url = 'http://localhost:3000/movies?query='; + // console.log(this.model.length) + + //what if no models are returned? report error here? + } + }, + + reportNoResults(title){ + if(this.model.length == 0) { + // this.$('#search-error-message').empty() + this.clearMessages('#search-error-message'); + console.log(`No movies match search term: ${title}`) + this.$('#search-error-message').append(`No movies match search term: ${title}`) + } + }, + clearMessages(tag){ + this.$(tag).empty(); + }, + +}); + +export default SearchListView; diff --git a/src/views/search_view.js b/src/views/search_view.js new file mode 100644 index 000000000..54a785603 --- /dev/null +++ b/src/views/search_view.js @@ -0,0 +1,42 @@ +import Backbone from 'backbone'; +import Search from '../models/search'; + +const SearchView = Backbone.View.extend({ + + initialize(params) { + this.template = params.template; + this.listenTo(this.model, 'change', this.render); + this.bus = params.bus + }, + events: { + }, + + render() { + let search = this.model; + const compiledTemplate = this.template(this.model.toJSON()); + this.$el.html(compiledTemplate); + console.log(this.model.attributes) + + return this; + }, + + events: { + 'click .add': 'addMovie', + }, + + addMovie(){ + console.log('I am in add movie') + console.log(this.model) + let movie_hash = this.model.attributes + console.log(movie_hash) + this.bus.trigger('addMovieDB', movie_hash) + this.$('.add').addClass('hide'); + this.$('.nothing').removeClass('hide'); + } + + + + +}); + +export default SearchView;