diff --git a/dist/index.html b/dist/index.html index b873b1e..f039345 100644 --- a/dist/index.html +++ b/dist/index.html @@ -1,20 +1,169 @@ + - - - My JavaScript App - - - -
- -
-
- -
- - - + + + My JavaScript App + + + +
+

Trek!

+
+ +
+
+ +

+ +

+
+
+
+
+
+
+

Trips!

+ + + + + + + + + + +
NameContinentCategoryWeeksCost
+
+
+ +
+ + +
+ + + + + + diff --git a/src/app.js b/src/app.js index e7af594..618826b 100644 --- a/src/app.js +++ b/src/app.js @@ -1,3 +1,8 @@ +// TODO: styles +// TODO: better error handling +// TODO: filtering +// TODO: showing sorting on user click with style feedback + // Vendor Modules import $ from 'jquery'; import _ from 'underscore'; @@ -6,8 +11,228 @@ import _ from 'underscore'; import './css/foundation.css'; import './css/style.css'; -console.log('it loaded!'); +// Our components +import TripList from './app/collections/trip_list'; +import Trip from './app/models/trip'; +import Reservation from './app/models/reservation'; + +const tripList = new TripList(); + +const TRIP_FIELDS = ["id", "about", "name", "continent", "category", "weeks", "cost"]; +const RES_FIELDS = ["trip_id","name","email"]; + +// variables for underscore templates +let tripTemplate; +let detailsTemplate; +let statusTemplate; + +// function to report client and server side user feedback for both trip and reservation forms +const reportStatus = function reportStatus(status, field, problem) { + console.log('in reportStatus function'); + // fail status + if (status === 'error') { + console.log('error'); + console.log(`Reporting ${ status } status: ${ field } problem: ${problem}`); + const errorSpanElement = $(`.form-${field}`) + errorSpanElement.html(''); + const generatedHTML = $(statusTemplate({'problem': `${problem}` })); + errorSpanElement.append(generatedHTML); + } else { + // success status + console.log('success'); + console.log(`Reporting ${ status } status: ${ field } `); + const messageHook = $('#success p') + messageHook.html(''); + const generatedHTML = `${field}` + messageHook.append(generatedHTML); + $('#success').css("display", "inline"); + } +}; + +// function to clear inline error handling messages in forms +const clearFormMessages = function clearFormMessages() { + $('#add-trip-form span').html('') + $('#reservation-form span').html('') +}; + +const handleValidationFailures = function handleValidationFailures(errors) { + clearFormMessages(); + console.log('in handleValidationFailures function'); + for (let field in errors) { + for (let problem of errors[field]) { + reportStatus('error', field, problem); + } + } +}; + +// function to add a trip +const addTripHandler = function(event) { + console.log('addTripHandler entered'); + event.preventDefault(); + + // client side validation + const trip = new Trip(readFormData(this.id)); + if (!trip.isValid()) { + console.log(`trip is not valid!`); + handleValidationFailures(trip.validationError); + return; + } + + tripList.add(trip); + + // server side validation + trip.save({}, { + success: (model, response) => { + console.log('Successfully saved Trip!'); + console.log('passed server side validation'); + // hide modal and clear form values + $('#myModal').hide(); + $(`#add-trip-form input`).val(''); + // show success status + reportStatus('success', 'Successfully saved trip!'); + }, + error: (model, response) => { + console.log('Failed to save trip! Server response:'); + console.log('Failed server side validation'); + + // remove bad trip from the list + tripList.remove(model); + console.log(response.responseJSON["errors"]); + // show inline errors in form + handleValidationFailures(response.responseJSON["errors"]); + }, + }); +}; + + +// function to read form data for both forms, just need to pass in form-id arg +const readFormData = function readFormData(formId){ + console.log('in readFromData function'); + const data = {}; + + // ternary operator to assign which field attributes variable should be used + let fields = formId.includes("trip") ? TRIP_FIELDS : RES_FIELDS + fields.forEach((field) => { + // select the input corresponding to the field we want + const inputElement = $(`#${formId} input[name="${ field }"]`); + const value = inputElement.val(); + + // Don't take empty strings, so that Backbone can fill in default values + if (value != '') { + data[field] = value; + } + }); + + return data; +}; + +const renderDetails = function renderDetails(trip){ + const detailsElement = $('#trip-details'); + // clear details section + detailsElement.html(''); + + // if we already have updated data from API don't make call otherwise fetch new data + if (trip.get('about')) { + const generatedHTML = $(detailsTemplate(trip.attributes)); + detailsElement.append(generatedHTML); + console.log(`my trip already has info`); + } else { + trip.fetch({ + success: (model) => { + const generatedHTML = $(detailsTemplate(trip.attributes)); + detailsElement.append(generatedHTML); + console.log(trip); + console.log(trip.attributes); + } + }); + } + // updates reservation from trip id value + $('form input#trip_id').attr('value' , `${trip.attributes.id}`); +}; + +const render = function render(tripList) { + // iterate through tripList, generate HTML for each model and attatch it to the DOM + const tripTableElement = $('#trip-list'); + + // clears the html so when we dynamically render again we get a new list vs just adding on + tripTableElement.html(''); + + tripList.forEach((trip) => { + const generatedHTML = $(tripTemplate(trip.attributes)); + generatedHTML.on('click', (event) => { + renderDetails(trip); + }); + tripTableElement.append(generatedHTML); + }); +}; + +// function to make a reservation +const addReservationHandler = function(event) { + console.log('In addReservationHandler function'); + event.preventDefault(); + clearFormMessages(); + const reservation = new Reservation(readFormData(this.id)) + + // client side validation + if (!reservation.isValid()) { + console.log(`reservation is not valid!`); + handleValidationFailures(reservation.validationError); + return; + } + + // server side validation + reservation.save({}, { + success: (model, response) => { + console.log('Successfully saved Reservation!'); + // console.log('passed server side validation'); + // clearn form values + $(`#clear-me input`).val(''); + reportStatus('success', 'Successfully saved reservation!'); + }, + error: (model, response) => { + console.log('Failed to save reservation! Server response:'); + // console.log('Failed server side validation'); + + // Server-side validations failed, so remove this bad trip from the list + console.log(response.responseJSON["errors"]); + handleValidationFailures(response.responseJSON["errors"]); + }, + }); +}; + + $(document).ready( () => { - $('main').html('

