Open In App

Simple Journal App with MERN Stack

Last Updated : 03 Apr, 2024
Improve
Improve
Like Article
Like
Save
Share
Report

Simple Journal App is a great way to showcase your understanding of full-stack development. In this project, we will be making a Blogging website using MERN stack (MongoDB, ExpressJS, React, NodeJS). We will be using React.js framework and tailwind CSS for our frontend and express.js and MongoDB as our backend. This project will showcase how to upload, View, save, and make blogs as well as keep a record of users on your site.

Project Preview:

Screenshot-2024-03-09-022206

Prerequisites:

  1. MongoDB
  2. Express.js
  3. React.js
  4. Node.js
  5. TinyMce
  6. Tailwind CSS

Approach:

  • Decide on all the functionalities that need to be added in the project
  • Chose the required dependencies and modules that need to be added to the project and install them
  • Start working on backend
  • Make routes needed divide them into user routes and post routes
  • start working on front end
  • make the required components
  • make pages with the said components
  • in app.jsx routes which page should lead to where
  • link backend and frontend

Steps to Create the Application:

Step 1. Create a folder for the project using the following command.

mkdir blogit
cd blogit

Step 2. Create the backend folder.

mkdir backend
cd backend

Step 3: Initialize the Node application using the following command.

npm init -y

Step 4: Install the required dependencies.

npm install mongoose express jsonwebtoken cors
npm install --save-dev nodemon

Folder Structure(backend):

Screenshot-2024-03-08-170803

Frontend Project structure

Dependencies:

"dependencies": {
"cors": "^2.8.5",
"express": "^4.19.2",
"jsonwebtoken": "^9.0.2",
"mongoose": "^8.2.4"
},
"devDependencies": {
"nodemon": "^3.1.0"
}

Example: Create the required files and add the following codes.

Javascript
//index.js

const UserRouter = require("./routes/user.js")
const express = require("express")
const mongo = require("mongoose");
const PostRouter = require("./routes/post.js");
const cors = require("cors")
const app = express();
app.use(cors())
const port = 8000;
app.use(express.json())

app.listen(port, () => {
    console.log(`Server live at http://localhost:${port}`)
})

app.use('/api/v1/user', UserRouter)
app.use('/api/v1/blog', PostRouter)
JavaScript
//db.js

const mongoose = require('mongoose')

mongoose.connect("Your MongoDB URI")

const userSchema = new mongoose.Schema({
    Username: {
        type: String,
        required: true,
        unique: true,
        trim: true,
        lowercase: true,
        minLength: 3,
        maxLength: 30
    },
    password: {
        type: String,
        required: true,
        minLength: 8,
    },
})

const PostSchema = new mongoose.Schema({
    title: {
        type: String,
        required: true
    },
    content: {
        type: String,
        required: true,
    },
    createdAt: {
        type: Date,
        default: Date.now
    },
    author: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true }
})

const User = mongoose.model("User", userSchema)
const Post = mongoose.model("Post", PostSchema)

module.exports = { User, Post }
JavaScript
//config.js

const JWT_SECRET = "Its Your Secret"

module.exports = { JWT_SECRET }
JavaScript
//routes/user.js

const { JWT_SECRET } = require("../config.js")
const jwt = require("jsonwebtoken")
const express = require("express")
const { User } = require("../db")
const mongo = require("mongoose")

const UserRouter = express.Router();

UserRouter.post('/signup', async (req, res) => {
    const existingUser = await User.findOne({
        Username: req.body.username
    })
    if (existingUser) {
        return res.status(411).json({
            message: "Username already taken/Incorrect inputs"
        })
    }
    try {
        const user = await User.create({
            Username: req.body.username,
            password: req.body.password,
        });
        const token = jwt.sign({ id: user._id }, JWT_SECRET);
        return res.json({ token });
    } catch (e) {
        res.status(403).json({ e });
    }
});
UserRouter.post('/signin', async (req, res) => {
    const user = await User.findOne({
        Username: req.body.username,
        password: req.body.password
    })
    console.log("d" + user)
    if (user) {
        const token = jwt.sign({
            userId: user._id
        }, JWT_SECRET);

        res.json({
            token: token
        })
        return;
    }
    res.status(411).json({
        message: "Error while logging in"
    })

});
module.exports = UserRouter
JavaScript
//routes/post.js

