├── client ├── images │ ├── logo.png │ ├── logo2.png │ └── mickey-fill-svgrepo-com.svg ├── index.html ├── stylesheets │ ├── _colors.scss │ ├── _navbar.scss │ ├── index.scss │ ├── _app.scss │ ├── _login.scss │ └── _trips.scss ├── index.js └── components │ ├── trip │ ├── AllTrips.js │ ├── UpcomingTrip.js │ ├── Trip.js │ ├── NewTrip.js │ └── TripDetails.js │ ├── NavBar.js │ ├── App.js │ ├── CreateUser.js │ ├── Home.js │ └── Login.js ├── server ├── middleware │ ├── cookieController.js │ ├── sessionController.js │ ├── tripController.js │ └── userController.js ├── routes │ ├── trips.js │ └── users.js ├── models.js └── server.js ├── README.md └── package.json /client/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krisette/imagine/HEAD/client/images/logo.png -------------------------------------------------------------------------------- /client/images/logo2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krisette/imagine/HEAD/client/images/logo2.png -------------------------------------------------------------------------------- /client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IMAGINE 6 | 7 | 8 |
9 | -------------------------------------------------------------------------------- /server/middleware/cookieController.js: -------------------------------------------------------------------------------- 1 | cookieController = { 2 | setSSIDCookie: (req, res, next) => { 3 | res.cookie('ssid', res.locals.userID, { httpOnly: true }) 4 | return next() 5 | } 6 | } 7 | 8 | 9 | module.exports = cookieController; -------------------------------------------------------------------------------- /client/stylesheets/_colors.scss: -------------------------------------------------------------------------------- 1 | $text-color: #444; 2 | $secondary-black: rgba($text-color, .83); 3 | 4 | 5 | $primary-color: #7B9DCA; 6 | $secondary-color: rgba($primary-color, .66); 7 | $tertiary-color: rgba($primary-color, .33); 8 | $primary-dark-color: #456086; 9 | 10 | $accent-color: #D7B9C6; -------------------------------------------------------------------------------- /client/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { createRoot } from 'react-dom/client'; 3 | import App from './components/App.js'; 4 | import './stylesheets/index.scss'; 5 | 6 | const root = createRoot(document.getElementById('root')); // createRoot(container!) if you use TypeScript 7 | root.render(); -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Imagine 2 | Imagine is a trip planning/management web app to assist you with your magical trips to Disney. 3 | 4 | ## v2 5 | Currently in development. 6 | Featuring TypeScript, GraphQL, and Redux. 7 | 8 | ## v1 Built with... 9 | - MongoDB 10 | - Mongoose ODM 11 | - React 12 | - Node.js & Express 13 | - Webpack 14 | -------------------------------------------------------------------------------- /client/images/mickey-fill-svgrepo-com.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /client/components/trip/AllTrips.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Trip from './Trip'; 3 | 4 | export default function AllTrips( {trips} ) { 5 | return ( 6 |
7 |

All Trips

