Open In App

Travel Journal App with MERN Stack with API

Last Updated : 08 Mar, 2024
Improve
Improve
Like Article
Like
Save
Share
Report

In this article, we are going to explore the project which is a travel journalling app, where users can log in and register, make journal entries describing the various places they traveled to, and also upload their images. They can search through the various entries they’ve made and also delete them.

The website enables authentication and authorization i.e. only users who have accounts can explore the app and only authors of entries can view them. It is a great solution for all travel lovers. We are going to make it using the MERN stack (Mongo DB, Express, React, and Node).

Output Preview: Let us have a look at how the final output will look like.

ReactApp-GoogleChrome2024-02-2823-47-11-ezgifcom-video-to-gif-converter

Final Preview of the Website

Prerequisites:

Approach to Create a Travel Journal App with MERN Stack:

  • Login/Register – The website enables users to login and register, so that only authenticated users can access the app. It also has the option of uploading profile pictures.
  • Home Page – It lists down all the entries create by the user. It gives a snapshot of the original entry and viewers can click the Read More button to navigate to the whole entry. Users can also search for entries based on title, location and date.
  • Create Page – This page is where users create the actual entries. They can also provide details like date of the entry, title of the entry, location and upto 3 images as memories.
  • View Page – The view page basically showcases all the details about the entry in full detail. It also has an image carousel to flip through multiple images.
  • Logout – Logout functionality will basically erase user data stored in browser cache.
  • Authorization – We are using authorization to facilitate privacy i.e. only users who’ve created the journal entry can view them and delete them.
  • Delete – Erases the journal entry.
  • State Management – Context API is being employed to facilitate state management.

Steps to Create a Backend Server:

Step 1: Create a server using the following command in your terminal.

npm init -y

Step 2: Install the required packages.

npm install bcryptjs cors dotenv express cookie-parser jsonwebtoken mongodb mongoose morgan helmet nodemon

Project Structure(Backend):

Screenshot-2024-02-29-113107

Project Strucure

The updated dependencies in package.json file will look like:

"dependencies": {
"bcryptjs": "^2.4.3",
"body-parser": "^1.20.2",
"cookie-parser": "^1.4.6",
"cors": "^2.8.5",
"dotenv": "^16.4.5",
"express": "^4.18.2",
"helmet": "^7.1.0",
"jsonwebtoken": "^9.0.2",
"mongoose": "^8.2.0",
"morgan": "^1.10.0",
"nodemon": "^3.1.0"
}

Step 3: Create a MongoDB project and access its connection url. Create a .env file where secret keys are stored. Here add your mongodb connection url and a randomly generate code for JWT.

MONGO = [mongo database connection string can be found at on cloud.mongodb.com > Clusters > Connect button]

JWT = randomly generate code

Example: Below is an example of creating a server of Travel Journal App.

Javascript




import express from "express";
import dotenv from "dotenv";
import helmet from "helmet";
import morgan from "morgan";
import mongoose from "mongoose";
import userRoute from "./routes/user.js";
import entryRoute from "./routes/entry.js";
import cookieParser from "cookie-parser";
import cors from "cors"
  
const app = express();
dotenv.config();
  
const PORT = process.env.PORT || 5500;
  
const connect = async () => {
  try {
    await mongoose.connect(process.env.MONGO);
    console.log("Connected to mongoDB.");
  } catch (error) {
    throw error;
  }
};
  
mongoose.connection.on("disconnected", () => {
  console.log("mongoDB disconnected!");
});
  
app.get('/', (req, res) => { res.send('Hello from Express!') });
  
//middlewares
app.use(cookieParser())
app.use(express.json());
app.use(helmet());
  
  
app.use(cors({
   origin: "http://localhost:3000",
   credentials: true
}))
  
  
  
app.use(morgan("common"));
  
app.use("/api/users", userRoute);
app.use("/api/entries", entryRoute);
  
app.listen(PORT, () => {
  console.log("Listening on port 5500");
  connect();
});


Javascript




export const createError = (status, message) => {
    const err = new Error();
    err.status = status;
    err.message = message;
    return err;
  };


Javascript




import mongoose from "mongoose";
  
const EntrySchema = new mongoose.Schema(
    {
        author: {
            type: mongoose.Schema.Types.ObjectId,
            ref: 'User',
        },
        title: {
            type: String,
            required: true,
        },
        location: {
            type: String,
            required: true,
        },
        date: {
            type: String,
            required: true
        },
        photos: {
            type: [String],
        },
        text: {
            type: String,
            required: true
        }
    }
)
  
export default mongoose.model("Entry", EntrySchema)


Javascript




import mongoose from "mongoose";
  
const UserSchema = new mongoose.Schema(
    {
        username: {  type: String, required: true, unique: true },
        email: { type: String, required: true, unique: true },
        password: { type: String, required: true },
        profilePicture: { type: String, default: "" },
        entries: [
            {
                type: mongoose.Schema.Types.ObjectId,
                ref: 'Entry'
            }
        ],
    },
    {
        timestamps: true
    }
)
  
export default mongoose.model("User", UserSchema);


Javascript




import Entry from "../models/Entry.js"
import User from "../models/User.js"
  
