Travel Journal App with MERN Stack with API
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.
Prerequisites:
- NPM & Node JS
- ReactJS
- MongoDB
- ExpressJS
- MERN Stack
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):
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):
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 inAuthContextProvider
for authentication data handling. RenderApp
within React strict mode. Define routes inApp.js
for page mapping. ImplementuseFetch
for data fetching. UseProtectedRoute
to limit access. Establish authentication context withAuthReducer
anduseReducer
, managed byAuthContextProvider
.
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 h 2 { 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.5 rem; } .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.2 rem; transition: all 0.3 s ease; } .picsContainer .formInput label:hover { transform: translateY( -2px ); } .picsContainer .uploadedPictures { display : flex; justify- content : center ; align-items: center ; flex-wrap: wrap; gap: 30px ; } .picsContainer h 1 { text-align : center ; padding : 0 0 20px 0 ; font-size : 25px ; font-size : 1.2 rem; } .input label { font-size : 1.5 rem; } .input input { font-size : 1.2 rem; 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 : 100 vh; overflow : hidden ; } . center { position : absolute ; top : 50% ; left : 50% ; transform: translate( -50% , -50% ); width : 400px ; background : white ; border-radius: 10px ; padding-top : 20px ; } . center h 1 { font-size : 1.2 rem; 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 h 1 { font-size : 1.2 rem; 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 h 1 { color : var(-- blue ); font-size : 2.5 rem; margin-bottom : 20px ; font-weight : bold ; } .upperContent p { margin-top : 10px ; font-size : 1.2 rem; 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.25 rem; 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 h 1 { color : #126e82 ; font-size : 2 rem; font-weight : bold ; } .upperContent p { margin-top : 10px ; font-size : 1 rem; } .postContainer { align-items: center ; justify- content : center ; width : 90% ; } .leftContainer { width : 90% ; align-items: center ; } .rightContainer { width : 90% ; margin-top : 30px ; font-size : 1 rem; 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.5 s; } .card:hover .content { transform: translateY( 0px ); } .card .content img { height : 200px ; width : 280 ; overflow : hidden ; padding : 10px ; } .card .content h 4 { padding : 10px 0 ; font-size : 1.5em ; color : var(--dark); z-index : 1 ; } .card .content h 6 { font-size : 1.1em ; color : var(--dark- purple ); font-weight : bold ; padding : 5px 0 ; } .card .content h 6 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.3 s 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.5 rem; 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 : 1 rem; padding : 20px ; color : white ; display : block ; transition: all 1 s; } .navbar ul li p:hover { transform: translateY( -1px ); border-bottom : solid 2px white ; } #menu-bar { display : none ; } .navContainer label { font-size : 1.5 rem; 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.3 s 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
- Data Saved in Database:
Contact Us