const { JWT_SECRET } = require("../config.js")
const jwt = require("jsonwebtoken")
const express = require("express")
const { Post, User } = require("../db")

const PostRouter = express.Router();

const authMiddleware = (req, res, next) => {
    const authHeader = req.headers.authorization;
    if (!authHeader || !authHeader.startsWith('Bearer ')) {
        return res.status(403).json({});
    }
    const token = authHeader.split(' ')[1];
    try {
        const decoded = jwt.verify(token, JWT_SECRET);
        req.userId = decoded.userId;
        next();
    } catch (err) {
        return res.status(403).json({});
    }
};

PostRouter.use('/*', authMiddleware)
PostRouter.get('/bulk', async (req, res) => {

    const blogs = await Post.find({}).populate('author', 'Username');
    return res.json({ blogs });

});

PostRouter.post('/post', async (req, res) => {
    const userId = req.userId;

    const body = req.body;
    const post = await Post.create({
        title: body.title,
        content: body.content,
        author: userId,
    });

    return res.json({
        id: post._id,
    });
});

PostRouter.get('/uname', async (req, res) => {
    const id = req.userId
    const user = await User.findById(id)
    return (res.json({ uname: user.Username }))
})

PostRouter.get('/:id', async (req, res) => {

    const id = req.params.id;
    const blog = await Post.findById(id).populate('author', 'Username')

    return res.json(blog)
});

module.exports = PostRouter;

To start the backend run the following command.

nodemon index.js


Step 5: Create the frontend application using the following command.

npm create vite@latest

name the project accordingly and chose react for framework and then variant javascript

Screenshot-2024-03-09-015213

Initiating frontend with vite


Step 6: Install all the dependencies we need run the commands in the terminal

npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p
npm i axios react-router-dom html-react-parser
npm install --save @tinymce/tinymce-react

Step 7: Replace content of tailwind.config.js file with the below code

/** @type {import('tailwindcss').Config} */
export default {
  content: [
    "./index.html",
    "./src/**/*.{js,ts,jsx,tsx}",
  ],
  theme: {
    extend: {},
  },
  plugins: [],
}

Step 8. Remove all the content in app.css and index.css

Step 9. Create a file config.js in frontend/src and add the below code in your index.css and config.js. Change the URL in config.js to be that of your Backend server

CSS
/*src/index.css */
@tailwind base;
@tailwind components;
@tailwind utilities;
JavaScript
//src/config.js

export const BACKEND_URL = "http://localhost:8000"


Folder Structure:

Screenshot-2024-03-16-155142

Dependencies:

"dependencies": {
"@tinymce/tinymce-react": "^5.0.0",
"axios": "^1.6.8",
"html-react-parser": "^5.1.10",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^6.22.3"
},
"devDependencies": {
"@types/react": "^18.2.66",
"@types/react-dom": "^18.2.22",
"@vitejs/plugin-react": "^4.2.1",
"autoprefixer": "^10.4.19",
"eslint": "^8.57.0",
"eslint-plugin-react": "^7.34.1",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.6",
"postcss": "^8.4.38",
"tailwindcss": "^3.4.3",
"vite": "^5.2.0"
}

Step 10: Create the required files as shown in folder structure and add the following codes.

Javascript
//components/header.jsx

import { Link } from "react-router-dom";