export const createEntry = async (req, res, next) => {
  
    const newEntry = new Entry(req.body);
    try {
      const savedEntry = await newEntry.save();
        
      try {
        const user = await User.findById(savedEntry.author);
        user.entries.push(savedEntry._id);
        await user.save();
      }
      catch(err) {
        next(err)
      }
      res.status(200).json(savedEntry);
    } catch (err) {
      next(err);
    }
  };
    
  export const updateEntry = async (req, res, next) => {
    try {
      const entry = await Entry.findByIdAndUpdate(
        req.params.id,
        { $set: req.body },
        { new: true }
      );
      res.status(200).json(entry);
    } catch (err) {
      next(err);
    }
  };
    
  export const deleteEntry = async (req, res, next) => {
    try {
      await Entry.findByIdAndDelete(req.params.id);
        
      try {
  
          await User.findOneAndUpdate(
            { entries: req.params.id }, // Find the user who has the entry id in their entries array
            { $pull: { entries: req.params.id } }, // Remove the entry id from the entries array
            { new: true }
          );
      }
  
      catch(err) {
        next(err)
      }
        
      res.status(200).json("the entry has been deleted");
    } catch (err) {
      next(err);
    }
  };
    
    
  export const getEntries = async (req, res, next) => {
    const userId = req.params.userId;
    try {
      const entries = await Entry.find({ author: userId })
      res.status(200).json(entries);
    } catch (err) {
      next(err)
    }
  }
  
  export const getEntry = async(req, res, next) => {
    try{
        const entry = await Entry.findById(req.params.id);
        res.status(200).json(entry);
    }
    catch(err) {
        next(err);
    }
  }


Javascript




import User from "../models/User.js";
import bcrypt from "bcryptjs";
import { createError } from "../error.js";
import jwt from "jsonwebtoken";
  
export const register = async (req, res, next) => {
    try {
  
        //check for already exist
        const em = await User.findOne({
            email: req.body.email
        });
        if (em)
            return res.status(409).send({
                message: "User with given email already exists"
            })
  
  
        const salt = bcrypt.genSaltSync(10);
        const hash = bcrypt.hashSync(req.body.password, salt);
  
        const newUser = new User({
            ...req.body,
            password: hash,
        });
  
        await newUser.save();
        res.status(200).send("User has been created.");
    } catch (err) {
        next(err);
    }
};
  
  
export const login = async (req, res, next) => {
    try {
        const user = await User.findOne({
            username: req.body.username
        });
        if (!user) return next(createError(404, "User not found!"));
  
        const isPasswordCorrect = await bcrypt.compare(
            req.body.password,
            user.password
        );
        if (!isPasswordCorrect)
            return next(createError(400,
                "Wrong password or username!"));
  
        const token = jwt.sign(
            { id: user._id, isAdmin: user.isAdmin },
            process.env.JWT
        );
  
        const { password, isAdmin, ...otherDetails } = user._doc;
        res
            .cookie("access_token", token, {
                httpOnly: true,
            })
            .status(200)
            .json({ details: { ...otherDetails }, isAdmin });
    } catch (err) {
        next(err);
    }
};
  
export const deleteUser = async (req, res, next) => {
    try {
        await User.findByIdAndDelete(req.params.id);
        res.status(200).json("User has been deleted.");
    } catch (err) {
        next(err);
    }
};


Javascript




import express from "express";
import {
    createEntry,
    deleteEntry,
    getEntries,
    updateEntry,
    getEntry,
} from "../controllers/entry.js";
  
const router = express.Router();
  
router.post("/", createEntry);
router.put("/:id", updateEntry);
router.delete("/:id", deleteEntry);
router.get("/author/:userId", getEntries);
router.get("/:id", getEntry)
  
export default router;


Javascript




import express from "express";
import {
    login,
    register,
    deleteUser
} from "../controllers/user.js ";
  
const router = express.Router();
  
router.post("/register", register)
router.post("/login", login)
router.delete('/:id', deleteUser)
  
export default router;


Start your server using the following command.

cd server
node index.js

Steps to Create a React Application and Installing Module:

Step 1: Create react application in your project folder using the following command and navigate to the folder.

npx create-react-app client
cd client

Step 2: Install additional packages

npm install axios react-router-dom @fortawesome/free-solid-svg-icons @fortawesome/react-fontawesome

Step 3: In the index.html file add the following google font embeddings so that we can use these particular fonts in our app.

<link rel=”preconnect” href=”https://fonts.googleapis.com”>

<link rel=”preconnect” href=”https://fonts.gstatic.com” crossorigin>

<link href=”https://fonts.googleapis.com/css2?family=Caveat:wght@400..700&family=Urbanist:ital,wght@0,100..900;1,100..900&display=swap” rel=”stylesheet”>

Project Structure(Frontend):

Screenshot-2024-03-04-111432

Project Structure

The updated dependencies in package.json file will look like:

"dependencies": {
"@fortawesome/free-solid-svg-icons": "^6.5.1",
"@fortawesome/react-fontawesome": "^0.2.0",
"@testing-library/jest-dom": "^5.17.0",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
"axios": "^1.6.7",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^6.22.1",
"react-scripts": "5.0.1",
"web-vitals": "^2.1.4"
}

Step 4: Set up the backbone of the frontend:

  • Initialize a React app with createRoot(), wrapped in AuthContextProvider for authentication data handling. Render App within React strict mode. Define routes in App.js for page mapping. Implement useFetch for data fetching. Use ProtectedRoute to limit access. Establish authentication context with AuthReducer and useReducer, managed by AuthContextProvider.

Step 5: Create the required components:

  • Home.jsx: Renders user entries and includes a search functionality.
  • Create.jsx: Allows users to create entries with up to three images using Cloudinary for image uploading.
  • Login.jsx & Register.jsx: Login and registration pages respectively.
  • View.jsx: Displays journal details and includes an image carousel for multiple images.
  • Card.jsx: Component used in the Home page to render entries.
  • Navbar.jsx: Provides navigation and options for logout/login/register.

HTML




// client/public/index.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <meta name="theme-color" content="#000000" />
    <meta
      name="description"
      content="Web site created using create-react-app"
    />
    <link rel="preconnect" href="https://fonts.googleapis.com">
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
    <link href="https://fonts.googleapis.com/css2?family=Caveat:wght@400..700&family=Urbanist:ital,wght@0,100..900;1,100..900&display=swap" rel="stylesheet">        
    <title>React App</title>
  </head>
  <body>
    <noscript>You need to enable JavaScript to run this app.</noscript>
    <div id="root"></div>
  </body>
