├── .vscode └── settings.json ├── public ├── favicon.ico ├── logo192.png ├── logo512.png ├── robots.txt ├── manifest.json └── index.html ├── config ├── ensureLoggedIn.js ├── database.js └── checkToken.js ├── src ├── setupTests.js ├── App.test.js ├── reportWebVitals.js ├── utilities │ ├── activity-service.js │ ├── trip-service.js │ ├── users-api.js │ ├── helper-api.js │ └── users-service.js ├── index.js ├── App.css ├── pages │ ├── AuthPage.js │ ├── NewTripPage │ │ └── NewTrip.js │ ├── DisplayTripPage │ │ └── DisplayTrip.js │ ├── HomePage │ │ └── Home.js │ └── EditTripPage │ │ └── EditTrip.js ├── components │ ├── NavBar.js │ ├── LogInForm.js │ ├── SignUpForm.js │ ├── AddActivity │ │ └── ActivityDialog.js │ └── AddTrip │ │ └── AddTrip.js ├── App.js ├── logo.svg └── index.css ├── tailwind.config.js ├── .gitignore ├── routes └── api │ ├── users.js │ ├── trips.js │ └── activities.js ├── models ├── activity.js ├── tripSchema.js └── user.js ├── controllers └── api │ ├── activities.js │ ├── users.js │ └── trips.js ├── package.json ├── server.js └── README.md /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tmp03099/Travel-Planner/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tmp03099/Travel-Planner/HEAD/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tmp03099/Travel-Planner/HEAD/public/logo512.png -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /config/ensureLoggedIn.js: -------------------------------------------------------------------------------- 1 | module.exports = function(req, res, next) { 2 | // Status code of 401 is Unauthorized 3 | if (!req.user) return res.status(401).json('Unauthorized'); 4 | // A okay 5 | next(); 6 | }; -------------------------------------------------------------------------------- /src/setupTests.js: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | -------------------------------------------------------------------------------- /src/App.test.js: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react'; 2 | import App from './App'; 3 | 4 | test('renders learn react link', () => { 5 | render(); 6 | const linkElement = screen.getByText(/learn react/i); 7 | expect(linkElement).toBeInTheDocument(); 8 | }); 9 | -------------------------------------------------------------------------------- /config/database.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | 3 | mongoose.set('strictQuery', true); 4 | mongoose.connect(process.env.DATABASE_URL); 5 | 6 | const db = mongoose.connection; 7 | 8 | db.on('connected', function () { 9 | console.log(`Connected to ${db.name} at ${db.host}:${db.port}`); 10 | }); -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | const withMT = require("@material-tailwind/react/utils/withMT"); 2 | 3 | module.exports = withMT({ 4 | content: [ 5 | "./src/**/*.{js,jsx,ts,tsx}", 6 | "./node_modules/react-tailwindcss-datepicker/dist/index.esm.js", 7 | ], 8 | theme: { 9 | extend: {}, 10 | }, 11 | plugins: [], 12 | }); 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /src/reportWebVitals.js: -------------------------------------------------------------------------------- 1 | const reportWebVitals = onPerfEntry => { 2 | if (onPerfEntry && onPerfEntry instanceof Function) { 3 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 4 | getCLS(onPerfEntry); 5 | getFID(onPerfEntry); 6 | getFCP(onPerfEntry); 7 | getLCP(onPerfEntry); 8 | getTTFB(onPerfEntry); 9 | }); 10 | } 11 | }; 12 | 13 | export default reportWebVitals; 14 | -------------------------------------------------------------------------------- /routes/api/users.js: -------------------------------------------------------------------------------- 1 | //* Routing Logic 2 | 3 | const express = require('express'); 4 | const router = express.Router(); 5 | const usersCtrl = require('../../controllers/api/users'); 6 | const ensureLoggedIn = require('../../config/ensureLoggedIn'); 7 | 8 | 9 | //* POST 10 | router.post('/', usersCtrl.create); 11 | 12 | router.post('/login', usersCtrl.login); 13 | 14 | router.get('/check-token', ensureLoggedIn, usersCtrl.checkToken); 15 | 16 | 17 | 18 | module.exports = router; -------------------------------------------------------------------------------- /routes/api/trips.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const router = express.Router(); 3 | const tripsCtrl = require("../../controllers/api/trips"); 4 | 5 | // GET /api/trip/new 6 | router.get("/", tripsCtrl.getAllTrips); 7 | 8 | router.post("/new", tripsCtrl.addTrip); 9 | 10 | router.get("/:id", tripsCtrl.getTrip); 11 | 12 | router.delete("/:id", tripsCtrl.deleteTrip); 13 | 14 | router.put("/:id", tripsCtrl.updateTrip); 15 | 16 | // POST /api/trip/ 17 | module.exports = router; 18 | -------------------------------------------------------------------------------- /routes/api/activities.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const router = express.Router(); 3 | const activitiesCtrl = require("../../controllers/api/activities"); 4 | 5 | // GET /api/trip/new 6 | 7 | router.post("/new", activitiesCtrl.addActivity); 8 | 9 | router.get("/:id", activitiesCtrl.getActivity); 10 | 11 | router.delete("/:id", activitiesCtrl.deleteActivity); 12 | 13 | router.put("/:id", activitiesCtrl.updateActivity); 14 | 15 | // POST /api/trip/ 16 | module.exports = router; 17 | -------------------------------------------------------------------------------- /models/activity.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | const Schema = mongoose.Schema; 3 | 4 | const activitySchema = new Schema({ 5 | type: { 6 | type: String, 7 | required: true, 8 | }, 9 | name: { 10 | type: String, 11 | required: true, 12 | trim: true, 13 | }, 14 | destination: { 15 | type: String, 16 | required: true, 17 | }, 18 | date: { 19 | type: Date, 20 | }, 21 | notes: { 22 | type: String, 23 | }, 24 | status: { 25 | type: Boolean, 26 | }, 27 | }); 28 | 29 | module.exports = mongoose.model("Activity", activitySchema); 30 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /src/utilities/activity-service.js: -------------------------------------------------------------------------------- 1 | import { sendRequest } from "./helper-api"; 2 | 3 | const ACTIVITY_URL = "/api/activities"; 4 | 5 | export async function getActivity(id) { 6 | return await sendRequest(`${ACTIVITY_URL}/${id}`); 7 | } 8 | 9 | export async function updateActivity(ActivityData) { 10 | return await sendRequest( 11 | `${ACTIVITY_URL}/${ActivityData._id}`, 12 | "PUT", 13 | ActivityData 14 | ); 15 | } 16 | 17 | export async function deleteActivity(id) { 18 | return await sendRequest(`${ACTIVITY_URL}/${id}`, "DELETE"); 19 | } 20 | 21 | export async function createActivity(ActivityData) { 22 | return await sendRequest(`${ACTIVITY_URL}/new`, "POST", ActivityData); 23 | } 24 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom/client"; 3 | import "./index.css"; 4 | import App from "./App"; 5 | import reportWebVitals from "./reportWebVitals"; 6 | import { BrowserRouter as Router } from "react-router-dom"; 7 | 8 | const root = ReactDOM.createRoot(document.getElementById("root")); 9 | root.render( 10 | 11 | 12 | 13 | 14 | 15 | ); 16 | 17 | // If you want to start measuring performance in your app, pass a function 18 | // to log results (for example: reportWebVitals(console.log)) 19 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 20 | reportWebVitals(); 21 | -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | .App-logo { 6 | height: 40vmin; 7 | pointer-events: none; 8 | } 9 | 10 | @media (prefers-reduced-motion: no-preference) { 11 | .App-logo { 12 | animation: App-logo-spin infinite 20s linear; 13 | } 14 | } 15 | 16 | .App-header { 17 | background-color: #282c34; 18 | min-height: 100vh; 19 | display: flex; 20 | flex-direction: column; 21 | align-items: center; 22 | justify-content: center; 23 | font-size: calc(10px + 2vmin); 24 | color: white; 25 | } 26 | 27 | .App-link { 28 | color: #61dafb; 29 | } 30 | 31 | @keyframes App-logo-spin { 32 | from { 33 | transform: rotate(0deg); 34 | } 35 | to { 36 | transform: rotate(360deg); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/utilities/trip-service.js: -------------------------------------------------------------------------------- 1 | import { sendRequest } from "./helper-api"; 2 | 3 | const TRIP_URL = "/api/trips"; 4 | 5 | export async function getTrips() { 6 | const result = await sendRequest(TRIP_URL); 7 | 8 | console.log(result); 9 | return result; 10 | } 11 | 12 | export async function getTrip(id) { 13 | return await sendRequest(`${TRIP_URL}/${id}`); 14 | } 15 | 16 | export async function updateTrip(tripData) { 17 | return await sendRequest(`${TRIP_URL}/${tripData._id}`, "PUT", tripData); 18 | } 19 | 20 | export async function deleteTrip(id) { 21 | return await sendRequest(`${TRIP_URL}/${id}`, "DELETE"); 22 | } 23 | 24 | export async function createTrip(tripData) { 25 | return await sendRequest(`${TRIP_URL}/new`, "POST", tripData); 26 | } 27 | -------------------------------------------------------------------------------- /src/pages/AuthPage.js: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | 3 | import SignUpForm from "../components/SignUpForm"; 4 | import LoginForm from "../components/LogInForm"; 5 | 6 | function AuthPage({ setUser }) { 7 | const [showLogin, setShowLogin] = useState(true); 8 | 9 | return ( 10 |
11 |

Auth Page

12 | 13 | 16 | 17 | {showLogin ? ( 18 | 19 | ) : ( 20 | 21 | )} 22 |
23 | ); 24 | } 25 | 26 | export default AuthPage; 27 | -------------------------------------------------------------------------------- /models/tripSchema.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | const Schema = mongoose.Schema; 3 | 4 | const User = require("./user"); 5 | 6 | const tripSchema = new Schema( 7 | { 8 | user: { type: Schema.Types.ObjectId, ref: "User" }, 9 | name: { type: String, required: true }, 10 | destination: { 11 | type: String, 12 | required: true, 13 | }, 14 | startDate: { 15 | type: Date, 16 | default: Date.now, 17 | }, 18 | endDate: { 19 | type: Date, 20 | default: Date.now, 21 | }, 22 | activities: [ 23 | { 24 | type: Schema.Types.ObjectId, 25 | ref: "Activities", 26 | }, 27 | ], 28 | }, 29 | { 30 | timestamps: true, 31 | } 32 | ); 33 | 34 | module.exports = mongoose.model("Trip", tripSchema); 35 | -------------------------------------------------------------------------------- /src/utilities/users-api.js: -------------------------------------------------------------------------------- 1 | // * The users-service.js module will definitely need to make AJAX requests to the Express server. 2 | 3 | import { sendRequest } from "./helper-api"; 4 | 5 | //* SignUpForm.jsx <--> users-service.js <--> users-api.js <-Internet-> server.js (Express) 6 | 7 | //* handleSubmit <--> [signUp]-users-service <--> [signUp]-users-api <-Internet-> server.js (Express) 8 | 9 | const BASE_URL = "/api/users"; 10 | 11 | //* SignUp 12 | export function signUp(userData) { 13 | return sendRequest(BASE_URL, "POST", userData); 14 | } 15 | 16 | //* Login 17 | export function login(credentials) { 18 | return sendRequest(`${BASE_URL}/login`, "POST", credentials); 19 | } 20 | 21 | //* Check Token 22 | export function checkToken() { 23 | return sendRequest(`${BASE_URL}/check-token`); 24 | } 25 | -------------------------------------------------------------------------------- /config/checkToken.js: -------------------------------------------------------------------------------- 1 | const jwt = require('jsonwebtoken'); 2 | 3 | module.exports = function(req, res, next) { 4 | // Check for the token being sent in a header or as a query parameter 5 | let token = req.get('Authorization') || req.query.token; 6 | if (token) { 7 | // Remove the 'Bearer ' if it was included in the token header 8 | token = token.replace('Bearer ', ''); 9 | // Check if token is valid and not expired 10 | jwt.verify(token, process.env.SECRET, function(err, decoded) { 11 | // If valid token, decoded will be the token's entire payload 12 | // If invalid token, err will be set 13 | req.user = err ? null : decoded.user; 14 | // If your app cares... (optional) 15 | req.exp = err ? null : new Date(decoded.exp * 1000); 16 | return next(); 17 | }); 18 | } else { 19 | // No token was sent 20 | req.user = null; 21 | return next(); 22 | } 23 | }; -------------------------------------------------------------------------------- /src/utilities/helper-api.js: -------------------------------------------------------------------------------- 1 | import { getToken } from "./users-service"; 2 | 3 | /*--- Helper Functions ---*/ 4 | 5 | export async function sendRequest(url, method = "GET", payload = null) { 6 | // Fetch accepts an options object as the 2nd argument 7 | // used to include a data payload, set headers, etc. 8 | const options = { method }; 9 | if (payload) { 10 | options.headers = { "Content-Type": "application/json" }; 11 | options.body = JSON.stringify(payload); 12 | } 13 | 14 | // sends token to backend 15 | const token = getToken(); 16 | 17 | if (token) { 18 | options.headers = options.headers || {}; 19 | options.headers.Authorization = `Bearer ${token}`; 20 | } 21 | 22 | const res = await fetch(url, options); 23 | // res.ok will be false if the status code set to 4xx in the controller action 24 | 25 | if (res.ok) { 26 | return res.json(); 27 | } else { 28 | throw new Error("Bad Request"); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/components/NavBar.js: -------------------------------------------------------------------------------- 1 | import { Link } from "react-router-dom"; 2 | import { logOut } from "../utilities/users-service"; 3 | 4 | function NavBar({ user, setUser }) { 5 | const handleLogOut = () => { 6 | logOut(); 7 | setUser(null); 8 | }; 9 | return ( 10 | 30 | ); 31 | } 32 | 33 | export default NavBar; 34 | -------------------------------------------------------------------------------- /src/pages/NewTripPage/NewTrip.js: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import AddTrip from "../../components/AddTrip/AddTrip"; 3 | import { Button } from "@material-tailwind/react"; 4 | import { createTrip } from "../../utilities/trip-service"; 5 | import { useNavigate } from "react-router-dom"; 6 | 7 | function NewTrip() { 8 | const navigate = useNavigate(); 9 | const [tripData, setTripData] = useState({}); 10 | 11 | const onAddClick = async () => { 12 | await createTrip(tripData); 13 | console.log(tripData); 14 | 15 | navigate("/"); 16 | }; 17 | 18 | return ( 19 |
20 |