export const HeaderS = ({ type }) => {
    return (
        <div className="px-10 flex-col justify-center">
            <div className="text-3xl font-extrabold">
                {type === "signup" ? "Create an Account" : "Log in To your Account"}
            </div>
            <div className="text-slate-400 mb-4 ">
                {type === "signup"
                    ? "Already have a Account?"
                    : "Dont Have a Account yet?"}
                <Link
                    className="pl-2 underline"
                    to={type === "signup" ? "/signin" : "/signup"}
                >
                    {type === "signup" ? "Sign in" : "Signup"}
                </Link>
            </div>
        </div>
    );
};
JavaScript
//components/Auth.jsx

import React, { useState } from "react";
import { useNavigate } from "react-router-dom";
import axios from "axios";
import { BACKEND_URL } from "../config";
import { HeaderS } from "./header";

export const Auth = ({ type }) => {
    const [username, setUsername] = useState("");
    const [password, setPassword] = useState("");

    const navigate = useNavigate();
    async function sendRequest() {
        try {
            console.log(`${BACKEND_URL}/api/v1/user/${type}`);
            const response = await axios
                .post(`${BACKEND_URL}/api/v1/user/${type}`, {
                    username: username,
                    password: password,
                });
            console.log(response);
            const jwt = response.data.token;
            console.log(jwt);
            localStorage.setItem("token", "Bearer " + jwt);

            navigate("/blogs");
        } catch (e) {
            alert(
                "Error, Try again| use a diffrent username | 
        make sure username is atleast 3 characters
         long and password 8 characters "
            );
        }
    }
    return (
        <div className="h-screen flex justify-center flex-col">
            <div className="flex justify-center">
                <div>
                    <HeaderS type={type}></HeaderS>

                    <LabelledInput
                        label="Username"
                        placeholder="Alucard"
                        onChange={(e) => {
                            setUsername(e.target.value);
                        }}
                    />
                    <LabelledInput
                        label="Password"
                        type={"password"}
                        placeholder="Apassword"
                        onChange={(e) => {
                            setPassword(e.target.value);
                        }}
                    />
                    <button
                        onClick={sendRequest}
                        type="button"
                        className="mt-8 w-full text-center text-white 
            bg-gray-800 hover:bg-gray-900 focus:outline-none
             focus:ring-4 focus:ring-gray-300 font-medium 
             rounded-lg text-sm px-5 py-2.5 me-2 mb-2"
                    >
                        {type === "signup" ? "SignUp" : "Sign in"}
                    </button>
                </div>
            </div>
        </div>
    );
};

function LabelledInput({ label, placeholder,
    onChange, type = "text" }) {
    return (
        <div className="mt-3 ">
            <label className="block mb-2 text-sm 
      font-medium text-gray-900">
                {label}
            </label>
            <input
                onChange={onChange}
                type={type}
                className="bg-gray-50 border border-gray-300 
        text-gray-900 text-sm rounded-lg focus:ring-blue-500 
        focus:border-blue-500 block w-full p-2.5"
                placeholder={placeholder}
            />
        </div>
    );
}
JavaScript
//components/Quote.jsx

export const Quote = () => {
    return (
        <div className="bg-slate-200 h-screen flex justify-center flex-col">
            <div className="flex justify-center">
                <div className="max-w-md ">
                    <div className="text-3xl font-bold">A great Bloging site</div>
                    <div className="text-xl font-semibold flex text-left mt-2">
                        LuffyToro
                    </div>
                    <div className="text-m font-sm text-slate-400 flex text-left">
                        CEO Akifena
                    </div>
                </div>
            </div>
        </div>
    );
};
JavaScript
//pages/signup.jsx


import { Auth } from "../components/Auth"
import { Quote } from "../components/Quote"

export const Signup = () => {
    return <div>
        <div className="grid lg:grid-cols-2">
            <div>
                <Auth type="signup" />
            </div>
            <div className="hidden lg:block">
                <Quote></Quote>
            </div>
        </div>
    </div>
}
JavaScript
//pages/signin.jsx

