Travel Journal App with MERN Stack with API
Last Updated :
08 Mar, 2024
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.
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):
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!' ) });
app.use(cookieParser())
app.use(express.json());
app.use(helmet());
app.use(cors({
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 },
{ $pull: { entries: req.params.id } },
{ 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 {
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):
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"
/>
< title >React App</ title >
</ head >
< body >
< noscript >You need to enable JavaScript to run this app.</ noscript >
< div id = "root" ></ div >
</ body >
</ html >
|
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
.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
.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
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
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
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
.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
* {
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
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
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
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
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
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}
_id={item._id}
photos={item.photos}
title={item.title}
date={item.date}
location={item.location}
text={item.text}
/>
))}
</>
)}
</div>
</div>
)
}
export default Home
|
Javascript
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({});
const handleChange = (e) => {
setInfo((prev) => ({ ...prev, [e.target.id]: e.target.value }));
}
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(
data, { withcredentials: false }
)
const { url } = uploadRes.data;
return url;
}))
newEntry = {
...info, author: user._id, photos: list
}
}
else {
newEntry = {
...info, author: user._id
}
}
try {
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
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 {
credentials);
dispatch({
type: "LOGIN_SUCCESS" ,
payload: res.data.details
});
navigate( '/' );
} catch (err) {
if (err.response && err.response.data) {
dispatch({
type: "LOGIN_FAILURE" ,
payload: err.response.data
});
} else {
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
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(
data, { withcredentials: false }
);
const { url } = uploadRes.data;
const newUser = {
...info,
profilePicture: url,
};
newUser, { withcredentials: false })
navigate( "/login" );
} catch (err) {
console.log(err);
}
} else {
try {
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)
}
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"
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
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:
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
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
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 ||
</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:
Final Application
Output
Share your thoughts in the comments
Please Login to comment...