diff --git a/README.md b/README.md index 9dc1aa8df..84179b59e 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,9 @@ # Video Store Consumer -In this project, you'll take a lot of the knowledge you have so far and create an end-to-end Video Store application. The final product will be something that a rental store employee might use to manage the video store where they work. +Class project for Ada Developers Academy +Created an end-to-end Video Store application using Backbone, Javascript, and jQuery -This is a [Phase 3](https://github.com/Ada-Developers-Academy/pedagogy/blob/master/rule-of-three.md) Pair project. - -## Learning Goals +## Goals - Leverage jQuery event observation along with Backbone to enable a dynamic user interface - Organize browser interactions according to Backbone's MVC pattern - Revisit Rails API functionality @@ -19,76 +18,43 @@ Our rental store employees want to be able to manage their rental inventory. The ## Project Information This project will be utilizing an external API within an API! Whoa! Your front-end implementation will be interacting with a **Rails API** that you will be modifying. The Rails API wraps an **external API** which contains many endpoints related to movies. The external API is [The Movie DB](https://www.themoviedb.org/documentation/api). -## Project Setup -### 1. Backbone Part - - One person shall fork this repository - - Add the pair as a collaborator on the project - - Both people shall clone the repository - - Install dependencies using `$npm install` - - Start the development server using `$npm start` - -### 2. API Configuration - - Follow the instructions on the API's [Getting Started](https://developers.themoviedb.org/3/getting-started) page to set up your account and request an API Key. - (You can use Ada's address in the registration process) - - Read through the documentation for the API. A few endpoints you may want to explore include: - - [Search Movies](https://developers.themoviedb.org/3/search/search-movies) - - [Get Movie Details](https://developers.themoviedb.org/3/movies/get-movie-details) - - [Configuration](https://developers.themoviedb.org/3/configuration/get-api-configuration) - -### 3. Rails Part - - One person shall fork the API repository. You can find it at your cohort's GitHub org, and the project's name is VideoStoreConsumer-API. You should immediately notice that this is a Rails project not a Backbone project. - - Add the pair as a collaborator on the project - - Both people shall clone the repository - - Set up the `.env` file with the API key from step #2 - - Set up the DB - - Ensure both people can run the API locally - - -## Project Requirements - -### Front-End -- Rental store employee should be able to: - - search all Movies - - add a movie to the rental library - - list all movies in the rental library - -### Back-End -- The search functionality is already implemented! -- Add support for adding an external movie to the rental library - - - -## Project Design -### Front-End -- Consider what Backbone models, collections and views you will need to utilize -- Consider what events you will want to handle -- Consider how you will make requests from the front-end to the API - -### Back-End -- Investigate the existing implementation to gain an understanding of what you are working with -- Consider what data the back-end will require to complete the tasks in the requirements - -## Optional Enhancements -Some of these optional requirements require work in the front-end only, back-end only or both. - -- Rental! - - Check out: Select a movie along with a customer to create a rental - - Check in: Select a customer to see the movies they have checked out. Select a single (or multiple) rentals to check back in - - See all overdue rentals -- CRUD customers -- Introduce scrolling pagination to dynamically load more movies when scrolling to the bottom (research!!) - -## Resources - -Note, you may need to use non restful API calls for rentals and this can be done by overriding Backbone.sync. Below are some resources on how to do so. - -- [Overriding Backbone.sync for non-RESTful APIs](https://thejsguy.com/2015/03/18/overriding-backbone-sync.html) -- [Non-RESTful backend with Backbonejs](https://stackoverflow.com/questions/24770250/non-restful-backend-with-backbone-js) +## Local Setup +### 1. Clone the repository + +### 2. Run Back End + - Follow the instructions on the API's [Getting Started](https://developers.themoviedb.org/3/getting-started) page to set up your account and request an API Key. + - Set up the `.env` file with the API key in VideoStoreConsumer-API/ + + **Example .env file:** + + ``` + MOVIEDB_KEY = + + SECRET_KEY_BASE = + + ``` + + + - Set up rails back end: + While in VideoStoreConsumer-API/ in your terminal Run: + _Note: You must have Rails installed._ + + ``` + bundle install + + rails server + ``` + + Your server should be running at this point + +### 3. Run Front End + - Run Front End Server: + Change directories to VideoStoreConsumer/ + It will be the directory with the json.lock file + In your terminal run: + + ``` + npm install + + npm start + ``` diff --git a/dist/index.html b/dist/index.html index 559b18ecd..a7550dd85 100644 --- a/dist/index.html +++ b/dist/index.html @@ -1,15 +1,93 @@ - - - Backbone Baseline - - -
- + + + Backbone Baseline + + + +
+
+ + +
+ + + + + + + + + +
+
+ +
+
+
+
+
+ +
+
+ + + + - - + + diff --git a/src/app.js b/src/app.js index 30c00d594..40abb2855 100644 --- a/src/app.js +++ b/src/app.js @@ -5,10 +5,32 @@ import './css/styles.css'; // Import jQuery & Underscore import $ from 'jquery'; import _ from 'underscore'; +import MovieList from './collections/movie_list'; +import Movie from './models/movie'; +import MovieListView from './views/movie_list_view'; +import MovieView from './views/movie_view'; +let movieTemplate; // ready to go +const movieList = new MovieList(); + + $(document).ready(function() { + movieTemplate = _.template($('#movie-template').html()); + $('.view-library').on( "click", function() { + event.preventDefault(); + movieList.fetch(); + }); + + + const movieListView = new MovieListView({ + el:'main', + model: movieList, + template: movieTemplate, + }); - $('#main-content').append('