</html>


CSS




/* client/src/style.css */
  
body {
  margin: 0;
  padding: 0;
  font-family: "Urbanist", sans-serif;
  background-color: var(--cream);
}
  
:root {
  --light-purple: #E1AFD1;
  --purple: #AD88C6;
  --dark-purple: #7469B6;
  --cream: #FFE6E6;
  --dark: #354259;
  --light-pink: #F1D4E5;
  --light-blue: #D4FAFC;
  --blue: #19A7CE;
}


CSS




/* client/src/styles/home.css */
  
.search {
  position: relative;
  background-color: var(--light-blue);
  height: 300px;
  background-size: cover;
}
  
.searchBackground #surfer {
  background-attachment: fixed;
  background-size: cover;
  height: 800px;
  width: 100%;
  filter: brightness(60%);
}
  
.searchBar {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
}
  
.searchBar h2 {
  color: var(--dark);
  text-align: center;
  font-size: 2.5em;
  margin-bottom: 20px;
}
  
.searchInput {
  background-color: white;
  padding: 5px 10px;
  border-radius: 50px;
  width: 400px;
  display: flex;
  align-items: center;
  justify-content: space-around;
}
  
.searchInput input {
  border: none;
  height: 40px;
  width: 70%;
  padding: 0 10px;
}
  
.searchInput input:focus {
  outline: none;
}
  
.searchInput .icon {
  cursor: pointer;
}
  
.searchedPosts {
  position: relative;
  display: flex;
  justify-content: center;
  align-items: center;
  flex-wrap: wrap;
  background-color: white;
}
  
#loading {
  width: 20px;
  height: 20px;
}
  
  
@media screen and (max-width: 700px) {
  .searchInput {
    width: 300px;
  }
  
}


CSS




/* client/src/styles/create.css */
  
.create .createContainer{
    width: 100%;
    display: flex;
    margin: 100px 0;
    flex-direction: column;
    height: fit-content;
    align-items: center;
    justify-content: center;
    gap: 20px;
}
  
.create .createContainer .input {
    display: flex;
    flex-direction: column;
    text-align: center;
    gap: 10px;
}
  
.create .createContainer textarea {
    border: none;
    margin-top: 10px;
    background-color: white;
    font-family: "Caveat", cursive;
    font-size: 1.5rem;
}
  
.picsContainer {
    width: 30%;
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: center;
    gap: 50px;
    padding: 0 50px;
  
}
  
.picsContainer .formInput {
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: center;
    gap: 20px;
}
  
.picsContainer .formInput label {
    cursor: pointer;
    font-size: 1.2rem;
    transition: all 0.3s ease;
}
  
.picsContainer .formInput label:hover {
    transform: translateY(-2px);
}
  
.picsContainer .uploadedPictures {
    display: flex;
    justify-content: center;
    align-items: center;
    flex-wrap: wrap;
    gap: 30px;
}
  
.picsContainer h1 {
    text-align: center;
    padding: 0 0 20px 0;
    font-size: 25px;
    font-size: 1.2rem;
}
  
.input label {
    font-size: 1.5rem;
}
  
.input input {
    font-size: 1.2rem;
    padding: 10px;
  font-family: "Urbanist", sans-serif;
}
  
.create .createBtn {
    width: 150px;
    height: 40px;
    border: none;
    background: var(--purple);
    border-radius: 25px;
    font-size: 15px;
    color: white;
    font-weight: 700;
    cursor: pointer;
    outline: none;
}


CSS




/* client/src/styles/login.css */
  
body,
html {
    overflow-x: hidden;
}
  
.loginCard {
    background-size: cover;
    background-repeat: no-repeat;
    height: 100vh;
    overflow: hidden;
}
  
.center {
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    width: 400px;
    background: white;
    border-radius: 10px;
    padding-top: 20px;
}
  
.center h1 {
    font-size: 1.2rem;
    text-align: center;
    padding: 0 0 20px 0;
    border-bottom: 1px solid silver;
}
  
.center form {
    padding: 0 40px;
    box-sizing: border-box;
}
  
form .txt_field {
    position: relative;
    border-bottom: 2px solid #adadad;
    margin: 30px 0;
}
  
.txt_field {
    width: 100%;
    padding: 0 5px;
    height: 40px;
    font-size: 16px;
    border: none;
    background: none;
    outline: none;
}
  
.txt_field input {
    width: 100%;
    padding: 0 5px;
    height: 40px;
    font-size: 16px;
    border: none;
    background: none;
    outline: none;
}
  
.login_button .button {
    width: 100%;
    height: 50px;
    border: none;
    background: transparent;
    border-radius: 25px;
    font-size: 18px;
    color: black;
    font-weight: 700;
    cursor: pointer;
    outline: none;
}
  
.login_button {
    width: 100%;
    height: 50px;
    border: none;
    background: var(--red);
    border-radius: 25px;
    font-size: 18px;
    color: white;
    font-weight: 700;
    cursor: pointer;
    outline: none;
}
  
.signup_link {
    margin: 30px 0;
    text-align: center;
    font-size: 16px;
    color: #666666;
}
  
.signup_link a {
    color: var(--orange);
    text-decoration: none;
}
  
.signup_link a:hover {
    text-decoration: underline;
}
  
@media screen and (max-width: 600px) {
    .loginCard .center {
        width: 300px;
    }
}


CSS




/* client/src/styles/register.css */
  
body,
html {
    overflow-x: hidden;
}
  
.registerCard {
    background-size: cover;
    background-repeat: no-repeat;
    height: 1000px;
    overflow: hidden;
    position: relative;
}
  
.registerCard .center {
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    width: 400px;
    background: white;
    border-radius: 10px;
}
  
.center h1 {
    font-size: 1.2rem;
    text-align: center;
    padding: 0 0 20px 0;
    border-bottom: 1px solid silver;
}
  
