diff --git a/dist/index.html b/dist/index.html index b873b1e..44a8036 100644 --- a/dist/index.html +++ b/dist/index.html @@ -2,19 +2,161 @@ - My JavaScript App + backTREK +
+
-
-
+

backTREK

+ + + +
- + + + + + + + + + + + + diff --git a/src/app.js b/src/app.js index e7af594..075776b 100644 --- a/src/app.js +++ b/src/app.js @@ -6,8 +6,208 @@ import _ from 'underscore'; import './css/foundation.css'; import './css/style.css'; -console.log('it loaded!'); +// Models and Collections +import Trip from './app/models/trip'; +import TripList from './app/collections/trip-list'; +import Reservation from './app/models/reservation'; + +let tripTemplate; +let emptyTripTemplate; +let tripDetailTemplate; +let reserveModalTemplate; +let addTripModalTemplate; + +// Trip List + +const tripList = new TripList(); + +const render = function render(tripList) { + $('.trip-row').remove(); + const tripListElement = $('#trip-list ul'); + if (tripList.length > 0) { + tripList.forEach((trip) => { + const generatedHTML = $(tripTemplate(trip.attributes)); + tripListElement.append(generatedHTML); + }); + } else { + const generatedHTML = $(emptyTripTemplate()); + tripListElement.append(generatedHTML); + } +}; + +const show = function show(e) { + // don't minimize trip details when the reserve button is clicked + if ($(e.target).is('#reserve-btn')) return; + const id = parseInt(findElementTripID(e)); + const tripElement = $(`#${id}`); + if (tripElement.hasClass('show')) { + clearShow(); + } else { + const trip = tripList.findWhere({id: id}); + trip.fetch({ + success: () => { + clearShow(); + $('.trip-row').removeClass('show'); + $('.trip-details').remove(); + const generatedHTML = $(tripDetailTemplate(trip.attributes)); + const reserveBtn = ('#reserve-btn'); + tripElement.append(generatedHTML).addClass('show'); + } + }); + } +}; + +const clearShow = function clearShow() { + $('.trip-row').removeClass('show'); + $('.trip-detail-holder').remove(); +}; + +const sort = function sort(e) { + $('.current-sort').removeClass('current-sort'); + let targetElement = $(e.target); + let field = targetElement[0].id; + tripList.comparator = field; + targetElement.addClass('current-sort'); + tripList.sort(); + render(tripList); +}; + +// Filtering + +const filter = function filter(e) { + e.preventDefault(); + const form = $('#trip-search'); + const searchData = getFormData(form, ['search-type', 'query']); + const newList = tripList.filterBy(searchData['search-type'], searchData['query']); + render(newList); +}; + +// Modals + +const addTripModal = function addTripModal() { + $('body').append(addTripModalTemplate()); +}; + +const reserveModal = function reserveModal(e) { + const id = findElementTripID(e); + const generatedHTML = $(reserveModalTemplate({'id': id})); + const form = $('#reservation-form'); + // form.on('submit', submitReservation); + $('body').append(generatedHTML); +}; + +const clearModal = function clearModal(e) { + if ($(e.target).hasClass('modal-close')) { + $('.modal').remove(); + } +}; + +// Forms + +const submitTrip = function submitTrip(e) { + e.preventDefault(); + clearErrors(); + const form = $(e.target); + const formData = getFormData(form, ['name', 'continent', 'category', 'weeks', 'cost', 'about']); + const newTrip = new Trip(formData); + saveIfValid(newTrip, form, 'trip'); +}; + +const submitReservation = function submitReservation(e) { + e.preventDefault(); + clearErrors(); + $('.form-messages').html(''); + const form = $('#reservation-form'); + const id = form.data('id'); + const formData = getFormData(form, ['name', 'email']); + formData['tripID'] = id; + const newReservation = new Reservation(formData); + saveIfValid(newReservation, form, 'reservation'); +}; + +const getFormData = function getFormData(target, values) { + const formData = {}; + values.forEach((value) => { + let targetElement = target.find(`[name="${ value }"]`); + formData[value] = targetElement.val(); + }); + return formData; +}; + +const saveIfValid = function saveIfValid(object, form, type) { + if (object.isValid()) { + object.save({}, { + success: (response) => { + formSuccess(type, form) + }, + error: (status, response) => { + const errors = ($.parseJSON(response.responseText))['errors']; + printErrors(errors, type); + }, + }); + } else { + printErrors(object.validationError, type); + } +}; + +const formSuccess = function formSuccess(item, form) { + const messageBox = $('#form-messages'); + messageBox.html(`

Successfully created ${item}!

`); + form[0].reset(); +} + +const printErrors = function printErrors(errors, type) { + for(let field in errors) { + let errorElementID = `#${type}-${field} label` + let errorElement = $(errorElementID); + errorElement.addClass('has-errors'); + errorElement.append(`

