DEV Community

Sylvester Asare Sarpong
Sylvester Asare Sarpong

Posted on

Learn the MERN stack - by building an Instagram clone (Part Two)

In the last article we set up our backend, now we need a frontend need to connect to our backend.
It is the MERN stack so we will use React.
But before anything let's start our server with node server.js.

Open your terminal and run npm create-react-app <project-name-here> to create a new react project. Replace <project-name-here> with the name of your project.
After the react project is done installing, cd into the project directory, we will need to run npm i react-router-dom axios. We will use axios to make request to the backend and react-router-dom to allow route between different pages in our web app.
In our root project directory let's create .env and add the following

REACT_APP_BE=http://localhost:5000
Enter fullscreen mode Exit fullscreen mode

This adds the value of our backend server which we can access anywhere in our code with process.env.REACT_APP_BE.

Clean up

By default React comes with some boiler plate code in the App.js file, which we will not need so you can delete everything in between the brackets of the return method.

//App.js
import "./App.css";

function App() {
  return (
    <div></div>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

Our App.js should look like this.
Now we have to set up the routes to the pages for logging and registering an account, but we have not any pages to point them to so let's go and create them.
In the src folder, create a new folder called pages.

Login Page

In the pages folder create a new folder and name it Login. In the Login folder, create two files, Login.js and styles.css.

//Login.js
import React, { useState } from "react";
import axios from "axios";
import "./styles.css";
import { useNavigate } from "react-router-dom";
const Login = () => {
  const navigate = useNavigate();

  const [username, setUsername] = useState("");
  const [password, setPassword] = useState("");
  const handleSubmit = (e) => {
    e.preventDefault();
    var data = JSON.stringify({
      username,
      password
    });

    var config = {
      method: "post",
      url: `${process.env.REACT_APP_BE}/users/login`,
      headers: {
        Accept: "application/json",
        "Content-Type": "application/json"
      },
      data: data
    };

    axios(config)
      .then(function (response) {
        localStorage.setItem("my_user_token", response.data.token);
        navigate("/home");
      })
      .catch(function (error) {
        console.log(error);
      });
  };
  return (
    <div className="login-wrapper">
      <form onSubmit={(e) => handleSubmit(e)}>
        <input
          onChange={(e) => setUsername(e.target.value)}
          placeholder="username"
          type="text"
        />
        <br />
        <input
          onChange={(e) => setPassword(e.target.value)}
          placeholder="password"
          type="password"
        />
        <br />
        <button>login</button>
        <a  href="/register" className="create-account">create an account</a>
      </form>
    </div>
  );
};

export default Login;

Enter fullscreen mode Exit fullscreen mode

The above is pretty easy, let's break it down

  1. We create a functional component for the Login page, with the input fields and button to login. There is also a link a register page which we will create later. We use the onChange event to listen for changes in the input fields and update the states.
  2. In the handleSubmit function, we execute the e.preventDefault() to prevent the form from being submitted. Then we store the values of the username and password and add them to the config object which contain other information like the type of request we will like to make, the url we are making the request to and the headers required.
  3. With the config object encapsulating all the data we need, we can make our post request. This the request is successful and we are able to login, we get back the user data along with the generate token which we will store in the brower's localStorage for use later.
/*styles.css*/
.login-wrapper{
    width: 100vw;
    height: 100vh;
    background-color: #222;
    display: flex;
    flex-direction: row;
    align-items: center;
    justify-content: center;
}



input{
    outline: none;
    margin-bottom: 10px;
    width: 300px;
    padding: 10px;
    font-size: 16px;
    font-family: 'Courgette', cursive;
    font-family: 'Raleway', sans-serif;
    border-radius: 5px;
    border: 0;
}

button{
    background: rgb(77, 247, 43);
    border: 0;
    width: 100%;
    padding: 10px;
    border-radius: 5px;
    outline: none;
    margin: auto;
    font-family: 'Courgette', cursive;
    font-family: 'Raleway', sans-serif;
    font-weight: bold;
    cursor: pointer;
    margin-bottom: 10px;
}

.create-account{
    color: white;
    text-decoration: none;
    float: right;
}

.create-account:hover{
text-decoration: underline;
}
Enter fullscreen mode Exit fullscreen mode

Styles for the Login.js.

Sign up page

//SignUp.js
import React, {useState} from "react";
import axios from "axios";
import { useNavigate } from "react-router-dom";
const SignUp = () => {

  const navigate = useNavigate();

  const [username, setUsername] = useState("");
  const [password, setPassword] = useState("");
  const handleSubmit = (e) => {
    e.preventDefault();
    var data = JSON.stringify({
      username,
      password
    });

    var config = {
      method: "post",
      url: `${process.env.REACT_APP_BE}/users/register`,
      headers: {
        Accept: "application/json",
        "Content-Type": "application/json"
      },
      data: data
    };

    axios(config)
      .then(function (response) {
        localStorage.setItem("my_user_token", response.data.token);
        navigate("/home");
      })
      .catch(function (error) {
        console.log(error);
      });
  };
  return (
    <div className="login-wrapper">
      <form onSubmit={(e) => handleSubmit(e)}>
        <input
          onChange={(e) => setUsername(e.target.value)}
          placeholder="username"
          type="text"
        />
        <br />
        <input
          onChange={(e) => setPassword(e.target.value)}
          placeholder="password"
          type="password"
        />
        <br />
        <button>register</button>
        <a href="/" className="create-account">
          already have an account
        </a>
      </form>
    </div>
  );
};

export default SignUp;
Enter fullscreen mode Exit fullscreen mode

In the pages folder, create a SignUp folder and create a SignUp.js file. The logic here is the same as the one in the Login.js file, where we get the username and password and make a post request to the /register route and navigate to the Home page if the credentials are valid.

Defining the Home page and defining the routes for all the pages

Now that we have designed pages for users to login and register, we need to set up a Home page to route users to after a successful request. In the pages folder create a Home folder and create a Home.js and styles.css file.

//Home.js
import React from "react";
import "./styles.css";

const Home = () => {

  return (
    <div>Home</div>
  );
};

export default Home;
Enter fullscreen mode Exit fullscreen mode

The code above creates a default Home.js file. We will come back to the Home file later, now let's go back to our App.js file and define the routes for the individual pages.

import { BrowserRouter, Route, Routes } from "react-router-dom";
import "./App.css";
import Home from "./pages/Home/Home";
import Login from "./pages/Login/Login";
import SignUp from "./pages/SignUp/SignUp";

function App() {
  return (
    <BrowserRouter>
      <Routes>
        <Route exact path="/"  element={<Login/>} />
        <Route exact path="/register" element={<SignUp/>} />
        <Route exact path="/home" element={<Home/>} />
      </Routes>
    </BrowserRouter>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

We import the BrowserRouter, Route and Routes from react-router-dom and also import the individual files themselves. Now we specify the our initial route should be a the Login page, /register for the SignUp and /home for Home.

Back to the Home page

Before we continue with the Home page, we need to know how our Home is going to look like. On the Home page:

  1. User should see their posts and other posts by other users.
  2. Add a new post.
  3. Also add comment on posts.

When the user gets routed to the Home page, they will see all the posts added by others, so we need to make a request to the /posts to get all post in the database.

//Home.js
import React, { useEffect, useState } from "react";
import axios from "axios";
import "./styles.css";

const Home = () => {
  const [posts, setPosts] = useState([]);
  const [userName, setUsername] = useState("");


  useEffect(() => {
    var token = localStorage.getItem("my_user_token");
    var base64Url = token.split(".")[1];
    var base64 = base64Url.replace("-", "+").replace("_", "/");
    setUsername(JSON.parse(atob(base64)).username);
    var config = {
      method: "get",
      url: `${process.env.REACT_APP_BE}/posts`,
      headers: {
        Authorization: `Bearer ${localStorage.getItem("my_user_token")}`
      }
    };

    axios(config)
      .then(function (response) {
        setPosts(response.data);
      })
      .catch(function (error) {
        navigate("/");
        console.log(error);
      });
  }, []);

  return (
    <div>Home</div>
  );
};

export default Home;
Enter fullscreen mode Exit fullscreen mode

In the above code, when the user gets routed to we make a request to the /posts routes to get the all the posts in the database. With posts we got from the database we can map from the posts and pass each one as props to our PostCard component. Remember when we sign the username and id when creating the token, now we use

    var base64 = base64Url.replace("-", "+").replace("_", "/");
    setUsername(JSON.parse(atob(base64)).username);
Enter fullscreen mode Exit fullscreen mode

to decode the token payload and set the username state to the extracted username.

All these will be the Home page, so will need component to manage each of these action want the user to perform. A PostCard component to display user posts, a Comments component view to view comments on posts and also add a new comment. One last component to help us add a new post, AddPost.

PostCard component

We will first start with the PostCard component.
In the src folder, create a components folder and create a new PostCard folder. In the PostCard folder, create two files, one for PostCard.js and the other styles.css.

//PostCard.js
import React from "react";
import profile from "../../assets/images/profile.jpg";


import "./styles.css";
const PostCard = ({ post }) => {


  return (
    <div className="post-card">
      <div className="post-card-header">
        <img src={profile} className="avatar" />
        {post.username}
      </div>
      <img src={post.image} alt={post.caption} className="post-image" />
      <div className="post-card-body">
        <span className="username">{post.username} </span>
        <span className="post-caption">{post.caption}</span>
        {post.comments.length > 0 ? (
          <p className="view-comments">
            View all comments
          </p>
        ) : (
          <p className="view-comments">
            No comments yet
          </p>
        )}
      </div>
    </div>
  );
};
export default PostCard;
Enter fullscreen mode Exit fullscreen mode

Later when pass our post in the PostCard component, we can get the username, caption and image associated with that post. We also import a default profile image to use as the avatar for each user.
Below is the styles for the post card.

/*styles.css*/
.wrapper {
  max-width: 900px;
  margin: auto;
  display: grid;
  grid-template-columns: 550px auto;
  border: 1px solid #f3f3f3;
}

.left-side {
  margin-top: 40px;
}

.right-side {
  margin-top: 40px;
}

.header {
  width: 100%;
  background-color: rgb(0, 255, 187);
  height: 40px;
  position: fixed;
}

.user-profile-wrapper {
  background-color: white;
  padding: 15px;
  font-weight: bold;
  margin-top: 20%;
  display: flex;
  align-items: center;
  justify-content: space-between;
}
.user-profile {
  display: flex;
  align-items: center;
  justify-content: center;
}
.logout {
  cursor: pointer;
  color: rgb(0, 57, 172);
}

.avatar-lg {
  width: 50px;
  height: 50px;
  border-radius: 50%;
  margin-right: 10px;
}

.inner-header {
  max-width: 900px;
  margin: auto;
  display: grid;
  grid-template-columns: 550px;
  text-align: right;
}

@media only screen and (max-width: 768px) {
  .wrapper {
    grid-template-columns: auto;
  }

  .user-profile-wrapper {
    border: 1px solid #f0ebeb;
    padding: 5px;
    flex-grow: 1;
  }
  .right-side {
    position: fixed;
    left: 0;
    bottom: 0;
    width: 100%;
  }

  .avatar-lg {
    width: 30px;
    height: 30px;
  }
  .header {
    display: flex;
    justify-content: flex-end;
  }
  .inner-header {
    padding-right: 10px;
  }
}

Enter fullscreen mode Exit fullscreen mode

Now that we have PostCard ready we can loop through our array of posts returned from the backend and pass them as props into the PostCard.
Before our state gets complicated let create a context file to make a state management easy.
In the components folder lets create a showContext.js file

//showContext.js
import React from "react";
export const ShowContext = React.createContext({});
Enter fullscreen mode Exit fullscreen mode

The context will help manage our state and make them accessible to all children in the context provider.

//Home.js
import axios from "axios";
import React, { useEffect, useState } from "react";
import Comments from "../../components/Comments/Comments";
import PostCard from "../../components/PostCard/PostCard";
import { ShowContext } from "../../components/showContext";
import "./styles.css";
import { useNavigate } from "react-router-dom";
import AddPost from "../../components/AddPost/AddPost";
const Home = () => {
  const navigate = useNavigate();
  const [showComments, toggleComments] = useState(false);
  const [showAddPosts, toggleAddPost] = useState(false);
  const [posts, setPosts] = useState([]);
  const [userName, setUsername] = useState("");
  useEffect(() => {
    var token = localStorage.getItem("my_user_token");
    var base64Url = token.split(".")[1];
    var base64 = base64Url.replace("-", "+").replace("_", "/");
    setUsername(JSON.parse(atob(base64)).username);
    var config = {
      method: "get",
      url: `${process.env.REACT_APP_BE}/posts`,
      headers: {
        Authorization: `Bearer ${localStorage.getItem("my_user_token")}`
      }
    };

    axios(config)
      .then(function (response) {
        setPosts(response.data);
      })
      .catch(function (error) {
        navigate("/");
        console.log(error);
      });
  }, [showAddPosts]);

  const handleLogout = () => {
    localStorage.removeItem("my_user_token");
    navigate("/");
  };
  return (
<ShowContext.Provider
 value={{
        comments: [showComments, toggleComments],
        add: [showAddPosts, toggleAddPost]
      }}
>
        <div className="wrapper">
          <div className="left-side">
            {posts.map((ele, i) => {
              return <PostCard post={ele} key={i} />;
            })}
          </div>
        </div>
    </ShowContext.Provider>
  );
};

export default Home;
Enter fullscreen mode Exit fullscreen mode

Since we going to have a component to make comments and another to add posts, we create the context to pass values from the top down the individual components.
We wrap the return jsx with a context provider and pass in the values of comments and add. Comments

/*styles.css*/
.wrapper {
  max-width: 900px;
  margin: auto;
  display: grid;
  grid-template-columns: 550px auto;
  border: 1px solid #f3f3f3;
}

.left-side {
  margin-top: 40px;
}

Enter fullscreen mode Exit fullscreen mode

Now our Home.js should look like this. We loop through the posts state and we have also some few classes to style the page.
example of post card
Our home page should look something like this if have posts in your database.

Right now we can only see the posts we made in the previous tutorial, but before we user provide users with a button to add new posts. Let's make sure they can add comments on existing posts.

Commenting on posts

//Comments.js
import React, { useContext, useRef, useEffect, useState } from "react";
import "./styles.css";
import profile from "../../assets/images/profile.jpg";
import { ShowContext } from "../showContext";
import UserComment from "./UserComment";
import axios from "axios";
const Comments = () => {
  const { comments } = useContext(ShowContext);
  const [showComments, toggleComments] = comments
  const [clickState, setClickState] = useState(false);
  const [content, setContent] = useState("");
  const cardRef = useRef();
  console.log(showComments);
  useEffect(() => {
    function handleClickOutside(event) {
      if (cardRef.current && !cardRef.current.contains(event.target)) {
        toggleComments({
          status: false,
          post: null
        });
      }
    }

    document.addEventListener("mousedown", handleClickOutside);
    return () => {
      document.removeEventListener("mousedown", handleClickOutside);
    };
  }, [clickState, toggleComments]);

  const handleSubmit = (e) => {
    e.preventDefault();
    var postId = showComments.post._id;
    var token = localStorage.getItem("my_user_token");
    var base64Url = token.split(".")[1];
    var base64 = base64Url.replace("-", "+").replace("_", "/");
    var userId = JSON.parse(atob(base64)).id;

    var data = JSON.stringify({
      content
    });
    var config = {
      method: "post",
      url: `${process.env.REACT_APP_BE}/posts/add-comment/${postId}/${userId}`,
      headers: {
        Accept: "application/json",
        "Content-Type": "application/json",
        Authorization: `Bearer ${localStorage.getItem("my_user_token")}`
      },
      data: data
    };

    axios(config)
      .then(function (response) {
        console.log(JSON.stringify(response.data));
      })
      .catch(function (error) {
        console.log(error);
      });
  };

  return (
    <div onClick={() => setClickState(!clickState)} className="comments-modal">
      <div ref={cardRef} className="comment-card">
        <div
          className="comment-img"
          style={{
            background: `url(${showComments.post.image})`,
            backgroundRepeat: "no-repeat",
            backgroundPosition: "center",
            backgroundSize: "cover"
          }}
        ></div>
        <div className="comments-main">
          <div className="post-card-header">
            <img src={profile} className="avatar" />
            {showComments.post.username}
          </div>
          {showComments.post.comments.map((ele, i) => {
            return <UserComment key={i} item={ele} />;
          })}
          <form onSubmit={(e) => handleSubmit(e)} className="form">
            <input
              onChange={(e) => setContent(e.target.value)}
              placeholder="say something..."
              className="form-input"
              type="text"
            />
          </form>
        </div>
      </div>
    </div>
  );
};
export default Comments;
Enter fullscreen mode Exit fullscreen mode

Let break the code.
Our comment component is going to be modal with black overlay, that has consist of a grid with the image of the post we are commenting on the left and the other comments on the right.

  1. The root div of the component a function setClickState to close the modal whenever the user clicks outside the comment box(with ref of cardRef) or the in the black overlay.
  2. The div is a grid container of the post image and the comments section with the list all comments. so now need to allow users to able to add new posts.
  3. When we submit our comment to hit the /post/add-comment endpoint passing in the id of the post and the id of the user.

Add new posts

In the header div in on our Home page, we will add a button to add a new button.

//AddPost.js
import React, { useRef, useEffect, useState, useContext } from "react";
import axios from "axios";
import FileBase64 from "react-file-base64";
import "./styles.css";
import { ShowContext } from "../showContext";
const AddPost = () => {
  const cardRef = useRef();

  const { add } = useContext(ShowContext);
  const [showAddPosts, toggleAddPost] = add;
  const [clickState, setClickState] = useState(false);
  const [picture, setPicture] = useState(null);
  const [caption, setCaption] = useState("");
  const [showError, setShowError] = useState(false);
  useEffect(
    () => {
      function handleClickOutside(event) {
        if (cardRef.current && !cardRef.current.contains(event.target)) {
          toggleAddPost(!showAddPosts)
        }
      }
      document.addEventListener("mousedown", handleClickOutside);
      return () => {
        document.removeEventListener("mousedown", handleClickOutside);
      };
    },
    [clickState]
  );
  function getFile(file) {
    var exp = /\d+/;
    if (file.size.match(exp)[0] > 100) {
      setShowError(true);
    } else {
      setShowError(false);
      setPicture(file);
    }
  }

  const handleSubmit = e => {
    e.preventDefault();
    var token = localStorage.getItem("my_user_token");
    var base64Url = token.split(".")[1];
    var base64 = base64Url.replace("-", "+").replace("_", "/");
    var userId = JSON.parse(atob(base64)).id;

    var data = JSON.stringify({
      caption,
      image: picture.base64
    });

    var config = {
      method: "post",
      url: `${process.env.REACT_APP_BE}/posts/add/${userId}`,
      headers: {
        Accept: "application/json",
        "Content-Type": "application/json",
        Authorization: `Bearer ${localStorage.getItem("my_user_token")}`
      },
      data: data
    };

    axios(config)
      .then(function(response) {
            toggleAddPost(!showAddPosts);

      })
      .catch(function(error) {
        console.log(error);
      });
  };
  return (
    <div onClick={() => setClickState(!clickState)} className="comments-modal">
      <div ref={cardRef} className="comment-card">
        <div
          className="comment-img add-post"
          style={{
            backgroundRepeat: "no-repeat",
            backgroundSize: "contain",
            backgroundPosition: "center",
            background: picture ? `url(${picture.base64})` : null
          }}
        >
          {showError && <p className="error">File must be less 100kb</p>}
          {!picture
            ? <FileBase64 onDone={getFile} />
            : <span onClick={() => setPicture(null)} className="remove-button">
                x
              </span>}
        </div>

        <div className="comments-main">
          <form onSubmit={e => handleSubmit(e)} className="form">
            <input
              onChange={e => setCaption(e.target.value)}
              placeholder="say something..."
              className="form-input"
              type="text"
            />
          </form>
        </div>
      </div>
    </div>
  );
};

export default AddPost;
Enter fullscreen mode Exit fullscreen mode

The AddPost component looks like Comments modal with a grid layout for one the image and one for the comments.
The user click on the choose file button to add an image and after they can type in the input field to caption the post.
On submit we hit /posts/add/ endpoint and also check the file size if below 100kb.

Now we can add new post and also make comments on existing posts.

Finishing up

<ShowContext.Provider
      value={{
        comments: [showComments, toggleComments],
        add: [showAddPosts, toggleAddPost]
      }}
    >
      <div>
        {showComments.status ? <Comments /> : null}
        {showAddPosts ? <AddPost /> : null}
        <div className="header">
          <div onClick={() => toggleAddPost(true)} className="inner-header">
            new post
          </div>
        </div>
        <div className="wrapper">
          <div className="left-side">
            {posts.map((ele, i) => {
              return <PostCard post={ele} key={i} />;
            })}
          </div>
          <div className="right-side">
            <div className="user-profile-wrapper">
              <div className="user-profile">
                {userName}
              </div>
              <span onClick={handleLogout} className="logout">
                logout
              </span>
            </div>
          </div>
        </div>
      </div>
    </ShowContext.Provider>
Enter fullscreen mode Exit fullscreen mode

Because the Comment and AddPost components are modal we can display all at once on the page. We only need to appear when a condition is met. So the Comments will show when the showComments.status is true and AddPost will show when the showAddPosts is true.
We also need a logout button.

Update the Home styles.css

.wrapper {
  max-width: 900px;
  margin: auto;
  display: grid;
  grid-template-columns: 550px auto;
  border: 1px solid #f3f3f3;
}

.left-side {
  margin-top: 40px;
}

.right-side {
  margin-top: 40px;
}

.header {
  width: 100%;
  background-color: rgb(0, 255, 187);
  height: 40px;
  position: fixed;
}

.user-profile-wrapper {
  background-color: white;
  padding: 15px;
  font-weight: bold;
  margin-top: 20%;
  display: flex;
  align-items: center;
  justify-content: space-between;
}
.user-profile {
  display: flex;
  align-items: center;
  justify-content: center;
}
.logout {
  cursor: pointer;
  color: rgb(0, 57, 172);
}

.avatar-lg {
  width: 50px;
  height: 50px;
  border-radius: 50%;
  margin-right: 10px;
}

.inner-header {
  max-width: 900px;
  margin: auto;
  display: grid;
  grid-template-columns: 550px;
  text-align: right;
  cursor: pointer;
}

@media only screen and (max-width: 768px) {
  .wrapper {
    grid-template-columns: auto;
  }

  .user-profile-wrapper {
    border: 1px solid #f0ebeb;
    padding: 5px;
    flex-grow: 1;
  }
  .right-side {
    position: fixed;
    left: 0;
    bottom: 0;
    width: 100%;
  }

  .avatar-lg {
    width: 30px;
    height: 30px;
  }
  .header {
    display: flex;
    justify-content: flex-end;
  }
  .inner-header {
    padding-right: 10px;
  }
}
Enter fullscreen mode Exit fullscreen mode

Now our code should be working perfectly. You can expand the currently feature and a like button and other stuff.

Check the out the full code here.
Check the demo here.

Top comments (0)