diff --git a/dist/index.html b/dist/index.html index 35332a6f9..d10f4c6d9 100644 --- a/dist/index.html +++ b/dist/index.html @@ -4,13 +4,63 @@ - Webpack Starter Kit + Travel Tracker - turing logo - - + +
+

Welcome to Travel Tracker!

+
+
+ + + + + +

+
+
+
+
+
+ + +
+
+ + - - + \ No newline at end of file diff --git a/js/api.js b/js/api.js new file mode 100644 index 000000000..c330ac612 --- /dev/null +++ b/js/api.js @@ -0,0 +1,78 @@ +function fetchData(url) { + return fetch(url) + .then(response => { + if (!response.ok) { + throw new Error(`No data found or data format incorrect`); + } + console.log(response) + return response.json() + }) + .then(data => { + return data; + }) + .catch(error => console.error(`Error fetching data`, error)); +} + +const basePostRequest = { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + } +} +function postTrip(url, { userId, destinationId, travelers, date, duration, status }) { + const postTripRequest = { + ...basePostRequest, + body: JSON.stringify({ + id: Date.now(), + userID: userId, + destinationID: parseInt(destinationId), + travelers: travelers, + date: date, + duration: duration, + status: 'pending', + suggestedActivities: [] + }) + } + + return fetch(url, postTripRequest) + .then(response => { + if (!response.ok) { + throw new Error('There was a problem posting your data.') + } + return response.json() + }) + .then(data => { + console.log('Posted Trip:', data) + return fetchData('http://localhost:3001/api/v1/trips/') + + }) + .catch(error => tripDetails.innerText = error.message) + +} + +function postDestination(url, { destination, estimatedLodgingCostPerDay, estimatedFlightCostPerPerson, image, alt }) { + const destinationPostRequest = { + ...basePostRequest, + body: JSON.stringify({ + destination: destination, + estimatedLodgingCostPerDay: estimatedLodgingCostPerDay, + estimatedFlightCostPerPerson: estimatedFlightCostPerPerson, + image: image, + alt: alt + }) + } + + .then(response => { + if(!response.ok) { + throw new Error('There was a problem posting your data.') + } + return response.json() + }) + .then(data => { + console.log('Posted Destination:', data) + return fetchData('http://localhost:3001/api/v1/trips/') + }) + .catch(error => console.error('Error posting destination:', error)) +} + +export { fetchData, postTrip, postDestination } \ No newline at end of file diff --git a/js/dom.js b/js/dom.js new file mode 100644 index 000000000..9a49b47ce --- /dev/null +++ b/js/dom.js @@ -0,0 +1,248 @@ +import { fetchData, postTrip } from './api.js' +import { getUserTrips, getUserExpenditures, getDestinationName, calculateEstimate } from './index.js' + +const userDataButton = document.getElementById('get-user-data-button') +const travelerDetails = document.getElementById('travelerDetails') +const tripDetails = document.getElementById('tripDetails') +const expenditureDetails = document.getElementById('expenditureDetails') +const destinationDetails = document.getElementById('destinationDetails') +const dropdown = document.getElementById('destinationDropdown') +const userInfo = document.querySelector('.user-info') +const inputGroup = document.querySelector('.input-group') +const formsSection = document.querySelector('.forms-section') +const tripForm = document.getElementById('tripForm') +const bookTripButton = document.getElementById('bookTripButton') +const destinationsSection = document.querySelector('.destinations') +const myTripsNav = document.getElementById('myTripsNav') +const bookTripNav = document.getElementById('bookTripNav') +const destinationsNav = document.getElementById('destinationsNav') +let estimateResult = document.getElementById('estimateResult') + +myTripsNav.addEventListener('click', () => { + show(tripDetails) + hide(formsSection) + hide(destinationsSection) +}) + +bookTripNav.addEventListener('click', () => { + show(formsSection) + hide(tripDetails) + hide(destinationsSection) +}) + +destinationsNav.addEventListener('click', () => { + show(destinationsSection) + show(formsSection) + hide(tripDetails) +}) + +document.addEventListener('DOMContentLoaded', function () {}) +userDataButton.addEventListener('click', displayUserData) + +bookTripButton.addEventListener('click', function (e) { + e.preventDefault() + const formData = new FormData(tripForm); + const tripObj = { + userId: parseInt(formData.get('tripUserID')), + destinationId: parseInt(dropdown.value, 10), + travelers: parseInt(formData.get('travelers'), 10), + date: formData.get('date'), + duration: parseInt(formData.get('duration'), 10), + status: 'pending' + } + + postTrip('http://localhost:3001/api/v1/trips', tripObj) + .then(() => { + loadData() + .then(([_, trips, destinations]) => { + const userID = tripObj.userId + const tripData = getUserTrips(userID, trips) + displayTripData(tripData, destinations) + }) + .catch(error => console.error('Error loading new trip:', error)) + }) + .catch(error => console.error('Error posting trip:', error)) + show(tripDetails) + hide(destinationsSection) +}) + + +tripForm.addEventListener('submit', function (e) { + e.preventDefault() + const formData = new FormData(tripForm) + const userId = formData.get('tripUserID') + const tripDetails = { + userId, + destinationId: parseInt(dropdown.value, 10), + travelers: parseInt(formData.get('travelers'), 10), + date: formData.get('date'), + duration: parseInt(formData.get('duration'), 10), + status: 'pending' + } + + seeEstimate(tripDetails) +}) + + +const loadData = () => { + return Promise.all([ + fetchData('http://localhost:3001/api/v1/travelers/'), + fetchData('http://localhost:3001/api/v1/trips/'), + fetchData('http://localhost:3001/api/v1/destinations/')]) +} + +function displayUserData() { + let userID = document.getElementById('userID').value + const userPassword = document.getElementById('user-password').value + const errorMessage = document.querySelector('.error-message') + const userIDPattern = /^traveler(\d{1,2}|50)$/ + + + if (!userID || !userPassword || userPassword !== 'travel' || !userIDPattern.test(userID)) { + errorMessage.innerText = 'Please enter correct Username and Password'; + setTimeout(() => { + errorMessage.innerText = '' + }, 1000) + return; + } + userID = parseInt(userID.replace('traveler', '')) + + loadData() + .then(([travelers, trips, destinations]) => { + + const travelerData = travelers.travelers.find(traveler => traveler.id === userID) + displayTravelerData(travelerData) + + const tripData = getUserTrips(userID, trips) + displayTripData(tripData, destinations) + + const expenditures = getUserExpenditures(tripData.tripsByYear, destinations,) + displayExpenditureData(expenditures) + + displayDestinationData(destinations.destinations) + show(userInfo) + hide(inputGroup) + show(formsSection) + + }) + .catch(error => console.error('There was a problem loading your data:', error)) +} + +const displayTravelerData = (data) => { + if (data) { + travelerDetails.innerHTML = ` +

Name: ${data.name}

+

Your Travel Penchant: ${data.travelerType}

+ ` + } else { + travelerDetails.innerHTML = `${data.error}` + } +} + +const displayTripData = (data, destinations) => { + + if (typeof data === 'string') { + tripDetails.innerHTML = `

${data}

` + } else { + const { tripsByYear, pendingTrips } = data + let tripHTML = '' + + for (const [year, trips] of Object.entries(tripsByYear)) { + tripHTML += `

Your Trips In ${year}

` + trips.forEach(trip => { + tripHTML += ` +
+

Date: ${trip.date}

+

Destination: ${getDestinationName(trip.destinationID, destinations)}

+

Duration: ${trip.duration} days

+

Travelers: ${trip.travelers}

+
+ ` + }) + } + + tripHTML += '

Pending Trips:' + if (!pendingTrips.length) { + tripHTML += '

No pending trips at this time.

' + } else { + pendingTrips.forEach(trip => { + tripHTML += ` +
+

Date: ${trip.date}

+

Destination: ${getDestinationName(trip.destinationID, destinations)}

+

Duration: ${trip.duration} days

+

Travelers: ${trip.travelers}

+
+ ` + }) + } + + tripDetails.innerHTML = tripHTML; + } +} + + +const displayExpenditureData = (expenditures) => { + if (typeof expenditures === 'string') { + expenditureDetails.innerHTML = `

${expenditures}

`; + } else { + let expenditureHTML = '

Expenditures by Year:

'; + for (const [year, amount] of Object.entries(expenditures)) { + expenditureHTML += `

${year}: $${amount.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}

`; + } + expenditureDetails.innerHTML = expenditureHTML; + } +} + + +const displayDestinationData = (destinations) => { + dropdown.innerHTML = destinations.map(destination => + `` + ).join(''); + destinationDetails.innerHTML = destinations.map(destination => ` +
+

${destination.destination}

+

Accommodation: $${destination.estimatedLodgingCostPerDay}

+

Airfare: $${destination.estimatedFlightCostPerPerson}

+ ${destination.destination} +
+ `).join('') +} + +const seeEstimate = (trip) => { + hide(tripDetails) + show(estimateResult) + show(bookTripButton) + fetchData('http://localhost:3001/api/v1/destinations/') + .then(destinations => { + const userDestination = destinations.destinations.find(place => place.id === trip.destinationId) + if(userDestination) { + const estimate = calculateEstimate(trip, userDestination) + estimateResult.innerText = `Estimated Cost: $${estimate.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}` + } else { + estimateResult.innerText = 'Destination not found.' + } + }) + .catch(error => { + console.error('Could not fetch your destination because:', error) + }) +} + +const hide = (element) => { + element.classList.add('hidden') +} + +const show = (element) => { + element.classList.remove('hidden') +} + +export { + loadData, + displayUserData, + displayTripData, + displayExpenditureData, + displayDestinationData, + seeEstimate, + hide, + show, +} \ No newline at end of file diff --git a/js/index.js b/js/index.js new file mode 100644 index 000000000..267864102 --- /dev/null +++ b/js/index.js @@ -0,0 +1,64 @@ + +const getUserTrips = (userID, trips) => { + if (typeof userID !== 'number') { + return 'You must enter a valid ID, please.' + } + const userTrips = trips.trips.filter(trip => trip.userID === userID) + if (!userTrips.length) { + return 'You have no trips on the itinerary.' + } + + const approvedTrips = userTrips.filter(trip => trip.status === 'approved') + const pendingTrips = userTrips.filter(trip => trip.status === 'pending') + + if (!approvedTrips.length && pendingTrips.length) { + return 'All your trips are pending approval.' + } + + const tripsByYear = approvedTrips.reduce((obj, trip) => { + const tripYear = new Date(trip.date).getFullYear() + if (!obj[tripYear]) { + obj[tripYear] = [] + } + obj[tripYear].push(trip) + return obj + }, {}) + return { tripsByYear, pendingTrips } +} + +const getUserExpenditures = (tripsByYear, destinations) => { + const expendituresByYear = {}; + + for (const [year, trips] of Object.entries(tripsByYear)) { + expendituresByYear[year] = trips.reduce((total, trip) => { + const destination = destinations.destinations.find(place => place.id === trip.destinationID) + if (destination) { + const tripCost = (destination.estimatedLodgingCostPerDay * trip.duration + + destination.estimatedFlightCostPerPerson) * trip.travelers + const totalTripCost = tripCost + tripCost * 0.1 + total += totalTripCost + } + return total + }, 0) + } + + return expendituresByYear; +} + +function calculateEstimate(trip, destination) { + const tripCost = (destination.estimatedLodgingCostPerDay * trip.duration + + destination.estimatedFlightCostPerPerson) * trip.travelers; + return tripCost * 1.1 +} + +const getDestinationName = (id, destinations) => { + if (!id) { + return 'No destination found'; + } + const destination = destinations.destinations.find(destination => id === destination.id); + return destination ? destination.destination : 'Unknown destination' +} + +export { getUserTrips, getUserExpenditures, getDestinationName, calculateEstimate } + + diff --git a/js/mockData.js b/js/mockData.js new file mode 100644 index 000000000..6ae33a6bd --- /dev/null +++ b/js/mockData.js @@ -0,0 +1,51 @@ +export const destinations = { + destinations: [ + { id: 1, destination: 'Lima, Peru', estimatedLodgingCostPerDay: 70, estimatedFlightCostPerPerson: 400, image: 'https://images.unsplash.com/photo-1489171084589-9b…cHBfaWQiOjEyMDd9&auto=format&fit=crop&w=2089&q=80' }, + { id: 2, destination: 'Stockholm, Sweden', estimatedLodgingCostPerDay: 100, estimatedFlightCostPerPerson: 780, image: 'https://images.unsplash.com/photo-1560089168-65160…cHBfaWQiOjEyMDd9&auto=format&fit=crop&w=1950&q=80' }, + { id: 3, destination: 'Sydney, Austrailia', estimatedLodgingCostPerDay: 130, estimatedFlightCostPerPerson: 950, image: 'https://images.unsplash.com/photo-1506973035872-a4…cHBfaWQiOjEyMDd9&auto=format&fit=crop&w=1950&q=80' }, + { id: 4, destination: 'Cartagena, Colombia', estimatedLodgingCostPerDay: 65, estimatedFlightCostPerPerson: 350, image: 'https://images.unsplash.com/photo-1558029697-a7ed1…cHBfaWQiOjEyMDd9&auto=format&fit=crop&w=1650&q=80' }, + { id: 5, destination: 'Madrid, Spain', estimatedLodgingCostPerDay: 150, estimatedFlightCostPerPerson: 650, image: 'https://images.unsplash.com/photo-1543785734-4b6e5…cHBfaWQiOjEyMDd9&auto=format&fit=crop&w=1950&q=80' }, + { id: 6, destination: 'Jakarta, Indonesia', estimatedLodgingCostPerDay: 70, estimatedFlightCostPerPerson: 890, image: 'https://images.unsplash.com/photo-1555333145-4acf1…cHBfaWQiOjEyMDd9&auto=format&fit=crop&w=1950&q=80' }, + { id: 7, destination: 'Paris, France', estimatedLodgingCostPerDay: 100, estimatedFlightCostPerPerson: 395, image: 'https://images.unsplash.com/photo-1524396309943-e0…cHBfaWQiOjEyMDd9&auto=format&fit=crop&w=1567&q=80' }, + { id: 8, destination: 'Tokyo, Japan', estimatedLodgingCostPerDay: 125, estimatedFlightCostPerPerson: 1000, image: 'https://images.unsplash.com/photo-1540959733332-ea…cHBfaWQiOjEyMDd9&auto=format&fit=crop&w=1971&q=80' }, + { id: 9, destination: 'Amsterdam, Netherlands', estimatedLodgingCostPerDay: 100, estimatedFlightCostPerPerson: 950, image: 'https://images.unsplash.com/photo-1534351590666-13…cHBfaWQiOjEyMDd9&auto=format&fit=crop&w=1950&q=80' }, + { id: 10, destination: 'Toronto, Canada', estimatedLodgingCostPerDay: 90, estimatedFlightCostPerPerson: 450, image: 'https://images.unsplash.com/photo-1535776142635-8f…cHBfaWQiOjEyMDd9&auto=format&fit=crop&w=2756&q=80' } +] +} + +export const trips = { + trips: [ + { date: "2022/09/16", destinationID: 2, duration: 8, id: 1, status: "approved", suggestedActivities: [], travelers: 1, userID: 4 }, + { date: "2023/01/20", destinationID: 22, duration: 5, id: 2, status: "pending", suggestedActivities: ["skiing"], travelers: 2, userID: 2 }, + { date: "2023/03/15", destinationID: 14, duration: 10, id: 3, status: "approved", suggestedActivities: ["hiking", "sightseeing"], travelers: 4, userID: 18 }, + { date: "2023/06/25", destinationID: 9, duration: 7, id: 4, status: "approved", suggestedActivities: ["beach"], travelers: 3, userID: 21 }, + { date: "2023/08/05", destinationID: 8, duration: 6, id: 5, status: "pending", suggestedActivities: ["diving"], travelers: 1, userID: 4 }, + { date: "2023/10/12", destinationID: 27, duration: 4, id: 6, status: "approved", suggestedActivities: ["museum", "theater"], travelers: 2, userID: 30 }, + { date: "2023/11/18", destinationID: 19, duration: 9, id: 7, status: "approved", suggestedActivities: ["shopping"], travelers: 5, userID: 26 }, + { date: "2024/02/22", destinationID: 41, duration: 12, id: 8, status: "approved", suggestedActivities: ["mountain biking"], travelers: 2, userID: 15 }, + { date: "2024/04/10", destinationID: 5, duration: 3, id: 9, status: "pending", suggestedActivities: [], travelers: 2, userID: 40 }, + { date: "2024/05/30", destinationID: 13, duration: 14, id: 10, status: "approved", suggestedActivities: ["sailing", "fishing"], travelers: 6, userID: 22 } + ] +} + + +export const travelers = { + travelers: [ + { id: 1, name: 'Ham Leadbeater', travelerType: 'relaxer' }, + { id: 2, name: 'Rex Tillman', travelerType: 'adventurer' }, + { id: 3, name: 'Alice Johnson', travelerType: 'explorer' }, + { id: 4, name: 'Charlie Smith', travelerType: 'sightseer' }, + { id: 5, name: 'David Brown', travelerType: 'shopper' }, + { id: 6, name: 'Eve Davis', travelerType: 'foodie' }, + { id: 7, name: 'Frank White', travelerType: 'nature lover' }, + { id: 8, name: 'Grace Black', travelerType: 'culture seeker' }, + { id: 9, name: 'Henry Green', travelerType: 'history buff' }, + { id: 10, name: 'Ivy Blue', travelerType: 'luxury seeker' } + ] +} + + +export const traveler = + { id: 10, name: 'Ivy Blue', travelerType: 'luxury seeker' } + + diff --git a/package-lock.json b/package-lock.json index 16c95ea89..5b5768660 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,8 +15,6 @@ "file-loader": "^6.2.0", "mocha": "^10.2.0", "mochapack": "^2.1.2", - "sass": "^1.34.0", - "sass-loader": "^12.0.0", "style-loader": "^3.3.4", "webpack": "^5.89.0", "webpack-cli": "^5.1.4", @@ -2976,15 +2974,6 @@ "node": ">=0.10.0" } }, - "node_modules/klona": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/klona/-/klona-2.0.4.tgz", - "integrity": "sha512-ZRbnvdg/NxqzC7L9Uyqzf4psi1OM4Cuc+sJAkQPjO6XkQIJTNbfK2Rsmbw8fx1p2mkZdp2FZYo2+LwXYY/uwIA==", - "dev": true, - "engines": { - "node": ">= 8" - } - }, "node_modules/launch-editor": { "version": "2.6.1", "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.6.1.tgz", @@ -4525,55 +4514,6 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "dev": true }, - "node_modules/sass": { - "version": "1.34.1", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.34.1.tgz", - "integrity": "sha512-scLA7EIZM+MmYlej6sdVr0HRbZX5caX5ofDT9asWnUJj21oqgsC+1LuNfm0eg+vM0fCTZHhwImTiCU0sx9h9CQ==", - "dev": true, - "dependencies": { - "chokidar": ">=3.0.0 <4.0.0" - }, - "bin": { - "sass": "sass.js" - }, - "engines": { - "node": ">=8.9.0" - } - }, - "node_modules/sass-loader": { - "version": "12.0.0", - "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-12.0.0.tgz", - "integrity": "sha512-LJQMyDdNdhcvoO2gJFw7KpTaioVFDeRJOuatRDUNgCIqyu4s4kgDsNofdGzAZB1zFOgo/p3fy+aR/uGXamcJBg==", - "dev": true, - "dependencies": { - "klona": "^2.0.4", - "neo-async": "^2.6.2" - }, - "engines": { - "node": ">= 12.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "fibers": ">= 3.1.0", - "node-sass": "^4.0.0 || ^5.0.0 || ^6.0.0", - "sass": "^1.3.0", - "webpack": "^5.0.0" - }, - "peerDependenciesMeta": { - "fibers": { - "optional": true - }, - "node-sass": { - "optional": true - }, - "sass": { - "optional": true - } - } - }, "node_modules/schema-utils": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.0.0.tgz", @@ -8227,12 +8167,6 @@ "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", "dev": true }, - "klona": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/klona/-/klona-2.0.4.tgz", - "integrity": "sha512-ZRbnvdg/NxqzC7L9Uyqzf4psi1OM4Cuc+sJAkQPjO6XkQIJTNbfK2Rsmbw8fx1p2mkZdp2FZYo2+LwXYY/uwIA==", - "dev": true - }, "launch-editor": { "version": "2.6.1", "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.6.1.tgz", @@ -9369,25 +9303,6 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "dev": true }, - "sass": { - "version": "1.34.1", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.34.1.tgz", - "integrity": "sha512-scLA7EIZM+MmYlej6sdVr0HRbZX5caX5ofDT9asWnUJj21oqgsC+1LuNfm0eg+vM0fCTZHhwImTiCU0sx9h9CQ==", - "dev": true, - "requires": { - "chokidar": ">=3.0.0 <4.0.0" - } - }, - "sass-loader": { - "version": "12.0.0", - "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-12.0.0.tgz", - "integrity": "sha512-LJQMyDdNdhcvoO2gJFw7KpTaioVFDeRJOuatRDUNgCIqyu4s4kgDsNofdGzAZB1zFOgo/p3fy+aR/uGXamcJBg==", - "dev": true, - "requires": { - "klona": "^2.0.4", - "neo-async": "^2.6.2" - } - }, "schema-utils": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.0.0.tgz", diff --git a/src/css/styles.css b/src/css/styles.css index d0637ec3a..427eb6ad9 100644 --- a/src/css/styles.css +++ b/src/css/styles.css @@ -1,4 +1,147 @@ - body { - background: radial-gradient(circle, rgba(63,94,251,1) 0%, rgba(252,70,107,1) 100%); + font-family: Arial, sans-serif; + background-color: rgba(237, 231, 231, 0.889); +} + +h1 { + display: flex; + justify-content: center; + box-shadow: rgba(0, 0, 0, 0.35) 0px 5px 15px; +} + +h4 { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; +} + +button { + border-radius: 10px; + cursor: pointer; + padding:.3rem; + margin:.5rem; + background-color: aliceblue; +} + +nav { + display: flex; + justify-content: flex-end; + background-color: #f8f9fa96; + padding: 10px; + box-shadow: rgba(6, 24, 44, 0.4) 0px 0px 0px 2px, rgba(6, 24, 44, 0.65) 0px 4px 6px -1px, rgba(255, 255, 255, 0.08) 0px 1px 0px inset; +} + +nav ul { + list-style-type: none; + display: flex; + gap: 1.5rem; +} + +nav ul li { + cursor: pointer; +} + +.parent-wrapper { + background-color: rgba(242, 152, 18, 0.685); +} +.container { + display: flex; + flex-direction: column; + align-items: center; + max-width: 50rem; + margin: auto; + padding: 1.5rem; + box-shadow: rgba(0, 0, 0, 0.35) 0px 5px 15px; +} + +.grid-cell-wrapper { + display:flex; + flex-direction: row; + justify-content: space-between; +} + +.input-group { + margin-bottom: 1.5rem; +} + +.input-group label { + display: block; + margin-bottom: .3rem; +} + +.input-group input { + width: 100%; + padding: .5rem; + box-sizing: border-box; + cursor: pointer; +} + +.data-display { + margin-top: 1.5rem; +} + +.data-display h2 { + display: flex; + justify-content: center; + font-size: 1.5em; +} + +.data-display p { + margin: .3rem 0; +} + +.destinations { + display: grid; + grid-template-columns: repeat(5, 1fr); + gap: .5rem; + width:90vw; } +.year-column { + display:flex; + flex-direction: column; + justify-content: flex-start; +} + + +.grid-container { + display: flex; + justify-content: center; + gap: 1rem; + width: 100vw; +} + +.grid-item { + border: 1px solid #ccc; + padding: 1rem; + border-radius: .5rem; + box-shadow: 0 0 .75rem rgba(0, 0, 0, 0.1); + background-color: #ffffff95; +} + +.traveler-data { + display: flex; + flex-direction: column; + justify-content: center; + border: 2px solid blue; +} + +.forms-section { + display:flex; + flex-direction:row; + justify-content: space-around; + align-items: center; + + margin: 2rem; + box-shadow: rgba(0, 0, 0, 0.16) 0px 10px 36px 0px, rgba(0, 0, 0, 0.06) 0px 0px 0px 1px; +} + +.forms { + display:flex; + flex-direction: column; +} + + +.hidden { + display: none; +} \ No newline at end of file diff --git a/src/css/variables.css b/src/css/variables.css new file mode 100644 index 000000000..e69de29bb diff --git a/src/scripts.js b/src/scripts.js index f743b4444..d2393d8b6 100644 --- a/src/scripts.js +++ b/src/scripts.js @@ -1,11 +1,6 @@ -// This is the JavaScript entry file - your code begins here -// Do not delete or rename this file ******** -// An example of how you tell webpack to use a CSS (SCSS) file import './css/styles.css'; +import '/js/dom.js' -// An example of how you tell webpack to use an image (also need to link to it in the index.html) -import './images/turing-logo.png' -console.log('This is the JavaScript entry file - your code begins here.'); diff --git a/test/sample-test.js b/test/sample-test.js index e756b0fb3..e69de29bb 100644 --- a/test/sample-test.js +++ b/test/sample-test.js @@ -1,8 +0,0 @@ -import chai from 'chai'; -const expect = chai.expect; - -describe('See if the tests are running', function() { - it('should return true', function() { - expect(true).to.equal(true); - }); -}); diff --git a/test/test.js b/test/test.js new file mode 100644 index 000000000..aeeb13edf --- /dev/null +++ b/test/test.js @@ -0,0 +1,167 @@ +import { trips, travelers, traveler, destinations } from '/js/mockData.js' +import { getUserTrips, getUserExpenditures, calculateEstimate, getDestinationName } from '../js' +import { expect } from 'chai'; + +before(function () { + trips, travelers, traveler, destinations +}) + +describe('Get User Trips', function () { + it('should return all relevant trips based on user ID', () => { + const result = getUserTrips(4, trips); + const expectedTripsByYear = { + "2022": [ + { date: "2022/09/16", destinationID: 2, duration: 8, id: 1, status: "approved", suggestedActivities: [], travelers: 1, userID: 4 } + ] + }; + expect(result.tripsByYear).to.deep.equal(expectedTripsByYear); + }) +}) + +it('should handle an invalid userID', () => { + const noTrips = getUserTrips('!', trips) + expect(noTrips).to.equal('You must enter a valid ID, please.') +}) + +it('should handle no ID', () => { + const badID = getUserTrips('', trips) + expect(badID).to.equal('You must enter a valid ID, please.') +}) + +it('should handle a return without any trips', () => { + const noTrips = getUserTrips(10, trips) + expect(noTrips).to.equal('You have no trips on the itinerary.') +}) + +it('should handle pending trips', () => { + const { pendingTrips } = getUserTrips(4, trips) + const pendingTrip = [ + { date: "2023/08/05", destinationID: 8, duration: 6, id: 5, status: "pending", suggestedActivities: ["diving"], travelers: 1, userID: 4 } + ] + expect(pendingTrips).to.deep.equal(pendingTrip) +}) + +describe('Get User Expenditures', function () { + it('should return total money spent on all approved trips in the target year, plus 10%', () => { + const userTrips = getUserTrips(4, trips) + const expenditures = getUserExpenditures(userTrips.tripsByYear, destinations) + const totalBill2023 = expenditures['2023'] || 0 + expect(totalBill2023).to.equal(0) + }) + + it('should handle user with only pending trips', () => { + const userTrips = getUserTrips(2, trips); + expect(userTrips).to.equal('All your trips are pending approval.'); + }) + + it('should handle trips with undefined status', () => { + const tripsWithUndefinedStatus = { + trips: [ + { date: "2023/01/20", destinationID: 22, duration: 5, id: 2, status: undefined, suggestedActivities: ["skiing"], travelers: 2, userID: 2 } + ] + }; + const userTrips = getUserTrips(2, tripsWithUndefinedStatus); + expect(userTrips.tripsByYear).to.deep.equal({}); + expect(userTrips.pendingTrips).to.deep.equal([]); +}) + +it('should calculate expenditures consistently for multiple users', () => { + const user1Trips = getUserTrips(4, trips); + const user2Trips = getUserTrips(2, trips); + + const user1Expenditures = user1Trips.tripsByYear ? getUserExpenditures(user1Trips.tripsByYear, destinations) : {}; + const user2Expenditures = user2Trips.tripsByYear ? getUserExpenditures(user2Trips.tripsByYear, destinations) : {}; + + const totalBillUser1_2023 = user1Expenditures['2023'] || 0; + const totalBillUser2_2023 = user2Expenditures['2023'] || 0; + + expect(totalBillUser1_2023).to.equal(0) + expect(totalBillUser2_2023).to.equal(0) +}) + + it('should return total money spent on all approved trips in the target year, plus 10%, for a different user', () => { + const tripsForTest = { + trips: [ + { date: "2022/09/16", destinationID: 2, duration: 8, id: 1, status: "approved", suggestedActivities: [], travelers: 1, userID: 4 }, + { date: "2023/01/20", destinationID: 22, duration: 5, id: 2, status: "pending", suggestedActivities: ["skiing"], travelers: 2, userID: 4 }, + { date: "2023/03/15", destinationID: 14, duration: 10, id: 3, status: "approved", suggestedActivities: ["hiking", "sightseeing"], travelers: 4, userID: 4 }, + { date: "2023/06/25", destinationID: 9, duration: 7, id: 4, status: "approved", suggestedActivities: ["beach"], travelers: 3, userID: 4 }, + { date: "2023/08/05", destinationID: 8, duration: 6, id: 5, status: "pending", suggestedActivities: ["diving"], travelers: 1, userID: 4 } + ] + } + const userTrips = getUserTrips(4, tripsForTest); + const expenditures = getUserExpenditures(userTrips.tripsByYear, destinations) + const totalBill2023 = expenditures['2023'] || 0 + expect(totalBill2023).to.equal(5445) + }) + + it('should handle user with no trips in the target year', () => { + const userTrips = getUserTrips(4, trips) + const expenditures = getUserExpenditures(userTrips.tripsByYear, destinations) + const totalBill2021 = expenditures['2021'] || 0 + expect(totalBill2021).to.equal(0); + }) + + describe('Calculate Estimate', function () { + it('should calculate the correct estimate for a simple trip', () => { + const trip = { duration: 5, travelers: 2 }; + const destination = { estimatedLodgingCostPerDay: 100, estimatedFlightCostPerPerson: 500 }; + const estimate = calculateEstimate(trip, destination); + expect(estimate).to.equal((5 * 100 + 500) * 2 * 1.1); + }) + + it('should return only the flight cost with zero duration', () => { + const trip = { duration: 0, travelers: 3 }; + const destination = { estimatedLodgingCostPerDay: 100, estimatedFlightCostPerPerson: 400 }; + const estimate = calculateEstimate(trip, destination); + expect(estimate).to.equal((400 * 3) * 1.1); + }) + + it('should return 0 when there are no travelers', () => { + const trip = { duration: 5, travelers: 0 }; + const destination = { estimatedLodgingCostPerDay: 200, estimatedFlightCostPerPerson: 600 }; + const estimate = calculateEstimate(trip, destination); + expect(estimate).to.equal(0); + }) + + it('should handle long duration trips correctly', () => { + const trip = { duration: 30, travelers: 4 }; + const destination = { estimatedLodgingCostPerDay: 150, estimatedFlightCostPerPerson: 700 }; + const estimate = calculateEstimate(trip, destination); + expect(estimate).to.equal((30 * 150 + 700) * 4 * 1.1); + }) + }) +}) + + describe('Get Destination Name', function () { + it('should return the correct destination name for a valid ID', () => { + const id = 2; + const name = getDestinationName(id, destinations); + expect(name).to.equal('Stockholm, Sweden'); + }) + + it('should return "Unknown destination" for an invalid ID', () => { + const id = 99; + const name = getDestinationName(id, destinations); + expect(name).to.equal('Unknown destination'); + }) + + it('should return "No destination found" when no ID is provided', () => { + const name = getDestinationName(null, destinations); + expect(name).to.equal('No destination found'); + }) + + it('should return "Unknown destination" for an ID provided as a string', () => { + const id = '2'; + const name = getDestinationName(id, destinations); + expect(name).to.equal('Unknown destination'); + }) + + it('should return the correct destination name for the first destination ID', () => { + const id = 1; + const name = getDestinationName(id, destinations); + expect(name).to.equal('Lima, Peru'); + }) + }) + +