.center form {
    padding: 0 40px;
    box-sizing: border-box;
}
  
form .txt_field {
    position: relative;
    border-bottom: 2px solid #adadad;
    margin: 30px 0;
}
  
form .txt_field_img {
    position: relative;
    margin-top: 10px;
}
  
.txt_field {
    width: 100%;
    padding: 0 5px;
    height: 40px;
    font-size: 16px;
    border: none;
    background: none;
    outline: none;
}
  
.txt_field input {
    width: 100%;
    padding: 0 5px;
    height: 40px;
    font-size: 16px;
    border: none;
    background: none;
    outline: none;
}
  
.registerCard form .image {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    margin-top: 20px;
}
  
.register input {
    width: 100%;
    height: 50px;
    border: none;
    background: transparent;
    border-radius: 25px;
    font-size: 16px;
    color: black;
    outline: none;
}
  
.login_button .button {
    width: 100%;
    height: 50px;
    border: none;
    background: var(--purple);
    border-radius: 25px;
    font-size: 18px;
    color: white;
    font-weight: 700;
    cursor: pointer;
    outline: none;
}
  
.login_button {
    width: 100%;
    height: 50px;
    border: none;
    background: var(--red);
    border-radius: 25px;
    font-size: 18px;
    color: white;
    font-weight: 700;
    cursor: pointer;
    outline: none;
}
  
.signup_link {
    margin: 30px 0;
    text-align: center;
    font-size: 16px;
    color: #666666;
}
  
.signup_link a {
    color: var(--orange);
    text-decoration: none;
}
  
.signup_link a:hover {
    text-decoration: underline;
}
  
@media screen and (max-width: 500px) {
    .registerCard .center {
        width: 300px;
        margin: 0;
    }
}


CSS




/* client/src/styles/view.css */
  
body,
html {
    overflow-x: hidden;
}
  
.view {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
  
}
  
.postPage {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
  
}
  
.postContainer {
    display: flex;
    align-items: flex-start;
    justify-content: space-evenly;
    width: 90%;
    margin: 50px;
    flex-wrap: wrap;
  
}
  
.postPageBG {
    position: relative;
    height: 300px;
    background: white;
    background-size: cover;
    width: 100%;
}
  
.postPageBG .upperContent {
    width: 90%;
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    display: flex;
    flex-direction: column;
    align-items: center;
    text-align: center;
}
  
.upperContent h1 {
    color: var(--blue);
    font-size: 2.5rem;
    margin-bottom: 20px;
    font-weight: bold;
}
  
.upperContent p {
    margin-top: 10px;
    font-size: 1.2rem;
    color: var(--dark);
}
  
.images {
    display: flex;
    flex-direction: column;
    align-items: center;
}
  
.images img {
    height: 400px;
}
  
.images .arrows {
    display: flex;
    justify-content: center;
}
  
.images .arrow {
    margin: 20px;
    font-size: 40px;
    color: var(--dark-purple);
    cursor: pointer;
}
  
.leftContainer {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    width: 50%;
    overflow: hidden;
}
  
  
.rightContainer {
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: center;
    width: 50%;
    font-size: 1.25rem;
    height: 500px;
}
  
.rightContainer .title {
    background-color: #dbecef;
    border-radius: 10px;
    padding: 10px 5px;
    text-align: center;
    margin: 20px 0;
}
  
.rightContainer span {
    color: #126e82;
    font-weight: bolder;
}
  
.rightContainer p {
    font-family: "Caveat", cursive;
    margin: 15px 0;
}
  
.rightContainer .icon {
    color: #51c4d3;
    padding-right: 10px;
}
  
.del_button {
    width: 100px;
    height: 40px;
    border: none;
    background: var(--purple);
    border-radius: 25px;
    font-size: 15px;
    color: white;
    font-weight: 700;
    cursor: pointer;
    outline: none;
}
  
@media screen and (max-width: 800px) {
      
    .postPageBG {
        height: 400px;
    }
      
    .upperContent h1 {
        color: #126e82;
        font-size: 2rem;
        font-weight: bold;
    }
  
    .upperContent p {
        margin-top: 10px;
        font-size: 1rem;
    }
  
    .postContainer {
        align-items: center;
        justify-content: center;
        width: 90%;
    }
  
    .leftContainer {
        width: 90%;
        align-items: center;
    }
  
    .rightContainer {
        width: 90%;
        margin-top: 30px;
        font-size: 1rem;
        height: fit-content;
    }
  
}


CSS




/* client/src/styles/card.css */
  
.card {
    position: relative;
    width: 300px;
    height: 500px;
    margin: 20px;
    box-shadow: 20px 20px 50px rgba(0, 0, 0, 0.5);
    background: var(--cream);
    padding: 10px;
    overflow: hidden;
    display: flex;
    justify-content: center;
    align-items: center;
    border-top: 1px solid rgba(255, 255, 255, 0.5);
    border-left: 1px solid rgba(255, 255, 255, 0.5);
    backdrop-filter: blur(5px);
}
  
.card .content {
    padding: 10px;
    text-align: center;
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: center;
    transform: translateY(20px);
    transition: 0.5s;
}
  
.card:hover .content {
    transform: translateY(0px);
}
  
.card .content img {
    height: 200px;
    width: 280;
    overflow: hidden;
    padding: 10px;
}
  
  
.card .content h4 {
    padding: 10px 0;
    font-size: 1.5em;
    color: var(--dark);
    z-index: 1;
}
  
.card .content h6 {
    font-size: 1.1em;
    color: var(--dark-purple);
    font-weight: bold;
    padding: 5px 0;
}
  
.card .content h6 span {
    color: var(--dark);
    font-weight: bold;
}
  
.card .content p {
    margin: 0 10px;
    padding: 5px 5px;
    color: var(--dark);
    background-color: rgba(255, 255, 255, 0.174);
    width: 80%;
}
  