import { Auth } from "../components/Auth"
import { Quote } from "../components/Quote"

export const Signin = () => {
    return <div>
        <div className="grid lg:grid-cols-2">
            <div>
                <Auth type="signin" />
            </div>
            <div className="hidden lg:block">
                <Quote></Quote>
            </div>
        </div>
    </div>
}
JavaScript
// frontend/App.jsx

import { BrowserRouter, Route, Routes } from 'react-router-dom'
import { Signin } from './pages/signin'
import { Signup } from './pages/signup'


function App() {

    return (
        <>
            <BrowserRouter>
                <Routes>
                    <Route path="/signup" element={<Signup />} />
                    <Route path="/signin" element={<Signin />} />
                    <Route path="" element={<Signin />}></Route>
                </Routes>
            </BrowserRouter>
        </>
    )
}

export default App
JavaScript
//components/Blogcard.jsx

import { Link } from "react-router-dom";

export const Blogcard = ({ id, authorName, title, 
    content, publishedDate }) => {
    return (
        <Link to={`/blog/${id}`}>
            <div className="border-b border-slate-200 p-4
             w-screen max-w-screen-md cursor-pointer">
                <div className="flex w-full">
                    <div className="flex justify-center flex-col">
                        <Avatar name={authorName} />
                    </div>
                    <div className="font-extralight pl-2
                     flex justify-center flex-col">
                        {authorName}
                    </div>
                    <div className="flex justify-center flex-col pl-2">
                        <Circle></Circle>
                    </div>
                    <div className="pl-2 font-thin text-slate-500
                     flex justify-center flex-col">
                        {publishedDate}
                    </div>
                </div>
                <div className="pt-3 text-xl font-semibold">{title}</div>
                <div className="pt-1 text-md font-thin">
                    {convHtml(content).slice(0, 100) + "..."}
                </div>
                <div className="text-slate-400 test-sm font-thin pt-4">
                    {`${Math.ceil(content.length / 500)} 
                     minute(s) read`}
                </div>
            </div>
        </Link>
    );
};

export function Circle() {
    return <div className="bg-slate-400 w-1 h-1 rounded-full"></div>;
}
export function Avatar({ name }) {
    return (
        <div className="relative inline-flex items-center justify-center
         w-7 h-7 overflow-hidden bg-gray-100 rounded-full dark:bg-gray-600">
            <span className="font-medium text-gray-600 dark:text-gray-300">
                {name[0]}
            </span>
        </div>
    );
}

function convHtml(html) {
    const plainText = html.replace(/<\/?[^>]+(>|$)/g, "");
    return plainText;
}
JavaScript
// frontend/src/components/Appbar.jsx

import { Link } from "react-router-dom"
import { Avatar } from "./Blogcard"

export const AppBar = () => {
    return <div className="py-2 border-b flex
     justify-between px-10">
        <Link to={'/blogs'}>
            <div className="pt-2">
                Medium
            </div>
        </Link>
        <div >
            <Link to={'/publish'}>
                <button type="button" className="text-white
             bg-green-700 hover:bg-green-800 focus:outline-none
              focus:ring-4 focus:ring-green-300 font-medium 
              rounded-full text-sm px-5 py-2.5 text-center
               me-5" >Publish</button>
            </Link>
            <Avatar name={localStorage.getItem("Username")}>

            </Avatar>
        </div>
    </div>
}
JavaScript
//components/BlogPage.jsx

import { AppBar } from "./Appbar"
import { Avatar } from "./Blogcard"
import parse from 'html-react-parser'

