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!
+
+
+ | Name |
+ Continent |
+ Category |
+ Weeks |
+ Cost |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Add a Trip
+
+
+
X
+
+
+
+
+
+
+
+
+
+
+
+
+
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;
+}