├── .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 |
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 |
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 |
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 | 
31 | 
32 | 
33 | 
34 | 
35 | 
36 | 
37 | 
38 | 
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 |
127 |
128 | );
129 | }
130 |
131 | export default ActivityDialog;
132 |
--------------------------------------------------------------------------------
/src/components/AddTrip/AddTrip.js:
--------------------------------------------------------------------------------
1 | import { useFormik } from "formik";
2 | import * as Yup from "yup";
3 | import React, { useEffect, useState } from "react";
4 | import Datepicker from "react-tailwindcss-datepicker";
5 |
6 | export default function AddTrip({ tripData, setTripData }) {
7 | const [init, setInit] = useState(false);
8 |
9 | // validate formik form
10 | const { setValues, ...formik } = useFormik({
11 | initialValues: {
12 | name: "",
13 | destination: "",
14 | startDate: Date.now(),
15 | endDate: Date.now(),
16 | },
17 |
18 | validationSchema: Yup.object({
19 | name: Yup.string().min(2, "Too Short!").required("Required"),
20 | destination: Yup.string()
21 | .min(2, "Invalid destination")
22 | .required("Required"),
23 | }),
24 | onSubmit: (values, { resetForm }) => {
25 | resetForm({ values: "" });
26 | alert("Your successfully submit the form ");
27 | },
28 | });
29 |
30 | useEffect(() => {
31 | console.log(init, tripData);
32 | //initalize first value of formik (to fix error run many times)
33 | if (!init) {
34 | //load the existing data value for edit trip
35 | if (tripData.name) {
36 | setValues(tripData);
37 | setInit(true);
38 | }
39 | }
40 | }, [tripData, setValues, init]);
41 |
42 | return (
43 |
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 |
229 |
230 |
233 |
234 |
235 |
236 | );
237 | }
238 |
239 | export default EditTrip;
240 |
--------------------------------------------------------------------------------