${errors[field]}

`); + } +}; + +const clearErrors = function clearErrors() { + $('.has-errors').removeClass('has-errors'); + $('.error').remove(); +}; + +const findElementTripID = function findElementTripID(e) { + return $(e.target).closest('li.trip-row').attr('id'); +}; $(document).ready( () => { - $('main').html('

Hello World!

'); + $('.on-load').hide(); + tripTemplate = _.template($('#trip-template').html()); + emptyTripTemplate = _.template($('#empty-trip-template').html()); + tripDetailTemplate = _.template($('#trip-detail-template').html()); + reserveModalTemplate = _.template($('#reserve-modal-template').html()); + addTripModalTemplate = _.template($('#add-trip-modal-template').html()); + + tripList.on('update', render); + + $('#intro-button').on('click', (e) => { + $('#intro-button').hide(200); + $('#loading').show(500); + tripList.fetch({ + success: () => { + $('#loading').hide(200); + $('#trip-list').show(500); + }, + }); + $('.on-load').show(500); + }); + + $('#trip-list').on('click', 'li', show); + + $('.sort').on('click', sort); + + $('#add-trip').on('click', addTripModal); + $('body').on('click', '.modal-close', clearModal); + + $('body').on('keyup', '#trip-search', filter); + $('#trip-search').on('submit', e => e.preventDefault()); + + $('#trip-list').on('click', '#reserve-btn', reserveModal); + $(document).on('submit', '#reservation-form', submitReservation); + $(document).on('submit', '#add-trip-form', submitTrip); + }); diff --git a/src/app/collections/trip-list.js b/src/app/collections/trip-list.js new file mode 100644 index 0000000..04ac71d --- /dev/null +++ b/src/app/collections/trip-list.js @@ -0,0 +1,21 @@ +import Backbone from 'backbone'; +import Trip from '../models/trip'; + +const TripList = Backbone.Collection.extend({ + model: Trip, + url: 'https://ada-backtrek-api.herokuapp.com/trips/', + + filterBy: function(field, value) { + const newList = this.filter(function(trip) { + if (['cost', 'weeks'].includes(field)){ + value = parseFloat(value); + return value >= trip.get(field); + } else { + return trip.get(field).includes(value); + } + }); + return new TripList(newList); + }, +}); + +export default TripList; diff --git a/src/app/images/michal-parzuchowski-40545.jpg b/src/app/images/michal-parzuchowski-40545.jpg new file mode 100644 index 0000000..dc6743c Binary files /dev/null and b/src/app/images/michal-parzuchowski-40545.jpg differ diff --git a/src/app/models/reservation.js b/src/app/models/reservation.js new file mode 100644 index 0000000..8b842b3 --- /dev/null +++ b/src/app/models/reservation.js @@ -0,0 +1,19 @@ +import Backbone from 'backbone'; + +const Reservation = Backbone.Model.extend({ + validate(attr) { + const errors = {}; + if (attr['name'] == "") { + errors['name'] = ['cannot be blank']; + } + if (attr['email'] == "") { + errors['email'] = ['cannot be blank']; + } + return Object.keys(errors).length > 0 ? errors : false; + }, + urlRoot() { + return `https://ada-backtrek-api.herokuapp.com/trips/${ this.get('tripID') }/reservations`; + }, +}); + +export default Reservation; diff --git a/src/app/models/trip.js b/src/app/models/trip.js new file mode 100644 index 0000000..40a5c3d --- /dev/null +++ b/src/app/models/trip.js @@ -0,0 +1,36 @@ +import Backbone from 'backbone'; + +const Trip = Backbone.Model.extend({ + defaults: { + name: 'Untitled Trip', + continent: 'Unknown', + category: 'none', + weeks: 0, + about: 'no info', + cost: 0, + }, + urlRoot: 'https://ada-backtrek-api.herokuapp.com/trips/', + + validate(attr) { + const errors = {}; + if (!['Africa', 'Antartica', 'Asia', 'Australasia', 'Europe', 'North America', 'South America'].includes(attr['continent'])) { + errors['continent'] = 'must be valid Earth continent'; + } + ['weeks', 'cost'].forEach((field) => { + if (parseFloat(attr[field]) === NaN) { + errors[field] = "must be a number"; + } + if (!parseFloat(attr[field]) > 0) { + errors[field] = "must be greater than 0"; + } + }); + ['name', 'continent', 'category', 'weeks', 'cost'].forEach((field) => { + if (attr[field] === '') { + errors[field] = "can't be blank"; + } + }); + return Object.keys(errors).length > 0 ? errors : false; + }, +}); + +export default Trip; diff --git a/src/css/style.css b/src/css/style.css index b7b2d44..4ac76c6 100644 --- a/src/css/style.css +++ b/src/css/style.css @@ -1,2 +1,274 @@ +:root { +--white: #fefefe; +--transpwhite: rgba(254, 254, 254, 0.7); +--dkwhite: #f0f0f0; +--transpdkwhite: rgba(240, 240, 240, 0.7); +--black: #333333; +--transpblack: rgba(51, 51, 51, 0.5); +--peach: #fec0aa; +--ltblue: #98C1D9; +--transpltblue: rgba(152, 193, 217, 0.7); +--salmon: #EE6352; +--indigo: #223843; +--dkgrey: #6D696A; +--grey: #E3E4E8; +--transpgrey: rgba(227, 227, 232, 0.7); +} -/* Your styles go here! */ +body { + background-attachment: fixed; + background-color: var(--white); + background-image: url('../app/images/michal-parzuchowski-40545.jpg'); + background-size: cover; + background-position: center center; +} + +h1 { + /* website title */ + color: var(--white); + display: inline-block; + font-family: 'Comfortaa', sans-serif; +} + +h2 { + /* modal titles */ + color: var(--indigo); + font-size: 2rem; + font-family: 'Comfortaa', sans-serif; +} + +h3 { + /* loading message */ + color: var(--transpwhite); + font-size: 3rem; + text-align: center; + font-family: 'Comfortaa', sans-serif; +} + +h4 { + /* list headers */ + color: var(--indigo); + display: inline-block; + font-family: 'Comfortaa', sans-serif; + margin: 0; + overflow: hidden; + padding: .5rem 0 0; +} + +h6 { + /* list data */ + color: var(--black); + display: inline-block; + overflow: hidden; +} + +p, label { + color: var(--black); +} + +ul { + list-style: none; +} + +form { + overflow: auto; +} + +.row { + max-width: 100%; +} + +.button { + border-radius: 1rem; + background-color: var(--ltblue); + font-family: 'Comfortaa', sans-serif; +} + +.button:hover { + background-color: var(--peach); +} + +.has-errors { + color: tomato; + font-weight: bold; +} + +p.error { + color: tomato; + display: inline; + font-style: italic; + font-weight: normal; + margin-left: 1rem; +} + +p.success { + color: mediumseagreen; + font-style: italic; + font-weight: normal; + margin-left: 1rem; +} + +/**********/ +/* HEADER */ +/**********/ + +header { + overflow: auto; + padding: 1rem; +} + +.add-trip { + border-radius: 50%; + float: right; + font-size: 2rem; + height: 3rem; + line-height: 3rem; + padding: 0; + width: 3rem; +} + +.form-holder { + padding: 1rem .5rem 0 .5rem; +} + +.form-holder input, .form-holder select { + border-radius: 1rem; +} + +/* SEARCH */ + +header form { + float: none !important; + margin: auto; + overflow: auto; +} + +[type="submit"] { + width: 100%; +} + +/*************/ +/* TRIP LIST */ +/*************/ + +.intro-button { + align-items: center; + display: flex; + float: none; + font-size: 8vh; + height: 30vh; + justify-content: center; + margin: 1rem auto; +} + +.trip-list ul { + margin: 0 auto; + width: 95%; +} + +.trip-list li { + padding: 1rem 2rem; +} + +.headers { + background-color: var(--transpgrey); + overflow: auto; +} + +.trip-row { + background-color: var(--transpwhite); +} + +.trip-row:nth-of-type(odd) { + background-color: var(--transpdkwhite); +} + +.name-col { + width: 23%; +} + +.cont-col { + text-align: center; + width: 20%; +} + +.cat-col { + text-align: center; + width: 20%; +} + +.weeks-col { + text-align: center; + width: 15%; +} + +.cost-col { + text-align: right; + width: 15%; +} + +.current-sort { + color: var(--salmon); + text-decoration: underline; +} + +.empty-col { + color: var(--dkgrey); + display: block; + font-size: 1.25rem; + font-style: italic; + margin: auto; + text-align: center; +} + +/* TRIP DISPLAY */ + +.show h6 { + font-weight: bold; +} + +.reserve-btn { + background-color: var(--indigo); + margin-bottom: 0; +} + +.reserve-holder { + text-align: center; +} + +/**********/ +/* MODALS */ +/**********/ + +.modal-shadow { + background-color: var(--transpblack); + height: 100vh; + position: fixed; + width: 100%; + top: 0; + left: 0; +} + +.modal-body { + background-color: var(--white); + border-radius: 1rem; + box-shadow: 5px 5px 20px var(--black); + float: none; + margin: 10% auto; + padding: 2rem; +} + +.close-btn { + border: 1px solid var(--grey); + border-radius: 5rem; + float: right; + font-weight: bold; + height: 2rem; + line-height: 1.75; + text-align: center; + width: 2rem; +} + +a.close-btn { + color: var(--grey); +}