Hello World!

'); + let modal = $('#myModal'); + + // compiled underscore templates + detailsTemplate = _.template($('#details-template').html()); + tripTemplate = _.template($('#trip-template').html()); + statusTemplate = _.template($('#status-message-template').html()); + + // adding new models to a collection triggers an update event + tripList.on('update', render); + + // get all of the trips from the API + tripList.fetch(); + + // EVENTS + $('#add-trip-form').on('submit', addTripHandler); + $('#reservation-form').on('submit', addReservationHandler); + + // MODAL + // displays modal on button click + $('#add-trip-button').on('click', function() { + modal.css("display", "block"); + }); + + // hides modal on click for things with correct class + $('body').on('click', '.modal-close', function(event){ + if($(event.target).hasClass('modal-close')) { + modal.hide(); + $('#success').hide() + // TODO: make it so that when you close modal form resets if you havent finished + clearFormMessages(); + } + }); }); diff --git a/src/app/collections/trip_list.js b/src/app/collections/trip_list.js new file mode 100644 index 0000000..1098250 --- /dev/null +++ b/src/app/collections/trip_list.js @@ -0,0 +1,13 @@ +import Backbone from 'backbone'; +import Trip from '../models/trip'; + +const Triplist = Backbone.Collection.extend({ + model: Trip, + url: 'https://ada-backtrek-api.herokuapp.com/trips', + comparator: 'name', + parse: function(response){ + return response; + } +}); + +export default Triplist; diff --git a/src/app/models/reservation.js b/src/app/models/reservation.js new file mode 100644 index 0000000..ffd8e9c --- /dev/null +++ b/src/app/models/reservation.js @@ -0,0 +1,33 @@ +import Backbone from 'backbone'; + +const Reservation = Backbone.Model.extend({ + + // baseUrl: "https://ada-backtrek-api.herokuapp.com/trips/", + // tripIdAttribute: this.get('trip_id'), + //QUESTION: can I access trip_id in here and have it be differnt for each model? Or do I need to have a more complex function to set the url that takes an argument (the trip_id) and then I can set it for each instance of the model before I call .save()? + urlRoot() { + return `https://ada-backtrek-api.herokuapp.com/trips/${this.get('trip_id')}/reservations` + }, // urlRoot + + validate(attributes) { + const errors = {}; + + if (!attributes.name) { + errors.name = ['cannot be blank!']; + } + + if (!attributes.email) { + errors.email = ['cannot be blank!']; + } + + // if there are no errors + if (Object.keys(errors).length < 1) { + return false; + } + + // if there are errors + return errors; + } +}); + +export default Reservation; diff --git a/src/app/models/trip.js b/src/app/models/trip.js new file mode 100644 index 0000000..80d54a1 --- /dev/null +++ b/src/app/models/trip.js @@ -0,0 +1,59 @@ +import Backbone from 'backbone'; + +const Trip = Backbone.Model.extend({ + + validate(attributes) { + // const CONTINENTS = ['africa', 'antartica', 'asia', 'australasia', 'europe', 'north america', 'south america'] + + const errors = {}; + + if (!attributes.name) { + errors.name = ['cannot be blank!']; + } + + // if (!attributes.title) { + // errors.title = ['cannot be blank!']; + // } + // if (!CONTINENTS.includes(attributes.continent.toLowerCase())){ + // errors.continent = ["that isn't a continent"]; + // } + + if (!attributes.continent){ + errors.continent = ['cannot be blank']; + } + + if (!attributes.about) { + errors.about = ['cannot be blank']; + } + + if (!attributes.category) { + errors.category = ['cannot be blank']; + } + + if (!attributes.weeks) { + errors.weeks = ['cannot be blank']; + } + + if (attributes.weeks <= 0) { + errors.weeks = ['trips must be greater than one week in length']; + } + + if (!attributes.cost) { + errors.cost = ['cannot be blank']; + } + + if (attributes.cost <= 0) { + errors.cost = ['A trips cost must be greater than 0']; + } + + if (Object.keys(errors).length < 1) { + return false; + } + console.log('errors'); + console.log(errors); + return errors; + } +}); + + +export default Trip; diff --git a/src/css/style.css b/src/css/style.css index b7b2d44..93729e5 100644 --- a/src/css/style.css +++ b/src/css/style.css @@ -1,2 +1,68 @@ - /* Your styles go here! */ +/****************/ +/* Whole doc */ +/****************/ + +#right{ + background-color: lightblue; +} +#left{ + background-color: lightyellow; + overflow: auto; +} + +#left .table { + max-width: 50vw; + +} + +.errorForm{ + color: red; +} +/*************/ +/* Modal */ +/*************/ +/* The Modal (background) */ +.modal { + display: none; /* Hidden by default */ + position: fixed; /* Stay in place */ + z-index: 1; /* Sit on top */ + padding-top: 10vh; /* Location of the box */ + 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 */ +.modal-content { + background-color: #fefefe; + margin: auto; + padding: 20px; + border: 1px solid #888; + width: 60vw; + border: solid var(--yellow1) 5px; +} + +/* The Close Button */ +#close { + font-weight: bold; + border: solid 4px red; + border-radius: 50%; +} + +.close:hover, +.close:focus { + color: #000; + text-decoration: none; + cursor: pointer; +} + +/* success message */ +#success { + display: none; +}