├── 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 |
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 |
18 | current wait times
19 |
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 |
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 |
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 |

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 |

50 |
51 |
65 |
66 |
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 |
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 |
124 |
125 |
126 |
127 |
128 | )
129 | }
--------------------------------------------------------------------------------