diff --git a/favicon.ico b/dist/favicon.ico similarity index 100% rename from favicon.ico rename to dist/favicon.ico diff --git a/dist/index.html b/dist/index.html index b873b1e..98f727b 100644 --- a/dist/index.html +++ b/dist/index.html @@ -3,18 +3,165 @@ My JavaScript App + + +
- +

BackTREK

+ + + ▶ +   Add Trip + +
+ + + +
+ +
+
+
+ + + + + + + + + + + +
NameContinentCategoryWeeksCost
+
+
+
+ +
+
+ + +
+ + + +
+ + + + + + + + + + diff --git a/src/app.js b/src/app.js index e7af594..70c111d 100644 --- a/src/app.js +++ b/src/app.js @@ -6,8 +6,236 @@ import _ from 'underscore'; import './css/foundation.css'; import './css/style.css'; +import TripList from './app/collections/trip_list'; +import Trip from './app/models/trip'; +import Reservation from './app/models/reservation'; + console.log('it loaded!'); +const tripList = new TripList() +// const trip = new Trip() +// const res = new Reservation() + +const TRIP_FIELDS = ['name', 'category', 'continent', 'weeks', 'cost', 'about']; +const RES_FIELDS = ['name', 'email', 'tripId']; + + +let tripsTemplate; +let tripDetailTemplate; + +// Clear status messages +const clearStatus = function clearStatus() { + $('#status-messages ul').html(''); + $('#status-messages').hide(); +}; + +// Add a new status message +const reportStatus = function reportStatus(status, message) { + console.log(`Reporting ${ status } status: ${ message }`); + + // Should probably use an Underscore template here. + const statusHTML = `
  • ${ message }
  • `; + + // note the symetry with clearStatus() + $('#status-messages ul').append(statusHTML); + $('#status-messages').show(); +}; + +const render = function render(tripList) { + const $tripsElement = $('#trip-list'); + $tripsElement.html(''); + + let tripId = $(this).attr('data-id'); + let trip = tripList.get(tripId) + + tripList.forEach((trip) => { + const generatedHTML = $(tripsTemplate(trip.attributes)); + generatedHTML.on('click', (event) => { + console.log('testing click trip detail'); + $('#trips').hide() + $('#trip').show() + // $('#add-res').show() + // due to asynchonous api responses we put the renderTrip into the .fetch() + trip.fetch({ + success(model, response) { + renderTrip(model); + } + }); + + }) + $tripsElement.append(generatedHTML) + }); +}; + + +const renderTrip = function renderTrip(trip) { + const $tripElement = $('#trip'); + $tripElement.html(''); + console.log(trip.attributes); + // console.log(trip.attributes.about); + + const generatedDetailHTML = tripDetailTemplate(trip.attributes); + $tripElement.html(generatedDetailHTML) + + $('#add-res').on('submit', addResHandler); + + // Provide visual feedback for sorting + $('th.sort').removeClass('current-sort-field'); + $(`th.sort.${ tripList.comparator }`).addClass('current-sort-field'); +} + +const loadTrips = function loadTrips() { + tripList.fetch({ + success(model, response) { + render(model); + } + }); +}; + +const readTripFormData = function readTripFormData() { + const tripData = {}; + TRIP_FIELDS.forEach((field) => { + // select the input corresponding to the field we want + const $inputElement = $(`#add-trip-form input[name="${ field }"]`); + const value = $inputElement.val(); + + // Don't take empty strings, so that Backbone can + // fill in default values + if (value != '') { + tripData[field] = value; + } + + $inputElement.val(''); + }); + + console.log("Read trip data"); + console.log(tripData); + + return tripData; +}; + +const readResFormData = function readResFormData() { + const resData = {}; + RES_FIELDS.forEach((field) => { + const $inputResElement = $(`#add-res input[name="${ field }"]`); + const value = $inputResElement.val(); + + if (value != '') { + resData[field] = value; + } + + $inputResElement.val(''); + }); + + + console.log("Read res data"); + console.log(resData); + + return resData; +}; + +const handleValidationFailures = function handleValidationFailures(errors) { + // Since these errors come from a Rails server, the strucutre of our + // error handling looks very similar to what we did in Rails. + for (let field in errors) { + for (let problem of errors[field]) { + reportStatus('error', `${field}: ${problem}`); + } + } +}; + + +const addTripHandler = function(event) { + event.preventDefault(); + console.log('addTripHandler test click'); + + const trip = new Trip(readTripFormData()); + + if (!trip.isValid()) { + handleValidationFailures(trip.validationError); + return; + } + + tripList.add(trip); + + // The first argument to .save is the attributes to save. + // If we leave it blank, it will save all the attributes! + // (Think of model.update in Rails, where it updates the + // model and saves to the DB in one step). We need the second + // argument for callbacks, so we pass in {} for the first. + trip.save({}, { + success: (model, response) => { + console.log('Successfully saved trip!'); + reportStatus('success', 'Successfully saved trip!'); + }, + error: (model, response) => { + console.log('Failed to save trip! Server response:'); + console.log(response); + + // Server-side validations failed, so remove this bad + // trip from the list + tripList.remove(model); + + handleValidationFailures(response.responseJSON["errors"]); + }, + }); +}; + +const addResHandler = function addResHandler(event) { + + event.preventDefault(); + console.log('click into addResHandler'); + + const res = new Reservation(readResFormData()); + + if (!res.isValid()) { + handleValidationFailures(res.validationError); + return; + } + + res.save({}, { + success: (model, response) => { + console.log('Successfully saved res'); + reportStatus('success', 'Successfully saved reservation!'); + }, + error: (model, response) => { + console.log('Failed to save res. Server response: '); + console.log(response); + + handleValidationFailures(response.responseJSON["errors"]); + } + }) +} + +// Jquery event handling $(document).ready( () => { - $('main').html('

    Hello World!

    '); + tripsTemplate = _.template($('#trip-template').html()); + tripDetailTemplate = _.template($('#trip-detail-template').html()); + + $('#trips').hide(); + // $('#add-res').hide(); + + // $('#trip').on('click', renderTrip); + + $('#load-trips').on('click', function (){ + $('#trips').show(); + loadTrips() + }); + + $('#add-trip-form').on('submit', addTripHandler); + + + tripList.on('sort', render); + + TRIP_FIELDS.forEach((field) => { + const headerElement = $(`th.sort.${ field }`); + headerElement.on('click', (event) => { + console.log(`Sorting table by ${ field }`); + tripList.comparator = field; + tripList.sort(); + }); + }); + + $('#status-messages button.clear').on('click', clearStatus); + }); diff --git a/src/app/collections/trip_list.js b/src/app/collections/trip_list.js new file mode 100644 index 0000000..f8f3b9c --- /dev/null +++ b/src/app/collections/trip_list.js @@ -0,0 +1,15 @@ +import Backbone from 'backbone'; +import Trip from '../models/trip'; + +const TripList = Backbone.Collection.extend({ + model: Trip, + url: 'https://ada-backtrek-api.herokuapp.com/trips', + + parse: function (response) { + return response; + }, + comparator: 'name', + +}); + +export default TripList; diff --git a/src/app/models/reservation.js b/src/app/models/reservation.js new file mode 100644 index 0000000..adad23d --- /dev/null +++ b/src/app/models/reservation.js @@ -0,0 +1,29 @@ +import Backbone from 'backbone'; + +const Reservation = Backbone.Model.extend({ + model: Reservation, + + urlRoot: function () { + console.log(this); + const tripId = this.get('tripId'); + return `https://ada-backtrek-api.herokuapp.com/trips/${ tripId }/reservations`; + }, + + validate(attributes) { + const errors = {}; + if (!attributes.name) { + errors.name = ['cannot be blank']; + } + + if (!attributes.email) { + errors.weeks = ['cannot be blank']; + } + + if (Object.keys(errors).length < 1) { + return false; + } + 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..f8dc180 --- /dev/null +++ b/src/app/models/trip.js @@ -0,0 +1,39 @@ +import Backbone from 'backbone'; +import _ from 'underscore'; +import Reservation from './reservation' + +const Trip = Backbone.Model.extend({ + model: Trip, + urlRoot: 'https://ada-backtrek-api.herokuapp.com/trips/', + + + + validate(attributes) { + const errors = {}; + if (!attributes.name) { + errors.name = ['cannot be blank']; + } + + if (!attributes.weeks) { + errors.weeks = ['cannot be blank']; + } + + if (!attributes.cost) { + errors.cost = ['cannot be blank']; + } + + if (Object.keys(errors).length < 1) { + return false; + } + return errors; + }, + + defaults: { + category: 'None provided', + continent: 'None provided', + about: 'None provided', + + }, +}); + +export default Trip; diff --git a/src/css/style.css b/src/css/style.css index b7b2d44..c4c5aa6 100644 --- a/src/css/style.css +++ b/src/css/style.css @@ -1,2 +1,91 @@ /* Your styles go here! */ +/* Styles for error section at the top */ +#status-messages { + background-color: #dfdfdf; + + /* Hide by default */ + display: none; + + border-radius: 1rem; + margin: 1rem; + height: 100%; + padding: .8rem; +} + +#status-messages ul { + list-style: none; + margin-left: 0; +} + +#status-messages button.clear { + margin-top: .2rem; + height: 100%; +} + +#status-messages button.clear img { + width: 2rem; +} + +#status-messages .error { + color: red; +} + +#status-messages .success { + color: green; +} + +th.sort { + cursor: pointer; +} + +.current-sort-field { + background-color: darkgrey; + color: white; +} + +header { + background-color: #4285f4; + color: white; + padding: 40px; +} + +header h1, section .button { + margin-left: 20px; +} + +a { + color: #ff8008; +} + +#load-trips { + margin-top: 20px; + background-color: white; + color: #1a0dab; + font-size: 20px; +} + + +#load-trips:hover { + background-color: white; + color: #1a0dab; + text-decoration: underline; +} + +#trips { + margin-left: 20px; + overflow-x: scroll; +} + +#trips ul{ + list-style-type: none; + font-size: 18px; +} + +div #trip { + margin: 20px; +} + +.bold { + font-weight: bold; +} diff --git a/webpack.config.js b/webpack.config.js index 1f901ae..e7595b9 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -16,7 +16,7 @@ module.exports = { path: path.resolve(__dirname, 'dist'), }, plugins: [ - new CleanWebpackPlugin(['dist'], { exclude: ['index.html'] }), + new CleanWebpackPlugin(['dist'], { exclude: ['index.html', 'favicon.ico'] }), new webpack.NamedModulesPlugin(), new webpack.HotModuleReplacementPlugin() ],