.card .content button {
    position: relative;
    display: inline-block;
    padding: 8px 20px;
    margin: 20px 0;
    background: white;
    color: black;
    border-radius: 20px;
    text-decoration: none;
    font-weight: 500;
    box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3);
    cursor: pointer;
    transition: all 0.3s ease-in-out;
}
  
.card .content button:hover {
    transform: translateY(-2px);
    background-color: rgb(233, 246, 254);
}


CSS




/* client/src/styles/navbar.css */
  
* {
    margin: 0;
    padding: 0;
    text-decoration: none;
}
  
.navContainer {
    overflow: hidden;
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    background-color: var(--dark);
    box-shadow: 0 5px 10px rgba(0, 0, 0, 0.1);
    padding: 0px 7%;
    display: flex;
    align-items: center;
    justify-content: space-between;
    z-index: 100;
}
  
  
.navLogo {
    color: white;
    font-weight: 900;
    font-size: 1.5rem;
    font-style: italic;
}
  
.navbar ul {
    list-style: none;
}
  
.navbar ul li {
    position: relative;
    float: left;
}
  
.profilePicture {
    height: 40px;
    width: 40px;
}
  
.profilePicture img {
    margin-top: 10px;
    height: 40px;
    width: 40px;
    border-radius: 50%;
    object-fit: cover;
}
  
#usernamename {
    display: none;
}
  
.navbar ul li p {
    font-size: 1rem;
    padding: 20px;
    color: white;
    display: block;
    transition: all 1s;
}
  
.navbar ul li p:hover {
    transform: translateY(-1px);
    border-bottom: solid 2px white;
}
  
#menu-bar {
    display: none;
}
  
.navContainer label {
    font-size: 1.5rem;
    color: white;
    cursor: pointer;
    display: none;
}
  
@media (max-width:800px) {
    .navContainer {
        height: 70px;
    }
  
    .navContainer label {
        display: initial;
    }
  
    .navContainer .navbar {
        position: fixed;
        top: 70px;
        left: -100%;
        text-align: center;
        background: white;
        border-top: 1px solid rgba(0, 0, 0, 0.1);
        display: block;
        transition: all 0.3s ease;
        width: 100%;
    }
  
    .profilePicture {
        display: none;
    }
  
    #usernamename {
        font-weight: bolder;
        display: block;
    }
  
    .navbar ul li p {
        color: black;
    }
  
    .navbar ul li p:hover {
        transform: translateY(-1px);
  
        border-bottom: none;
    }
  
    .navbar ul li {
        width: 100%;
    }
  
    #menu-bar:checked~.navbar {
        left: 0;
    }
}


Javascript




// client/src/index.js
  
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import "./style.css"
import { AuthContextProvider } from './authContext';
  
  
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
    <AuthContextProvider>
        <React.StrictMode>
            <App />
        </React.StrictMode>
    </AuthContextProvider>
);


Javascript




// client/src/App.js
  
import {BrowserRouter, Routes, Route} from "react-router-dom"
import Home from "./pages/Home";
import Login from "./pages/Login";
import Register from "./pages/Register";
import Create from "./pages/Create"
import View from "./pages/View"
import { useContext } from "react";
import { AuthContext } from "./authContext";
  
function App() {
  
  const { user } = useContext(AuthContext);
  
  const ProtectedRoute = ({ children }) => {
    if (!user) {
      return <Login/>;
    } else {
      return children;
    }
  };
  
  return (
    <BrowserRouter>
      <Routes>
        <Route path="/" element={<ProtectedRoute><Home /></ProtectedRoute>} />
        <Route path="/login" element={<Login/>} />
        <Route path="/register" element={<Register/>} />
        <Route path="/create" element={<ProtectedRoute><Create/></ProtectedRoute>} />
        <Route path="/view/:id" element={<ProtectedRoute><View/></ProtectedRoute>} />
      </Routes>
    </BrowserRouter>
  );
}
  
export default App;


Javascript




// client/src/authContext.js
  
import { createContext, useReducer, useEffect } from "react"
  
const INITIAL_STATE = {
    user: JSON.parse(localStorage.getItem("user")) || null,
    loading: false,
    error: null,
};
  
  
export const AuthContext = createContext(INITIAL_STATE)
  
const AuthReducer = (state, action) => {
    switch (action.type) {
        case "LOGIN_START":
            return {
                user: null,
                loading: true,
                error: null
            };
  
        case "LOGIN_SUCCESS":
            return {
                user: action.payload,
                loading: false,
                error: null
            };
  
        case "LOGIN_FAILURE":
            return {
                user: null,
                loading: false,
                error: action.payload
            };
  
        case "LOGOUT":
            return {
                user: null,
                loading: false,
                error: null
            };
        default:
            return state;
    }
}
  
export const AuthContextProvider = ({ children }) => {
    const [state, dispatch] = useReducer(AuthReducer, INITIAL_STATE)
  
    useEffect(() => {
        localStorage.setItem("user", JSON.stringify(state.user))
    }, [state.user])
  
  
    return (
        <AuthContext.Provider
            value={{
                user: state.user,
                loading: state.loading,
                error: state.error,
                dispatch
            }}
        >
            {children}
        </AuthContext.Provider>
    )
}


Javascript




// client/src/useFetch.js
  
import { useEffect, useState } from "react";
import axios from "axios";
  
const useFetch = (url) => {
    const [data, setData] = useState([]);
    const [loading, setLoading] = useState(false);
    const [error, setError] = useState(false);
  
    useEffect(() => {
        const fetchData = async () => {
            setLoading(true);
            try {
  
                const res = await axios.get(url)
                  
                setData(res.data);
            } catch (err) {
                setError(err);
            }
            setLoading(false);
        };
        fetchData();
    }, [url]);
  
    const reFetch = async () => {
        setLoading(true);
        try {
            const res = await axios.get(url)
  
            setData(res.data);
        } catch (err) {
            setError(err);
        }
        setLoading(false);
    };
  
    return { data, loading, error, reFetch };
};
  