8 | {trips && trips.map((trip, index) => { 9 | return ( 10 | 11 | ) 12 | })} 13 |
14 | ) 15 | } -------------------------------------------------------------------------------- /client/stylesheets/_navbar.scss: -------------------------------------------------------------------------------- 1 | @import '_colors'; 2 | 3 | .navbar { 4 | display: flex; 5 | align-items: center; 6 | width: 100%; 7 | background: rgba(95, 95, 95, 0.382); 8 | color: white; 9 | font-family: Helvetica; 10 | font-weight: 300; 11 | position: absolute; 12 | top: 0; 13 | left: 0; 14 | a { 15 | color: white; 16 | } 17 | } 18 | 19 | .navbar__title { 20 | margin-right: auto; 21 | font-size: 1rem; 22 | padding: 5px 16x; 23 | } 24 | 25 | .navbar__item { 26 | padding: 5px 16px; 27 | cursor: pointer; 28 | vertical-align: middle; 29 | } 30 | 31 | .navbar__item:hover { 32 | background: rgba(0, 0, 0, 0.2); 33 | } 34 | 35 | -------------------------------------------------------------------------------- /client/components/NavBar.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import "../stylesheets/_navbar.scss"; 3 | // import { Link } from "react-router-dom"; 4 | export default function NavBar( {user, setAllTripsShown} ) { 5 | 6 | const showAllTrips = () => { 7 | setAllTripsShown(current => !current); 8 | } 9 | 10 | const logOut = () => { 11 | window.location.reload(); 12 | } 13 | 14 | return ( 15 |
16 |
welcome, {user}!
17 |
all trips
18 |
current wait times
19 |
log out
20 |
21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /client/stylesheets/index.scss: -------------------------------------------------------------------------------- 1 | @import '_colors'; 2 | @import url('https://fonts.googleapis.com/css2?family=Nunito+Sans:wght@300&display=swap'); 3 | 4 | 5 | body { 6 | background: linear-gradient(60deg, #DCB9C5 0%, #98B6CC 50%, #E9D2C7 100%); 7 | background-attachment: fixed; 8 | color: $text-color; 9 | font-family: Graphik, 'Nunito Sans', sans-serif; 10 | } 11 | 12 | #root { 13 | display: flex; 14 | flex-direction: column; 15 | align-items: center; 16 | justify-content: center; 17 | width: 100%; 18 | } 19 | 20 | .main-container { 21 | display: flex; 22 | flex-direction: column; 23 | align-items: center; 24 | justify-content: center; 25 | width: 100%; 26 | margin: 0 auto; 27 | padding: 0 1rem; 28 | } 29 | 30 | a { 31 | color: $primary-dark-color; 32 | text-decoration: none; 33 | font-weight: bold; 34 | 35 | &:hover { 36 | text-decoration: underline; 37 | cursor: pointer; 38 | } 39 | } -------------------------------------------------------------------------------- /server/routes/trips.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const router = express.Router(); 3 | const tripController = require('../middleware/tripController'); 4 | 5 | 6 | 7 | 8 | router.get('/parks', 9 | tripController.getParks, 10 | (req, res) => { 11 | return res.status(200).json(res.locals.parks); 12 | } 13 | ); 14 | 15 | router.get('/upcoming/:id', 16 | tripController.getUpcomingTrip, 17 | (req, res) => { 18 | res.status(200).json(res.locals.trip) 19 | }); 20 | 21 | router.get('/:id', 22 | tripController.getTrips, 23 | (req, res) => { 24 | res.status(200).send(res.locals.trips) 25 | }); 26 | 27 | router.get('/', 28 | tripController.getTrips, 29 | (req, res) => { 30 | res.status(200).json(res.locals.trips) 31 | }); 32 | 33 | router.post('/', 34 | tripController.createTrip, 35 | (req, res) => { 36 | res.status(201); 37 | }); 38 | 39 | router.put('/:id', 40 | tripController.updateTrip, 41 | (req, res) => { 42 | res.status(200); 43 | }); 44 | 45 | router.delete('/:id', 46 | tripController.deleteTrip, 47 | (req, res) => { 48 | res.status(200); 49 | }); 50 | 51 | module.exports = router -------------------------------------------------------------------------------- /server/routes/users.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const router = express.Router(); 3 | const userController = require('../middleware/userController'); 4 | // const sessionController = require('../middleware/sessionController'); 5 | // const cookieController = require('../middleware/cookieController'); 6 | 7 | router.get('/isAuth', (req, res) => { 8 | if (req.session.user) { 9 | return res.json(req.session.user); 10 | } else { 11 | return res.status(401).json('unauthorized'); 12 | } 13 | }); 14 | 15 | router.post('/signup', 16 | userController.createUser, 17 | (req, res) => { 18 | console.log('signed up'); 19 | return res.status(201); 20 | }); 21 | 22 | router.post('/login', 23 | userController.verifyUser, 24 | (req, res) => { 25 | console.log('logged in'); 26 | return res.status(200).json({ status: 200, id: res.locals.userID }); 27 | }); 28 | 29 | router.delete('/logout', (req, res) => { 30 | req.session.destroy((error) => { 31 | if (error) throw error; 32 | res.clearCookie('session-id') // cleaning the cookies from the user session 33 | res.status(200).send('Logout Success'); 34 | }); 35 | }); 36 | 37 | module.exports = router; -------------------------------------------------------------------------------- /client/components/trip/UpcomingTrip.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default function UpcomingTrip( {upcomingTrip} ) { 4 | 5 | const dateConverter = (date) => { 6 | const newDate = new Date(date); 7 | return newDate.toLocaleString('en-US', { timeZone: 'UTC', year: 'numeric', month: '2-digit', day: '2-digit' }); 8 | } 9 | 10 | const differenceInDays = (date) => { 11 | const today = new Date(); 12 | const tripDate = new Date(date); 13 | const difference = tripDate - today; 14 | const days = Math.floor(difference / (1000 * 60 * 60 * 24)); 15 | return days; 16 | } 17 | 18 | const daysUntilTrip = differenceInDays(upcomingTrip.start_date); 19 | 20 | return ( 21 |
22 |
23 |

You're going to Walt Disney World 24 | {daysUntilTrip === 0 ? 'TODAY!' : 'in '} 25 | {daysUntilTrip > 1 ? `${daysUntilTrip} days!` : null}{daysUntilTrip === 1 ? '1 day!' : null}

26 |

Next vacation: {dateConverter(upcomingTrip.start_date)} - {dateConverter(upcomingTrip.end_date)}

27 |
28 |
29 | ) 30 | } -------------------------------------------------------------------------------- /server/middleware/sessionController.js: -------------------------------------------------------------------------------- 1 | const models = require('../models'); 2 | const jwt = require('jsonwebtoken'); 3 | require('dotenv').config({ path: path.resolve(__dirname, '../.env') }) 4 | 5 | const sessionController = { 6 | 7 | startSession: (req, res, next) => { 8 | models.Session.create({ cookieId: res.locals.user._id.id }, (err, session) => { 9 | if (err) console.log(err); 10 | return next(); 11 | }) 12 | }, 13 | 14 | isLoggedIn: (req, res, next) => { 15 | models.Session.find({ cookieId: req.cookies.ssid }, (err, session) => { 16 | if (err) { 17 | res.status(401).send('Unauthorized'); 18 | } else { 19 | console.log('succesfully logged in'); 20 | return next(); 21 | } 22 | }) 23 | }, 24 | 25 | verifyToken: (req, res, next) => { 26 | let token = req.session.token; 27 | if (!token) { 28 | return res.status(403).send({ message: "No token provided!" }); 29 | } 30 | jwt.verify(token, process.env.JWT_SECRET, (err, decoded) => { 31 | if (err) { 32 | return res.status(401).send({ message: "unauthorized" }); 33 | } 34 | req.userId = decoded.id; 35 | next(); 36 | }); 37 | } 38 | } 39 | 40 | module.exports = sessionController; -------------------------------------------------------------------------------- /client/stylesheets/_app.scss: -------------------------------------------------------------------------------- 1 | @import '_colors'; 2 | 3 | .main-container { 4 | display: flex; 5 | flex-direction: column; 6 | align-items: center; 7 | justify-content: center; 8 | width: 100%; 9 | margin: 0 auto; 10 | padding: 0 1rem; 11 | font-size: 10pt; 12 | } 13 | 14 | .main { 15 | background-color: rgba(255, 255, 255, 0.5); 16 | border: 1px solid rgb(221, 221, 221); 17 | box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.12), 0px 6px 16px 0px rgba(0, 0, 0, 0.24); 18 | border-radius: 12px; 19 | height: 20%; 20 | display: flex; 21 | flex-direction: column; 22 | align-items: center; 23 | justify-content: center; 24 | padding: 25px; 25 | box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.2), 0 2px 8px 0 rgba(0, 0, 0, 0.19); 26 | margin-top: 25px; 27 | width: 70%; 28 | } 29 | 30 | .upcoming-trip { 31 | display: flex; 32 | flex-direction: column; 33 | align-items: center; 34 | justify-content: center; 35 | width: 100%; 36 | text-align: center; 37 | } 38 | 39 | #wow-a-trip { 40 | font-size: 1.3rem; 41 | } 42 | 43 | #wow-a-trip-numbers { 44 | font-weight: bold; 45 | text-decoration: underline; 46 | } 47 | 48 | button { 49 | cursor: pointer; 50 | font-size: 12px; 51 | background-color: $primary-color; 52 | border: 0; 53 | border-radius: 5px; 54 | color: #fff; 55 | padding: 5px 20px; 56 | transition: background-color 0.4s ease-in-out; 57 | } 58 | 59 | button:hover { 60 | background-color: $primary-dark-color; 61 | } 62 | -------------------------------------------------------------------------------- /client/components/App.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import { BrowserRouter, Routes, Route } from 'react-router-dom'; 3 | import '../stylesheets/_app.scss'; 4 | import Home from './Home.js'; 5 | import Login from './Login.js'; 6 | import NavBar from './NavBar.js'; 7 | 8 | 9 | const App = () => { 10 | const [loginStatus, setLoginStatus] = useState(false); 11 | // const [userSession, setUserSession] = useState(true); 12 | const [user, setUser] = useState(); 13 | const [userID, setUserID] = useState(); 14 | const [allTripsShown, setAllTripsShown] = useState(false); 15 | 16 | 17 | // useEffect(() => { 18 | // const checkUserAuth = () => { 19 | // fetch('/users/isAuth') 20 | // .then(res => res.json()) 21 | // .then(data => { 22 | // if (data.status === 200) { 23 | // console.log('hi'); 24 | // } else { 25 | // setLoginStatus(undefined); 26 | // } 27 | // }) 28 | // .catch(err => console.log(err)); 29 | // } 30 | // checkUserAuth(); 31 | // }, []); 32 | 33 | if (!loginStatus) { 34 | return 35 | } 36 | 37 | return ( 38 |
39 | 40 |