21 | NEW TRIP 22 |

23 | 24 | 25 |
26 | 29 |
30 |
31 | ); 32 | } 33 | 34 | export default NewTrip; 35 | -------------------------------------------------------------------------------- /models/user.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | 3 | const Schema = mongoose.Schema; 4 | const bcrypt = require('bcrypt'); 5 | 6 | //* determines how much processing time it will take to perform the hash 7 | const SALT_ROUNDS = 6; // 6 is a reasonable value 8 | 9 | const userSchema = new Schema({ 10 | name: { type: String, required: true }, 11 | email: { 12 | type: String, 13 | unique: true, 14 | trim: true, 15 | lowercase: true, 16 | required: true, 17 | }, 18 | password: { 19 | type: String, 20 | trim: true, 21 | minLength: 3, 22 | required: true, 23 | }, 24 | }, { 25 | timestamps: true, 26 | toJSON: function(doc, ret) { 27 | delete ret.password; 28 | return ret; 29 | } 30 | }); 31 | 32 | //* Pre Hook 33 | userSchema.pre('save', async function(next) { 34 | // if password was NOT modified continue to the next middleware 35 | if (!this.isModified('password')) return next(); 36 | 37 | // update the password with the computed hash 38 | this.password = await bcrypt.hash(this.password, SALT_ROUNDS) 39 | return next(); 40 | }) 41 | 42 | module.exports = mongoose.model("User", userSchema); 43 | -------------------------------------------------------------------------------- /controllers/api/activities.js: -------------------------------------------------------------------------------- 1 | const Activity = require("../../models/activity"); 2 | 3 | async function getActivity(req, res) { 4 | try { 5 | const activity = await Activity.findById(req.params.id); 6 | 7 | res.json(activity); 8 | } catch (e) { 9 | res.status(400).json({ msg: e.message }); 10 | } 11 | } 12 | 13 | async function addActivity(req, res) { 14 | try { 15 | const activity = await Activity.create(req.body); 16 | 17 | console.log(activity); 18 | 19 | res.status(200).json(activity); 20 | } catch (e) { 21 | res.status(400).json({ msg: e.message }); 22 | } 23 | } 24 | 25 | async function updateActivity(req, res) { 26 | try { 27 | const activity = await Activity.findByIdAndUpdate(req.params.id, req.body); 28 | 29 | res.json(activity); 30 | } catch (e) { 31 | res.status(400).json({ msg: e.message }); 32 | } 33 | } 34 | 35 | async function deleteActivity(req, res) { 36 | try { 37 | await Activity.findByIdAndDelete(req.params.id); 38 | 39 | res.status(200).json({}); 40 | } catch (e) { 41 | res.status(400).json({ msg: e.message }); 42 | } 43 | } 44 | 45 | module.exports = { 46 | getActivity, 47 | addActivity, 48 | updateActivity, 49 | deleteActivity, 50 | }; 51 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | 3 | import { Routes, Route } from "react-router-dom"; 4 | 5 | import NavBar from "./components/NavBar"; 6 | import Home from "./pages/HomePage/Home"; 7 | import NewTrip from "./pages/NewTripPage/NewTrip"; 8 | import EditTrip from "./pages/EditTripPage/EditTrip"; 9 | import AuthPage from "./pages/AuthPage"; 10 | 11 | import { getUser } from "./utilities/users-service"; 12 | 13 | import "./App.css"; 14 | import DisplayTrip from "./pages/DisplayTripPage/DisplayTrip"; 15 | 16 | function App() { 17 | const [user, setUser] = useState(getUser()); 18 | 19 | return ( 20 |
21 | {user ? ( 22 | <> 23 | 24 | 25 | } /> 26 | } /> 27 | } /> 28 | } /> 29 | 30 | 31 | ) : ( 32 | 33 | )} 34 |
35 | © My Phung Tieu 36 |
37 |
38 | ); 39 | } 40 | 41 | export default App; 42 | -------------------------------------------------------------------------------- /controllers/api/users.js: -------------------------------------------------------------------------------- 1 | //* Request handler Logic 2 | const User = require("../../models/user"); 3 | const jwt = require("jsonwebtoken"); 4 | const bcrypt = require("bcrypt"); 5 | 6 | //* /*-- Helper Functions --*/ 7 | function createJWT(user) { 8 | return jwt.sign({ user }, process.env.SECRET, { expiresIn: "24h" }); 9 | } 10 | 11 | async function create(req, res) { 12 | // console.log('[From POST handler]', req.body) 13 | try { 14 | //* creating a new user 15 | const user = await User.create(req.body); 16 | console.log(user); 17 | 18 | //* creating a new jwt 19 | const token = createJWT(user); 20 | 21 | res.json(token); 22 | } catch (error) { 23 | console.log(error); 24 | res.status(400).json(error); 25 | } 26 | } 27 | 28 | async function login(req, res) { 29 | try { 30 | // find user in db 31 | const user = await User.findOne({ email: req.body.email }); 32 | // check if we found an user 33 | if (!user) throw new Error(); 34 | // compare the password to hashed password 35 | const match = await bcrypt.compare(req.body.password, user.password); 36 | // check is password matched 37 | if (!match) throw new Error(); 38 | // send back a new token with the user data in the payload 39 | res.json(createJWT(user)); 40 | } catch { 41 | res.status(400).json("Bad Credentials"); 42 | } 43 | } 44 | 45 | async function checkToken(req, res) { 46 | console.log(req.user); 47 | res.json(req.exp); 48 | } 49 | 50 | module.exports = { 51 | create, 52 | login, 53 | checkToken, 54 | }; 55 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mern-infrastructure", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@material-tailwind/react": "^1.4.2", 7 | "@testing-library/jest-dom": "^5.16.5", 8 | "@testing-library/react": "^13.4.0", 9 | "@testing-library/user-event": "^13.5.0", 10 | "bcrypt": "^5.1.0", 11 | "cors": "^2.8.5", 12 | "dayjs": "^1.11.6", 13 | "dotenv": "^16.0.3", 14 | "express": "^4.18.2", 15 | "formik": "^2.2.9", 16 | "jsonwebtoken": "^9.0.0", 17 | "mongoose": "^6.10.5", 18 | "morgan": "^1.10.0", 19 | "primereact": "^9.3.1", 20 | "react": "^18.2.0", 21 | "react-dom": "^18.2.0", 22 | "react-dropdown-select": "^4.9.3", 23 | "react-icons": "^4.8.0", 24 | "react-router-dom": "^6.10.0", 25 | "react-scripts": "5.0.1", 26 | "react-tailwindcss-datepicker": "^1.6.0", 27 | "serve-favicon": "^2.5.0", 28 | "web-vitals": "^2.1.4", 29 | "yup": "^1.1.1" 30 | }, 31 | "scripts": { 32 | "start": "react-scripts start", 33 | "build": "react-scripts build", 34 | "test": "react-scripts test", 35 | "eject": "react-scripts eject" 36 | }, 37 | "eslintConfig": { 38 | "extends": [ 39 | "react-app", 40 | "react-app/jest" 41 | ] 42 | }, 43 | "browserslist": { 44 | "production": [ 45 | ">0.2%", 46 | "not dead", 47 | "not op_mini all" 48 | ], 49 | "development": [ 50 | "last 1 chrome version", 51 | "last 1 firefox version", 52 | "last 1 safari version" 53 | ] 54 | }, 55 | "proxy": "http://localhost:3001", 56 | "devDependencies": { 57 | "tailwindcss": "^3.3.2" 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | require("dotenv").config(); 2 | require("./config/database"); // connects to db 3 | const express = require("express"); 4 | const path = require("path"); // node module 5 | const favicon = require("serve-favicon"); 6 | const logger = require("morgan"); 7 | const ensureLoggedIn = require("./config/ensureLoggedIn"); 8 | const cors = require("cors"); 9 | 10 | const app = express(); 11 | // development port: 3001 12 | // in production we'll a PORT number set in the environment variables 13 | const PORT = process.env.PORT || 3001; 14 | 15 | //* Config 16 | // Logger middleware 17 | app.use(logger("dev")); 18 | // JSON payload middleware (for data coming from frontend functions) 19 | app.use(express.json()); 20 | // Configure both serve-favicon & static middleware 21 | // to serve from the production 'build' folder 22 | app.use(favicon(path.join(__dirname, "build", "favicon.ico"))); 23 | app.use(express.static(path.join(__dirname, "build"))); 24 | // checks if token was sent and sets a user data on the req (req.user) 25 | app.use(require("./config/checkToken")); 26 | 27 | app.use( 28 | cors({ 29 | origin: ["https://travel-planner-2yih.onrender.com/"], 30 | }) 31 | ); 32 | 33 | // * All other routes 34 | app.use("/api/users", require("./routes/api/users")); 35 | app.use("/api/trips", ensureLoggedIn, require("./routes/api/trips")); 36 | app.use("/api/activities", ensureLoggedIn, require("./routes/api/activities")); 37 | 38 | // Put API routes here, before the "catch all" route 39 | // The following "catch all" route (note the *) is necessary 40 | // to return the index.html on all non-AJAX requests 41 | app.get("/*", (req, res) => { 42 | res.sendFile(path.join(__dirname, "build", "index.html")); 43 | }); 44 | 45 | app.listen(PORT, () => { 46 | console.log(`Server is running on port: ${PORT}`); 47 | }); 48 | -------------------------------------------------------------------------------- /src/utilities/users-service.js: -------------------------------------------------------------------------------- 1 | // * We will use a src/utilities/users-service.js module to organize functions used to sign-up, log in, log out, etc. 2 | 3 | //* SignUpForm.jsx <--> users-service.js <--> users-api.js <-Internet-> server.js (Express) 4 | 5 | //* handleSubmit <--> [signUp]-users-service <--> [signUp]-users-api <-Internet-> server.js (Express) 6 | 7 | import * as usersApi from "./users-api"; 8 | 9 | //* Get Token 10 | export function getToken() { 11 | const token = localStorage.getItem("token"); 12 | // if there is no token 13 | if (!token) return null; 14 | 15 | const payload = JSON.parse(atob(token.split(".")[1])); 16 | console.log(payload); 17 | 18 | // if token is expired 19 | if (payload.exp < Date.now() / 1000) { 20 | localStorage.removeItem("token"); 21 | return null; 22 | } 23 | 24 | // token is valid 25 | return token; 26 | } 27 | 28 | //* Get User 29 | export function getUser() { 30 | const token = getToken(); 31 | return token ? JSON.parse(atob(token.split(".")[1])).user : null; 32 | } 33 | 34 | //* SignUp 35 | export async function signUp(userData) { 36 | // Delegate the network request code to the users-api.js API module 37 | // which will ultimately return a JSON Web Token (JWT) 38 | // console.log('[From SignUP function]', userData); 39 | const token = await usersApi.signUp(userData); 40 | // saves token to localStorage 41 | localStorage.setItem("token", token); 42 | 43 | return getUser(); 44 | } 45 | 46 | //* LogOut 47 | export function logOut() { 48 | localStorage.removeItem("token"); 49 | } 50 | 51 | export async function login(credentials) { 52 | const token = await usersApi.login(credentials); 53 | localStorage.setItem("token", token); 54 | return getUser(); 55 | } 56 | 57 | export async function checkToken() { 58 | return usersApi.checkToken().then((dateStr) => new Date(dateStr)); 59 | } 60 | -------------------------------------------------------------------------------- /src/components/LogInForm.js: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import { login } from "../utilities/users-service"; 3 | 4 | export default function LoginForm({ setUser }) { 5 | const [credentials, setCredentials] = useState({ 6 | email: "", 7 | password: "", 8 | }); 9 | 10 | const [error, setError] = useState(""); 11 | 12 | function handleChange(evt) { 13 | setCredentials({ ...credentials, [evt.target.name]: evt.target.value }); 14 | setError(""); 15 | } 16 | 17 | async function handleSubmit(evt) { 18 | // Prevent form from being submitted to the server 19 | evt.preventDefault(); 20 | try { 21 | // The promise returned by the signUp service method 22 | // will resolve to the user object included in the 23 | // payload of the JSON Web Token (JWT) 24 | const user = await login(credentials); 25 | console.log(user); 26 | setUser(user); 27 | } catch { 28 | setError("Log In Failed - Try Again"); 29 | } 30 | } 31 | 32 | return ( 33 |
34 |
35 |
36 | 37 | 44 | 45 | 46 | 53 | 56 |
57 |
58 |

 {error}