export default useFetch;


Javascript




// client/src/pages/Home.jsx
  
import React, {
    useContext,
    useState
} from 'react'
import Navbar from '../components/Navbar'
import {
    faMagnifyingGlass
} from "@fortawesome/free-solid-svg-icons";
import {
    FontAwesomeIcon
} from "@fortawesome/react-fontawesome";
import useFetch from "../useFetch"
import {
    AuthContext
} from '../authContext';
import '../styles/home.css'
import Card from '../components/Card';
  
const Home = () => {
    const [query, setQuery] = useState("");
    const { user } = useContext(AuthContext)
    const { data, loading } = useFetch(
        `/entries/author/${user._id}`)
  
    const keys = ["title", "location", "date"];
  
    const search = (data) => {
        return data.filter((item) =>
            keys.some((key) => item[key] &&
                item[key].toLowerCase().includes(query))
        );
    };
  
  
    return (
        <div>
            <Navbar />
            <div className="search">
                <div className="searchBar">
                    <h2>Explore</h2>
                    <div className="searchInput">
                        <input
                            type="text"
                            placeholder="Search places or dates"
                            onChange={(e) => setQuery(e.target.value)}
                        />
                        <FontAwesomeIcon
                            className="icon"
                            icon={faMagnifyingGlass} />
                    </div>
                </div>
            </div>
  
            <div className="searchedPosts">
                {loading ? (
                    <>
                        <div className="p"
                            style={{
                                color: "white", "fontFamily":
                                    "'Kaushan Script', cursive"
                            }}>
                            Loading...
                        </div>
                    </>
                ) : (
                    <>
                        {search(data)?.map((item, i) => (
                            <Card
                                key={i} // Remember to add a unique key
                                _id={item._id}
                                photos={item.photos}
                                title={item.title}
                                date={item.date}
                                location={item.location}
                                text={item.text}
                            />
                        ))}
                    </>
                )}
            </div>
        </div>
    )
}
  
export default Home


Javascript




// client/src/pages/Create.jsx
  
import React, { useContext, useState } from 'react'
import axios from "axios"
import { AuthContext } from '../authContext';
import "../styles/create.css"
import { useNavigate } from 'react-router-dom';
import Navbar from '../components/Navbar';
import { faPlusCircle } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import "../styles/create.css"
  
const Create = () => {
    const navigate = useNavigate();
    const { user } = useContext(AuthContext);
    const [files, setFiles] = useState("");
    const [info, setInfo] = useState({});
  
    // set the usestate to the data user passed 
    const handleChange = (e) => {
        setInfo((prev) => ({ ...prev, [e.target.id]: e.target.value }));
    }
  
    // post the usestate to database
    const handleClick = async (e) => {
        e.preventDefault();
  
        var newEntry
  
        if (files) {
            const list = await Promise.all(Object.values(files).map(async (file) => {
                const data = new FormData();
                data.append("file", file);
                data.append("upload_preset", "upload")
                const uploadRes = await axios.post(
                    "https://api.cloudinary.com/v1_1/<your_cloudinary_key>/image/upload",
                    data, { withcredentials: false }
                )
                const { url } = uploadRes.data;
                return url;
            }))
  
  
            newEntry = {
                ...info, author: user._id, photos: list
            }
  
        }
        else {
            newEntry = {
                ...info, author: user._id
            }
        }
  
  
        try {
            const response = await axios.post('http://localhost:5500/api/entries/',
                newEntry, {
                withCredentials: false
            })
  
            navigate(`/view/${response?.data?._id}`);
        }
        catch (err) {
            console.log(err)
        }
    }
  
    return (
        <div className='create'>
            <Navbar />
            <div className="createContainer">
  
                <div className="picsContainer">
  
                    <div className="formInput">
                        <h2>Upload Images (Max 3)</h2>
                        <label htmlFor="file">
                            <FontAwesomeIcon
                                className="icon" icon={faPlusCircle} />
                        </label>
                        <input
                            type="file"
                            id="file"
                            multiple
                            onChange={(e) => setFiles(e.target.files)}
                            style={{ display: "none" }}
                        />
                    </div>
                    <div className="uploadedPictures">
                        <div className="upload_pic">
                            <img
                                src={
                                    files[0]
                                        ? URL.createObjectURL(files[0])
                                        : ""
                                }
                                alt=""
                                height="80px"
                            />
                        </div>
                        <div className="upload_pic">
                            <img
                                src={
                                    files[1]
                                        ? URL.createObjectURL(files[1])
                                        : ""
                                }
                                alt=""
                                height="80px"
                            />
                        </div>
                        <div className="upload_pic">
                            <img
                                src={
                                    files[2]
                                        ? URL.createObjectURL(files[2])
                                        : ""
                                }
                                alt=""
                                height="80px"
                            />
                        </div>
                    </div>
  
                </div>
  
                <div className="input">
                    <label htmlFor="title">Title</label>
                    <input
                        onChange={handleChange}
                        type="text"
                        id="title"
                        placeholder="Enter Title"
                    />
                </div>
                <div className="input">
                    <label htmlFor="title">
                        Location
                    </label>
                    <input
                        onChange={handleChange}
                        type="text"
                        id="location"
                        placeholder="Enter Location"
                    />
                </div>
  
                <div className="input">
                    <label htmlFor="date">
                        What is the Date
                    </label>
                    <input
                        onChange={handleChange}
                        type="date"
                        id="date"
                        placeholder="Choose Date"
                    />
                </div>
  
                <div className="input">
                    <label htmlFor="entry">
                        Write your thoughts..
                    </label>
                    <textarea
                        name='entry'
                        id='text'
                        cols="150"
                        rows='25'
                        onChange={handleChange}
                        autoFocus
                    ></textarea>
                </div>
  
                <button className='createBtn'
                    onClick={handleClick}>
                    Create Entry
                </button>
            </div>
        </div>
    )
}
  