41 | 42 |
43 | ) 44 | } 45 | 46 | export default App; -------------------------------------------------------------------------------- /server/models.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | 3 | const Schema = mongoose.Schema; 4 | 5 | // trips schema 6 | const tripSchema = new Schema({ 7 | start_date: { type: Date, required: true }, 8 | end_date: { type: Date, required: true }, 9 | hotel: { type: String, required: true }, 10 | // parks: { type: String, required: true }, 11 | parks: [{ 12 | name: { type: String, required: true }, 13 | date: { type: Date }, 14 | reservations: { type: Boolean, default: false }, 15 | tickets: { type: Boolean, default: false } 16 | }], 17 | user_id: { 18 | type: String, 19 | required: true 20 | } 21 | }); 22 | 23 | const Trip = mongoose.model('trip', tripSchema); 24 | 25 | // parks schema 26 | const parkSchema = new Schema({ 27 | name: { type: String, required: true }, 28 | }); 29 | 30 | const Park = mongoose.model('park', parkSchema); 31 | 32 | // users schema 33 | const userSchema = new Schema({ 34 | username: { type: String, required: true, unique: true }, 35 | password: { type: String, required: true }, 36 | trips: [{ 37 | type: Schema.Types.ObjectId, 38 | ref: 'trip' 39 | }] 40 | }); 41 | 42 | const User = mongoose.model('user', userSchema); 43 | 44 | // sessions schema 45 | const sessionSchema = new Schema({ 46 | cookieId: { type: String, required: true, unique: true }, 47 | createdAt: { type: Date, expires: 3600, default: Date.now } 48 | }); 49 | 50 | const Session = mongoose.model('session', sessionSchema); 51 | 52 | // export all models 53 | module.exports = { Trip, Park, User, Session }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "imagine", 3 | "version": "1.0.0", 4 | "description": "codesmith wcri 53 project #1", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "concurrently \"nodemon server/server.js\" \"webpack-dev-server --mode development --open --hot\"", 8 | "build": "webpack --mode production", 9 | "dev": "nodemon server/server.js" 10 | }, 11 | "author": "Krisette Odegard", 12 | "license": "ISC", 13 | "dependencies": { 14 | "bcrypt": "^5.0.1", 15 | "connect-mongodb-session": "^3.1.1", 16 | "cookie-parser": "^1.4.6", 17 | "express": "^4.18.1", 18 | "express-session": "^1.17.3", 19 | "jsonwebtoken": "^8.5.1", 20 | "luxon": "^3.0.4", 21 | "react": "^18.2.0", 22 | "react-dom": "^18.2.0", 23 | "react-router-dom": "^6.4.1", 24 | "sass": "^1.55.0" 25 | }, 26 | "devDependencies": { 27 | "@babel/core": "^7.19.3", 28 | "@babel/preset-env": "^7.19.3", 29 | "@babel/preset-react": "^7.18.6", 30 | "babel-loader": "^8.2.5", 31 | "concurrently": "^7.4.0", 32 | "css-loader": "^6.7.1", 33 | "dotenv": "^16.0.2", 34 | "eslint": "^8.24.0", 35 | "eslint-import-resolver-webpack": "^0.13.2", 36 | "eslint-plugin-import": "^2.26.0", 37 | "eslint-plugin-react": "^7.31.8", 38 | "file-loader": "^6.2.0", 39 | "html-webpack-plugin": "^5.5.0", 40 | "mongoose": "^6.6.2", 41 | "nodemon": "^2.0.20", 42 | "sass-loader": "^13.0.2", 43 | "style-loader": "^3.3.1", 44 | "web-dev-server": "^3.0.27", 45 | "webpack": "^5.74.0", 46 | "webpack-cli": "^4.10.0", 47 | "webpack-dev-server": "^4.11.1" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /client/components/CreateUser.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | 3 | export default function CreateUser() { 4 | const [username, setUsername] = useState(''); 5 | const [password, setPassword] = useState(''); 6 | const [createStatus, setCreateStatus] = useState(''); 7 | 8 | const handleSubmit = (e) => { 9 | e.preventDefault(); 10 | const user = { username, password }; 11 | fetch('/users/signup', { 12 | method: 'POST', 13 | headers: { 14 | 'Content-Type': 'application/json' 15 | }, 16 | body: JSON.stringify(user) 17 | }).then(() => { 18 | setCreateStatus('User account created! Please login.'); 19 | }).catch(err => { 20 | console.log(err); 21 | setCreateStatus('Error creating user account.'); 22 | }); 23 | } 24 | 25 | return ( 26 |
27 |

Create an Imagine Account