Hello World!

'); + movieList.fetch({ + success: function(collection, response){} + }); }); diff --git a/src/collections/movie_list.js b/src/collections/movie_list.js new file mode 100644 index 000000000..4b71decbb --- /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', + + //TODO: put in correct comparator // I don't think we need this + comparator: 'title', + + fetchSearch: function (query, options) { + options = options || {}; + + options.data = {query: query['query']} + + return this.fetch(options); + + }, +}); + +export default MovieList; diff --git a/src/css/styles.css b/src/css/styles.css index 68a79a569..9f91625e2 100644 --- a/src/css/styles.css +++ b/src/css/styles.css @@ -1,12 +1,44 @@ @include foundation-everything; +@import url('https://fonts.googleapis.com/css?family=Bungee|Gudea|Volkhov'); +.hero { + background: url("https://images.unsplash.com/photo-1501141178950-e7fa06e4adf5?auto=format&fit=crop&w=1050&q=80") + no-repeat; + background-size: cover; + height: 300px; + width: 100%; + /* background-color: black; */ + margin: 0; + color: white; + /* box-sizing: content-box; */ + padding: 1% 5% 2% 5%; + text-align: center; + /* opacity: 1.2; */ + /* background-color: black; */ +} +.hero h1{ + font-family: 'Bungee', cursive; + font-size: 96px; + position: relative; + top: 50%; + transform: translateY(-50%); + text-shadow: 5px 5px lightgray; + /* opacity: 1; */ +} +p, div{ + font-family: 'Gudea', sans-serif; +} +h2, h3, h4, h5, h6{ + font-family: 'Volkhov', serif; +} main { - background: lightblue; + background: white; + margin-left: 2%; + margin-right: 2%; } header { - background-color: lightgreen; - padding: 0.5rem; + /* padding: 0.5rem; */ } #completed-checkbox { @@ -24,10 +56,10 @@ button.success { aside.create-tasklist { background-color: navy; - color: #FFFFFF; + /* color: #FFFFFF; */ } aside label { - color: #FFFFFF; + /* color: #FFFFFF; */ } .completed { @@ -37,8 +69,106 @@ aside label { div { display: inline; } + +/* #myModal { +width: 500px; +background-color: red; +} */ +.modal { + position: fixed; /* Stay in place */ + z-index: 1; /* Sit on top */ + left: 0; + top: 0; + width: 100%; /* Full width */ + height: 100%; /* Full height */ + overflow: auto; /* Enable scroll if needed */ + background-color: rgb(0,0,0); /* Fallback color */ + background-color: rgba(0,0,0,0.4); /* Black w/ opacity */ + +} + +/* Modal Content/Box */ +.modal-content { + background-color: #fefefe; + margin: 15% auto; /* 15% from the top and centered */ + padding: 20px; + border: 1px solid #888; + width: 80%; /* Could be more or less, depending on screen size */ + display: flex; +} +.show{ + display:block; +} + +.hidden{ + display: none; +} + +.button.close { + display: block; + width: 5%; + height: 2%; + /* float: right; */ + align-content: right; + /* margin-left: 75%; */ + background-color: lightblue; + margin-right: 15%; + font-size: 30%; +} + +p.display-status { + max-width: 500px; + width: 100%; + display: block; +} +nav.top-bar{ + background-color: black; +} +nav button.button { + background-color: #FF47FA; + font-size: 20px; + font-weight: bold; + margin: 1%; +} + +nav button.button:hover { +text-shadow: 1px 1px lightgray; +background-color: #771FD4; +} + +#search-form { + /* width: 80%; */ + /* margin: 1% 5% 1% 5%; */ + align-self: center; + padding: 1%; + /* background-color: black; */ +} + +#movie-field { + width: 35%; + /* padding-left: 2%; */ + margin-left: 1%; + margin-right: 2%; + /* background-color: #FF47FA; */ + color: black; + font-weight: bold; + display: inline-block; +} + +input.movie-filter { + background-color: white; + height: 30px; + display: inline-block; + background-color: #FF47FA; + font-weight: bold; +} + +#movie-list { + display: inline-block; + /* max-width: 40%; */ +} /* * { - border-style: solid; +border-style: solid; } */ diff --git a/src/models/movie.js b/src/models/movie.js new file mode 100644 index 000000000..5269b69d8 --- /dev/null +++ b/src/models/movie.js @@ -0,0 +1,68 @@ +import Backbone from 'backbone'; + +const Movie = Backbone.Model.extend({ + urlRoot: 'http://localhost:3000/movies', + defaults: { + + }, + initialize(attributes) { + }, + validate(attributes) { + + const errors = {}; + const title = this.get('title'); + const releaseDate = this.get('release_date'); + + console.log(title); + console.log(`release date is ${releaseDate}`); + + if (!title) { + console.log("Cannot add a movie without a name"); + errors['title'] = ["Cannot add a movie without a name"]; + } + + if (!releaseDate) { + errors['releaseDate'] = ["Cannot add a move that does not have a release date"]; + } + + if ( Object.keys(errors).length > 0 ) { + return errors; + } else { + return false; + } + }, + + add(newMovie) { + if(!newMovie.isValid()){ + $('.display-status').html('') + $('.display-status').html(`${newMovie.errors}`); + // modalDisplay(); + } else { + newMovie.save( {}, { + success: (model, response) => { + const movieSuccess = `successfully added ${this.get('title')}!`; + console.log(movieSuccess); + $('.display-status').html(''); + console.log(response); + $('.display-status').html(movieSuccess); + // $('#add-trip-form').remove(); + // modalDisplay(); + // reportStatus('success', 'Successfully added reservation!'); + }, + error: (model, response) => { + const movieFailure = 'Failed to save movie! Server response:'; + // console.log(`validationError ${response.attributes['validationError']}`); + $('.display-status').html('') + console.log(response.errors); + $('.display-status').html(movieFailure); + + // modalDisplay(); + }, + }); + } + }, + +}); + + +export default Movie; diff --git a/src/views/movie_list_view.js b/src/views/movie_list_view.js new file mode 100644 index 000000000..26dab29f6 --- /dev/null +++ b/src/views/movie_list_view.js @@ -0,0 +1,70 @@ +import Backbone from 'backbone'; +import MovieList from '../collections/movie_list'; +import Movie from '../models/movie'; +import MovieView from './movie_view'; + + +const MovieListView = Backbone.View.extend({ + initialize(params){ + + this.template = params.template; + this.listenTo(this.model, 'update', this.render); + + }, + events:{ + 'click input.movie-filter': 'searchMovies', + // When the user clicks on (x), close the modal + 'click span': 'modalHide', + 'click window': 'modalHide', + }, + render(params){ + event.preventDefault(); + const movieListView = new MovieListView({ + el: '#movie-list', + template: this.movieTemplate, + }); + this.$('#movie-list').empty(); + this.model.each((movie)=>{ + + const movieView = new MovieView({ + model: movie, + template: this.template, + tagName: 'div', + className: 'movie', + }); + this.listenTo(movieView, 'show_modal', this.modalDisplay); + this.listenTo(movieView, 'show_movie', this.resetModel); + this.$('#movie-list').append(movieView.render().$el); + + + + }); + return this; + }, + resetModel(movie){ + this.model.set([movie]) + }, + searchMovies(event){ + event.preventDefault(); + console.log('in search movies'); + this.model.fetchSearch({ + query: this.$('#movie-field').val(), + success: function(query, response){console.log(response);}, + }); + }, + + modalDisplay() { + this.$('.modal').addClass('show'); + this.$('.modal').removeClass('hidden'); + console.log('changed modal display to block') + }, + + modalHide() { + this.$('.modal').addClass('hidden'); + this.$('.modal').removeClass('show'); + console.log('changed modal display to hidden') + }, + +}); + +export default MovieListView; diff --git a/src/views/movie_view.js b/src/views/movie_view.js new file mode 100644 index 000000000..0c700bb18 --- /dev/null +++ b/src/views/movie_view.js @@ -0,0 +1,54 @@ +import Backbone from 'backbone'; +import Movie from '../models/movie'; +import MovieList from '../collections/movie_list' + + +const MovieView = Backbone.View.extend({ + initialize(params){ + this.template = params.template; + this.listenTo(this.model, 'change', this.render) + }, + render(){ + const compiledTemplate = this.template(this.model.toJSON()); + this.$el.html(compiledTemplate); + return this; + }, + events: { + 'click button.btn-add': 'add', + 'click button.btn-show': 'showSingle' + }, + showSingle(){ + event.preventDefault(); + let tempId = this.model.attributes.id; + this.model.set('id', this.model.attributes.title); + let tempThis = this; + this.model.fetch({ + success: function(option, response){ + console.log(response); + let newMovie = new Movie(response); + tempThis.trigger('show_movie', newMovie); + console.log('triggering movie reset'); + console.log('response'); + }, + }); + this.model.set('id', tempId); + + }, + add(event){ + event.preventDefault(); + + let newMovieObject = this.model.attributes; + + console.log(newMovieObject); + let newMovie = new Movie(newMovieObject); + console.log(newMovie); + // debugger + this.model.add(newMovie); + this.trigger('show_modal'); + }, + show(event) { + + }, +}); + +export default MovieView;