export default Create


Javascript




// client/src/pages/Login.jsx
  
import React from "react";
import Navbar from "../components/Navbar";
import "../styles/login.css";
import axios from "axios";
import {
    useContext,
    useState
} from "react";
import {
    useNavigate,
    Link
} from "react-router-dom";
import { AuthContext } from "../authContext";
  
function Login() {
    const [credentials, setCredentials] = useState({
        username: undefined,
        password: undefined,
    });
  
    const { dispatch } = useContext(AuthContext);
    const navigate = useNavigate();
  
    const handleChange = (e) => {
        setCredentials((prev) => ({
            ...prev,
            [e.target.id]: e.target.value
        }));
    };
  
    const handleClick = async (e) => {
        e.preventDefault();
        dispatch({ type: "LOGIN_START" });
        try {
            const res = await axios.post("http://localhost:5500/api/users/login",
                credentials);
            dispatch({
                type: "LOGIN_SUCCESS",
                payload: res.data.details
            });
            navigate('/');
        } catch (err) {
            if (err.response && err.response.data) {
                /*
                If error response and data exist, 
                dispatch LOGIN_FAILURE with error message
                */
                dispatch({
                    type: "LOGIN_FAILURE",
                    payload: err.response.data
                });
            } else {
                /*
                 If no error response or data,
                 dispatch generic error message
                */
                dispatch({
                    type: "LOGIN_FAILURE",
                    payload: "An error occurred while logging in"
                });
            }
        }
    };
  
  
    return (
        <div className="login">
            <Navbar />
            <div className="loginCard">
                <div className="center">
                    <h1>Welcome Back!</h1>
                    <form>
                        <div className="txt_field">
                            <input
                                type="text"
                                placeholder="username"
                                id="username"
                                onChange={handleChange}
                                className="lInput"
                            />
                        </div>
                        <div className="txt_field">
                            <input
                                type="password"
                                placeholder="password"
                                id="password"
                                onChange={handleChange}
                                className="lInput"
                            />
                        </div>
                        <div className="login_button">
                            <button className="button"
                                onClick={handleClick}>
                                Login
                            </button>
                        </div>
                        <div className="signup_link">
                            <p>
                                Not registered?
                                <Link to="/register">Register</Link>
                            </p>
                        </div>
                    </form>
  
                </div>
            </div>
        </div>
    );
}
  
export default Login;


Javascript




// client/src/pages/Register.jsx
  
import React from "react";
import Navbar from "../components/Navbar";
import "../styles/register.css";
import {
    faPlusCircle
} from "@fortawesome/free-solid-svg-icons";
import {
    FontAwesomeIcon
} from "@fortawesome/react-fontawesome";
import {
    Link
} from "react-router-dom";
import {
    useState
} from "react";
import {
    useNavigate
} from "react-router-dom";
import axios from "axios";
  
function Register() {
    const navigate = useNavigate();
  
    const [file, setFile] = useState("");
    const [info, setInfo] = useState({});
  
    const handleChange = (e) => {
        setInfo((prev) => ({ ...prev, [e.target.id]: e.target.value }));
    };
  
    const handleClick = async (e) => {
        e.preventDefault();
  
        if (file) {
            const data = new FormData();
  
            data.append("file", file);
            data.append("upload_preset", "upload");
  
  
            try {
                const uploadRes = await axios.post(
                    "https://api.cloudinary.com/v1_1/<your_cloudinary_key>/image/upload",
                    data, { withcredentials: false }
                );
  
                const { url } = uploadRes.data;
  
                const newUser = {
                    ...info,
                    profilePicture: url,
                };
  
                await axios.post("http://localhost:5500/api/users/register",
                    newUser, { withcredentials: false })
  
                navigate("/login");
            } catch (err) {
                console.log(err);
            }
        } else {
            try {
                await axios.post("http://localhost:5500/api/users/register",
                    info, { withcredentials: false })
  
                navigate("/login");
            } catch (err) {
                console.log(err)
            }
        }
    };
  
  
  
    return (
        <div className="register">
            <Navbar />
            <div className="registerCard">
                <div className="center">
                    <h1>Join Us</h1>
  
                    <form>
                        <div className="image">
                            <img
                                src={
                                    file
                                        ? URL.createObjectURL(file)
                                        : "https://icon-library.com/images/no-image-icon/no-image-icon-0.jpg"
                                }
                                alt=""
                                height="100px"
                            />
  
                            <div className="txt_field_img">
                                <label htmlFor="file">
                                    Image
                                    <FontAwesomeIcon className="icon"
                                        icon={faPlusCircle} />
                                </label>
                                <input
                                    type="file"
                                    id="file"
                                    onChange={(e) => setFile(e.target.files[0])}
                                    style={{ display: "none" }}
                                />
                            </div>
                        </div>
  
                        <div className="formInput">
  
  
                            <div className="txt_field">
                                <input
                                    type="text"
                                    placeholder="username"
                                    name="username"
                                    onChange={handleChange}
                                    id="username"
                                    required
                                />
                            </div>
                            <div className="txt_field">
                                <input
                                    type="email"
                                    placeholder="email"
                                    name="email"
                                    onChange={handleChange}
                                    id="email"
                                    required
                                />
                            </div>
                            <div className="txt_field">
                                <input
                                    type="password"
                                    placeholder="password"
                                    name="password"
                                    onChange={handleChange}
                                    id="password"
                                    //   value={data.password}
                                    required
                                />
                            </div>
                        </div>
                        <div className="login_button">
                            <button className="button"
                                onClick={handleClick}>
                                Register
                            </button>
                        </div>
                        <div className="signup_link">
                            <p>
                                Already Registered?
                                <Link to="/login">Login</Link>
                            </p>
                        </div>
                    </form>
                </div>
            </div>
        </div>
    );
}
  
