diff --git a/dist/index.html b/dist/index.html index b873b1e..e5dc284 100644 --- a/dist/index.html +++ b/dist/index.html @@ -2,19 +2,116 @@ - My JavaScript App + Trek! + + +
- +

Trek!

+

Adventure awaits...

+ +
+
+ +
+
+ + +
+ + + + + + + + + + + +
Trips
NameContinentWeeksCategoryCost
+
+
+ + + diff --git a/images/crater_lake.jpg b/images/crater_lake.jpg new file mode 100644 index 0000000..5a6d291 Binary files /dev/null and b/images/crater_lake.jpg differ diff --git a/src/app.js b/src/app.js index e7af594..e38148a 100644 --- a/src/app.js +++ b/src/app.js @@ -6,8 +6,201 @@ import _ from 'underscore'; import './css/foundation.css'; import './css/style.css'; +import Trip from './app/models/trip'; +import TripList from './app/collections/trip_list'; + +const TRIP_FIELDS = ['name', 'continent', 'category', 'weeks', 'cost']; + +let tripList; +let tripTemplate; +let tripDetailsTemplate; + console.log('it loaded!'); +const render = function render(tripList) { + const tripListElement = $('#trip-list'); + tripListElement.empty(); + + tripList.forEach((trip) => { + tripListElement.append(tripTemplate(trip.attributes)); + }); +}; + +const failLoad = function failLoad(model, response) { + updateStatusMessageFrom(response.responseJSON.errors); +}; + +const loadTrips = function loadTrips() { + tripList.fetch(); + tripList.on('update', render, tripList); + $('#trips').show(); +}; + +const sortTrips = function sortTrips(event) { + $('.current-sort-field').removeClass('current-sort-field'); + $(this).addClass('current-sort-field'); + + const classes = $(this).attr('class').split(/\s+/); + + classes.forEach((className) => { + if (TRIP_FIELDS.includes(className)) { + if (className === tripList.comparator) { + tripList.models.reverse(); + tripList.trigger('sort', tripList); + } else { + tripList.comparator = className; + tripList.sort(); + } + } + }); +}; + +const addTrip = function addTrip(event) { + event.preventDefault(); + + const tripData = readForm(); + const trip = new Trip(tripData); + if (trip.isValid()) { + trip.save({}, { + success: successfulSave, + error: failedSave, + }); + } else { + updateStatusMessageFrom(trip.validationError); + } +}; + +const readForm = function readForm() { + const tripData = {}; + TRIP_FIELDS.forEach((field) => { + const inputElement = $(`#new-trip input[name="${ field }"]`); + tripData[field] = inputElement.val(); + }); + return tripData; +}; + +const clearForm = function clearForm() { + $('#new-trip input[name]').val(''); +}; + +const clearFilter = function clearFilter() { + $('#filters input').val(''); + $('#filters option').prop('selected', function() { + return this.defaultSelected; + }); +}; + +const updateStatusMessageFrom = (messageHash) => { + $('span.error').empty(); + for (let messageType in messageHash) { + messageHash[messageType].forEach((message) => { + $(`#new-trip label[for="${ messageType }"]`).append(` ${ message }`); + }); + } +}; + +const updateStatusMessagesWith = (message) => { + $('#status-messages ul').empty(); + $('#status-messages ul').append(`
  • ${ message }
  • `); + $('#status-messages').show(); +}; + +const successfulSave = function(trip, response) { + updateStatusMessagesWith(`${trip.get('name')} added!`) + clearForm(); + $('#add-trip').css("display", "none"); + tripList.fetch(); +}; + +const failedSave = function(trip, response) { + updateStatusMessageFrom(response.responseJSON.errors); + trip.destroy(); +}; + +const loadTrip = function loadTrip(trip) { + $('#trip').empty(); + $('#trip').addClass('green-border'); + $('#trip').append(tripDetailsTemplate(trip.attributes)); + $('#reservation').hide(); + $('#trip').on('click', 'button', function() { + $('#reservation').slideToggle(); + }); + + $('#reservation').submit(function(e) { + e.preventDefault(); + + const url = $(this).attr('action') + const formData = $(this).serialize(); + + if ($('#cust-name').val() === '' || $('#email').val() === '') { + $('#reservation span').remove(); + $('#reservation').slideToggle(); + if ($('#cust-name').val() === '') { + console.log('appending name error'); + console.log($('#name').val()); + $(`#reservation label[for="name"]`).append(` Cannot be blank`); + } + if ($('#email').val() === '') { + console.log('appending email error'); + $(`#reservation label[for="email"]`).append(` Cannot be blank`); + } + console.log('before hide'); + } else { + $.post(url, formData, (response) => { + $('#message').html('

    Spot reserved!

    '); + $('#reservation').hide(); + console.log('after hide'); + clearForm(); + }).fail(() => { + $('#message').html('

    Reservation failed to save.

    '); + }); + } + }); +}; + $(document).ready( () => { - $('main').html('

    Hello World!

    '); + $('#trips').hide(); + clearFilter(); + tripTemplate = _.template($('#trip-template').html()); + tripDetailsTemplate = _.template($('#trip-details-template').html()); + + tripList = new TripList(); + + tripList.on('update', render, tripList); + tripList.on('sort', render, tripList); + + $('button#search').on('click', loadTrips); + $('#filters input').on('keyup', function() { + const query = $(this).val().toLowerCase(); + const filterCategory = $('#filters select').val().toLowerCase(); + const filteredList = tripList.filterBy(filterCategory, query); + if (filteredList.length === 0) { + $('#trips').append('

    No trips match your search

    '); + } else { + render(filteredList); + } + }); + + $('.sort').click(sortTrips); + $('#new-trip').on('submit', addTrip); + + $('#trip-list').on('click', 'tr', function() { + const trip = tripList.get($(this).attr('data-id')); + trip.fetch({ + success: loadTrip, + error: failLoad, + }); + }); + + $('button#add').click(function() { + $('#add-trip').css("display", "block"); + }); + $('span.close').click(function() { + $('#add-trip').css("display", "none"); + }); + $('body').click(function(event) { + if (event.target === $('body')) { + $('#add-trip').css("display", "none"); + } + }); }); diff --git a/src/app/collections/trip_list.js b/src/app/collections/trip_list.js new file mode 100644 index 0000000..63a137a --- /dev/null +++ b/src/app/collections/trip_list.js @@ -0,0 +1,22 @@ +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; + }, + filterBy: function(filterCategory, query) { + if (query === '') { + return this; + } + if (filterCategory === 'weeks' || filterCategory === 'cost') { + return this.where(trip => trip.get(filterCategory) <= Number(query)); + } else { + return this.where((trip) => trip.get(filterCategory).toLowerCase().includes(query)); + } + }, +}); + +export default TripList; diff --git a/src/app/models/trip.js b/src/app/models/trip.js new file mode 100644 index 0000000..3c82f54 --- /dev/null +++ b/src/app/models/trip.js @@ -0,0 +1,46 @@ +import Backbone from 'backbone'; + +const Trip = Backbone.Model.extend({ + urlRoot: 'https://ada-backtrek-api.herokuapp.com/trips/', + validate: function(attributes) { + const errors = {}; + const CONTINENTS = ['Africa', 'Antartica', 'Asia', 'Australasia', 'Europe', 'North America', 'South America']; + if (!attributes.name) { + errors['name'] = ['Cannot be blank']; + } + + if (!attributes.continent) { + errors['continent'] = ['Cannot be blank']; + } else if (!CONTINENTS.includes(attributes.continent)) { + errors['continent'] = ['Must be a valid continent']; + } + + if (!attributes.category) { + errors['category'] = ['Cannot be blank']; + } + + if (!attributes.weeks) { + errors['weeks'] = ['Cannot be blank']; + } else if (isNaN(attributes.weeks)) { + errors['weeks'] = ['Must be a number']; + } else if (attributes.weeks < 1) { + errors['weeks'] = ['Must be greater than 0']; + } + + if (!attributes.cost) { + errors['cost'] = ['Cannot be blank']; + } else if (isNaN(attributes.cost)) { + errors['cost'] = ['Must be a number']; + } else if (attributes.cost <= 0) { + errors['cost'] = ['Must be greater than 0']; + } + + if (Object.keys(errors).length > 0) { + return errors; + } else { + return false; + } + } +}); + +export default Trip; diff --git a/src/css/style.css b/src/css/style.css index b7b2d44..795815b 100644 --- a/src/css/style.css +++ b/src/css/style.css @@ -1,2 +1,122 @@ /* Your styles go here! */ +header { + background-image: url("../../images/crater_lake.jpg"); + background-position: center; + background-repeat: no-repeat; + background-size: cover; + color: #eeeeee; + height: 20em; + padding: 2em; + text-align: center; +} + +header h2 { + font-family: 'Dosis', sans-serif; + font-size: 6em; + text-shadow: 2px 2px #054d01; +} + +header h3 { + font-family: 'Indie Flower', cursive; +} + +button, p, table { + font-family: 'Dosis', sans-serif; +} + +p { + font-size: 1.25em; + font-weight: bold; +} + +#trips { + display: inline-block; + margin: 1em; + padding: .5em; + top: -37em; +} + +table { + table-layout: fixed; +} + +#trip { + display: inline-block; + margin: 1em; + padding: .5em; +} + +.green-border { + border: 2px #054d01 solid; + border-radius: 4px; +} + +button#search, button#add { + font-size: 2em; + border: 2px white solid; + border-radius: 6px; + padding: 10px; + margin: 8px; +} + +button#reserve, button[type="submit"] { + background-color: #eee; + border: 4px black #054d01; + border-radius: 4px; + color: #054d01; + margin: 4px; + padding: 1em; +} + +th.sort { + cursor: pointer; +} + +.current-sort-field { + background-color: darkgrey; + color: white; +} + +.modal { + display: none; + position: fixed; + z-index: 1; + left: 0; + 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: #fefefe; + margin: 15% auto; + padding: 20px; + border: 1px solid #888; + width: 80%; +} + +.close { + color: #aaa; + float: right; + font-size: 28px; + font-weight: bold; +} + +.close:hover, .close:focus { + color: black; + text-decoration: none; + cursor: pointer; +} + +label span { + color: red; +} + +footer { + clear: both; + padding: 1em; +}