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
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;
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;
The above is pretty easy, let's break it down
- 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. - In the
handleSubmit
function, we execute thee.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. - 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;
}
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;
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;
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;
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:
- User should see their posts and other posts by other users.
- Add a new post.
- 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;
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);
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;
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;
}
}
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({});
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;
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;
}
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.
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;
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.
- The root div of the component a function
setClickState
to close the modal whenever the user clicks outside the comment box(with ref ofcardRef
) or the in the black overlay. - 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.
- 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;
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>
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;
}
}
Now our code should be working perfectly. You can expand the currently feature and a like button and other stuff.
Top comments (0)