28 |
29 |
30 | 31 | setUsername(e.target.value)} /> 32 |
33 |
34 | 35 | setPassword(e.target.value)} /> 36 |
37 |
38 | 39 | 40 |
41 |
42 |

{createStatus}

43 |
44 | ) 45 | } 46 | -------------------------------------------------------------------------------- /client/components/Home.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useEffect, useState } from 'react'; 3 | import AllTrips from './trip/AllTrips'; 4 | import NewTrip from './Trip/NewTrip'; 5 | import logo from '../images/logo.png'; 6 | import UpcomingTrips from './trip/UpcomingTrip'; 7 | 8 | const Home = ( { user, userID, allTripsShown } ) => { 9 | const [trips, setTrips] = useState(null); 10 | const [isShown, setIsShown] = useState(false); 11 | const [upcomingTrip, setUpcomingTrip] = useState({}); 12 | 13 | useEffect(() => { 14 | const getTrips = () => { 15 | fetch(`/trips/${userID}`) 16 | .then(res => res.json()) 17 | .then(data => { 18 | setTrips(data); 19 | }) 20 | .catch(err => console.log(err)); 21 | } 22 | getTrips(); 23 | }, [trips]); 24 | 25 | useEffect(() => { 26 | const getUpcomingTrip = () => { 27 | fetch(`/trips/upcoming/${userID}`) 28 | .then(res => res.json()) 29 | .then(data => { 30 | setUpcomingTrip(data); 31 | }) 32 | .catch(err => console.log(err)); 33 | } 34 | getUpcomingTrip(); 35 | }, [upcomingTrip]); 36 | 37 | const handleClick = () => { 38 | setIsShown(current => !current); 39 | } 40 | 41 | return ( 42 |
43 | Imagine Logo 44 | 45 | {allTripsShown && } 46 |
47 | {!isShown && ()} 48 |
49 | {isShown && } 50 |
51 | ) 52 | } 53 | 54 | export default Home; -------------------------------------------------------------------------------- /client/components/trip/Trip.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import '../../stylesheets/_trips.scss'; 3 | import TripDetails from './TripDetails'; 4 | 5 | export default function Trip( {trip} ) { 6 | // component hide status state 7 | const [isDetailShown, setIsDetailShown] = useState(false); 8 | 9 | const dateConverter = (date) => { 10 | const newDate = new Date(date); 11 | return newDate.toLocaleString('en-US', { timeZone: 'UTC', year: 'numeric', month: '2-digit', day: '2-digit' }); 12 | } 13 | 14 | // prompt user to confirm deletion 15 | const deletePrompt = (e) => { 16 | const confirmDelete = window.confirm('Are you sure you want to delete this trip?'); 17 | if (confirmDelete) { 18 | deleteTrip(e); 19 | } 20 | } 21 | 22 | // delete trip from db 23 | const deleteTrip = (e) => { 24 | fetch(`/trips/${trip._id}`, { 25 | method: 'DELETE', 26 | headers: { "Content-Type": "application/json" }, 27 | }).then(() => { 28 | console.log('deleted trip'); 29 | }).catch(err => console.log(err)); 30 | } 31 | 32 | const handleClick = () => { 33 | setIsDetailShown(current => !current); 34 | } 35 | 36 | /* currently trip-parks is hardcoded to WDW. will eventually change to dynamic fetching from db when all disney resorts are added */ 37 | return ( 38 |
39 |
40 |
{dateConverter(trip.start_date)} - {dateConverter(trip.end_date)}
41 |
Walt Disney World
42 |
43 | 44 |
45 |
46 |
47 | {isDetailShown && } 48 |
49 | ) 50 | } -------------------------------------------------------------------------------- /server/server.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const path = require('path'); 3 | const mongoose = require('mongoose'); 4 | const cookieParser = require('cookie-parser'); 5 | const session = require('express-session'); 6 | const MongoDBStore = require('connect-mongodb-session')(session) 7 | // for storing secrets 8 | require('dotenv').config({ path: path.resolve(__dirname, './.env') }) 9 | const MONGO_URI = process.env.MONGO_URI; 10 | 11 | // start express app! 12 | const app = express(); 13 | 14 | // body parser & cookie parser 15 | app.use(express.json()); 16 | app.use(cookieParser()); 17 | 18 | // set up mongostore 19 | const mongoDBstore = new MongoDBStore({ 20 | uri: MONGO_URI, 21 | collection: 'sessions', 22 | }) 23 | 24 | app.use(session({ 25 | secret: process.env.SECRET, 26 | name: 'ssid', 27 | store: mongoDBstore, 28 | cookie: { 29 | maxAge: Number(process.env.MAX_AGE), 30 | sameSite: false, 31 | secure: false, // turn on in production 32 | }, 33 | resave: true, 34 | saveUninitialized: false, 35 | }) 36 | ) 37 | // serve static files 38 | app.use('/images', express.static(path.resolve(__dirname, '../client/images'))); 39 | 40 | 41 | // declare route handlers 42 | const tripRouter = require(path.join(__dirname, '/routes/trips.js')); 43 | const userRouter = require(path.join(__dirname, '/routes/users.js')); 44 | 45 | // route handler for trips 46 | app.use('/trips', tripRouter); 47 | app.use('/users', userRouter); 48 | 49 | // root route handler 50 | app.get('/', (req, res) => { 51 | res.sendFile(path.join(__dirname, '../client/index.html')); 52 | }); 53 | 54 | // connect to db and start server 55 | mongoose.connect(MONGO_URI, { 56 | useNewUrlParser: true, 57 | useUnifiedTopology: true, 58 | dbName: 'imagine' }) 59 | .then(() => { 60 | app.listen(process.env.PORT, () => { 61 | console.log(`db connected & server listening on port ${process.env.PORT}`); 62 | }); 63 | }) 64 | .catch((err) => { 65 | console.log('error connecting to db', err); 66 | }); 67 | 68 | // error handler for unknown route 69 | app.use((req, res) => res.status(404).send("404: No magic here!")); 70 | 71 | -------------------------------------------------------------------------------- /client/components/Login.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import CreateUser from './CreateUser.js'; 3 | import '../stylesheets/_login.scss'; 4 | import logo from '../images/logo.png'; 5 | 6 | const Login = ( {setLoginStatus, setUser, setUserID} ) => { 7 | const [username, setUsername] = useState(''); 8 | const [password, setPassword] = useState(''); 9 | // const [loginDisplayStatus, setLoginDisplayStatus] = useState(''); 10 | const [isShown, setIsShown] = useState(false); 11 | 12 | const handleSubmit = e => { 13 | e.preventDefault(); 14 | const user = { username, password }; 15 | fetch('/users/login', { 16 | method: 'POST', 17 | headers: { 18 | 'Content-Type': 'application/json' 19 | }, 20 | body: JSON.stringify(user) 21 | }) 22 | .then(res => res.json()) 23 | .then(data => { 24 | console.log(data); 25 | if (data.status === 200) { 26 | console.log(data); 27 | setLoginStatus(true); 28 | setUser(username); 29 | setUserID(data.id); 30 | // redirect to home page 31 | 32 | } 33 | }) 34 | .catch(err => { 35 | if (err.status === 401) { 36 | setLoginDisplayStatus('Invalid username or password.'); 37 | } 38 | }); 39 | } 40 | 41 | const handleClick = () => { 42 | setIsShown(current => !current); 43 | } 44 | 45 | return ( 46 |
47 |
48 |
49 | Imagine Logo 50 |
51 |
52 |
53 | 54 | setUsername(e.target.value)} required /> 55 |
56 |
57 | 58 | setPassword(e.target.value)} required /> 59 |
60 |
61 | 62 | 63 |
64 |
65 |
66 |
Don't have an account? Create one
67 | {isShown && } 68 |
69 | ) 70 | } 71 | 72 | export default Login; -------------------------------------------------------------------------------- /client/stylesheets/_login.scss: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css2?family=Public+Sans&display=swap'); 2 | @import '_colors'; 3 | 4 | .user-container { 5 | font-family: Arial, Helvetica, sans-serif; 6 | max-width: 935px; 7 | } 8 | 9 | .login { 10 | background-color: rgba(255, 255, 255, 0.5); 11 | border: 1px solid rgb(221, 221, 221); 12 | box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.12), 0px 6px 16px 0px rgba(0, 0, 0, 0.24); 13 | border-radius: 12px; 14 | height: 20%; 15 | display: flex; 16 | flex-direction: column; 17 | align-items: center; 18 | justify-content: center; 19 | padding: 25px; 20 | margin-bottom: 25px; 21 | } 22 | 23 | #login-title { 24 | font-size: 2rem; 25 | font-weight: bold; 26 | margin-bottom: 1rem; 27 | text-align: center; 28 | font-family: 'Public Sans', sans-serif; 29 | } 30 | 31 | #username-field, #password-field, #button-container { 32 | position: relative; 33 | display: flex; 34 | flex-direction: column; 35 | align-items: center; 36 | } 37 | 38 | #username-field:focus-within label { 39 | transform: translate(15px, 12px) scale(0.8); 40 | color: gray; 41 | } 42 | 43 | #username-field .filled { 44 | transform: translate(15px, 12px) scale(0.8); 45 | color: gray; 46 | } 47 | 48 | #username-field label { 49 | position: absolute; 50 | pointer-events: none; 51 | transform: translate(15px, 20px) scale(1); 52 | transform-origin: top left; 53 | transition: 200ms cubic-bezier(0, 0, 0.2, 1) 0ms; 54 | color: gray; 55 | font-size: 12px; 56 | line-height: 1; 57 | left: 16px; 58 | } 59 | 60 | #password-field label { 61 | position: absolute; 62 | pointer-events: none; 63 | transform: translate(15px, 20px) scale(1); 64 | transform-origin: top left; 65 | transition: 200ms cubic-bezier(0, 0, 0.2, 1) 0ms; 66 | color: gray; 67 | font-size: 12px; 68 | line-height: 1; 69 | left: 16px; 70 | } 71 | 72 | #password-field:focus-within label { 73 | transform: translate(15px, 12px) scale(0.8); 74 | color: gray; 75 | } 76 | 77 | #password-field .filled { 78 | transform: translate(15px, 12px) scale(0.8); 79 | color: gray; 80 | } 81 | 82 | #login-input { 83 | max-width: 80%; 84 | padding: 15px 5px 5px 5px; 85 | margin: 8px 0; 86 | display: inline-block; 87 | border: 1px solid #ccc; 88 | border-radius: 4px; 89 | box-sizing: border-box; 90 | color: gray; 91 | width: 30vh; 92 | font-size: 12px; 93 | } 94 | 95 | #login-button { 96 | width: 30vh; 97 | max-width: 80%; 98 | margin-top: 10px; 99 | cursor: pointer; 100 | font-size: 12px; 101 | background-image: linear-gradient($secondary-color, $primary-color); 102 | border: 0; 103 | border-radius: 5px; 104 | color: #fff; 105 | padding: 5px 20px; 106 | } 107 | 108 | .sign-up, .create-user { 109 | background-color: rgba(255, 255, 255, 0.5); 110 | border: 1px solid rgb(221, 221, 221); 111 | box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.12), 0px 6px 16px 0px rgba(0, 0, 0, 0.24); 112 | border-radius: 12px; 113 | padding: 10px; 114 | display: flex; 115 | flex-direction: column; 116 | align-items: center; 117 | justify-content: center; 118 | font-size: 12px; 119 | color: gray; 120 | box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.2), 0 2px 8px 0 rgba(0, 0, 0, 0.19); 121 | margin-bottom: 25px; 122 | } -------------------------------------------------------------------------------- /client/stylesheets/_trips.scss: -------------------------------------------------------------------------------- 1 | @import '_colors'; 2 | 3 | .all-trips { 4 | width: 100%; 5 | } 6 | 7 | .trip-row { 8 | display: flex; 9 | flex-direction: row; 10 | align-items: center; 11 | justify-content: space-evenly; 12 | margin: 1rem auto; 13 | padding: 0.5rem; 14 | width: 100%; 15 | border-radius: 12px; 16 | background-color: rgba(255, 255, 255, 0.3); 17 | } 18 | 19 | .trip-dates { 20 | flex: 0 1 auto; 21 | width: 12rem; 22 | } 23 | 24 | .trip-parks { 25 | flex: 0 1 auto; 26 | width: 10rem; 27 | } 28 | 29 | .trip-delete { 30 | flex: 0 1 auto; 31 | width: 2rem; 32 | text-align: center; 33 | } 34 | 35 | #details-edit-text { 36 | text-align: center; 37 | } 38 | 39 | .trip-details { 40 | display: flex; 41 | flex-direction: column; 42 | align-items: left; 43 | justify-content: left; 44 | width: 60%; 45 | margin: 0 auto 1rem; 46 | } 47 | 48 | .park-details { 49 | display: flex; 50 | flex-direction: row; 51 | justify-content: space-between; 52 | text-align: left; 53 | } 54 | 55 | #park-details-name-and-date { 56 | display: flex; 57 | align-items: center; 58 | justify-content: center; 59 | margin-right: 1rem; 60 | } 61 | 62 | #park-details-reservation { 63 | display: flex; 64 | align-items: center; 65 | justify-content: center; 66 | } 67 | 68 | .res-checkbox, .park-checkbox { 69 | accent-color: $primary-dark-color; 70 | } 71 | 72 | .new-trip { 73 | margin-top: 20px; 74 | 75 | input { 76 | max-width: 80%; 77 | padding: 5px; 78 | border: 1px solid #ccc; 79 | border-radius: 4px; 80 | box-sizing: border-box; 81 | color: $text-color; 82 | font-size: 12px; 83 | } 84 | } 85 | 86 | .new-date-range, .new-hotel { 87 | display: flex; 88 | flex-direction: row; 89 | justify-content: left; 90 | align-items: center; 91 | } 92 | 93 | .new-date-range { 94 | margin: 10px; 95 | } 96 | 97 | .new-hotel { 98 | margin-top: 5px; 99 | } 100 | 101 | .new-parks { 102 | display: flex; 103 | flex-direction: column; 104 | justify-content: left; 105 | align-items: left; 106 | margin: 10px; 107 | } 108 | 109 | .new-trip-2nd-row { 110 | display: flex; 111 | flex-direction: row; 112 | justify-content: space-between; 113 | align-items: flex-start; 114 | } 115 | 116 | #start-date, #new-hotel-label-container { 117 | margin-right: 10px; 118 | align-items: flex-start; 119 | } 120 | 121 | .new-hotel-input-container { 122 | width: auto; 123 | justify-content: right; 124 | } 125 | 126 | small { 127 | color: $secondary-black; 128 | } 129 | 130 | .add-trip-button-container { 131 | display: flex; 132 | justify-content: center; 133 | align-items: center; 134 | } 135 | 136 | #details-button{ 137 | cursor: pointer; 138 | font-size: 12px; 139 | background-color: transparent; 140 | border: 1px solid $primary-color; 141 | border-radius: 5px; 142 | color: $secondary-black; 143 | padding: 5px 20px; 144 | transition: background-color 0.4s ease-in-out; 145 | } 146 | 147 | #details-button:hover { 148 | background-color: $tertiary-color; 149 | } 150 | 151 | #trip-details-change-menu, #submit-changes-container { 152 | display: flex; 153 | justify-content: center; 154 | align-items: center; 155 | } 156 | 157 | #submit-changes-container { 158 | margin-top: 10px; 159 | } -------------------------------------------------------------------------------- /server/middleware/tripController.js: -------------------------------------------------------------------------------- 1 | const models = require('../models'); 2 | 3 | const tripController = { 4 | // create 5 | createTrip: (req, res, next) => { 6 | console.log(req.body); 7 | const { start_date, end_date, hotel, parks, user_id } = req.body; 8 | models.Trip.create({ start_date: start_date, end_date: end_date, hotel: hotel, parks: parks, user_id: user_id }) 9 | .then(data => next()) 10 | .catch(err => { 11 | return next({ 12 | log: `tripController.createTrip: ERROR: ${err}`, 13 | message: { err: 'Could not add trip' }, 14 | }); 15 | }); 16 | }, 17 | 18 | // read 19 | getTrips: (req, res, next) => { 20 | models.Trip.find( { user_id: req.session.user.id } ).sort( { start_date: 1 } ) 21 | .then(data => { 22 | res.locals.trips = data; 23 | return next(); 24 | }) 25 | .catch(err => { 26 | return next({ 27 | log: `tripController.getTrips: ERROR: ${err}`, 28 | message: { err: 'Could not find characters' }, 29 | }); 30 | }); 31 | }, 32 | 33 | getOneTrip: (req, res, next) => { 34 | models.Trip.findById(req.params.id) 35 | .then(data => { 36 | res.locals.trip = data; 37 | return next(); 38 | }) 39 | .catch(err => { 40 | return next({ 41 | log: `tripController.getOneTrip: ERROR: ${err}`, 42 | message: { err: 'Could not find character' }, 43 | }); 44 | }); 45 | }, 46 | 47 | getUpcomingTrip: (req, res, next) => { 48 | models.Trip.findOne({ 49 | start_date: 50 | { $gte: new Date() }, 51 | user_id: req.session.user.id 52 | }).sort( { start_date: 1 }) 53 | .then(data => { 54 | res.locals.trip = data; 55 | console.log(res.locals.trip); 56 | return next(); 57 | }) 58 | .catch(err => { 59 | return next({ 60 | log: `tripController.getUpcomingTrip: ERROR: ${err}`, 61 | message: { err: 'Could not find character' }, 62 | }); 63 | }); 64 | }, 65 | 66 | // get list of parks from db for checkboxes in new trip form 67 | getParks: (req, res, next) => { 68 | models.Park.find() 69 | .then(data => { 70 | res.locals.parks = data; 71 | return next(); 72 | }) 73 | .catch(err => { 74 | return next({ 75 | log: `tripController.getParks: ERROR: ${err}`, 76 | message: { err: 'Could not find parks' }, 77 | }); 78 | }); 79 | }, 80 | 81 | // update 82 | updateTrip: (req, res, next) => { 83 | const { id } = req.params; 84 | const { start_date, end_date, hotel, parks } = req.body; 85 | models.Trip.findByIdAndUpdate(id, { start_date: start_date, end_date: end_date, hotel: hotel, parks: parks }) 86 | .then(data => next()) 87 | .catch(err => { 88 | return next({ 89 | log: `tripController.updateTrip: ERROR: ${err}`, 90 | message: { err: 'Could not update trip' }, 91 | }); 92 | }); 93 | }, 94 | 95 | // delete 96 | deleteTrip: (req, res, next) => { 97 | models.Trip.deleteOne({ _id: req.params.id }) 98 | .then(data => next()) 99 | .catch(err => { 100 | return next({ 101 | log: `tripController.deleteTrip: ERROR: ${err}`, 102 | message: { err: 'Could not delete trip' }, 103 | }); 104 | }); 105 | }, 106 | 107 | }; 108 | 109 | module.exports = tripController; -------------------------------------------------------------------------------- /server/middleware/userController.js: -------------------------------------------------------------------------------- 1 | const models = require('../models'); 2 | const bcrypt = require('bcrypt'); 3 | 4 | 5 | const userController = { 6 | 7 | // create new user 8 | createUser: (req, res, next) => { 9 | const { username, password } = req.body; 10 | // console.log(req.body); 11 | // console.log('is createUser working?') 12 | if (username === '' || password === '') { 13 | return next({ 14 | log: 'userController.createUser: ERROR: Missing username or password', 15 | message: { err: 'Missing username or password' }, 16 | }); 17 | } 18 | 19 | // hash password asynchronously before storing in db 20 | const saltRounds = 10; 21 | bcrypt.hash(password, saltRounds, (err, hash) => { 22 | if (err) { 23 | return next({ 24 | log: `userController.createUser: ERROR: Error hashing password: ${err}`, 25 | message: { err: 'Error hashing password' }, 26 | }); 27 | } 28 | 29 | // store hashed password in db with new user 30 | models.User.create({ username: username, password: hash }) 31 | .then(data => { 32 | res.locals.userID = data._id; 33 | let token = jwt.sign({ id: res.locals.userID }, process.env.JWT_SECRET, { 34 | expiresIn: 86400 35 | }); 36 | req.session.token = token; 37 | return next()}) 38 | .catch(err => { 39 | return next({ 40 | log: `userController.createUser: ERROR: Error creating user: ${err}`, 41 | message: { err: 'Error creating user' }, 42 | }); 43 | }); 44 | }); 45 | }, 46 | 47 | // verify user logging in 48 | verifyUser: (req, res, next) => { 49 | console.log(`user is logging in with ${req.body}`) 50 | const { username, password } = req.body; 51 | 52 | // if no username or password, return error 53 | if (!username || !password) { 54 | return next({ 55 | log: 'userController.verifyUser: ERROR: Missing username or password', 56 | message: { err: 'Missing username or password' }, 57 | }); 58 | } 59 | 60 | // hash password 61 | const saltRounds = 10; 62 | bcrypt.hash(password, saltRounds, (err, hash) => { 63 | if (err) { 64 | return next({ 65 | log: `userController.verifyUser: ERROR: Error hashing password: ${err}`, 66 | message: { err: 'Error hashing password' }, 67 | }); 68 | } 69 | console.log('successfully hashed') 70 | // find user in db 71 | models.User.findOne({ username: username }) 72 | .then(data => { 73 | // compare hashed password with password in db 74 | bcrypt.compare(password, data.password, (err, result) => { 75 | // if other error, return error 76 | if (err) { 77 | return next({ 78 | log: `userController.verifyUser: ERROR: Error comparing passwords: ${err}`, 79 | message: { err: 'Error comparing passwords' }, 80 | }); 81 | } 82 | // if passwords match, store user id as userSession 83 | if (result) { 84 | console.log('successfully verified user') 85 | console.log(data); 86 | const userSession = { id: data._id }; 87 | console.log(req.session); 88 | req.session.user = userSession; 89 | res.locals.userID = data._id; 90 | return next(); 91 | } else { 92 | // if passwords don't match, return error 93 | return next({ 94 | log: 'userController.verifyUser: ERROR: Incorrect password', 95 | message: { err: 'Incorrect password' }, 96 | }); 97 | } 98 | }); 99 | }) 100 | // if user not found, return error 101 | .catch(err => { 102 | return next({ 103 | log: `userController.verifyUser: ERROR: Error finding user: ${err}`, 104 | message: { err: 'Error finding user' }, 105 | }); 106 | }); 107 | }); 108 | } 109 | } 110 | 111 | module.exports = userController; -------------------------------------------------------------------------------- /client/components/trip/NewTrip.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react'; 2 | import { useState } from 'react'; 3 | 4 | const NewTrip = ({ userID, setIsShown }) => { 5 | const [start_date, setStartDate] = useState(''); 6 | const [end_date, setEndDate] = useState(''); 7 | const [hotel, setHotel] = useState(''); 8 | const [parksData, setParksData] = useState(''); 9 | const [parks, setParks] = useState([]); 10 | 11 | // adds new trip to db 12 | const handleSubmit = (e) => { 13 | e.preventDefault(); 14 | const trip = { start_date, end_date, hotel, parks, user_id: userID }; 15 | setIsShown(false); 16 | 17 | fetch('/trips', { 18 | method: 'POST', 19 | headers: { "Content-Type": "application/json" }, 20 | body: JSON.stringify(trip) 21 | }).then(() => { 22 | console.log('new trip added'); 23 | setIsShown(false); 24 | }).catch(err => console.log(err)); 25 | } 26 | 27 | // fetch list of parks from api and store in state 28 | useEffect(() => { 29 | fetch('/trips/parks') 30 | .then(res => res.json()) 31 | .then(data => { 32 | setParksData(data); 33 | }) 34 | .catch(err => console.log(err)); 35 | }, []); 36 | 37 | // handle parkscheckbox change 38 | const handleParkCheck = (e) => { 39 | const parkIndex = e.target.value; 40 | const parkName = parksData[parkIndex].name; 41 | // if checkbox is checked, add park object to parks array 42 | if (e.target.checked) { 43 | const parkObj = { name: parkName, date: '', reservations: false }; 44 | setParks(current => { 45 | return [...current, parkObj]; 46 | }); 47 | } 48 | else { 49 | // if checkbox is unchecked, remove park object from parks array 50 | setParks(current => current.filter(park => park.name !== parkName)); 51 | } 52 | } 53 | 54 | // TODO: convert all date handlers to moment.js or luxon 55 | // disabling below for demo 56 | /* const startDateValidator = (value) => { 57 | const today = new Date().toJSON().slice(0, 10) 58 | if (value < today) { 59 | return alert('You cannot travel back in time! Please pick a date in the future.'); 60 | } 61 | else { 62 | setStartDate(value); 63 | } 64 | } 65 | 66 | const endDateValidator = (value) => { 67 | if (value < start_date) { 68 | return alert('You cannot end your trip before it begins!\nPlease pick a new end date.'); 69 | } 70 | else { 71 | setEndDate(value); 72 | } 73 | } */ 74 | 75 | return ( 76 |
77 |
78 |
79 |
80 | 81 | setStartDate(e.target.value)} required /> 82 |
83 |
84 | 85 | setEndDate(e.target.value)} required /> 86 |
87 |
88 |
89 |
90 | (Select all that apply) 91 | {parksData && parksData.map((parkObj, index) => { 92 | return ( 93 |
94 | 95 | {parkObj.name} 96 |
97 | ); 98 | })} 99 |
100 | 101 |
102 |
103 | 104 |
105 |
106 | setHotel(e.target.value)} required /> 107 |
108 |
109 |
110 |
111 | 112 |
113 |
114 |
115 | ) 116 | } 117 | 118 | export default NewTrip; -------------------------------------------------------------------------------- /client/components/trip/TripDetails.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { DateTime } from "luxon"; 3 | 4 | export default function TripDetails( {trip, setIsDetailShown} ) { 5 | // declare all state variables needed 6 | const [start_date, setStartDate] = useState(trip.start_date); 7 | const [end_date, setEndDate] = useState(trip.end_date); 8 | const [hotel, setHotel] = useState(trip.hotel); 9 | const [parks, setParks] = useState(trip.parks); 10 | 11 | // update start date 12 | const updateStartDate = (e) => { 13 | const newStartDate = prompt('What is the new start date? (YYYY-MM-DD)'); 14 | if (newStartDate) { 15 | const convertedDate = new Date.UTC(newStartDate); 16 | console.log(convertedDate); 17 | setStartDate(convertedDate); 18 | } 19 | } 20 | 21 | // update end date 22 | const updateEndDate = (e) => { 23 | const newEndDate = prompt('What is the new end date? (YYYY-MM-DD)'); 24 | if (newEndDate) { 25 | setEndDate(newEndDate); 26 | } 27 | } 28 | 29 | // update hotel 30 | const updateHotel = (e) => { 31 | const newHotel = prompt('What is the new hotel?'); 32 | if (newHotel) { 33 | setHotel(newHotel); 34 | } 35 | } 36 | 37 | // update specific park dates 38 | const updateParkDate = (e, id) => { 39 | const newParkDate = prompt('What is the new park date? (YYYY-MM-DD)'); 40 | // change park date in state to new date 41 | if (newParkDate) { 42 | const parkIndex = id; 43 | const parkName = parks[parkIndex].name; 44 | const parkDate = newParkDate; 45 | const parkRes = parks[parkIndex].reservations; 46 | const parkObj = { name: parkName, date: parkDate, reservations: parkRes }; 47 | setParks(current => { 48 | const newParks = current.filter(park => park.name !== parkName); 49 | return [...newParks, parkObj]; 50 | }); 51 | } 52 | } 53 | 54 | // update whether or not you made park reservations 55 | const handleResCheck = (e) => { 56 | if (e.target.checked) { 57 | // change park reservation status to true 58 | const parkIndex = e.target.value; 59 | const parkName = parks[parkIndex].name; 60 | const parkDate = parks[parkIndex].date; 61 | const parkObj = { name: parkName, date: parkDate, reservations: true }; 62 | setParks(current => { 63 | const newParks = current.filter(park => park.name !== parkName); 64 | return [...newParks, parkObj]; 65 | }); 66 | } else { 67 | // change park reservation status to false 68 | const parkIndex = e.target.value; 69 | const parkName = parks[parkIndex].name; 70 | const parkDate = parks[parkIndex].date; 71 | const parkObj = { name: parkName, date: parkDate, reservations: false }; 72 | setParks(current => { 73 | const newParks = current.filter(park => park.name !== parkName); 74 | return [...newParks, parkObj]; 75 | }); 76 | } 77 | } 78 | 79 | // submits updated trip to database 80 | const handleSubmit = (e) => { 81 | e.preventDefault(); 82 | setIsDetailShown(false); 83 | const updatetrip = { start_date, end_date, hotel, parks }; 84 | fetch(`/trips/${trip._id}`, { 85 | method: 'PUT', 86 | headers: { "Content-Type": "application/json" }, 87 | body: JSON.stringify(updatetrip) 88 | }).then(() => { 89 | console.log('updated trip'); 90 | }).catch(err => console.log(err)); 91 | } 92 | 93 | const dateConverter = (date) => { 94 | const newDate = new Date(date); 95 | return newDate.toLocaleString('en-US', { timeZone: 'UTC', year: 'numeric', month: '2-digit', day: '2-digit' }); 96 | } 97 | 98 | const adrCalculator = (date) => { 99 | let result = DateTime.fromISO(date).minus({days: 60}).toLocaleString('en-US', { timeZone: 'UTC', year: 'numeric', month: '2-digit', day: '2-digit' }); 100 | return result; 101 | } 102 | 103 | // converts invalid date from db to "pick a date" 104 | const invalidDate = (date) => { 105 | if (dateConverter(date) === '01/01/1970') { 106 | return 'Pick a date for'; 107 | } else { 108 | return dateConverter(date); 109 | } 110 | } 111 | 112 | return ( 113 |
114 | {trip.parks.map((park, index) => { 115 | return ( 116 |
117 | 118 |
119 | Reservation
120 |
121 | ) 122 | })} 123 |

Staying at {trip.hotel}

124 |

You can begin to make Advanced Dining Reservations on {adrCalculator(trip.start_date)}.

125 | 126 |
127 |
128 | ) 129 | } --------------------------------------------------------------------------------