59 |
60 | ); 61 | } 62 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /src/pages/DisplayTripPage/DisplayTrip.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import { useParams } from "react-router-dom"; 3 | import { getTrip } from "../../utilities/trip-service"; 4 | import { getActivity } from "../../utilities/activity-service"; 5 | export default function DisplayTrip() { 6 | const [detail, setDetail] = useState([]); 7 | const [activity, setActivities] = useState([]); 8 | const { tripId } = useParams(); 9 | 10 | //Update component when trip ID changed 11 | useEffect(() => { 12 | const loadTrip = async () => { 13 | //get the trip infor from id 14 | const editTrip = await getTrip(tripId); 15 | console.log(editTrip); 16 | 17 | setDetail(editTrip); 18 | 19 | const activityList = []; 20 | 21 | //loop the activities to get activity information then push to the list 22 | for (let id of editTrip.activities) { 23 | const newActivity = await getActivity(id); 24 | 25 | activityList.push(newActivity); 26 | } 27 | 28 | //update activityList 29 | setActivities(activityList); 30 | }; 31 | loadTrip(); 32 | }, [tripId]); 33 | 34 | //convert date to string 35 | function getDateString(input) { 36 | return new Date(input).toLocaleDateString(); 37 | } 38 | 39 | return ( 40 |
41 |

42 | YOUR TRIP DETAIL 43 |

44 |

{detail.name}

45 |

DESTINATION: {detail.destination}

46 |

START: {getDateString(detail.startDate)}

47 |

END: {getDateString(detail.endDate)}

48 | {activity.map((activiti) => { 49 | return ( 50 |
51 |

{activiti.name}

52 |

Place: {activiti.destination}

53 |

Date: {getDateString(activiti.date)}

54 |
55 | ); 56 | })} 57 |
58 | ); 59 | } 60 | -------------------------------------------------------------------------------- /controllers/api/trips.js: -------------------------------------------------------------------------------- 1 | const Trip = require("../../models/tripSchema"); 2 | 3 | async function getAllTrips(req, res) { 4 | console.log("get all trip"); 5 | try { 6 | console.log(req.user); 7 | const trips = await Trip.find({ user: req.user }); 8 | console.log(trips); 9 | 10 | res.json({ trips: trips }); 11 | } catch (e) { 12 | res.status(400).json({ msg: e.message }); 13 | } 14 | } 15 | 16 | async function getTrip(req, res) { 17 | console.log("get trip"); 18 | 19 | try { 20 | const trip = await Trip.findById(req.params.id); 21 | console.log(trip); 22 | 23 | res.status(200).json(trip); 24 | } catch (e) { 25 | res.status(400).json({ msg: e.message }); 26 | } 27 | } 28 | 29 | // Add a new Trip 30 | async function addTrip(req, res) { 31 | console.log("add trip"); 32 | console.log(req.body); 33 | try { 34 | const newTrip = req.body; 35 | newTrip.user = req.user; 36 | console.log(newTrip); 37 | 38 | const trip = await Trip.create(newTrip); 39 | 40 | res.status(200).json(trip); 41 | } catch (e) { 42 | res.status(400).json({ msg: e.message }); 43 | } 44 | } 45 | 46 | async function deleteTrip(req, res) { 47 | console.log("delete trip"); 48 | console.log(req.params.id); 49 | 50 | try { 51 | await Trip.findByIdAndDelete(req.params.id); 52 | 53 | res.status(200).json({}); 54 | } catch (e) { 55 | res.status(400).json({ msg: e.message }); 56 | } 57 | } 58 | 59 | async function updateTrip(req, res) { 60 | console.log("update trip"); 61 | 62 | try { 63 | const trip = await Trip.findByIdAndUpdate(req.params.id, req.body); 64 | console.log(trip); 65 | 66 | // await Trip.findByIdAndUpdate(req.params.id, req.body); 67 | 68 | res.status(200).json(trip); 69 | } catch (e) { 70 | res.status(400).json({ msg: e.message }); 71 | } 72 | } 73 | 74 | module.exports = { 75 | getAllTrips, 76 | getTrip, 77 | addTrip, 78 | deleteTrip, 79 | updateTrip, 80 | }; 81 | -------------------------------------------------------------------------------- /src/pages/HomePage/Home.js: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import { IoIosAddCircle } from "react-icons/io"; 3 | import { AiFillMinusCircle } from "react-icons/ai"; 4 | import { RiEditCircleFill } from "react-icons/ri"; 5 | import { getTrips, deleteTrip } from "../../utilities/trip-service"; 6 | import { useNavigate } from "react-router-dom"; 7 | 8 | function Home() { 9 | const navigate = useNavigate(); 10 | 11 | const [trips, setTrips] = useState([]); 12 | 13 | const loadTrips = async () => { 14 | console.log("load trips"); 15 | const result = await getTrips(); 16 | console.log(result.trips); 17 | setTrips(result.trips); 18 | }; 19 | 20 | useEffect(() => { 21 | loadTrips(); 22 | }, []); 23 | 24 | return ( 25 |
26 |