export const BlogPage = ({ blog }) => {
    const RederedC = parse(blog.content)
    console.log(RederedC)
    return (<div>
        <AppBar name={blog.author.Username} />
        <div className="grid grid-cols-12 px-10 w-full
     pt-12 max-w-screen-2xl">
            <div className=" col-span-8">
                <div className="text-5xl font-extrabold">
                    {blog.title}
                </div>
                <div className="text-slate-500 pt-2">
                    Posted on 2nd Dec 2032
                </div>
                <div className="pt-4">
                    {RederedC}
                </div>
            </div>
            <div className="col-span-4">
                <div className="text-slate-600">
                    Author
                </div>
                <div className="flex">
                    <div className="pr-2 flex justify-center flex-col">
                        <Avatar name={blog.author.Username || "Anon"}></Avatar>
                    </div>
                    <div>
                        <div className="text-xl font-bold">
                            {blog.author.Username || "Anonymous"}
                        </div>
                    </div>
                </div>

            </div>
        </div>
    </div>
    )
}
JavaScript
//components/tinytext.jsx

import React, { useRef, useState } from "react";
import { Editor } from "@tinymce/tinymce-react";
import { BACKEND_URL } from "../config";
import axios from "axios";
import { useNavigate } from "react-router-dom";

export function Editortiny() {
    const editorRef = useRef(null);
    const [title, stitle] = useState("");

    let ctemp = "";
    const navigate = useNavigate();

    async function sendRequest() {
        try {
            const lk = `${BACKEND_URL}/api/v1/blog/post`;
            if (ctemp === "") {
                console.log("input empty");
                return;
            }

            const response = await axios.post(
                lk,
                {
                    title: title,
                    content: ctemp,
                },
                {
                    headers: {
                        Authorization: localStorage.getItem("token"),
                    },
                }
            );

            navigate(`/blog/${response.data.id}`);
        } catch (e) { }
    }
    const savedata = async () => {
        if (editorRef.current) {
            ctemp = await editorRef.current.getContent();
            sendRequest();
        } else {
            return null;
        }
    };

    return (
        <div>
            <div className="flex justify-center w-full pt-8">
                <div className="max-w-screen-lg w-full me-20">
                    <input
                        onChange={(e) => {
                            stitle(e.target.value);
                        }}
                        className="m-5  bg-gray-50 border border-gray-300
                         text-gray-900 text-sm rounded-lg 
                         focus:ring-blue-500 focus:border-blue-500
                          block w-full p-2.5"
                        placeholder="Title"
                    />
                </div>
            </div>
            <div className="p-6">
                <Editor
                    apiKey="YOUR_API_KEY"
                    onInit={(evt, editor) =>
                        (editorRef.current = editor)}
                    initialValue="<p>This is the initial content
                     of the editor.</p>"
                    init={{
                        height: 750,
                        menubar: false,
                        plugins: [
                            "advlist",
                            "autolink",
                            "lists",
                            "link",
                            "image",
                            "charmap",
                            "preview",
                            "anchor",
                            "searchreplace",
                            "visualblocks",
                            "code",
                            "fullscreen",
                            "insertdatetime",
                            "media",
                            "table",
                            "code",
                            "help",
                            "wordcount",
                        ],
                        toolbar:
                            "undo redo | blocks | " +
                            "bold italic forecolor | alignleft aligncenter " +
                            "alignright alignjustify | bullist numlist outdent indent | " +
                            "removeformat | help",
                        content_style:
                            "body { font-family:Helvetica,Arial,sans-serif; font-size:14px }",
                    }}
                />
                <div className="flex justify-center flex-col pt-5">
                    <button
                        onClick={savedata}
                        type="button"
                        className="text-white bg-blue-700 hover:bg-blue-800
                         focus:outline-none focus:ring-4 focus:ring-blue-300
                          font-medium rounded-full text-sm px-5 py-2.5
                           text-center me-2 mb-2"
                    >
                        Submit
                    </button>
                </div>
            </div>
        </div>
    );
}
JavaScript
//hooks/index.js

import { useEffect, useState } from "react";
import { BACKEND_URL } from "../config";
import axios from "axios";