export default Register;


Javascript




// client/src/pages/View.jsx
  
import React, {
    useContext,
    useState
} from 'react'
import Navbar from '../components/Navbar'
import useFetch from '../useFetch'
import {
    faCalendar,
    faMapLocationDot,
    faCircleArrowLeft,
    faCircleArrowRight
} from "@fortawesome/free-solid-svg-icons";
import {
    useLocation,
    useNavigate
} from "react-router-dom";
import {
    FontAwesomeIcon
} from "@fortawesome/react-fontawesome";
import "../styles/view.css"
import axios from "axios";
import { AuthContext } from "../authContext";
  
const View = () => {
    const location = useLocation();
    const id = location.pathname.split("/")[2];
    const { user } = useContext(AuthContext);
    const { data } = useFetch(`/entries/${id}`)
    const [slideNumber, setSlideNumber] = useState(0);
  
    const navigate = useNavigate();
  
    const handleDelete = async (id) => {
        try {
  
            await axios.delete(`http://localhost:5500/api/entries/${data._id}`)
  
            navigate('/')
        } catch (err) {
            console.log(err)
        }
    };
  
    const handleMove = (direction) => {
        let newSlideNumber;
        let size = data.photos.length
        if (direction === "l") {
            newSlideNumber = slideNumber === 0 ? size - 1 : slideNumber - 1;
        } else {
            newSlideNumber = slideNumber === size - 1 ? 0 : slideNumber + 1;
        }
        setSlideNumber(newSlideNumber)
    }
  
    return (
        <div className='view'>
            <Navbar />
            <div className="postPageBG">
                <div className="upperContent">
                    <h1>{data.title}</h1>
                    <p><FontAwesomeIcon className="icon"
                        icon={faCalendar} />
                        {data.date}
                    </p>
                    <p><FontAwesomeIcon className="icon"
                        icon={faMapLocationDot} />
                        {data.location}
                    </p>
                </div>
            </div>
  
            <div className="postContainer">
  
                <div className="leftContainer">
  
  
                    {data.photos ? (<div className="images">
  
  
                        <img src={data.photos[slideNumber]}
                            height="300px" alt="" />
  
                        {data.photos.length > 1 ? <div className="arrows">
                            <FontAwesomeIcon
                                icon={faCircleArrowLeft}
                                className="arrow"
                                onClick={() => handleMove("l")}
                            />
                            <FontAwesomeIcon
                                icon={faCircleArrowRight}
                                className="arrow"
                                onClick={() => handleMove("r")}
                            />
                        </div> : ""}
                    </div>) : ("no Images")}
  
                </div>
  
                <div className="rightContainer">
  
                    <p>
                        " {data.text} "
                    </p>
                    <button className="del_button"
                        style={{ "marginRight": "5px" }}
                        onClick={handleDelete}>
                        Delete
                    </button>
  
                </div>
  
            </div>
        </div>
    )
}
  
export default View


Javascript




// client/src/components/Card.jsx
  
import React from "react";
import { Link } from "react-router-dom";
import "../styles/card.css";
  
function Card(props) {
  
  
    return (
        <div className="card">
            <div class="content">
                <img id="post-image" src={props.photos[0]}
                    alt="no content" />
                <h4>{props.title}</h4>
                <h6>
                    <span>Date : </span> {props.date}
                </h6>
                <h6>
                    <span>Location : </span> {props.location}
                </h6>
                <p>{props.text.slice(0, 60)}...</p>
                <Link to={`view/${props._id}`}>
                    <button>Read More</button>
                </Link>
            </div>
        </div>
    );
}
  
export default Card;


Javascript




// client/src/components/Navbar.jsx
  
import '../styles/navbar.css'
import {
    useContext
} from 'react';
import {
    faBars
} from '@fortawesome/free-solid-svg-icons'
import {
    FontAwesomeIcon
} from "@fortawesome/react-fontawesome";
import {
    Link, useNavigate
} from "react-router-dom"
import {
    AuthContext
} from "../authContext"
  
  
const Navbar = () => {
  
    const navigate = useNavigate()
  
    const { user, dispatch } = useContext(AuthContext)
    const handleClick = async (e) => {
        e.preventDefault();
        dispatch({ type: "LOGOUT" });
        navigate("/")
    }
  
  
  
    return (
        <div className='navContainer'>
            <Link to="/">
                <p className='navLogo'>Reminisce</p>
            </Link>
  
            <input type="checkbox" id='menu-bar' />
            <label htmlFor="menu-bar">
                <FontAwesomeIcon icon={faBars} className="icon" />
            </label>
            <nav className='navbar'>
                <ul>
                    <Link to="/">
                        <li><p>Home</p></li>
                    </Link>
                    <Link to="/create">
                        <li><p>Create</p></li>
                    </Link>
                    {user ? (<>
  
                        <li onClick={handleClick} style={{ cursor: "pointer" }}>
                            <p>Logout</p>
                        </li>
                        <li><div className="profilePicture">
                            <img src={user.profilePicture ||
                                "https://i.ibb.co/MBtjqXQ/no-avatar.gif"} alt="" />
                        </div></li>
                        <li id="usernamename"><p>{user.username}</p></li>
  
                    </>
                    )
                        :
                        (
                            <>
                                <Link to="/register">
                                    <li><p>Register</p></li>
                                </Link>
                                <Link to="/login">
                                    <li><p>Login</p></li>
                                </Link>
                            </>
                        )}
                </ul>
            </nav>
        </div >
    )
}
  
export default Navbar


Start your application using the following command in your terminal.

cd client
npm start

Output:

  • Browser Output
ReactApp-GoogleChrome2024-02-2823-44-55-ezgifcom-video-to-gif-converter

Final Application

  • Data Saved in Database:
gfg69

Output



Like Article
Suggest improvement
Share your thoughts in the comments

Similar Reads