27 | YOUR TRIPS 28 |

29 | 30 |
31 | {trips.map((trip, idx) => { 32 | return ( 33 |
37 |
{ 40 | navigate(`trip/display/${trip._id}`); 41 | }} 42 | > 43 | {trip.name} 44 |
45 | 53 | 62 |
63 | ); 64 | })} 65 |
66 |
67 | 72 |
73 |
74 | ); 75 | } 76 | 77 | export default Home; 78 | -------------------------------------------------------------------------------- /src/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/SignUpForm.js: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import { signUp } from "../utilities/users-service"; 3 | 4 | function SignUpForm({ setUser }) { 5 | const [formData, setFormData] = useState({ 6 | name: "", 7 | email: "", 8 | password: "", 9 | confirm: "", 10 | error: "", 11 | }); 12 | 13 | const disable = formData.password !== formData.confirm; 14 | 15 | const handleSubmit = async (e) => { 16 | e.preventDefault(); 17 | 18 | try { 19 | console.log(formData); 20 | // data to be send to the backend to create a new user 21 | const userData = { 22 | name: formData.name, 23 | email: formData.email, 24 | password: formData.password, 25 | }; 26 | // returns a token with the user info 27 | const user = await signUp(userData); // user service 28 | setUser(user); 29 | } catch (error) { 30 | setFormData({ ...formData, error: "Sign Up Failed - Try Again" }); 31 | } 32 | }; 33 | 34 | const handleChange = (evt) => { 35 | setFormData({ 36 | ...formData, 37 | [evt.target.name]: evt.target.value, 38 | error: "", 39 | }); 40 | }; 41 | 42 | return ( 43 |
44 |
45 |
46 | 47 | 54 | 55 | 56 | 63 | 64 | 65 | 72 | 73 | 74 | 81 | 82 | 89 |
90 |
91 | 92 |

{formData.error}

93 |
94 | ); 95 | } 96 | 97 | export default SignUpForm; 98 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | .bg-img { 6 | background-image: url("https://wallpaperaccess.com/full/185289.jpg"); 7 | height: 500px; 8 | background-position: center; 9 | background-repeat: no-repeat; 10 | width: 100%; 11 | overflow: hidden; 12 | background-size: cover; 13 | } 14 | 15 | .footer { 16 | position: fixed; 17 | left: 0; 18 | bottom: 0; 19 | width: 100%; 20 | color: #615954; 21 | } 22 | 23 | /* CSS Custom Properties */ 24 | :root { 25 | --white: #ffffff; 26 | --tan-1: #fbf9f6; 27 | --tan-2: #e7e2dd; 28 | --tan-3: #e2d9d1; 29 | --tan-4: #d3c1ae; 30 | --orange: #ce8c41; 31 | --text-light: #968c84; 32 | --text-dark: #615954; 33 | } 34 | 35 | *, 36 | *:before, 37 | *:after { 38 | box-sizing: border-box; 39 | } 40 | 41 | body { 42 | margin: 0; 43 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", 44 | "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", 45 | sans-serif; 46 | -webkit-font-smoothing: antialiased; 47 | -moz-osx-font-smoothing: grayscale; 48 | background-color: #0f192a; 49 | color: whitesmoke; 50 | padding: 2vmin; 51 | height: 100vh; 52 | } 53 | 54 | code { 55 | font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", 56 | monospace; 57 | } 58 | 59 | #root { 60 | height: 100%; 61 | } 62 | 63 | .align-ctr { 64 | text-align: center; 65 | } 66 | 67 | .align-rt { 68 | text-align: right; 69 | } 70 | 71 | .smaller { 72 | font-size: smaller; 73 | } 74 | 75 | .flex-ctr-ctr { 76 | display: flex; 77 | justify-content: center; 78 | align-items: center; 79 | } 80 | 81 | .flex-col { 82 | flex-direction: column; 83 | } 84 | 85 | .flex-j-end { 86 | justify-content: flex-end; 87 | } 88 | 89 | .scroll-y { 90 | overflow-y: scroll; 91 | } 92 | 93 | .section-heading { 94 | display: flex; 95 | justify-content: space-around; 96 | align-items: center; 97 | background-color: var(--tan-1); 98 | color: var(--text-dark); 99 | border: 0.1vmin solid var(--tan-3); 100 | border-radius: 1vmin; 101 | padding: 0.6vmin; 102 | text-align: center; 103 | font-size: 2vmin; 104 | } 105 | 106 | .form-container { 107 | padding: 3vmin; 108 | background-color: var(--tan-1); 109 | border: 0.1vmin solid var(--tan-3); 110 | border-radius: 1vmin; 111 | } 112 | 113 | p.error-message { 114 | color: var(--orange); 115 | text-align: center; 116 | } 117 | 118 | form { 119 | display: grid; 120 | grid-template-columns: 1fr 3fr; 121 | gap: 1.25vmin; 122 | color: var(--text-light); 123 | } 124 | 125 | label { 126 | font-size: 2vmin; 127 | display: flex; 128 | align-items: center; 129 | } 130 | 131 | input { 132 | padding: 1vmin; 133 | font-size: 2vmin; 134 | border: 0.1vmin solid var(--tan-3); 135 | border-radius: 0.5vmin; 136 | color: var(--text-dark); 137 | background-image: none !important; 138 | outline: none; 139 | } 140 | 141 | input:focus { 142 | border-color: var(--orange); 143 | } 144 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TRAVEL PLANNER APP 2 | 3 | [My Travel Planner App](https://travel-planner-api.onrender.com/) 4 | 5 | ## About 6 | ### The purpose of Travel Planner App is to help user managing and planing their trip faster and easier. 7 | 8 | 9 | Before start coding for the project I created the [project planner](https://github.com/users/tmp03099/projects/1/views/1?layout=board) to keep track of my tasks and make sure that I'm on time. 10 | 11 | 12 | 13 | This website is created with the [WIREFRAME](https://wireframe.cc/6x7iBE) and [ERD](https://drive.google.com/file/d/1ypiG4mybDiA9Dnpozdf9xAkw7EYbpP50/view) design diagrams as wireframes for better development. 14 | 15 | 16 | This project demonstrated my knowledge as a Full-stack developer where I am able to create websites that connected to backend server to save and load information to and from database server. 17 | 18 | While working on this project, I encounted a big issue where I found out that formik is incompatible with Use Effect hook in React. This issue is causing infinite looping of rendering the component. I spend a lot of time researching and trying different way to address the problem and finnaly found a solution. I learned a lot from this project. With just a week of coding, I was able to provide a working version of the application. While it is simple, it have enough functionality that allow user to plan their trip. I will continue to further refining the current features and adding new capabilities further improve this project. 19 | 20 | 21 | ### Main Features 22 | - Allow user login with Authorization. 23 | - User Information and trips information were stored in database. 24 | - CRUD applied for create, read, update and delete the trip and activities data. 25 | - Used different types of React Hooks. 26 | - Applied input validation with formik. 27 | - Deploy front end application and back end server using Render. 28 | 29 | ### Build With 30 | ![MongoDB](https://img.shields.io/badge/MongoDB-%234ea94b.svg?style=for-the-badge&logo=mongodb&logoColor=white) 31 | ![NodeJS](https://img.shields.io/badge/node.js-6DA55F?style=for-the-badge&logo=node.js&logoColor=white) 32 | ![Express.js](https://img.shields.io/badge/express.js-%23404d59.svg?style=for-the-badge&logo=express&logoColor=%2361DAFB) 33 | ![React](https://img.shields.io/badge/react-%2320232a.svg?style=for-the-badge&logo=react&logoColor=%2361DAFB) 34 | ![JavaScript](https://img.shields.io/badge/javascript-%23323330.svg?style=for-the-badge&logo=javascript&logoColor=%23F7DF1E) 35 | ![Render](https://img.shields.io/badge/Render-%46E3B7.svg?style=for-the-badge&logo=render&logoColor=white) 36 | ![TailwindCSS](https://img.shields.io/badge/tailwindcss-%2338B2AC.svg?style=for-the-badge&logo=tailwind-css&logoColor=white) 37 | ![CSS3](https://img.shields.io/badge/css3-%231572B6.svg?style=for-the-badge&logo=css3&logoColor=white) 38 | ![HTML5](https://img.shields.io/badge/html5-%23E34F26.svg?style=for-the-badge&logo=html5&logoColor=white) 39 | 40 | 41 | ## Road map 42 | [ ] Responsive Design 43 | 44 | 45 | [ ] Add Attractive for Front end 46 | 47 | 48 | [ ] Mobile devices 49 | 50 | ## License 51 | Distributed under the MIT License. See `LICENSE.txt` for more information 52 | 53 | ## Contact 54 | My Phung Tieu - [LinkedIn](https://www.linkedin.com/in/my-phung-tieu-0bba22219/) - [Github](https://github.com/tmp03099):heart_eyes: 55 | 56 | ## Acknowledgements 57 | [Formik](https://formik.org/docs/api/formik) 58 | 59 | 60 | [React Select](https://react-select.com/home) 61 | 62 | 63 | [Render Deploy](https://paragon.ba/en/how-to-deploy-a-mern-application-on-render-com/) 64 | 65 | 66 | [Formik Issue](https://github.com/jaredpalmer/formik/issues/2397) 67 | 68 | 69 | [UseEffect Issue](https://www.learnbestcoding.com/post/94/maximum-update-depth-exceeded-react) 70 | 71 | 72 | [Tailwindcss](https://tailwindcss.com/docs/installation) 73 | 74 | 75 | [MongoDB](https://account.mongodb.com/account/login?n=%2Fv2%2F643561ae823d3b5841609903&nextHash=%23clusters) 76 | 77 | 78 | -------------------------------------------------------------------------------- /src/components/AddActivity/ActivityDialog.js: -------------------------------------------------------------------------------- 1 | import { 2 | Button, 3 | Dialog, 4 | DialogHeader, 5 | DialogBody, 6 | DialogFooter, 7 | } from "@material-tailwind/react"; 8 | import { React, Fragment, useState } from "react"; 9 | import Select from "react-dropdown-select"; 10 | import { createActivity } from "../../utilities/activity-service"; 11 | 12 | function ActivityDialog({ dateOptions, activities, setActivities }) { 13 | const [open, setOpen] = useState(false); 14 | 15 | const [activity, setActivty] = useState({}); 16 | 17 | const typeOptions = [ 18 | { 19 | value: "food", 20 | label: "Food", 21 | }, 22 | { 23 | value: "activity", 24 | label: "Activity", 25 | }, 26 | { 27 | value: "attraction", 28 | label: "Attraction", 29 | }, 30 | ]; 31 | 32 | const onAddActivityClick = () => { 33 | setOpen(true); 34 | }; 35 | 36 | const onCancelClick = () => { 37 | setOpen(!open); 38 | }; 39 | 40 | const onAddClick = async () => { 41 | const newActivity = await createActivity(activity); 42 | 43 | setActivities([...activities, newActivity]); 44 | console.log(activity); 45 | setOpen(!open); 46 | }; 47 | 48 | return ( 49 | 50 | 53 | 54 | Add Activity 55 | 56 |
57 |
58 | { 74 | setActivty({ 75 | ...activity, 76 | name: event.target.value, 77 | }); 78 | }} 79 | /> 80 |
81 |
82 | 83 | { 87 | setActivty({ 88 | ...activity, 89 | destination: event.target.value, 90 | }); 91 | }} 92 | /> 93 |
94 |
95 | 98 |
99 |
100 | { 60 | //update name 61 | setTripData({ ...tripData, name: e.target.value }); 62 | formik.handleChange(e); 63 | }} 64 | onBlur={formik.handleBlur} 65 | value={formik.values.name} 66 | className={ 67 | "bg-gray-200 appearance-none border-2 border-gray-200 rounded w-full py-2 px-4 text-gray-700 leading-tight focus:outline-none focus:bg-white focus:border-purple-500 " + 68 | (formik.touched.name && formik.errors.name 69 | ? "border-red-500" 70 | : null) 71 | } 72 | /> 73 | {formik.touched.name && formik.errors.name ? ( 74 |
{formik.errors.name}
75 | ) : null} 76 |
77 |
78 |
79 |
80 | 86 |
87 |
88 | { 93 | //update destination 94 | setTripData({ ...tripData, destination: e.target.value }); 95 | formik.handleChange(e); 96 | }} 97 | onBlur={formik.handleBlur} 98 | value={formik.values.destination} 99 | className={ 100 | "bg-gray-200 appearance-none border-2 border-gray-200 rounded w-full py-2 px-4 text-gray-700 leading-tight focus:outline-none focus:bg-white focus:border-purple-500 " + 101 | (formik.touched.destination && formik.errors.destination 102 | ? "border-red-500" 103 | : null) 104 | } 105 | /> 106 | {formik.touched.destination && formik.errors.destination ? ( 107 |
{formik.errors.destination}
108 | ) : null} 109 |
110 |
111 |
112 |
113 | 119 |
120 |
121 | { 129 | //update date 130 | setTripData({ 131 | ...tripData, 132 | startDate: value.startDate, 133 | endDate: value.endDate, 134 | }); 135 | }} 136 | /> 137 |
138 |
139 | 140 | 141 | ); 142 | } 143 | -------------------------------------------------------------------------------- /src/pages/EditTripPage/EditTrip.js: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import AddTrip from "../../components/AddTrip/AddTrip"; 3 | import { useNavigate, useParams } from "react-router-dom"; 4 | import { getTrip, updateTrip } from "../../utilities/trip-service"; 5 | import ActivityDialog from "../../components/AddActivity/ActivityDialog"; 6 | import { Button } from "@material-tailwind/react"; 7 | import { deleteActivity, getActivity } from "../../utilities/activity-service"; 8 | import { AiFillMinusCircle } from "react-icons/ai"; 9 | 10 | function EditTrip() { 11 | const navigate = useNavigate(); 12 | 13 | const { tripId } = useParams(); 14 | 15 | const [trip, setTrip] = useState({}); 16 | 17 | const [dateOptions, setDateOptions] = useState([]); 18 | 19 | const [activities, setActivities] = useState([]); 20 | 21 | //Set up the date dropdown 22 | const updateDateOptions = (start, end) => { 23 | const from = new Date(start); 24 | const to = new Date(end); 25 | 26 | var newOptions = []; 27 | 28 | //loop the range Date 29 | for ( 30 | let option = from; 31 | option <= to; 32 | option.setDate(option.getDate() + 1) 33 | ) { 34 | //push to newOptions array 35 | newOptions.push({ 36 | value: option.getTime(), 37 | label: option.toLocaleDateString(), 38 | }); 39 | } 40 | 41 | //update date 42 | setDateOptions(newOptions); 43 | }; 44 | 45 | //Update trip when click the button 46 | const onUpdateClick = async () => { 47 | console.log(trip); 48 | 49 | await updateTrip(trip); 50 | 51 | //navigate back to home 52 | navigate("/"); 53 | }; 54 | 55 | //Update component when trip ID changed 56 | useEffect(() => { 57 | const loadTrip = async () => { 58 | //get the trip infor from id 59 | const editTrip = await getTrip(tripId); 60 | console.log(editTrip); 61 | 62 | setTrip(editTrip); 63 | 64 | const activityList = []; 65 | 66 | //loop the activities to get activity information then push to the list 67 | for (let id of editTrip.activities) { 68 | const newActivity = await getActivity(id); 69 | 70 | activityList.push(newActivity); 71 | } 72 | 73 | //update activityList 74 | setActivities(activityList); 75 | }; 76 | loadTrip(); 77 | }, [tripId]); 78 | 79 | // Update component when trip date changed 80 | useEffect(() => { 81 | updateDateOptions(trip.startDate, trip.endDate); 82 | }, [trip.startDate, trip.endDate]); 83 | 84 | // Handle when activity changed. 85 | useEffect(() => { 86 | const updateActivity = () => { 87 | const activitiesIds = []; 88 | 89 | activities.forEach((a) => { 90 | activitiesIds.push(a._id); 91 | }); 92 | 93 | setTrip((trip) => ({ ...trip, activities: activitiesIds })); 94 | }; 95 | updateActivity(); 96 | }, [activities]); 97 | 98 | //! Get the type of activities 99 | /** 100 | * Get food list from activities list 101 | * 102 | * @returns activities list that have type = food 103 | */ 104 | const foodList = () => activities.filter((a) => a.type === "food"); 105 | 106 | /** 107 | * Get activity list from activities list 108 | * 109 | * @returns activities list that have type = activity 110 | */ 111 | const activityList = () => activities.filter((a) => a.type === "activity"); 112 | 113 | /** 114 | * Get attraction list from activities list 115 | * 116 | * @returns activities list that have type = attraction 117 | */ 118 | const attractionList = () => 119 | activities.filter((a) => a.type === "attraction"); 120 | 121 | //convert date to string 122 | function getDateString(input) { 123 | return new Date(input).toLocaleDateString(); 124 | } 125 | 126 | return ( 127 |
128 |
129 |

EDIT YOUR TRIP

130 |
131 | 132 |
133 |
134 | {foodList().length > 0 ? ( 135 |
136 |

137 | Food List 138 |

139 | {foodList().map((activity) => { 140 | return ( 141 |
142 |
{activity.name}
143 |
Where: {activity.destination}
144 |
Time: {getDateString(activity.date)}
145 | 154 |
155 | ); 156 | })} 157 |
158 | ) : ( 159 | "" 160 | )} 161 |
162 |
163 | {activityList().length > 0 ? ( 164 |
165 |

166 | Activity List 167 |

168 | {activityList().map((activity) => { 169 | return ( 170 |
171 |
{activity.name}
172 |
{activity.destination}
173 |
{getDateString(activity.date)}
174 | 183 |
184 | ); 185 | })} 186 |
187 | ) : ( 188 | "" 189 | )} 190 |
191 |
192 | {attractionList().length > 0 ? ( 193 |
194 |

195 | Attraction List 196 |

197 | {attractionList().map((activity) => { 198 | return ( 199 |
200 |
{activity.name}
201 |
{activity.destination}
202 |
{getDateString(activity.date)}
203 | 212 |
213 | ); 214 | })} 215 |
216 | ) : ( 217 | "" 218 | )} 219 |
220 |
221 |
222 |
223 | 228 |
229 |
230 | 233 |
234 |
235 |
236 | ); 237 | } 238 | 239 | export default EditTrip; 240 | --------------------------------------------------------------------------------