export const useBlog = ({ id }) => {
    const [loading, setLoading] = useState(true);
    const [blog, setBlog] = useState(null);

    useEffect(() => {
        axios
            .get(`${BACKEND_URL}/api/v1/blog/${id}`, {
                headers: {
                    Authorization: localStorage.getItem("token"),
                },
            })
            .then((response) => {
                setBlog(response.data);
                setLoading(false);
            });
    }, [id]);
    getuname();
    return {
        loading,
        blog,
    };
};

export const useBlogs = () => {
    const [loading, setLoading] = useState(true);
    const [blogs, setBlogs] = useState([]);

    useEffect(() => {
        axios
            .get(`${BACKEND_URL}/api/v1/blog/bulk`, {
                headers: {
                    Authorization: localStorage.getItem("token"),
                },
            })
            .then((response) => {
                setBlogs(response.data.blogs);
                setLoading(false);
            });
    }, []);
    getuname();
    return {
        loading,
        blogs,
    };
};

const getuname = () => {
    useEffect(() => {
        axios
            .get(`${BACKEND_URL}/api/v1/blog/uname`, {
                headers: {
                    Authorization: localStorage.getItem("token"),
                },
            })
            .then((response) => {
                localStorage.setItem("Username", response.data.uname);
            });
    }, []);
};
JavaScript
//pages/Blog.jsx

import { useParams } from "react-router-dom";
import { useBlog } from "../hooks";
import { BlogPage } from "../components/BlogPage";

export const Blog = () => {
    const { id } = useParams();
    const { loading, blog } = useBlog({
        id: id || ""
    });
    if (loading) {
        return <div>
            loading...
        </div>
    }
    return <div>
        <BlogPage blog={blog} />
    </div>
}
JavaScript
//pages/Blogs.jsx

import { AppBar } from "../components/Appbar"
import { Blogcard } from "../components/Blogcard"
import { useBlogs } from "../hooks";

export const Blogs = () => {
    const { loading, blogs } = useBlogs();
    if (loading) {
        return <div>
            Loading...
        </div>
    }

    return (<div>
        <AppBar />
        <div className="flex justify-center">
            <div className="max-w-xl ">
                {blogs.map(blog => <Blogcard
                    authorName={blog.author.Username || "Anon"}
                    title={blog.title}
                    publishedDate={blog.createdAt.substring(0, 10)}
                    content={blog.content}
                    id={blog._id}>
                </Blogcard>)}
            </div>
        </div>
    </div>
    )
}
JavaScript
//pages/publish.jsx

import { AppBar } from "../components/Appbar"
import { Editortiny } from "../components/tinytext"


export const Publish = () => {

    return <div>
        <AppBar />
        <Editortiny />
    </div>
}
JavaScript
//App.jsx


import { BrowserRouter, Route, Routes } from 'react-router-dom'
import { Signin } from './pages/signin'
import { Blog } from './pages/Blog'
import { Blogs } from './pages/Blogs'
import { Signup } from './pages/signup'
import { Publish } from './pages/publish'

function App() {

    return (
        <>
            <BrowserRouter>
                <Routes>
                    <Route path="/signup" element={<Signup />} />
                    <Route path="/signin" element={<Signin />} />
                    <Route path="/blog/:id" element={<Blog />} />
                    <Route path="/blogs" element={<Blogs />} />
                    <Route path="/publish" element={<Publish />}></Route>
                    <Route path="" element={<Signin />}></Route>
                </Routes>
            </BrowserRouter>
        </>
    )
}

export default App


Step 11. For the next step we will be using tinymce library, and you will need a API key to use there text editor
you can get one for free using the following steps:

  1. go to https://www.tiny.cloud/
  2. Log in or Signup to your account
  3. Go To https://www.tiny.cloud/my-account/integrate/#react to get your API Key.

To start the frontend run the following command

npm run dev

Output:

screen-capture

Blogging website



Like Article
Suggest improvement
Share your thoughts in the comments

Similar Reads